Skip to content

Commit 902152d

Browse files
committed
feat(session): auto-resume by default; /new wipes context; /debug inspects state
The TUI never set opts.resume, so the session save we already did on every agent_end was discarded on next launch. Default the resume flag on for the interactive path and add --new to opt out. /new mid-session clears both the display transcript and the agent's internal message array, for hard reset after a topic shift without having to quit and relaunch with --new. /debug shows the display message count next to the agent's internal message count so users can self-diagnose 'the model forgot what I told it'; mismatch points at our wiring, parity points at the wire. docs/session-plan.md sequences the remaining save/resume/compaction work for pre.26+.
1 parent a7d0a95 commit 902152d

4 files changed

Lines changed: 192 additions & 1 deletion

File tree

docs/session-plan.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Session save / resume / compaction plan
2+
3+
> Status: draft — pre.25 lands auto-resume + `/new` + `/debug context`; the rest is sequenced below.
4+
5+
The TUI starts a fresh agent on every launch by default (until pre.25). Sessions ARE saved on every `agent_end` (`src/agent/agent.ts:239`) but the interactive path never sets `opts.resume`, so all that persistence does nothing for the in-CLI experience. This plan closes that gap and lays out the compaction story.
6+
7+
## What pre.25 ships
8+
9+
1. **Auto-resume by default.** `codebase` in a directory with a session ≤7 days old + same model loads the prior messages back into the agent before the user can prompt. `--new` flag opts out per-launch.
10+
2. **`/new` slash command.** Wipes both `state.messages` (display) and `bundle.agent.state.messages` (model context). The mid-session "start over" button.
11+
3. **`/debug` slash command.** Shows display message count vs agent internal message count side by side — diagnostic for "the model isn't remembering me" complaints.
12+
13+
## Today, on disk
14+
15+
```
16+
~/.codebase/sessions/{sha256(cwd)[:8]}.json
17+
{
18+
"formatVersion": 1,
19+
"workDir": "/abs/path",
20+
"modelId": "MiniMax-M2.7",
21+
"title": null,
22+
"messages": [...],
23+
"usage": {...},
24+
"updatedAt": 1747...
25+
}
26+
```
27+
28+
One file per cwd. Overwritten on every `agent_end`. Auto-discarded if older than 7 days or if the saved `modelId` doesn't match the currently-resolved model. Path computed from `sha256(cwd).slice(0, 8)` so multiple worktrees stay separate.
29+
30+
## Gaps to close (priority order)
31+
32+
### 1. Save on abort and graceful exit, not just agent_end
33+
**Why:** A turn cancelled mid-flight (Ctrl-C) or a clean exit (`/exit`, Ctrl-D) leaves the most recent in-flight assistant message unsaved. Reopening the CLI loses that last message.
34+
**How:** Hook into `bundle.agent.abort()` and the process exit path; call `sessions.save(...)` with the agent's current `_state.messages`.
35+
**Effort:** S
36+
37+
### 2. Multi-session per cwd (rotation)
38+
**Why:** Today one file per cwd; new session overwrites the old one. Users can't browse history.
39+
**How:**
40+
- Keep last N sessions per cwd (e.g. N=5). Rename current to `{hash}.{ts}.json` on rotation.
41+
- `/sessions` lists them with timestamps + first-message previews.
42+
- `/resume {id}` loads a specific archived session.
43+
**Effort:** M
44+
45+
### 3. Allow resume across model changes (with a warning)
46+
**Why:** Today `parsed.modelId !== modelId` returns null — switching from Claude to MiniMax discards your session. Compaction summaries and tool call shapes mostly survive a model swap, just not perfectly.
47+
**How:** On model mismatch, still resume but emit a soft warning ("Resumed from a session on a different model — some context may be re-interpreted").
48+
**Effort:** S
49+
50+
### 4. Session metadata for findability
51+
**Why:** Resumed-from cards in the welcome banner only show "N hours ago, M messages." If users have several worktrees, that's not enough to distinguish.
52+
**How:**
53+
- Generate a short title via the glue LLM on first save (one-line summary of the first user prompt).
54+
- Display "Resumed: 'add OAuth flow' · 47 messages · 2h ago".
55+
- `/sessions` list uses titles too.
56+
**Effort:** S (glue call is already wired)
57+
58+
### 5. Per-session log of file operations
59+
**Why:** Compaction loses the body of tool calls. If we want to answer "did I edit `src/foo.ts` in this session?" after a compaction pass, we need a separate audit log.
60+
**How:** Append to `~/.codebase/sessions/{hash}.fileops.jsonl` on every successful `edit_file` / `write_file` / `multi_edit`. Survives compaction.
61+
**Effort:** M
62+
63+
## Compaction — what's there and what's missing
64+
65+
`src/compaction/engine.ts` already does the work:
66+
- Threshold: 75% of model context window (`DEFAULT_THRESHOLD = 0.75`).
67+
- `transformContext` hook on the agent loop calls `compact()` when the threshold is crossed.
68+
- `glue` LLM summarizes older messages, keeps recent N verbatim.
69+
- `compaction_start` / `compaction_end` events emitted (we don't render them yet).
70+
71+
What's missing:
72+
73+
### A. Visible "Compacting…" notification
74+
**Why:** When the agent pauses for several seconds during a long session, users have no signal that it's compaction (not a hang).
75+
**How:** Subscribe to `compaction_start` in the reducer → set a `status: "compacting"` state → render in the status bar. `compaction_end` clears it.
76+
**Effort:** S
77+
78+
### B. CompactionDetails with file-op tracking
79+
**Why:** Per pi-mono pattern (`.settings/compare/pi-mono.md:103-122` in polyvibe-poc). After compaction, the summary should preserve which files were read / modified across the compacted range. The model uses this to maintain coherent file references when older context is summarized away.
80+
**How:** Extend `CompactionResult.details` with `{ readFiles: string[], modifiedFiles: string[] }` extracted from the compacted message range's tool calls. Surface via `/context` and `/debug`.
81+
**Effort:** M
82+
83+
### C. Snip-then-summarize pre-step (Claude Code pattern)
84+
**Why:** Today compaction summarizes the whole compactable range in one LLM call. If half of that range is verbose tool output that the model already extracted what it needs from, we're paying for summarization tokens twice.
85+
**How:** Before the summarize call, "snip": drop tool_result bodies for tool calls whose result the assistant clearly used (e.g. the assistant message immediately after quoted the file content). Summarize what remains.
86+
**Effort:** L (heuristics are tricky)
87+
88+
### D. User control: `/compact` already exists
89+
**Why:** Already shipped — forces a compaction pass on demand. Good. Just needs the visible notification (item A).
90+
91+
## Where to look for inspiration
92+
93+
| Topic | Source |
94+
|---|---|
95+
| Compaction with file-op preservation | `.pi-mono/packages/coding-agent/src/core/compaction/compaction.ts` |
96+
| Session JSONL format with branches | `.pi-mono/packages/coding-agent/src/core/session-manager.ts` |
97+
| Multi-session UX | Claude Code's `claude --continue` / `claude --resume {id}` pattern (`~/claude-code-source/`) |
98+
| Web app's conversation manager | `web/backend/agent/conversationManager.js` in polyvibe-poc |
99+
100+
## Sequencing recommendation
101+
102+
1. **pre.25 (this release):** auto-resume default, `--new`, `/new`, `/debug context`. → unblocks the "across launches" complaint.
103+
2. **pre.26:** items 1 + 3 + A. → makes the auto-resume safer (save on abort) and visible (compacting indicator).
104+
3. **pre.27:** items 2 + 4 (multi-session rotation + titles). → makes history navigable.
105+
4. **pre.28+:** B / C / 5. → polish for power users / very long sessions.

src/cli.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ for (const a of rawArgv) {
3737
process.env.CODEBASE_DEBUG_INPUT = "1";
3838
continue;
3939
}
40+
if (a === "--new" || a === "--fresh") {
41+
// Skip the auto-resume that the interactive TUI does by default —
42+
// useful when the prior session is no longer relevant or after a
43+
// destructive change to the working tree.
44+
process.env.CODEBASE_FRESH = "1";
45+
continue;
46+
}
4047
argv.push(a);
4148
}
4249

