osborn 0.9.226 → 0.9.227
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/embedder.js +33 -10
- package/dist/session-store.js +52 -19
- package/package.json +1 -1
package/dist/embedder.js
CHANGED
|
@@ -25,7 +25,16 @@ const OPENROUTER_URL = process.env.OPENROUTER_EMBED_URL || 'https://openrouter.a
|
|
|
25
25
|
const MODEL_ID = EMBED_MODEL;
|
|
26
26
|
const MAX_BATCH = 64; // inputs per request (OpenAI-compatible arrays)
|
|
27
27
|
const REQUEST_TIMEOUT_MS = Number(process.env.OSBORN_EMBED_TIMEOUT_MS || 20_000);
|
|
28
|
-
let
|
|
28
|
+
let lastEmbedWarn = 0;
|
|
29
|
+
/** Throttled failure log (max ~1/30s) so a bulk re-embed can't spam, but embedded=0 is
|
|
30
|
+
* never silent — every failure surfaces a reason (status/network) in the machine logs. */
|
|
31
|
+
function embedWarn(msg) {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
if (now - lastEmbedWarn > 30_000) {
|
|
34
|
+
lastEmbedWarn = now;
|
|
35
|
+
console.warn(`[embedder] ${msg}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
29
38
|
function apiKey() {
|
|
30
39
|
return process.env.OPENROUTER_API_KEY || process.env.OPENROUTER_WORKSPACE_API_KEY;
|
|
31
40
|
}
|
|
@@ -72,26 +81,35 @@ async function embedBatch(inputs, key) {
|
|
|
72
81
|
}),
|
|
73
82
|
signal: controller.signal,
|
|
74
83
|
});
|
|
75
|
-
if (!res.ok)
|
|
84
|
+
if (!res.ok) {
|
|
85
|
+
const body = await res.text().catch(() => '');
|
|
86
|
+
embedWarn(`OpenRouter ${res.status} ${res.statusText}: ${body.slice(0, 200)}`);
|
|
76
87
|
return null;
|
|
88
|
+
}
|
|
77
89
|
const json = await res.json();
|
|
78
90
|
const data = json?.data;
|
|
79
|
-
if (!Array.isArray(data) || !data.length)
|
|
91
|
+
if (!Array.isArray(data) || !data.length) {
|
|
92
|
+
embedWarn('empty/invalid embeddings response');
|
|
80
93
|
return null;
|
|
94
|
+
}
|
|
81
95
|
// OpenAI-compatible: each entry carries its input `index`; align by it.
|
|
96
|
+
// Fall back to array order if `index` is absent so a missing field never voids the batch.
|
|
82
97
|
const byIndex = new Array(inputs.length).fill(null);
|
|
83
|
-
|
|
84
|
-
const idx = typeof d?.index === 'number' ? d.index :
|
|
98
|
+
data.forEach((d, pos) => {
|
|
99
|
+
const idx = typeof d?.index === 'number' ? d.index : pos;
|
|
85
100
|
const emb = d?.embedding;
|
|
86
101
|
if (idx >= 0 && idx < inputs.length && Array.isArray(emb)) {
|
|
87
102
|
byIndex[idx] = normalizeAndQuantize(emb);
|
|
88
103
|
}
|
|
104
|
+
});
|
|
105
|
+
if (byIndex.some((v) => v == null)) {
|
|
106
|
+
embedWarn('response missing/misaligned embeddings');
|
|
107
|
+
return null;
|
|
89
108
|
}
|
|
90
|
-
if (byIndex.some((v) => v == null))
|
|
91
|
-
return null; // malformed / partial response
|
|
92
109
|
return byIndex;
|
|
93
110
|
}
|
|
94
|
-
catch {
|
|
111
|
+
catch (err) {
|
|
112
|
+
embedWarn(`request failed: ${err?.name || ''} ${err?.message || err}`);
|
|
95
113
|
return null; // network error, timeout/abort, bad JSON — degrade to keyword-only
|
|
96
114
|
}
|
|
97
115
|
finally {
|
|
@@ -103,11 +121,16 @@ async function embedBatch(inputs, key) {
|
|
|
103
121
|
* The returned function degrades to null on any per-call failure (keyword-only recall).
|
|
104
122
|
*/
|
|
105
123
|
export async function getEmbedder() {
|
|
106
|
-
if (process.env.OSBORN_EMBED === '0'
|
|
124
|
+
if (process.env.OSBORN_EMBED === '0')
|
|
107
125
|
return null;
|
|
126
|
+
// Re-read the key EVERY call and never latch off on a missing key: the platform
|
|
127
|
+
// OpenRouter key is hydrated into the env by the secrets layer, which can finish
|
|
128
|
+
// AFTER the first (boot-time) warm call. A permanent latch there disabled embedding
|
|
129
|
+
// for the whole session (embedded=0) even though fast-brain — which reads the key
|
|
130
|
+
// fresh per request — kept working. Re-reading recovers as soon as the key lands.
|
|
108
131
|
const key = apiKey();
|
|
109
132
|
if (!key) {
|
|
110
|
-
|
|
133
|
+
embedWarn('no OPENROUTER_API_KEY in env yet — keyword-only for this call');
|
|
111
134
|
return null;
|
|
112
135
|
}
|
|
113
136
|
const embed = async (texts) => {
|
package/dist/session-store.js
CHANGED
|
@@ -256,12 +256,22 @@ function countLines(filePath, upToBytes) {
|
|
|
256
256
|
return c;
|
|
257
257
|
}
|
|
258
258
|
/**
|
|
259
|
-
* Max bytes ingested from a
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
* the single Node event loop and starved the SDK's time-boxed UserPromptSubmit hook.
|
|
259
|
+
* Max bytes ingested from a SINGLE JSONL per sweep. Kept small so each synchronous
|
|
260
|
+
* read+parse+gzip+FTS-insert block is short (~sub-300ms) and never stutters realtime
|
|
261
|
+
* voice (VAD) between event-loop yields.
|
|
263
262
|
*/
|
|
264
|
-
const MAX_INGEST_BYTES =
|
|
263
|
+
const MAX_INGEST_BYTES = 1 * 1024 * 1024;
|
|
264
|
+
/**
|
|
265
|
+
* Max bytes ingested across ALL sources in one turn. This is the cap that actually
|
|
266
|
+
* matters: a session can have HUNDREDS of subagent JSONL files, and on a store
|
|
267
|
+
* migration the offsets reset so every source re-ingests from 0 at once. The old
|
|
268
|
+
* code looped all sources inside ONE synchronous db.transaction — hundreds of MB of
|
|
269
|
+
* synchronous work that pegged the single Node thread for minutes and starved VAD,
|
|
270
|
+
* the SDK's UserPromptSubmit hook (30s timeout), and fast-brain. Bounding the total
|
|
271
|
+
* per turn (and yielding between sources) drains large sessions over many turns
|
|
272
|
+
* without ever monopolizing the loop.
|
|
273
|
+
*/
|
|
274
|
+
const MAX_INGEST_BYTES_PER_TURN = 4 * 1024 * 1024;
|
|
265
275
|
/**
|
|
266
276
|
* Ingest new bytes from a single JSONL file into the store.
|
|
267
277
|
* Reads only from `fromOffset` forward, at most `maxBytes` per call. Returns rows
|
|
@@ -370,21 +380,44 @@ export async function updateSessionStore(sessionId, workingDir, opts) {
|
|
|
370
380
|
const upSrc = db.prepare(`INSERT INTO sources(source, jsonl_path, byte_offset, line_count) VALUES (@source,@path,@off,@lines)
|
|
371
381
|
ON CONFLICT(source) DO UPDATE SET jsonl_path=@path, byte_offset=@off, line_count=@lines`);
|
|
372
382
|
const freshRows = [];
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
lines: (prev?.line_count ?? 0) + res.linesProcessed,
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
freshRows.push(...res.inserted);
|
|
383
|
+
let budget = MAX_INGEST_BYTES_PER_TURN;
|
|
384
|
+
let sinceYield = 0;
|
|
385
|
+
for (const s of sources) {
|
|
386
|
+
// Yield to the event loop periodically even across drained sources, so iterating
|
|
387
|
+
// a large source list (statSync + SELECT each) can't itself stall voice/hook.
|
|
388
|
+
if (++sinceYield >= 8) {
|
|
389
|
+
sinceYield = 0;
|
|
390
|
+
await new Promise((r) => setImmediate(r));
|
|
385
391
|
}
|
|
386
|
-
|
|
387
|
-
|
|
392
|
+
if (budget <= 0)
|
|
393
|
+
break; // per-turn cap reached — remaining sources drain next turn
|
|
394
|
+
const prev = getSrc.get(s.source);
|
|
395
|
+
const fromOffset = prev?.byte_offset ?? 0;
|
|
396
|
+
const cap = Math.min(budget, MAX_INGEST_BYTES);
|
|
397
|
+
let res;
|
|
398
|
+
try {
|
|
399
|
+
// Per-source transaction (not one giant txn over all sources) so we can yield
|
|
400
|
+
// between them and never hold the thread for the whole fleet of subagent files.
|
|
401
|
+
res = db.transaction(() => ingestFile(db, s.path, s.source, fromOffset, cap))();
|
|
402
|
+
}
|
|
403
|
+
catch {
|
|
404
|
+
continue; // a bad source never blocks the rest
|
|
405
|
+
}
|
|
406
|
+
if (res.inserted.length || res.newOffset !== fromOffset) {
|
|
407
|
+
upSrc.run({
|
|
408
|
+
source: s.source, path: s.path, off: res.newOffset,
|
|
409
|
+
lines: (prev?.line_count ?? 0) + res.linesProcessed,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
const consumed = res.newOffset - fromOffset;
|
|
413
|
+
budget -= consumed;
|
|
414
|
+
freshRows.push(...res.inserted);
|
|
415
|
+
// Yield after any source that did real synchronous work.
|
|
416
|
+
if (consumed > 0) {
|
|
417
|
+
sinceYield = 0;
|
|
418
|
+
await new Promise((r) => setImmediate(r));
|
|
419
|
+
}
|
|
420
|
+
}
|
|
388
421
|
opts?.onProgress?.(`content: +${freshRows.length} rows across ${sources.length} sources`);
|
|
389
422
|
// ── Embeddings (best-effort, never blocks the keyword layer) ──
|
|
390
423
|
let embeddedRows = 0;
|