@securecoms/agent-bridge 0.11.3 → 0.15.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 (40) hide show
  1. package/README.md +75 -0
  2. package/dist/bin.js +2626 -90
  3. package/dist/bridge-agent.d.ts +331 -0
  4. package/dist/config.d.ts +3 -0
  5. package/dist/errors.d.ts +1 -1
  6. package/dist/http-server.d.ts +4 -4
  7. package/dist/index.js +2643 -109
  8. package/dist/memory-taps.d.ts +6 -2
  9. package/dist/rate-limit.d.ts +10 -0
  10. package/dist/summarize.d.ts +54 -0
  11. package/dist/voice/fast-client.d.ts +13 -0
  12. package/dist/voice/fast-prompt.d.ts +53 -0
  13. package/dist/voice/persona.d.ts +23 -0
  14. package/dist/voice/prompt-compiler.d.ts +4 -0
  15. package/dist/voice/settings.d.ts +31 -0
  16. package/package.json +6 -1
  17. package/dist/attachments.js +0 -49
  18. package/dist/auth.js +0 -56
  19. package/dist/bin.js.pre-c1-f803007 +0 -5795
  20. package/dist/bin.js.pre-c3-096a9a3 +0 -5832
  21. package/dist/bin.js.pre-ffrebuild-b162d4a +0 -5639
  22. package/dist/bin.js.pre-kpheal-20260708-150924 +0 -5531
  23. package/dist/bin.js.pre-kppersist-20260708-152341 +0 -5543
  24. package/dist/bin.js.pre-p0-20260708-102415 +0 -5196
  25. package/dist/bin.js.pre-p1b-02f47e1 +0 -5639
  26. package/dist/bin.js.pre-warmfix-20260708-153705 +0 -5613
  27. package/dist/bin.js.pretrace-113205 +0 -5395
  28. package/dist/bridge-agent.js +0 -831
  29. package/dist/config.js +0 -74
  30. package/dist/coordination.js +0 -63
  31. package/dist/errors.js +0 -25
  32. package/dist/http-server.js +0 -220
  33. package/dist/index.js.pre-p0-20260708-102415 +0 -4879
  34. package/dist/latency.js +0 -87
  35. package/dist/rate-limit.js +0 -56
  36. package/dist/service/config.js +0 -26
  37. package/dist/service/install.js +0 -60
  38. package/dist/service/launchd.js +0 -30
  39. package/dist/service/systemd.js +0 -25
  40. package/dist/ws-server.js +0 -254
@@ -27,9 +27,13 @@ export type ChannelObservation = {
27
27
  last_decrypt_at: number | null;
28
28
  observed_at: number;
29
29
  };
30
+ import { type OwnerAuthorityStore, type OwnerPin } from "@agentvault/connect-clients-core";
30
31
  import type { BridgeConfig } from "./config.js";
31
32
  import { LatencyRecorder } from "./latency.js";
32
33
  import type { AttachmentMeta } from "./attachments.js";
34
+ import { type ChatMessage } from "./voice/fast-prompt.js";
35
+ import { VOICE_CALL_START_MARKER } from "./voice/prompt-compiler.js";
36
+ import { type VaultListNode } from "./vault.js";
33
37
  import type { AgentActionNodeType, AgentActionNodeStatus, AgentActionNodeDetail } from "@agentvault/connect-core";
