osborn 0.9.231 → 0.9.233
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/dist/claude-llm.js +18 -4
- package/dist/index.js +6 -0
- package/dist/prompts/direct-mode-research.md +3 -3
- package/dist/session-store.js +12 -2
- package/package.json +1 -1
package/dist/claude-llm.js
CHANGED
|
@@ -58,7 +58,13 @@ async function buildRecallInjection(sessionId, workingDir, prompt) {
|
|
|
58
58
|
// fetch, not a local model load). If embed is null, recall() runs keyword-only.
|
|
59
59
|
const embed = process.env.OSBORN_EMBED !== '0' ? (await getEmbedder(QUERY_EMBED_TIMEOUT_MS)) ?? undefined : undefined;
|
|
60
60
|
const { getStorePath } = await import('./session-store.js');
|
|
61
|
-
|
|
61
|
+
// READ-ONLY open (regression fix): the writable branch runs synchronous
|
|
62
|
+
// migration/DDL + meta writes every turn AND lock-contends with the concurrent
|
|
63
|
+
// ingest writer (triggerStoreUpdate) — under WAL those serialize, stacking the
|
|
64
|
+
// 5s busy_timeout per statement and blowing the UserPromptSubmit hook past 30s.
|
|
65
|
+
// A readonly connection takes only SHARED locks and never blocks. storeExists()
|
|
66
|
+
// above guarantees the DB is present, so readonly can't fail on a missing file.
|
|
67
|
+
const db = openStore(getStorePath(sessionId, workingDir), { readonly: true });
|
|
62
68
|
let hits;
|
|
63
69
|
try {
|
|
64
70
|
hits = await recall(db, q, { mode: embed ? 'hybrid' : 'keyword', topK: RECALL_TOP_K, embed });
|
|
@@ -896,9 +902,12 @@ export const VERIFIER_CHAIN = {
|
|
|
896
902
|
'at most 2 fix rounds, after which surface the outcome to the user rather than looping.',
|
|
897
903
|
},
|
|
898
904
|
researcher: {
|
|
899
|
-
afterReminder: 'The researcher sub-agent just finished. Before you finalize, use the Task tool to
|
|
900
|
-
'`reasoner`
|
|
901
|
-
'question
|
|
905
|
+
afterReminder: 'The researcher sub-agent just finished. Before you finalize, you MUST now use the Task tool to ' +
|
|
906
|
+
'dispatch the `reasoner` GATE to judge whether the research is complete and well-sourced for the ' +
|
|
907
|
+
'original question — it returns GATE: PASS or GATE: NEEDS-MORE. ' +
|
|
908
|
+
'If it returns NEEDS-MORE, dispatch the `researcher` again with the gaps it named and then re-gate — ' +
|
|
909
|
+
'at most 2 rounds, after which surface the outcome to the user rather than looping. ' +
|
|
910
|
+
"This is the research analogue of writer→reviewer: don't rely on the findings until an independent agent has vetted them.",
|
|
902
911
|
},
|
|
903
912
|
};
|
|
904
913
|
const RESEARCH_TOOLS = [
|
|
@@ -1627,6 +1636,11 @@ export class ClaudeLLM extends llm.LLM {
|
|
|
1627
1636
|
else {
|
|
1628
1637
|
console.error('❌ Persistent session error:', error);
|
|
1629
1638
|
callbacks.eventEmitter.emit('tts_say', { text: 'Sorry, I encountered an error.' });
|
|
1639
|
+
// Surface the failure to any listener (background slots wire this to a
|
|
1640
|
+
// background_session_error → frontend). Without it a cold-start crash here
|
|
1641
|
+
// is swallowed and the "Spawning…" UI hangs forever. Harmless for the main
|
|
1642
|
+
// agent (no consumer_error listener; it already spoke the error above).
|
|
1643
|
+
callbacks.eventEmitter.emit('consumer_error', { message: String(error?.message || error) });
|
|
1630
1644
|
}
|
|
1631
1645
|
}
|
|
1632
1646
|
finally {
|
package/dist/index.js
CHANGED
|
@@ -2259,6 +2259,12 @@ async function main() {
|
|
|
2259
2259
|
console.error(`❌ Background slot ${slotId.substring(0, 8)}: resume failed — requested ${requestedSessionId?.substring(0, 8)} got ${actualSessionId?.substring(0, 8)}`);
|
|
2260
2260
|
sendToFrontend({ type: 'background_session_error', slotId, error: 'Session resume failed' }).catch(() => { });
|
|
2261
2261
|
});
|
|
2262
|
+
// Cold-start crash in the background consumer (claude-llm #startBackgroundConsumer
|
|
2263
|
+
// catch): surface it so the frontend's "Spawning…" clears instead of hanging forever.
|
|
2264
|
+
bgEmitter.on('consumer_error', ({ message }) => {
|
|
2265
|
+
console.error(`❌ Background slot ${slotId.substring(0, 8)}: consumer error — ${message}`);
|
|
2266
|
+
sendToFrontend({ type: 'background_session_error', slotId, error: String(message || 'Background session failed to start') }).catch(() => { });
|
|
2267
|
+
});
|
|
2262
2268
|
// P1: a real dispatch finished — notify the frontend. The [BACKGROUND_INIT]
|
|
2263
2269
|
// ack also fires assistant_result, so gate on dispatchInFlight to skip it.
|
|
2264
2270
|
bgEmitter.on('assistant_result', ({ text }) => {
|
|
@@ -113,9 +113,9 @@ THE SUB-AGENTS:
|
|
|
113
113
|
· NEVER use the SDK's built-in 'general-purpose' agent — it is not configured for this project and will hit write blocks. Always pick researcher, reasoner, writer, tester, or reviewer explicitly.
|
|
114
114
|
|
|
115
115
|
DIVISION OF LABOR — follow this chain by default for any substantive or code task:
|
|
116
|
-
researcher (gather facts) → planner (step plan for multi-step work) → THEN dispatch writer AND tester IN THE SAME STEP, in parallel: the writer executes the plan while the tester writes tests from the spec, blind to the implementation (TDD) → after the writer returns, RUN the tester's tests against the new code AND dispatch the reviewer on the diff → on test-FAIL or REJECT, send the writer back to fix and re-verify (at most 2 rounds) → you synthesize and speak.
|
|
117
|
-
For a quick factual
|
|
118
|
-
NEVER skip the tester+reviewer
|
|
116
|
+
researcher (gather facts) → after the researcher returns on a substantive question, dispatch the `reasoner` GATE to judge whether the research is complete and well-sourced (GATE: PASS / NEEDS-MORE); on NEEDS-MORE send the researcher back with the named gaps and re-gate (at most 2 rounds) → planner (step plan for multi-step work) → THEN dispatch writer AND tester IN THE SAME STEP, in parallel: the writer executes the plan while the tester writes tests from the spec, blind to the implementation (TDD) → after the writer returns, RUN the tester's tests against the new code AND dispatch the reviewer on the diff → on test-FAIL or REJECT, send the writer back to fix and re-verify (at most 2 rounds) → you synthesize and speak.
|
|
117
|
+
For a quick factual lookup: researcher only → you speak (a trivial lookup needs no gate).
|
|
118
|
+
NEVER skip verification — it is symmetric across both flows: the reasoner GATE after substantive research, and the tester+reviewer after a code change. Both are the same pattern (generate → an INDEPENDENT agent verifies before you rely on it). Dispatch the tester IN PARALLEL with the writer (not after) so tests are written independently of the code.
|
|
119
119
|
You may OVERRIDE this chain at any time — stop a running agent, inject, or reorder — because you can see each task's live state. The chain is the default, not a cage.
|
|
120
120
|
Surfacing findings and communicating to the user is YOUR job, not a sub-agent's.
|
|
121
121
|
</turn-shape>
|
package/dist/session-store.js
CHANGED
|
@@ -525,8 +525,18 @@ export async function recall(db, query, opts) {
|
|
|
525
525
|
const qvecs = await opts.embed([query]);
|
|
526
526
|
const qv = qvecs?.[0];
|
|
527
527
|
if (qv) {
|
|
528
|
-
|
|
529
|
-
|
|
528
|
+
try {
|
|
529
|
+
// sqlite-vec KNN REQUIRES an explicit `k = ?` bound; relying on LIMIT alone can
|
|
530
|
+
// degrade to a brute-force full scan over every int8[EMBED_DIM] row — a multi-second
|
|
531
|
+
// synchronous stall on a large session. `k = pool` caps the KNN search itself.
|
|
532
|
+
// Wrapped like the keyword leg: a vec0 version/index mismatch degrades to
|
|
533
|
+
// keyword-only rather than throwing out of recall() for callers without an outer guard.
|
|
534
|
+
const rows = db.prepare('SELECT rowid AS id FROM vec WHERE embedding MATCH vec_int8(?) AND k = ? ORDER BY distance').all(Buffer.from(qv.buffer, qv.byteOffset, qv.byteLength), pool);
|
|
535
|
+
rows.forEach((r, i) => vec.set(r.id, i));
|
|
536
|
+
}
|
|
537
|
+
catch {
|
|
538
|
+
// vec query failed (stale index / dim mismatch) — ignore vector leg, keep keyword
|
|
539
|
+
}
|
|
530
540
|
}
|
|
531
541
|
}
|
|
532
542
|
// ── Reciprocal Rank Fusion ──
|