@securecoms/agent-bridge 0.26.0 → 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.
@@ -382,7 +382,7 @@ export declare class InvalidMessageIdError extends Error {
382
382
  * `messageId`, formatted as an RFC 4122 UUID. Live finding 2026-09-06: the
383
383
  * previous `fast-<messageId>` string failed `resolveMessageId` ("message_id
384
384
  * must be a UUID") on every turn, so the fast path never sent once. Same input
385
- * → 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.
386
386
  *
387
387
  * SEC-06 #18: KEYED with the identity-seed-derived reply key and bound to the
388
388
  * agent and channel. It used to be SHA-256 of `fast:<messageId>` alone, which
@@ -732,6 +732,16 @@ export declare class BridgeAgent {
732
732
  private fastPathFailed;
733
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(). */
734
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;
735
745
  /**
736
746
  * Task 8: count of live AUTHENTICATED /v1/ws sessions, so a later `status`
737
747
  * verb can tell "bridge process up" (the app's green dot, which only
@@ -1632,37 +1642,69 @@ export declare class BridgeAgent {
1632
1642
  private memoryForTurn;
1633
1643
  memoryContext(conversationId: string, limit?: number): Promise<import("@agentvault/connect-mls-store").MemoryContextResult>;
1634
1644
  /**
1635
- * Task 4 (self-hosted fast voice): instant persona-voiced acknowledgement
1636
- * for a live voice turn, sent ahead of the full agent reply — fired from
1637
- * the ingestOnce text branch as `void this.runFastPath(...)`.
1638
- *
1639
- * MUST NEVER throw into ingestOnce: every failure path (memory lookup, the
1640
- * LLM call, normalization, or the send itself) is caught internally, counted
1641
- * in `fastPathFailed`, and logged at most once — WITHOUT the turn text or
1642
- * the model's reply (only the channel/message id, never conversation
1643
- * content, reaches stderr).
1644
- *
1645
- * Dedupe: `fastPathSeen` is keyed by messageId so a redelivered/duplicate
1646
- * InboxEntry (e.g. after an ack race) never fires the fast LLM — and never
1647
- * double-sends — twice for the same turn. The `fastReplyId(...)` send
1648
- * id is itself also deterministic, so even a duplicate call that slipped past the
1649
- * in-memory dedupe (e.g. a process restart) resolves to the same envelope
1650
- * message id rather than a second spoken reply.
1651
- *
1652
- * Room token reservation (controller ruling): `send()` charges the shared
1653
- * `room:<channelId>` bucket (see `enforceRateFloor`) for any non-DM
1654
- * channel. A fast-path ack spent from the SAME bucket could starve the
1655
- * runtime's own reply moments later, so the CALL SITE (the ingestOnce text
1656
- * branch) only calls `runFastPath` at all when the bucket has at least 2
1657
- * tokens available — one earmarked for this ack, one reserved for the
1658
- * runtime's reply — and, critically, decides the buffered turn's guidance
1659
- * text using that SAME decision. That decision must not be re-made (and
1660
- * cannot silently diverge) here: `runFastPath` itself no longer touches the
1661
- * room bucket at all, it only handles messageId dedupe and the LLM/send
1662
- * mechanics. See the call site's `fastPathRoomBlocked` for the skip +
1663
- * `fastPathSkipped` counting.
1664
- */
1665
- 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;
1666
1708
  /**
1667
1709
  * List channels in one enclave (default: the daemon's own home enclave).
1668
1710
  * Task 3: called per-*known* enclave by refreshChannelMeta so a
@@ -1717,6 +1759,15 @@ export declare class BridgeAgent {
1717
1759
  * 5xx, and genuine non-member 404s only.
1718
1760
  */
1719
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;
1720
1771
  /** Cache for the server-side enclave enumeration (see enclavesToCover). */
1721
1772
  private enclaveEnumCache;
1722
1773
  /** Enclaves whose channel listing failed on the NEWEST sweep (audit HIGH:
@@ -2164,6 +2215,12 @@ export declare class BridgeAgent {
2164
2215
  fast_path_failed: number;
2165
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. */
2166
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";
2167
2224
  /** Plan 2026-09-06 (agent voice settings): version of the loaded backend
2168
2225
  * voice-settings row, or null when no backend row has ever loaded
2169
2226
  * (source is "file" or "none"). */
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: {