@securecoms/agent-bridge 0.20.0 → 0.23.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 +48 -2
- package/connectors/hermes/adapter.py +38 -8
- package/connectors/hermes/tests/test_adapter_wiring.py +6 -1
- package/connectors/hermes/tests/test_logging_privacy.py +61 -0
- package/connectors/openclaw/openclaw-entry.inbound.test.ts +560 -0
- package/connectors/openclaw/openclaw-entry.js +248 -51
- package/connectors/openclaw/openclaw-entry.smoke.test.ts +23 -1
- package/dist/bin.d.ts +3 -7
- package/dist/bin.js +3300 -476
- package/dist/bridge-agent.d.ts +280 -8
- package/dist/coordination.d.ts +90 -5
- package/dist/decrypt-error-log-limiter.d.ts +6 -30
- package/dist/errors.d.ts +1 -1
- package/dist/http-server.d.ts +20 -1
- package/dist/index.js +3289 -456
- package/dist/reply-receipts.d.ts +30 -0
- package/dist/service/runtime-config-dir.d.ts +34 -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/ws-server.d.ts +30 -3
- package/package.json +2 -2
package/dist/bridge-agent.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { MlsGroupKeyProvider } from "@agentvault/connect-sdk-ts";
|
|
2
|
+
import type { MemoryContextResult } from "@agentvault/connect-mls-store";
|
|
2
3
|
/**
|
|
3
4
|
* M6 (ADR-0013 Decision H): what the sweep observed about one channel.
|
|
4
5
|
*
|
|
@@ -35,12 +36,45 @@ import { type ChatMessage } from "./voice/fast-prompt.js";
|
|
|
35
36
|
import { VOICE_CALL_START_MARKER } from "./voice/prompt-compiler.js";
|
|
36
37
|
import { type VaultListNode } from "./vault.js";
|
|
37
38
|
import type { AgentActionNodeType, AgentActionNodeStatus, AgentActionNodeDetail } from "@agentvault/connect-core";
|
|
39
|
+
import type { RuntimeReplyReceipt } from "./reply-receipts.js";
|
|
40
|
+
export type AgentAudience = "owner_dm" | "team_room" | "partner_room" | "other_human_dm" | "agent_to_agent" | "unknown";
|
|
41
|
+
export type BridgeToolGrant = {
|
|
42
|
+
readonly tool: string;
|
|
43
|
+
readonly mode: "read";
|
|
44
|
+
} | {
|
|
45
|
+
readonly tool: string;
|
|
46
|
+
readonly mode: "side_effect";
|
|
47
|
+
readonly operation_sha256: string;
|
|
48
|
+
readonly confirmation_id: string;
|
|
49
|
+
readonly idempotency_key: string;
|
|
50
|
+
readonly expires_at: string;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Immutable authorization context derived from authenticated backend roster
|
|
54
|
+
* data. The E2E plaintext never contributes to these claims. Tool grants are
|
|
55
|
+
* intentionally empty until the channel-grant persistence/API lands; this
|
|
56
|
+
* makes the new contract fail closed during staged deployment.
|
|
57
|
+
*/
|
|
58
|
+
export interface BridgeAuthorization {
|
|
59
|
+
readonly version: 1;
|
|
60
|
+
readonly audience: AgentAudience;
|
|
61
|
+
readonly sender_membership: "active" | "unknown";
|
|
62
|
+
readonly tool_grants: readonly BridgeToolGrant[];
|
|
63
|
+
}
|
|
38
64
|
export interface BridgeMessage {
|
|
39
65
|
channel_id: string;
|
|
40
66
|
message_id: string;
|
|
41
67
|
sender_id: string;
|
|
42
68
|
text: string;
|
|
43
69
|
ts: number;
|
|
70
|
+
authorization: BridgeAuthorization;
|
|
71
|
+
/**
|
|
72
|
+
* Protected, channel-scoped memory captured before this inbound turn is
|
|
73
|
+
* recorded. The runtime can therefore prepend prior context without owning
|
|
74
|
+
* a second plaintext persistence path or duplicating the current message.
|
|
75
|
+
* Missing only for non-text/legacy buffered entries.
|
|
76
|
+
*/
|
|
77
|
+
memory?: MemoryContextResult;
|
|
44
78
|
/**
|
|
45
79
|
* Phase 2 (sender attribution): the sender's display name, resolved from the
|
|
46
80
|
* channel roster cache. Undefined when the roster hasn't resolved the
|
|
@@ -179,6 +213,19 @@ export interface BridgeAgentDeps {
|
|
|
179
213
|
* recent (a real-time messaging consumer cares about the newest). Default 1000.
|
|
180
214
|
*/
|
|
181
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;
|
|
182
229
|
/**
|
|
183
230
|
* Review I3/M3: clock seam for the agent.action.* partial-throttle token
|
|
184
231
|
* buckets AND the unsettled-run stale-TTL sweep. Defaults to Date.now.
|
|
@@ -211,10 +258,46 @@ export interface BridgeAgentDeps {
|
|
|
211
258
|
}
|
|
212
259
|
/** Default inbound-buffer cap (messages) when deps.maxBuffer is unset. */
|
|
213
260
|
export declare const DEFAULT_MAX_BUFFER = 1000;
|
|
261
|
+
/**
|
|
262
|
+
* Phase 3 F1: default per-message delivery cap for the /v1/ws runtime handoff.
|
|
263
|
+
* Deliberately ABOVE the OpenClaw connector's own MAX_INBOUND_ATTEMPTS (3,
|
|
264
|
+
* connectors/openclaw/openclaw-entry.js): that connector acks a poison
|
|
265
|
+
* message itself after three failed turns, so this daemon cap is only the
|
|
266
|
+
* backstop for consumers without one, and the two extra deliveries leave
|
|
267
|
+
* headroom for the connector's "runtime not ready" / receipt-lookup retries,
|
|
268
|
+
* which it deliberately does not count.
|
|
269
|
+
*/
|
|
270
|
+
export declare const DEFAULT_MAX_DELIVERY_ATTEMPTS = 5;
|
|
271
|
+
/**
|
|
272
|
+
* Final-wave I1(a): a runtime delivery that failed (a `nack`, or the socket
|
|
273
|
+
* closing with the message in flight) is not redelivered before
|
|
274
|
+
* 2^attempts × this base. With the default cap of 5 the deliveries span
|
|
275
|
+
* minutes, not seconds, so a provider outage no longer dead-letters work
|
|
276
|
+
* within seconds. The attempt counter is durable, and the delay is derived
|
|
277
|
+
* from it — a restart re-arms the same backoff from the restore.
|
|
278
|
+
*/
|
|
279
|
+
export declare const REDELIVERY_BACKOFF_BASE_MS = 5000;
|
|
280
|
+
export declare function redeliveryDelayMs(attempts: number): number;
|
|
281
|
+
/**
|
|
282
|
+
* Final-wave I1(c): the one content-free reply a channel gets when one of its
|
|
283
|
+
* messages is dead-lettered. Fixed text — it never echoes the message.
|
|
284
|
+
*/
|
|
285
|
+
export declare const DEAD_LETTER_NOTICE = "I couldn't process a message. Please resend it.";
|
|
286
|
+
/** At most one {@link DEAD_LETTER_NOTICE} per channel per this window. */
|
|
287
|
+
export declare const DEAD_LETTER_NOTICE_INTERVAL_MS: number;
|
|
288
|
+
/**
|
|
289
|
+
* Phase 3 F1 (pause stops inference): after a send is refused with
|
|
290
|
+
* `agent_paused`, inbound traffic for that channel is not handed to the
|
|
291
|
+
* runtime (no model call). The pause is reversible, so once this long has
|
|
292
|
+
* passed since it was last confirmed, the next inbound message re-probes the
|
|
293
|
+
* agent's own membership row before deciding.
|
|
294
|
+
*/
|
|
295
|
+
export declare const PAUSE_RECHECK_MS = 30000;
|
|
214
296
|
/**
|
|
215
297
|
* TTL for the Phase 2 channelMeta cache. `is_dm` is immutable for a channel_id,
|
|
216
|
-
* so this exists only so a cached `name`
|
|
217
|
-
*
|
|
298
|
+
* so this exists only so a cached channel `name` re-resolves after a channel
|
|
299
|
+
* rename. The channel name is NOT an @mention name for this agent (2026-09-25:
|
|
300
|
+
* the filter uses this agent's own roster display_name). Lazy expiry on read.
|
|
218
301
|
*/
|
|
219
302
|
export declare const CHANNEL_META_TTL_MS: number;
|
|
220
303
|
/**
|
|
@@ -289,6 +372,17 @@ export declare class AgentPausedSendError extends Error {
|
|
|
289
372
|
readonly channelId: string;
|
|
290
373
|
constructor(channelId: string);
|
|
291
374
|
}
|
|
375
|
+
/**
|
|
376
|
+
* Phase 3 F1: the backend refused a publish with a bare 401/403 (not the
|
|
377
|
+
* typed `agent_paused` body) — the agent's credential or device has been
|
|
378
|
+
* revoked or it is no longer a member. Retrying cannot succeed, so this is
|
|
379
|
+
* TERMINAL for the reply: the runtime's inbound message is acknowledged and
|
|
380
|
+
* the model is not run again for it.
|
|
381
|
+
*/
|
|
382
|
+
export declare class SendForbiddenError extends Error {
|
|
383
|
+
readonly channelId: string;
|
|
384
|
+
constructor(channelId: string);
|
|
385
|
+
}
|
|
292
386
|
/** Thrown by `agent.action.*` when a runId (or nodeId within a run) is unknown
|
|
293
387
|
* or the run has already settled — settle() is terminal, mirroring the client
|
|
294
388
|
* ingest side's "later partials for a settled run are dropped" rule. */
|
|
@@ -372,6 +466,15 @@ export declare class BridgeAgent {
|
|
|
372
466
|
* per channel on a miss.
|
|
373
467
|
*/
|
|
374
468
|
private readonly channelRoster;
|
|
469
|
+
/**
|
|
470
|
+
* 2026-09-25: this agent's own display_name (`ConnectIdentity.display_name`,
|
|
471
|
+
* identity-level, so the same in every channel) from the MOST RECENT roster
|
|
472
|
+
* refresh that listed it. Read by the room @mention filter as its
|
|
473
|
+
* `roomDisplayName`. Kept across a later failed refresh, so a transient
|
|
474
|
+
* members-fetch error does not make the agent deaf to its own name; a
|
|
475
|
+
* successful refresh after a rename replaces it.
|
|
476
|
+
*/
|
|
477
|
+
private lastOwnDisplayName;
|
|
375
478
|
/**
|
|
376
479
|
* Task 8 (owner-attested Reset Secure Session): per-channel STRUCTURAL owner
|
|
377
480
|
* cache, fed by resolveAndPinOwner() on every roster refresh and consumed by
|
|
@@ -382,7 +485,37 @@ export declare class BridgeAgent {
|
|
|
382
485
|
*/
|
|
383
486
|
private readonly channelOwner;
|
|
384
487
|
private readonly buffer;
|
|
488
|
+
/** Message ids durably staged for the attached runtime but not yet acked. */
|
|
489
|
+
private readonly runtimePendingIds;
|
|
385
490
|
private readonly maxBuffer;
|
|
491
|
+
/** Phase 3 F1: deliveries so far per pending runtime message (seeded from the store). */
|
|
492
|
+
private readonly deliveryAttempts;
|
|
493
|
+
private readonly maxDeliveryAttempts;
|
|
494
|
+
/** Phase 3 F1: cumulative messages dead-lettered at the delivery cap. */
|
|
495
|
+
private deadLetteredMessages;
|
|
496
|
+
/** Final-wave I1(a): message id → earliest redelivery time (epoch ms). */
|
|
497
|
+
private readonly redeliverNotBefore;
|
|
498
|
+
/** Final-wave I1(a): one timer poking consumers when the earliest backoff ends. */
|
|
499
|
+
private redeliveryTimer;
|
|
500
|
+
private redeliveryTimerAt;
|
|
501
|
+
/** Final-wave I1(c): channel → when its last dead-letter notice was posted. */
|
|
502
|
+
private readonly deadLetterNoticeAt;
|
|
503
|
+
/** Phase 3 F1: channel → when a pause was last confirmed (send 403 or probe). */
|
|
504
|
+
private readonly pausedChannels;
|
|
505
|
+
/**
|
|
506
|
+
* Phase 3 F1: runtime messages retired by a TERMINAL reply failure reported
|
|
507
|
+
* outside the /v1/ws socket that holds them in flight (HTTP /v1/send). The
|
|
508
|
+
* socket consults this so it neither redelivers them nor stays busy on them.
|
|
509
|
+
* Bounded; oldest forgotten first.
|
|
510
|
+
*/
|
|
511
|
+
private readonly retiredRuntimeIds;
|
|
512
|
+
/** Phase 3 F1: one pending membership recheck per paused channel holding work. */
|
|
513
|
+
private readonly pauseRecheckTimers;
|
|
514
|
+
/** Phase 3 F3: consecutive agent turns per channel. */
|
|
515
|
+
private readonly agentHops;
|
|
516
|
+
private readonly maxAgentHops;
|
|
517
|
+
/** Phase 3 F3: cumulative agent-authored messages not answered (no explicit mention / hop limit). */
|
|
518
|
+
private agentLoopSuppressed;
|
|
386
519
|
/** Cumulative count of messages shed by the buffer cap (surfaced on health()/metrics()). */
|
|
387
520
|
private droppedMessages;
|
|
388
521
|
/**
|
|
@@ -519,7 +652,7 @@ export declare class BridgeAgent {
|
|
|
519
652
|
private static readonly REQUEST_WELCOME_COOLDOWN_MS;
|
|
520
653
|
private static readonly GRACEFUL_DRAIN_MS;
|
|
521
654
|
/**
|
|
522
|
-
* P0b (daemon-stability): collapses the onDecryptError
|
|
655
|
+
* P0b (daemon-stability): collapses the onDecryptError diagnostic from
|
|
523
656
|
* "one line per failure" (the 114 MB/765k-line-in-a-day log source) to at
|
|
524
657
|
* most one line immediately + one summary line per 60s per channel. LOG
|
|
525
658
|
* OUTPUT ONLY — does not touch errorStats()/quarantine counters, which
|
|
@@ -601,6 +734,7 @@ export declare class BridgeAgent {
|
|
|
601
734
|
private readonly waiters;
|
|
602
735
|
private pingHandle?;
|
|
603
736
|
private safetyTimer?;
|
|
737
|
+
private runtimeConfigTimer?;
|
|
604
738
|
private readonly listeners;
|
|
605
739
|
private startedAt;
|
|
606
740
|
private readonly store;
|
|
@@ -819,9 +953,16 @@ export declare class BridgeAgent {
|
|
|
819
953
|
* reads `null` and therefore degraded.
|
|
820
954
|
*/
|
|
821
955
|
private keypackageExpired;
|
|
956
|
+
/** #335: dedupes the expiry-measurement warning to once per failure STREAK —
|
|
957
|
+
* any non-throwing resolution (including the known-unknown `null` returns,
|
|
958
|
+
* which are answers, not failures) re-arms it. Mirrors `enclaveListWarned`. */
|
|
959
|
+
private keypackageExpiryUnknownWarned;
|
|
822
960
|
/** Resolve the flag from the SAME two inputs the guard uses, with every unknown
|
|
823
961
|
* mapped to `null`. Never throws: a failure to measure is itself unknown. */
|
|
824
962
|
private resolveKeypackageExpired;
|
|
963
|
+
/** Every non-throwing resolution ends the failure streak (the next throw is
|
|
964
|
+
* a NEW streak and warns again) and passes the value through unchanged. */
|
|
965
|
+
private kpExpiryResolved;
|
|
825
966
|
private rotateDeviceKeyPackage;
|
|
826
967
|
private selfHealSweep;
|
|
827
968
|
/**
|
|
@@ -853,7 +994,14 @@ export declare class BridgeAgent {
|
|
|
853
994
|
* already resolves internally) propagates unchanged.
|
|
854
995
|
*/
|
|
855
996
|
private sendWithSelfHealTerminal;
|
|
856
|
-
|
|
997
|
+
/**
|
|
998
|
+
* `opts.agentTurn` (default true): whether this send is a SUBSTANTIVE agent
|
|
999
|
+
* turn for the Phase 3 F3 hop count. Notices (daily limit, dead letter) and
|
|
1000
|
+
* continuation parts of one multi-part reply pass false (final-wave I2).
|
|
1001
|
+
*/
|
|
1002
|
+
send(channelId: string, text: string, messageId?: string, opts?: {
|
|
1003
|
+
agentTurn?: boolean;
|
|
1004
|
+
}): Promise<{
|
|
857
1005
|
message_id: string;
|
|
858
1006
|
status: "sent";
|
|
859
1007
|
}>;
|
|
@@ -861,7 +1009,9 @@ export declare class BridgeAgent {
|
|
|
861
1009
|
a2a_version: string;
|
|
862
1010
|
kind: "message" | "task" | "artifact";
|
|
863
1011
|
object: Record<string, unknown>;
|
|
864
|
-
}, messageId?: string
|
|
1012
|
+
}, messageId?: string, opts?: {
|
|
1013
|
+
agentTurn?: boolean;
|
|
1014
|
+
}): Promise<{
|
|
865
1015
|
message_id: string;
|
|
866
1016
|
status: "sent";
|
|
867
1017
|
}>;
|
|
@@ -937,6 +1087,21 @@ export declare class BridgeAgent {
|
|
|
937
1087
|
seq: number;
|
|
938
1088
|
deduped: boolean;
|
|
939
1089
|
}>;
|
|
1090
|
+
/**
|
|
1091
|
+
* OpenClaw collapses text history and workspace facts into the owner's
|
|
1092
|
+
* session. Resolve the channel from fresh, authenticated server metadata
|
|
1093
|
+
* before exposing that provider through the local bridge API. A cold or
|
|
1094
|
+
* stale cache is refreshed once; any failure remains a fail-closed denial.
|
|
1095
|
+
*/
|
|
1096
|
+
private mayReadOpenClawMemory;
|
|
1097
|
+
/** An inbound turn may receive OpenClaw memory only from an active owner DM. */
|
|
1098
|
+
private mayReadOpenClawMemoryForTurn;
|
|
1099
|
+
/**
|
|
1100
|
+
* Resolve the prior-turn snapshot carried to the native runtime. OpenClaw
|
|
1101
|
+
* stays owner-DM-only; every provider failure degrades to empty context and
|
|
1102
|
+
* never falls back to the runtime's retired plaintext JSON files.
|
|
1103
|
+
*/
|
|
1104
|
+
private memoryForTurn;
|
|
940
1105
|
memoryContext(conversationId: string, limit?: number): Promise<import("@agentvault/connect-mls-store").MemoryContextResult>;
|
|
941
1106
|
/**
|
|
942
1107
|
* Task 4 (self-hosted fast voice): instant persona-voiced acknowledgement
|
|
@@ -1118,6 +1283,13 @@ export declare class BridgeAgent {
|
|
|
1118
1283
|
* attribution must NEVER block or drop a message.
|
|
1119
1284
|
*/
|
|
1120
1285
|
private refreshChannelRoster;
|
|
1286
|
+
/**
|
|
1287
|
+
* Classify the sender only from server-authenticated channel/roster data.
|
|
1288
|
+
* Missing or stale metadata is UNKNOWN with zero grants, never a permissive
|
|
1289
|
+
* guess. The returned object and grant array are frozen before they cross the
|
|
1290
|
+
* daemon boundary so one consumer cannot widen a later execution check.
|
|
1291
|
+
*/
|
|
1292
|
+
private authorizationFor;
|
|
1121
1293
|
/**
|
|
1122
1294
|
* Task 8 (owner-attested Reset Secure Session): STRUCTURAL owner selection —
|
|
1123
1295
|
* NEVER read the server's `role` field to pick the owner. In a DM the owner
|
|
@@ -1174,9 +1346,6 @@ export declare class BridgeAgent {
|
|
|
1174
1346
|
/** call_summary rides a system_event envelope with a body-level type, the
|
|
1175
1347
|
* same shape sendActionFrame uses for agent_action. */
|
|
1176
1348
|
private sendSummary;
|
|
1177
|
-
/** Minimal stderr logger for non-fatal daemon events (mirrors the ad hoc
|
|
1178
|
-
* `process.stderr.write` calls elsewhere in this file). */
|
|
1179
|
-
private log;
|
|
1180
1349
|
/**
|
|
1181
1350
|
* The coalescing throttle (review H4 / Global Constraint: ~1/sec per run).
|
|
1182
1351
|
* If the run has no dirty nodes, no-op. If the per-run bucket has no token,
|
|
@@ -1211,6 +1380,97 @@ export declare class BridgeAgent {
|
|
|
1211
1380
|
subscribe(listener: () => void): () => void;
|
|
1212
1381
|
/** Synchronous splice of buffered messages (all, or one channel). Shared by drain + SSE. */
|
|
1213
1382
|
takeBuffered(channelId?: string): BridgeMessage[];
|
|
1383
|
+
/**
|
|
1384
|
+
* Phase 3 F2: per-channel ordered delivery for the /v1/ws runtime consumer.
|
|
1385
|
+
* Splices out, oldest first, at most ONE buffered message per channel that
|
|
1386
|
+
* is not in `busyChannels` (the channels the consumer still holds a message
|
|
1387
|
+
* in flight for); everything else stays buffered in order. A runtime is
|
|
1388
|
+
* therefore never handed a second message for a channel it is still
|
|
1389
|
+
* working on — which it used to refuse with a "busy" reply and ack — and a
|
|
1390
|
+
* backlog replays one message per channel at a time after a restart.
|
|
1391
|
+
*/
|
|
1392
|
+
takeDeliverable(busyChannels: ReadonlySet<string>): BridgeMessage[];
|
|
1393
|
+
/**
|
|
1394
|
+
* Phase 3 F1: gate ONE delivery of `message` to the runtime consumer. Returns
|
|
1395
|
+
* true when it may be sent (and counts the attempt durably), false when the
|
|
1396
|
+
* message already reached the delivery cap — then it is dead-lettered:
|
|
1397
|
+
* acknowledged, never redelivered, never reaching a model. (Paused channels
|
|
1398
|
+
* never get here: takeDeliverable holds them, so a pause spends no attempts.)
|
|
1399
|
+
* Logs are content-free (event + counts only).
|
|
1400
|
+
*/
|
|
1401
|
+
admitRuntimeDelivery(message: BridgeMessage): boolean;
|
|
1402
|
+
/**
|
|
1403
|
+
* Final-wave I1: put back runtime deliveries that FAILED — the runtime sent a
|
|
1404
|
+
* `nack` for the message, or the socket closed while it was in flight (an
|
|
1405
|
+
* older runtime closes on any failed turn). Each one's attempt was already
|
|
1406
|
+
* counted durably at delivery; here it either dead-letters (cap reached) or
|
|
1407
|
+
* is requeued at the front behind a 2^attempts × 5 s redelivery backoff.
|
|
1408
|
+
* Messages that were never handed to the runtime go back via requeueFront
|
|
1409
|
+
* instead and are not affected.
|
|
1410
|
+
*/
|
|
1411
|
+
requeueFailedRuntimeDeliveries(messages: BridgeMessage[]): void;
|
|
1412
|
+
/**
|
|
1413
|
+
* Phase 3 F1 dead letter: acknowledged, never redelivered, never reaching a
|
|
1414
|
+
* model — plus (final-wave I1(c)) one content-free notice to the channel,
|
|
1415
|
+
* at most once per channel per hour. Logs carry counts only.
|
|
1416
|
+
*/
|
|
1417
|
+
private deadLetter;
|
|
1418
|
+
private noticeDeadLetter;
|
|
1419
|
+
/** Poke consumers once the earliest redelivery backoff ends. */
|
|
1420
|
+
private armRedeliveryWake;
|
|
1421
|
+
/**
|
|
1422
|
+
* 2026-09-25: this agent's OWN display_name for the room @mention filter
|
|
1423
|
+
* (`roomDisplayName`). Never the channel's name: a channel called
|
|
1424
|
+
* "Hermesav, Cortina011" must not make `@hermesav` address this agent. A pure
|
|
1425
|
+
* field read; the roster refresh that fills it runs once per ingest cycle,
|
|
1426
|
+
* TTL-gated, before the filter. Undefined until any roster has listed this
|
|
1427
|
+
* agent, in which case the filter still matches the credential agentName and
|
|
1428
|
+
* the identity id.
|
|
1429
|
+
*/
|
|
1430
|
+
private ownRoomDisplayName;
|
|
1431
|
+
/** Phase 3 F3: the loop guard's view of the sender — server roster first. */
|
|
1432
|
+
private loopSenderIsAgent;
|
|
1433
|
+
private noteAgentTurnSuppressed;
|
|
1434
|
+
/**
|
|
1435
|
+
* Phase 3 F1: whether `channelId`'s buffered work must be HELD because the
|
|
1436
|
+
* agent is paused there. Held work stays durably pending (never acked, no
|
|
1437
|
+
* delivery attempt spent) and is released by {@link recheckPause} once the
|
|
1438
|
+
* owner resumes the agent. Only a channel whose send was refused with
|
|
1439
|
+
* `agent_paused` is ever held. Holding arms a recheck at the end of the
|
|
1440
|
+
* PAUSE_RECHECK_MS window, so release does not depend on new traffic.
|
|
1441
|
+
*/
|
|
1442
|
+
private heldForPause;
|
|
1443
|
+
private schedulePauseRecheck;
|
|
1444
|
+
/**
|
|
1445
|
+
* Re-read this agent's own membership row. Still paused → keep holding and
|
|
1446
|
+
* re-arm while work is held. Resumed — or a failed/inconclusive probe, which
|
|
1447
|
+
* fails OPEN so a channel is never left deaf (the worst case is one model
|
|
1448
|
+
* call whose reply is refused again, re-arming the pause) — releases the
|
|
1449
|
+
* held work and pokes consumers to drain it now.
|
|
1450
|
+
*/
|
|
1451
|
+
private recheckPause;
|
|
1452
|
+
private clearPause;
|
|
1453
|
+
/** Read this agent's own `paused_at` from the enclave roster; undefined = unknown. */
|
|
1454
|
+
private probeOwnPause;
|
|
1455
|
+
recordRuntimeReply(inReplyTo: string, channelId: string, replyMessageId: string): RuntimeReplyReceipt;
|
|
1456
|
+
runtimeReplyReceipt(inReplyTo: string): RuntimeReplyReceipt | null;
|
|
1457
|
+
/**
|
|
1458
|
+
* Phase 3 F1: retire a runtime message whose reply failed TERMINALLY
|
|
1459
|
+
* (agent_paused / send_forbidden): acknowledged durably, never redelivered,
|
|
1460
|
+
* so the model is not run again for it. Pokes consumers so a socket holding
|
|
1461
|
+
* it in flight frees the channel for its next message.
|
|
1462
|
+
*/
|
|
1463
|
+
retireRuntimeMessage(messageId: string): void;
|
|
1464
|
+
isRuntimeMessageRetired(messageId: string): boolean;
|
|
1465
|
+
/** Complete the durable handoff only after the runtime explicitly acks. */
|
|
1466
|
+
ackRuntimeMessages(messageIds: string[]): void;
|
|
1467
|
+
private restoreRuntimeInbox;
|
|
1468
|
+
/**
|
|
1469
|
+
* Seal a runtime frame before the backend queue id may be acknowledged.
|
|
1470
|
+
* Returning false is backpressure: the caller leaves the backend row unacked
|
|
1471
|
+
* so it remains the durable queue rather than shedding accepted work.
|
|
1472
|
+
*/
|
|
1473
|
+
private stageRuntimeMessage;
|
|
1214
1474
|
/**
|
|
1215
1475
|
* Put messages back at the front (SSE/disconnect write-failure recovery).
|
|
1216
1476
|
*
|
|
@@ -1256,6 +1516,12 @@ export declare class BridgeAgent {
|
|
|
1256
1516
|
dropped: number;
|
|
1257
1517
|
/** Cumulative ROOM messages dropped by the @mention filter (correct filtering ≠ idle). */
|
|
1258
1518
|
filtered_room: number;
|
|
1519
|
+
/** Phase 3 F1: inbound messages retired at the runtime delivery-attempt cap. */
|
|
1520
|
+
dead_lettered: number;
|
|
1521
|
+
/** Phase 3 F1: channels whose inbound is currently held because the agent is paused. */
|
|
1522
|
+
paused_channels: number;
|
|
1523
|
+
/** Phase 3 F3: agent-authored messages not answered (no explicit mention / hop limit). */
|
|
1524
|
+
agent_loop_suppressed: number;
|
|
1259
1525
|
/** Cumulative sends shed by the per-channel rate-limit floor (correct backpressure ≠ idle). */
|
|
1260
1526
|
rate_limited: number;
|
|
1261
1527
|
/** Cumulative inbound rows acked-and-dropped because they were provably past-epoch undecryptable (forward-secrecy secret gone). */
|
|
@@ -1374,6 +1640,12 @@ export declare class BridgeAgent {
|
|
|
1374
1640
|
errors: ReturnType<MlsGroupKeyProvider["errorStats"]>;
|
|
1375
1641
|
dropped: number;
|
|
1376
1642
|
filteredRoomMessages: number;
|
|
1643
|
+
/** Phase 3 F1: inbound messages retired at the runtime delivery cap. */
|
|
1644
|
+
deadLetteredMessages: number;
|
|
1645
|
+
/** Phase 3 F1: channels whose inbound is currently held (agent paused). */
|
|
1646
|
+
pausedChannels: number;
|
|
1647
|
+
/** Phase 3 F3: agent-authored messages not answered by the loop guard. */
|
|
1648
|
+
agentLoopSuppressed: number;
|
|
1377
1649
|
rateLimitedSends: number;
|
|
1378
1650
|
permanentlySkipped: number;
|
|
1379
1651
|
/** SDK-6: cumulative rows ack-drained via QUARANTINE, distinct from the
|
package/dist/coordination.d.ts
CHANGED
|
@@ -1,24 +1,109 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Extract @mention names from plaintext. Returns lowercased names.
|
|
3
|
+
*
|
|
4
|
+
* A token starts with a word character and may contain `.` and `-`, so an agent
|
|
5
|
+
* handle (`@sc-agent-01`) and a full identity UUID mention stay whole (the gen2
|
|
6
|
+
* `@(\w[\w]*)` cut both at the first hyphen, so neither could ever address its
|
|
7
|
+
* agent). Trailing `.`/`-` is sentence punctuation and is trimmed
|
|
8
|
+
* (`thanks @Stephen.` → "stephen").
|
|
9
|
+
*/
|
|
2
10
|
export declare function parseMentions(text: string): string[];
|
|
3
11
|
/**
|
|
4
12
|
* Lower-cased set of names that count as "this agent" in a room: framework
|
|
5
13
|
* agentName + its first word, accountId + its first word, and (when known) the
|
|
6
14
|
* AgentVault room display_name + its first word. First word = split on
|
|
7
|
-
* whitespace or "("
|
|
15
|
+
* whitespace or "("; it also counts with trailing punctuation removed
|
|
16
|
+
* ("Stephen, PhD" → "stephen,", "stephen"), matching {@link parseMentions}.
|
|
17
|
+
*
|
|
18
|
+
* `roomDisplayName` is THIS AGENT's own display_name as a member of the room
|
|
19
|
+
* (its roster row), never the channel's name: a channel called
|
|
20
|
+
* "Hermesav, Cortina011" must not make `@hermesav` address this agent.
|
|
8
21
|
*/
|
|
9
22
|
export declare function ownRoomNames(agentName: string, accountId: string, roomDisplayName?: string): Set<string>;
|
|
23
|
+
/**
|
|
24
|
+
* Phase 3 F3 — default per-room agent-to-agent hop limit: at most this many
|
|
25
|
+
* CONSECUTIVE agent-authored messages (since the last human message) in one
|
|
26
|
+
* channel. Two agents that keep @mentioning each other stop after this many
|
|
27
|
+
* turns instead of spending the owners' BYOK budget forever.
|
|
28
|
+
*/
|
|
29
|
+
export declare const DEFAULT_MAX_AGENT_HOPS = 3;
|
|
30
|
+
/**
|
|
31
|
+
* Whether an agent-authored message may be answered. `agentHops` counts the
|
|
32
|
+
* consecutive agent-authored messages in the channel INCLUDING this one; our
|
|
33
|
+
* reply would be one more agent turn, so it is allowed only while
|
|
34
|
+
* `agentHops < maxAgentHops`.
|
|
35
|
+
*/
|
|
36
|
+
export declare function agentHopAllowed(agentHops: number, maxAgentHops?: number): boolean;
|
|
10
37
|
/**
|
|
11
38
|
* Whether this agent should process a room message based on its @mentions.
|
|
12
39
|
*
|
|
13
|
-
*
|
|
14
|
-
* NOT gate on senderIsAgent — every message is processed unless it @mentions a
|
|
15
|
-
* *different* agent specifically:
|
|
40
|
+
* Human (or unknown) sender — the gen2 floor, unchanged:
|
|
16
41
|
* - no mentions → true (broadcast)
|
|
17
42
|
* - @all / @everyone → true
|
|
18
43
|
* - this agent is mentioned → true
|
|
19
44
|
* - else (a different agent is mentioned) → false
|
|
45
|
+
*
|
|
46
|
+
* Agent sender (Phase 3 F3 loop guard): processed ONLY when that agent
|
|
47
|
+
* @mentions THIS agent by name (a broadcast, @all or @everyone from an agent
|
|
48
|
+
* is not an address) AND the per-room hop limit is not exhausted
|
|
49
|
+
* (`agentHops`, see {@link agentHopAllowed}). An unknown sender type keeps the
|
|
50
|
+
* fail-open human semantics so an unclassifiable message is never dropped.
|
|
20
51
|
*/
|
|
21
52
|
export declare function shouldProcessRoomMessage(plaintext: string, agentName: string, accountId: string, opts?: {
|
|
22
53
|
senderIsAgent?: boolean;
|
|
23
54
|
roomDisplayName?: string;
|
|
55
|
+
/** Consecutive agent-authored messages in this room, including this one. */
|
|
56
|
+
agentHops?: number;
|
|
57
|
+
maxAgentHops?: number;
|
|
58
|
+
}): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Final-wave I2: a channel's agent-hop count resets once this long has passed
|
|
61
|
+
* since the last agent-authored message in it. Without it, two agents in a DM
|
|
62
|
+
* (where no human ever speaks) went permanently deaf to each other after
|
|
63
|
+
* DEFAULT_MAX_AGENT_HOPS turns. A live loop is seconds apart, so the cap still
|
|
64
|
+
* stops it; a conversation resumed ten quiet minutes later starts a new run.
|
|
65
|
+
*/
|
|
66
|
+
export declare const AGENT_HOP_QUIET_MS: number;
|
|
67
|
+
/**
|
|
68
|
+
* Per-channel count of consecutive agent-authored messages since the last
|
|
69
|
+
* human message (or the last quiet window, {@link AGENT_HOP_QUIET_MS}). Fed
|
|
70
|
+
* with EVERY inbound text message of a channel (whether or not this agent
|
|
71
|
+
* processes it) plus this agent's own SUBSTANTIVE sends, so two agents in one
|
|
72
|
+
* room keep the same count from their own vantage points.
|
|
73
|
+
*
|
|
74
|
+
* In memory and bounded (oldest channel evicted first): a restart resets the
|
|
75
|
+
* counts, which can at worst allow one more bounded run of hops.
|
|
76
|
+
*/
|
|
77
|
+
export declare class AgentHopTracker {
|
|
78
|
+
private readonly maxChannels;
|
|
79
|
+
private readonly hops;
|
|
80
|
+
private readonly now;
|
|
81
|
+
private readonly quietMs;
|
|
82
|
+
constructor(maxChannels?: number, opts?: {
|
|
83
|
+
now?: () => number;
|
|
84
|
+
quietMs?: number;
|
|
85
|
+
});
|
|
86
|
+
/** Record an inbound message; returns the consecutive agent hops including it. */
|
|
87
|
+
noteInbound(channelId: string, senderIsAgent: boolean | undefined): number;
|
|
88
|
+
/**
|
|
89
|
+
* Record one of this agent's own SUBSTANTIVE sends (an agent turn in the
|
|
90
|
+
* channel). Callers do not report status frames, daily-limit or dead-letter
|
|
91
|
+
* notices, or continuation parts of one multi-part reply.
|
|
92
|
+
*/
|
|
93
|
+
noteOwnSend(channelId: string): void;
|
|
94
|
+
size(): number;
|
|
95
|
+
private bump;
|
|
96
|
+
private set;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Final-wave I2: whether a runtime `send` is a SUBSTANTIVE agent turn for the
|
|
100
|
+
* hop count. Not one: a notice (`notice: true` — e.g. the daily-limit reply)
|
|
101
|
+
* or a continuation part of a multi-part reply (its in_reply_to already has a
|
|
102
|
+
* durable reply receipt, i.e. an earlier part was published).
|
|
103
|
+
*/
|
|
104
|
+
export declare function isAgentTurn(receipts: {
|
|
105
|
+
runtimeReplyReceipt(inReplyTo: string): unknown;
|
|
106
|
+
}, frame: {
|
|
107
|
+
notice?: unknown;
|
|
108
|
+
in_reply_to?: unknown;
|
|
24
109
|
}): boolean;
|
|
@@ -1,25 +1,19 @@
|
|
|
1
|
+
import { type ServiceLog } from "./service-log.js";
|
|
1
2
|
export interface DecryptErrorInfo {
|
|
2
|
-
/** MLS epoch the failing row was at,
|
|
3
|
+
/** MLS epoch the failing row was at, when it is a finite number. */
|
|
3
4
|
epoch?: number | string;
|
|
4
|
-
/**
|
|
5
|
-
|
|
6
|
-
/** The decrypt error's message (already stringified by the caller). */
|
|
7
|
-
message: string;
|
|
5
|
+
/** Original thrown value; reduced to a bounded class by the service logger. */
|
|
6
|
+
error: unknown;
|
|
8
7
|
}
|
|
9
8
|
export interface DecryptErrorLogLimiterOpts {
|
|
10
9
|
/** Suppression window in ms. Default 60_000 (60s). */
|
|
11
10
|
windowMs?: number;
|
|
12
11
|
/** Clock seam for tests. Defaults to Date.now. */
|
|
13
12
|
now?: () => number;
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
log?: (line: string) => void;
|
|
13
|
+
/** Structured service-log seam for tests. */
|
|
14
|
+
log?: ServiceLog;
|
|
17
15
|
}
|
|
18
|
-
/** Default suppression window: 60 seconds. */
|
|
19
16
|
export declare const DEFAULT_DECRYPT_ERROR_LOG_WINDOW_MS = 60000;
|
|
20
|
-
/** Hard cap on distinct error reasons surfaced per channel per window. A wedged
|
|
21
|
-
* channel has a small handful of genuine reasons; beyond this the extra volume
|
|
22
|
-
* is noise (or an adversarial/buggy server cycling messages) and is suppressed. */
|
|
23
17
|
export declare const MAX_DISTINCT_PER_WINDOW = 4;
|
|
24
18
|
export declare class DecryptErrorLogLimiter {
|
|
25
19
|
private readonly windowMs;
|
|
@@ -27,25 +21,7 @@ export declare class DecryptErrorLogLimiter {
|
|
|
27
21
|
private readonly log;
|
|
28
22
|
private readonly windows;
|
|
29
23
|
constructor(opts?: DecryptErrorLogLimiterOpts);
|
|
30
|
-
/**
|
|
31
|
-
* Record one decrypt error for `channelId`. Logs immediately if this is
|
|
32
|
-
* the channel's first error since its window opened (or its prior window
|
|
33
|
-
* had already elapsed, in which case a summary for that just-closed
|
|
34
|
-
* window is flushed first); otherwise the error is counted silently
|
|
35
|
-
* toward the window's eventual summary line.
|
|
36
|
-
*/
|
|
37
24
|
record(channelId: string, info: DecryptErrorInfo): void;
|
|
38
|
-
/**
|
|
39
|
-
* Clear a channel's window (e.g. once the caller learns the channel
|
|
40
|
-
* cleared/healed) so its NEXT error logs immediately as a fresh "first"
|
|
41
|
-
* rather than being folded into a stale window's summary. There is
|
|
42
|
-
* currently no onDecryptSuccess-style hook from the SDK to wire this to
|
|
43
|
-
* automatically (see bridge-agent.ts's onDecryptError wiring comment) —
|
|
44
|
-
* exposed here so a future success signal (or a caller with its own
|
|
45
|
-
* channel-cleared signal) can reset explicitly. In its absence, the
|
|
46
|
-
* window simply rolls over on interval elapse, which is an accepted
|
|
47
|
-
* simplification (see report).
|
|
48
|
-
*/
|
|
49
25
|
reset(channelId: string): void;
|
|
50
26
|
private logFirst;
|
|
51
27
|
private flushSummary;
|
package/dist/errors.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* WebSocket returns `{type:"error", error, message, detail?}`. A client gets the
|
|
7
7
|
* same `error` code regardless of which transport it used. (A2A §9.5/§10.6 pattern.)
|
|
8
8
|
*/
|
|
9
|
-
export type BridgeErrorCode = "unauthorized" | "forbidden_origin" | "not_found" | "invalid_request" | "invalid_json" | "body_too_large" | "channel_pending" | "rate_limited" | "unknown_action_run" | "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" | "unknown_action_run" | "vault_integrity_failed" | "internal";
|
|
10
10
|
export interface BridgeErrorSpec {
|
|
11
11
|
/** HTTP status this code maps to (the WS binding ignores it). */
|
|
12
12
|
http: number;
|
package/dist/http-server.d.ts
CHANGED
|
@@ -1,11 +1,30 @@
|
|
|
1
1
|
import { type Server } from "node:http";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import type { BridgeAgent, BridgeMessage } from "./bridge-agent.js";
|
|
4
|
+
/**
|
|
5
|
+
* #335 handling decision: the throwing-write swallow stays (a dying socket
|
|
6
|
+
* must cost only that connection's remainder — the caller requeues it and
|
|
7
|
+
* only acks what was written — never crash the flush loop), but it is no
|
|
8
|
+
* longer invisible: the optional `onWriteError` hook reports the thrown
|
|
9
|
+
* value so the caller can service-log its sanitized class. The hook is
|
|
10
|
+
* telemetry-only by contract: a throwing hook is contained here, so it can
|
|
11
|
+
* never change the returned remainder or turn a skipped write into a crash.
|
|
12
|
+
* (Relay precedent: connect-room-relay src/http.ts `writeSse`, PR #654.)
|
|
13
|
+
*/
|
|
4
14
|
export declare function writeSse(res: {
|
|
5
15
|
destroyed?: boolean;
|
|
6
16
|
writableEnded?: boolean;
|
|
7
17
|
write: (s: string) => boolean;
|
|
8
|
-
}, msgs: BridgeMessage[]): BridgeMessage[];
|
|
18
|
+
}, msgs: BridgeMessage[], onWriteError?: (error: unknown) => void): BridgeMessage[];
|
|
19
|
+
/**
|
|
20
|
+
* Once-per-connection write-failure reporter (#335). A throwing `res.write`
|
|
21
|
+
* means the socket is dying — teardown follows via the request's `'close'` —
|
|
22
|
+
* so one WARN per connection is the whole story; repeats from a re-entered
|
|
23
|
+
* flush on the same corpse stay silent. Logs the sanitized `error_class`
|
|
24
|
+
* only (ServiceLogFields is number/boolean-typed by design — message text,
|
|
25
|
+
* channel ids, and exception prose cannot reach the log line).
|
|
26
|
+
*/
|
|
27
|
+
export declare function sseWriteFailureReporter(): (error: unknown) => void;
|
|
9
28
|
export interface HttpServerOptions {
|
|
10
29
|
token: string;
|
|
11
30
|
allowOrigins: string[];
|