@@ -145,6 +152,10 @@ function printHelp(): void {
145152
" codebase --version print version and exit",
146153
" codebase --help show this message",
147154
"",
155+
"Session:",
156+
" codebase resume the prior session for this directory if recent (≤7d)",
157+
" codebase --new start a fresh session, ignoring saved history",
158+
"",
148159
"Diagnostics:",
149160
" --debug-input log every keystroke to ~/.codebase/logs/input.log",
150161
" (use when reporting a keyboard/terminal issue)",

src/commands/builtins.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,24 @@ const clear: Command = {
4444
},
4545
};
4646

47+
/**
48+
* Hard reset: drop both the display transcript AND the agent's internal
49+
* message history, so the next turn starts with zero context. Use after a
50+
* topic shift, when the model's gotten stuck in a stale plan, or to free
51+
* up context space without waiting for compaction.
52+
*/
53+
const fresh: Command = {
54+
name: "new",
55+
description: "Start a fresh conversation — wipes both display history and agent context.",
56+
mutates: true,
57+
handler: (_args, ctx) => {
58+
ctx.bundle.agent.state.messages = [];
59+
ctx.clearDisplay();
60+
ctx.emit("Started a fresh conversation. Prior context is gone for this and the next turn.");
61+
return { handled: true };
62+
},
63+
};
64+
4765
const compact: Command = {
4866
name: "compact",
4967
description: "Force a compaction pass on the running transcript.",
@@ -345,6 +363,56 @@ const context: Command = {
345363
},
346364
};
347365

