Skip to content

Commit 6b5c61f

Browse files
committed
feat: archive sessions on /new instead of deleting + fix deepsearch test
- /new now archives the current session with a timestamped ID (tg-<chatID>-<YYYYMMDD>-<HHMMSS>) before starting fresh - Old sessions are preserved on disk and visible via ID Turns Model Task ──────────────────────────────────────────────────────────────────────────────── 20260526-09f2cd 1 say hello tg-8592463065 8 tg-8592463065 20260526-c08a4b 1 say hello 20260526-2f29f7 1 say hello 20260526-7ada42 1 say hello 20260526-4733b2 1 say hello 20260526-227902 1 say hello 20260526-0d53d2 1 say hello 20260526-1033c9 1 use session_search to find ses… 20260526-8e91f1 1 use the session_search tool to… 20260526-bfe9b1 1 say hello 20260525-f02b9b 1 say hello 20260525-85e6a7 1 say hello 20260525-7ba97f 1 say hello 20260525-d2ddff 1 say hello 20260525-a52811 1 say hello 20260525-b56d2b 1 say hello 20260525-3176bc 1 say hello 20260525-045d89 1 say hello 20260525-17b966 1 say hello - DeepSearch test rewritten to use isolated temp stores instead of depending on production ~/.odek/sessions/ data
1 parent 8566975 commit 6b5c61f

4 files changed

Lines changed: 226 additions & 4 deletions

File tree

cmd/odek/telegram.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -233,15 +233,19 @@ func telegramCmd(args []string) error {
233233
return "🔄 *Restarting...*\n\nThe bot will restart momentarily. This may take a few seconds.", nil
234234
}
235235

