@securecoms/agent-bridge 0.25.4 → 0.26.1

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.
@@ -31,10 +31,10 @@ export type ChannelObservation = {
31
31
  import { type OwnerAuthorityStore, type OwnerPin } from "@agentvault/connect-clients-core";
32
32
  import type { BridgeConfig } from "./config.js";
33
33
  import { LatencyRecorder } from "./latency.js";
34
- import type { AttachmentMeta } from "./attachments.js";
34
+ import type { AttachmentMeta, VaultRefEntry } from "./attachments.js";
35
35
  import { type ChatMessage } from "./voice/fast-prompt.js";
36
36
  import { VOICE_CALL_START_MARKER } from "./voice/prompt-compiler.js";
37
- import { type VaultListNode } from "./vault.js";
37
+ import { type VaultListNode, type VaultReadExpectations } from "./vault.js";
38
38
  import type { AgentActionNodeType, AgentActionNodeStatus, AgentActionNodeDetail } from "@agentvault/connect-core";
39
39
  import type { RuntimeReplyReceipt } from "./reply-receipts.js";
40
40
  export type AgentAudience = "owner_dm" | "team_room" | "partner_room" | "other_human_dm" | "agent_to_agent" | "unknown";
@@ -94,6 +94,16 @@ export interface BridgeMessage {
94
94
  * Undefined for plain `text` messages.
95
95
  */
96
96
  attachments?: AttachmentMeta[];
97
+ /**
98
+ * Files link (`vault_ref`, review 2026-10-01 section 6): references to
99
+ * FileSafe items, NOT fetched. Open one with POST /v1/vault-read passing
100
+ * `enclave_id`, `node_id`, `expected_content_hash` = `content_hash` and
101
+ * `expected_dek_commit` = `dek_commit`; the daemon refuses it if the item
102
+ * changed after it was shared. `name_hint`/`mime_hint`/`size_hint` are
103
+ * UNTRUSTED claims by the sender, data only (like `speaker`): never a path,
104
+ * an instruction or an authorization input. Undefined for every other turn.
105
+ */
106
+ vault_refs?: VaultRefEntry[];
97
107
  /**
98
108
  * Task 4 (voice-mode carrier): true when the InboxEntry that produced this
99
109
  * buffered message had `voiceMode` set by the server (the agent device's
@@ -129,6 +139,8 @@ export interface BridgeMessage {
129
139
  * for voice-flagged turns. Selected by `InboxEntry.voiceMode` in the entry
130
140
  * loop below — never transmitted, never a playback trigger.
131
141
  */
142
+ /** Files link: the turn text of a `vault_ref` that carries no caption. */
143
+ export declare const VAULT_REF_BARE_TEXT = "[Files link]";
132
144
  export declare const VOICE_MODE_GUIDANCE = "Reply immediately and conversationally, one short sentence per message; never bundle multiple points into one message.";
133
145
  /**
134
146
  * Task 2 (room voice guidance): compiled reply guidance for a voice-flagged
@@ -370,7 +382,7 @@ export declare class InvalidMessageIdError extends Error {
370
382
  * `messageId`, formatted as an RFC 4122 UUID. Live finding 2026-09-06: the
371
383
  * previous `fast-<messageId>` string failed `resolveMessageId` ("message_id
372
384
  * must be a UUID") on every turn, so the fast path never sent once. Same input
373
- * → same UUID, so the idempotency argument in `runFastPath`'s doc still holds.
385
+ * → same UUID, so the idempotency argument in `runFastReply`'s doc still holds.
374
386
  *
375
387
  * SEC-06 #18: KEYED with the identity-seed-derived reply key and bound to the
376
388
  * agent and channel. It used to be SHA-256 of `fast:<messageId>` alone, which
@@ -678,6 +690,8 @@ export declare class BridgeAgent {
678
690
  * counter is incremented. Surfaced on health()/metrics().
679
691
  */
680
692
  private failedAttachments;
693
+ /** Files link: vault_ref bodies refused by R1 (strict parse) or R2 (enclave binding). */
694
+ private refusedVaultRefs;
681
695
  /**
682
696
  * Task 4 (self-hosted fast voice): test/deploy seam for the fast-path LLM
683
697
  * client. Undefined whenever `cfg.voice?.fastPath.enabled` is not true —
@@ -718,6 +732,16 @@ export declare class BridgeAgent {
718
732
  private fastPathFailed;
719
733
  /** 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(). */
720
734
  private fastPathSkipped;
735
+ /** Off-cycle fast replies: per-channel promise chains + the in-flight job count. */
736
+ private readonly fastReplyChains;
737
+ private fastReplyJobs;
738
+ /** Room send-floor tokens reserved by queued fast replies, per channel (F3). */
739
+ private readonly fastReplyReserved;
740
+ private static readonly FAST_REPLY_MAX_JOBS;
741
+ /** Fast-path turns handed to the runtime behind a spoken ack (LLM [[DEFER]], pre-check, or ack_only). */
742
+ private fastPathDeferred;
743
+ /** Of those, `full`-mode turns the deterministic pre-check deferred without calling the fast model. */
744
+ private fastPathIntentDeferred;
721
745
  /**
722
746
  * Task 8: count of live AUTHENTICATED /v1/ws sessions, so a later `status`
723
747
  * verb can tell "bridge process up" (the app's green dot, which only
@@ -1581,7 +1605,7 @@ export declare class BridgeAgent {
1581
1605
  * useVault.downloadFile returns `{name, bytes}` in-memory — here the
1582
1606
  * caller already supplies nodeId so we return bytes, not a re-resolved
1583
1607
  * name the bridge has no cached listing to look up). */
1584
- vaultRead(enclaveId: string, vaultChannelId: string | null | undefined, nodeId: string): Promise<{
1608
+ vaultRead(enclaveId: string, vaultChannelId: string | null | undefined, nodeId: string, expected?: VaultReadExpectations): Promise<{
1585
1609
  bytes_b64: string;
1586
1610
  }>;
1587
1611
  /** List + decrypt names for the vault nodes under `parentId` (null = root). */
@@ -1618,37 +1642,69 @@ export declare class BridgeAgent {
1618
1642
  private memoryForTurn;
1619
1643
  memoryContext(conversationId: string, limit?: number): Promise<import("@agentvault/connect-mls-store").MemoryContextResult>;
1620
1644
  /**
1621
- * Task 4 (self-hosted fast voice): instant persona-voiced acknowledgement
1622
- * for a live voice turn, sent ahead of the full agent reply — fired from
1623
- * the ingestOnce text branch as `void this.runFastPath(...)`.
1624
- *
1625
- * MUST NEVER throw into ingestOnce: every failure path (memory lookup, the
1626
- * LLM call, normalization, or the send itself) is caught internally, counted
1627
- * in `fastPathFailed`, and logged at most once — WITHOUT the turn text or
1628
- * the model's reply (only the channel/message id, never conversation
1629
- * content, reaches stderr).
1630
- *
1631
- * Dedupe: `fastPathSeen` is keyed by messageId so a redelivered/duplicate
1632
- * InboxEntry (e.g. after an ack race) never fires the fast LLM — and never
1633
- * double-sends — twice for the same turn. The `fastReplyId(...)` send
1634
- * id is itself also deterministic, so even a duplicate call that slipped past the
1635
- * in-memory dedupe (e.g. a process restart) resolves to the same envelope
1636
- * message id rather than a second spoken reply.
1637
- *
1638
- * Room token reservation (controller ruling): `send()` charges the shared
1639
- * `room:<channelId>` bucket (see `enforceRateFloor`) for any non-DM
1640
- * channel. A fast-path ack spent from the SAME bucket could starve the
1641
- * runtime's own reply moments later, so the CALL SITE (the ingestOnce text
1642
- * branch) only calls `runFastPath` at all when the bucket has at least 2
1643
- * tokens available — one earmarked for this ack, one reserved for the
1644
- * runtime's reply — and, critically, decides the buffered turn's guidance
1645
- * text using that SAME decision. That decision must not be re-made (and
1646
- * cannot silently diverge) here: `runFastPath` itself no longer touches the
1647
- * room bucket at all, it only handles messageId dedupe and the LLM/send
1648
- * mechanics. See the call site's `fastPathRoomBlocked` for the skip +
1649
- * `fastPathSkipped` counting.
1650
- */
1651
- private runFastPath;
1645
+ * Voice front desk, step 1 — decided SYNCHRONOUSLY inside the ingest cycle,
1646
+ * so the cycle never waits on a model call or a publish (Cortina011
1647
+ * recv.cycle max 10.8 s came from two inline fast replies back to back).
1648
+ * Pure: it changes no state; the caller commits a plan (markFastPathSeen,
1649
+ * counters) only after the turn is durably staged.
1650
+ * - "dup": this turn already went through the fast path (a redelivery);
1651
+ * it is neither spoken again nor handed to the runtime again.
1652
+ * - "skip": no fast path; the runtime gets the turn with plain guidance
1653
+ * (mode `off`, no fast client, or the reply queue is full).
1654
+ * - "ack": the turn certainly goes to the runtime (mode `ack_only`, or the
1655
+ * deterministic pre-check in `full`): `line` is spoken first.
1656
+ * - "fast": the fast model (or, in `ack_only`, the greeting template)
1657
+ * decides whether the runtime answers next.
1658
+ * Owner report 2026-10-02 (calls never used tools) motivates "ack".
1659
+ */
1660
+ private planFastPath;
1661
+ private markFastPathSeen;
1662
+ /**
1663
+ * Off-cycle per-channel queue: one promise chain per channel, so within a
1664
+ * channel spoken lines publish, and runtime frames are released, strictly
1665
+ * in turn order (a turn's ack always publishes before its frame reaches
1666
+ * the runtime). `spends` marks a job that will publish a fast reply: those
1667
+ * are bounded by FAST_REPLY_MAX_JOBS (planFastPath degrades to "skip") and
1668
+ * reserve a room send-floor token until they finish. A job never throws.
1669
+ */
1670
+ private enqueueFastReply;
1671
+ /**
1672
+ * Stage a runtime frame without overtaking a turn of the same channel that
1673
+ * is still queued behind the fast path (F2): while the channel has a queue,
1674
+ * the frame is staged durably but held, and released in queue order.
1675
+ */
1676
+ private stageRuntimeMessageInOrder;
1677
+ /** Test seam: resolves once every queued fast reply has settled. */
1678
+ settleFastReplies(): Promise<void>;
1679
+ /** Speak a fixed acknowledgement line; failures are counted, never thrown. */
1680
+ private sendFastLine;
1681
+ /**
1682
+ * Voice front desk, step 2 for a "fast" plan (runs off the ingest cycle):
1683
+ * the fast model's reply — or, in `ack_only`, the greeting template — is
1684
+ * spoken, and the outcome decides whether the runtime answers next.
1685
+ *
1686
+ * MUST NEVER throw: every failure path (memory lookup, the LLM call,
1687
+ * normalization, or the send itself) is caught, counted in
1688
+ * `fastPathFailed`, and logged WITHOUT the turn text or the model's reply.
1689
+ * The `fastReplyId(...)` send id is deterministic, so a duplicate that
1690
+ * slipped past the in-memory dedupe (e.g. a restart) resolves to the same
1691
+ * envelope message id rather than a second spoken reply.
1692
+ *
1693
+ * Room token reservation (controller ruling): the CALL SITE only plans a
1694
+ * fast path when the room bucket has headroom for this reply AND the
1695
+ * runtime's, and decides the buffered guidance from that same decision.
1696
+ */
1697
+ private runFastReply;
1698
+ /**
1699
+ * Durably stage a runtime frame WITHOUT handing it to the runtime yet (a
1700
+ * "fast" plan: the fast reply decides). Same durability and backpressure
1701
+ * as stageRuntimeMessage; a restart restores it as an ordinary pending
1702
+ * frame (the "fast path failed" semantics), so a crash mid-call never loses
1703
+ * the turn.
1704
+ */
1705
+ private stageHeldRuntimeMessage;
1706
+ /** Settle a held frame: the fast path answered alone (retire it) or the runtime answers next (release it, in channel order). */
1707
+ private settleHeldRuntimeMessage;
1652
1708
  /**
1653
1709
  * List channels in one enclave (default: the daemon's own home enclave).
1654
1710
  * Task 3: called per-*known* enclave by refreshChannelMeta so a
@@ -1703,6 +1759,15 @@ export declare class BridgeAgent {
1703
1759
  * 5xx, and genuine non-member 404s only.
1704
1760
  */
1705
1761
  private refreshChannelMeta;
1762
+ /**
1763
+ * Cache server-listed channel metadata (is_dm, name) for the receive cycle.
1764
+ * The ONLY input is a `listChannels` result, the same backend rows
1765
+ * refreshChannelMeta has always cached: refreshChannelMeta and the
1766
+ * self-heal sweep both feed it, so the sweep's own listing (every 90-120 s)
1767
+ * keeps the 5 min cache warm and a turn arriving after idle does not pay a
1768
+ * listing round trip per enclave before it reaches the runtime.
1769
+ */
1770
+ private recordListedChannelMeta;
1706
1771
  /** Cache for the server-side enclave enumeration (see enclavesToCover). */
1707
1772
  private enclaveEnumCache;
1708
1773
  /** Enclaves whose channel listing failed on the NEWEST sweep (audit HIGH:
@@ -2142,12 +2207,20 @@ export declare class BridgeAgent {
2142
2207
  custody_store_unavailable: boolean;
2143
2208
  /** Track 5: cumulative artifact_share messages where download/decrypt/digest failed (fail-soft — acked, no redeliver). */
2144
2209
  failed_attachments: number;
2210
+ /** Files link: vault_ref bodies refused by R1 (strict parse) or R2 (enclave binding), acked. */
2211
+ refused_vault_refs: number;
2145
2212
  /** Task 4 (self-hosted fast voice): cumulative fast-path replies actually sent. */
2146
2213
  fast_path_sent: number;
2147
2214
  /** Task 4 (self-hosted fast voice): cumulative fast-path attempts that errored (LLM call, normalization, or send) — fail-soft, never blocks the buffered turn. */
2148
2215
  fast_path_failed: number;
2149
2216
  /** 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. */
2150
2217
  fast_path_skipped: number;
2218
+ /** Fast-path turns handed to the runtime behind a spoken ack (LLM [[DEFER]], pre-check, or ack_only). */
2219
+ fast_path_deferred: number;
2220
+ /** `full`-mode turns deferred by the deterministic pre-check (no fast model call). */
2221
+ fast_path_intent_deferred: number;
2222
+ /** Credential `voice_fast_path_mode`: "full" (default), "ack_only" or "off". */
2223
+ fast_path_mode: "full" | "ack_only" | "off";
2151
2224
  /** Plan 2026-09-06 (agent voice settings): version of the loaded backend
2152
2225
  * voice-settings row, or null when no backend row has ever loaded
2153
2226
  * (source is "file" or "none"). */
@@ -2292,6 +2365,8 @@ export declare class BridgeAgent {
2292
2365
  welcomeRequestsSent: number;
2293
2366
  /** Track 5: cumulative artifact_share failures (download/decrypt/digest). */
2294
2367
  failedAttachments: number;
2368
+ /** Files link: vault_ref bodies refused by R1 (strict parse) or R2 (enclave binding), acked. */
2369
+ refusedVaultRefs: number;
2295
2370
  /** SDK-6: bridge build id, so a /v1/metrics reading captured across a daemon
2296
2371
  * restart can be attributed to a version — counters are process-lifetime
2297
2372
  * and zero on restart (spec §5.5). */
package/dist/config.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type VoiceConfig } from "./voice/persona.js";
2
+ import { type FastPathMode } from "./voice/defer-intent.js";
2
3
  export interface BridgeConfig {
3
4
  apiKey: string;
4
5
  identityId: string;
@@ -30,6 +31,13 @@ export interface BridgeConfig {
30
31
  memoryFactsCharBudget?: number;
31
32
  /** Optional voice persona + fast-path LLM config (self-hosted fast voice reply). Undefined when the credential has no `voice` block. */
32
33
  voice?: VoiceConfig;
34
+ /**
35
+ * Voice fast-path mode (credential top-level `voice_fast_path_mode`):
36
+ * "full" (default), "ack_only" or "off" — see voice/defer-intent.ts. Kept
37
+ * outside `voice` because backend-managed agents have no `voice` block and
38
+ * a backend voice-settings refetch replaces `voice` wholesale.
39
+ */
40
+ voiceFastPathMode?: FastPathMode;
33
41
  }
34
42
  export declare const REQUIRED: readonly ["api_key", "identity_id", "device_id", "signing_private_key", "enclave_id", "api_base"];
35
43
  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" | "agent_paused" | "send_forbidden" | "group_superseded" | "unknown_action_run" | "vault_integrity_failed" | "internal";
9
+ export type BridgeErrorCode = "unauthorized" | "forbidden_origin" | "not_found" | "invalid_request" | "invalid_json" | "body_too_large" | "channel_pending" | "rate_limited" | "agent_paused" | "send_forbidden" | "group_superseded" | "unknown_action_run" | "vault_integrity_failed" | "vault_ref_changed" | "internal";
10
10
  export interface BridgeErrorSpec {
11
11
  /** HTTP status this code maps to (the WS binding ignores it). */
12
12
  http: number;
@@ -43,6 +43,7 @@ export declare function capabilities(opts: {
43
43
  receive_modes: string[];
44
44
  send_types: string[];
45
45
  webhook_push: boolean;
46
+ vault_ref: number;
46
47
  };
47
48
  export declare const memoryRecordSchema: z.ZodEffects<z.ZodObject<{
48
49
  conversation_id: z.ZodString;
@@ -56,7 +57,7 @@ export declare const memoryRecordSchema: z.ZodEffects<z.ZodObject<{
56
57
  text: string;
57
58
  role: "agent" | "peer";
58
59
  conversation_id: string;
59
- modality: "voice" | "text";
60
+ modality: "text" | "voice";
60
61
  message_id?: string | undefined;
61
62
  idempotency_key?: string | undefined;
62
63
  peer_id?: string | undefined;
@@ -64,7 +65,7 @@ export declare const memoryRecordSchema: z.ZodEffects<z.ZodObject<{
64
65
  text: string;
65
66
  role: "agent" | "peer";
66
67
  conversation_id: string;
67
- modality: "voice" | "text";
68
+ modality: "text" | "voice";
68
69
  message_id?: string | undefined;
69
70
  idempotency_key?: string | undefined;
70
71
  peer_id?: string | undefined;
@@ -72,7 +73,7 @@ export declare const memoryRecordSchema: z.ZodEffects<z.ZodObject<{
72
73
  text: string;
73
74
  role: "agent" | "peer";
74
75
  conversation_id: string;
75
- modality: "voice" | "text";
76
+ modality: "text" | "voice";
76
77
  message_id?: string | undefined;
77
78
  idempotency_key?: string | undefined;
78
79
  peer_id?: string | undefined;
@@ -80,7 +81,7 @@ export declare const memoryRecordSchema: z.ZodEffects<z.ZodObject<{
80
81
  text: string;
81
82
  role: "agent" | "peer";
82
83
  conversation_id: string;
83
- modality: "voice" | "text";
84
+ modality: "text" | "voice";
84
85
  message_id?: string | undefined;
85
86
  idempotency_key?: string | undefined;
86
87
  peer_id?: string | undefined;