@securecoms/agent-bridge 0.30.3 → 0.31.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/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const BRIDGE_VERSION = "0.30.3";
1
+ export declare const BRIDGE_VERSION = "0.31.0";
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Voice fast-path privacy (spec §2.1, OQ-1 ruled option (iii); amendments §2.2 A-1 to A-6): the CALL BUFFER.
3
+ *
4
+ * An in-process, per-channel list of the current call's spoken turns. It is the ONLY history a voice-scoped turn
5
+ * sees: it never reads the memory provider, the SecureComs store, the unified store or the 7b index. What enters it can
6
+ * reach the fast-complete proxy and OpenRouter, so its admission rules (enforced by the bridge, not here) are the
7
+ * privacy boundary:
8
+ * - peer entries: the text of a VS-1 or VS-2 turn only (never VS-3 or VS-4 alone), never a turn that failed owner
9
+ * verification;
10
+ * - agent entries: `sendSpoken` lines (origin "fast_path") and admitted runtime replies (origin "reply").
11
+ *
12
+ * Every reset signal (reset(), the idle rule, a restart) only REMOVES entries. No persistence of any kind.
13
+ *
14
+ * A-5: an entry's sequence number is internal (`bufSeq`, monotonic across all channels and resets, so a snapshot's
15
+ * end seq never collides with a later call's entries). Entries handed out by `recent` and `agentAfter` carry `seq: 0`,
16
+ * a value no store row has, so the bridge's store-meta readers can skip them.
17
+ *
18
+ * Final fix I-1: a peer entry is admitted once per turn id (the admitted-id set), so a redelivered row never re-enters
19
+ * the call and its re-snapshot never contains itself.
20
+ *
21
+ * A-4: the voice-scoped id set and the per-message seq notes exist ONLY to admit runtime replies and for the release
22
+ * re-read. No delivery gate may key on them: forgetting an id (a reset, the idle rule, the cap) only stops a later
23
+ * reply from being buffered, which fails closed.
24
+ */
25
+ /** Spec §2.1: 50 entries per channel, the `memoryForTurn` default limit. */
26
+ export declare const CALL_BUFFER_CAP = 50;
27
+ /** Spec §2.1 reset (c): 30 minutes with no append clears the channel. */
28
+ export declare const CALL_BUFFER_IDLE_MS: number;
29
+ /** Spec §2.1 bounds (audit finding 10a): the id set and the seq notes, as `taintedTurns` / `cleanTurns` (5000). */
30
+ export declare const CALL_BUFFER_ID_CAP = 5000;
31
+ /**
32
+ * A point in a channel's buffer: the newest bufSeq at that moment and the channel's reset generation then. Final fix
33
+ * M-4: `agentAfter` from a mark of an earlier generation returns nothing, so lines of a later call never join an older
34
+ * snapshot.
35
+ */
36
+ export interface CallBufferMark {
37
+ seq: number;
38
+ generation: number;
39
+ }
40
+ /** N1 (A-1): where an agent entry came from. Peer entries carry none. */
41
+ export type CallBufferOrigin = "fast_path" | "reply";
42
+ /** One entry as handed out: shaped to `MemoryTurn` (`seq` required), with `seq` always 0 (A-5). */
43
+ export interface CallBufferTurn {
44
+ role: "peer" | "agent";
45
+ text: string;
46
+ seq: 0;
47
+ origin?: CallBufferOrigin;
48
+ }
49
+ export interface CallBufferOptions {
50
+ cap: number;
51
+ idleMs: number;
52
+ now: () => number;
53
+ /** False when the agent's memory provider is "none": appends and marks are dropped (one guard, at the write), so reads return nothing. */
54
+ enabled: boolean;
55
+ }
56
+ export declare class CallBuffer {
57
+ private readonly opts;
58
+ private readonly channels;
59
+ /** messageId -> channelId, insertion ordered (oldest first) for the cap. */
60
+ private readonly voiceIds;
61
+ /** messageId -> { channelId, the channel's bufSeq at the mark, the channel's generation then }. */
62
+ private readonly seqNotes;
63
+ /**
64
+ * Final fix I-1: messageId -> { channelId, the channel's bufSeq just before the turn's own peer entry }. A row the
65
+ * backend redelivers (a failed ack, runtime_inbox.full backpressure) is ingested again; its id here means its text is
66
+ * already in the call, so it is not appended again and its snapshot stops before its own entry (A-6).
67
+ */
68
+ private readonly admitted;
69
+ /** The last bufSeq handed out per channel (kept across resets, so the sequence never runs backwards). */
70
+ private readonly lastSeq;
71
+ private nextSeq;
72
+ /** Per-channel reset generation (advanced by every reset, the idle rule included). */
73
+ private readonly gens;
74
+ constructor(opts: CallBufferOptions);
75
+ /**
76
+ * Append a spoken turn's text, once per turn id (final fix I-1). Returns false (and appends nothing) when the id was
77
+ * already admitted in this call, or when the buffer is disabled.
78
+ */
79
+ appendPeer(channelId: string, text: string, messageId: string): boolean;
80
+ /**
81
+ * Final fix I-1: for a turn already admitted in this call, the channel's bufSeq just before its own entry (the end seq
82
+ * of its first snapshot); undefined for a turn not admitted (or forgotten by a reset, the idle rule or the id cap).
83
+ */
84
+ admittedAt(messageId: string): number | undefined;
85
+ appendAgent(channelId: string, text: string, origin: CallBufferOrigin): void;
86
+ /**
87
+ * Record a voice-scoped turn's id (for reply admission only, A-4) and note the channel's seq at this point. Final fix
88
+ * I-1: a turn already admitted (a redelivered row) keeps the seq of its first admission, so the note never moves past
89
+ * the turn's own entry.
90
+ */
91
+ markVoiceScoped(channelId: string, messageId: string): void;
92
+ /** A-4: read ONLY by the bridge's `afterPublish` reply admission. */
93
+ isVoiceScopedId(messageId: string): boolean;
94
+ /** The channel's mark noted when `messageId` was marked (the release re-read), or undefined (e.g. after a restart). */
95
+ seqNoted(messageId: string): CallBufferMark | undefined;
96
+ /** The bufSeq of the channel's newest entry (0 before any append). A snapshot's end seq. */
97
+ seqAt(channelId: string): number;
98
+ /**
99
+ * The channel's last `limit` entries, oldest first. With `upTo` (final fix I-1, a redelivered turn's re-snapshot),
100
+ * only entries at or below that bufSeq.
101
+ */
102
+ recent(channelId: string, limit: number, upTo?: number): CallBufferTurn[];
103
+ /**
104
+ * The channel's AGENT entries appended after `since.seq` (lines spoken or answered since a snapshot), oldest first.
105
+ * Final fix M-4: nothing when the channel has been reset since the mark (a later call's lines).
106
+ */
107
+ agentAfter(channelId: string, since: CallBufferMark): CallBufferTurn[];
108
+ /**
109
+ * The channel's reset generation: it changes on every reset (explicit or idle) and on nothing else. A caller that
110
+ * holds text across an await (the coalesce window) compares it to skip an append into a call that has since ended.
111
+ */
112
+ generation(channelId: string): number;
113
+ /** Clear the channel: its entries, its voice-scoped ids, its seq notes and its admitted ids. Only ever removes. */
114
+ reset(channelId: string): void;
115
+ private append;
116
+ /** Reset (c): more than `idleMs` since the channel's last append clears it, checked on every read and append. */
117
+ private expireIfIdle;
118
+ }
@@ -1,5 +1,12 @@
1
1
  import { type VoiceSettings } from "./settings.js";
