@securecoms/agent-bridge 0.28.1 → 0.28.2
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/README.md +16 -0
- package/dist/bin.js +121 -36
- package/dist/bridge-agent.d.ts +6 -0
- package/dist/embedder.js +2 -1
- package/dist/index.js +121 -36
- package/dist/memory/embedder/client.d.ts +20 -0
- package/dist/memory/embedder/protocol.d.ts +5 -0
- package/dist/memory/recall/recall-book.d.ts +12 -2
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -798,6 +798,22 @@ If you embed the bridge as a library (`new BridgeAgent(cfg, deps)`), semantic re
|
|
|
798
798
|
|
|
799
799
|
---
|
|
800
800
|
|
|
801
|
+
## Release notes: 0.28.2
|
|
802
|
+
|
|
803
|
+
<!-- [COPY, NEW] this section. Approved by Eric 2026-10-09 ("approve 0.28.2 copy"). -->
|
|
804
|
+
|
|
805
|
+
0.28.2 keeps semantic memory recall working after quiet periods. Install it in
|
|
806
|
+
place of 0.28.1 and restart the bridge. `@securecoms/agent-runtime` 0.11.0 is
|
|
807
|
+
unchanged.
|
|
808
|
+
|
|
809
|
+
- **After a quiet period.** When the agent has been idle for 30 minutes or
|
|
810
|
+
more, the first private message now waits for the memory search inside the
|
|
811
|
+
same time limit (600 ms typed, 250 ms in a voice call) instead of skipping it.
|
|
812
|
+
If the limit runs out, that message has no recall, and the next one does.
|
|
813
|
+
- **Idle is not an error.** The memory helper process still stops after 30 idle
|
|
814
|
+
minutes to free memory. The bridge no longer logs or reports that as
|
|
815
|
+
unavailable.
|
|
816
|
+
|
|
801
817
|
## Release notes: 0.28.1
|
|
802
818
|
|
|
803
819
|
<!-- [COPY, NEW] this section. Gate: Eric sign-off before npm publish (release PR). -->
|
package/dist/bin.js
CHANGED
|
@@ -30085,6 +30085,7 @@ init_manifest();
|
|
|
30085
30085
|
// src/memory/embedder/protocol.ts
|
|
30086
30086
|
import { StringDecoder } from "node:string_decoder";
|
|
30087
30087
|
var EMBEDDER_PROTOCOL = 1;
|
|
30088
|
+
var EMBEDDER_IDLE_MS = 30 * 6e4;
|
|
30088
30089
|
var MAX_LINE_BYTES = 69632;
|
|
30089
30090
|
function decodeVec64(s) {
|
|
30090
30091
|
const b = Buffer.from(s, "base64");
|
|
@@ -30476,6 +30477,7 @@ var INERT_EMBEDDER = Object.freeze({
|
|
|
30476
30477
|
ensureStarted: () => {
|
|
30477
30478
|
},
|
|
30478
30479
|
filesPresent: () => false,
|
|
30480
|
+
ready: () => Promise.resolve(false),
|
|
30479
30481
|
embed: () => Promise.reject(new Error("inert embedder"))
|
|
30480
30482
|
});
|
|
30481
30483
|
var SERVER_CODES = /* @__PURE__ */ new Set(["busy", "too_long", "worker_reset", "failed"]);
|
|
@@ -30505,6 +30507,10 @@ var EmbedderClient = class {
|
|
|
30505
30507
|
verifyAttempt = 0;
|
|
30506
30508
|
fails = 0;
|
|
30507
30509
|
closed = false;
|
|
30510
|
+
/** When this client last sent a job (or connected, if later): the idle-stop test (cold-path ruling 2026-10-09). */
|
|
30511
|
+
lastActivity = -Infinity;
|
|
30512
|
+
/** ready(ms) waiters: settled on the next transition out of "preparing". */
|
|
30513
|
+
readyWaiters = /* @__PURE__ */ new Set();
|
|
30508
30514
|
m;
|
|
30509
30515
|
now;
|
|
30510
30516
|
log;
|
|
@@ -30512,6 +30518,24 @@ var EmbedderClient = class {
|
|
|
30512
30518
|
state() {
|
|
30513
30519
|
return this.st;
|
|
30514
30520
|
}
|
|
30521
|
+
/** Cold-path ruling (2026-10-09): see Embedder.ready. Pure like state(): never starts, connects or spawns anything. */
|
|
30522
|
+
ready(ms) {
|
|
30523
|
+
if (this.st.state === "ready") return Promise.resolve(true);
|
|
30524
|
+
if (this.st.state !== "preparing" || !(ms > 0)) return Promise.resolve(false);
|
|
30525
|
+
return new Promise((resolve3) => {
|
|
30526
|
+
let t;
|
|
30527
|
+
const w = (ok) => {
|
|
30528
|
+
clearTimeout(t);
|
|
30529
|
+
this.readyWaiters.delete(w);
|
|
30530
|
+
resolve3(ok);
|
|
30531
|
+
};
|
|
30532
|
+
this.readyWaiters.add(w);
|
|
30533
|
+
t = setTimeout(() => w(false), ms);
|
|
30534
|
+
});
|
|
30535
|
+
}
|
|
30536
|
+
settleReady(ok) {
|
|
30537
|
+
for (const w of [...this.readyWaiters]) w(ok);
|
|
30538
|
+
}
|
|
30515
30539
|
tcp() {
|
|
30516
30540
|
return (this.o.platform ?? process.platform) === "win32";
|
|
30517
30541
|
}
|
|
@@ -30580,6 +30604,24 @@ var EmbedderClient = class {
|
|
|
30580
30604
|
for (const p of this.pending.values()) p.rej(new Error(reason));
|
|
30581
30605
|
this.pending.clear();
|
|
30582
30606
|
if (reason !== "verify_failed" && reason !== "embedder_crashed") this.notBefore = Math.max(this.notBefore, this.now() + reconnectDelayMs(++this.fails));
|
|
30607
|
+
this.settleReady(false);
|
|
30608
|
+
}
|
|
30609
|
+
/**
|
|
30610
|
+
* Rule 6, cold-path ruling (2026-10-09): the served socket closed. With no job pending and this client's last job (or
|
|
30611
|
+
* its connect, if later) at least the shared idle constant ago, it is the shared embedder's idle stop, which is not a
|
|
30612
|
+
* failure: back to `off`, no `fails++`, no `notBefore`, no unavailable report, so the next turn's ensureStarted starts
|
|
30613
|
+
* it at once. Anything else (a job pending, or recent use) stays a failure with its backoff.
|
|
30614
|
+
*/
|
|
30615
|
+
onServedClose() {
|
|
30616
|
+
if (this.closed) return;
|
|
30617
|
+
if (this.pending.size > 0 || this.now() - this.lastActivity < EMBEDDER_IDLE_MS) return this.fail("embedder_unavailable");
|
|
30618
|
+
this.log("info", "memory.embedder_idle_closed");
|
|
30619
|
+
this.st = { state: "off" };
|
|
30620
|
+
const s = this.sock;
|
|
30621
|
+
this.sock = null;
|
|
30622
|
+
this.chan = null;
|
|
30623
|
+
s?.destroy();
|
|
30624
|
+
this.settleReady(false);
|
|
30583
30625
|
}
|
|
30584
30626
|
ensureStarted() {
|
|
30585
30627
|
if (this.closed || this.st.state === "preparing" || this.st.state === "ready") return;
|
|
@@ -30791,9 +30833,11 @@ var EmbedderClient = class {
|
|
|
30791
30833
|
this.st = { state: "ready" };
|
|
30792
30834
|
this.fails = 0;
|
|
30793
30835
|
this.starts = [];
|
|
30836
|
+
this.lastActivity = this.now();
|
|
30794
30837
|
s.on("close", () => {
|
|
30795
|
-
if (this.sock === s) this.
|
|
30838
|
+
if (this.sock === s) this.onServedClose();
|
|
30796
30839
|
});
|
|
30840
|
+
this.settleReady(true);
|
|
30797
30841
|
done(true);
|
|
30798
30842
|
} catch {
|
|
30799
30843
|
refuse("embedder_unavailable");
|
|
@@ -30827,6 +30871,7 @@ var EmbedderClient = class {
|
|
|
30827
30871
|
const id = ++this.seq;
|
|
30828
30872
|
const s = this.sock;
|
|
30829
30873
|
const chan = this.chan;
|
|
30874
|
+
this.lastActivity = this.now();
|
|
30830
30875
|
return new Promise((res, rej) => {
|
|
30831
30876
|
const signal = opts.signal;
|
|
30832
30877
|
const onAbort = () => {
|
|
@@ -30855,6 +30900,7 @@ var EmbedderClient = class {
|
|
|
30855
30900
|
for (const p of this.pending.values()) p.rej(new Error("closed"));
|
|
30856
30901
|
this.pending.clear();
|
|
30857
30902
|
this.st = { state: "off" };
|
|
30903
|
+
this.settleReady(false);
|
|
30858
30904
|
}
|
|
30859
30905
|
};
|
|
30860
30906
|
|
|
@@ -30934,9 +30980,12 @@ var RecallBook = class _RecallBook {
|
|
|
30934
30980
|
static IDLE_MS = 30 * 6e4;
|
|
30935
30981
|
static TOP_K = 5;
|
|
30936
30982
|
static TEXT_MAX_CHARS = 2e3;
|
|
30983
|
+
/** About 1.5 ms of real opens at the measured ~40 µs per row: the longest a warm holds the event loop. */
|
|
30984
|
+
static WARM_YIELD_ROWS = 32;
|
|
30937
30985
|
caches = /* @__PURE__ */ new Map();
|
|
30938
30986
|
entries = /* @__PURE__ */ new Map();
|
|
30939
|
-
|
|
30987
|
+
/** The warm in flight per conversation: a cold turn awaits the same warm instead of starting a second one. */
|
|
30988
|
+
warming = /* @__PURE__ */ new Map();
|
|
30940
30989
|
same(a, b) {
|
|
30941
30990
|
return a.clearGeneration === b.clearGeneration && a.modeEpoch === b.modeEpoch && a.purgeEpoch === b.purgeEpoch && a.contentEpoch === b.contentEpoch;
|
|
30942
30991
|
}
|
|
@@ -30970,15 +31019,23 @@ var RecallBook = class _RecallBook {
|
|
|
30970
31019
|
* re-read rows (ids gone are dropped, the few new ids are opened here, under the mutex) and installed with the
|
|
30971
31020
|
* re-read capture.
|
|
30972
31021
|
*/
|
|
30973
|
-
|
|
30974
|
-
|
|
30975
|
-
|
|
31022
|
+
warm(conversationId) {
|
|
31023
|
+
const inFlight = this.warming.get(conversationId);
|
|
31024
|
+
if (inFlight) return inFlight;
|
|
31025
|
+
const w = this.warmOnce(conversationId).finally(() => {
|
|
31026
|
+
if (this.warming.get(conversationId) === w) this.warming.delete(conversationId);
|
|
31027
|
+
});
|
|
31028
|
+
this.warming.set(conversationId, w);
|
|
31029
|
+
return w;
|
|
31030
|
+
}
|
|
31031
|
+
async warmOnce(conversationId) {
|
|
30976
31032
|
try {
|
|
30977
31033
|
const snap = this.d.store.readRecallRows(conversationId);
|
|
30978
31034
|
const rows = snap.rows.filter((r) => r.modelId === this.d.modelId);
|
|
30979
31035
|
const opened = /* @__PURE__ */ new Map();
|
|
30980
31036
|
const failed = /* @__PURE__ */ new Set();
|
|
30981
|
-
for (const r of rows) {
|
|
31037
|
+
for (const [i, r] of rows.entries()) {
|
|
31038
|
+
if (i > 0 && i % _RecallBook.WARM_YIELD_ROWS === 0) await new Promise((r2) => setImmediate(r2));
|
|
30982
31039
|
const v = await this.d.store.openRecallVec(conversationId, r);
|
|
30983
31040
|
if (!v || v.length !== DIMS) {
|
|
30984
31041
|
failed.add(r.recallId);
|
|
@@ -31020,46 +31077,61 @@ var RecallBook = class _RecallBook {
|
|
|
31020
31077
|
if (unreadable > 0) (this.d.log ?? writeServiceLog)("warn", "memory.recall_open_failed", { count: unreadable });
|
|
31021
31078
|
} catch (e) {
|
|
31022
31079
|
(this.d.log ?? writeServiceLog)("warn", "memory.recall_warm_failed", {}, e);
|
|
31023
|
-
} finally {
|
|
31024
|
-
this.warming.delete(conversationId);
|
|
31025
31080
|
}
|
|
31026
31081
|
}
|
|
31027
31082
|
/**
|
|
31028
31083
|
* §7.1: compute at ingest. Snapshot the capture FIRST (H2); `receipt`
|
|
31029
31084
|
* replaces its purge and mode epochs with the ones captured when the turn
|
|
31030
31085
|
* was received (Task 16 fix H-2), so a purge or a mode change after receipt
|
|
31031
|
-
* refuses too.
|
|
31032
|
-
*
|
|
31086
|
+
* refuses too.
|
|
31087
|
+
*
|
|
31088
|
+
* Cold-path ruling (architect, 2026-10-09; §7.1 and §7.3 errata): ONE deadline, start + `timeoutMs` (the caller
|
|
31089
|
+
* passes its turn kind's bound: 600 ms text, 250 ms voice), covers the ready wait, the warm and the query embed
|
|
31090
|
+
* together; no phase has a budget of its own. A cold embedder is started (ensureStarted) and waited for; a cold or
|
|
31091
|
+
* stale cache is warmed and waited for. Past the deadline there is no recall and nothing attaches late: a warm that
|
|
31092
|
+
* outlives it keeps running and may install (warm enforces H2 itself), but this turn gets no entry.
|
|
31033
31093
|
*/
|
|
31034
31094
|
async prepare(p) {
|
|
31035
31095
|
const fresh = this.d.store.recallCapture(p.conversationId);
|
|
31036
31096
|
const capture = p.receipt ? { ...fresh, purgeEpoch: p.receipt.purgeEpoch, modeEpoch: p.receipt.modeEpoch } : fresh;
|
|
31037
31097
|
this.entries.delete(p.messageId);
|
|
31038
|
-
|
|
31039
|
-
|
|
31040
|
-
if (this.d.embedder.state().state !== "ready") {
|
|
31041
|
-
this.d.embedder.ensureStarted();
|
|
31042
|
-
return;
|
|
31043
|
-
}
|
|
31044
|
-
const cache = this.caches.get(p.conversationId);
|
|
31045
|
-
if (!cache || !this.same(cache.capture, capture)) {
|
|
31046
|
-
void this.warm(p.conversationId);
|
|
31047
|
-
return;
|
|
31048
|
-
}
|
|
31049
|
-
cache.lastUse = now;
|
|
31098
|
+
this.dropIdleCaches(this.nowMs());
|
|
31099
|
+
const deadlineAt = Date.now() + p.timeoutMs;
|
|
31050
31100
|
const ac = new AbortController();
|
|
31101
|
+
let late = false;
|
|
31051
31102
|
let timer;
|
|
31052
|
-
const
|
|
31053
|
-
|
|
31054
|
-
|
|
31055
|
-
|
|
31056
|
-
|
|
31057
|
-
|
|
31058
|
-
|
|
31059
|
-
|
|
31060
|
-
|
|
31061
|
-
|
|
31062
|
-
|
|
31103
|
+
const expired = new Promise((r) => {
|
|
31104
|
+
timer = setTimeout(() => {
|
|
31105
|
+
late = true;
|
|
31106
|
+
ac.abort();
|
|
31107
|
+
r("expired");
|
|
31108
|
+
}, p.timeoutMs);
|
|
31109
|
+
});
|
|
31110
|
+
try {
|
|
31111
|
+
if (this.d.embedder.state().state !== "ready") {
|
|
31112
|
+
this.d.embedder.ensureStarted();
|
|
31113
|
+
if (await Promise.race([this.d.embedder.ready(Math.max(0, deadlineAt - Date.now())).catch(() => false), expired]) !== true) return;
|
|
31114
|
+
}
|
|
31115
|
+
let cache = this.caches.get(p.conversationId);
|
|
31116
|
+
if (!cache || !this.same(cache.capture, capture)) {
|
|
31117
|
+
const cur = this.d.store.recallCapture(p.conversationId);
|
|
31118
|
+
if (cur.clearGeneration !== capture.clearGeneration || cur.modeEpoch !== capture.modeEpoch || cur.purgeEpoch !== capture.purgeEpoch) return;
|
|
31119
|
+
if (await Promise.race([this.warm(p.conversationId).then(() => "warmed"), expired]) !== "warmed") return;
|
|
31120
|
+
cache = this.caches.get(p.conversationId);
|
|
31121
|
+
if (!cache || !this.same(cache.capture, capture)) return;
|
|
31122
|
+
}
|
|
31123
|
+
cache.lastUse = this.nowMs();
|
|
31124
|
+
const q = await Promise.race([
|
|
31125
|
+
this.d.embedder.embed(p.query, 0, { signal: ac.signal }).catch(() => null),
|
|
31126
|
+
expired.then(() => null)
|
|
31127
|
+
]);
|
|
31128
|
+
if (!q || q.length !== DIMS) return;
|
|
31129
|
+
await Promise.race([this.attach(p, capture, cache, q, () => late), expired]);
|
|
31130
|
+
} finally {
|
|
31131
|
+
clearTimeout(timer);
|
|
31132
|
+
}
|
|
31133
|
+
}
|
|
31134
|
+
async attach(p, capture, cache, q, late) {
|
|
31063
31135
|
const ranked = rankRecall(
|
|
31064
31136
|
q,
|
|
31065
31137
|
{ vecs: cache.vecs, dims: DIMS, messageIds: cache.messageIds, recallIds: cache.recallIds },
|
|
@@ -31081,7 +31153,7 @@ var RecallBook = class _RecallBook {
|
|
|
31081
31153
|
items.push({ id: r.recallId, text, sent_at: row.sentAt });
|
|
31082
31154
|
}
|
|
31083
31155
|
if (unreadable > 0) (this.d.log ?? writeServiceLog)("warn", "memory.recall_open_failed", { count: unreadable });
|
|
31084
|
-
if (items.length === 0) return;
|
|
31156
|
+
if (items.length === 0 || late()) return;
|
|
31085
31157
|
if (this.entries.size >= _RecallBook.MAP_MAX) this.entries.delete(this.entries.keys().next().value);
|
|
31086
31158
|
this.entries.set(p.messageId, { conversationId: p.conversationId, capture, items });
|
|
31087
31159
|
}
|
|
@@ -33164,6 +33236,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
33164
33236
|
recallPrewarmPending = false;
|
|
33165
33237
|
/** The DM prewarm in flight (one at a time); close() waits for it (bounded). */
|
|
33166
33238
|
recallPrewarmWork = null;
|
|
33239
|
+
/**
|
|
33240
|
+
* Cold-path ruling B (2026-10-09): conversations whose turn passed Q1/Q2, for the maintenance tick to warm once the
|
|
33241
|
+
* embedder is ready (runRecallPrewarm). Bounded; dropped whenever a tick finds recall off.
|
|
33242
|
+
*/
|
|
33243
|
+
recallPrewarmTargets = /* @__PURE__ */ new Set();
|
|
33244
|
+
static RECALL_PREWARM_TARGETS_MAX = 256;
|
|
33167
33245
|
/**
|
|
33168
33246
|
* Final-review item 2: conversations whose `audience` skip was already logged in this process. A room (or any
|
|
33169
33247
|
* conversation the owner-DM gate refuses) refuses every turn the same way, so the line is written once per
|
|
@@ -35413,14 +35491,19 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
35413
35491
|
* before or during it stops it (I1, Q1). Never throws.
|
|
35414
35492
|
*/
|
|
35415
35493
|
runRecallPrewarm() {
|
|
35416
|
-
if (!this.recallPrewarmPending || this.recallPrewarmWork || this.closingDown) return;
|
|
35494
|
+
if (!this.recallPrewarmPending && this.recallPrewarmTargets.size === 0 || this.recallPrewarmWork || this.closingDown) return;
|
|
35417
35495
|
if (this.embedder.state().state !== "ready") return;
|
|
35496
|
+
const all = this.recallPrewarmPending;
|
|
35418
35497
|
this.recallPrewarmPending = false;
|
|
35498
|
+
const targets = [...this.recallPrewarmTargets];
|
|
35499
|
+
this.recallPrewarmTargets.clear();
|
|
35419
35500
|
this.recallPrewarmWork = (async () => {
|
|
35420
35501
|
try {
|
|
35421
|
-
const
|
|
35502
|
+
const isDm = (c) => !this.vaultChannelIds.has(c) && this.store.getChannelMeta(c)?.isDm === true;
|
|
35503
|
+
const dms = [.../* @__PURE__ */ new Set([...targets, ...all ? this.store.memoryConversationIds() : []])].filter(isDm);
|
|
35422
35504
|
for (const c of dms) {
|
|
35423
35505
|
if (this.closingDown || !this.recallProviderOn()) return;
|
|
35506
|
+
if (this.recall.cacheWarm(c)) continue;
|
|
35424
35507
|
await this.recall.warm(c);
|
|
35425
35508
|
}
|
|
35426
35509
|
} catch (e) {
|
|
@@ -35452,6 +35535,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
35452
35535
|
async prepareRecallForTurn(t) {
|
|
35453
35536
|
try {
|
|
35454
35537
|
if (!this.recallProviderOn() || !this.notesReadable(t.authorization)) return;
|
|
35538
|
+
if (this.recallPrewarmTargets.size < _BridgeAgent.RECALL_PREWARM_TARGETS_MAX) this.recallPrewarmTargets.add(t.channelId);
|
|
35455
35539
|
const marks = this.recallReceipt.get(t.messageId) ?? this.recallStageEpochs.get(t.messageId);
|
|
35456
35540
|
if (marks !== void 0 && (marks.purgeEpoch === void 0 || marks.modeEpoch === void 0)) return;
|
|
35457
35541
|
const receipt = marks === void 0 ? void 0 : { purgeEpoch: marks.purgeEpoch, modeEpoch: marks.modeEpoch };
|
|
@@ -35620,6 +35704,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
35620
35704
|
if (!this.recallProviderOn()) {
|
|
35621
35705
|
this.reembedHot = false;
|
|
35622
35706
|
this.recallPrewarmPending = false;
|
|
35707
|
+
this.recallPrewarmTargets.clear();
|
|
35623
35708
|
return;
|
|
35624
35709
|
}
|
|
35625
35710
|
this.runRecallPrewarm();
|
package/dist/bridge-agent.d.ts
CHANGED
|
@@ -1199,6 +1199,12 @@ export declare class BridgeAgent {
|
|
|
1199
1199
|
private recallPrewarmPending;
|
|
1200
1200
|
/** The DM prewarm in flight (one at a time); close() waits for it (bounded). */
|
|
1201
1201
|
private recallPrewarmWork;
|
|
1202
|
+
/**
|
|
1203
|
+
* Cold-path ruling B (2026-10-09): conversations whose turn passed Q1/Q2, for the maintenance tick to warm once the
|
|
1204
|
+
* embedder is ready (runRecallPrewarm). Bounded; dropped whenever a tick finds recall off.
|
|
1205
|
+
*/
|
|
1206
|
+
private readonly recallPrewarmTargets;
|
|
1207
|
+
private static readonly RECALL_PREWARM_TARGETS_MAX;
|
|
1202
1208
|
/**
|
|
1203
1209
|
* Final-review item 2: conversations whose `audience` skip was already logged in this process. A room (or any
|
|
1204
1210
|
* conversation the owner-DM gate refuses) refuses every turn the same way, so the line is written once per
|
package/dist/embedder.js
CHANGED
|
@@ -58,6 +58,7 @@ import { join as join2 } from "node:path";
|
|
|
58
58
|
import { StringDecoder } from "node:string_decoder";
|
|
59
59
|
var EMBEDDER_PROTOCOL = 1;
|
|
60
60
|
var MAX_TEXT_BYTES = 8192;
|
|
61
|
+
var EMBEDDER_IDLE_MS = 30 * 6e4;
|
|
61
62
|
var MAX_LINE_BYTES = 69632;
|
|
62
63
|
function encodeVec64(v) {
|
|
63
64
|
const b = Buffer.alloc(v.length * 4);
|
|
@@ -1216,7 +1217,7 @@ async function serve(o, log, p, lock, token, published, server, tcp, publish) {
|
|
|
1216
1217
|
let stopped = false;
|
|
1217
1218
|
const touchIdle = () => {
|
|
1218
1219
|
clearTimeout(idleTimer);
|
|
1219
|
-
idleTimer = setTimeout(() => void stop("idle"), o.idleMs ??
|
|
1220
|
+
idleTimer = setTimeout(() => void stop("idle"), o.idleMs ?? EMBEDDER_IDLE_MS);
|
|
1220
1221
|
};
|
|
1221
1222
|
const send = (conn, f2) => {
|
|
1222
1223
|
const s = conns.get(conn);
|
package/dist/index.js
CHANGED
|
@@ -29580,6 +29580,7 @@ var TOKENIZER_OPTIONS = Object.freeze({
|
|
|
29580
29580
|
// src/memory/embedder/protocol.ts
|
|
29581
29581
|
import { StringDecoder } from "node:string_decoder";
|
|
29582
29582
|
var EMBEDDER_PROTOCOL = 1;
|
|
29583
|
+
var EMBEDDER_IDLE_MS = 30 * 6e4;
|
|
29583
29584
|
var MAX_LINE_BYTES = 69632;
|
|
29584
29585
|
function decodeVec64(s) {
|
|
29585
29586
|
const b = Buffer.from(s, "base64");
|
|
@@ -29970,6 +29971,7 @@ var INERT_EMBEDDER = Object.freeze({
|
|
|
29970
29971
|
ensureStarted: () => {
|
|
29971
29972
|
},
|
|
29972
29973
|
filesPresent: () => false,
|
|
29974
|
+
ready: () => Promise.resolve(false),
|
|
29973
29975
|
embed: () => Promise.reject(new Error("inert embedder"))
|
|
29974
29976
|
});
|
|
29975
29977
|
var SERVER_CODES = /* @__PURE__ */ new Set(["busy", "too_long", "worker_reset", "failed"]);
|
|
@@ -29999,6 +30001,10 @@ var EmbedderClient = class {
|
|
|
29999
30001
|
verifyAttempt = 0;
|
|
30000
30002
|
fails = 0;
|
|
30001
30003
|
closed = false;
|
|
30004
|
+
/** When this client last sent a job (or connected, if later): the idle-stop test (cold-path ruling 2026-10-09). */
|
|
30005
|
+
lastActivity = -Infinity;
|
|
30006
|
+
/** ready(ms) waiters: settled on the next transition out of "preparing". */
|
|
30007
|
+
readyWaiters = /* @__PURE__ */ new Set();
|
|
30002
30008
|
m;
|
|
30003
30009
|
now;
|
|
30004
30010
|
log;
|
|
@@ -30006,6 +30012,24 @@ var EmbedderClient = class {
|
|
|
30006
30012
|
state() {
|
|
30007
30013
|
return this.st;
|
|
30008
30014
|
}
|
|
30015
|
+
/** Cold-path ruling (2026-10-09): see Embedder.ready. Pure like state(): never starts, connects or spawns anything. */
|
|
30016
|
+
ready(ms) {
|
|
30017
|
+
if (this.st.state === "ready") return Promise.resolve(true);
|
|
30018
|
+
if (this.st.state !== "preparing" || !(ms > 0)) return Promise.resolve(false);
|
|
30019
|
+
return new Promise((resolve) => {
|
|
30020
|
+
let t;
|
|
30021
|
+
const w = (ok) => {
|
|
30022
|
+
clearTimeout(t);
|
|
30023
|
+
this.readyWaiters.delete(w);
|
|
30024
|
+
resolve(ok);
|
|
30025
|
+
};
|
|
30026
|
+
this.readyWaiters.add(w);
|
|
30027
|
+
t = setTimeout(() => w(false), ms);
|
|
30028
|
+
});
|
|
30029
|
+
}
|
|
30030
|
+
settleReady(ok) {
|
|
30031
|
+
for (const w of [...this.readyWaiters]) w(ok);
|
|
30032
|
+
}
|
|
30009
30033
|
tcp() {
|
|
30010
30034
|
return (this.o.platform ?? process.platform) === "win32";
|
|
30011
30035
|
}
|
|
@@ -30074,6 +30098,24 @@ var EmbedderClient = class {
|
|
|
30074
30098
|
for (const p of this.pending.values()) p.rej(new Error(reason));
|
|
30075
30099
|
this.pending.clear();
|
|
30076
30100
|
if (reason !== "verify_failed" && reason !== "embedder_crashed") this.notBefore = Math.max(this.notBefore, this.now() + reconnectDelayMs(++this.fails));
|
|
30101
|
+
this.settleReady(false);
|
|
30102
|
+
}
|
|
30103
|
+
/**
|
|
30104
|
+
* Rule 6, cold-path ruling (2026-10-09): the served socket closed. With no job pending and this client's last job (or
|
|
30105
|
+
* its connect, if later) at least the shared idle constant ago, it is the shared embedder's idle stop, which is not a
|
|
30106
|
+
* failure: back to `off`, no `fails++`, no `notBefore`, no unavailable report, so the next turn's ensureStarted starts
|
|
30107
|
+
* it at once. Anything else (a job pending, or recent use) stays a failure with its backoff.
|
|
30108
|
+
*/
|
|
30109
|
+
onServedClose() {
|
|
30110
|
+
if (this.closed) return;
|
|
30111
|
+
if (this.pending.size > 0 || this.now() - this.lastActivity < EMBEDDER_IDLE_MS) return this.fail("embedder_unavailable");
|
|
30112
|
+
this.log("info", "memory.embedder_idle_closed");
|
|
30113
|
+
this.st = { state: "off" };
|
|
30114
|
+
const s = this.sock;
|
|
30115
|
+
this.sock = null;
|
|
30116
|
+
this.chan = null;
|
|
30117
|
+
s?.destroy();
|
|
30118
|
+
this.settleReady(false);
|
|
30077
30119
|
}
|
|
30078
30120
|
ensureStarted() {
|
|
30079
30121
|
if (this.closed || this.st.state === "preparing" || this.st.state === "ready") return;
|
|
@@ -30285,9 +30327,11 @@ var EmbedderClient = class {
|
|
|
30285
30327
|
this.st = { state: "ready" };
|
|
30286
30328
|
this.fails = 0;
|
|
30287
30329
|
this.starts = [];
|
|
30330
|
+
this.lastActivity = this.now();
|
|
30288
30331
|
s.on("close", () => {
|
|
30289
|
-
if (this.sock === s) this.
|
|
30332
|
+
if (this.sock === s) this.onServedClose();
|
|
30290
30333
|
});
|
|
30334
|
+
this.settleReady(true);
|
|
30291
30335
|
done(true);
|
|
30292
30336
|
} catch {
|
|
30293
30337
|
refuse("embedder_unavailable");
|
|
@@ -30321,6 +30365,7 @@ var EmbedderClient = class {
|
|
|
30321
30365
|
const id = ++this.seq;
|
|
30322
30366
|
const s = this.sock;
|
|
30323
30367
|
const chan = this.chan;
|
|
30368
|
+
this.lastActivity = this.now();
|
|
30324
30369
|
return new Promise((res, rej) => {
|
|
30325
30370
|
const signal = opts.signal;
|
|
30326
30371
|
const onAbort = () => {
|
|
@@ -30349,6 +30394,7 @@ var EmbedderClient = class {
|
|
|
30349
30394
|
for (const p of this.pending.values()) p.rej(new Error("closed"));
|
|
30350
30395
|
this.pending.clear();
|
|
30351
30396
|
this.st = { state: "off" };
|
|
30397
|
+
this.settleReady(false);
|
|
30352
30398
|
}
|
|
30353
30399
|
};
|
|
30354
30400
|
|
|
@@ -30424,9 +30470,12 @@ var RecallBook = class _RecallBook {
|
|
|
30424
30470
|
static IDLE_MS = 30 * 6e4;
|
|
30425
30471
|
static TOP_K = 5;
|
|
30426
30472
|
static TEXT_MAX_CHARS = 2e3;
|
|
30473
|
+
/** About 1.5 ms of real opens at the measured ~40 µs per row: the longest a warm holds the event loop. */
|
|
30474
|
+
static WARM_YIELD_ROWS = 32;
|
|
30427
30475
|
caches = /* @__PURE__ */ new Map();
|
|
30428
30476
|
entries = /* @__PURE__ */ new Map();
|
|
30429
|
-
|
|
30477
|
+
/** The warm in flight per conversation: a cold turn awaits the same warm instead of starting a second one. */
|
|
30478
|
+
warming = /* @__PURE__ */ new Map();
|
|
30430
30479
|
same(a, b) {
|
|
30431
30480
|
return a.clearGeneration === b.clearGeneration && a.modeEpoch === b.modeEpoch && a.purgeEpoch === b.purgeEpoch && a.contentEpoch === b.contentEpoch;
|
|
30432
30481
|
}
|
|
@@ -30460,15 +30509,23 @@ var RecallBook = class _RecallBook {
|
|
|
30460
30509
|
* re-read rows (ids gone are dropped, the few new ids are opened here, under the mutex) and installed with the
|
|
30461
30510
|
* re-read capture.
|
|
30462
30511
|
*/
|
|
30463
|
-
|
|
30464
|
-
|
|
30465
|
-
|
|
30512
|
+
warm(conversationId) {
|
|
30513
|
+
const inFlight = this.warming.get(conversationId);
|
|
30514
|
+
if (inFlight) return inFlight;
|
|
30515
|
+
const w = this.warmOnce(conversationId).finally(() => {
|
|
30516
|
+
if (this.warming.get(conversationId) === w) this.warming.delete(conversationId);
|
|
30517
|
+
});
|
|
30518
|
+
this.warming.set(conversationId, w);
|
|
30519
|
+
return w;
|
|
30520
|
+
}
|
|
30521
|
+
async warmOnce(conversationId) {
|
|
30466
30522
|
try {
|
|
30467
30523
|
const snap = this.d.store.readRecallRows(conversationId);
|
|
30468
30524
|
const rows = snap.rows.filter((r) => r.modelId === this.d.modelId);
|
|
30469
30525
|
const opened = /* @__PURE__ */ new Map();
|
|
30470
30526
|
const failed = /* @__PURE__ */ new Set();
|
|
30471
|
-
for (const r of rows) {
|
|
30527
|
+
for (const [i, r] of rows.entries()) {
|
|
30528
|
+
if (i > 0 && i % _RecallBook.WARM_YIELD_ROWS === 0) await new Promise((r2) => setImmediate(r2));
|
|
30472
30529
|
const v = await this.d.store.openRecallVec(conversationId, r);
|
|
30473
30530
|
if (!v || v.length !== DIMS) {
|
|
30474
30531
|
failed.add(r.recallId);
|
|
@@ -30510,46 +30567,61 @@ var RecallBook = class _RecallBook {
|
|
|
30510
30567
|
if (unreadable > 0) (this.d.log ?? writeServiceLog2)("warn", "memory.recall_open_failed", { count: unreadable });
|
|
30511
30568
|
} catch (e) {
|
|
30512
30569
|
(this.d.log ?? writeServiceLog2)("warn", "memory.recall_warm_failed", {}, e);
|
|
30513
|
-
} finally {
|
|
30514
|
-
this.warming.delete(conversationId);
|
|
30515
30570
|
}
|
|
30516
30571
|
}
|
|
30517
30572
|
/**
|
|
30518
30573
|
* §7.1: compute at ingest. Snapshot the capture FIRST (H2); `receipt`
|
|
30519
30574
|
* replaces its purge and mode epochs with the ones captured when the turn
|
|
30520
30575
|
* was received (Task 16 fix H-2), so a purge or a mode change after receipt
|
|
30521
|
-
* refuses too.
|
|
30522
|
-
*
|
|
30576
|
+
* refuses too.
|
|
30577
|
+
*
|
|
30578
|
+
* Cold-path ruling (architect, 2026-10-09; §7.1 and §7.3 errata): ONE deadline, start + `timeoutMs` (the caller
|
|
30579
|
+
* passes its turn kind's bound: 600 ms text, 250 ms voice), covers the ready wait, the warm and the query embed
|
|
30580
|
+
* together; no phase has a budget of its own. A cold embedder is started (ensureStarted) and waited for; a cold or
|
|
30581
|
+
* stale cache is warmed and waited for. Past the deadline there is no recall and nothing attaches late: a warm that
|
|
30582
|
+
* outlives it keeps running and may install (warm enforces H2 itself), but this turn gets no entry.
|
|
30523
30583
|
*/
|
|
30524
30584
|
async prepare(p) {
|
|
30525
30585
|
const fresh = this.d.store.recallCapture(p.conversationId);
|
|
30526
30586
|
const capture = p.receipt ? { ...fresh, purgeEpoch: p.receipt.purgeEpoch, modeEpoch: p.receipt.modeEpoch } : fresh;
|
|
30527
30587
|
this.entries.delete(p.messageId);
|
|
30528
|
-
|
|
30529
|
-
|
|
30530
|
-
if (this.d.embedder.state().state !== "ready") {
|
|
30531
|
-
this.d.embedder.ensureStarted();
|
|
30532
|
-
return;
|
|
30533
|
-
}
|
|
30534
|
-
const cache = this.caches.get(p.conversationId);
|
|
30535
|
-
if (!cache || !this.same(cache.capture, capture)) {
|
|
30536
|
-
void this.warm(p.conversationId);
|
|
30537
|
-
return;
|
|
30538
|
-
}
|
|
30539
|
-
cache.lastUse = now;
|
|
30588
|
+
this.dropIdleCaches(this.nowMs());
|
|
30589
|
+
const deadlineAt = Date.now() + p.timeoutMs;
|
|
30540
30590
|
const ac = new AbortController();
|
|
30591
|
+
let late = false;
|
|
30541
30592
|
let timer;
|
|
30542
|
-
const
|
|
30543
|
-
|
|
30544
|
-
|
|
30545
|
-
|
|
30546
|
-
|
|
30547
|
-
|
|
30548
|
-
|
|
30549
|
-
|
|
30550
|
-
|
|
30551
|
-
|
|
30552
|
-
|
|
30593
|
+
const expired = new Promise((r) => {
|
|
30594
|
+
timer = setTimeout(() => {
|
|
30595
|
+
late = true;
|
|
30596
|
+
ac.abort();
|
|
30597
|
+
r("expired");
|
|
30598
|
+
}, p.timeoutMs);
|
|
30599
|
+
});
|
|
30600
|
+
try {
|
|
30601
|
+
if (this.d.embedder.state().state !== "ready") {
|
|
30602
|
+
this.d.embedder.ensureStarted();
|
|
30603
|
+
if (await Promise.race([this.d.embedder.ready(Math.max(0, deadlineAt - Date.now())).catch(() => false), expired]) !== true) return;
|
|
30604
|
+
}
|
|
30605
|
+
let cache = this.caches.get(p.conversationId);
|
|
30606
|
+
if (!cache || !this.same(cache.capture, capture)) {
|
|
30607
|
+
const cur = this.d.store.recallCapture(p.conversationId);
|
|
30608
|
+
if (cur.clearGeneration !== capture.clearGeneration || cur.modeEpoch !== capture.modeEpoch || cur.purgeEpoch !== capture.purgeEpoch) return;
|
|
30609
|
+
if (await Promise.race([this.warm(p.conversationId).then(() => "warmed"), expired]) !== "warmed") return;
|
|
30610
|
+
cache = this.caches.get(p.conversationId);
|
|
30611
|
+
if (!cache || !this.same(cache.capture, capture)) return;
|
|
30612
|
+
}
|
|
30613
|
+
cache.lastUse = this.nowMs();
|
|
30614
|
+
const q = await Promise.race([
|
|
30615
|
+
this.d.embedder.embed(p.query, 0, { signal: ac.signal }).catch(() => null),
|
|
30616
|
+
expired.then(() => null)
|
|
30617
|
+
]);
|
|
30618
|
+
if (!q || q.length !== DIMS) return;
|
|
30619
|
+
await Promise.race([this.attach(p, capture, cache, q, () => late), expired]);
|
|
30620
|
+
} finally {
|
|
30621
|
+
clearTimeout(timer);
|
|
30622
|
+
}
|
|
30623
|
+
}
|
|
30624
|
+
async attach(p, capture, cache, q, late) {
|
|
30553
30625
|
const ranked = rankRecall(
|
|
30554
30626
|
q,
|
|
30555
30627
|
{ vecs: cache.vecs, dims: DIMS, messageIds: cache.messageIds, recallIds: cache.recallIds },
|
|
@@ -30571,7 +30643,7 @@ var RecallBook = class _RecallBook {
|
|
|
30571
30643
|
items.push({ id: r.recallId, text, sent_at: row.sentAt });
|
|
30572
30644
|
}
|
|
30573
30645
|
if (unreadable > 0) (this.d.log ?? writeServiceLog2)("warn", "memory.recall_open_failed", { count: unreadable });
|
|
30574
|
-
if (items.length === 0) return;
|
|
30646
|
+
if (items.length === 0 || late()) return;
|
|
30575
30647
|
if (this.entries.size >= _RecallBook.MAP_MAX) this.entries.delete(this.entries.keys().next().value);
|
|
30576
30648
|
this.entries.set(p.messageId, { conversationId: p.conversationId, capture, items });
|
|
30577
30649
|
}
|
|
@@ -32914,6 +32986,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
32914
32986
|
recallPrewarmPending = false;
|
|
32915
32987
|
/** The DM prewarm in flight (one at a time); close() waits for it (bounded). */
|
|
32916
32988
|
recallPrewarmWork = null;
|
|
32989
|
+
/**
|
|
32990
|
+
* Cold-path ruling B (2026-10-09): conversations whose turn passed Q1/Q2, for the maintenance tick to warm once the
|
|
32991
|
+
* embedder is ready (runRecallPrewarm). Bounded; dropped whenever a tick finds recall off.
|
|
32992
|
+
*/
|
|
32993
|
+
recallPrewarmTargets = /* @__PURE__ */ new Set();
|
|
32994
|
+
static RECALL_PREWARM_TARGETS_MAX = 256;
|
|
32917
32995
|
/**
|
|
32918
32996
|
* Final-review item 2: conversations whose `audience` skip was already logged in this process. A room (or any
|
|
32919
32997
|
* conversation the owner-DM gate refuses) refuses every turn the same way, so the line is written once per
|
|
@@ -35163,14 +35241,19 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
35163
35241
|
* before or during it stops it (I1, Q1). Never throws.
|
|
35164
35242
|
*/
|
|
35165
35243
|
runRecallPrewarm() {
|
|
35166
|
-
if (!this.recallPrewarmPending || this.recallPrewarmWork || this.closingDown) return;
|
|
35244
|
+
if (!this.recallPrewarmPending && this.recallPrewarmTargets.size === 0 || this.recallPrewarmWork || this.closingDown) return;
|
|
35167
35245
|
if (this.embedder.state().state !== "ready") return;
|
|
35246
|
+
const all = this.recallPrewarmPending;
|
|
35168
35247
|
this.recallPrewarmPending = false;
|
|
35248
|
+
const targets = [...this.recallPrewarmTargets];
|
|
35249
|
+
this.recallPrewarmTargets.clear();
|
|
35169
35250
|
this.recallPrewarmWork = (async () => {
|
|
35170
35251
|
try {
|
|
35171
|
-
const
|
|
35252
|
+
const isDm = (c) => !this.vaultChannelIds.has(c) && this.store.getChannelMeta(c)?.isDm === true;
|
|
35253
|
+
const dms = [.../* @__PURE__ */ new Set([...targets, ...all ? this.store.memoryConversationIds() : []])].filter(isDm);
|
|
35172
35254
|
for (const c of dms) {
|
|
35173
35255
|
if (this.closingDown || !this.recallProviderOn()) return;
|
|
35256
|
+
if (this.recall.cacheWarm(c)) continue;
|
|
35174
35257
|
await this.recall.warm(c);
|
|
35175
35258
|
}
|
|
35176
35259
|
} catch (e) {
|
|
@@ -35202,6 +35285,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
35202
35285
|
async prepareRecallForTurn(t) {
|
|
35203
35286
|
try {
|
|
35204
35287
|
if (!this.recallProviderOn() || !this.notesReadable(t.authorization)) return;
|
|
35288
|
+
if (this.recallPrewarmTargets.size < _BridgeAgent.RECALL_PREWARM_TARGETS_MAX) this.recallPrewarmTargets.add(t.channelId);
|
|
35205
35289
|
const marks = this.recallReceipt.get(t.messageId) ?? this.recallStageEpochs.get(t.messageId);
|
|
35206
35290
|
if (marks !== void 0 && (marks.purgeEpoch === void 0 || marks.modeEpoch === void 0)) return;
|
|
35207
35291
|
const receipt = marks === void 0 ? void 0 : { purgeEpoch: marks.purgeEpoch, modeEpoch: marks.modeEpoch };
|
|
@@ -35370,6 +35454,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
35370
35454
|
if (!this.recallProviderOn()) {
|
|
35371
35455
|
this.reembedHot = false;
|
|
35372
35456
|
this.recallPrewarmPending = false;
|
|
35457
|
+
this.recallPrewarmTargets.clear();
|
|
35373
35458
|
return;
|
|
35374
35459
|
}
|
|
35375
35460
|
this.runRecallPrewarm();
|
|
@@ -16,6 +16,12 @@ export interface Embedder {
|
|
|
16
16
|
state(): EmbedderState;
|
|
17
17
|
ensureStarted(): void;
|
|
18
18
|
filesPresent(): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Cold-path ruling (2026-10-09): true at once when ready; while preparing, true on the transition to ready and false
|
|
21
|
+
* on a failure, a stop or after `ms`. False at once in any other state. Never rejects and never starts, connects or
|
|
22
|
+
* spawns anything itself (the caller runs ensureStarted first).
|
|
23
|
+
*/
|
|
24
|
+
ready(ms: number): Promise<boolean>;
|
|
19
25
|
embed(text: string, priority: 0 | 1, opts?: {
|
|
20
26
|
signal?: AbortSignal;
|
|
21
27
|
}): Promise<Float32Array>;
|
|
@@ -64,12 +70,19 @@ export declare class EmbedderClient implements Embedder {
|
|
|
64
70
|
private verifyAttempt;
|
|
65
71
|
private fails;
|
|
66
72
|
private closed;
|
|
73
|
+
/** When this client last sent a job (or connected, if later): the idle-stop test (cold-path ruling 2026-10-09). */
|
|
74
|
+
private lastActivity;
|
|
75
|
+
/** ready(ms) waiters: settled on the next transition out of "preparing". */
|
|
76
|
+
private readonly readyWaiters;
|
|
67
77
|
private readonly m;
|
|
68
78
|
private readonly now;
|
|
69
79
|
private readonly log;
|
|
70
80
|
constructor(o: EmbedderClientOptions);
|
|
71
81
|
/** Pure: the report path reads this, and reading it never starts, connects or spawns anything (plan audit LOW). */
|
|
72
82
|
state(): EmbedderState;
|
|
83
|
+
/** Cold-path ruling (2026-10-09): see Embedder.ready. Pure like state(): never starts, connects or spawns anything. */
|
|
84
|
+
ready(ms: number): Promise<boolean>;
|
|
85
|
+
private settleReady;
|
|
73
86
|
private tcp;
|
|
74
87
|
filesPresent(): boolean;
|
|
75
88
|
private key;
|
|
@@ -87,6 +100,13 @@ export declare class EmbedderClient implements Embedder {
|
|
|
87
100
|
private aclToken;
|
|
88
101
|
private aclOk;
|
|
89
102
|
private fail;
|
|
103
|
+
/**
|
|
104
|
+
* Rule 6, cold-path ruling (2026-10-09): the served socket closed. With no job pending and this client's last job (or
|
|
105
|
+
* its connect, if later) at least the shared idle constant ago, it is the shared embedder's idle stop, which is not a
|
|
106
|
+
* failure: back to `off`, no `fails++`, no `notBefore`, no unavailable report, so the next turn's ensureStarted starts
|
|
107
|
+
* it at once. Anything else (a job pending, or recent use) stays a failure with its backoff.
|
|
108
|
+
*/
|
|
109
|
+
private onServedClose;
|
|
90
110
|
ensureStarted(): void;
|
|
91
111
|
private start;
|
|
92
112
|
/** The embedder's fixed-code status file (exit 3). True when it decided the state. */
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/** Spec §20 E1.2 to E1.4: newline-delimited JSON frames over the local socket; vectors as base64 little-endian float32. */
|
|
2
2
|
export declare const EMBEDDER_PROTOCOL = 1;
|
|
3
3
|
export declare const MAX_TEXT_BYTES = 8192;
|
|
4
|
+
/**
|
|
5
|
+
* The shared embedder stops after this long with no job from any bridge (server `idleMs` default). The client uses the
|
|
6
|
+
* same value to tell that idle stop from a failure (cold-path ruling 2026-10-09): ONE constant, used by both.
|
|
7
|
+
*/
|
|
8
|
+
export declare const EMBEDDER_IDLE_MS: number;
|
|
4
9
|
/**
|
|
5
10
|
* 11b review M4: the longest sealed job line. Worst case for MAX_TEXT_BYTES of text: every byte a control character that
|
|
6
11
|
* JSON escapes as \u00XX (6 bytes) = 49152, plus the frame's other fields (< 100), plus the 24-byte nonce and 16-byte
|
|
@@ -62,8 +62,11 @@ export declare class RecallBook {
|
|
|
62
62
|
private static readonly IDLE_MS;
|
|
63
63
|
private static readonly TOP_K;
|
|
64
64
|
private static readonly TEXT_MAX_CHARS;
|
|
65
|
+
/** About 1.5 ms of real opens at the measured ~40 µs per row: the longest a warm holds the event loop. */
|
|
66
|
+
private static readonly WARM_YIELD_ROWS;
|
|
65
67
|
private readonly caches;
|
|
66
68
|
private readonly entries;
|
|
69
|
+
/** The warm in flight per conversation: a cold turn awaits the same warm instead of starting a second one. */
|
|
67
70
|
private readonly warming;
|
|
68
71
|
private same;
|
|
69
72
|
private nowMs;
|
|
@@ -89,12 +92,18 @@ export declare class RecallBook {
|
|
|
89
92
|
* re-read capture.
|
|
90
93
|
*/
|
|
91
94
|
warm(conversationId: string): Promise<void>;
|
|
95
|
+
private warmOnce;
|
|
92
96
|
/**
|
|
93
97
|
* §7.1: compute at ingest. Snapshot the capture FIRST (H2); `receipt`
|
|
94
98
|
* replaces its purge and mode epochs with the ones captured when the turn
|
|
95
99
|
* was received (Task 16 fix H-2), so a purge or a mode change after receipt
|
|
96
|
-
* refuses too.
|
|
97
|
-
*
|
|
100
|
+
* refuses too.
|
|
101
|
+
*
|
|
102
|
+
* Cold-path ruling (architect, 2026-10-09; §7.1 and §7.3 errata): ONE deadline, start + `timeoutMs` (the caller
|
|
103
|
+
* passes its turn kind's bound: 600 ms text, 250 ms voice), covers the ready wait, the warm and the query embed
|
|
104
|
+
* together; no phase has a budget of its own. A cold embedder is started (ensureStarted) and waited for; a cold or
|
|
105
|
+
* stale cache is warmed and waited for. Past the deadline there is no recall and nothing attaches late: a warm that
|
|
106
|
+
* outlives it keeps running and may install (warm enforces H2 itself), but this turn gets no entry.
|
|
98
107
|
*/
|
|
99
108
|
prepare(p: {
|
|
100
109
|
messageId: string;
|
|
@@ -107,6 +116,7 @@ export declare class RecallBook {
|
|
|
107
116
|
modeEpoch: number;
|
|
108
117
|
};
|
|
109
118
|
}): Promise<void>;
|
|
119
|
+
private attach;
|
|
110
120
|
/** Q3 + Q6: the entry's own conversation, and its capture still current at delivery. Synchronous (ws flush). */
|
|
111
121
|
deliverable(messageId: string, conversationId: string): readonly RecallItem[] | undefined;
|
|
112
122
|
/**
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const BRIDGE_VERSION = "0.28.
|
|
1
|
+
export declare const BRIDGE_VERSION = "0.28.2";
|