@securecoms/agent-bridge 0.26.6 → 0.27.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.
@@ -33,6 +33,7 @@ import { type OwnerAuthorityStore, type OwnerPin } from "@agentvault/connect-cli
33
33
  import type { BridgeConfig } from "./config.js";
34
34
  import { LatencyRecorder } from "./latency.js";
35
35
  import type { AttachmentMeta, VaultRefEntry } from "./attachments.js";
36
+ import { type MemoryNoteResultFrame } from "./memory/note-frames.js";
36
37
  import { type ChatMessage } from "./voice/fast-prompt.js";
37
38
  import { VOICE_CALL_START_MARKER } from "./voice/prompt-compiler.js";
38
39
  import { type VaultListNode, type VaultReadExpectations } from "./vault.js";
@@ -114,7 +115,48 @@ export interface BridgeAuthorization {
114
115
  * audience owner_dm or on this mark, nothing else.
115
116
  */
116
117
  readonly relay_speaker?: "owner";
118
+ /**
119
+ * Markdown memory 7a (spec §8.3, OQ-B): true ONLY on authorizationFor's
120
+ * ownerDirect branch with memoryProvider "securecoms": the channel's pinned
121
+ * owner (never "anomalous") typed this turn in their DM, the roster is
122
+ * fresh at the held epoch and group (or the DM is cycle-fresh), and the
123
+ * owner-DM kill switch is on. Present only when true. An OFFER HINT: the
124
+ * bridge's W1 to W8 are the gate. Never set on the ownerRelayed branch
125
+ * (OQ-C, Eric approved 2026-10-06): voice turns read notes, never write,
126
+ * and never on any turn with a relay speaker claim (review I-1).
127
+ * NOT a tool grant: it is not, and must never be, in `tool_grants`.
128
+ * Reaches only the per-tool runtime consumer: withConsumerGrants strips it
129
+ * for a harness socket and the HTTP/SSE paths (review m-3).
130
+ */
131
+ readonly memory_write?: true;
132
+ }
133
+ /**
134
+ * Spec §8.2: what ws-server tells applyMemoryNote about the socket a
135
+ * memory_note frame arrived on. Read live: W2 is re-evaluated after awaits.
136
+ */
137
+ export interface MemoryNoteContext {
138
+ /**
139
+ * THIS socket's in-flight record for a message id (the original, never the
140
+ * consumer copy) and the token of that delivery. A nack, backoff and
141
+ * redelivery on the same socket reuses the record object but gets a new
142
+ * token (review m-1).
143
+ */
144
+ lookup(messageId: string): {
145
+ record: BridgeMessage;
146
+ token: number;
147
+ } | undefined;
148
+ /** The socket's own session facts (OQ-J): consumer and the sticky harness claim. */
149
+ readonly session: {
150
+ readonly consumer: "runtime" | "unknown";
151
+ readonly harnessClaimed?: boolean;
152
+ };
153
+ /** The delivery token of the frame's message id, captured when the frame arrived; undefined when not in flight. */
154
+ token: number | undefined;
155
+ /** Per-socket request ids still being decided (M-2). At most MAX_PENDING_NOTE_REQUESTS (review m-3). */
156
+ pendingRequests: Set<string>;
117
157
  }
