Skip to content

Commit 53481cb

Browse files
feat: add cross-session recall functionality and auto-generated session titles
- Implemented `/recall <query>` command to search across all saved sessions, with options for summarizing results and resuming the top match. - Enhanced session management by introducing AI-generated titles for sessions, improving readability in session lists and recall results. - Updated configuration to allow users to enable or disable auto-session title generation. - Added utility functions for managing personal configurations, including syncing personalities and custom commands across devices. - Improved documentation to reflect new features and usage instructions.
1 parent 6124f6b commit 53481cb

16 files changed

Lines changed: 790 additions & 21 deletions

File tree

CHANGELOG.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,82 @@ For releases before v1.3.35, see [GitHub Releases](https://github.com/VladoIvank
1111
> as the social-share summary (IFTTT → X/Bluesky), capped at 220 chars.
1212
> If omitted, the feed falls back to the first paragraph.
1313
14+
## [2.1.0] — 2026-05-19
15+
16+
> Session memory: `/recall <query>` searches across **all** your saved sessions, `--resume` jumps straight back into the best match, `--summarize` asks the LLM what you accomplished, and sessions now get readable AI-generated titles instead of truncated first messages.
17+
18+
### Added — `/recall` cross-session search
19+
20+
- **`/recall <query>`** scans every saved session in the active scope
21+
(project `.codeep/sessions/` when in a project, else global
22+
`~/.codeep/sessions/`), matches with AND semantics (every query term
23+
must appear), and ranks results by term-hit count plus a recency
24+
boost. Each result shows a context snippet and the session name.
25+
- **`/recall <query> --resume`** loads the top-matching session
26+
directly into the current conversation — skips the list + `/sessions`
27+
picker dance. (TUI only; ACP shows results since it can't swap the
28+
client's conversation in place.)
29+
- **`/recall <query> --summarize`** reads the matching sessions and
30+
returns a short LLM recap of what you actually accomplished across
31+
them — "ask your history a question". Works in TUI + ACP.
32+
- No new dependency: in-memory JSON scan, fast for the realistic
33+
tens-to-hundreds-of-sessions case.
34+
35+
### Added — portable personal config sync
36+
37+
- **Personalities and custom commands now sync across your machines**
38+
via `codeep account sync` (pull) and `codeep account push`. Global
39+
ones (`~/.codeep/personalities/*.md`, `~/.codeep/commands/*.md`)
40+
travel with your account alongside API keys and profiles — set up a
41+
`senior-reviewer` personality or a `/deploy` command once, get it
42+
everywhere. New endpoints `/api/personalities` + `/api/commands`,
43+
new DB tables `user_personalities` + `user_commands`.
44+
- **Additive merge, never destructive**: pull only writes files that
45+
don't already exist locally, so a sync can't clobber edits you
46+
haven't pushed. Last-write-wins on the server via upsert.
47+
- **Dashboard sections** to view + delete synced personalities and
48+
commands at codeep.dev/dashboard (read + prune; editing stays in the
49+
CLI).
50+
- **Deliberately not synced**: lifecycle hooks (arbitrary shell —
51+
syncing + auto-running on another machine is a security risk) and
52+
MCP server configs (contain tokens). Those stay local by design.
53+
54+
### Added — AI-generated session titles
55+
56+
- Sessions now get a concise LLM-generated title ("OAuth2 migration
57+
for auth module") instead of the first user message truncated to 60
58+
chars ("help me with the…"). Generated once per session in the
59+
background after it has ≥3 messages — fire-and-forget on autosave,
60+
never blocks a save, never regenerates once set. Makes both
61+
`/sessions` and `/recall` dramatically more readable.
62+
- Title priority: AI title > stored title > first-message fallback >
63+
session name. Stored under `aiTitle` in the session JSON.
64+
- **Opt-out: `autoSessionTitle` setting** (default on). This is the
65+
only feature that makes a background API call you didn't explicitly
66+
request, so it's toggleable in `/settings` for privacy/cost-conscious
67+
users. Off → sessions keep the first-message title, zero background
68+
calls.
69+
70+
### Changed
71+
72+
- **`/search` description clarified** to "search the current session"
73+
(vs `/recall` for cross-session) — the two were easy to confuse when
74+
both said "search history".
75+
76+
### Fixed
77+
78+
- **`/sessions` picker showed raw session ids** (`session-2026-05-20-757cbda5`)
79+
instead of readable titles. Now shows the title (AI-generated > stored
80+
> first-message) with a short date + message count, so the list is
81+
scannable.
82+
- **Models hallucinating their identity in chat mode.** Asked "which
83+
model are you", GLM (and others) would claim to be Claude because the
84+
chat system prompt never stated the actual identity. Both the chat
85+
and agent system prompts now inject the real `model` + `provider`
86+
from config, so the answer is truthful. (Agent mode already said
87+
"never call yourself Claude" but didn't state the real model; now it
88+
does.)
89+
1490
## [2.0.4] — 2026-05-19
1591

1692
> Discoverability patch: new `/docs <command>` jumps from any slash command to its full guide on codeep.dev, the `/help` footer now points at the same place, and `/personality` and `/insights` have proper docs pages instead of one-liners.

README.md

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,9 @@ When started in a project directory, Codeep automatically:
7373
- **Session picker** - Choose which session to continue on startup
7474
- **Per-project sessions** - Sessions stored in `.codeep/sessions/`
7575
- **Rename sessions** - Give meaningful names with `/rename`
76-
- **Search history** - Find past conversations with `/search`
76+
- **AI session titles** - Sessions auto-title themselves with an LLM one-liner ("OAuth2 migration for auth module") instead of a truncated first message. This makes one small background API call per session; turn it off with the `autoSessionTitle` setting (`/settings`) if you prefer zero unsolicited calls
77+
- **Search current session** - Find text in the open conversation with `/search`
78+
- **Cross-session recall** - `/recall <query>` searches across **all** saved sessions, ranked by relevance + recency. Add `--resume` to load the top match, or `--summarize` for an LLM recap of what you accomplished across matches
7779
- **Export** - Save to Markdown, JSON, or plain text
7880
- `/cost` - Per-session token usage and estimated cost (per provider/model)
7981
- `/compact [keepN]` - AI-summarize older messages to free up context (keeps last N, default 4)
@@ -709,19 +711,40 @@ codeep account # Opens browser → sign in with GitHub → CLI is linked
709711
- **Project archiving** — hide projects from the list with one click
710712
- **Tasks** — create/complete bug, feature, and task items from the web or directly from the CLI with `/tasks add` and `/tasks done`
711713
- **API key sync** — store provider keys securely on codeep.dev, sync to any machine in one command
714+
- **Personal config sync** — personalities and custom slash commands sync across machines; view and prune them on the dashboard
712715
- **Connected devices** — see all machines linked to your account (hostname, last seen), revoke access per device
713716

714717
### API key sync
715718

716719
Add keys once on the dashboard, then sync them to any machine:
717720

718721
```bash
719-
codeep account sync # Pull keys from codeep.dev → local config
720-
codeep account push # Push local keys → codeep.dev
722+
codeep account sync # Pull keys + config from codeep.dev → local
723+
codeep account push # Push local keys + config → codeep.dev
721724
```
722725

723726
Keys are encrypted at rest using AES-256-GCM.
724727

728+
### Personal config sync
729+
730+
`account sync` / `account push` also carry your **personalities** and **custom
731+
slash commands** between machines — the Markdown files in
732+
`~/.codeep/personalities/` and `~/.codeep/commands/`:
733+
734+
```bash
735+
codeep account push # Upload local personalities + commands to codeep.dev
736+
codeep account sync # Download them onto a new machine
737+
```
738+
739+
Merging is **additive**`sync` only writes files that don't already exist
740+
locally, so it never clobbers a personality or command you've edited on this
741+
machine. View what's synced (and prune stale entries) under **Personalities**
742+
and **Custom commands** on the [dashboard](https://codeep.dev/dashboard).
743+
744+
Hooks and MCP server configs are deliberately **not** synced: hooks run
745+
arbitrary shell, and MCP configs often embed tokens, so both stay local to each
746+
machine.
747+
725748
### Tasks
726749

727750
Create, view, and complete tasks directly from the CLI — or manage them on the codeep.dev dashboard:
@@ -973,8 +996,8 @@ In `dangerous` mode, configure which tools require confirmation via `/settings`:
973996
| Command | Description |
974997
|---------|-------------|
975998
| `codeep account` | Link CLI to codeep.dev (GitHub OAuth) |
976-
| `codeep account sync` | Pull API keys from codeep.dev → local config |
977-
| `codeep account push` | Push local API keys → codeep.dev |
999+
| `codeep account sync` | Pull API keys + personalities + commands from codeep.dev → local |
1000+
| `codeep account push` | Push local API keys + personalities + commands → codeep.dev |
9781001

9791002
### Authentication
9801003

src/acp/commands.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,31 @@ Anything else the agent should know — edge cases, gotchas, things to double-ch
680680

681681
// ─── Export ────────────────────────────────────────────────────────────────
682682

683+
// ─── Cross-session recall (2.1.0) ─────────────────────────────────────────
684+
685+
case 'recall': {
686+
const wantSummarize = args.includes('--summarize');
687+
// --resume is TUI-only (ACP can't swap the client's conversation
688+
// in place); ignore the flag here and just show results.
689+
const query = args.filter(a => a !== '--resume' && a !== '--summarize').join(' ');
690+
if (!query) {
691+
return { handled: true, response: 'Usage: `/recall <query> [--summarize]` — searches across all saved sessions (vs `/search`, current-session only).' };
692+
}
693+
const { recallSessions, formatRecall, summarizeRecall } = await import('../utils/recall.js');
694+
const matches = recallSessions(query, session.workspaceRoot);
695+
const header = formatRecall(query, matches);
696+
if (wantSummarize && matches.length > 0) {
697+
onChunk('_Summarizing matching sessions…_\n\n');
698+
const summary = await summarizeRecall(query, matches, session.workspaceRoot);
699+
return {
700+
handled: true,
701+
response: summary ? `${header}\n\n---\n\n### Summary\n\n${summary}` : header,
702+
streaming: true,
703+
};
704+
}
705+
return { handled: true, response: header };
706+
}
707+
683708
// ─── Personalities + insights (2.0.3) ─────────────────────────────────────
684709

685710
case 'personality': {

src/acp/server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ const AVAILABLE_COMMANDS = [
5757
// Sessions
5858
{ name: 'session', description: 'List sessions, or: new / load <name>', input: { hint: 'new | load <name>' } },
5959
{ name: 'save', description: 'Save current session', input: { hint: '[name]' } },
60+
{ name: 'recall', description: 'Search across ALL saved sessions (cross-session)', input: { hint: '<query> [--summarize]' } },
6061
// Context
6162
{ name: 'add', description: 'Add files to agent context', input: { hint: '<file> [file2…]' } },
6263
{ name: 'drop', description: 'Remove files from context (no args = clear all)', input: { hint: '[file…]' } },

src/api/index.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -271,15 +271,44 @@ const LANGUAGE_NAMES: Record<string, string> = {
271271
'hr': 'Croatian (Hrvatski)',
272272
};
273273

274+
/**
275+
* Build the agent's identity sentence from the active provider + model.
276+
* Codeep is the product; the model underneath varies. Stating it
277+
* explicitly stops models from guessing their own identity (GLM and
278+
* others often claim to be Claude because their training data includes
279+
* Claude transcripts).
280+
*/
281+
function buildIdentityLine(): string {
282+
const model = String(config.get('model') || '');
283+
const providerId = String(config.get('provider') || '');
284+
const known: Record<string, string> = {
285+
'z.ai': 'Z.AI', 'z.ai-api': 'Z.AI', 'z.ai-cn': 'Z.AI', 'z.ai-cn-api': 'Z.AI',
286+
openai: 'OpenAI', anthropic: 'Anthropic', deepseek: 'DeepSeek', google: 'Google',
287+
minimax: 'MiniMax', 'minimax-api': 'MiniMax', 'minimax-cn': 'MiniMax',
288+
openrouter: 'OpenRouter', ollama: 'Ollama (local)',
289+
};
290+
const providerName = known[providerId] || providerId || 'your configured provider';
291+
if (model) {
292+
return `You are Codeep, an AI coding assistant. You are running on the \`${model}\` model via ${providerName}. If asked which model or provider you are, answer truthfully with these details — do not claim to be a different model.`;
293+
}
294+
return `You are Codeep, an AI coding assistant.`;
295+
}
296+
274297
function getSystemPrompt(): string {
275298
const language = config.get('language');
276-
299+
300+
// Identity line so the model doesn't hallucinate its name when asked
301+
// "what model are you" — without it, models guess from their training
302+
// data (e.g. GLM claiming to be Claude). State the truth: the product
303+
// is Codeep, the underlying model + provider come from config.
304+
const identity = buildIdentityLine();
305+
277306
let basePrompt: string;
278307
if (language === 'auto') {
279-
basePrompt = `You are a helpful AI coding assistant. Always respond in the same language as the user's message. Detect the language of the user's input and reply in that same language.`;
308+
basePrompt = `${identity} Always respond in the same language as the user's message. Detect the language of the user's input and reply in that same language.`;
280309
} else {
281310
const langName = LANGUAGE_NAMES[language] || 'English';
282-
basePrompt = `You are a helpful AI coding assistant. Always respond in ${langName}, regardless of what language the user writes in.`;
311+
basePrompt = `${identity} Always respond in ${langName}, regardless of what language the user writes in.`;
283312
}
284313

285314
// Important: This is CHAT mode, not agent mode

src/config/index.ts

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import { createSecureStorage, type SecureStorage } from '../utils/keychain';
1010
interface Session {
1111
name: string;
1212
title?: string;
13+
/** LLM-generated one-liner (utils/sessionTitles.ts). Preferred over
14+
* `title` when present — see listSessionsWithInfo. */
15+
aiTitle?: string;
1316
history: Message[];
1417
createdAt: string;
1518
}
@@ -49,6 +52,10 @@ interface ConfigSchema {
4952
plan: 'lite' | 'pro' | 'max';
5053
language: LanguageCode;
5154
autoSave: boolean;
55+
/** Auto-generate an LLM one-liner title for sessions on save. Makes a
56+
* small background API call (uses the active model) once per session.
57+
* Default true; set false to avoid any unsolicited API calls. */
58+
autoSessionTitle: boolean;
5259
currentSessionId: string;
5360
temperature: number;
5461
maxTokens: number;
@@ -267,6 +274,7 @@ function createConfig(): Conf<ConfigSchema> {
267274
plan: 'lite',
268275
language: 'en',
269276
autoSave: true,
277+
autoSessionTitle: true,
270278
currentSessionId: '',
271279
temperature: 0.7,
272280
maxTokens: 32768,
@@ -763,23 +771,84 @@ export function saveSession(name: string, history: Message[], projectPath?: stri
763771
const title = firstUserMsg
764772
? firstUserMsg.content.replace(/\n/g, ' ').trim().slice(0, 60)
765773
: name;
774+
const sessionsDir = getSessionsDir(projectPath);
775+
const filePath = join(sessionsDir, `${name}.json`);
776+
// Preserve an existing aiTitle across re-saves so we don't regenerate.
777+
let existingAiTitle: string | undefined;
778+
if (existsSync(filePath)) {
779+
try {
780+
const prev = JSON.parse(readFileSync(filePath, 'utf-8')) as Session;
781+
existingAiTitle = prev.aiTitle;
782+
} catch { /* ignore corrupt prior file */ }
783+
}
766784
const session: Session = {
767785
name,
768786
title,
787+
aiTitle: existingAiTitle,
769788
history,
770789
createdAt: new Date().toISOString(),
771790
};
772-
const sessionsDir = getSessionsDir(projectPath);
773-
const filePath = join(sessionsDir, `${name}.json`);
774791
writeFileSync(filePath, JSON.stringify(session, null, 2));
775792
logSession('save', name, true);
793+
794+
// Fire-and-forget: generate an AI title once the session has enough
795+
// content. The orchestrator early-returns if one already exists, so
796+
// this is a no-op on every save after the first successful generation.
797+
// Gated by autoSessionTitle so privacy/cost-conscious users can opt out
798+
// of the (small, background) API call entirely.
799+
if (config.get('autoSessionTitle') !== false
800+
&& !existingAiTitle
801+
&& history.filter(m => m.role !== 'system').length >= 3) {
802+
void maybeGenerateSessionTitle(name, projectPath).catch(() => {});
803+
}
776804
return true;
777805
} catch (error) {
778806
logSession('save', name, false);
779807
return false;
780808
}
781809
}
782810

811+
// Guard against concurrent title generation for the same session during
812+
// the 5s autosave cadence — only one in-flight call per session name.
813+
const titlesInFlight = new Set<string>();
814+
815+
/**
816+
* Generate and persist an AI title for a session, if it doesn't have
817+
* one yet. Safe to call repeatedly — early-returns when aiTitle exists
818+
* or a generation is already in flight.
819+
*/
820+
export async function maybeGenerateSessionTitle(name: string, projectPath?: string): Promise<void> {
821+
if (titlesInFlight.has(name)) return;
822+
const sessionsDir = getSessionsDir(projectPath);
823+
const filePath = join(sessionsDir, `${name}.json`);
824+
if (!existsSync(filePath)) return;
825+
826+
let data: Session;
827+
try {
828+
data = JSON.parse(readFileSync(filePath, 'utf-8')) as Session;
829+
} catch {
830+
return;
831+
}
832+
if (data.aiTitle) return;
833+
834+
titlesInFlight.add(name);
835+
try {
836+
const { generateSessionTitle } = await import('../utils/sessionTitles.js');
837+
const aiTitle = await generateSessionTitle(data.history);
838+
if (aiTitle) {
839+
// Re-read in case the session was re-saved while we were generating,
840+
// so we don't clobber newer history with our stale copy.
841+
try {
842+
const fresh = JSON.parse(readFileSync(filePath, 'utf-8')) as Session;
843+
fresh.aiTitle = aiTitle;
844+
writeFileSync(filePath, JSON.stringify(fresh, null, 2));
845+
} catch { /* ignore */ }
846+
}
847+
} finally {
848+
titlesInFlight.delete(name);
849+
}
850+
}
851+
783852
export function loadSession(name: string, projectPath?: string): Message[] | null {
784853
try {
785854
const sessionsDir = getSessionsDir(projectPath);
@@ -902,9 +971,12 @@ export function listSessionsWithInfo(projectPath?: string): SessionInfo[] {
902971
const stat = statSync(filePath);
903972
const data = JSON.parse(readFileSync(filePath, 'utf-8')) as Session;
904973
const sessionName = data.name || file.replace('.json', '');
905-
// Derive title: use stored title, else first user message, else session name
974+
// Title priority: AI-generated one-liner > stored title > first
975+
// user message > session name. aiTitle reads far better in
976+
// /sessions + /recall ("OAuth2 migration" vs "help me with the…").
906977
const firstUserMsg = data.history?.find(m => m.role === 'user');
907-
const title = data.title
978+
const title = data.aiTitle
979+
|| data.title
908980
|| (firstUserMsg ? firstUserMsg.content.replace(/\n/g, ' ').trim().slice(0, 60) : null)
909981
|| sessionName;
910982
sessions.push({

src/renderer/App.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ const COMMAND_DESCRIPTIONS: Record<string, string> = {
109109
'go': 'Execute the pending plan from /plan',
110110
'personality': 'Switch agent tone: concise / verbose / security / senior-reviewer / etc',
111111
'insights': 'Activity summary over the last N days (default 7): runs, files, tools, projects',
112+
'recall': 'Search across ALL saved sessions (cross-session; /search is current-session only)',
112113
};
113114

114115
import { helpCategories, keyboardShortcuts } from './components/Help';
@@ -305,6 +306,8 @@ export class App {
305306
'plan', 'go',
306307
// 2.0.3 — personalities + insights.
307308
'personality', 'insights',
309+
// 2.1.0 — cross-session recall.
310+
'recall',
308311
'c', 't', 'd', 'r', 'f', 'e', 'o', 'b', 'p',
309312
];
310313

0 commit comments

Comments
 (0)