2
2
  export declare const CONTINUITY_CLAUSE: string;
3
+ /**
4
+ * Voice fast-path privacy, spec §3 OQ-3 (ruled 2026-10-18): the clause that ends the fast prompt (and, Task 6, a
5
+ * voice-scoped runtime frame's guidance) in place of CONTINUITY_CLAUSE. On a voice-scoped turn the model sees only the
6
+ * call's own spoken turns, so it is told so and given a safe script instead of being told it sees the whole chat.
7
+ * Model instruction, not user copy. CONTINUITY_CLAUSE itself is unchanged (the compiled prompt, the worker).
8
+ */
9
+ export declare const VOICE_CALL_CLAUSE: string;
3
10
  /**
4
11
  * `displayName` is legacy (the credential file's `voice.persona.display_name`,
5
12
  * or the backend's voice settings `display_name`). It is still parsed so old
@@ -19,6 +26,8 @@ export interface FastPathConfig {
19
26
  apiKey?: string;
20
27
  timeoutMs: number;
21
28
  maxTokens: number;
29
+ /** Extra request headers. Set only by settingsToVoiceConfig (the SecureComs proxy); never by parseVoiceConfig. */
30
+ headers?: Readonly<Record<string, string>>;
22
31
  }
23
32
  export interface VoiceConfig {
24
33
  persona: VoicePersona;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Voice fast-path privacy (spec §1; OQ-2 ruled): a turn is VOICE-SCOPED when ANY of these holds, evaluated once at
3
+ * ingest from authenticated or narrowing-only inputs:
4
+ * VS-1 `speaker !== undefined`: a relay-attributed spoken turn (the body claims `relay: true` with both speaker strings
5
+ * AND the envelope sender's roster row is `is_relay`);
6
+ * VS-2 `transcribed === true`: the E2E body's own claim (a peer that forges it only narrows its own turn);
7
+ * VS-3 the envelope sender's roster row is `is_relay`: a relay-sent text is not a human's typed turn;
8
+ * VS-4 the turn text is exactly the call-start marker (after trim).
9
+ * A typed message sent during a live call (server `voiceMode` true, none of VS-1 to VS-4) is NOT voice-scoped.
10
+ * A voice-scoped verdict narrows what the turn's model sees and the owner's grants (G-1); no disjunct may widen anything.
11
+ */
12
+ export declare function isVoiceScopedTurn(t: {
13
+ speaker: {
14
+ identityId: string;
15
+ } | undefined;
16
+ transcribed: unknown;
17
+ senderIsRelay: boolean;
18
+ text: string;
19
+ }): boolean;
20
+ /**
21
+ * Task 4b (spec §1 split table, audit round 1 finding 3): a voice-scoped turn's text is admitted to the call buffer
22
+ * only when VS-1 (relay speaker) or VS-2 (the body's own `transcribed`) holds. VS-3 (the server's `is_relay` flag
23
+ * alone) and VS-4 (the call-start marker, an instruction, not speech) never admit: what a forged admitting disjunct can
24
+ * add is only the sender's own words.
25
+ */
26
+ export declare function admitsToCallBuffer(t: {
27
+ speaker: {
28
+ identityId: string;
29
+ } | undefined;
30
+ transcribed: unknown;
31
+ }): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@securecoms/agent-bridge",
3
- "version": "0.30.3",
3
+ "version": "0.31.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24"