34
38
  export interface BridgeMessage {
35
39
  channel_id: string;
@@ -56,7 +60,86 @@ export interface BridgeMessage {
56
60
  * Undefined for plain `text` messages.
57
61
  */
58
62
  attachments?: AttachmentMeta[];
63
+ /**
64
+ * Task 4 (voice-mode carrier): true when the InboxEntry that produced this
65
+ * buffered message had `voiceMode` set by the server (the agent device's
66
+ * row for a live 2-member agent call). This is a metadata flag ONLY — it
67
+ * never drives playback or any connect-web behavior. Undefined/false for
68
+ * every ordinary text turn.
69
+ */
70
+ voice?: boolean;
71
+ /**
72
+ * Task 4: compiled-in reply guidance selected by `voice`, exposed here at
73
+ * the buffer-drain seam so the connector handing context to the agent host
74
+ * can surface it. Set ONLY when `voice` is true; never transmitted over the
75
+ * wire — the constant lives in this process, selected by the server-set
76
+ * flag alone.
77
+ */
78
+ guidance?: string;
79
+ /**
80
+ * Task 2 (I2 safety, per-speaker attribution): set ONLY when the decrypted
81
+ * body parsed with `relay: true` and both `speaker_identity_id` and
82
+ * `speaker_display_name` as strings (the exact shape `sendTurn` in
83
+ * connect-room-relay stamps on a relayed human turn — see turns.ts:71-78).
84
+ * This is DATA describing who spoke; it is never merged into `guidance` or
85
+ * any other instruction text, so one speaker's identity can never ride
86
+ * into another agent's context as an instruction.
87
+ */
88
+ speaker?: {
89
+ identityId: string;
90
+ displayName: string;
91
+ };
59
92
  }
93
+ /**
94
+ * Task 4 (voice-mode carrier): the one-sentence reply guidance compiled in
95
+ * for voice-flagged turns. Selected by `InboxEntry.voiceMode` in the entry
96
+ * loop below — never transmitted, never a playback trigger.
97
+ */
98
+ export declare const VOICE_MODE_GUIDANCE = "Reply immediately and conversationally, one short sentence per message; never bundle multiple points into one message.";
99
+ /**
100
+ * Task 2 (room voice guidance): compiled reply guidance for a voice-flagged
101
+ * turn in a GROUP call (bridge's freshMeta says `is_dm === false`), used
102
+ * INSTEAD of VOICE_MODE_GUIDANCE. Two static variants — chosen only by the
103
+ * entry's SERVER-set `voiceDefaultResponder` flag (never by anything in the
104
+ * message body; see the entry loop's attribution parsing, which reads
105
+ * speaker fields only and never a body-supplied responder/voiceMode claim).
106
+ * No string interpolation of peer data — these are fixed, compiled-in
107
+ * sentences.
108
+ */
109
+ /**
110
+ * Task 4 (self-hosted fast voice): appended to the runtime-facing guidance
111
+ * (never sent anywhere) ONLY on turns where the fast path already fired a
112
+ * spoken acknowledgement ahead of the full agent reply. Compiled-in constant,
113
+ * concatenated only with the other compiled-in guidance strings above — never
114
+ * with peer/body data (I2).
115
+ */
116
+ /** Posted by the web client once the call row confirms (CallPanel GREETING_MARKER). */
117
+ export { VOICE_CALL_START_MARKER };
118
+ /**
119
+ * The fast path's "user turn" for a call-start marker. The caller's first
120
+ * name (when the roster knows it) rides in the Speaker line of the prompt,
121
+ * so this only has to say what a good greeting is. Data region label keeps
122
+ * it I2-safe like every other injected line.
123
+ *
124
+ * Delegates to `greetingTurnFor` with the default voice settings (whose
125
+ * `greetingStyle` is "continue"), so the wording stays byte-equal to the
126
+ * pre-Task-6 behaviour for callers/tests that only know a bare caller name.
127
+ */
128
+ export declare function greetingTurn(callerName?: string): string;
129
+ export declare const FAST_PATH_SPOKEN_GUIDANCE = "A one-sentence spoken acknowledgement has already been sent for this turn. Reply only with information it did not contain, in one or two short sentences; if there is nothing to add, send nothing.";
130
+ export declare const ROOM_VOICE_GUIDANCE_MENTION_ONLY = "You are in a group voice room. Reply ONLY when a message @mentions your display name. Keep replies to one short sentence. When you should not reply, send nothing.";
131
+ export declare const ROOM_VOICE_GUIDANCE_WITH_RESPONDER = "You are in a group voice room. Reply ONLY when a message @mentions your display name, or when you are the designated default responder and no other agent is mentioned. Keep replies to one short sentence. When you should not reply, send nothing.";
132
+ /**
133
+ * Task 2: selects the compiled voice guidance for a buffered entry.
134
+ * `isDm` is `freshMeta(channelId)?.is_dm`. `undefined` (unknown channel
135
+ * meta — e.g. a cold-start miss) and `true` both take the conservative DM
136
+ * branch: VOICE_MODE_GUIDANCE is one short sentence, harmless to attach even
137
+ * if the channel later turns out to be a group, whereas misclassifying a DM
138
+ * as a room would wrongly suppress the agent's replies. Only an explicit
139
+ * `is_dm === false` selects a room variant, gated on the entry's SERVER-set
140
+ * `voiceDefaultResponder` (never a body-supplied claim).
141
+ */
142
+ export declare function selectVoiceGuidance(isDm: boolean | undefined, voiceDefaultResponder: boolean | undefined): string;
60
143
  export interface BridgeAgentDeps {
61
144
  transport?: any;
62
145
  backend?: any;
@@ -102,6 +185,22 @@ export interface BridgeAgentDeps {
102
185
  * timer delays.
103
186
  */
104
187
  rng?: () => number;
188
+ /**
189
+ * Task 4 (self-hosted fast voice): test seam for the fast-path LLM client.
190
+ * Defaults to `new FastLlmClient(cfg.voice.fastPath, fetchImpl)` when
191
+ * `cfg.voice?.fastPath.enabled` is true; left undefined (fast path off)
192
+ * otherwise. Overriding this in a test bypasses real HTTP.
193
+ */
194
+ fastLlm?: {
195
+ complete(messages: ChatMessage[]): Promise<string>;
196
+ };
197
+ /**
198
+ * B7 (FileSafe vault): persistence for the vault kekset's owner-pin TOFU
199
+ * state. Defaults to a {@link FileOwnerAuthorityStore} at
200
+ * `join(dirname(cfg.storePath), "vault-owner-pins.json")`. Override in
201
+ * tests to avoid real file IO / assert pin persistence directly.
202
+ */
203
+ vaultOwnerStore?: OwnerAuthorityStore;
105
204
  }
106
205
  /** Default inbound-buffer cap (messages) when deps.maxBuffer is unset. */
107
206
  export declare const DEFAULT_MAX_BUFFER = 1000;
@@ -143,6 +242,32 @@ export declare class InvalidMessageIdError extends Error {
143
242
  readonly messageId: string;
144
243
  constructor(messageId: string);
145
244
  }
245
+ /**
246
+ * Deterministic message id for the voice fast-path reply to inbound turn
247
+ * `messageId`: SHA-256 of `fast:<messageId>`, first 16 bytes, RFC 4122
248
+ * version/variant nibbles set. Live finding 2026-09-06: the previous
249
+ * `fast-<messageId>` string failed `resolveMessageId` ("message_id must be a
250
+ * UUID") on every turn, so the fast path never sent once. Same input → same
251
+ * UUID, so the idempotency argument in `runFastPath`'s doc still holds.
252
+ */
253
+ export declare function fastReplyId(messageId: string): string;
254
+ /**
255
+ * B7 (FileSafe vault): a file-backed {@link OwnerAuthorityStore} for the
256
+ * daemon's owner-pin TOFU state (mirrors B4b's browser-side store, which
257
+ * persists to IndexedDB — the daemon has no IndexedDB, so this persists one
258
+ * JSON blob alongside the SQLite MLS store). This is the security-load-
259
+ * bearing half of the kekset open (kekset.ts's `openKekset` pins the header
260
+ * signer to `pinnedOwnerCred`; THIS is where that pin survives restarts) —
261
+ * without persistence, every restart would TOFU-trust whatever "owner" the
262
+ * server presents on the next open, defeating the pin entirely.
263
+ */
264
+ export declare class FileOwnerAuthorityStore implements OwnerAuthorityStore {
265
+ private readonly path;
266
+ constructor(path: string);
267
+ private readAll;
268
+ load(enclaveId: string): Promise<OwnerPin | null>;
269
+ save(enclaveId: string, pin: OwnerPin): Promise<void>;
270
+ }
146
271
  /**
147
272
  * Per-room agent pause toggle (daemon side): the backend rejected a publish
148
273
  * with `403 {detail:{error:"agent_paused"}}`, which the SDK's `RestTransport`
@@ -274,6 +399,46 @@ export declare class BridgeAgent {
274
399
  * counter is incremented. Surfaced on health()/metrics().
275
400
  */
276
401
  private failedAttachments;
402
+ /**
403
+ * Task 4 (self-hosted fast voice): test/deploy seam for the fast-path LLM
404
+ * client. Undefined whenever `cfg.voice?.fastPath.enabled` is not true —
405
+ * the text-branch push checks this field (not cfg) so a disabled/missing
406
+ * voice config is a single falsy check.
407
+ */
408
+ private fastLlm?;
409
+ /** Plan 2026-09-06 (agent voice settings): true when the constructor deps
410
+ * injected a fastLlm (tests, or a real deploy seam) — refetchVoiceSettings
411
+ * must never overwrite an injected client with one built from the backend
412
+ * row's proxy config. */
413
+ private readonly injectedFastLlm;
414
+ /** Controller ruling (PR B review, fix round 1): the credential's ORIGINAL
415
+ * `voice` block, captured once at construction before `cfg.voice` is ever
416
+ * mutated by a refetch. `GET .../voice-settings` returning `version: 0`
417
+ * means "no row configured for this agent" — a hand-tuned file persona
418
+ * (e.g. Cortina011) must win over that all-defaults row rather than being
419
+ * blanked by it. Undefined when the credential had no `voice` block. */
420
+ private readonly fileVoiceConfig?;
421
+ /** Plan 2026-09-06: the loaded voice settings row (backend or none). Undefined
422
+ * until a successful `refetchVoiceSettings()` — the file `voice.persona`
423
+ * block (if any) is used for the prompt/greeting until then. */
424
+ private voiceSettings;
425
+ /** Plan 2026-09-06: version of the last successfully loaded backend row, or
426
+ * null when no backend row has ever loaded (file or none source). Surfaced
427
+ * on health(). */
428
+ private voiceSettingsVersion;
429
+ /** Plan 2026-09-06: where the active voice config came from. "backend" wins
430
+ * over "file" the moment a fetch succeeds; a later failed refetch keeps the
431
+ * last good "backend" row rather than falling back. Surfaced on health(). */
432
+ private voiceSettingsSource;
433
+ /** Task 4: messageIds already sent through the fast path (capped FIFO, so a redelivered/duplicate entry never double-sends). */
434
+ private readonly fastPathSeen;
435
+ private static readonly FAST_PATH_SEEN_CAP;
436
+ /** Cumulative count of fast-path replies actually sent (surfaced on health()/metrics()). */
437
+ private fastPathSent;
438
+ /** Cumulative count of fast-path attempts that errored (LLM timeout/http/network/shape, or send failure) — surfaced on health()/metrics(). */
439
+ private fastPathFailed;
440
+ /** Cumulative count of fast-path turns skipped in a room channel because the shared send-floor bucket lacked headroom for both the ack and the runtime's own reply (controller ruling) — surfaced on health()/metrics(). */
441
+ private fastPathSkipped;
277
442
  /**
278
443
  * Task 8: count of live AUTHENTICATED /v1/ws sessions, so a later `status`
279
444
  * verb can tell "bridge process up" (the app's green dot, which only
@@ -444,7 +609,39 @@ export declare class BridgeAgent {
444
609
  private readonly uploadBlobImpl;
445
610
  /** Track 5: directory where received attachments are saved. */
446
611
  private readonly attachmentsDirImpl;
612
+ /** B7: persistence for the vault kekset's owner-pin TOFU state. */
613
+ private readonly vaultOwnerStoreImpl;
614
+ /** B7: last-seen kekset generation per enclave, enforced monotonic across
615
+ * calls for the daemon's lifetime (mirrors useVault's `currentGenRef` —
616
+ * openKekset rejects a generation <= this, catching a server that tries
617
+ * to roll a joiner back to a superseded kekset). */
618
+ private readonly vaultLastSeenGeneration;
447
619
  constructor(cfg: BridgeConfig, deps?: BridgeAgentDeps);
620
+ /**
621
+ * Plan 2026-09-06 (agent voice settings, Task 7): pull this agent's voice
622
+ * settings row from the backend and hot-swap the fast-path prompt/config in
623
+ * place. Backend settings win over the credential's `voice` file block the
624
+ * moment a REAL row loads (spec §5 fallback order backend -> file -> none);
625
+ * on a failed fetch AFTER a previously successful one, keep the last good
626
+ * row rather than falling back — only the source's own health fields move.
627
+ *
628
+ * Controller ruling (PR B review, fix round 1): `version: 0` means "no row
629
+ * configured for this agent" — the endpoint returns the all-defaults shape
630
+ * (blank persona) rather than 404. Installing this on a daemon whose
631
+ * credential carries a hand-tuned `voice` file block (e.g. Cortina011) must
632
+ * NOT blank that persona. So a `version: 0` response with a file block
633
+ * present keeps the file persona/displayName/quirks, adopts only the
634
+ * proxy's `fastPath` (so the SecureComs proxy is still used for the LLM
635
+ * call), and reports source "file" / version 0 — exactly the file-only
636
+ * state, just routed through the proxy. Only `version >= 1` (a REAL row)
637
+ * makes the backend persona win. A `version: 0` response with NO file block
638
+ * has nothing to prefer, so it behaves as before: the (empty-persona)
639
+ * defaults row via the proxy, source "backend".
640
+ *
641
+ * Safe to call any time (start(), or the receive pump on a
642
+ * `voice_settings_changed` ping); never throws.
643
+ */
644
+ refetchVoiceSettings(): Promise<void>;
448
645
  /**
449
646
  * Enforce the inbound-buffer cap with a DROP-OLDEST policy.
450
647
  *
@@ -650,12 +847,105 @@ export declare class BridgeAgent {
650
847
  message_id: string;
651
848
  status: "sent";
652
849
  }>;
850
+ private vaultRoot;
851
+ private vaultAuthHeaders;
852
+ /** M6 (Task 11): idempotent get-or-create resolve of the enclave's hidden
853
+ * vault channel id — used as the fallback when a caller omits
854
+ * vaultChannelId. Mirrors connect-web's filesafe-api.ts resolveVaultChannel
855
+ * exactly (same endpoint, same caller_identity_id convention). */
856
+ private vaultResolveChannel;
857
+ /** Resolves the effective vault channel id: the caller-supplied value when
858
+ * present (unchanged behavior), else the M6 resolver (best-effort). */
859
+ private resolveVaultChannelId;
860
+ private vaultCid;
861
+ private vaultGetKekset;
862
+ private vaultCreateNode;
863
+ private vaultCommitNode;
864
+ /** Multipart POST the ciphertext to the presigned POST target — mirrors
865
+ * useVault.uploadFile's FormData construction (B6) exactly. */
866
+ private vaultPutBlob;
867
+ private vaultGetNodeForDownload;
868
+ private vaultGetBlob;
869
+ private vaultListNodesRest;
870
+ private vaultDeleteNodeRest;
871
+ /**
872
+ * Join the vault channel (if not already), resolve its live epoch, derive
873
+ * the MLS exporter secret at that epoch, and open the sealed kekset —
874
+ * verifying the header signer against the PERSISTED owner pin (TOFU-seeded
875
+ * on first open) and rejecting a generation rollback. Returns a
876
+ * generation→KEK lookup plus the CURRENT generation, exactly the shape
877
+ * vault.ts's `*Deps.exporterKekForGeneration`/`currentGeneration` want.
878
+ *
879
+ * Every kekset-open-time failure (signer not the pinned owner, bad
880
+ * signature, generation <= last seen) is re-thrown as
881
+ * {@link VaultIntegrityError} — same taxonomy as vault.ts's read-time
882
+ * checks, so the HTTP boundary maps ALL vault trust failures to one code.
883
+ */
884
+ private getVaultSession;
885
+ /** Read `path` off local disk, encrypt + wrap its DEK under the vault's
886
+ * current KEK generation, and create+upload+commit it as a new vault
887
+ * file node named `name` (which may differ from `path`'s basename). */
888
+ vaultWrite(enclaveId: string, vaultChannelId: string | null | undefined, path: string, name: string): Promise<{
889
+ node_id: string;
890
+ }>;
891
+ /** Download + verify + decrypt a vault file node. Returns the plaintext
892
+ * base64-encoded (the loopback boundary is JSON-only, mirroring how
893
+ * useVault.downloadFile returns `{name, bytes}` in-memory — here the
894
+ * caller already supplies nodeId so we return bytes, not a re-resolved
895
+ * name the bridge has no cached listing to look up). */
896
+ vaultRead(enclaveId: string, vaultChannelId: string | null | undefined, nodeId: string): Promise<{
897
+ bytes_b64: string;
898
+ }>;
899
+ /** List + decrypt names for the vault nodes under `parentId` (null = root). */
900
+ vaultList(enclaveId: string, vaultChannelId: string | null | undefined, parentId: string | null): Promise<{
901
+ nodes: VaultListNode[];
902
+ }>;
903
+ /** Delete a vault node. No crypto involved — the server drops an opaque
904
+ * row — but still requires the caller to have joined the vault channel
905
+ * (consistency with write/read/list; also surfaces channel_pending the
906
+ * same way if the vault channel Welcome hasn't landed yet). */
907
+ vaultDelete(enclaveId: string, vaultChannelId: string | null | undefined, nodeId: string): Promise<{
908
+ deleted: string;
909
+ bytes_freed: number;
910
+ }>;
653
911
  /** Task 3: delegates to the configured MemoryProvider (SecureComsLocal | Null). */
654
912
  recordMemory(input: import("@agentvault/connect-mls-store").MemoryRecordInput): Promise<{
655
913
  seq: number;
656
914
  deduped: boolean;
657
915
  }>;
658
916
  memoryContext(conversationId: string, limit?: number): Promise<import("@agentvault/connect-mls-store").MemoryContextResult>;
917
+ /**
918
+ * Task 4 (self-hosted fast voice): instant persona-voiced acknowledgement
919
+ * for a live voice turn, sent ahead of the full agent reply — fired from
920
+ * the ingestOnce text branch as `void this.runFastPath(...)`.
921
+ *
922
+ * MUST NEVER throw into ingestOnce: every failure path (memory lookup, the
923
+ * LLM call, normalization, or the send itself) is caught internally, counted
924
+ * in `fastPathFailed`, and logged at most once — WITHOUT the turn text or
925
+ * the model's reply (only the channel/message id, never conversation
926
+ * content, reaches stderr).
927
+ *
928
+ * Dedupe: `fastPathSeen` is keyed by messageId so a redelivered/duplicate
929
+ * InboxEntry (e.g. after an ack race) never fires the fast LLM — and never
930
+ * double-sends — twice for the same turn. The `fastReplyId(messageId)` send
931
+ * id is itself also deterministic, so even a duplicate call that slipped past the
932
+ * in-memory dedupe (e.g. a process restart) resolves to the same envelope
933
+ * message id rather than a second spoken reply.
934
+ *
935
+ * Room token reservation (controller ruling): `send()` charges the shared
936
+ * `room:<channelId>` bucket (see `enforceRateFloor`) for any non-DM
937
+ * channel. A fast-path ack spent from the SAME bucket could starve the
938
+ * runtime's own reply moments later, so the CALL SITE (the ingestOnce text
939
+ * branch) only calls `runFastPath` at all when the bucket has at least 2
940
+ * tokens available — one earmarked for this ack, one reserved for the
941
+ * runtime's reply — and, critically, decides the buffered turn's guidance
942
+ * text using that SAME decision. That decision must not be re-made (and
943
+ * cannot silently diverge) here: `runFastPath` itself no longer touches the
944
+ * room bucket at all, it only handles messageId dedupe and the LLM/send
945
+ * mechanics. See the call site's `fastPathRoomBlocked` for the skip +
946
+ * `fastPathSkipped` counting.
947
+ */
948
+ private runFastPath;
659
949
  /**
660
950
  * List channels in one enclave (default: the daemon's own home enclave).
661
951
  * Task 3: called per-*known* enclave by refreshChannelMeta so a
@@ -836,6 +1126,33 @@ export declare class BridgeAgent {
836
1126
  * (settled, always exactly once).
837
1127
  */
838
1128
  private sendActionFrame;
1129
+ /**
1130
+ * Task 13: the daemon's half of the `summarize` request (spec §3.5/§4.5).
1131
+ * Runs bounded (runSummarize's 60 s internal timeout) and, on success, sends
1132
+ * exactly one `call_summary`. On timeout or failure it posts nothing — the
1133
+ * client's own wait already renders "Summary unavailable" — so there is
1134
+ * nothing to catch here besides the send itself.
1135
+ */
1136
+ private handleSummarize;
1137
+ /**
1138
+ * A thin adapter over the daemon's one-shot completion seam. Voice-enabled
1139
+ * agents already have `this.fastLlm` (the self-hosted fast-path LLM); reuse
1140
+ * it here with the fixed summary prompt as a single user message. An agent
1141
+ * with no fast-path configured has no one-shot completion route at all in
1142
+ * this daemon (the full runtime only ever answers via the buffered
1143
+ * `send`/`drain` seam, which `summarize` deliberately bypasses — see the
1144
+ * dispatch arm above) — a runtime-backed variant is a follow-up. Per spec
1145
+ * §4.5/§5, a missing runtime is a FAILURE, not a fabricated answer: reject
1146
+ * so `runSummarize` logs and returns null and `handleSummarize` posts
1147
+ * nothing (identical to a genuine timeout from the client's point of view).
1148
+ */
1149
+ private completeForSummary;
1150
+ /** call_summary rides a system_event envelope with a body-level type, the
1151
+ * same shape sendActionFrame uses for agent_action. */
1152
+ private sendSummary;
1153
+ /** Minimal stderr logger for non-fatal daemon events (mirrors the ad hoc
1154
+ * `process.stderr.write` calls elsewhere in this file). */
1155
+ private log;
839
1156
  /**
840
1157
  * The coalescing throttle (review H4 / Global Constraint: ~1/sec per run).
841
1158
  * If the run has no dirty nodes, no-op. If the per-run bucket has no token,
@@ -927,6 +1244,20 @@ export declare class BridgeAgent {
927
1244
  retired_drained: number;
928
1245
  /** Track 5: cumulative artifact_share messages where download/decrypt/digest failed (fail-soft — acked, no redeliver). */
929
1246
  failed_attachments: number;
1247
+ /** Task 4 (self-hosted fast voice): cumulative fast-path replies actually sent. */
1248
+ fast_path_sent: number;
1249
+ /** Task 4 (self-hosted fast voice): cumulative fast-path attempts that errored (LLM call, normalization, or send) — fail-soft, never blocks the buffered turn. */
1250
+ fast_path_failed: number;
1251
+ /** Task 4 (self-hosted fast voice): cumulative fast-path turns skipped in a room because the send-floor bucket lacked headroom for both the ack and the runtime's reply. */
1252
+ fast_path_skipped: number;
1253
+ /** Plan 2026-09-06 (agent voice settings): version of the loaded backend
1254
+ * voice-settings row, or null when no backend row has ever loaded
1255
+ * (source is "file" or "none"). */
1256
+ voice_settings_version: number | null;
1257
+ /** Plan 2026-09-06: where the active voice config came from — "backend"
1258
+ * once a fetch has ever succeeded (sticky even if a later refetch
1259
+ * fails), else "file" (credential's voice block) or "none". */
1260
+ voice_settings_source: "backend" | "file" | "none";
930
1261
  /** 2026-07-03 latency triage: delivery pings received on the WS stream.
931
1262
  * 0 while messages flow = push is dead; inbound rides the 25s floor. */
932
1263
  delivery_pings: number;
package/dist/config.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type VoiceConfig } from "./voice/persona.js";
1
2
  export interface BridgeConfig {
2
3
  apiKey: string;
3
4
  identityId: string;
@@ -27,6 +28,8 @@ export interface BridgeConfig {
27
28
  memoryWorkspaceDir?: string;
28
29
  /** For memoryProvider="openclaw": total facts cap in UTF-16 code units. 0 = facts kill switch (preserved via ?? downstream, never ||). Absent/junk → provider default 8000. */
29
30
  memoryFactsCharBudget?: number;
31
+ /** Optional voice persona + fast-path LLM config (self-hosted fast voice reply). Undefined when the credential has no `voice` block. */
32
+ voice?: VoiceConfig;
30
33
  }
31
34
  export declare const REQUIRED: readonly ["api_key", "identity_id", "device_id", "signing_private_key", "enclave_id", "api_base"];
32
35
  export declare function loadConfig(opts: {
package/dist/errors.d.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * WebSocket returns `{type:"error", error, message, detail?}`. A client gets the
7
7
  * same `error` code regardless of which transport it used. (A2A §9.5/§10.6 pattern.)
8
8
  */
9
- export type BridgeErrorCode = "unauthorized" | "forbidden_origin" | "not_found" | "invalid_request" | "invalid_json" | "body_too_large" | "channel_pending" | "rate_limited" | "unknown_action_run" | "internal";
9
+ export type BridgeErrorCode = "unauthorized" | "forbidden_origin" | "not_found" | "invalid_request" | "invalid_json" | "body_too_large" | "channel_pending" | "rate_limited" | "unknown_action_run" | "vault_integrity_failed" | "internal";
10
10
  export interface BridgeErrorSpec {
11
11
  /** HTTP status this code maps to (the WS binding ignores it). */
12
12
  http: number;
@@ -37,7 +37,7 @@ export declare const memoryRecordSchema: z.ZodEffects<z.ZodObject<{
37
37
  role: "agent" | "peer";
38
38
  text: string;
39
39
  conversation_id: string;
40
- modality: "text" | "voice";
40
+ modality: "voice" | "text";
41
41
  message_id?: string | undefined;
42
42
  idempotency_key?: string | undefined;
43
43
  peer_id?: string | undefined;
@@ -45,7 +45,7 @@ export declare const memoryRecordSchema: z.ZodEffects<z.ZodObject<{
45
45
  role: "agent" | "peer";
46
46
  text: string;
47
47
  conversation_id: string;
48
- modality: "text" | "voice";
48
+ modality: "voice" | "text";
49
49
  message_id?: string | undefined;
50
50
  idempotency_key?: string | undefined;
51
51
  peer_id?: string | undefined;
@@ -53,7 +53,7 @@ export declare const memoryRecordSchema: z.ZodEffects<z.ZodObject<{
53
53
  role: "agent" | "peer";
54
54
  text: string;
55
55
  conversation_id: string;
56
- modality: "text" | "voice";
56
+ modality: "voice" | "text";
57
57
  message_id?: string | undefined;
58
58
  idempotency_key?: string | undefined;
59
59
  peer_id?: string | undefined;
@@ -61,7 +61,7 @@ export declare const memoryRecordSchema: z.ZodEffects<z.ZodObject<{
61
61
  role: "agent" | "peer";
62
62
  text: string;
63
63
  conversation_id: string;
64
- modality: "text" | "voice";
64
+ modality: "voice" | "text";
65
65
  message_id?: string | undefined;
66
66
  idempotency_key?: string | undefined;
67
67
  peer_id?: string | undefined;