osborn 0.9.211 → 0.9.212
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/recall/SKILL.md +87 -0
- package/bin/recall.js +42 -0
- package/dist/claude-llm.d.ts +28 -0
- package/dist/claude-llm.js +172 -11
- package/dist/embedder.d.ts +23 -0
- package/dist/embedder.js +98 -0
- package/dist/pipeline-direct-llm.js +4 -0
- package/dist/prompts/grounding-recall.md +15 -0
- package/dist/prompts/recalled-context.md +7 -0
- package/dist/prompts.d.ts +2 -0
- package/dist/prompts.js +53 -0
- package/dist/recall-cli.d.ts +17 -0
- package/dist/recall-cli.js +140 -0
- package/dist/session-store.d.ts +81 -0
- package/dist/session-store.js +458 -0
- package/package.json +6 -2
- package/scripts/backfill-stores.ts +99 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Recall
|
|
2
|
+
|
|
3
|
+
## SKILL IDENTITY
|
|
4
|
+
Name: recall
|
|
5
|
+
Install path: ~/.claude/skills/recall/SKILL.md
|
|
6
|
+
Portable: yes — drops into any agent's skills dir (Claude Code, osborn on Fly, other Claude Agent SDK hosts)
|
|
7
|
+
|
|
8
|
+
## WHEN THIS SKILL ACTIVATES
|
|
9
|
+
Whenever you need something from EARLIER in this session (or a prior session) that is
|
|
10
|
+
no longer in your context window. Specifically:
|
|
11
|
+
|
|
12
|
+
- The user refers to a past decision, file, error, number, or name you don't currently see
|
|
13
|
+
("what did we decide about…", "that bug from earlier", "the token we used", "remind me why…").
|
|
14
|
+
- You are a GROUNDED agent (researcher, reviewer, editor, planner, tester) about to act, and you
|
|
15
|
+
must not contradict prior decisions or redo prior work — check the record FIRST.
|
|
16
|
+
- You suspect the answer was established before the last compaction.
|
|
17
|
+
- Anytime you would otherwise say "I don't have that in context" about this project's history.
|
|
18
|
+
|
|
19
|
+
Explicit triggers: "recall", "search the session", "what did we say about", "look back".
|
|
20
|
+
|
|
21
|
+
## CORE PRINCIPLE
|
|
22
|
+
This session's FULL history — every user message, assistant reply, thinking block, and tool
|
|
23
|
+
call, untruncated — is stored in a per-session embedded database (`session.db`: SQLite +
|
|
24
|
+
FTS5 keyword index + sqlite-vec semantic vectors). You do NOT grep a flat summary file and
|
|
25
|
+
you do NOT rely only on what's in your context window. You query the store with a fixed
|
|
26
|
+
command and read the real prior messages.
|
|
27
|
+
|
|
28
|
+
Prefer recalling over guessing. If a fact was ever said in this project, it is retrievable.
|
|
29
|
+
|
|
30
|
+
## HOW TO USE — the `osborn-recall` command
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
osborn-recall "<query>" [--mode hybrid|keyword|vector] [--top-k 8]
|
|
34
|
+
[--session <id> --cwd <dir> | --db <path>]
|
|
35
|
+
[--max-chars 1200] [--type user,assistant,thinking,tool_use,tool_result]
|
|
36
|
+
[--json]
|
|
37
|
+
osborn-recall --list [--cwd <dir>] # list available session stores, newest first
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- **Default is `hybrid`** — fuses keyword (BM25) + semantic (vector) ranking with RRF. Use it
|
|
41
|
+
unless you have a reason not to. It finds both exact terms and paraphrases.
|
|
42
|
+
- **`--mode keyword`** — exact terms only; never loads the embedding model, so it's fastest.
|
|
43
|
+
Use for identifiers, error strings, tokens, file paths, function names.
|
|
44
|
+
- **`--mode vector`** — pure semantic; use when you remember the MEANING but not the words
|
|
45
|
+
("how we backed up the machine" → finds Fly/rsync content with no shared keywords).
|
|
46
|
+
- **`--top-k`** — how many hits to return (default 8). Raise for a broad sweep, lower to focus.
|
|
47
|
+
- **Store resolution**: with no `--db`/`--session`, it picks the newest `session.db` under the
|
|
48
|
+
current project. Pass `--session <id> --cwd <dir>` to target a specific past session, or
|
|
49
|
+
`--db <path>` to point directly at a file. Use `--list` to see what's available.
|
|
50
|
+
- **`--type`** — filter to message kinds (comma-separated). E.g. `--type user` to see only what
|
|
51
|
+
the user actually asked; `--type tool_use,tool_result` to find a past command and its output.
|
|
52
|
+
|
|
53
|
+
### Examples
|
|
54
|
+
```
|
|
55
|
+
# What did we decide about the deploy order?
|
|
56
|
+
osborn-recall "deploy order npm publish git push railway fly" --top-k 5
|
|
57
|
+
|
|
58
|
+
# Find the exact Supabase token string we used (exact match, fast)
|
|
59
|
+
osborn-recall "SUPABASE_PERSONAL_ACCESS_TOKEN" --mode keyword
|
|
60
|
+
|
|
61
|
+
# Semantic: remember the meaning, not the words
|
|
62
|
+
osborn-recall "how did we recover the lost sessions" --mode vector
|
|
63
|
+
|
|
64
|
+
# Only the user's own asks about a topic
|
|
65
|
+
osborn-recall "marnmorgan authenticated onboarding" --type user
|
|
66
|
+
|
|
67
|
+
# Machine-readable for programmatic use
|
|
68
|
+
osborn-recall "sqlite-vec int8 rowid bug" --json
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## WORKFLOW
|
|
72
|
+
1. Turn the thing you're missing into a short query — include distinctive terms (names, error
|
|
73
|
+
text, identifiers) for keyword strength, but plain-language is fine (hybrid handles both).
|
|
74
|
+
2. Run `osborn-recall "<query>"`. Read the returned hits — each is a REAL prior message with its
|
|
75
|
+
source line, message type, model, and timestamp.
|
|
76
|
+
3. If nothing relevant: broaden the query, raise `--top-k`, or switch `--mode` (keyword↔vector).
|
|
77
|
+
If still empty, `--list` to confirm a store exists; the flat `search-index.txt` is the legacy
|
|
78
|
+
fallback but the store is authoritative and untruncated.
|
|
79
|
+
4. Ground your next action in what you found. Cite the source line when it matters
|
|
80
|
+
("per L214, we settled on X").
|
|
81
|
+
|
|
82
|
+
## NOTES
|
|
83
|
+
- The store is written incrementally each turn, so recent messages are usually present within a
|
|
84
|
+
turn or two. Very-latest exchanges may lag by one turn — that's fine, they're still in context.
|
|
85
|
+
- Keyword mode never downloads the embedding model; hybrid/vector load MiniLM once (~12s cold,
|
|
86
|
+
cached after). If the embedder is unavailable, recall silently falls back to keyword-only.
|
|
87
|
+
- This skill is READ-only recall. It never writes; the pipeline owns the write path.
|
package/bin/recall.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// osborn-recall — thin shim. Runs src/recall-cli.ts via tsx in dev, or the
|
|
4
|
+
// compiled dist/recall-cli.js after `npm install`. Mirrors bin/cli.js.
|
|
5
|
+
|
|
6
|
+
import { spawn } from 'child_process'
|
|
7
|
+
import { fileURLToPath } from 'url'
|
|
8
|
+
import { dirname, join } from 'path'
|
|
9
|
+
import { existsSync, lstatSync, symlinkSync } from 'fs'
|
|
10
|
+
import os from 'os'
|
|
11
|
+
|
|
12
|
+
// Same /workspace/.claude symlink guard as cli.js so stores resolve on Fly machines.
|
|
13
|
+
try {
|
|
14
|
+
const home = os.homedir()
|
|
15
|
+
const target = '/workspace/.claude'
|
|
16
|
+
const link = join(home, '.claude')
|
|
17
|
+
if (existsSync(target) && home !== '/workspace') {
|
|
18
|
+
let needsLink = true
|
|
19
|
+
try { needsLink = !lstatSync(link).isSymbolicLink() } catch {}
|
|
20
|
+
if (needsLink) symlinkSync(target, link)
|
|
21
|
+
}
|
|
22
|
+
} catch {}
|
|
23
|
+
|
|
24
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
25
|
+
const args = process.argv.slice(2)
|
|
26
|
+
|
|
27
|
+
const srcPath = join(__dirname, '..', 'src', 'recall-cli.ts')
|
|
28
|
+
const distPath = join(__dirname, '..', 'dist', 'recall-cli.js')
|
|
29
|
+
|
|
30
|
+
let child
|
|
31
|
+
if (existsSync(srcPath)) {
|
|
32
|
+
const tsxPath = join(__dirname, '..', 'node_modules', '.bin', 'tsx')
|
|
33
|
+
child = spawn(tsxPath, [srcPath, ...args], { stdio: 'inherit', cwd: join(__dirname, '..'), env: process.env })
|
|
34
|
+
} else if (existsSync(distPath)) {
|
|
35
|
+
child = spawn('node', [distPath, ...args], { stdio: 'inherit', cwd: join(__dirname, '..'), env: process.env })
|
|
36
|
+
} else {
|
|
37
|
+
console.error('Error: neither src/recall-cli.ts nor dist/recall-cli.js found')
|
|
38
|
+
process.exit(1)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
child.on('error', (err) => { console.error('Failed to start osborn-recall:', err.message); process.exit(1) })
|
|
42
|
+
child.on('exit', (code) => process.exit(code || 0))
|
package/dist/claude-llm.d.ts
CHANGED
|
@@ -45,6 +45,7 @@ export declare const NAMED_AGENTS: {
|
|
|
45
45
|
researcher: {
|
|
46
46
|
description: string;
|
|
47
47
|
tools: string[];
|
|
48
|
+
grounded: boolean;
|
|
48
49
|
model: string;
|
|
49
50
|
prompt: string;
|
|
50
51
|
};
|
|
@@ -57,6 +58,7 @@ export declare const NAMED_AGENTS: {
|
|
|
57
58
|
writer: {
|
|
58
59
|
description: string;
|
|
59
60
|
tools: string[];
|
|
61
|
+
grounded: boolean;
|
|
60
62
|
model: string;
|
|
61
63
|
prompt: string;
|
|
62
64
|
};
|
|
@@ -69,6 +71,7 @@ export declare const NAMED_AGENTS: {
|
|
|
69
71
|
planner: {
|
|
70
72
|
description: string;
|
|
71
73
|
tools: string[];
|
|
74
|
+
grounded: boolean;
|
|
72
75
|
model: string;
|
|
73
76
|
prompt: string;
|
|
74
77
|
};
|
|
@@ -88,6 +91,23 @@ export declare const FALLBACK_MODEL = "minimax/minimax-m3";
|
|
|
88
91
|
* NEVER mutates the input — NAMED_AGENTS and DB-sourced rows are untouched.
|
|
89
92
|
*/
|
|
90
93
|
export declare function applyTurbo(agents: Record<string, any>, turbo: boolean): Record<string, any>;
|
|
94
|
+
/**
|
|
95
|
+
* Inject session-recall grounding into any agent flagged `grounded: true`.
|
|
96
|
+
*
|
|
97
|
+
* WHY: a sub-agent's file tools (Read/Grep/Glob) are sandboxed to its cwd +
|
|
98
|
+
* additionalDirectories, so it CANNOT read the session index / session.db that live
|
|
99
|
+
* under $HOME/.claude/projects/… — the old "grep search-index.txt" grounding silently
|
|
100
|
+
* failed. Bash, however, is NOT cwd-restricted, so `osborn-recall` reaches the store.
|
|
101
|
+
*
|
|
102
|
+
* So for every grounded agent we (1) hand it the EXACT, absolute-path osborn-recall
|
|
103
|
+
* command (resolved once here — the single dynamic resolver, so we never hardcode a
|
|
104
|
+
* per-agent path; works for named AND user-created custom grounded agents), and
|
|
105
|
+
* (2) guarantee Bash is in its tool set so it can run that command. The `grounded`
|
|
106
|
+
* flag is stripped before the roster reaches the SDK. NEVER mutates the input.
|
|
107
|
+
*
|
|
108
|
+
* Adversarial agents (reviewer/tester) leave `grounded` unset → untouched, stay blind.
|
|
109
|
+
*/
|
|
110
|
+
export declare function applyGrounding(agents: Record<string, any>, sessionId: string | null, workingDir: string | undefined): Record<string, any>;
|
|
91
111
|
/**
|
|
92
112
|
* Claude LLM - Wraps Claude Agent SDK for LiveKit
|
|
93
113
|
* Research mode: reads anything, writes only to session workspace
|
|
@@ -111,6 +131,14 @@ export declare class ClaudeLLM extends llm.LLM {
|
|
|
111
131
|
toolName: string;
|
|
112
132
|
input: any;
|
|
113
133
|
} | null;
|
|
134
|
+
/**
|
|
135
|
+
* Guarded, fire-and-forget write-through to the embedded session.db. Called from the
|
|
136
|
+
* main agent's UserPromptSubmit hook (once per real user submission). Sweeps the FULL
|
|
137
|
+
* source set — main JSONL + every sub-agent JSONL — incrementally (byte-offset resume).
|
|
138
|
+
* The guard lives here (on the long-lived ClaudeLLM, not the per-turn stream) so a slow
|
|
139
|
+
* write can't overlap the next turn's write. Never throws; never blocks the caller.
|
|
140
|
+
*/
|
|
141
|
+
triggerStoreUpdate(sessionId: string, workingDir: string): void;
|
|
114
142
|
/**
|
|
115
143
|
* Get all currently enabled MCP servers
|
|
116
144
|
*/
|
package/dist/claude-llm.js
CHANGED
|
@@ -11,8 +11,10 @@ import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
|
11
11
|
import { EventEmitter } from 'events';
|
|
12
12
|
import { saveSessionMetadata, getSessionWorkspace } from './config.js';
|
|
13
13
|
import { statusManager } from './status-manager.js';
|
|
14
|
-
import { getResearchSystemPrompt, getDirectModeResearchPrompt } from './prompts.js';
|
|
14
|
+
import { getResearchSystemPrompt, getDirectModeResearchPrompt, getGroundingBlock, getRecalledContextBlock } from './prompts.js';
|
|
15
15
|
import { getIndexPath } from './summary-index.js';
|
|
16
|
+
import { openStore, recall, storeExists, updateSessionStore, getStorePath } from './session-store.js';
|
|
17
|
+
import { getEmbedder } from './embedder.js';
|
|
16
18
|
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
17
19
|
import { join, dirname, resolve, basename } from 'node:path';
|
|
18
20
|
import { fileURLToPath } from 'node:url';
|
|
@@ -20,6 +22,70 @@ import { homedir } from 'node:os';
|
|
|
20
22
|
// Directory of this module — used to locate co-located prompt files (e.g., turn-shape reminder).
|
|
21
23
|
const __claudeLlmDir = dirname(fileURLToPath(import.meta.url));
|
|
22
24
|
const TURN_SHAPE_REMINDER_PATH = join(__claudeLlmDir, 'prompts', 'turn-shape-reminder.md');
|
|
25
|
+
// ── Recall auto-injection (reliable "remembers to check" per Agent-SDK research) ──
|
|
26
|
+
// The RELIABLE primitive the Agent SDK exposes is a UserPromptSubmit hook returning
|
|
27
|
+
// additionalContext: deterministic per-turn injection the model cannot skip (vs. an
|
|
28
|
+
// MCP/skill recall tool the model must elect to call). We piggyback on the existing
|
|
29
|
+
// turn-shape-reminder hook and append the top-K prior messages relevant to THIS prompt.
|
|
30
|
+
//
|
|
31
|
+
// Scope: this hook lives on the MAIN conductor's query options only. Named adversarial
|
|
32
|
+
// sub-agents (reviewer/tester) are spawned as SEPARATE query() calls with their own
|
|
33
|
+
// options and NO UserPromptSubmit hook — they stay deliberately un-grounded, so this
|
|
34
|
+
// injection never leaks into them. Task-delegated agents (researcher/writer/…) don't
|
|
35
|
+
// fire UserPromptSubmit (that event is for real user turns), so they're unaffected too.
|
|
36
|
+
//
|
|
37
|
+
// COMPACT by design: paid on every turn, so top-5 with a per-hit char cap (~800) →
|
|
38
|
+
// ~4KB. FULL untruncated text stays available on demand via `osborn-recall`.
|
|
39
|
+
const RECALL_TOP_K = 5;
|
|
40
|
+
const RECALL_PER_HIT_CHARS = 800;
|
|
41
|
+
const RECALL_ENABLED = () => process.env.OSBORN_RECALL_INJECT !== '0';
|
|
42
|
+
// Warm the embedder once (fire-and-forget) so hybrid recall is ready without blocking
|
|
43
|
+
// the first turn; until it's warm, recall() falls back to keyword-only (~16ms).
|
|
44
|
+
let __embedderWarmed = false;
|
|
45
|
+
function warmEmbedder() {
|
|
46
|
+
if (__embedderWarmed || process.env.OSBORN_EMBED === '0')
|
|
47
|
+
return;
|
|
48
|
+
__embedderWarmed = true;
|
|
49
|
+
getEmbedder().catch(() => { });
|
|
50
|
+
}
|
|
51
|
+
/** Build the recalled-context block to inject alongside the turn-shape reminder. '' on any miss. */
|
|
52
|
+
async function buildRecallInjection(sessionId, workingDir, prompt) {
|
|
53
|
+
try {
|
|
54
|
+
if (!RECALL_ENABLED() || !sessionId || !workingDir)
|
|
55
|
+
return '';
|
|
56
|
+
const q = String(prompt || '').trim();
|
|
57
|
+
if (q.length < 3)
|
|
58
|
+
return '';
|
|
59
|
+
if (!storeExists(sessionId, workingDir))
|
|
60
|
+
return '';
|
|
61
|
+
warmEmbedder();
|
|
62
|
+
// Only use the embedder if it's already loaded — never block the turn on a cold load.
|
|
63
|
+
const embed = (__embedderWarmed && process.env.OSBORN_EMBED !== '0') ? (await getEmbedder()) ?? undefined : undefined;
|
|
64
|
+
const { getStorePath } = await import('./session-store.js');
|
|
65
|
+
const db = openStore(getStorePath(sessionId, workingDir));
|
|
66
|
+
let hits;
|
|
67
|
+
try {
|
|
68
|
+
hits = await recall(db, q, { mode: embed ? 'hybrid' : 'keyword', topK: RECALL_TOP_K, embed });
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
db.close();
|
|
72
|
+
}
|
|
73
|
+
if (!hits.length)
|
|
74
|
+
return '';
|
|
75
|
+
const lines = hits.map((h, i) => {
|
|
76
|
+
let body = h.text.replace(/\n{3,}/g, '\n\n').trim();
|
|
77
|
+
if (body.length > RECALL_PER_HIT_CHARS)
|
|
78
|
+
body = body.slice(0, RECALL_PER_HIT_CHARS) + ' …';
|
|
79
|
+
const src = `${h.source} L${h.lineNum} · ${h.msgType}${h.toolName ? `:${h.toolName}` : ''}`;
|
|
80
|
+
return `[${i + 1}] (${src})\n${body}`;
|
|
81
|
+
});
|
|
82
|
+
// Static wrapper is centralized + parameterized in ./prompts/recalled-context.md.
|
|
83
|
+
return getRecalledContextBlock(lines.join('\n\n'));
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return ''; // recall is best-effort — never break the turn
|
|
87
|
+
}
|
|
88
|
+
}
|
|
23
89
|
// ≤3 direct tool call budget per turn. Reset on every UserPromptSubmit (new user message).
|
|
24
90
|
// Enforced mechanically in PreToolUse — the model CANNOT exceed this regardless of JSONL history.
|
|
25
91
|
// Task/Agent delegations are exempt (delegation is what we WANT). Sub-agent tool calls
|
|
@@ -201,6 +267,7 @@ export const NAMED_AGENTS = {
|
|
|
201
267
|
'GROUNDED: reads the session search-index for prior findings before researching.',
|
|
202
268
|
].join(' '),
|
|
203
269
|
tools: ['Read', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch', 'Task'],
|
|
270
|
+
grounded: true, // applyGrounding() injects the osborn-recall command + ensures Bash
|
|
204
271
|
model: 'sonnet',
|
|
205
272
|
prompt: [
|
|
206
273
|
'You are Osborn\'s research agent. Your job is information gathering — thorough, structured, factual.',
|
|
@@ -209,8 +276,7 @@ export const NAMED_AGENTS = {
|
|
|
209
276
|
'Gather information the main agent needs to answer the user\'s question or make a decision.',
|
|
210
277
|
'You are a scout — go find things, read them carefully, and report back.',
|
|
211
278
|
'',
|
|
212
|
-
'
|
|
213
|
-
'Before researching, locate the session index (search-index.txt — a compact line-per-message log of this mission, under .claude/projects/<slug>/osb/<session>/; if several exist pick the most recently modified) and Grep it for the topic you are about to investigate. Read ONLY the matching slice, never the whole file.',
|
|
279
|
+
'Before researching, GROUND yourself using the recall command in the Grounding section appended to this prompt — check prior findings, decisions, and gotchas so you do not redo settled work.',
|
|
214
280
|
'Purpose: find what has ALREADY been decided, answered, or ruled out so you do not re-research a settled question. If the index already establishes the answer, report that (with the index reference) instead of redoing the work.',
|
|
215
281
|
'If you cannot find the index, proceed normally — this is an optimization, not a hard dependency.',
|
|
216
282
|
'',
|
|
@@ -303,6 +369,7 @@ export const NAMED_AGENTS = {
|
|
|
303
369
|
'GROUNDED: reads the session search-index before editing to avoid contradicting prior decisions.',
|
|
304
370
|
].join(' '),
|
|
305
371
|
tools: ['Read', 'Write', 'Edit', 'MultiEdit', 'Bash', 'Glob', 'Grep', 'NotebookRead', 'NotebookEdit'],
|
|
372
|
+
grounded: true, // applyGrounding() injects the osborn-recall command
|
|
306
373
|
model: 'opus',
|
|
307
374
|
prompt: [
|
|
308
375
|
'You are Osborn\'s writer agent. You execute file changes with a verify-first approach.',
|
|
@@ -311,8 +378,7 @@ export const NAMED_AGENTS = {
|
|
|
311
378
|
'Handle ALL file operations — code, config, documentation, scripts, data files.',
|
|
312
379
|
'You are the only agent that writes. The main agent and reasoner produce plans; you execute them.',
|
|
313
380
|
'',
|
|
314
|
-
'
|
|
315
|
-
'Before editing, locate the session index (search-index.txt under .claude/projects/<slug>/osb/<session>/; newest if several) and Grep it ONLY for: (a) the files/symbols you are about to change, and (b) any recorded DECISIONS or known GOTCHAS relevant to this change. Read only the matching lines — do NOT read the whole index (thousands of lines). This is a lighter dose than the reviewer: a targeted lookup.',
|
|
381
|
+
'Before editing, GROUND yourself using the recall command in the Grounding section appended to this prompt — check for prior DECISIONS and known GOTCHAS on the files/symbols you are about to change.',
|
|
316
382
|
'If a decision or gotcha contradicts your task, STOP and report to the main agent before editing. If you find nothing or no index exists, proceed normally.',
|
|
317
383
|
'',
|
|
318
384
|
'## VERIFY-FIRST workflow (mandatory)',
|
|
@@ -367,7 +433,7 @@ export const NAMED_AGENTS = {
|
|
|
367
433
|
'observed behavior without a matching requirement is a regression — treat behavioral surprise as a defect.',
|
|
368
434
|
'',
|
|
369
435
|
'## Grounding — consult shared context before writing or running tests',
|
|
370
|
-
'Before deciding what to test,
|
|
436
|
+
'Before deciding what to test, check project docs and known-issues files (these live under the working dir and ARE readable). Key doc locations to consult: `/workspace/osborn/CLAUDE.md`, `/workspace/osborn/docs/critical-patterns.md`, the `docs/` directory, `README.md`, and `CHANGELOG.md`. Check these for: (a) KNOWN ISSUES and gotchas already recorded, and (b) what behavior is ALREADY covered by existing tests. (You are adversarial and deliberately NOT given session recall — validate with fresh eyes.)',
|
|
371
437
|
'Purpose: target regression coverage at real GAPS and known-risk areas rather than testing blind or duplicating coverage — and stay IN SYNC with the reviewer, which reads the same sources.',
|
|
372
438
|
'Read only the relevant slice of the index, never the whole file. If you find nothing or no index/docs exist, proceed normally — this is an optimization, not a hard dependency.',
|
|
373
439
|
'If the change adds or renames a feature, flag any doc now out of date (see DOC STALENESS in "What to return").',
|
|
@@ -450,6 +516,7 @@ export const NAMED_AGENTS = {
|
|
|
450
516
|
'GROUNDED: reads the session search-index before planning to respect prior decisions.',
|
|
451
517
|
].join(' '),
|
|
452
518
|
tools: ['Read', 'Glob', 'Grep', 'WebSearch'],
|
|
519
|
+
grounded: true, // applyGrounding() injects the osborn-recall command + adds Bash
|
|
453
520
|
model: 'sonnet',
|
|
454
521
|
prompt: [
|
|
455
522
|
'You are Osborn\'s planning agent. Your job is to decompose complex tasks into clear, atomic steps.',
|
|
@@ -459,7 +526,7 @@ export const NAMED_AGENTS = {
|
|
|
459
526
|
'step by step without guessing. You are the bridge between "what" and "how".',
|
|
460
527
|
'',
|
|
461
528
|
'## Grounding — plan against what already exists',
|
|
462
|
-
'Before drafting a plan,
|
|
529
|
+
'Before drafting a plan, GROUND yourself using the recall command in the Grounding section appended to this prompt — check prior DECISIONS, constraints, and known GOTCHAS relevant to the task.',
|
|
463
530
|
'Purpose: make the plan fit what has already been decided or tried — do not propose an approach the mission already ruled out. If a prior decision conflicts with the obvious plan, surface it in the plan rather than silently contradicting it.',
|
|
464
531
|
'If you cannot find the index, proceed normally.',
|
|
465
532
|
'',
|
|
@@ -630,6 +697,47 @@ export function applyTurbo(agents, turbo) {
|
|
|
630
697
|
}
|
|
631
698
|
return out;
|
|
632
699
|
}
|
|
700
|
+
/**
|
|
701
|
+
* Inject session-recall grounding into any agent flagged `grounded: true`.
|
|
702
|
+
*
|
|
703
|
+
* WHY: a sub-agent's file tools (Read/Grep/Glob) are sandboxed to its cwd +
|
|
704
|
+
* additionalDirectories, so it CANNOT read the session index / session.db that live
|
|
705
|
+
* under $HOME/.claude/projects/… — the old "grep search-index.txt" grounding silently
|
|
706
|
+
* failed. Bash, however, is NOT cwd-restricted, so `osborn-recall` reaches the store.
|
|
707
|
+
*
|
|
708
|
+
* So for every grounded agent we (1) hand it the EXACT, absolute-path osborn-recall
|
|
709
|
+
* command (resolved once here — the single dynamic resolver, so we never hardcode a
|
|
710
|
+
* per-agent path; works for named AND user-created custom grounded agents), and
|
|
711
|
+
* (2) guarantee Bash is in its tool set so it can run that command. The `grounded`
|
|
712
|
+
* flag is stripped before the roster reaches the SDK. NEVER mutates the input.
|
|
713
|
+
*
|
|
714
|
+
* Adversarial agents (reviewer/tester) leave `grounded` unset → untouched, stay blind.
|
|
715
|
+
*/
|
|
716
|
+
export function applyGrounding(agents, sessionId, workingDir) {
|
|
717
|
+
const out = {};
|
|
718
|
+
// Resolve the store command ONCE — absolute --db path, cwd-independent.
|
|
719
|
+
const dbPath = (sessionId && sessionId !== 'pending' && workingDir)
|
|
720
|
+
? getStorePath(sessionId, workingDir) : null;
|
|
721
|
+
for (const [name, agent] of Object.entries(agents)) {
|
|
722
|
+
if (!agent?.grounded) {
|
|
723
|
+
out[name] = agent;
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
const { grounded, ...rest } = agent;
|
|
727
|
+
// Ensure Bash is available so the agent can actually run osborn-recall.
|
|
728
|
+
const tools = Array.isArray(rest.tools) ? [...rest.tools] : [];
|
|
729
|
+
if (!tools.includes('Bash'))
|
|
730
|
+
tools.push('Bash');
|
|
731
|
+
// Push the grounding block into the system prompt (arrives at spawn — sandbox-proof).
|
|
732
|
+
const cmd = dbPath
|
|
733
|
+
? `osborn-recall "<terms from your task>" --db ${dbPath} --top-k 8`
|
|
734
|
+
: `osborn-recall "<terms from your task>" --top-k 8`;
|
|
735
|
+
// Body is centralized + parameterized in ./prompts/grounding-recall.md.
|
|
736
|
+
const groundingBlock = getGroundingBlock(cmd);
|
|
737
|
+
out[name] = { ...rest, tools, prompt: `${rest.prompt || ''}\n${groundingBlock}` };
|
|
738
|
+
}
|
|
739
|
+
return out;
|
|
740
|
+
}
|
|
633
741
|
const RESEARCH_TOOLS = [
|
|
634
742
|
'Read', 'Write', 'Edit', 'Glob', 'Grep',
|
|
635
743
|
'Bash', 'WebSearch', 'WebFetch',
|
|
@@ -714,6 +822,11 @@ export class ClaudeLLM extends llm.LLM {
|
|
|
714
822
|
// Dedup guard — prevents double-firing reviewer/gate if SubagentStop fires
|
|
715
823
|
// more than once for the same agent_id (e.g. retry edge cases).
|
|
716
824
|
#dispatchedFor = new Set();
|
|
825
|
+
// Embedded session.db write-through guard. The store write is triggered from the
|
|
826
|
+
// UserPromptSubmit hook (once per real user submission, main-thread only) and sweeps
|
|
827
|
+
// the FULL source set — main JSONL + every sub-agent JSONL — exactly like the flat
|
|
828
|
+
// index. Incremental (byte-offset resume) + fire-and-forget, so it never blocks a turn.
|
|
829
|
+
#storeUpdating = false;
|
|
717
830
|
// Turbo mode — when true, every spawned agent (main + sub-agents) runs on
|
|
718
831
|
// FAST_MODEL regardless of individual model config. Default off = no-op.
|
|
719
832
|
#turbo = false;
|
|
@@ -806,6 +919,34 @@ export class ClaudeLLM extends llm.LLM {
|
|
|
806
919
|
// ============================================================
|
|
807
920
|
// MCP SERVER MANAGEMENT - Runtime enable/disable MCP servers
|
|
808
921
|
// ============================================================
|
|
922
|
+
/**
|
|
923
|
+
* Guarded, fire-and-forget write-through to the embedded session.db. Called from the
|
|
924
|
+
* main agent's UserPromptSubmit hook (once per real user submission). Sweeps the FULL
|
|
925
|
+
* source set — main JSONL + every sub-agent JSONL — incrementally (byte-offset resume).
|
|
926
|
+
* The guard lives here (on the long-lived ClaudeLLM, not the per-turn stream) so a slow
|
|
927
|
+
* write can't overlap the next turn's write. Never throws; never blocks the caller.
|
|
928
|
+
*/
|
|
929
|
+
triggerStoreUpdate(sessionId, workingDir) {
|
|
930
|
+
if (this.#storeUpdating || !sessionId || sessionId === 'pending' || !workingDir)
|
|
931
|
+
return;
|
|
932
|
+
if (process.env.OSBORN_STORE === '0')
|
|
933
|
+
return;
|
|
934
|
+
this.#storeUpdating = true;
|
|
935
|
+
(async () => {
|
|
936
|
+
try {
|
|
937
|
+
const embed = process.env.OSBORN_EMBED === '0' ? undefined : (await getEmbedder()) ?? undefined;
|
|
938
|
+
const stats = await updateSessionStore(sessionId, workingDir, { embed });
|
|
939
|
+
if (stats.newRows > 0)
|
|
940
|
+
console.log(`🗄️ [store] +${stats.newRows} rows (${stats.totalRows} total, embedded=${stats.embeddedRows}) across main+subagents`);
|
|
941
|
+
}
|
|
942
|
+
catch (err) {
|
|
943
|
+
console.error('🗄️ [store] update failed:', err?.message);
|
|
944
|
+
}
|
|
945
|
+
finally {
|
|
946
|
+
this.#storeUpdating = false;
|
|
947
|
+
}
|
|
948
|
+
})();
|
|
949
|
+
}
|
|
809
950
|
/**
|
|
810
951
|
* Get all currently enabled MCP servers
|
|
811
952
|
*/
|
|
@@ -1920,11 +2061,31 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
1920
2061
|
turnToolCallCount = 0;
|
|
1921
2062
|
const reminder = readFileSync(TURN_SHAPE_REMINDER_PATH, 'utf-8');
|
|
1922
2063
|
const promptPreview = String(input?.prompt || '').substring(0, 60).replace(/\n/g, ' ');
|
|
1923
|
-
|
|
2064
|
+
// Reliable recall: retrieve prior messages relevant to THIS prompt and inject
|
|
2065
|
+
// them deterministically. MAIN CONDUCTOR ONLY — gate explicitly on agent_id.
|
|
2066
|
+
// Per the SDK: agent_id is present ONLY when a hook fires from within a subagent,
|
|
2067
|
+
// absent on the main thread. So `agent_id` set ⇒ skip (grounded sub-agents pull
|
|
2068
|
+
// via osborn-recall from their own prompt; adversarial ones stay un-grounded).
|
|
2069
|
+
const fromSubagent = Boolean(input?.agent_id);
|
|
2070
|
+
const sid = input?.session_id || this.#sessionId;
|
|
2071
|
+
// Consolidated WRITE trigger: on every real user submission (main thread only),
|
|
2072
|
+
// sweep main + ALL sub-agent JSONLs into session.db. Naturally debounced to the
|
|
2073
|
+
// user's speech cadence (one submission = one sweep); guarded + fire-and-forget
|
|
2074
|
+
// on the long-lived ClaudeLLM so injection below never waits on it. This is the
|
|
2075
|
+
// single canonical write path — mirrors the flat index's sub-agent sweep, but
|
|
2076
|
+
// stores FULL untruncated text + FTS5 + sqlite-vec instead of truncated summaries.
|
|
2077
|
+
if (!fromSubagent && sid && this.#opts.workingDirectory) {
|
|
2078
|
+
this.#llmRef.triggerStoreUpdate(sid, this.#opts.workingDirectory);
|
|
2079
|
+
}
|
|
2080
|
+
const recalled = fromSubagent
|
|
2081
|
+
? ''
|
|
2082
|
+
: await buildRecallInjection(sid, this.#opts.workingDirectory, String(input?.prompt || ''));
|
|
2083
|
+
const additionalContext = recalled ? `${reminder}\n\n${recalled}` : reminder;
|
|
2084
|
+
console.log(`📌 UserPromptSubmit: injected turn-shape reminder (${reminder.length} chars)${recalled ? ` + recall (${recalled.length} chars)` : ''} for prompt="${promptPreview}..." [tool budget reset to 0/${TOOL_CALL_BUDGET}]`);
|
|
1924
2085
|
return {
|
|
1925
2086
|
hookSpecificOutput: {
|
|
1926
2087
|
hookEventName: 'UserPromptSubmit',
|
|
1927
|
-
additionalContext
|
|
2088
|
+
additionalContext,
|
|
1928
2089
|
},
|
|
1929
2090
|
};
|
|
1930
2091
|
}
|
|
@@ -2159,9 +2320,9 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
2159
2320
|
// opts.agents is undefined — the ?? NAMED_AGENTS fallback would skip
|
|
2160
2321
|
// the override entirely. Explicitly apply applyTurbo(NAMED_AGENTS)
|
|
2161
2322
|
// so built-in agents always get FAST_MODEL when turbo is on.
|
|
2162
|
-
agents: this.#llmRef.turbo
|
|
2323
|
+
agents: applyGrounding(this.#llmRef.turbo
|
|
2163
2324
|
? applyTurbo(this.#opts.agents ?? NAMED_AGENTS, true)
|
|
2164
|
-
: (this.#opts.agents ?? NAMED_AGENTS),
|
|
2325
|
+
: (this.#opts.agents ?? NAMED_AGENTS), this.#sessionId, this.#opts.workingDirectory),
|
|
2165
2326
|
};
|
|
2166
2327
|
// Run Claude Agent SDK query() and stream results
|
|
2167
2328
|
let hasOutput = false;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* embedder.ts — Local text→vector embedder for the semantic (vec) layer of session-store.
|
|
3
|
+
*
|
|
4
|
+
* Uses all-MiniLM-L6-v2 (384-dim) via @xenova/transformers, running fully local (no API,
|
|
5
|
+
* no per-write network cost). Output is L2-normalized float32, quantized to int8[384]
|
|
6
|
+
* (×127) to match the sqlite-vec `int8[384]` column — 4× smaller than float32 with
|
|
7
|
+
* ~98–99% recall.
|
|
8
|
+
*
|
|
9
|
+
* DESIGN FOR RELIABILITY:
|
|
10
|
+
* • Lazy — the model is loaded on first use, never at import (startup stays fast).
|
|
11
|
+
* • Best-effort — if the package or model can't load, getEmbedder() returns null and
|
|
12
|
+
* the store runs keyword-only. Embeddings never block the keyword write path.
|
|
13
|
+
* • Gated — set OSBORN_EMBED=0 to force keyword-only (e.g. on machines without the
|
|
14
|
+
* model cached, or to avoid the first-run model download).
|
|
15
|
+
*
|
|
16
|
+
* Model cache honors TRANSFORMERS_CACHE / HF_HOME so it can be baked into the image.
|
|
17
|
+
*/
|
|
18
|
+
import { type Embedder } from './session-store.js';
|
|
19
|
+
/**
|
|
20
|
+
* Returns an Embedder, or null if embeddings are unavailable/disabled.
|
|
21
|
+
* The returned function itself also degrades to null on runtime failure.
|
|
22
|
+
*/
|
|
23
|
+
export declare function getEmbedder(): Promise<Embedder | null>;
|
package/dist/embedder.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* embedder.ts — Local text→vector embedder for the semantic (vec) layer of session-store.
|
|
3
|
+
*
|
|
4
|
+
* Uses all-MiniLM-L6-v2 (384-dim) via @xenova/transformers, running fully local (no API,
|
|
5
|
+
* no per-write network cost). Output is L2-normalized float32, quantized to int8[384]
|
|
6
|
+
* (×127) to match the sqlite-vec `int8[384]` column — 4× smaller than float32 with
|
|
7
|
+
* ~98–99% recall.
|
|
8
|
+
*
|
|
9
|
+
* DESIGN FOR RELIABILITY:
|
|
10
|
+
* • Lazy — the model is loaded on first use, never at import (startup stays fast).
|
|
11
|
+
* • Best-effort — if the package or model can't load, getEmbedder() returns null and
|
|
12
|
+
* the store runs keyword-only. Embeddings never block the keyword write path.
|
|
13
|
+
* • Gated — set OSBORN_EMBED=0 to force keyword-only (e.g. on machines without the
|
|
14
|
+
* model cached, or to avoid the first-run model download).
|
|
15
|
+
*
|
|
16
|
+
* Model cache honors TRANSFORMERS_CACHE / HF_HOME so it can be baked into the image.
|
|
17
|
+
*/
|
|
18
|
+
import { EMBED_DIM } from './session-store.js';
|
|
19
|
+
const MODEL_ID = process.env.OSBORN_EMBED_MODEL || 'Xenova/all-MiniLM-L6-v2';
|
|
20
|
+
let pipelinePromise = null;
|
|
21
|
+
let disabled = false;
|
|
22
|
+
/** Quantize a normalized float32 embedding to int8[dim] (×127, clamped). */
|
|
23
|
+
function quantizeInt8(floats) {
|
|
24
|
+
const out = new Int8Array(EMBED_DIM);
|
|
25
|
+
const n = Math.min(EMBED_DIM, floats.length);
|
|
26
|
+
for (let i = 0; i < n; i++) {
|
|
27
|
+
let v = Math.round(floats[i] * 127);
|
|
28
|
+
if (v > 127)
|
|
29
|
+
v = 127;
|
|
30
|
+
else if (v < -128)
|
|
31
|
+
v = -128;
|
|
32
|
+
out[i] = v;
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
async function loadPipeline() {
|
|
37
|
+
if (disabled)
|
|
38
|
+
return null;
|
|
39
|
+
if (process.env.OSBORN_EMBED === '0') {
|
|
40
|
+
disabled = true;
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
if (!pipelinePromise) {
|
|
44
|
+
pipelinePromise = (async () => {
|
|
45
|
+
// Lazy import — @xenova/transformers is heavy and optional.
|
|
46
|
+
const mod = await import('@xenova/transformers').catch(() => null);
|
|
47
|
+
if (!mod)
|
|
48
|
+
throw new Error('@xenova/transformers not installed');
|
|
49
|
+
if (mod.env) {
|
|
50
|
+
mod.env.allowLocalModels = true;
|
|
51
|
+
// Avoid noisy multi-thread wasm issues in the agent process.
|
|
52
|
+
if (mod.env.backends?.onnx?.wasm)
|
|
53
|
+
mod.env.backends.onnx.wasm.numThreads = 1;
|
|
54
|
+
}
|
|
55
|
+
return mod.pipeline('feature-extraction', MODEL_ID);
|
|
56
|
+
})().catch((err) => {
|
|
57
|
+
disabled = true;
|
|
58
|
+
pipelinePromise = null;
|
|
59
|
+
throw err;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return pipelinePromise;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Returns an Embedder, or null if embeddings are unavailable/disabled.
|
|
66
|
+
* The returned function itself also degrades to null on runtime failure.
|
|
67
|
+
*/
|
|
68
|
+
export async function getEmbedder() {
|
|
69
|
+
if (process.env.OSBORN_EMBED === '0')
|
|
70
|
+
return null;
|
|
71
|
+
let pipe;
|
|
72
|
+
try {
|
|
73
|
+
pipe = await loadPipeline();
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
if (!pipe)
|
|
79
|
+
return null;
|
|
80
|
+
const embed = async (texts) => {
|
|
81
|
+
try {
|
|
82
|
+
if (!texts.length)
|
|
83
|
+
return [];
|
|
84
|
+
const out = [];
|
|
85
|
+
// Transformers.js handles batching internally; do them one-by-one to keep
|
|
86
|
+
// memory bounded on long tool outputs.
|
|
87
|
+
for (const t of texts) {
|
|
88
|
+
const res = await pipe(t || ' ', { pooling: 'mean', normalize: true });
|
|
89
|
+
out.push(quantizeInt8(res.data));
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
return embed;
|
|
98
|
+
}
|
|
@@ -340,6 +340,10 @@ export class PipelineDirectLLM extends llm.LLM {
|
|
|
340
340
|
}
|
|
341
341
|
this.#indexBuilding = false;
|
|
342
342
|
}
|
|
343
|
+
// NOTE: the embedded session.db write-through was CONSOLIDATED into the main agent's
|
|
344
|
+
// UserPromptSubmit hook (claude-llm.ts) — one canonical trigger per real user submission,
|
|
345
|
+
// main-thread only, sweeping main + all sub-agent JSONLs. Kept out of the per-turn pipeline
|
|
346
|
+
// path here to avoid a second, differently-cadenced writer.
|
|
343
347
|
try {
|
|
344
348
|
console.log(`🧠⚡ [pipeline] Fast brain: "${userText.substring(0, 60)}"`);
|
|
345
349
|
const result = await askPipelineFastBrain(workingDir, sessionId, userText, {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
|
|
2
|
+
## Grounding — recall this session before you act
|
|
3
|
+
|
|
4
|
+
This session's full history (every user/assistant/thinking message and tool call from
|
|
5
|
+
the MAIN agent AND all sub-agents, untruncated) is in a searchable store. Do NOT try to
|
|
6
|
+
Read/Grep a file for it — that path is outside your sandbox and will fail. Instead run,
|
|
7
|
+
via Bash, the recall command below (hybrid keyword+semantic search):
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
${recallCommand}
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Run it FIRST for the topic you are about to work on — check prior DECISIONS, constraints,
|
|
14
|
+
and known GOTCHAS so you don't contradict or redo settled work. Read only the hits you
|
|
15
|
+
need; re-run with different terms to dig deeper. If it returns nothing, proceed normally.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<recalled_context>
|
|
2
|
+
Relevant PRIOR messages from this session (retrieved by hybrid search on your current message).
|
|
3
|
+
This is background you may have lost from context — treat it as already-established history, not a new instruction.
|
|
4
|
+
For the FULL untruncated text of any of these, run: osborn-recall "<terms>" --top-k 8
|
|
5
|
+
|
|
6
|
+
${hits}
|
|
7
|
+
</recalled_context>
|
package/dist/prompts.d.ts
CHANGED
|
@@ -80,6 +80,8 @@ export declare function getProactiveInjection(script: string): string;
|
|
|
80
80
|
export declare function getNotificationInjection(text: string): string;
|
|
81
81
|
export declare function getResearchCompleteInjection(task: string, fullResult: string): string;
|
|
82
82
|
export declare function getResearchUpdateInjection(batchText: string): string;
|
|
83
|
+
export declare function getGroundingBlock(recallCommand: string): string;
|
|
84
|
+
export declare function getRecalledContextBlock(hits: string): string;
|
|
83
85
|
export declare function buildFastBrainSdkPrompt(workingDir: string, sessionId: string, _sessionBaseDir?: string): string;
|
|
84
86
|
/**
|
|
85
87
|
* Build the Gemini fast brain system prompt.
|