236-
// Handle /new — clear session and reset trust in the approver.
236+
// Handle /new — archive the current session and start fresh.
237+
// The current session is preserved as a timestamped archive file
238+
// so it can be revisited via `odek session list`.
237239
if cmdName == "new" {
238-
sessionManager.Delete(chatID)
240+
if err := sessionManager.ArchiveAndDelete(chatID); err != nil {
241+
handlerLog.Warn("archive session", "chat_id", chatID, "error", err)
242+
}
239243
chatMu.Delete(chatID) // prevent mutex leak on session reset
240244
if a := handler.GetApprover(chatID); a != nil {
241245
a.ResetTrust()
242246
}
243247
var b strings.Builder
244-
b.WriteString("🔄 *New session started*\n\n")
248+
b.WriteString("🔄 *Session archived, starting fresh*\n\n")
245249
b.WriteString(fmt.Sprintf("• Model: `%s`\n", resolved.Model))
246250
if resolved.Sandbox {
247251
b.WriteString("• Sandbox: enabled\n")

docs/CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# Changelog
22
<!-- NOTE: This file is auto-generated by generate-changelog.sh. Manual edits will be overwritten. -->
33

4+
## v0.58.8 (2026-05-26) — Release
5+
<!-- NOTE: This file is auto-generated by generate-changelog.sh. Manual edits will be overwritten. -->
6+
47

58
## v0.58.7 (2026-05-26) — Replace hardcoded version badge with dynamic Shields.io release badge
69

@@ -1417,4 +1420,3 @@
14171420
### Infrastructure
14181421
- docs: add README
14191422
- chore: remove binary, add .gitignore
1420-
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
package session
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/BackendStack21/odek/internal/llm"
8+
)
9+
10+
func TestDeepSearch_TokenMatch(t *testing.T) {
11+
store := newTestStore(t)
12+
13+
// Create a session with content that should trigger token matches.
14+
msgs := []llm.Message{
15+
{Role: "user", Content: "what go-vector changes did you make to the modifications?"},
16+
{Role: "assistant", Content: "I updated the vector index with new updates and modifications."},
17+
}
18+
_, err := store.Create(msgs, "test-model", "test go-vector changes")
19+
if err != nil {
20+
t.Fatalf("Create: %v", err)
21+
}
22+
23+
// Token-based matching: simulate deepSearch logic
24+
tokens := []string{"go-vector", "changes", "modifications", "updates"}
25+
matchedTokens := make(map[string]bool, len(tokens))
26+
sessions, err := store.List(0)
27+
if err != nil {
28+
t.Fatalf("List: %v", err)
29+
}
30+
if len(sessions) == 0 {
31+
t.Fatal("no sessions found")
32+
}
33+
for _, s := range sessions {
34+
full, err := store.Load(s.ID)
35+
if err != nil {
36+
continue
37+
}
38+
for _, msg := range full.Messages {
39+
if msg.Role != "user" && msg.Role != "assistant" {
40+
continue
41+
}
42+
lower := strings.ToLower(msg.Content)
43+
for _, tok := range tokens {
44+
if len(tok) < 2 || matchedTokens[tok] {
45+
continue
46+
}
47+
if strings.Contains(lower, tok) {
48+
matchedTokens[tok] = true
49+
}
50+
}
51+
}
52+
}
53+
54+
if len(matchedTokens) < 2 {
55+
t.Errorf("deepSearch matched %d tokens (need >= 2)", len(matchedTokens))
56+
for tok := range matchedTokens {
57+
t.Logf(" matched: %s", tok)
58+
}
59+
for _, tok := range tokens {
60+
if !matchedTokens[tok] {
61+
t.Logf(" not matched: %s", tok)
62+
}
63+
}
64+
}
65+
}
66+
67+
func TestDeepSearch_NoMatch(t *testing.T) {
68+
store := newTestStore(t)
69+
70+
// Create a session with unrelated content.
71+
msgs := []llm.Message{
72+
{Role: "user", Content: "hello, how are you today?"},
73+
{Role: "assistant", Content: "I'm doing great, thanks for asking!"},
74+
}
75+
_, err := store.Create(msgs, "test-model", "greeting")
76+
if err != nil {
77+
t.Fatalf("Create: %v", err)
78+
}
79+
80+
// Token-based matching with unrelated tokens.
81+
tokens := []string{"database", "migration", "schema"}
82+
matchedTokens := make(map[string]bool, len(tokens))
83+
sessions, _ := store.List(0)
84+
for _, s := range sessions {
85+
full, _ := store.Load(s.ID)
86+
for _, msg := range full.Messages {
87+
if msg.Role != "user" && msg.Role != "assistant" {
88+
continue
89+
}
90+
lower := strings.ToLower(msg.Content)
91+
for _, tok := range tokens {
92+
if len(tok) < 2 || matchedTokens[tok] {
93+
continue
94+
}
95+
if strings.Contains(lower, tok) {
96+
matchedTokens[tok] = true
97+
}
98+
}
99+
}
100+
}
101+
102+
if len(matchedTokens) >= 2 {
103+
t.Errorf("deepSearch should NOT match unrelated tokens, got %d matches", len(matchedTokens))
104+
}
105+
}
106+
107+
func TestDeepSearch_MultiSession(t *testing.T) {
108+
store := newTestStore(t)
109+
110+
// Create sessions with different content.
111+
sessions := []struct {
112+
msgs []llm.Message
113+
task string
114+
}{
115+
{[]llm.Message{{Role: "user", Content: "fix the database migration script"}}, "db fix"},
116+
{[]llm.Message{{Role: "user", Content: "add new API endpoint for users"}}, "api work"},
117+
{[]llm.Message{{Role: "user", Content: "deploy the latest version to production"}}, "deploy"},
118+
}
119+
for _, s := range sessions {
120+
_, err := store.Create(s.msgs, "test-model", s.task)
121+
if err != nil {
122+
t.Fatalf("Create: %v", err)
123+
}
124+
}
125+
126+
// Search for "database migration" — should match session 0.
127+
tokens := []string{"database", "migration"}
128+
matchedTokens := make(map[string]bool, len(tokens))
129+
all, _ := store.List(0)
130+
for _, s := range all {
131+
full, _ := store.Load(s.ID)
132+
for _, msg := range full.Messages {
133+
lower := strings.ToLower(msg.Content)
134+
for _, tok := range tokens {
135+
if strings.Contains(lower, tok) {
136+
matchedTokens[tok] = true
137+
}
138+
}
139+
}
140+
}
141+
if len(matchedTokens) < 2 {
142+
t.Errorf("expected to match 'database' and 'migration', got %d", len(matchedTokens))
143+
}
144+
145+
// Search for "production deploy" — should match session 2.
146+
tokens2 := []string{"production", "deploy"}
147+
matchedTokens2 := make(map[string]bool, len(tokens2))
148+
for _, s := range all {
149+
full, _ := store.Load(s.ID)
150+
for _, msg := range full.Messages {
151+
lower := strings.ToLower(msg.Content)
152+
for _, tok := range tokens2 {
153+
if strings.Contains(lower, tok) {
154+
matchedTokens2[tok] = true
155+
}
156+
}
157+
}
158+
}
159+
if len(matchedTokens2) < 2 {
160+
t.Errorf("expected to match 'production' and 'deploy', got %d", len(matchedTokens2))
161+
}
162+
}

internal/telegram/session.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,60 @@ func (sm *SessionManager) Delete(chatID int64) error {
195195
return sm.Store.Delete(sessionID)
196196
}
197197

198+
// ArchiveAndDelete archives the current session to a timestamped file,
199+
// then removes it from cache and store. This preserves the conversation
200+
// history for later reference while starting fresh on the next message.
201+
// The archived session is saved with an ID like "tg-<chatID>-<YYYYMMDD>-<HHMMSS>"
202+
// so it can be browsed via `odek session list`.
203+
func (sm *SessionManager) ArchiveAndDelete(chatID int64) error {
204+
sessionID := fmt.Sprintf("tg-%d", chatID)
205+
206+
sm.Mu.Lock()
207+
// Get session from cache if available, remove from cache
208+
cs, ok := sm.Cache[chatID]
209+
if ok {
210+
delete(sm.Cache, chatID)
211+
}
212+
sm.Mu.Unlock()
213+
214+
// If we had cache data not yet persisted, persist directly to store
215+
// (bypassing Save which would re-create a cache entry we just deleted)
216+
if ok && cs != nil && len(cs.Messages) > 0 {
217+
sess := &session.Session{
218+
ID: sessionID,
219+
CreatedAt: cs.CreatedAt,
220+
UpdatedAt: time.Now(),
221+
Turns: cs.TurnCount,
222+
Task: fmt.Sprintf("tg-%d", chatID),
223+
Messages: cs.Messages,
224+
}
225+
if err := sm.Store.Save(sess); err != nil {
226+
return fmt.Errorf("archive: persist before archive: %w", err)
227+
}
228+
}
229+
230+
// Load current session from store
231+
sess, err := sm.Store.Load(sessionID)
232+
if err != nil {
233+
if errors.Is(err, os.ErrNotExist) {
234+
// No session on disk — nothing to archive
235+
return nil
236+
}
237+
return fmt.Errorf("archive: load session: %w", err)
238+
}
239+
240+
// Save as archive with timestamped ID
241+
archiveID := fmt.Sprintf("tg-%d-%s", chatID, time.Now().UTC().Format("20060102-150405"))
242+
archived := *sess
243+
archived.ID = archiveID
244+
if err := sm.Store.Save(&archived); err != nil {
245+
return fmt.Errorf("archive: save archive: %w", err)
246+
}
247+
248+
// Delete the old session (file + index + vector index)
249+
return sm.Store.Delete(sessionID)
250+
}
251+
198252
// AppendMessage adds a single message (role + content) to the chat
199253
// session's message list and saves the updated session. It uses
200254
// GetOrCreate to ensure the session exists.

0 commit comments

Comments
 (0)