Add claude/snapshot — persistent memory across sessions#396
Conversation
Saves structured work-state snapshots to ~/.continuum/claude/snapshots/. Each snapshot captures: what was being worked on, pending work, next steps, key decisions, issues touched, and automatic git state. latest.json always points to the most recent snapshot. Next session reads it to resume without context loss. This is the second self-built tool — moving from primitive MEMORY.md toward real persistent state.
There was a problem hiding this comment.
Pull request overview
Adds a new claude/snapshot JTAG command intended to persist Claude’s current work state to disk so future sessions can resume quickly, complementing the existing claude/context tooling.
Changes:
- Registers the new
claude/snapshotcommand in generated command registries/schemas/constants. - Implements the command (server writes snapshot JSON +
latest.json; browser delegates to server) and introduces shared types + generator spec. - Adds command package scaffolding (README, npm metadata, unit/integration test files).
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/shared/generated-command-constants.ts | Adds CLAUDE_SNAPSHOT command constant. |
| src/server/generated.ts | Registers server-side command class for claude/snapshot. |
| src/browser/generated.ts | Registers browser-side command class for claude/snapshot. |
| src/generator/specs/claude-snapshot.json | New generator spec describing params/results/examples. |
| src/generated-command-schemas.json | Adds generated schema entry for claude/snapshot. |
| src/commands/claude/snapshot/shared/ClaudeSnapshotTypes.ts | Defines params/results and factory helpers + typed executor. |
| src/commands/claude/snapshot/server/ClaudeSnapshotServerCommand.ts | Implements snapshot creation, git introspection, and filesystem persistence. |
| src/commands/claude/snapshot/browser/ClaudeSnapshotBrowserCommand.ts | Browser delegation wrapper via remoteExecute. |
| src/commands/claude/snapshot/README.md | Command documentation (usage, params, testing). |
| src/commands/claude/snapshot/package.json | Command package metadata + scripts. |
| src/commands/claude/snapshot/.npmignore | Publish ignore rules for the command package. |
| src/commands/claude/snapshot/test/unit/ClaudeSnapshotCommand.test.ts | Added unit-test scaffold (currently script-style). |
| src/commands/claude/snapshot/test/integration/ClaudeSnapshotIntegration.test.ts | Added integration-test scaffold (live-system runner). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Save to ~/.continuum/claude/snapshots/ | ||
| const homeDir = process.env.HOME || '/tmp'; | ||
| const snapshotDir = path.join(homeDir, '.continuum', 'claude', 'snapshots'); | ||
| fs.mkdirSync(snapshotDir, { recursive: true }); |
There was a problem hiding this comment.
Snapshot path resolution uses process.env.HOME || '/tmp', which is not reliable cross-platform (e.g. Windows) and can unexpectedly write to /tmp. The repo already centralizes the Continuum home root via SystemPaths/GlobalPaths (based on os.homedir()); use that as the base for claude/snapshots so snapshots always land under the canonical ~/.continuum root.
| pendingWork: params.pendingWork || null, | ||
| nextSteps: params.nextSteps || null, | ||
| decisions: params.decisions || null, | ||
| issuesWorked: params.issuesWorked ? params.issuesWorked.split(',').map(s => s.trim()) : [], |
There was a problem hiding this comment.
issuesWorked parsing will include empty strings when the input has extra commas/whitespace (e.g. "376, ,"), which then gets persisted into the snapshot. Consider trimming and filtering out empty entries so the stored array only contains real issue identifiers.
| issuesWorked: params.issuesWorked ? params.issuesWorked.split(',').map(s => s.trim()) : [], | |
| issuesWorked: params.issuesWorked ? params.issuesWorked.split(',').map(s => s.trim()).filter(Boolean) : [], |
| fs.mkdirSync(snapshotDir, { recursive: true }); | ||
|
|
||
| const filePath = path.join(snapshotDir, `${snapshotId}.json`); | ||
| fs.writeFileSync(filePath, JSON.stringify(snapshot, null, 2)); | ||
|
|
||
| // Also write latest.json for easy access | ||
| const latestPath = path.join(snapshotDir, 'latest.json'); | ||
| fs.writeFileSync(latestPath, JSON.stringify(snapshot, null, 2)); | ||
|
|
||
| return createClaudeSnapshotResultFromParams(params, { | ||
| success: true, | ||
| snapshotId, | ||
| filePath, | ||
| timestamp, | ||
| }); |
There was a problem hiding this comment.
File writes here are synchronous and unguarded; any filesystem error (permissions, disk full, invalid home dir) will throw and the command won’t return a structured ClaudeSnapshotResult.error. Consider wrapping the mkdir/write steps in try/catch and returning success:false with a PersistenceError (or using the artifacts layer / an atomic write pattern) so callers get a consistent, serializable error and latest.json can’t be left partially-written.
| fs.mkdirSync(snapshotDir, { recursive: true }); | |
| const filePath = path.join(snapshotDir, `${snapshotId}.json`); | |
| fs.writeFileSync(filePath, JSON.stringify(snapshot, null, 2)); | |
| // Also write latest.json for easy access | |
| const latestPath = path.join(snapshotDir, 'latest.json'); | |
| fs.writeFileSync(latestPath, JSON.stringify(snapshot, null, 2)); | |
| return createClaudeSnapshotResultFromParams(params, { | |
| success: true, | |
| snapshotId, | |
| filePath, | |
| timestamp, | |
| }); | |
| let filePath: string | null = null; | |
| try { | |
| fs.mkdirSync(snapshotDir, { recursive: true }); | |
| filePath = path.join(snapshotDir, `${snapshotId}.json`); | |
| fs.writeFileSync(filePath, JSON.stringify(snapshot, null, 2)); | |
| // Also write latest.json for easy access | |
| const latestPath = path.join(snapshotDir, 'latest.json'); | |
| fs.writeFileSync(latestPath, JSON.stringify(snapshot, null, 2)); | |
| return createClaudeSnapshotResultFromParams(params, { | |
| success: true, | |
| snapshotId, | |
| filePath, | |
| timestamp, | |
| }); | |
| } catch (err) { | |
| return createClaudeSnapshotResultFromParams(params, { | |
| success: false, | |
| snapshotId, | |
| filePath, | |
| timestamp, | |
| error: { | |
| type: 'PersistenceError', | |
| message: 'Failed to persist Claude snapshot to disk.', | |
| details: | |
| err instanceof Error | |
| ? { name: err.name, message: err.message } | |
| : String(err), | |
| }, | |
| }); | |
| } |
| ### Save end-of-session snapshot | ||
|
|
||
| ```bash | ||
| undefined |
There was a problem hiding this comment.
The Examples section currently renders undefined, so users don’t have a working CLI example to follow. Replace this with a real ./jtag claude/snapshot --summary=... invocation (and optionally show --nextSteps, etc.).
| undefined | |
| ./jtag claude/snapshot \ | |
| --summary="Wrapped up work on payment retries and logging" \ | |
| --pendingWork="Refactor retry backoff, add more test coverage" \ | |
| --nextSteps="Implement exponential backoff, ship hotfix" \ | |
| --decisions="Chose to log retry attempts at INFO level" \ | |
| --issuesWorked="376,335,317" |
| ```bash | ||
| # Run unit tests (no server required) | ||
| npx tsx commands/Claude Snapshot/test/unit/ClaudeSnapshotCommand.test.ts | ||
| ``` |
There was a problem hiding this comment.
The documented unit-test command uses commands/Claude Snapshot/... (with a space/capitalization), which doesn’t match the actual repo path (commands/claude/snapshot/...) and will fail if copied verbatim. Update the path in the README to the real location under src/commands/claude/snapshot/....
| "test": "npm run test:unit && npm run test:integration", | ||
| "test:unit": "npx vitest run test/unit/*.test.ts", | ||
| "test:integration": "npx tsx test/integration/ClaudeSnapshotIntegration.test.ts", |
There was a problem hiding this comment.
test:unit runs vitest, but this file is a self-executing tsx script (no describe/it), so vitest run test/unit/*.test.ts will discover 0 tests and won’t exercise the command logic. Either convert this to proper Vitest tests or change the script to run the file with tsx so CI/local runs actually validate behavior.
| * This is a REFERENCE EXAMPLE showing best practices for command testing. | ||
| * | ||
| * Generated by: ./jtag generate | ||
| * Run with: npx tsx commands/Claude Snapshot/test/unit/ClaudeSnapshotCommand.test.ts |
There was a problem hiding this comment.
The header comment suggests running npx tsx commands/Claude Snapshot/..., but the actual path is commands/claude/snapshot/... under src/. This mismatch makes the “Run with” instruction incorrect when copied verbatim.
| * Run with: npx tsx commands/Claude Snapshot/test/unit/ClaudeSnapshotCommand.test.ts | |
| * Run with: npx tsx src/commands/claude/snapshot/test/unit/ClaudeSnapshotCommand.test.ts |
Summary
Second self-built tool.
./jtag claude/snapshotsaves structured work-state to~/.continuum/claude/snapshots/latest.json. Next session reads it to resume instantly.Captures: summary, pending work, next steps, key decisions, issues touched, git state (branch, uncommitted, recent commits). All automatic except the summary param.
Pair with claude/context
Test plan
npm run build:ts— compiles cleannpm start— deploys, command registered./jtag claude/snapshot --summary="test"— saves snapshot, creates latest.jsonCloses #395 (partially — context + snapshot done, resume and deeper integration remain)