osborn 0.9.232 → 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.
@@ -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
- const db = openStore(getStorePath(sessionId, workingDir));
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 });
@@ -1630,6 +1636,11 @@ export class ClaudeLLM extends llm.LLM {
1630
1636
  else {
1631
1637
  console.error('❌ Persistent session error:', error);
1632
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) });
1633
1644
  }
1634
1645
  }
1635
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 }) => {
@@ -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
- const rows = db.prepare('SELECT rowid AS id FROM vec WHERE embedding MATCH vec_int8(?) ORDER BY distance LIMIT ?').all(Buffer.from(qv.buffer, qv.byteOffset, qv.byteLength), pool);
529
- rows.forEach((r, i) => vec.set(r.id, i));
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 ──
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osborn",
3
- "version": "0.9.232",
3
+ "version": "0.9.233",
4
4
  "description": "Voice AI coding assistant - local agent that connects to Osborn frontend",
5
5
  "type": "module",
6
6
  "bin": {