158
+ /** Review m-3: pending memory_note requests per socket; a frame beyond this refuses `invalid` with no echo. */
159
+ export declare const MAX_PENDING_NOTE_REQUESTS = 8;
118
160
  export interface BridgeMessage {
119
161
  channel_id: string;
120
162
  message_id: string;
@@ -142,6 +184,27 @@ export interface BridgeMessage {
142
184
  * @mention filter reads (not recomputed differently).
143
185
  */
144
186
  sender_is_agent?: boolean;
187
+ /**
188
+ * Audit r2 N-L3 (PR-1): this turn is a voice transcript. W8 never quotes
189
+ * such a record. Present only when true. Set when either holds:
190
+ * - the turn is relay-attributed (`speaker` set). `speaker` is honoured
191
+ * only from a sender the server roster marks `is_relay`, so on this path
192
+ * the mark does not depend on the body. The room relay's `sendTurn`
193
+ * (connect-room-relay/src/turns.ts) also sets `transcribed: true`, and
194
+ * the hosted voice worker reaches the bridge only through that relay;
195
+ * - the E2E body says `transcribed: true`. The owner's own web client sets
196
+ * it on an on-device transcript (connect-web MessagingShell.tsx
197
+ * `sendConfirmedVoiceToChannel`, reached from CallPanel's
198
+ * `sendAgentMessage` via `confirmedSendBody`).
199
+ * LIMITATION (spec §15): on the owner-device path the mark is an
200
+ * UNAUTHENTICATED claim in the body. A modified or legacy client, or a
201
+ * device added under the owner's identity by a compromised server (the
202
+ * ADR-0010 gap), can send speech-derived text without it, and the bridge
203
+ * then treats it as typed. That adds no capability (the sender could type
204
+ * the same words), so the mark is fail-closed but not a cryptographic
205
+ * guarantee.
206
+ */
207
+ transcribed?: true;
145
208
  /**
146
209
  * Track 5: materialized attachment metadata for `artifact_share` messages.
147
210
  * Each entry corresponds to one downloaded+decrypted file saved to disk.
@@ -1067,6 +1130,56 @@ export declare class BridgeAgent {
1067
1130
  private startedAt;
1068
1131
  private readonly store;
1069
1132
  private readonly memory;
1133
+ /**
1134
+ * Markdown memory 7a: the one owner of every note mutation and of the
1135
+ * delivery cache (spec §6.3, §7.2). One per store, so one per identity.
1136
+ */
1137
+ private readonly notes;
1138
+ /**
1139
+ * Fix round 1 (concern 1): the applied clear generation when each pending
1140
+ * runtime turn was FIRST staged (W7b compares it). Keyed by message id,
1141
+ * persisted inside the sealed runtime_inbox payload and restored with it;
1142
+ * never a BridgeMessage field. Removed when the turn is acked or shed.
1143
+ */
1144
+ private readonly ingestGeneration;
1145
+ /** Spec §11.2 (OQ-L): set by refetchRuntimeConfig (Task 8); notes are on until a payload says otherwise. */
1146
+ private memoryConfig;
1147
+ /**
1148
+ * Task 8 fix round 1 (review I-1, N-L5): set once the BOOT refetch has
1149
+ * returned, on success or failure. Until then prepareNotesForTurn only
1150
+ * fills and never runs the legacy import: the boot drain pulls turns before
1151
+ * the boot refetch, and an import there would move facts into notes that a
1152
+ * clear issued while the machine was offline then deletes for good.
1153
+ * warmRestoredNotes runs the import for the buffered (drained) turns after.
1154
+ */
1155
+ private bootConfigSettled;
1156
+ /**
1157
+ * Final review M-1: false until a config refetch has returned a payload in
1158
+ * THIS process. Until then notes are off for reads and writes
1159
+ * (notesProviderOn), whatever memoryConfig says: the last applied payload
1160
+ * may have turned notes off, and that state is not persisted. Distinct from
1161
+ * bootConfigSettled, which turns true when the boot refetch RETURNED, on
1162
+ * success or failure (settled is not notes-on).
1163
+ */
1164
+ private memoryConfigConfirmed;
1165
+ /**
1166
+ * Final review I-1: true once a /v1/ws consumer completed a hello that
1167
+ * declares it reads notes (MEMORY_NOTES_FEATURE) as the runtime, with no
1168
+ * harness claim on that socket (ws-server). A 0.9.x runtime's hello does not
1169
+ * declare it, a harness socket never qualifies, and a socket that sends no
1170
+ * hello never does. Until then no legacy fact is moved into notes (those
1171
+ * consumers recall facts, never notes) and the report says
1172
+ * memory_notes: false. Per process and never reset: a runtime restart
1173
+ * closes the socket but is not a downgrade.
1174
+ */
1175
+ private notesConsumerSeen;
1176
+ /** Review M-1: the refetch now running, and the ONE follow-up every caller during it shares. */
1177
+ private refetchInFlight;
1178
+ private refetchFollowUp;
1179
+ /** Review M-4: the (payload, applied) pair last logged as regressed. */
1180
+ private regressedLogged;
1181
+ /** Review M-5: an out-of-range applied generation is logged once. */
1182
+ private generationRangeLogged;
1070
1183
  /** Task 2: warn-once-per-channel guard for the enclave-map fallback (below). */
1071
1184
  private enclaveFallbackWarned;
1072
1185
  /** Task 2: dedupe guard so ingestOnce writes a channel's enclave at most once
@@ -1128,6 +1241,7 @@ export declare class BridgeAgent {
1128
1241
  * loose until the next save.
1129
1242
  */
1130
1243
  refetchRuntimeConfig(): Promise<void>;
1244
+ private refetchRuntimeConfigOnce;
1131
1245
  /**
1132
1246
  * Enforce the inbound-buffer cap with a DROP-OLDEST policy.
1133
1247
  *
@@ -1777,6 +1891,101 @@ export declare class BridgeAgent {
1777
1891
  * never falls back to the runtime's retired plaintext JSON files.
1778
1892
  */
1779
1893
  private memoryForTurn;
1894
+ /**
1895
+ * Spec §11.2 (OQ-L), §6.5: apply one parsed `payload.memory`. An unreadable
1896
+ * value turns notes off and deletes nothing. A readable one goes through
1897
+ * NotesBook.clear, which purges only on a HIGHER generation (under the note
1898
+ * mutex) and resyncs on equal or lower; a lower one logs
1899
+ * memory.clear_generation_regressed. Content-free logs only.
1900
+ *
1901
+ * While a purge is pending (the payload is above the applied generation)
1902
+ * notes are held OFF, and they come back on only after the purge committed.
1903
+ * A purge that throws therefore leaves notes off (fail closed); it is
1904
+ * caught here (review M-2) so the rest of the config still applies and
1905
+ * reports, and the next refetch retries it. A refetch that fails before
1906
+ * this point keeps the last applied state.
1907
+ */
1908
+ private applyPayloadMemory;
1909
+ /**
1910
+ * Review M-5: the report key only for a value the backend accepts
1911
+ * (0..MAX_CLEAR_GENERATION); otherwise the key is omitted, so one bad
1912
+ * mirror value cannot 422 the whole report, and the event logs once.
1913
+ */
1914
+ private reportedClearGeneration;
1915
+ /**
1916
+ * W1 and R1: the provider is securecoms, a config payload has arrived in
1917
+ * this process (final review M-1), and the parsed payload keeps notes on.
1918
+ */
1919
+ private notesProviderOn;
1920
+ /**
1921
+ * Final review I-1: ws-server calls this when a socket completes a hello as
1922
+ * the runtime (no harness claim) that declares MEMORY_NOTES_FEATURE. The
1923
+ * first one also refetches the runtime config, so the report says
1924
+ * memory_notes: true now rather than at the next 6-hour refetch.
1925
+ */
1926
+ noteNotesConsumer(): void;
1927
+ /**
1928
+ * R2 (spec §7.3): the frozen marks that only the full owner predicate in
1929
+ * authorizationFor sets. `memory_write` (the pinned owner typed this turn)
1930
+ * or `relay_speaker === "owner"` (the pinned owner spoke it through the
1931
+ * relay; read only, never write, OQ-C). Strict `=== true` / `=== "owner"`:
1932
+ * `audience === "owner_dm"` alone is not enough (§0.1 item 7).
1933
+ *
1934
+ * Review I-1 (spec §8.3): the owner-DM kill switch is re-checked HERE, at
1935
+ * read time, and so is the provider (R1). A turn restored from
1936
+ * runtime_inbox carries the marks frozen by the previous process; with
1937
+ * SECURECOMS_OWNER_DM_DEFAULT_GRANTS now off, or the provider no longer
1938
+ * securecoms, it gets no notes, and the boot warm runs no legacy import.
1939
+ */
1940
+ private notesReadable;
1941
+ /** The applied clear generation, for a socket that gets no notes (R5 decided first, review m-2). */
1942
+ notesAppliedGeneration(): number;
1943
+ /**
1944
+ * ws-server flush (synchronous). R1 to R3 here; R5 in withConsumerNotes; R4
1945
+ * holds because nothing else calls this. Reads only the generation-checked
1946
+ * cache (N-M1): a miss gives no notes. `generation` is the applied clear
1947
+ * generation the socket records for W7b.
1948
+ */
1949
+ notesForDelivery(m: BridgeMessage): {
1950
+ notes?: string;
1951
+ generation: number;
1952
+ };
1953
+ /**
1954
+ * §7.2 Warm and §10 import, BEFORE the turn is staged. Never throws.
1955
+ * The import runs only for a `memory_write` turn (M-6: owner-DM channels
1956
+ * only); the turn's own frozen authorization is the proof NotesBook checks.
1957
+ */
1958
+ private prepareNotesForTurn;
1959
+ /**
1960
+ * §7.2 + N-L5: after the boot refetch applied any clear, warm each channel
1961
+ * holding an R2-eligible restored turn, before start() returns (bin.ts
1962
+ * attaches /v1/ws only after start()). `this.buffer` also holds the turns
1963
+ * the boot drain pulled; their prepare ran fill-only (review I-1), so the
1964
+ * legacy import for them runs here, after the purge.
1965
+ */
1966
+ private warmRestoredNotes;
1967
+ /**
1968
+ * Spec §8.2: the write gate for one `memory_note` frame, fail closed, in
1969
+ * table order: W1, well formed (and a request_id not already pending on
1970
+ * this socket, M-2), W2, W3, W4(a), W4(b), W8, W5, then W2 and W1 once more
1971
+ * after the await, then under the note mutex W7, W2 and W1 again after the
1972
+ * seal and right before the synchronous commit, and W7b (the generation
1973
+ * stamped when the turn was first staged, re-checked inside the store
1974
+ * transaction; a turn with no stamp refuses `cleared`). Every refusal logs `memory.note_refused
1975
+ * { reason_class }` and nothing else (§8.6); no note text, frame body or id
1976
+ * is ever logged. Never rejects: an unexpected throw refuses with `store`.
1977
+ *
1978
+ * Everything that decides WHERE and WHETHER a note is written comes from
1979
+ * the bridge-held in-flight record that `ctx.lookup` returns for THIS
1980
+ * socket, never from the frame: the channel (W3), the frozen
1981
+ * authorization (W4(a)), the sender (W4(b)), and the source text and the
1982
+ * `transcribed` mark (W8). The note text that reaches NotesBook is the
1983
+ * owner's own span, produced only by `prepareStoredNote` (W8 then W5).
1984
+ *
1985
+ * LEAF LOCK: heldPosition (W4(b)) is awaited BEFORE the note mutex is
1986
+ * taken; nothing under the mutex awaits it.
1987
+ */
1988
+ applyMemoryNote(raw: Record<string, unknown>, ctx: MemoryNoteContext): Promise<MemoryNoteResultFrame>;
1780
1989
  memoryContext(conversationId: string, limit?: number): Promise<import("@agentvault/connect-mls-store").MemoryContextResult>;
1781
1990
  /**
1782
1991
  * Voice front desk, step 1 — decided SYNCHRONOUSLY inside the ingest cycle,
@@ -2107,6 +2316,8 @@ export declare class BridgeAgent {
2107
2316
  * room_kill_switch SECURECOMS_ROOM_DEFAULT_GRANTS as read at startup
2108
2317
  * per_tool_consumer a consumer that declared per-tool grant
2109
2318
  * enforcement is attached
2319
+ * memory_write the turn carries memory_write (Markdown memory
2320
+ * 7a, spec §8.3): a boolean, never content
2110
2321
  */
2111
2322
  private logAuthzDecision;
2112
2323
  /**
@@ -2377,6 +2588,19 @@ export declare class BridgeAgent {
2377
2588
  isRuntimeMessageRetired(messageId: string): boolean;
2378
2589
  /** Complete the durable handoff only after the runtime explicitly acks. */
2379
2590
  ackRuntimeMessages(messageIds: string[]): void;
2591
+ /** The sealed runtime_inbox payload: the message plus its ingest stamp (never on the message object). */
2592
+ private stagedPayload;
2593
+ /**
2594
+ * Seal `message` into runtime_inbox with the current ingest stamp and record
2595
+ * the stamp that is actually persisted. Task 7 n-1: a same-id re-stage is
2596
+ * compared on the MESSAGE only, never the stamp, so a clear between two
2597
+ * stagings of one message is not a payload conflict (which would fail the
2598
+ * stage and leave the backend row redelivered forever). The row is not
2599
+ * rewritten, so the FIRST staging's stamp stays authoritative: the owner's
2600
+ * clear still covers a turn first ingested before it. A prior row with no
2601
+ * valid stamp gets none, so its write refuses `cleared`.
2602
+ */
2603
+ private stageSealed;
2380
2604
  private restoreRuntimeInbox;
2381
2605
  /**
2382
2606
  * Seal a runtime frame before the backend queue id may be acknowledged.
@@ -17,6 +17,7 @@ export declare const PER_TOOL_GRANTS_FEATURE = "per_tool_grants";
17
17
  *
18
18
  * Never mutates `m`: the buffer and in-flight records keep the original, so a
19
19
  * later per-tool consumer still receives it. The stripped copy keeps every
20
- * other field (audience, relay_speaker) as is.
20
+ * other field (audience, relay_speaker) as is, except `memory_write`, which
21
+ * no non-per-tool consumer ever receives (Markdown memory 7a, review m-3).
21
22
  */
22
23
  export declare function withConsumerGrants(m: BridgeMessage, perToolConsumer: boolean): BridgeMessage;