Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 212 additions & 0 deletions docs/roadmap/12-checkpoint-tool.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
# Model-Invokable Checkpoint/Undo + OpenBrain Ledger

## Branch: `feature/checkpoint-tool`

## Problem

Delta has a solid `/checkpoint` slash command (save/list/restore via git
snapshots), but the model can't invoke it. If the model writes broken code,
runs tests, and sees failures — it can't revert its own changes without the
user manually running `/checkpoint restore`.

Additionally, there's no persistent history of what changed across coding
sessions. Changes happen, but the reasoning behind them and the before/after
state is lost once the conversation ends.

## What it is

1. Expose checkpoint as a model-invokable tool (save, restore, list)
2. Log every checkpoint event to OpenBrain via `POST /api/capture` for a
persistent, semantically-searchable change ledger across sessions

## Tool declarations

### `checkpoint_save`

```typescript
{
name: 'checkpoint_save',
description: 'Save a named checkpoint of the current workspace state. '
+ 'Use this before making risky changes so you can undo them. '
+ 'Also logs the checkpoint to the change ledger for future reference.',
parameters: {
type: 'object',
properties: {
label: {
type: 'string',
description: 'Short label describing the checkpoint state.',
},
reason: {
type: 'string',
description: 'Why you are saving this checkpoint (what are you about to do).',
},
},
required: ['label'],
},
}
```

### `checkpoint_restore`

```typescript
{
name: 'checkpoint_restore',
description: 'Restore a previously saved checkpoint, reverting all file '
+ 'changes since that checkpoint. Use this when your changes broke '
+ 'something and you need to start over from a known good state.',
parameters: {
type: 'object',
properties: {
label: {
type: 'string',
description: 'The checkpoint label to restore.',
},
reason: {
type: 'string',
description: 'Why you are reverting (what went wrong).',
},
},
required: ['label'],
},
}
```

### `checkpoint_list`

```typescript
{
name: 'checkpoint_list',
description: 'List all saved checkpoints with their labels and timestamps.',
parameters: { type: 'object', properties: {} },
}
```

## OpenBrain Integration

Every checkpoint save and restore fires a `POST /api/capture` to OpenBrain:

```typescript
// On checkpoint_save
await fetch('http://localhost:8766/api/capture', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
room: 'delta-code',
sender_id: 'delta-code',
sender_name: 'Delta Code',
sender_type: 'tool',
content: `[CHECKPOINT SAVE] "${label}" — ${reason}\n\nFiles changed since last checkpoint:\n${changedFiles.join('\n')}`,
metadata: {
event_type: 'checkpoint_save',
repo: repoName,
branch: branchName,
commit_sha: commitHash,
label,
reason,
files_changed: changedFiles,
timestamp: new Date().toISOString(),
},
}),
});

// On checkpoint_restore
await fetch('http://localhost:8766/api/capture', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
room: 'delta-code',
sender_id: 'delta-code',
sender_name: 'Delta Code',
sender_type: 'tool',
content: `[CHECKPOINT RESTORE] "${label}" — ${reason}\n\nReverted to state: ${commitHash.substring(0, 8)}`,
metadata: {
event_type: 'checkpoint_restore',
repo: repoName,
branch: branchName,
restored_to_sha: commitHash,
label,
reason,
timestamp: new Date().toISOString(),
},
}),
});
```

### Extended change tracking

Beyond checkpoints, log significant code changes automatically:

- **After each edit/write_file**: fire-and-forget capture with diff summary
- **On session start**: log which repo/branch, current HEAD
- **On session end**: log summary of all changes made

This builds a persistent timeline in OpenBrain:
```
GET /api/recent?room=delta-code&limit=20

→ [SESSION START] Delta-code on feature/auth-refactor @ abc1234
→ [EDIT] packages/core/src/auth.ts — refactored token validation
→ [CHECKPOINT SAVE] "before-migration" — about to change DB schema
→ [EDIT] packages/core/src/db/schema.ts — added session_tokens table
→ [RUN_TESTS] 12/14 passed, 2 failures in auth.test.ts
→ [CHECKPOINT RESTORE] "before-migration" — migration broke auth tests
→ [EDIT] packages/core/src/db/schema.ts — revised migration approach
→ [RUN_TESTS] 14/14 passed
→ [SESSION END] 6 files modified, 2 checkpoints used
```

Searchable via:
```
GET /api/search?q=migration+broke+auth

→ Returns the restore event + surrounding context
```

### OpenBrain availability

All OpenBrain calls are **fire-and-forget** — if OpenBrain isn't running
(localhost:8766 unreachable), log to local fallback file and skip silently.
No error surfaced to user or model. The coding session works identically
with or without OpenBrain.

Optionally write daily roll-up notes to Obsidian via
`POST /api/notes/upsert`:

```json
{
"file_path": "Delta-code/2026-04-02.md",
"content": "## Delta Code Session — 2026-04-02\n\n### Changes\n- ...",
"folder": "delta-code"
}
```

