@securecoms/agent-bridge 0.11.2 → 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.
- package/README.md +75 -0
- package/dist/bin.js +2657 -93
- package/dist/bridge-agent.d.ts +331 -0
- package/dist/config.d.ts +3 -0
- package/dist/errors.d.ts +1 -1
- package/dist/http-server.d.ts +4 -4
- package/dist/index.js +2674 -112
- package/dist/memory-taps.d.ts +6 -2
- package/dist/rate-limit.d.ts +10 -0
- package/dist/sodium-ready.d.ts +16 -8
- package/dist/summarize.d.ts +54 -0
- package/dist/voice/fast-client.d.ts +13 -0
- package/dist/voice/fast-prompt.d.ts +53 -0
- package/dist/voice/persona.d.ts +23 -0
- package/dist/voice/prompt-compiler.d.ts +4 -0
- package/dist/voice/settings.d.ts +31 -0
- package/package.json +7 -1
- package/dist/attachments.js +0 -49
- package/dist/auth.js +0 -56
- package/dist/bin.js.pre-c1-f803007 +0 -5795
- package/dist/bin.js.pre-c3-096a9a3 +0 -5832
- package/dist/bin.js.pre-ffrebuild-b162d4a +0 -5639
- package/dist/bin.js.pre-kpheal-20260708-150924 +0 -5531
- package/dist/bin.js.pre-kppersist-20260708-152341 +0 -5543
- package/dist/bin.js.pre-p0-20260708-102415 +0 -5196
- package/dist/bin.js.pre-p1b-02f47e1 +0 -5639
- package/dist/bin.js.pre-warmfix-20260708-153705 +0 -5613
- package/dist/bin.js.pretrace-113205 +0 -5395
- package/dist/bridge-agent.js +0 -831
- package/dist/config.js +0 -74
- package/dist/coordination.js +0 -63
- package/dist/errors.js +0 -25
- package/dist/http-server.js +0 -220
- package/dist/index.js.pre-p0-20260708-102415 +0 -4879
- package/dist/latency.js +0 -87
- package/dist/rate-limit.js +0 -56
- package/dist/service/config.js +0 -26
- package/dist/service/install.js +0 -60
- package/dist/service/launchd.js +0 -30
- package/dist/service/systemd.js +0 -25
- package/dist/ws-server.js +0 -254
package/dist/bridge-agent.d.ts
CHANGED
|
@@ -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;
|
package/dist/http-server.d.ts
CHANGED
|
@@ -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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
64
|
+
modality: "voice" | "text";
|
|
65
65
|
message_id?: string | undefined;
|
|
66
66
|
idempotency_key?: string | undefined;
|
|
67
67
|
peer_id?: string | undefined;
|