366+
/**
367+
* Diagnostic for "the model isn't remembering what I told it earlier."
368+
* Compares the UI's display state (what the user sees in the transcript)
369+
* with the agent's internal _state.messages (what actually ships to the
370+
* model on the next turn). If those diverge, that's the bug. If they
371+
* match but the model still acts amnesiac, the issue is in the wire
372+
* call — pi-ai's openai-completions builder, the proxy, or the upstream.
373+
*/
374+
const debug: Command = {
375+
name: "debug",
376+
description: "Inspect internal agent state — message count, token estimate, last few roles.",
377+
handler: (_args, ctx) => {
378+
const display = ctx.state.messages;
379+
const internal = ctx.bundle.agent.state.messages;
380+
const rolesTail = (msgs: readonly { role: string }[], n: number) =>
381+
msgs
382+
.slice(-n)
383+
.map((m) => m.role)
384+
.join(" → ") || "(empty)";
385+
const u = ctx.state.usage;
386+
const used = u.input + u.cacheRead;
387+
const compactAt = ctx.bundle.compaction.threshold();
388+
const divergent = display.length !== internal.length;
389+
const lines = [
390+
"Internal state inspection:",
391+
"",
392+
` Display messages (UI): ${display.length}`,
393+
` Agent state messages: ${internal.length}${divergent ? " ← MISMATCH!" : ""}`,
394+
"",
395+
` Last 5 display roles: ${rolesTail(display, 5)}`,
396+
` Last 5 agent state roles: ${rolesTail(internal, 5)}`,
397+
"",
398+
` Estimated tokens used: ${used.toLocaleString()}`,
399+
` Compaction triggers at: ${compactAt.toLocaleString()}`,
400+
` Streaming in progress: ${ctx.state.streaming ? "yes" : "no"}`,
401+
"",
402+
divergent
403+
? "Mismatch means the agent and the UI disagree about what's been said. " +
404+
"That's the source of 'the model forgot' — the next turn ships internal " +
405+
"messages, not display messages. Report this with a `codebase --debug-input` " +
406+
"transcript so we can see how it happened."
407+
: "Display and agent state match. If the model is still acting amnesiac, the " +
408+
"context is leaving the CLI correctly but something on the wire is dropping " +
409+
"it — capture with OPENAI_LOG=debug codebase to see the raw HTTP request body.",
410+
];
411+
ctx.emit(lines.join("\n"));
412+
return { handled: true };
413+
},
414+
};
415+
348416
// ─── auth / session ───────────────────────────────────────────────────
349417

350418
const login: Command = {
@@ -526,6 +594,7 @@ const pwd: Command = {
526594
export const BUILTIN_COMMANDS: readonly Command[] = [
527595
help,
528596
clear,
597+
fresh,
529598
compact,
530599
session,
531600
cost,
@@ -545,5 +614,6 @@ export const BUILTIN_COMMANDS: readonly Command[] = [
545614
mcp,
546615
pwd,
547616
redo,
617+
debug,
548618
exit,
549619
];

src/ui/App.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,12 @@ export function App() {
4141
const { bundle, configError } = useMemo(() => {
4242
void setupAttempt;
4343
try {
44-
return { bundle: createAgent(), configError: undefined as string | undefined };
44+
// Auto-resume the prior session for this cwd by default. The
45+
// `--new` CLI flag (parsed in cli.tsx) sets CODEBASE_FRESH so
46+
// users who explicitly want a clean slate can opt out without
47+
// having to wipe ~/.codebase/sessions manually.
48+
const resume = process.env.CODEBASE_FRESH !== "1";
49+
return { bundle: createAgent({ resume }), configError: undefined as string | undefined };
4550
} catch (err) {
4651
return {
4752
bundle: undefined,

0 commit comments

Comments
 (0)