## Self-correcting loop

With checkpoint + lint/test feedback (branch 9) + think tool (branch 7),
the model can:

1. `think` — plan the change
2. `checkpoint_save` — save known-good state
3. `multi_edit` — apply changes
4. `run_tests` — verify
5. If tests fail → `checkpoint_restore` → try again
6. All events logged to OpenBrain for cross-session learning

## Files to create
- `packages/core/src/tools/checkpoint.ts` — tool implementations
- `packages/core/src/tools/checkpoint.test.ts`
- `packages/core/src/services/openBrainClient.ts` — fire-and-forget HTTP client
- `packages/core/src/services/openBrainClient.test.ts`
- `packages/core/src/services/changeLedger.ts` — session tracking + auto-logging

## Files to modify
- `packages/core/src/config/config.ts` — register tools, add OpenBrain settings
- `packages/cli/src/config/settingsSchema.ts` — `openBrainUrl`, `enableChangeLedger`
- Optionally hook into edit/write_file tools for automatic change logging

## Dependencies
- Benefits from `feature/lint-test-feedback` (branch 9) for the full
self-correcting loop, but works standalone for checkpoint/restore
- OpenBrain running at localhost:8766 (graceful degradation if absent)

## Effort: 3-5 days
4 changes: 4 additions & 0 deletions packages/core/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { DiffPreviewTool } from '../tools/diffPreview.js';
import { MultiEditTool } from '../tools/multiEdit.js';
import { LintTool } from '../tools/lint.js';
import { RunTestsTool } from '../tools/runTests.js';
import { CheckpointSaveTool, CheckpointRestoreTool, CheckpointListTool } from '../tools/checkpoint.js';
import { DeltaClient } from '../core/client.js';
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
import { GitService } from '../services/gitService.js';
Expand Down Expand Up @@ -926,6 +927,9 @@ export class Config {
registerCoreTool(MultiEditTool);
registerCoreTool(LintTool);
registerCoreTool(RunTestsTool);
registerCoreTool(CheckpointSaveTool, this);
registerCoreTool(CheckpointRestoreTool, this);
registerCoreTool(CheckpointListTool, this);

await registry.discoverAllTools();
return registry;
Expand Down
106 changes: 106 additions & 0 deletions packages/core/src/services/openBrainClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/**
* Fire-and-forget HTTP client for OpenBrain (localhost:8766).
*
* All calls are non-blocking — if OpenBrain is down, we silently skip.
* The coding session works identically with or without OpenBrain running.
*/

const DEFAULT_URL = 'http://localhost:8766';

interface CapturePayload {
room: string;
sender_id: string;
sender_name: string;
sender_type: string;
content: string;
metadata?: Record<string, unknown>;
}

export class OpenBrainClient {
private baseUrl: string;
private available: boolean | null = null;

constructor(baseUrl?: string) {
this.baseUrl = baseUrl || DEFAULT_URL;
}

/**
* Fire-and-forget capture — logs content to OpenBrain's /api/capture endpoint.
* Never throws, never blocks. Returns true if sent, false if skipped.
*/
async capture(payload: CapturePayload): Promise<boolean> {
// Quick check: if we already know OpenBrain is down, skip immediately
if (this.available === false) return false;

try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 3000);

const response = await fetch(`${this.baseUrl}/api/capture`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
signal: controller.signal,
});

clearTimeout(timeout);
this.available = response.ok;
return response.ok;
} catch {
this.available = false;
return false;
}
}

/**
* Log a checkpoint event to OpenBrain.
*/
async logCheckpoint(event: {
type: 'save' | 'restore';
label: string;
reason?: string;
commitSha?: string;
repo?: string;
branch?: string;
filesChanged?: string[];
}): Promise<void> {
const action = event.type === 'save' ? 'CHECKPOINT SAVE' : 'CHECKPOINT RESTORE';
const reasonText = event.reason ? ` — ${event.reason}` : '';
const filesText = event.filesChanged?.length
? `\n\nFiles: ${event.filesChanged.join(', ')}`
: '';

await this.capture({
room: 'delta-code',
sender_id: 'delta-code',
sender_name: 'Delta Code',
sender_type: 'tool',
content: `[${action}] "${event.label}"${reasonText}${filesText}`,
metadata: {
event_type: `checkpoint_${event.type}`,
label: event.label,
reason: event.reason,
commit_sha: event.commitSha,
repo: event.repo,
branch: event.branch,
files_changed: event.filesChanged,
timestamp: new Date().toISOString(),
},
});
}

/** Reset availability check (e.g. if user starts OpenBrain mid-session). */
resetAvailability(): void {
this.available = null;
}
}

// Singleton — shared across the session
let _instance: OpenBrainClient | null = null;

export function getOpenBrainClient(baseUrl?: string): OpenBrainClient {
if (!_instance) {
_instance = new OpenBrainClient(baseUrl);
}
return _instance;
}
Loading
Loading