@securecoms/agent-bridge 0.26.6 → 0.28.0

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.
Files changed (54) hide show
  1. package/README.md +119 -0
  2. package/dist/bin.d.ts +24 -1
  3. package/dist/bin.js +4490 -375
  4. package/dist/bridge-agent.d.ts +405 -0
  5. package/dist/config.d.ts +7 -0
  6. package/dist/consumer-grants.d.ts +2 -1
  7. package/dist/embedder-manifest.json +52 -0
  8. package/dist/embedder-worker.js +2898 -0
  9. package/dist/embedder.js +1574 -0
  10. package/dist/index.js +4282 -297
  11. package/dist/memory/consumer-notes.d.ts +34 -0
  12. package/dist/memory/consumer-recall.d.ts +21 -0
  13. package/dist/memory/embedder/client.d.ts +103 -0
  14. package/dist/memory/embedder/download.d.ts +28 -0
  15. package/dist/memory/embedder/embedder-main.d.ts +1 -0
  16. package/dist/memory/embedder/embedder-paths.d.ts +14 -0
  17. package/dist/memory/embedder/engine.d.ts +21 -0
  18. package/dist/memory/embedder/gc.d.ts +2 -0
  19. package/dist/memory/embedder/handshake.d.ts +7 -0
  20. package/dist/memory/embedder/heartbeats.d.ts +4 -0
  21. package/dist/memory/embedder/import-bundle.d.ts +6 -0
  22. package/dist/memory/embedder/inner-source.d.ts +2 -0
  23. package/dist/memory/embedder/layout.d.ts +36 -0
  24. package/dist/memory/embedder/locks.d.ts +17 -0
  25. package/dist/memory/embedder/manifest.d.ts +30 -0
  26. package/dist/memory/embedder/no-dial.d.ts +3 -0
  27. package/dist/memory/embedder/protocol.d.ts +55 -0
  28. package/dist/memory/embedder/run-dir.d.ts +11 -0
  29. package/dist/memory/embedder/scheduler.d.ts +24 -0
  30. package/dist/memory/embedder/server.d.ts +46 -0
  31. package/dist/memory/embedder/session.d.ts +17 -0
  32. package/dist/memory/embedder/spawn.d.ts +40 -0
  33. package/dist/memory/embedder/tokenizer.d.ts +41 -0
  34. package/dist/memory/embedder/types.d.ts +32 -0
  35. package/dist/memory/embedder/windows.d.ts +46 -0
  36. package/dist/memory/embedder/worker-body.d.ts +44 -0
  37. package/dist/memory/embedder/worker.d.ts +1 -0
  38. package/dist/memory/memory-config.d.ts +23 -0
  39. package/dist/memory/note-frames.d.ts +47 -0
  40. package/dist/memory/note-provenance.d.ts +40 -0
  41. package/dist/memory/notes-book.d.ts +187 -0
  42. package/dist/memory/provider.d.ts +2 -0
  43. package/dist/memory/recall/call-heartbeat.d.ts +35 -0
  44. package/dist/memory/recall/membership-purge.d.ts +82 -0
  45. package/dist/memory/recall/rank.d.ts +15 -0
  46. package/dist/memory/recall/recall-book.d.ts +140 -0
  47. package/dist/memory/recall/reembed.d.ts +35 -0
  48. package/dist/memory/secret-scan.d.ts +5 -0
  49. package/dist/memory-taps.d.ts +3 -2
  50. package/dist/ort-wasm-simd-threaded.wasm +0 -0
  51. package/dist/version.d.ts +1 -1
  52. package/dist/voice/fast-prompt.d.ts +5 -0
  53. package/dist/ws-server.d.ts +28 -1
  54. package/package.json +2 -1
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Semantic memory 7b: the bridge's recall index and read path (spec §4, §7).
3
+ * Runs every mutation inside the NotesBook mutex (`exclusive`), which is the
4
+ * per-identity memory mutex. Embedding and sealing happen OUTSIDE it (M2).
5
+ *
6
+ * I8: `index` is reached only from BridgeAgent.tapRecall (the ingest tap and
7
+ * the restore re-run). recall-single-writer.arch.test.ts pins that.
8
+ */
9
+ import type { RecallCapture, RecallRow, RuntimeInboxRewrite, SqliteMlsStateStore } from "@agentvault/connect-mls-store";
10
+ import { type ServiceLog } from "../../service-log.js";
11
+ import type { Embedder } from "../embedder/client.js";
12
+ export type IndexSkip = "mode" | "audience" | "voice" | "length" | "secret" | "stamp_missing" | "cleared" | "epoch" | "purged" | "duplicate" | "store";
13
+ export interface RecallTapCapture {
14
+ readonly generation: number | undefined;
15
+ readonly capture: RecallCapture;
16
+ }
17
+ export interface RecallItem {
18
+ readonly id: number;
19
+ readonly text: string;
20
+ readonly sent_at: number;
21
+ }
22
+ export type RecallStore = Pick<SqliteMlsStateStore, "recallState" | "recallCapture" | "sealRecallText" | "sealRecallVec" | "insertRecall" | "readRecallRows" | "readRecallRow" | "openRecallText" | "openRecallVec" | "updateRecallVec" | "listPendingRecall" | "deleteRecall" | "recentMemoryMessageIds">;
23
+ export declare class RecallBook {
24
+ private readonly d;
25
+ constructor(d: {
26
+ store: RecallStore;
27
+ embedder: Embedder;
28
+ exclusive: <T>(fn: () => Promise<T>) => Promise<T>;
29
+ modelId: string;
30
+ threshold: number;
31
+ now?: () => number;
32
+ log?: ServiceLog;
33
+ });
34
+ /**
35
+ * Spec §4 order: (1) seal and embed outside the mutex; (2) take the mutex;
36
+ * (3) one transaction (insertRecall) that re-checks I6, I7 and M-A, then I9.
37
+ * A seal failure inserts nothing (fail closed, §5.2). An embedder failure
38
+ * inserts a pending row (sealed_vec NULL) for the re-embed (§8).
39
+ */
40
+ index(i: {
41
+ conversationId: string;
42
+ messageId: string;
43
+ text: string;
44
+ sentAt: number;
45
+ capture: RecallCapture;
46
+ }): Promise<{
47
+ ok: true;
48
+ recallId: number;
49
+ } | {
50
+ ok: false;
51
+ reason: IndexSkip;
52
+ }>;
53
+ private static readonly MAP_MAX;
54
+ private static readonly IDLE_MS;
55
+ private static readonly TOP_K;
56
+ private static readonly TEXT_MAX_CHARS;
57
+ private static readonly QUERY_TIMEOUT_MS;
58
+ private readonly caches;
59
+ private readonly entries;
60
+ private readonly warming;
61
+ private same;
62
+ private nowMs;
63
+ cacheWarm(conversationId: string): boolean;
64
+ /**
65
+ * §7.3 (H2): snapshot first, open outside the mutex, install under it. Never on a turn's path. Never rejects:
66
+ * a read failure leaves the cache cold (fail closed: no recall).
67
+ *
68
+ * Final-review item 1 (HIGH): prepare() starts this warm on a cold cache and the same turn's tap inserts while
69
+ * the vectors are still opening. At the row cap that insert count-prunes and moves the content epoch, so the old
70
+ * "install only if all four epochs are unchanged" rule never installed, and the next turn repeated the race
71
+ * forever; below the cap the new row was missing from the installed cache. So at install, under the mutex, the
72
+ * rows and the capture are re-read synchronously:
73
+ * - the clear generation, the mode epoch and the purge epoch must equal the snapshot's, else NOTHING installs
74
+ * (a clear, a mode change or a purge during the warm always wins);
75
+ * - the content epoch may differ (an insert, a prune, a forget or a re-embed): the cache is reconciled to the
76
+ * re-read rows (ids gone are dropped, the few new ids are opened here, under the mutex) and installed with the
77
+ * re-read capture.
78
+ */
79
+ warm(conversationId: string): Promise<void>;
80
+ /**
81
+ * §7.1: compute at ingest. Snapshot the capture FIRST (H2); `receipt`
82
+ * replaces its purge and mode epochs with the ones captured when the turn
83
+ * was received (Task 16 fix H-2), so a purge or a mode change after receipt
84
+ * refuses too. 150 ms bound on the query embed; a cold cache starts a warm
85
+ * and gives nothing.
86
+ */
87
+ prepare(p: {
88
+ messageId: string;
89
+ conversationId: string;
90
+ query: string;
91
+ excludeIds: ReadonlySet<string>;
92
+ timeoutMs?: number;
93
+ receipt?: {
94
+ purgeEpoch: number;
95
+ modeEpoch: number;
96
+ };
97
+ }): Promise<void>;
98
+ /** Q3 + Q6: the entry's own conversation, and its capture still current at delivery. Synchronous (ws flush). */
99
+ deliverable(messageId: string, conversationId: string): readonly RecallItem[] | undefined;
100
+ /**
101
+ * Spec §9.1 (H5, W9): delete one recall row and spend this message's destructive operation, in ONE store
102
+ * transaction (`deleteRecall` with the prepared rewrite). The caller has already checked that the id was
103
+ * shown with this delivery. `precommit` is the caller's last check (W1, W2), run inside the mutex with no
104
+ * await before the synchronous delete; a non-null veto deletes nothing.
105
+ *
106
+ * A forget is a delete, never an insert: I8 (single writer of insertRecall) is untouched.
107
+ * The delete moves the content epoch (audit r1 H5). The vector cache is dropped. A pending map entry that
108
+ * matched the capture just before the delete keeps its other items and adopts the new capture (the delete
109
+ * is the only change between the two reads, both inside the mutex with no await between them).
110
+ */
111
+ forget(conversationId: string, recallId: number, rewrite: RuntimeInboxRewrite, precommit?: () => "message" | "mode" | null): Promise<"ok" | "unknown_id" | "budget" | "message" | "mode">;
112
+ /**
113
+ * Spec §8 (Task 19): a pending row just took its vector (`updateRecallVec`, which moves no epoch). A warm cache
114
+ * whose capture is still current gains the row, so an active conversation's cache (kept warm by use) does not
115
+ * hide re-embedded rows until it idles out. A stale cache is dropped. Called inside `exclusive`.
116
+ */
117
+ onReembeddedLocked(conversationId: string, row: RecallRow, vec: Float32Array): void;
118
+ /** Spec §7.1: removed at ack, nack or retire. */
119
+ drop(messageId: string): void;
120
+ /** §5.4: one conversation's cache and its pending map entries. Called inside `exclusive`. */
121
+ dropConversationLocked(conversationId: string): void;
122
+ /** A retention change or a prune can remove rows: drop every vector cache. Called inside `exclusive`. */
123
+ dropAllCachesLocked(): void;
124
+ /** A clear or markdown mode: drop every cache and every map entry. Called inside `exclusive`. */
125
+ dropAllLocked(): void;
126
+ /** §7.3: a cache idle for 30 minutes is dropped. */
127
+ dropIdleCaches(now: number): void;
128
+ /**
129
+ * Task 17 fix round 1 (HIGH): at the row cap every insert count-prunes, so without this every pending map
130
+ * entry of the conversation went stale at the next insert and recall stopped. An entry is rebased only when
131
+ * its capture equals the exact pre-insert capture (clear generation, mode and purge epochs equal; the content
132
+ * epoch moved by this one insert alone) and none of its items was pruned. Anything else stays stale.
133
+ */
134
+ private rebaseEntriesLocked;
135
+ /** Spec §7.3 and audit r1 H7: an insert APPENDS its vector to a warm, current cache, inside the mutex. */
136
+ protected onInsertedLocked(conversationId: string, row: RecallRow, vec: Float32Array | null, r: {
137
+ pruned: readonly number[];
138
+ capture: RecallCapture;
139
+ }): void;
140
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Spec §8: lazy re-embed. Rows whose model_id is NULL (pending) or another model's are embedded again by background
3
+ * jobs: open the text, a priority-1 embedder job, seal under the new vector AAD, then inside the memory mutex a
4
+ * compare-and-set on the old model_id. One row per job, at most 16 rows per tick (the bridge ticks once a second;
5
+ * the shared embedder enforces 16 per second per machine, §20 E1.4). Paused while any call heartbeat of this OS user
6
+ * is fresh (H1, B-20).
7
+ *
8
+ * The capture is read BEFORE the row is re-read, opened and embedded, and the write re-checks it inside its
9
+ * transaction (`updateRecallVec`), bound to the recall id read (Task 18 carry M-4). A clear, a mode change or a purge
10
+ * that lands during the embed refuses the write; a row forgotten, pruned or purged takes no vector.
11
+ *
12
+ * Never an insert: the single-writer rule (I8) is untouched. Embedding and sealing happen outside the mutex (M2).
13
+ */
14
+ import type { RecallRow, SqliteMlsStateStore } from "@agentvault/connect-mls-store";
15
+ import type { Embedder } from "../embedder/client.js";
16
+ export type ReembedStore = Pick<SqliteMlsStateStore, "recallCapture" | "listPendingRecall" | "readRecallRow" | "openRecallText" | "sealRecallVec" | "updateRecallVec">;
17
+ export declare class ReembedQueue {
18
+ private readonly d;
19
+ constructor(d: {
20
+ store: ReembedStore;
21
+ embedder: Embedder;
22
+ exclusive: <T>(fn: () => Promise<T>) => Promise<T>;
23
+ modelId: string;
24
+ paused: () => boolean;
25
+ now?: () => number;
26
+ /** Inside the mutex, after a successful write: the bridge appends the vector to a warm, current cache. */
27
+ onEmbeddedLocked?: (conversationId: string, row: RecallRow, vec: Float32Array) => void;
28
+ embedTimeoutMs?: number;
29
+ });
30
+ private running;
31
+ private readonly parked;
32
+ /** Rows embedded and written by this call (0 when paused, not ready, already running or nothing pending). */
33
+ tick(): Promise<number>;
34
+ private tickOnce;
35
+ }
@@ -0,0 +1,5 @@
1
+ /** The `_suspect` port with skip_long_token false. Same order, same 20-character floor. */
2
+ export declare function suspectWholeField(v: string): string | null;
3
+ export declare function scanNote(input: string): string | null;
4
+ /** Test-only: expand the fixture placeholders (the Python twin is `expand`). */
5
+ export declare function expandFixture(text: string): string;
@@ -3,6 +3,7 @@ interface MemorySink {
3
3
  record(input: MemoryRecordInput): Promise<{
4
4
  seq: number;
5
5
  deduped: boolean;
6
+ dropped?: true;
6
7
  }>;
7
8
  }
8
9
  /** Record an inbound (peer) text turn the agent actually consumed. Best-effort:
@@ -11,8 +12,8 @@ interface MemorySink {
11
12
  * `modality` defaults to "text"; Task 4 (voice-mode carrier) passes "voice"
12
13
  * for a turn whose InboxEntry had the server-set `voiceMode` flag, so memory
13
14
  * continuity reflects how the turn actually arrived. */
14
- export declare function recordInboundMemory(sink: MemorySink, channelId: string, senderId: string, text: string, messageId: string, modality?: "text" | "voice"): Promise<void>;
15
+ export declare function recordInboundMemory(sink: MemorySink, channelId: string, senderId: string, text: string, messageId: string, modality?: "text" | "voice", expectPurgeEpoch?: number): Promise<void>;
15
16
  /** Record the agent's own outbound reply. Best-effort. `modality` is "voice"
16
17
  * for a line the voice fast path spoke (H1), else "text". */
17
- export declare function recordOutboundMemory(sink: MemorySink, channelId: string, text: string, messageId: string, modality?: "text" | "voice"): Promise<void>;
18
+ export declare function recordOutboundMemory(sink: MemorySink, channelId: string, text: string, messageId: string, modality?: "text" | "voice", expectPurgeEpoch?: number): Promise<void>;
18
19
  export {};
Binary file
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const BRIDGE_VERSION = "0.26.6";
1
+ export declare const BRIDGE_VERSION = "0.28.0";
@@ -38,6 +38,8 @@ export declare const FAST_FACTS_CHARS_CAP = 2000;
38
38
  */
39
39
  export declare const FAST_DEFER_RULE: string;
40
40
  export declare function isRefusal(reply: string): boolean;
41
+ /** Spec §15 item 7 (OQ-M, Eric approved): the same line with or without notes, so it tells the proxy nothing. */
42
+ export declare const FAST_NOTES_DEFER_LINE = "If the answer may depend on something the owner asked you to remember, defer.";
41
43
  export declare function buildFastMessages(args: {
42
44
  persona: VoicePersona;
43
45
  guidance: string;
@@ -61,6 +63,9 @@ export declare function buildFastMessages(args: {
61
63
  /** The agent's identity display name ("one name", 2026-10-01): the only
62
64
  * source of the prompt's "You are <name>." line. */
63
65
  agentName?: string;
66
+ /** Spec §15 item 7: append the fixed notes-defer line. The caller sets it
67
+ * from provider and DM-ness only, never from whether notes exist. */
68
+ notesDeferLine?: boolean;
64
69
  }): ChatMessage[];
65
70
  /** The fast model appends this when the full agent must answer next. */
66
71
  export declare const DEFER_TOKEN = "[[DEFER]]";
@@ -9,6 +9,11 @@ export interface AgentWebSocketOptions {
9
9
  inflightStaleAfterMs?: number;
10
10
  /** Test seam: the clock the delivery ages are measured on. */
11
11
  now?: () => number;
12
+ /**
13
+ * Test seam: how long a socket's deliveries wait for its hello (default
14
+ * HELLO_WAIT_MS). 0 delivers at once, as before N-H2.
15
+ */
16
+ helloWaitMs?: number;
12
17
  }
13
18
  /**
14
19
  * FB-P1-024 / #842 ruling §3: a stale-in-flight OBSERVATION, not a lease.
@@ -21,6 +26,28 @@ export interface AgentWebSocketOptions {
21
26
  */
22
27
  export declare const INFLIGHT_STALE_SWEEP_MS = 60000;
23
28
  export declare const INFLIGHT_STALE_AFTER_MS: number;
24
- export declare function attachAgentWebSocket(server: HttpServer, agent: BridgeAgent, opts: AgentWebSocketOptions): {
29
+ /**
30
+ * Audit r2 N-H2 (ruling PR-2): a new socket's deliveries wait for its first
31
+ * hello, or this long for a consumer that sends none. Without the hold, a
32
+ * turn buffered before the runtime connected went out in the connection
33
+ * handler's first flush, before the hello could mark the socket a runtime
34
+ * consumer, so it never carried its notes.
35
+ */
36
+ export declare const HELLO_WAIT_MS = 2000;
37
+ /** Spec §8.5: the hello-reply feature that tells the runtime it may offer memory_note on this socket. */
38
+ export declare const MEMORY_NOTES_FEATURE = "memory_notes";
39
+ /** Semantic memory 7b (spec §7.2 Q4): the hello feature a runtime declares to receive `memory.recall`; the bridge echoes it. */
40
+ export declare const MEMORY_RECALL_FEATURE = "memory_recall";
41
+ /**
42
+ * Every BridgeAgent member this module touches, directly or through the
43
+ * consumer helpers. The handlers take `WsAgent`, so tsc rejects a call to a
44
+ * member missing from this list. The ws-server test fakes are passed `as
45
+ * never` from files tsc does not check, so the fake-coverage tests assert each
46
+ * fake against this list at run time (7b gate fix: a member added here without
47
+ * a fake made three tests throw "is not a function").
48
+ */
49
+ export declare const WS_AGENT_MEMBERS: readonly ["ackRuntimeMessages", "action", "admitRuntimeDelivery", "applyMemoryNote", "clearHarnessReport", "clearPerToolConsumer", "isRuntimeMessageRetired", "noteNotesConsumer", "notePerToolConsumer", "noteRecallConsumer", "noteSessionClose", "noteSessionEvicted", "noteSessionOpen", "notesAppliedGeneration", "notesForDelivery", "recallForDelivery", "recordHarnessReport", "recordRuntimeReply", "reportStatusGated", "requeueFailedRuntimeDeliveries", "requeueFront", "runtimeReplyReceipt", "send", "sendA2A", "subscribe", "takeDeliverable", "wakeListeners"];
50
+ export type WsAgent = Pick<BridgeAgent, (typeof WS_AGENT_MEMBERS)[number]>;
51
+ export declare function attachAgentWebSocket(server: HttpServer, agent: WsAgent, opts: AgentWebSocketOptions): {
25
52
  close(): void;
26
53
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@securecoms/agent-bridge",
3
- "version": "0.26.6",
3
+ "version": "0.28.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22.5.0"
@@ -58,6 +58,7 @@
58
58
  "@types/node": "^22.0.0",
59
59
  "@types/ws": "^8.18.1",
60
60
  "esbuild": "0.24.0",
61
+ "onnxruntime-web": "1.29.0",
61
62
  "typescript": "^5.4.0",
62
63
  "vitest": "^3.2.6"
63
64
  }