@securecoms/agent-bridge 0.21.0 → 0.24.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 +91 -3
- package/connectors/hermes/adapter.py +63 -7
- package/connectors/hermes/tests/test_adapter_wiring.py +23 -0
- package/connectors/hermes/tests/test_logging_privacy.py +61 -0
- package/connectors/openclaw/openclaw-entry.inbound.test.ts +561 -0
- package/connectors/openclaw/openclaw-entry.js +270 -47
- package/connectors/openclaw/openclaw-entry.smoke.test.ts +26 -0
- package/dist/bin.d.ts +3 -7
- package/dist/bin.js +7626 -954
- package/dist/bridge-agent.d.ts +846 -30
- package/dist/coordination.d.ts +112 -5
- package/dist/decrypt-error-log-limiter.d.ts +6 -30
- package/dist/errors.d.ts +1 -1
- package/dist/http-server.d.ts +24 -5
- package/dist/index.js +7608 -927
- package/dist/reply-intent.d.ts +68 -0
- package/dist/service-log.d.ts +7 -0
- package/dist/summarize.d.ts +3 -2
- package/dist/version.d.ts +1 -1
- package/dist/voice/fast-client.d.ts +12 -2
- package/dist/ws-server.d.ts +37 -3
- package/package.json +4 -1
package/dist/bridge-agent.d.ts
CHANGED
|
@@ -36,7 +36,7 @@ import { type ChatMessage } from "./voice/fast-prompt.js";
|
|
|
36
36
|
import { VOICE_CALL_START_MARKER } from "./voice/prompt-compiler.js";
|
|
37
37
|
import { type VaultListNode } from "./vault.js";
|
|
38
38
|
import type { AgentActionNodeType, AgentActionNodeStatus, AgentActionNodeDetail } from "@agentvault/connect-core";
|
|
39
|
-
import {
|
|
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";
|
|
41
41
|
export type BridgeToolGrant = {
|
|
42
42
|
readonly tool: string;
|
|
@@ -213,6 +213,19 @@ export interface BridgeAgentDeps {
|
|
|
213
213
|
* recent (a real-time messaging consumer cares about the newest). Default 1000.
|
|
214
214
|
*/
|
|
215
215
|
maxBuffer?: number;
|
|
216
|
+
/**
|
|
217
|
+
* Phase 3 F1: how many times one inbound message may be delivered to a
|
|
218
|
+
* runtime consumer over /v1/ws before it is dead-lettered (content-free log +
|
|
219
|
+
* ack) instead of redelivered forever. Every redelivery can re-run the
|
|
220
|
+
* owner's model on their own key. Default {@link DEFAULT_MAX_DELIVERY_ATTEMPTS}.
|
|
221
|
+
*/
|
|
222
|
+
maxDeliveryAttempts?: number;
|
|
223
|
+
/**
|
|
224
|
+
* Phase 3 F3: consecutive agent-authored messages allowed in one channel
|
|
225
|
+
* before this agent stops answering other agents there (a human message
|
|
226
|
+
* resets the count). Default {@link DEFAULT_MAX_AGENT_HOPS}.
|
|
227
|
+
*/
|
|
228
|
+
maxAgentHops?: number;
|
|
216
229
|
/**
|
|
217
230
|
* Review I3/M3: clock seam for the agent.action.* partial-throttle token
|
|
218
231
|
* buckets AND the unsettled-run stale-TTL sweep. Defaults to Date.now.
|
|
@@ -226,6 +239,18 @@ export interface BridgeAgentDeps {
|
|
|
226
239
|
* timer delays.
|
|
227
240
|
*/
|
|
228
241
|
rng?: () => number;
|
|
242
|
+
/**
|
|
243
|
+
* How long start() waits for its boot drain before it stops blocking on it
|
|
244
|
+
* (default {@link BOOT_DRAIN_WAIT_MS_DEFAULT}). Past it, start() finishes and
|
|
245
|
+
* the boot key-package publish is DEFERRED until an ingest cycle settles:
|
|
246
|
+
* see start(). Injected so tests do not wait a minute.
|
|
247
|
+
*/
|
|
248
|
+
bootDrainWaitMs?: number;
|
|
249
|
+
/**
|
|
250
|
+
* SEC-06 #18: the wait between in-process reply publish retries after a rate
|
|
251
|
+
* limit. Defaults to an unref'd setTimeout. Injected so tests do not sleep.
|
|
252
|
+
*/
|
|
253
|
+
replySleep?: (ms: number) => Promise<void>;
|
|
229
254
|
/**
|
|
230
255
|
* Task 4 (self-hosted fast voice): test seam for the fast-path LLM client.
|
|
231
256
|
* Defaults to `new FastLlmClient(cfg.voice.fastPath, fetchImpl)` when
|
|
@@ -245,10 +270,67 @@ export interface BridgeAgentDeps {
|
|
|
245
270
|
}
|
|
246
271
|
/** Default inbound-buffer cap (messages) when deps.maxBuffer is unset. */
|
|
247
272
|
export declare const DEFAULT_MAX_BUFFER = 1000;
|
|
273
|
+
/**
|
|
274
|
+
* Phase 3 F1: default per-message delivery cap for the /v1/ws runtime handoff.
|
|
275
|
+
* Deliberately ABOVE the OpenClaw connector's own MAX_INBOUND_ATTEMPTS (3,
|
|
276
|
+
* connectors/openclaw/openclaw-entry.js): that connector acks a poison
|
|
277
|
+
* message itself after three failed turns, so this daemon cap is only the
|
|
278
|
+
* backstop for consumers without one, and the two extra deliveries leave
|
|
279
|
+
* headroom for the connector's "runtime not ready" / receipt-lookup retries,
|
|
280
|
+
* which it deliberately does not count.
|
|
281
|
+
*/
|
|
282
|
+
export declare const DEFAULT_MAX_DELIVERY_ATTEMPTS = 5;
|
|
283
|
+
/**
|
|
284
|
+
* Final-wave I1(a): a runtime delivery that failed (a `nack`, or the socket
|
|
285
|
+
* closing with the message in flight) is not redelivered before
|
|
286
|
+
* 2^attempts × this base. With the default cap of 5 the deliveries span
|
|
287
|
+
* minutes, not seconds, so a provider outage no longer dead-letters work
|
|
288
|
+
* within seconds. The attempt counter is durable, and the delay is derived
|
|
289
|
+
* from it — a restart re-arms the same backoff from the restore.
|
|
290
|
+
*/
|
|
291
|
+
export declare const REDELIVERY_BACKOFF_BASE_MS = 5000;
|
|
292
|
+
export declare function redeliveryDelayMs(attempts: number): number;
|
|
293
|
+
/**
|
|
294
|
+
* Final-wave I1(c): the one content-free reply a channel gets when one of its
|
|
295
|
+
* messages is dead-lettered. Fixed text — it never echoes the message.
|
|
296
|
+
*/
|
|
297
|
+
export declare const DEAD_LETTER_NOTICE = "I couldn't process a message. Please resend it.";
|
|
298
|
+
/** At most one {@link DEAD_LETTER_NOTICE} per channel per this window. */
|
|
299
|
+
export declare const DEAD_LETTER_NOTICE_INTERVAL_MS: number;
|
|
300
|
+
/**
|
|
301
|
+
* Phase 3 F1 (pause stops inference): after a send is refused with
|
|
302
|
+
* `agent_paused`, inbound traffic for that channel is not handed to the
|
|
303
|
+
* runtime (no model call). The pause is reversible, so once this long has
|
|
304
|
+
* passed since it was last confirmed, the next inbound message re-probes the
|
|
305
|
+
* agent's own membership row before deciding.
|
|
306
|
+
*/
|
|
307
|
+
export declare const PAUSE_RECHECK_MS = 30000;
|
|
308
|
+
/**
|
|
309
|
+
* Stranded sender (PR #751 review H-1): how often a channel held after a send
|
|
310
|
+
* 409 `mls.group_superseded` re-reads its held group to see whether it
|
|
311
|
+
* converged (a local read, no network).
|
|
312
|
+
*/
|
|
313
|
+
export declare const SUPERSEDE_RECHECK_MS = 30000;
|
|
314
|
+
/**
|
|
315
|
+
* Stranded sender (PR #751 review H-1): the longest a channel's inbound is
|
|
316
|
+
* held, without spending delivery attempts, after a send 409
|
|
317
|
+
* `mls.group_superseded`. Convergence needs the OWNER's client online (a
|
|
318
|
+
* forked or stranded member cannot self-heal), so the bound must cover an
|
|
319
|
+
* owner who is away for a working day or a night, not the ~150 s the
|
|
320
|
+
* dead-letter budget (5 attempts, 2^n x 5 s) allows. 24 h does, and it sits
|
|
321
|
+
* well inside the 7-day reply-intent retention, so a held message never
|
|
322
|
+
* outlives the intent that answers it. The hold is measured from the FIRST
|
|
323
|
+
* 409 (a repeat 409 does not extend it). When it expires the channel is
|
|
324
|
+
* released and the ordinary budget applies again (the refund stops), so a
|
|
325
|
+
* channel that never converges still dead-letters, about 24 h later instead
|
|
326
|
+
* of about 150 s later.
|
|
327
|
+
*/
|
|
328
|
+
export declare const SUPERSEDE_HOLD_MAX_MS: number;
|
|
248
329
|
/**
|
|
249
330
|
* TTL for the Phase 2 channelMeta cache. `is_dm` is immutable for a channel_id,
|
|
250
|
-
* so this exists only so a cached `name`
|
|
251
|
-
*
|
|
331
|
+
* so this exists only so a cached channel `name` re-resolves after a channel
|
|
332
|
+
* rename. The channel name is NOT an @mention name for this agent (2026-09-25:
|
|
333
|
+
* the filter uses this agent's own roster display_name). Lazy expiry on read.
|
|
252
334
|
*/
|
|
253
335
|
export declare const CHANNEL_META_TTL_MS: number;
|
|
254
336
|
/**
|
|
@@ -285,13 +367,53 @@ export declare class InvalidMessageIdError extends Error {
|
|
|
285
367
|
}
|
|
286
368
|
/**
|
|
287
369
|
* Deterministic message id for the voice fast-path reply to inbound turn
|
|
288
|
-
* `messageId
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
370
|
+
* `messageId`, formatted as an RFC 4122 UUID. Live finding 2026-09-06: the
|
|
371
|
+
* previous `fast-<messageId>` string failed `resolveMessageId` ("message_id
|
|
372
|
+
* 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.
|
|
374
|
+
*
|
|
375
|
+
* SEC-06 #18: KEYED with the identity-seed-derived reply key and bound to the
|
|
376
|
+
* agent and channel. It used to be SHA-256 of `fast:<messageId>` alone, which
|
|
377
|
+
* any channel member could compute and publish under first (the agent's fast
|
|
378
|
+
* reply then collides on the backend's message_id key and never publishes),
|
|
379
|
+
* and which two agents fast-answering the same room message shared. Its scope
|
|
380
|
+
* (`fast:`) never meets a runtime reply's (`reply:`).
|
|
381
|
+
*/
|
|
382
|
+
export declare function fastReplyId(key: Uint8Array, identityId: string, channelId: string, messageId: string): string;
|
|
383
|
+
/**
|
|
384
|
+
* SEC-06 #18: how many times one reply publish is retried in-process after a
|
|
385
|
+
* rate limit (the bridge's room floor, or a backend 429) before the failure is
|
|
386
|
+
* surfaced. A surfaced failure leaves the reply intent pending, so the
|
|
387
|
+
* redelivery republishes the SAME reply rather than re-running the model.
|
|
388
|
+
*/
|
|
389
|
+
export declare const REPLY_RATE_LIMIT_RETRIES = 3;
|
|
390
|
+
/** SEC-06 #18: bounds on one rate-limit wait (ms). */
|
|
391
|
+
export declare const REPLY_RATE_LIMIT_MIN_WAIT_MS = 250;
|
|
392
|
+
export declare const REPLY_RATE_LIMIT_MAX_WAIT_MS = 30000;
|
|
393
|
+
/**
|
|
394
|
+
* SEC-06 #18 (#706 @1fe47aa2): an EXACT retry that arrives while its original
|
|
395
|
+
* is still in its transaction gets 409 `message_in_flight`. That is
|
|
396
|
+
* retry-later, never "re-encrypt": the original may yet be accepted. The same
|
|
397
|
+
* bytes are resent after 1 s, 2 s, 4 s; after that the failure surfaces
|
|
398
|
+
* (intent still pending) and the redelivery backoff and delivery cap bound it.
|
|
293
399
|
*/
|
|
294
|
-
export declare
|
|
400
|
+
export declare const REPLY_IN_FLIGHT_RETRIES = 3;
|
|
401
|
+
export declare const REPLY_IN_FLIGHT_BASE_WAIT_MS = 1000;
|
|
402
|
+
/** PR #716 review HIGH-1: bound on part advances after slot collisions, per send. */
|
|
403
|
+
export declare const REPLY_MAX_PART_ADVANCES = 8;
|
|
404
|
+
/** Options shared by `send` and `sendA2A`. */
|
|
405
|
+
export interface SendOptions {
|
|
406
|
+
/** Phase 3 F3: whether this send is a substantive agent turn (default true). */
|
|
407
|
+
agentTurn?: boolean;
|
|
408
|
+
/**
|
|
409
|
+
* SEC-06 #18: the inbound message this send answers. Makes the send an
|
|
410
|
+
* idempotent reply: a deterministic message_id plus a durable intent written
|
|
411
|
+
* before the publish.
|
|
412
|
+
*/
|
|
413
|
+
inReplyTo?: string;
|
|
414
|
+
/** SEC-06 #18: caller idempotency key (WS path); names the reply part. */
|
|
415
|
+
idempotencyKey?: string;
|
|
416
|
+
}
|
|
295
417
|
/**
|
|
296
418
|
* B7 (FileSafe vault): a file-backed {@link OwnerAuthorityStore} for the
|
|
297
419
|
* daemon's owner-pin TOFU state (mirrors B4b's browser-side store, which
|
|
@@ -323,6 +445,36 @@ export declare class AgentPausedSendError extends Error {
|
|
|
323
445
|
readonly channelId: string;
|
|
324
446
|
constructor(channelId: string);
|
|
325
447
|
}
|
|
448
|
+
/**
|
|
449
|
+
* Phase 3 F1: the backend refused a publish with a bare 401/403 (not the
|
|
450
|
+
* typed `agent_paused` body) — the agent's credential or device has been
|
|
451
|
+
* revoked or it is no longer a member. Retrying cannot succeed, so this is
|
|
452
|
+
* TERMINAL for the reply: the runtime's inbound message is acknowledged and
|
|
453
|
+
* the model is not run again for it.
|
|
454
|
+
*/
|
|
455
|
+
export declare class SendForbiddenError extends Error {
|
|
456
|
+
readonly channelId: string;
|
|
457
|
+
constructor(channelId: string);
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Stranded sender (spec 2026-09-26 §7.4): the backend refused a publish with
|
|
461
|
+
* 409 `mls.group_superseded`: the envelope was sealed under a group that is no
|
|
462
|
+
* longer the channel's active one. Nothing was stored, nothing fanned out.
|
|
463
|
+
*
|
|
464
|
+
* TERMINAL for THIS send (no in-place retry; a re-seal under the same held
|
|
465
|
+
* group would be refused the same way), but NOT for the reply: the answered
|
|
466
|
+
* inbound message is not retired and a reply intent stays pending, so the
|
|
467
|
+
* reply is republished once the bridge holds the active group. The bridge
|
|
468
|
+
* never wipes on it (no `healWedgedChannel`, no `resetChannelForRejoin`).
|
|
469
|
+
*
|
|
470
|
+
* `activeGroupId` is the group the backend named, or null when the channel has
|
|
471
|
+
* no active group (fail closed: nothing to converge to).
|
|
472
|
+
*/
|
|
473
|
+
export declare class GroupSupersededSendError extends Error {
|
|
474
|
+
readonly channelId: string;
|
|
475
|
+
readonly activeGroupId: string | null;
|
|
476
|
+
constructor(channelId: string, activeGroupId: string | null);
|
|
477
|
+
}
|
|
326
478
|
/** Thrown by `agent.action.*` when a runId (or nodeId within a run) is unknown
|
|
327
479
|
* or the run has already settled — settle() is terminal, mirroring the client
|
|
328
480
|
* ingest side's "later partials for a settled run are dropped" rule. */
|
|
@@ -375,6 +527,8 @@ export interface AgentActionAPI {
|
|
|
375
527
|
wrap<T>(runId: string, fn: () => T | Promise<T>, opts: AgentActionWrapOpts): Promise<T>;
|
|
376
528
|
}
|
|
377
529
|
export declare const BRIDGE_VERSION: string;
|
|
530
|
+
/** start() waits this long for its boot drain before it stops blocking on it. */
|
|
531
|
+
export declare const BOOT_DRAIN_WAIT_MS_DEFAULT = 60000;
|
|
378
532
|
export declare class BridgeAgent {
|
|
379
533
|
private readonly cfg;
|
|
380
534
|
private readonly transport;
|
|
@@ -406,6 +560,15 @@ export declare class BridgeAgent {
|
|
|
406
560
|
* per channel on a miss.
|
|
407
561
|
*/
|
|
408
562
|
private readonly channelRoster;
|
|
563
|
+
/**
|
|
564
|
+
* 2026-09-25: this agent's own display_name (`ConnectIdentity.display_name`,
|
|
565
|
+
* identity-level, so the same in every channel) from the MOST RECENT roster
|
|
566
|
+
* refresh that listed it. Read by the room @mention filter as its
|
|
567
|
+
* `roomDisplayName`. Kept across a later failed refresh, so a transient
|
|
568
|
+
* members-fetch error does not make the agent deaf to its own name; a
|
|
569
|
+
* successful refresh after a rename replaces it.
|
|
570
|
+
*/
|
|
571
|
+
private lastOwnDisplayName;
|
|
409
572
|
/**
|
|
410
573
|
* Task 8 (owner-attested Reset Secure Session): per-channel STRUCTURAL owner
|
|
411
574
|
* cache, fed by resolveAndPinOwner() on every roster refresh and consumed by
|
|
@@ -416,13 +579,68 @@ export declare class BridgeAgent {
|
|
|
416
579
|
*/
|
|
417
580
|
private readonly channelOwner;
|
|
418
581
|
private readonly buffer;
|
|
582
|
+
/** Message ids durably staged for the attached runtime but not yet acked. */
|
|
583
|
+
private readonly runtimePendingIds;
|
|
419
584
|
private readonly maxBuffer;
|
|
420
|
-
/**
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
585
|
+
/** Phase 3 F1: deliveries so far per pending runtime message (seeded from the store). */
|
|
586
|
+
private readonly deliveryAttempts;
|
|
587
|
+
private readonly maxDeliveryAttempts;
|
|
588
|
+
/** Phase 3 F1: cumulative messages dead-lettered at the delivery cap. */
|
|
589
|
+
private deadLetteredMessages;
|
|
590
|
+
/** Final-wave I1(a): message id → earliest redelivery time (epoch ms). */
|
|
591
|
+
private readonly redeliverNotBefore;
|
|
592
|
+
/** Final-wave I1(a): one timer poking consumers when the earliest backoff ends. */
|
|
593
|
+
private redeliveryTimer;
|
|
594
|
+
private redeliveryTimerAt;
|
|
595
|
+
/** Final-wave I1(c): channel → when its last dead-letter notice was posted. */
|
|
596
|
+
private readonly deadLetterNoticeAt;
|
|
597
|
+
/** Phase 3 F1: channel → when a pause was last confirmed (send 403 or probe). */
|
|
598
|
+
private readonly pausedChannels;
|
|
599
|
+
/**
|
|
600
|
+
* Phase 3 F1: runtime messages retired by a TERMINAL reply failure reported
|
|
601
|
+
* outside the /v1/ws socket that holds them in flight (HTTP /v1/send). The
|
|
602
|
+
* socket consults this so it neither redelivers them nor stays busy on them.
|
|
603
|
+
* Bounded; oldest forgotten first.
|
|
604
|
+
*/
|
|
605
|
+
private readonly retiredRuntimeIds;
|
|
606
|
+
/** SEC-06 #18: HMAC key for deterministic reply ids and body MACs. */
|
|
607
|
+
private readonly replyIdKey;
|
|
608
|
+
/**
|
|
609
|
+
* PR #716 review MEDIUM-5: in-flight idempotent sends and intent replays.
|
|
610
|
+
* close() waits for them before it closes the store.
|
|
611
|
+
*/
|
|
612
|
+
private readonly replyWork;
|
|
613
|
+
/** SEC-06 #18: channels whose redelivered message is being republished from its intent. */
|
|
614
|
+
private readonly replayingChannels;
|
|
615
|
+
/** SEC-06 #18: in-process locks: one resolve+claim per scope, one publish per part. */
|
|
616
|
+
private readonly replyIntentLocks;
|
|
617
|
+
private readonly replySleep;
|
|
618
|
+
/** SEC-06 #18: redelivered messages answered from a reply intent, not the model. */
|
|
619
|
+
private replyIntentReplays;
|
|
620
|
+
/** SEC-06 #18: in-process reply publish retries after a rate limit. */
|
|
621
|
+
private replyRateLimitRetries;
|
|
622
|
+
/** Phase 3 F1: one pending membership recheck per paused channel holding work. */
|
|
623
|
+
private readonly pauseRecheckTimers;
|
|
624
|
+
/**
|
|
625
|
+
* Stranded sender (PR #751 review H-1): channels whose send was refused 409
|
|
626
|
+
* `mls.group_superseded`, with the group held at that moment and when the
|
|
627
|
+
* hold began. Their inbound is held (never acked, no attempt spent) until
|
|
628
|
+
* the held group changes, a send succeeds, or SUPERSEDE_HOLD_MAX_MS passes.
|
|
629
|
+
*/
|
|
630
|
+
private readonly supersededChannels;
|
|
631
|
+
/** H-1: channels whose hold EXPIRED; their 409s spend attempts again. */
|
|
632
|
+
private readonly supersedeHoldExpired;
|
|
633
|
+
/** H-1: one pending held-group recheck per held channel. */
|
|
634
|
+
private readonly supersedeRecheckTimers;
|
|
635
|
+
/** H-1: runtime messages whose CURRENT delivery attempt may be refunded once. */
|
|
636
|
+
private readonly supersedeRefundable;
|
|
637
|
+
/** M-1: channels this process has used as a FileSafe vault channel. */
|
|
638
|
+
private readonly vaultChannelIds;
|
|
639
|
+
/** Phase 3 F3: consecutive agent turns per channel. */
|
|
640
|
+
private readonly agentHops;
|
|
641
|
+
private readonly maxAgentHops;
|
|
642
|
+
/** Phase 3 F3: cumulative agent-authored messages not answered (no explicit mention / hop limit). */
|
|
643
|
+
private agentLoopSuppressed;
|
|
426
644
|
/** Cumulative count of messages shed by the buffer cap (surfaced on health()/metrics()). */
|
|
427
645
|
private droppedMessages;
|
|
428
646
|
/**
|
|
@@ -559,7 +777,7 @@ export declare class BridgeAgent {
|
|
|
559
777
|
private static readonly REQUEST_WELCOME_COOLDOWN_MS;
|
|
560
778
|
private static readonly GRACEFUL_DRAIN_MS;
|
|
561
779
|
/**
|
|
562
|
-
* P0b (daemon-stability): collapses the onDecryptError
|
|
780
|
+
* P0b (daemon-stability): collapses the onDecryptError diagnostic from
|
|
563
781
|
* "one line per failure" (the 114 MB/765k-line-in-a-day log source) to at
|
|
564
782
|
* most one line immediately + one summary line per 60s per channel. LOG
|
|
565
783
|
* OUTPUT ONLY — does not touch errorStats()/quarantine counters, which
|
|
@@ -578,10 +796,55 @@ export declare class BridgeAgent {
|
|
|
578
796
|
private readonly selfHealSweepEnabled;
|
|
579
797
|
/** Task 12: proactive sweep cadence (in addition to the once-at-boot fire). */
|
|
580
798
|
private static readonly SELF_HEAL_SWEEP_INTERVAL_MS;
|
|
799
|
+
/**
|
|
800
|
+
* A trigger that finds the in-flight ingest cycle older than this logs
|
|
801
|
+
* `receive.ingest_stalled` once for that cycle. Log only: the trigger still
|
|
802
|
+
* queues behind it (see ingestOnce). Two missed safety ticks.
|
|
803
|
+
*/
|
|
804
|
+
private static readonly INGEST_STALL_LOG_MS;
|
|
805
|
+
/**
|
|
806
|
+
* An in-flight ingest cycle older than this is abandoned by the next trigger
|
|
807
|
+
* (see ingestOnce). A WALL-CLOCK age for the whole cycle, not a bound on any
|
|
808
|
+
* one request: a cycle makes many requests in sequence, each with its own
|
|
809
|
+
* budget, so a cycle that is only slow can pass it (see ingestOnce for what
|
|
810
|
+
* that costs).
|
|
811
|
+
*/
|
|
812
|
+
private static readonly INGEST_ABANDON_MS;
|
|
581
813
|
private static readonly SELF_HEAL_SWEEP_JITTER_MS;
|
|
582
814
|
private selfHealTimer?;
|
|
583
815
|
private bootSweepTimer?;
|
|
584
816
|
private readonly rng;
|
|
817
|
+
private readonly bootDrainWaitMs;
|
|
818
|
+
/** Resolved (and cleared) each time an ingest cycle settles: see start(). */
|
|
819
|
+
private ingestSettledWaiters;
|
|
820
|
+
/** The boot key-package publish, when start() had to defer it. Test seam. */
|
|
821
|
+
private deferredBootPublish;
|
|
822
|
+
/**
|
|
823
|
+
* True from the moment start() begins its boot drain until an ingest cycle
|
|
824
|
+
* has SETTLED (the drain itself, or the valve's replacement). While it is
|
|
825
|
+
* true, no path outside that drain may rotate the device key package: see
|
|
826
|
+
* kpRotationRefusedByBootDrain.
|
|
827
|
+
*/
|
|
828
|
+
private bootDrainPending;
|
|
829
|
+
/**
|
|
830
|
+
* #811 L-1: channels whose onWedged heal the boot drain REFUSED (a send the
|
|
831
|
+
* drain started quarantined them outside its exemption, #799 M-2). The SDK
|
|
832
|
+
* fires onWedged once per episode, so nothing else would ask again until a
|
|
833
|
+
* sweep proves the channel stuck. Signalled once after the drain settles:
|
|
834
|
+
* see signalWedgesRefusedByBootDrain. A set, so bounded by the channel count;
|
|
835
|
+
* a refusal can only happen while `bootDrainPending`, once per process.
|
|
836
|
+
*/
|
|
837
|
+
private readonly wedgeSignalsRefusedByBootDrain;
|
|
838
|
+
/** #811 L-1: the post-drain pass has run; a refusal queued after it (its
|
|
839
|
+
* catch ran on a later microtask than the settle) kicks the pass itself. */
|
|
840
|
+
private wedgeSignalsAfterBootDrainArmed;
|
|
841
|
+
/** #811 L-2: the rotation sites whose boot-drain refusal was already logged. */
|
|
842
|
+
private readonly bootDrainRefusalLogged;
|
|
843
|
+
/** The running ingest cycle's exemption, for everything the cycle runs.
|
|
844
|
+
* ⛔ Read ONLY through drainExemptionAtCallIn(), and only where the drain
|
|
845
|
+
* calls in: timers created inside a cycle inherit this context, so a check
|
|
846
|
+
* that read it anywhere else would exempt them too (#789 LOW). */
|
|
847
|
+
private readonly ingestCycleContext;
|
|
585
848
|
private readonly rateFloor;
|
|
586
849
|
/**
|
|
587
850
|
* agent.action.* run state, keyed by runId.
|
|
@@ -641,6 +904,10 @@ export declare class BridgeAgent {
|
|
|
641
904
|
private readonly waiters;
|
|
642
905
|
private pingHandle?;
|
|
643
906
|
private safetyTimer?;
|
|
907
|
+
/** The ingest cycle now running, if any (see ingestOnce: single-flight). */
|
|
908
|
+
private ingestInFlight;
|
|
909
|
+
/** The ONE follow-up cycle every trigger during the in-flight cycle shares. */
|
|
910
|
+
private ingestFollowUp;
|
|
644
911
|
private runtimeConfigTimer?;
|
|
645
912
|
private readonly listeners;
|
|
646
913
|
private startedAt;
|
|
@@ -720,6 +987,96 @@ export declare class BridgeAgent {
|
|
|
720
987
|
*/
|
|
721
988
|
private capBuffer;
|
|
722
989
|
start(): Promise<void>;
|
|
990
|
+
/**
|
|
991
|
+
* #811 L-1: send the re-Welcome signal ONCE for each channel whose onWedged
|
|
992
|
+
* heal the boot drain refused. Runs after the drain settled and after the
|
|
993
|
+
* boot key-package publish, so `bootDrainPending` is false and nothing here
|
|
994
|
+
* can be refused again.
|
|
995
|
+
*
|
|
996
|
+
* ⛔ Never a wipe. The refused heal already ran its wipe step (under its own
|
|
997
|
+
* anti-thrash floor) before it reached the signal. This pass only re-sends
|
|
998
|
+
* the half that was refused, and only for a channel that STILL holds no
|
|
999
|
+
* state: one that holds state now rejoined meanwhile (or someone else put
|
|
1000
|
+
* state there), and asking for it is the "ask without repudiating" churn
|
|
1001
|
+
* healWedgedChannel itself refuses. The signal is the same one the
|
|
1002
|
+
* no-local-state Task-11 branch and the sweep send, under the same shared
|
|
1003
|
+
* per-channel cooldown. Never throws: each channel has its own catch.
|
|
1004
|
+
*/
|
|
1005
|
+
private signalWedgesRefusedByBootDrain;
|
|
1006
|
+
/** #811 L-1: queue a channel whose wedge heal the boot drain refused. */
|
|
1007
|
+
private queueWedgeSignalAfterBootDrain;
|
|
1008
|
+
/**
|
|
1009
|
+
* Arm the self-heal sweeps: the periodic one and the once-at-boot one.
|
|
1010
|
+
*
|
|
1011
|
+
* ⛔ Only after the boot drain has settled AND the boot key-package publish
|
|
1012
|
+
* has run. selfHealSweep opens with rotateDeviceKeyPackage(), which can
|
|
1013
|
+
* delete a TTL-lapsed bundle, and a Welcome still waiting in the boot drain
|
|
1014
|
+
* is sealed to that bundle: it would be discarded and acked (#789
|
|
1015
|
+
* verification NO-GO: rng 0 fired the boot sweep inside the slow-drain
|
|
1016
|
+
* window).
|
|
1017
|
+
*/
|
|
1018
|
+
private armSelfHealSweeps;
|
|
1019
|
+
/**
|
|
1020
|
+
* Defence in depth for DRAIN BEFORE ROTATE (see start()). True, and logged,
|
|
1021
|
+
* when a key-package rotation is attempted while the boot drain is still
|
|
1022
|
+
* unsettled from OUTSIDE that drain: a send's supersede convergence, a
|
|
1023
|
+
* sweep, any future caller. Such a rotation can delete the held bundle that
|
|
1024
|
+
* a Welcome still inside the drain is sealed to.
|
|
1025
|
+
*
|
|
1026
|
+
* The drain's own calls are exempt: the SDK's onWedged / onSupersedeUnopenable
|
|
1027
|
+
* heals fired from its receive pass (the onWedged heal is detached), and its
|
|
1028
|
+
* phase-0 re-Welcome signal. That ordering is unchanged from before the boot
|
|
1029
|
+
* wait was bounded, and refusing it would change the drain. #789 LOW: the
|
|
1030
|
+
* exemption is an explicit DrainExemption handed down from those three
|
|
1031
|
+
* call-in points, never the inherited AsyncLocalStorage context, so a timer
|
|
1032
|
+
* or sweep scheduled inside the drain that calls a rotator is refused, as is
|
|
1033
|
+
* the late work of a cycle the valve abandoned. The hook call-ins DO read
|
|
1034
|
+
* the context, so sends the cycle starts and the send-side convergence run
|
|
1035
|
+
* outside it (#799 M-2; see DrainExemption for the residual).
|
|
1036
|
+
* Only the bridge-level rotators route through here. The SDK's own
|
|
1037
|
+
* publishes (ensureSession bootstrap, vault owner, reset supersede) mint only
|
|
1038
|
+
* when NO bundle is held, so they delete nothing.
|
|
1039
|
+
*/
|
|
1040
|
+
/**
|
|
1041
|
+
* The boot KeyPackage publish (architect ruling on the #793 M1 proposal, 1B).
|
|
1042
|
+
* Runs only once the boot drain settled (`bootDrainPending` is already false).
|
|
1043
|
+
* The plain publish can no longer rotate an expired KeyPackage without a drain
|
|
1044
|
+
* receipt, and the bridge's boot drain is its own ingest cycle, which cannot
|
|
1045
|
+
* mint an SDK receipt. So the helper runs its OWN drain here (one pull, joins
|
|
1046
|
+
* only channels with no state, right after the boot ingest) and rotates only
|
|
1047
|
+
* with that receipt. A failed read must never fail the boot: fall back to the
|
|
1048
|
+
* plain publish, which mints when no bundle is held and never deletes.
|
|
1049
|
+
*/
|
|
1050
|
+
private bootReconcileKeyPackage;
|
|
1051
|
+
private kpRotationRefusedByBootDrain;
|
|
1052
|
+
/**
|
|
1053
|
+
* #799 L-1: give back a re-Welcome cooldown spent on a signal the boot drain
|
|
1054
|
+
* refused (the same restore as a "deferred" outcome). `last` is the value
|
|
1055
|
+
* read before the signal: an earlier request time, or -Infinity for never.
|
|
1056
|
+
* True when `e` was that refusal: it is a wait, already logged once per
|
|
1057
|
+
* site (kpRotationRefusedByBootDrain), so the caller logs no failure line
|
|
1058
|
+
* for it (#811 L-2).
|
|
1059
|
+
*/
|
|
1060
|
+
private restoreWelcomeCooldownIfRefused;
|
|
1061
|
+
/**
|
|
1062
|
+
* #799 M-2: run `fn` OUTSIDE the ingest cycle's AsyncLocalStorage context.
|
|
1063
|
+
* For a SEND a cycle starts (the fast-path reply, the summarize answer) and
|
|
1064
|
+
* for the send-side supersede convergence: a send is not the drain, but it
|
|
1065
|
+
* can reach the SDK's onSupersedeUnopenable hook (its reconcileForSend
|
|
1066
|
+
* receive pass, or the send-path adopt), and the hook reads the context at
|
|
1067
|
+
* call-in. Outside it, the hook finds no exemption and a rotation while the
|
|
1068
|
+
* boot drain is unsettled is refused.
|
|
1069
|
+
*/
|
|
1070
|
+
private outsideIngestCycle;
|
|
1071
|
+
/**
|
|
1072
|
+
* The running ingest cycle's exemption, when the caller runs inside one.
|
|
1073
|
+
* Call it ONLY synchronously at a place where the drain itself calls in (the
|
|
1074
|
+
* two SDK hooks and the phase-0 signal), and pass the result down
|
|
1075
|
+
* explicitly. Nothing else may read the context: see ingestCycleContext.
|
|
1076
|
+
* Whether it still counts is decided at the rotator (`live`), because a
|
|
1077
|
+
* detached heal can hold it past the moment the valve abandons its cycle.
|
|
1078
|
+
*/
|
|
1079
|
+
private drainExemptionAtCallIn;
|
|
723
1080
|
/** C3: a random 0..SELF_HEAL_SWEEP_JITTER_MS decorrelation offset. */
|
|
724
1081
|
private sweepJitter;
|
|
725
1082
|
/**
|
|
@@ -748,15 +1105,32 @@ export declare class BridgeAgent {
|
|
|
748
1105
|
* "private KeyPackage key no longer held" and the Welcome is discarded. A bare
|
|
749
1106
|
* re-Welcome re-seals to the SAME diverged published KP — un-openable again —
|
|
750
1107
|
* looping until the channel quarantines. So:
|
|
751
|
-
* - published == held →
|
|
752
|
-
*
|
|
753
|
-
* - published != held →
|
|
754
|
-
*
|
|
755
|
-
*
|
|
756
|
-
*
|
|
757
|
-
*
|
|
1108
|
+
* - published == held → kept (no-op publish), then re-Welcome re-seals to
|
|
1109
|
+
* the current KP we hold.
|
|
1110
|
+
* - published != held → provider.reconcileHeldKeyPackage DRAINS the queued
|
|
1111
|
+
* Welcomes first, then rotates, so the owner's re-Welcome is openable and
|
|
1112
|
+
* nothing already queued is stranded (architect ruling on the #793 M1
|
|
1113
|
+
* proposal). The text below describes the pre-ruling hazard.
|
|
1114
|
+
* ⛔ NOT ORPHAN-SAFE, and nothing here drains first. `false` covers two
|
|
1115
|
+
* different states. (a) The server vends DIFFERENT bytes: Welcomes sealed
|
|
1116
|
+
* since that publish are un-openable anyway, but ones sealed to the held
|
|
1117
|
+
* bundle before it may still be queued. (b) The server vends NOTHING because
|
|
1118
|
+
* the held KeyPackage EXPIRED: it is absent, not diverged, and every Welcome
|
|
1119
|
+
* sealed to it before expiry is still queued AND still openable with the
|
|
1120
|
+
* held bundle. The unconditional delete strands both. Disclosed residual
|
|
1121
|
+
* (#792 review M1); the proposed fix is one shared SDK rotate helper that
|
|
1122
|
+
* drains before it deletes.
|
|
758
1123
|
* getLastPublishedKpId then names the reconciled KP so the owner re-seals to it.
|
|
759
1124
|
* Callers own their cooldown/rate-limit gate and try/catch — this does neither.
|
|
1125
|
+
*
|
|
1126
|
+
* ⛔ An UNDETERMINED answer never rotates. The predicate is the THROWING
|
|
1127
|
+
* variant: a failed batch KeyPackage read (a read deadline, 401/403, network)
|
|
1128
|
+
* or a failed held-bundle store read REJECTS out of here, so nothing is
|
|
1129
|
+
* published, nothing is deleted and no Welcome is requested. The catching
|
|
1130
|
+
* variant answered `false` for those, which routed an outage to
|
|
1131
|
+
* forceRepublishKeyPackage(): it deleted the device-wide bundle and every
|
|
1132
|
+
* Welcome already queued to it, for any channel, was discarded un-openable
|
|
1133
|
+
* (N-2). Every caller catches, and retries on its own cadence.
|
|
760
1134
|
*/
|
|
761
1135
|
private signalHeldKpWelcome;
|
|
762
1136
|
/**
|
|
@@ -776,7 +1150,11 @@ export declare class BridgeAgent {
|
|
|
776
1150
|
* group (`continue`, no signal). ANY failure along the way (probe throws,
|
|
777
1151
|
* signal throws) is swallowed: a wedge heal must never throw back into the
|
|
778
1152
|
* SDK's onWedged caller. Single-shot per wedge episode — full rejoin
|
|
779
|
-
* reliability is P1, not this hook's job.
|
|
1153
|
+
* reliability is P1, not this hook's job. #811 L-1: the one exception is a
|
|
1154
|
+
* signal the boot drain REFUSED (a send the drain started fired onWedged
|
|
1155
|
+
* outside its exemption): the channel is queued and signalled once after
|
|
1156
|
+
* the drain settles (signalWedgesRefusedByBootDrain), since the SDK will
|
|
1157
|
+
* not fire onWedged again for this episode.
|
|
780
1158
|
*/
|
|
781
1159
|
private healWedgedChannel;
|
|
782
1160
|
/**
|
|
@@ -860,9 +1238,16 @@ export declare class BridgeAgent {
|
|
|
860
1238
|
* reads `null` and therefore degraded.
|
|
861
1239
|
*/
|
|
862
1240
|
private keypackageExpired;
|
|
1241
|
+
/** #335: dedupes the expiry-measurement warning to once per failure STREAK —
|
|
1242
|
+
* any non-throwing resolution (including the known-unknown `null` returns,
|
|
1243
|
+
* which are answers, not failures) re-arms it. Mirrors `enclaveListWarned`. */
|
|
1244
|
+
private keypackageExpiryUnknownWarned;
|
|
863
1245
|
/** Resolve the flag from the SAME two inputs the guard uses, with every unknown
|
|
864
1246
|
* mapped to `null`. Never throws: a failure to measure is itself unknown. */
|
|
865
1247
|
private resolveKeypackageExpired;
|
|
1248
|
+
/** Every non-throwing resolution ends the failure streak (the next throw is
|
|
1249
|
+
* a NEW streak and warns again) and passes the value through unchanged. */
|
|
1250
|
+
private kpExpiryResolved;
|
|
866
1251
|
private rotateDeviceKeyPackage;
|
|
867
1252
|
private selfHealSweep;
|
|
868
1253
|
/**
|
|
@@ -894,18 +1279,236 @@ export declare class BridgeAgent {
|
|
|
894
1279
|
* already resolves internally) propagates unchanged.
|
|
895
1280
|
*/
|
|
896
1281
|
private sendWithSelfHealTerminal;
|
|
897
|
-
|
|
1282
|
+
/**
|
|
1283
|
+
* Stranded sender (spec 2026-09-26 §7.4, architect M-6 ruling + audit N-1):
|
|
1284
|
+
* the ONE place a bridge publish meets 409 `mls.group_superseded`. Every
|
|
1285
|
+
* `transport.publish` in this file goes through it, so the rule cannot be
|
|
1286
|
+
* skipped by a send path:
|
|
1287
|
+
*
|
|
1288
|
+
* 1. Terminal for the send: the typed {@link GroupSupersededSendError} is
|
|
1289
|
+
* thrown, and the SDK send helpers never retry it in place.
|
|
1290
|
+
* 2. NEVER a wipe. `healWedgedChannel` / `resetChannelForRejoin` are not
|
|
1291
|
+
* called: a wipe drops the held state, and the next Welcome then lands on
|
|
1292
|
+
* a device with no local state, where the join takes the first Welcome
|
|
1293
|
+
* for the channel with NO attestation check. A server-chosen response
|
|
1294
|
+
* code must not launder a refused supersede into an unverified join.
|
|
1295
|
+
* 3. With a pinned owner: `adoptSupersededGroup` once (see
|
|
1296
|
+
* {@link convergeAfterSendSuperseded}).
|
|
1297
|
+
* 4. The non-destructive signal (ditto).
|
|
1298
|
+
*
|
|
1299
|
+
* Any other error passes through unchanged.
|
|
1300
|
+
*/
|
|
1301
|
+
private publishEnvelope;
|
|
1302
|
+
/**
|
|
1303
|
+
* PR #751 review M-1 (plan invariant F-1): adoption and the Welcome signal
|
|
1304
|
+
* run only on a channel KNOWN not to be a FileSafe vault channel. FileSafe
|
|
1305
|
+
* halts on a vault group mismatch by design and must never adopt, and the
|
|
1306
|
+
* SDK's adoptSupersededGroup leaves the vault skip to its caller.
|
|
1307
|
+
*
|
|
1308
|
+
* Fail closed. A channel is allowed only when the enclave channel listing
|
|
1309
|
+
* has shown it (the listing excludes hidden channels, and `vault` is the
|
|
1310
|
+
* only kind that is ever hidden; the self-heal sweep works from the same
|
|
1311
|
+
* listing, so this is the sweep's own scope) AND this process has never
|
|
1312
|
+
* used it as a vault channel. An unlisted channel gets one listing refresh;
|
|
1313
|
+
* still unlisted, it is skipped (a vault channel after a restart lands here,
|
|
1314
|
+
* as does a channel whose listing is unavailable).
|
|
1315
|
+
*/
|
|
1316
|
+
private supersedeConvergenceAllowed;
|
|
1317
|
+
/**
|
|
1318
|
+
* H-1: hold `channelId`'s buffered inbound after a send 409. The FIRST 409
|
|
1319
|
+
* starts the hold; a repeat keeps its start (the bound is never extended)
|
|
1320
|
+
* and a channel whose hold already expired is not held again until it
|
|
1321
|
+
* converges or sends. Never throws.
|
|
1322
|
+
*/
|
|
1323
|
+
private holdSuperseded;
|
|
1324
|
+
/**
|
|
1325
|
+
* H-1: whether `channelId`'s buffered work must be HELD (a send 409 is
|
|
1326
|
+
* outstanding and the hold has not expired). Holding arms a recheck so the
|
|
1327
|
+
* release does not depend on new traffic. Synchronous: it only reads the
|
|
1328
|
+
* map; the held-group read happens in {@link recheckSupersedeHold}.
|
|
1329
|
+
*/
|
|
1330
|
+
private heldForSupersede;
|
|
1331
|
+
private scheduleSupersedeRecheck;
|
|
1332
|
+
/**
|
|
1333
|
+
* H-1: release the hold once the held group CHANGED (an adoption, a
|
|
1334
|
+
* receive-path supersede, or a rejoin converged it), or once it expired.
|
|
1335
|
+
* Otherwise keep holding, re-armed while work is buffered for the channel.
|
|
1336
|
+
*/
|
|
1337
|
+
private recheckSupersedeHold;
|
|
1338
|
+
private expireSupersedeHold;
|
|
1339
|
+
private releaseSupersedeHold;
|
|
1340
|
+
private clearSupersedeHold;
|
|
1341
|
+
/**
|
|
1342
|
+
* H-1: a reply to `inReplyTo` was refused 409 `mls.group_superseded`. That
|
|
1343
|
+
* says nothing about the message, so the delivery that produced the reply
|
|
1344
|
+
* does not count against its dead-letter budget: give its attempt back, in
|
|
1345
|
+
* memory and durably. At most once per delivery (a multi-part reply cannot
|
|
1346
|
+
* refund twice), and not once the channel's hold has expired (then the
|
|
1347
|
+
* ordinary budget bounds a channel that never converges).
|
|
1348
|
+
*/
|
|
1349
|
+
private refundSupersededDelivery;
|
|
1350
|
+
/**
|
|
1351
|
+
* Stranded sender, §7.4 rules 3 and 4, in that order.
|
|
1352
|
+
*
|
|
1353
|
+
* Rule 3 (audit N-1): with a PINNED owner, `adoptSupersededGroup(channelId,
|
|
1354
|
+
* activeGroupId, pinnedOwner)` once. The owner comes ONLY from the bridge's
|
|
1355
|
+
* own pin ({@link resolvePinnedOwnerHook}: the cached structural owner AND a
|
|
1356
|
+
* persisted pin that matches it), never from the server's roster. It is the
|
|
1357
|
+
* same verified core `trySupersede` runs on receive with the same pin, so it
|
|
1358
|
+
* adds no trust; it converges without waiting for the next receive pass.
|
|
1359
|
+
* Without a pin: no adoption.
|
|
1360
|
+
*
|
|
1361
|
+
* Rule 4 (architect M-6): the sweep's non-destructive diverged-channel
|
|
1362
|
+
* action, `signalHeldKpWelcome(activeGroupId)`, at most once per the SHARED
|
|
1363
|
+
* per-channel `lastWelcomeRequest` cooldown (the same map and floor as the
|
|
1364
|
+
* self-heal sweep, so a 409 storm cannot out-pace the sweep's own cadence).
|
|
1365
|
+
*
|
|
1366
|
+
* Both are skipped when there is nothing to converge to (no active group:
|
|
1367
|
+
* fail closed). The signal is also skipped when the bridge already holds
|
|
1368
|
+
* the group the backend named (an adoption just converged it, or it
|
|
1369
|
+
* converged between the seal and the answer): a request for a group we hold
|
|
1370
|
+
* costs the committer a Remove+Add and cannot be consumed. Never throws: a
|
|
1371
|
+
* failed adoption or signal must not mask the typed send error.
|
|
1372
|
+
*/
|
|
1373
|
+
private convergeAfterSendSuperseded;
|
|
1374
|
+
/**
|
|
1375
|
+
* The terminal classification of {@link sendWithSelfHealTerminal}, shared
|
|
1376
|
+
* with the SEC-06 #18 cached-envelope resend (which publishes prebuilt bytes
|
|
1377
|
+
* and so does not go through sendWithSelfHeal).
|
|
1378
|
+
*/
|
|
1379
|
+
private publishTerminal;
|
|
1380
|
+
/**
|
|
1381
|
+
* `opts.agentTurn` (default true): whether this send is a SUBSTANTIVE agent
|
|
1382
|
+
* turn for the Phase 3 F3 hop count. Notices (daily limit, dead letter) and
|
|
1383
|
+
* continuation parts of one multi-part reply pass false (final-wave I2).
|
|
1384
|
+
*
|
|
1385
|
+
* SEC-06 #18: `opts.inReplyTo` / `opts.idempotencyKey` make the send an
|
|
1386
|
+
* idempotent reply (see {@link sendIdempotent}); `messageId` must then be
|
|
1387
|
+
* absent, because the id is derived.
|
|
1388
|
+
*/
|
|
1389
|
+
send(channelId: string, text: string, messageId?: string, opts?: SendOptions): Promise<{
|
|
898
1390
|
message_id: string;
|
|
899
1391
|
status: "sent";
|
|
900
1392
|
}>;
|
|
1393
|
+
/** PR #716 review MEDIUM-5: register reply work so close() can wait for it. */
|
|
1394
|
+
private trackReplyWork;
|
|
901
1395
|
sendA2A(channelId: string, a2a: {
|
|
902
1396
|
a2a_version: string;
|
|
903
1397
|
kind: "message" | "task" | "artifact";
|
|
904
1398
|
object: Record<string, unknown>;
|
|
905
|
-
}, messageId?: string): Promise<{
|
|
1399
|
+
}, messageId?: string, opts?: SendOptions): Promise<{
|
|
906
1400
|
message_id: string;
|
|
907
1401
|
status: "sent";
|
|
908
1402
|
}>;
|
|
1403
|
+
/**
|
|
1404
|
+
* Seal and publish one application message under `id`: the room/a2a rate
|
|
1405
|
+
* floor, join reconcile, and the shared self-heal seam. `onEnvelope` runs
|
|
1406
|
+
* for every envelope built, BEFORE it is published (SEC-06 #18 persists it
|
|
1407
|
+
* there so a retry can resend the exact bytes).
|
|
1408
|
+
*/
|
|
1409
|
+
private publishBody;
|
|
1410
|
+
private enforceBodyRateFloor;
|
|
1411
|
+
/** Bookkeeping after a CONFIRMED publish of `body` under `id`. */
|
|
1412
|
+
private afterPublish;
|
|
1413
|
+
private isIdempotentSend;
|
|
1414
|
+
/**
|
|
1415
|
+
* SEC-06 #18: publish a reply part idempotently.
|
|
1416
|
+
*
|
|
1417
|
+
* 1. Resolve and claim the intent under one lock per scope. An explicit
|
|
1418
|
+
* idempotency_key names the part. Without one, a send whose body equals
|
|
1419
|
+
* the MOST RECENT part of this inbound is a retry of that part (PR #716
|
|
1420
|
+
* review MEDIUM-2: an older identical part is not matched, so A, B, A are
|
|
1421
|
+
* three messages); anything else is a new part, numbered from the STORE
|
|
1422
|
+
* (1 + the highest recorded ordinal, review HIGH-1), never from memory,
|
|
1423
|
+
* so a restart cannot reuse a published part's id.
|
|
1424
|
+
* 2. The claim is durable BEFORE any publish. An existing intent for the
|
|
1425
|
+
* slot is authoritative when it holds this body; one holding a different
|
|
1426
|
+
* body is skipped past (the next ordinal).
|
|
1427
|
+
* 3. Publish it (see {@link publishIntent}).
|
|
1428
|
+
*/
|
|
1429
|
+
private sendIdempotent;
|
|
1430
|
+
/**
|
|
1431
|
+
* SEC-06 #18 / PR #716 review HIGH-1: resolve and claim an unkeyed part of
|
|
1432
|
+
* the reply to `inReplyTo`. Caller holds the scope lock. `matchRetry` false
|
|
1433
|
+
* after a slot collision: the body must go to a NEW part.
|
|
1434
|
+
*/
|
|
1435
|
+
private claimUnkeyedPart;
|
|
1436
|
+
/** SEC-06 #18: run `fn` after every earlier holder of `lockKey` settled. */
|
|
1437
|
+
private withReplyLock;
|
|
1438
|
+
/**
|
|
1439
|
+
* SEC-06 #18: publish one pending intent, at most one publish in flight per
|
|
1440
|
+
* intent in this process. A sent intent returns its id without publishing.
|
|
1441
|
+
*
|
|
1442
|
+
* - The row is verified first (review LOW-6); a row that fails is dropped.
|
|
1443
|
+
* - A TERMINAL refusal abandons the reply's unpublished parts (the inbound
|
|
1444
|
+
* is retired by the caller), or the one keyed intent, so no sealed reply
|
|
1445
|
+
* text lingers (review LOW-8).
|
|
1446
|
+
* - A slot collision (review HIGH-1): an unkeyed part is marked skipped and
|
|
1447
|
+
* its body re-claimed at the next ordinal; a keyed one is refused with
|
|
1448
|
+
* {@link IdempotencyConflictError}.
|
|
1449
|
+
*
|
|
1450
|
+
* Returns the id the reply was published under (a moved part's new id).
|
|
1451
|
+
*/
|
|
1452
|
+
private publishIntent;
|
|
1453
|
+
/**
|
|
1454
|
+
* PR #716 review LOW-6: the row's id derives from its own identity, channel,
|
|
1455
|
+
* scope and part, and its body matches its MAC (which binds id and channel).
|
|
1456
|
+
*/
|
|
1457
|
+
private intentVerifies;
|
|
1458
|
+
/**
|
|
1459
|
+
* SEC-06 #18 item 3: a rate-limited reply retries the PUBLISH in-process
|
|
1460
|
+
* with backoff, never the model turn; so does an exact resend whose
|
|
1461
|
+
* original is still in flight. After REPLY_RATE_LIMIT_RETRIES a rate limit
|
|
1462
|
+
* surfaces as the bridge's own `rate_limited`, and after
|
|
1463
|
+
* REPLY_IN_FLIGHT_RETRIES an in-flight answer surfaces as an ambiguous
|
|
1464
|
+
* failure. Either way the intent stays pending, so the redelivery
|
|
1465
|
+
* republishes the same reply.
|
|
1466
|
+
*/
|
|
1467
|
+
private publishIntentWithRetry;
|
|
1468
|
+
/**
|
|
1469
|
+
* One publish attempt of a pending intent.
|
|
1470
|
+
*
|
|
1471
|
+
* If a signed envelope was persisted by an earlier attempt, resend THOSE
|
|
1472
|
+
* BYTES: the earlier attempt may have been accepted, and the backend
|
|
1473
|
+
* replays the original ack only for identical canonical bytes (#706; a
|
|
1474
|
+
* resealed retry of an accepted message is a 409). Never re-encrypt a reply
|
|
1475
|
+
* that may already have been accepted. The #706 @1fe47aa2 table:
|
|
1476
|
+
* - 200 (`idempotent_replay`): done, the original ack.
|
|
1477
|
+
* - 409 `message_in_flight`, 429 `tenant_busy`, 503 `server_busy`: retry
|
|
1478
|
+
* the same bytes later ({@link ReplyInFlightError}).
|
|
1479
|
+
* - 409 `replay_nonce` on the cached bytes: the earlier attempt burned the
|
|
1480
|
+
* nonce and did NOT commit, so nothing is stored under this id: re-seal
|
|
1481
|
+
* (same id, fresh nonce, current epoch).
|
|
1482
|
+
* - 400 `mls.stale_epoch`: answered only for a NEW id: re-seal.
|
|
1483
|
+
* - 409 `message_id_conflict`: the id is held by OTHER bytes, so it is a
|
|
1484
|
+
* slot collision ({@link ReplyIdCollisionError}), never "delivered".
|
|
1485
|
+
* - 400 `mls.group_header_required` (stranded sender): the cached bytes
|
|
1486
|
+
* carry no group header and this population must name its group.
|
|
1487
|
+
* Answered before the nonce is burned and before anything is stored:
|
|
1488
|
+
* re-seal (a fresh seal captures the group, so it carries the header).
|
|
1489
|
+
* - 409 `mls.group_superseded` (stranded sender): step 4b answers a stored
|
|
1490
|
+
* id first, so reaching this answer proves NOTHING is stored under the
|
|
1491
|
+
* id; the nonce was burned at step 5. Clear the bytes, and stop: the send
|
|
1492
|
+
* is terminal ({@link GroupSupersededSendError}, spec §7.4), and a
|
|
1493
|
+
* re-seal now would seal under the same superseded group. The NEXT
|
|
1494
|
+
* attempt re-seals, after the bridge holds the active group.
|
|
1495
|
+
*
|
|
1496
|
+
* Stranded sender, carriage (spec §7.3): a cached envelope is resent with the
|
|
1497
|
+
* group recorded for those exact bytes (the store checks the digest), or with
|
|
1498
|
+
* NO header when none is recorded (an intent from before this change). It is
|
|
1499
|
+
* never re-sealed pre-emptively for lack of a header.
|
|
1500
|
+
*
|
|
1501
|
+
* "Never publish twice" rests on the #706 contract plus one invariant of
|
|
1502
|
+
* this function: the cached envelope ALWAYS holds the last bytes posted
|
|
1503
|
+
* under this id. The envelope is saved (awaited) BEFORE every fresh POST,
|
|
1504
|
+
* and it is cleared only after the backend answered `mls.stale_epoch` or
|
|
1505
|
+
* `replay_nonce` for those very bytes, which #706 defines as "not accepted"
|
|
1506
|
+
* (nothing is stored under the id). So a POST of our own possibly-accepted
|
|
1507
|
+
* bytes is always a resend of those same bytes, which #706 answers with the
|
|
1508
|
+
* original ack; a conflict can only mean the slot belongs to another
|
|
1509
|
+
* message (for example an old reply whose intent was pruned).
|
|
1510
|
+
*/
|
|
1511
|
+
private publishIntentOnce;
|
|
909
1512
|
private sendDeps;
|
|
910
1513
|
private receiveDeps;
|
|
911
1514
|
sendAttachment(channelId: string, filePath: string, caption?: string): Promise<{
|
|
@@ -914,6 +1517,14 @@ export declare class BridgeAgent {
|
|
|
914
1517
|
}>;
|
|
915
1518
|
private vaultRoot;
|
|
916
1519
|
private vaultAuthHeaders;
|
|
1520
|
+
/**
|
|
1521
|
+
* SEC-06 #11 (FB-P1-024): every backend call this class makes directly goes
|
|
1522
|
+
* through the SDK's one deadline helper, so a stalled backend or proxy can
|
|
1523
|
+
* never hold a lane forever. `read` for idempotent GETs, `control` for
|
|
1524
|
+
* writes; blob transfers use `fetchWithDeadline` with a size-scaled budget.
|
|
1525
|
+
* A timeout rejects with the SDK's `FetchDeadlineError`.
|
|
1526
|
+
*/
|
|
1527
|
+
private fetchBounded;
|
|
917
1528
|
/** M6 (Task 11): idempotent get-or-create resolve of the enclave's hidden
|
|
918
1529
|
* vault channel id — used as the fallback when a caller omits
|
|
919
1530
|
* vaultChannelId. Mirrors connect-web's filesafe-api.ts resolveVaultChannel
|
|
@@ -1007,7 +1618,7 @@ export declare class BridgeAgent {
|
|
|
1007
1618
|
*
|
|
1008
1619
|
* Dedupe: `fastPathSeen` is keyed by messageId so a redelivered/duplicate
|
|
1009
1620
|
* InboxEntry (e.g. after an ack race) never fires the fast LLM — and never
|
|
1010
|
-
* double-sends — twice for the same turn. The `fastReplyId(
|
|
1621
|
+
* double-sends — twice for the same turn. The `fastReplyId(...)` send
|
|
1011
1622
|
* id is itself also deterministic, so even a duplicate call that slipped past the
|
|
1012
1623
|
* in-memory dedupe (e.g. a process restart) resolves to the same envelope
|
|
1013
1624
|
* message id rather than a second spoken reply.
|
|
@@ -1237,9 +1848,6 @@ export declare class BridgeAgent {
|
|
|
1237
1848
|
/** call_summary rides a system_event envelope with a body-level type, the
|
|
1238
1849
|
* same shape sendActionFrame uses for agent_action. */
|
|
1239
1850
|
private sendSummary;
|
|
1240
|
-
/** Minimal stderr logger for non-fatal daemon events (mirrors the ad hoc
|
|
1241
|
-
* `process.stderr.write` calls elsewhere in this file). */
|
|
1242
|
-
private log;
|
|
1243
1851
|
/**
|
|
1244
1852
|
* The coalescing throttle (review H4 / Global Constraint: ~1/sec per run).
|
|
1245
1853
|
* If the run has no dirty nodes, no-op. If the per-run bucket has no token,
|
|
@@ -1268,14 +1876,174 @@ export declare class BridgeAgent {
|
|
|
1268
1876
|
*/
|
|
1269
1877
|
private actionWrap;
|
|
1270
1878
|
protected startReceivePump(): void;
|
|
1879
|
+
/**
|
|
1880
|
+
* Run one ingest cycle, SINGLE-FLIGHT. Every trigger goes through here: the
|
|
1881
|
+
* delivery-ping pump, the 25 s safety tick, and start()'s boot drain.
|
|
1882
|
+
*
|
|
1883
|
+
* Two cycles must never run at once. Both pull the same unacked rows, custody
|
|
1884
|
+
* serves the same application message to both, and each copy feeds
|
|
1885
|
+
* per-message state before the copies merge in the runtime inbox: the
|
|
1886
|
+
* agent-hop counter counted one message twice, and the inflated run
|
|
1887
|
+
* suppressed and ACKED the next agent-authored message (a silent loss). Two
|
|
1888
|
+
* concurrent reconcileJoin passes over one channel's Welcome are not a
|
|
1889
|
+
* state this pump should be able to reach either.
|
|
1890
|
+
*
|
|
1891
|
+
* A trigger that lands while a cycle is in flight is NOT satisfied by that
|
|
1892
|
+
* cycle: it pulled before the trigger, so a message the ping announced could
|
|
1893
|
+
* sit until the next safety tick. It waits for ONE follow-up cycle, shared by
|
|
1894
|
+
* every trigger that arrived during the in-flight one, and its promise
|
|
1895
|
+
* settles when that follow-up settles. So "ingestOnce() resolved" still means
|
|
1896
|
+
* "a cycle that started after my call has run", and a burst of pings costs
|
|
1897
|
+
* at most one extra cycle.
|
|
1898
|
+
*
|
|
1899
|
+
* A slow cycle is NOT bypassed: a trigger behind it queues, and past
|
|
1900
|
+
* INGEST_STALL_LOG_MS logs `receive.ingest_stalled` once.
|
|
1901
|
+
*
|
|
1902
|
+
* THE VALVE. A cycle older than INGEST_ABANDON_MS is abandoned by the next
|
|
1903
|
+
* trigger: `receive.ingest_abandoned` is logged, the gate forgets it, and
|
|
1904
|
+
* every waiter queued behind it is released to a fresh cycle. It exists
|
|
1905
|
+
* because a cycle can still wait on something with no deadline: every
|
|
1906
|
+
* RestMlsBackend READ, ack and nack is bounded (read 10 s, control 15 s),
|
|
1907
|
+
* but its state-changing MLS writes (distribute, request-welcome, init,
|
|
1908
|
+
* reinit, key-package publish, commit lease) are deliberately not, and a
|
|
1909
|
+
* channel lock can be held by a send waiting on one. Without the valve, one
|
|
1910
|
+
* such await that never settles stops inbound for the life of the process,
|
|
1911
|
+
* where the pre-single-flight code's overlapping 25 s tick kept rows flowing.
|
|
1912
|
+
*
|
|
1913
|
+
* ⚠️ What the 10 minutes is and is not. It is the age of the whole cycle,
|
|
1914
|
+
* not a per-request bound: each request carries its own budget, and a cycle
|
|
1915
|
+
* makes many in sequence (a pull, a join per unjoined channel, a roster read
|
|
1916
|
+
* per channel, one decrypt pass over up to 100 rows, an ack). So a cycle
|
|
1917
|
+
* that is merely slow, and still progressing, can be abandoned too.
|
|
1918
|
+
*
|
|
1919
|
+
* ⚠️ What a late wake costs. The abandoned cycle keeps running if it ever
|
|
1920
|
+
* wakes, now beside the replacement: the overlap this gate exists to stop.
|
|
1921
|
+
* Two consequences, both at-least-once, neither a loss:
|
|
1922
|
+
* - Hop counting: the per-message idempotency in AgentHopTracker absorbs
|
|
1923
|
+
* the duplicate sighting.
|
|
1924
|
+
* - Redelivery to the runtime: the replacement may already have staged,
|
|
1925
|
+
* delivered AND had the runtime ack a message the abandoned cycle still
|
|
1926
|
+
* holds. The runtime ack removes it from `runtimePendingIds`, so the late
|
|
1927
|
+
* cycle's stageRuntimeMessage stages it again and the runtime is handed an
|
|
1928
|
+
* ALREADY-ACKED message a second time. The first-party runtime dedupes by
|
|
1929
|
+
* message_id; a third-party consumer of the bridge sees a duplicate. The
|
|
1930
|
+
* delivery row's own double ack is a no-op at the server
|
|
1931
|
+
* (`acked_at IS NULL`).
|
|
1932
|
+
*/
|
|
1271
1933
|
private ingestOnce;
|
|
1934
|
+
private startIngestCycle;
|
|
1935
|
+
private ingestCycle;
|
|
1272
1936
|
drain(channelId: string | undefined, waitSeconds: number): Promise<BridgeMessage[]>;
|
|
1273
1937
|
/** Notify-only SSE signal: fired after a buffer push. Listener pulls via takeBuffered. */
|
|
1274
1938
|
subscribe(listener: () => void): () => void;
|
|
1275
1939
|
/** Synchronous splice of buffered messages (all, or one channel). Shared by drain + SSE. */
|
|
1276
1940
|
takeBuffered(channelId?: string): BridgeMessage[];
|
|
1941
|
+
/**
|
|
1942
|
+
* Phase 3 F2: per-channel ordered delivery for the /v1/ws runtime consumer.
|
|
1943
|
+
* Splices out, oldest first, at most ONE buffered message per channel that
|
|
1944
|
+
* is not in `busyChannels` (the channels the consumer still holds a message
|
|
1945
|
+
* in flight for); everything else stays buffered in order. A runtime is
|
|
1946
|
+
* therefore never handed a second message for a channel it is still
|
|
1947
|
+
* working on — which it used to refuse with a "busy" reply and ack — and a
|
|
1948
|
+
* backlog replays one message per channel at a time after a restart.
|
|
1949
|
+
*/
|
|
1950
|
+
takeDeliverable(busyChannels: ReadonlySet<string>): BridgeMessage[];
|
|
1951
|
+
private hasReplyIntent;
|
|
1952
|
+
/**
|
|
1953
|
+
* SEC-06 #18: answer a redelivered message from its reply intent. Counts as
|
|
1954
|
+
* a delivery attempt (so the cap and backoff still bound it) and holds the
|
|
1955
|
+
* channel like an in-flight message. Success acknowledges the inbound; an
|
|
1956
|
+
* ambiguous failure requeues it behind the usual backoff, still intercepted.
|
|
1957
|
+
*/
|
|
1958
|
+
private startReplyReplay;
|
|
1959
|
+
/**
|
|
1960
|
+
* Publish every pending part of `m`'s reply, in order, from the intent
|
|
1961
|
+
* (same id, same text), then record the reply receipt. A TERMINAL refusal
|
|
1962
|
+
* (paused, forbidden) resolves too: the inbound is retired, exactly as the
|
|
1963
|
+
* live send path retires it. Any other failure rejects.
|
|
1964
|
+
*/
|
|
1965
|
+
private replayReplyIntents;
|
|
1966
|
+
/**
|
|
1967
|
+
* Phase 3 F1: gate ONE delivery of `message` to the runtime consumer. Returns
|
|
1968
|
+
* true when it may be sent (and counts the attempt durably), false when the
|
|
1969
|
+
* message already reached the delivery cap — then it is dead-lettered:
|
|
1970
|
+
* acknowledged, never redelivered, never reaching a model. (Paused channels
|
|
1971
|
+
* never get here: takeDeliverable holds them, so a pause spends no attempts.)
|
|
1972
|
+
* Logs are content-free (event + counts only).
|
|
1973
|
+
*/
|
|
1974
|
+
admitRuntimeDelivery(message: BridgeMessage): boolean;
|
|
1975
|
+
/**
|
|
1976
|
+
* Final-wave I1: put back runtime deliveries that FAILED — the runtime sent a
|
|
1977
|
+
* `nack` for the message, or the socket closed while it was in flight (an
|
|
1978
|
+
* older runtime closes on any failed turn). Each one's attempt was already
|
|
1979
|
+
* counted durably at delivery; here it either dead-letters (cap reached) or
|
|
1980
|
+
* is requeued at the front behind a 2^attempts × 5 s redelivery backoff.
|
|
1981
|
+
* Messages that were never handed to the runtime go back via requeueFront
|
|
1982
|
+
* instead and are not affected.
|
|
1983
|
+
*/
|
|
1984
|
+
requeueFailedRuntimeDeliveries(messages: BridgeMessage[]): void;
|
|
1985
|
+
/**
|
|
1986
|
+
* Phase 3 F1 dead letter: acknowledged, never redelivered, never reaching a
|
|
1987
|
+
* model — plus (final-wave I1(c)) one content-free notice to the channel,
|
|
1988
|
+
* at most once per channel per hour. Logs carry counts only.
|
|
1989
|
+
*/
|
|
1990
|
+
private deadLetter;
|
|
1991
|
+
private noticeDeadLetter;
|
|
1992
|
+
/** Poke consumers once the earliest redelivery backoff ends. */
|
|
1993
|
+
private armRedeliveryWake;
|
|
1994
|
+
/**
|
|
1995
|
+
* 2026-09-25: this agent's OWN display_name for the room @mention filter
|
|
1996
|
+
* (`roomDisplayName`). Never the channel's name: a channel called
|
|
1997
|
+
* "Hermesav, Cortina011" must not make `@hermesav` address this agent. A pure
|
|
1998
|
+
* field read; the roster refresh that fills it runs once per ingest cycle,
|
|
1999
|
+
* TTL-gated, before the filter. Undefined until any roster has listed this
|
|
2000
|
+
* agent, in which case the filter still matches the credential agentName and
|
|
2001
|
+
* the identity id.
|
|
2002
|
+
*/
|
|
2003
|
+
private ownRoomDisplayName;
|
|
2004
|
+
/** Phase 3 F3: the loop guard's view of the sender — server roster first. */
|
|
2005
|
+
private loopSenderIsAgent;
|
|
2006
|
+
private noteAgentTurnSuppressed;
|
|
2007
|
+
/**
|
|
2008
|
+
* Phase 3 F1: whether `channelId`'s buffered work must be HELD because the
|
|
2009
|
+
* agent is paused there. Held work stays durably pending (never acked, no
|
|
2010
|
+
* delivery attempt spent) and is released by {@link recheckPause} once the
|
|
2011
|
+
* owner resumes the agent. Only a channel whose send was refused with
|
|
2012
|
+
* `agent_paused` is ever held. Holding arms a recheck at the end of the
|
|
2013
|
+
* PAUSE_RECHECK_MS window, so release does not depend on new traffic.
|
|
2014
|
+
*/
|
|
2015
|
+
private heldForPause;
|
|
2016
|
+
private schedulePauseRecheck;
|
|
2017
|
+
/**
|
|
2018
|
+
* Re-read this agent's own membership row. Still paused → keep holding and
|
|
2019
|
+
* re-arm while work is held. Resumed — or a failed/inconclusive probe, which
|
|
2020
|
+
* fails OPEN so a channel is never left deaf (the worst case is one model
|
|
2021
|
+
* call whose reply is refused again, re-arming the pause) — releases the
|
|
2022
|
+
* held work and pokes consumers to drain it now.
|
|
2023
|
+
*/
|
|
2024
|
+
private recheckPause;
|
|
2025
|
+
private clearPause;
|
|
2026
|
+
/** Read this agent's own `paused_at` from the enclave roster; undefined = unknown. */
|
|
2027
|
+
private probeOwnPause;
|
|
1277
2028
|
recordRuntimeReply(inReplyTo: string, channelId: string, replyMessageId: string): RuntimeReplyReceipt;
|
|
1278
2029
|
runtimeReplyReceipt(inReplyTo: string): RuntimeReplyReceipt | null;
|
|
2030
|
+
/**
|
|
2031
|
+
* Phase 3 F1: retire a runtime message whose reply failed TERMINALLY
|
|
2032
|
+
* (agent_paused / send_forbidden): acknowledged durably, never redelivered,
|
|
2033
|
+
* so the model is not run again for it. Pokes consumers so a socket holding
|
|
2034
|
+
* it in flight frees the channel for its next message.
|
|
2035
|
+
*/
|
|
2036
|
+
retireRuntimeMessage(messageId: string): void;
|
|
2037
|
+
isRuntimeMessageRetired(messageId: string): boolean;
|
|
2038
|
+
/** Complete the durable handoff only after the runtime explicitly acks. */
|
|
2039
|
+
ackRuntimeMessages(messageIds: string[]): void;
|
|
2040
|
+
private restoreRuntimeInbox;
|
|
2041
|
+
/**
|
|
2042
|
+
* Seal a runtime frame before the backend queue id may be acknowledged.
|
|
2043
|
+
* Returning false is backpressure: the caller leaves the backend row unacked
|
|
2044
|
+
* so it remains the durable queue rather than shedding accepted work.
|
|
2045
|
+
*/
|
|
2046
|
+
private stageRuntimeMessage;
|
|
1279
2047
|
/**
|
|
1280
2048
|
* Put messages back at the front (SSE/disconnect write-failure recovery).
|
|
1281
2049
|
*
|
|
@@ -1321,6 +2089,12 @@ export declare class BridgeAgent {
|
|
|
1321
2089
|
dropped: number;
|
|
1322
2090
|
/** Cumulative ROOM messages dropped by the @mention filter (correct filtering ≠ idle). */
|
|
1323
2091
|
filtered_room: number;
|
|
2092
|
+
/** Phase 3 F1: inbound messages retired at the runtime delivery-attempt cap. */
|
|
2093
|
+
dead_lettered: number;
|
|
2094
|
+
/** Phase 3 F1: channels whose inbound is currently held because the agent is paused. */
|
|
2095
|
+
paused_channels: number;
|
|
2096
|
+
/** Phase 3 F3: agent-authored messages not answered (no explicit mention / hop limit). */
|
|
2097
|
+
agent_loop_suppressed: number;
|
|
1324
2098
|
/** Cumulative sends shed by the per-channel rate-limit floor (correct backpressure ≠ idle). */
|
|
1325
2099
|
rate_limited: number;
|
|
1326
2100
|
/** Cumulative inbound rows acked-and-dropped because they were provably past-epoch undecryptable (forward-secrecy secret gone). */
|
|
@@ -1331,6 +2105,15 @@ export declare class BridgeAgent {
|
|
|
1331
2105
|
/** SDK-6: cumulative rows ack-drained because their group_id is RETIRED for
|
|
1332
2106
|
* the channel (superseded fork-loser traffic). */
|
|
1333
2107
|
retired_drained: number;
|
|
2108
|
+
/** INV-CUSTODY-DURABLE §5.C.6: rows silently drained past expires_at. */
|
|
2109
|
+
expired_drained: number;
|
|
2110
|
+
/** INV-CUSTODY-DURABLE §5.C.3: a decrypted, un-acked row is memory-only
|
|
2111
|
+
* (its durable write failed); a restart now would lose it. */
|
|
2112
|
+
custody_persist_failing: boolean;
|
|
2113
|
+
custody_persist_failures: number;
|
|
2114
|
+
/** #772 audit M2/M1: custody reads failing; the store's breaker open. */
|
|
2115
|
+
custody_read_failing: boolean;
|
|
2116
|
+
custody_store_unavailable: boolean;
|
|
1334
2117
|
/** Track 5: cumulative artifact_share messages where download/decrypt/digest failed (fail-soft — acked, no redeliver). */
|
|
1335
2118
|
failed_attachments: number;
|
|
1336
2119
|
/** Task 4 (self-hosted fast voice): cumulative fast-path replies actually sent. */
|
|
@@ -1379,6 +2162,26 @@ export declare class BridgeAgent {
|
|
|
1379
2162
|
* not be resolved. `null` folds into `degraded` exactly like `true`: healthy
|
|
1380
2163
|
* requires a positive, current proof of non-expiry. */
|
|
1381
2164
|
keypackage_expired: boolean | null;
|
|
2165
|
+
/** #792 review O4: FAILED held-vs-published KeyPackage reads (a read
|
|
2166
|
+
* deadline, 401/403, 429, network, a store read that throws). Since #792 a
|
|
2167
|
+
* failed read defers the re-Welcome request instead of rotating, so a
|
|
2168
|
+
* PERSISTENT failure is a device that silently never asks to be re-added.
|
|
2169
|
+
* `_consecutive` resets on the next read that returns an answer; alert on
|
|
2170
|
+
* it staying above zero across several sweeps. Not folded into `degraded`:
|
|
2171
|
+
* the read runs only when a channel needs a heal, so a stale streak can
|
|
2172
|
+
* outlive the need. A mismatch is an answer, never counted here. */
|
|
2173
|
+
keypackage_match_read_failures: number;
|
|
2174
|
+
keypackage_match_read_failures_consecutive: number;
|
|
2175
|
+
/** ms since epoch of the newest failed read, or null. */
|
|
2176
|
+
keypackage_match_read_last_failed_at: number | null;
|
|
2177
|
+
/** Architect ruling on the #793 M1 proposal, 1D: reconciles that DEFERRED
|
|
2178
|
+
* (an in-lock drain arm) and that were REFUSED on a full delivery page.
|
|
2179
|
+
* A device that keeps refusing is one the server cannot vend: alert on the
|
|
2180
|
+
* `_consecutive` streak. */
|
|
2181
|
+
keypackage_reconcile_deferred: number;
|
|
2182
|
+
keypackage_reconcile_deferred_consecutive: number;
|
|
2183
|
+
keypackage_reconcile_refused_page_full: number;
|
|
2184
|
+
keypackage_reconcile_refused_page_full_consecutive: number;
|
|
1382
2185
|
/** A2 (dr-evil F3, decision D-3): the truthful roll-up — true when any
|
|
1383
2186
|
* channel is definitively diverged OR the observations cannot be trusted
|
|
1384
2187
|
* (stale/absent). Additive: `status` stays "ok" until the flip ships as
|
|
@@ -1439,7 +2242,17 @@ export declare class BridgeAgent {
|
|
|
1439
2242
|
errors: ReturnType<MlsGroupKeyProvider["errorStats"]>;
|
|
1440
2243
|
dropped: number;
|
|
1441
2244
|
filteredRoomMessages: number;
|
|
2245
|
+
/** Phase 3 F1: inbound messages retired at the runtime delivery cap. */
|
|
2246
|
+
deadLetteredMessages: number;
|
|
2247
|
+
/** Phase 3 F1: channels whose inbound is currently held (agent paused). */
|
|
2248
|
+
pausedChannels: number;
|
|
2249
|
+
/** Phase 3 F3: agent-authored messages not answered by the loop guard. */
|
|
2250
|
+
agentLoopSuppressed: number;
|
|
1442
2251
|
rateLimitedSends: number;
|
|
2252
|
+
/** SEC-06 #18: redelivered messages answered from a reply intent (no model run). */
|
|
2253
|
+
replyIntentReplays: number;
|
|
2254
|
+
/** SEC-06 #18: in-process reply publish retries after a rate limit. */
|
|
2255
|
+
replyRateLimitRetries: number;
|
|
1443
2256
|
permanentlySkipped: number;
|
|
1444
2257
|
/** SDK-6: cumulative rows ack-drained via QUARANTINE, distinct from the
|
|
1445
2258
|
* same-group past-epoch permanentlySkipped drain. */
|
|
@@ -1447,6 +2260,9 @@ export declare class BridgeAgent {
|
|
|
1447
2260
|
/** SDK-6: cumulative rows ack-drained because their group_id is RETIRED for
|
|
1448
2261
|
* the channel (superseded fork-loser traffic). */
|
|
1449
2262
|
retiredDrained: number;
|
|
2263
|
+
expiredDrained: number;
|
|
2264
|
+
/** INV-CUSTODY-DURABLE §5.C.3 custody health snapshot. */
|
|
2265
|
+
custody: ReturnType<MlsGroupKeyProvider["custodyHealth"]>;
|
|
1450
2266
|
welcomeRequestsSent: number;
|
|
1451
2267
|
/** Track 5: cumulative artifact_share failures (download/decrypt/digest). */
|
|
1452
2268
|
failedAttachments: number;
|