osborn 0.9.232 → 0.9.234
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 +24 -4
- package/dist/index.js +6 -0
- 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 });
|
|
@@ -1577,8 +1583,15 @@ export class ClaudeLLM extends llm.LLM {
|
|
|
1577
1583
|
if (msg.requestId) {
|
|
1578
1584
|
callbacks.eventEmitter.emit('query_request_id', { requestId: msg.requestId });
|
|
1579
1585
|
}
|
|
1580
|
-
// Stream assistant text → tts_say events
|
|
1581
|
-
|
|
1586
|
+
// Stream assistant text → tts_say events.
|
|
1587
|
+
// SUBAGENT GUARD (regression fix): the SDK surfaces a Task sub-agent's assistant
|
|
1588
|
+
// messages inline in THIS same stream, tagged with a non-null parent_tool_use_id
|
|
1589
|
+
// ("non-null when the message was produced inside a subagent" — SDK docs). Only the
|
|
1590
|
+
// MAIN orchestrator (parent_tool_use_id === null) may speak or post a chat bubble;
|
|
1591
|
+
// a sub-agent's raw narration must NOT reach TTS/UI (its work already shows via the
|
|
1592
|
+
// tool_use activity events). Without this, once verifiers/researcher became real Task
|
|
1593
|
+
// sub-agents their text "took over the mic" and leaked raw code/markdown into audio.
|
|
1594
|
+
if (msg.type === 'assistant' && msg.message?.content && !msg.parent_tool_use_id) {
|
|
1582
1595
|
// Assign a stable messageId for this turn (first block sets it, rest reuse)
|
|
1583
1596
|
if (!this.#currentTurnMessageId) {
|
|
1584
1597
|
this.#currentTurnMessageId = crypto.randomUUID();
|
|
@@ -1630,6 +1643,11 @@ export class ClaudeLLM extends llm.LLM {
|
|
|
1630
1643
|
else {
|
|
1631
1644
|
console.error('❌ Persistent session error:', error);
|
|
1632
1645
|
callbacks.eventEmitter.emit('tts_say', { text: 'Sorry, I encountered an error.' });
|
|
1646
|
+
// Surface the failure to any listener (background slots wire this to a
|
|
1647
|
+
// background_session_error → frontend). Without it a cold-start crash here
|
|
1648
|
+
// is swallowed and the "Spawning…" UI hangs forever. Harmless for the main
|
|
1649
|
+
// agent (no consumer_error listener; it already spoke the error above).
|
|
1650
|
+
callbacks.eventEmitter.emit('consumer_error', { message: String(error?.message || error) });
|
|
1633
1651
|
}
|
|
1634
1652
|
}
|
|
1635
1653
|
finally {
|
|
@@ -2380,7 +2398,9 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
2380
2398
|
this.#onCheckpoint(checkpointId);
|
|
2381
2399
|
}
|
|
2382
2400
|
// Stream text chunks — send each assistant text block to TTS
|
|
2383
|
-
|
|
2401
|
+
// SUBAGENT GUARD (see the persistent-consumer loop above): skip sub-agent messages
|
|
2402
|
+
// (non-null parent_tool_use_id) so only the main orchestrator's text speaks/streams.
|
|
2403
|
+
if (message.type === 'assistant' && message.message?.content && !message.parent_tool_use_id) {
|
|
2384
2404
|
// Emit SDK requestId on first assistant message — identifies this query()
|
|
2385
2405
|
// in the JSONL for tracking which research task produced which output
|
|
2386
2406
|
const sdkRequestId = message.requestId;
|
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 }) => {
|
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 ──
|