@cotal-ai/connector-core 0.29.2 → 0.30.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.d.ts +47 -11
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +213 -31
- package/dist/agent.js.map +1 -1
- package/dist/docs-bundle.generated.js +11 -11
- package/dist/docs-bundle.generated.js.map +1 -1
- package/dist/tool-specs.d.ts.map +1 -1
- package/dist/tool-specs.js +29 -21
- package/dist/tool-specs.js.map +1 -1
- package/package.json +3 -2
package/dist/agent.d.ts
CHANGED
|
@@ -11,6 +11,14 @@ export declare const SPAWN_TIMEOUT_MS = 40000;
|
|
|
11
11
|
/** A message that has arrived for us, normalized for the agent to read. */
|
|
12
12
|
export interface InboxItem {
|
|
13
13
|
id: string;
|
|
14
|
+
/** Opaque per-delivery RECEIVE key (#624): the address a host uses to drain/ack THIS buffered
|
|
15
|
+
* delivery. It is the wire `id` for every message that carries one; a message whose `id` is the
|
|
16
|
+
* empty string gets a minted key, because an empty id is never a dedup key and never a
|
|
17
|
+
* selectable one. It is NOT wire identity and NOT dedup authority: nothing coalesces on it, so
|
|
18
|
+
* a redelivered copy of an id-less message mints its own key and surfaces again (at-least-once,
|
|
19
|
+
* the disclosed cost). It exists so the exact-id drains and in-flight protection can select the
|
|
20
|
+
* DELIVERY rather than an id value that two distinct messages share. */
|
|
21
|
+
recvKey: string;
|
|
14
22
|
ts: number;
|
|
15
23
|
fromId: string;
|
|
16
24
|
fromName: string;
|
|
@@ -40,7 +48,7 @@ export declare function afterRecallMark(a: RecallMark, b: RecallMark): boolean;
|
|
|
40
48
|
export type InboxScope = "all" | "automatic" | "pull-only";
|
|
41
49
|
export interface ExactDrainResult {
|
|
42
50
|
items: InboxItem[];
|
|
43
|
-
|
|
51
|
+
missingKeys: string[];
|
|
44
52
|
}
|
|
45
53
|
/**
|
|
46
54
|
* A thin, mesh-native agent: a {@link CotalEndpoint} plus a buffered inbox and
|
|
@@ -75,6 +83,9 @@ export declare class MeshAgent extends EventEmitter {
|
|
|
75
83
|
* permanently degrades unknown ambient to pull-only for this session rather than risk a late
|
|
76
84
|
* live/durable copy changing from quiet to automatic. */
|
|
77
85
|
private evictedClassifications;
|
|
86
|
+
/** How many times overflow has evicted each directed id without it ever being handled. Bounds the
|
|
87
|
+
* un-ack reprieve so a redelivery cycle cannot run forever — see the eviction path in `buffer`. */
|
|
88
|
+
private overflowEvictions;
|
|
78
89
|
/** Surfaced to the host but not yet committed or abandoned, counted per holding frame because
|
|
79
90
|
* frames overlap. See {@link holdInFlight}. */
|
|
80
91
|
private inFlightIds;
|
|
@@ -86,6 +97,10 @@ export declare class MeshAgent extends EventEmitter {
|
|
|
86
97
|
private protectedDropIds;
|
|
87
98
|
private dropUnsafe;
|
|
88
99
|
private _connected;
|
|
100
|
+
/** Latest connection failure, retained until the endpoint binds so a bounded readiness gate can
|
|
101
|
+
* explain why an otherwise healthy host never joined the mesh. */
|
|
102
|
+
private lastConnectionError?;
|
|
103
|
+
private endpointErrorLog;
|
|
89
104
|
private _status;
|
|
90
105
|
private _attention;
|
|
91
106
|
private _recallCursor;
|
|
@@ -103,19 +118,30 @@ export declare class MeshAgent extends EventEmitter {
|
|
|
103
118
|
* published after it ("since you entered focus"). Undefined unless in focus. */
|
|
104
119
|
private focusSince?;
|
|
105
120
|
private enteringFocus;
|
|
106
|
-
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
121
|
+
/** The receive-key namespace secret (#624): a per-session random value minted at construction,
|
|
122
|
+
* never written to any wire or log. Minted receive keys are `${secret}.${seq}`, so they are
|
|
123
|
+
* DISJOINT from wire ids by construction: an attacker-chosen wire id cannot equal one, so one
|
|
124
|
+
* verdict can never select two entries through a forged collision. Recognition is FUNCTIONAL
|
|
125
|
+
* (the key starts with the secret), so it cannot saturate the way a bounded set would: every
|
|
126
|
+
* minted key stays recognizable for the session's life, with nothing to expire or overflow. */
|
|
127
|
+
private readonly recvKeySecret;
|
|
128
|
+
private recvKeySeq;
|
|
109
129
|
private focusExcludedIds;
|
|
110
130
|
private focusRecallUnsafeChannels;
|
|
111
131
|
private stopping;
|
|
112
132
|
constructor(config: AgentConfig);
|
|
113
133
|
get id(): string;
|
|
114
134
|
get connected(): boolean;
|
|
135
|
+
/** The latest safe diagnostic for a connection that has not become live yet. */
|
|
136
|
+
get connectionIssue(): string | undefined;
|
|
137
|
+
/** Wait for the endpoint's real post-bind connection signal. `start()` deliberately stays
|
|
138
|
+
* background for connectors whose MCP surface must boot while the broker is absent; a host that
|
|
139
|
+
* advertises mesh readiness uses this bounded gate before making that claim. */
|
|
140
|
+
waitUntilConnected(timeoutMs?: number): Promise<void>;
|
|
115
141
|
/** Correlates outgoing messages to the host agent's current context/window. */
|
|
116
142
|
setContextId(contextId: string | undefined): void;
|
|
117
|
-
/** Begin connecting
|
|
118
|
-
start(retryMs?: number): void
|
|
143
|
+
/** Begin connecting with background retry. Resolves after the first completed mesh join. */
|
|
144
|
+
start(retryMs?: number): Promise<void>;
|
|
119
145
|
private connectLoop;
|
|
120
146
|
stop(): Promise<void>;
|
|
121
147
|
/** Manual reconnect: tear down the mesh connection and rebuild it in-process, WITHOUT
|
|
@@ -166,9 +192,14 @@ export declare class MeshAgent extends EventEmitter {
|
|
|
166
192
|
releaseInFlight(ids: readonly string[]): void;
|
|
167
193
|
/** Return scoped pending messages and ack them — call only when they're actually surfaced. */
|
|
168
194
|
drainInbox(limit?: number, scope?: InboxScope): InboxItem[];
|
|
169
|
-
/** Ack exact surfaced
|
|
170
|
-
*
|
|
171
|
-
|
|
195
|
+
/** Ack exact surfaced deliveries without assuming they still form the physical inbox prefix.
|
|
196
|
+
* Takes RECEIVE keys ({@link InboxItem.recvKey}): the wire id for real messages, a minted key
|
|
197
|
+
* for id-less ones, and selects by them, so a host that surfaced one empty-id item drains THAT
|
|
198
|
+
* delivery, never its neighbors: the sweep the raw id produced (every pending empty-id item
|
|
199
|
+
* acked and marked handled in one call) is closed by construction, not by filtering. Every
|
|
200
|
+
* requested key whose item is present is acked and (for a real id) marked handled, including an
|
|
201
|
+
* item overflow-evicted during the turn. */
|
|
202
|
+
drainInboxDeliveries(keys: readonly string[]): ExactDrainResult;
|
|
172
203
|
private commitPending;
|
|
173
204
|
private inScope;
|
|
174
205
|
/** Record an id as surfaced/handled, for {@link ingest}'s commit-aware cross-path dedup. Bounded via
|
|
@@ -256,8 +287,9 @@ export declare class MeshAgent extends EventEmitter {
|
|
|
256
287
|
* already cleared here; the mark that walks it was not.
|
|
257
288
|
*/
|
|
258
289
|
private resetRecallWalk;
|
|
259
|
-
/** Buffered receive-time lane for one
|
|
260
|
-
|
|
290
|
+
/** Buffered receive-time lane for one delivery, addressed by its receive key. Undefined means it
|
|
291
|
+
* is no longer pending. */
|
|
292
|
+
inboxScope(key: string): Exclude<InboxScope, "all"> | undefined;
|
|
261
293
|
/** Count of buffered messages that count as *directed* for a wake decision: real dm/anycast
|
|
262
294
|
* (authenticated kind) or a channel @-mention. The Stop→idle flush uses this in `dnd`/`focus`
|
|
263
295
|
* so held *ambient* alone never wakes a turn (which would empty-wake busy-loop). In `focus`
|
|
@@ -464,6 +496,10 @@ export declare class MeshAgent extends EventEmitter {
|
|
|
464
496
|
}>;
|
|
465
497
|
private who;
|
|
466
498
|
private assertConnected;
|
|
499
|
+
/** Keep an ordered-consumer reset storm from painting hundreds of status lines through an
|
|
500
|
+
* attached Codex TUI. Consumer names are generated per reset, so normalize them before
|
|
501
|
+
* deduplicating; otherwise every `_71`, `_72`, ... would look like a new fault. */
|
|
502
|
+
private handleEndpointError;
|
|
467
503
|
private log;
|
|
468
504
|
}
|
|
469
505
|
//# sourceMappingURL=agent.d.ts.map
|
package/dist/agent.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EAOL,aAAa,EAOb,KAAK,YAAY,EAIjB,KAAK,QAAQ,EACb,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,YAAY,EAClB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAI/C,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,CAAC;AAE3C;;;;wBAIwB;AACxB,eAAO,MAAM,gBAAgB,QAAS,CAAC;AAgCvC,2EAA2E;AAC3E,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,SAAS,GAAG,IAAI,GAAG,SAAS,CAAC;IACnC,mCAAmC;IACnC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wDAAwD;IACxD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,kGAAkG;IAClG,UAAU,EAAE,OAAO,CAAC;IACpB,+FAA+F;IAC/F,UAAU,EAAE,OAAO,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAWD,0GAA0G;AAC1G,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,0GAA0G;AAC1G,wBAAgB,eAAe,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,UAAU,GAAG,OAAO,
|
|
1
|
+
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EAOL,aAAa,EAOb,KAAK,YAAY,EAIjB,KAAK,QAAQ,EACb,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,YAAY,EAClB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAI/C,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,CAAC;AAE3C;;;;wBAIwB;AACxB,eAAO,MAAM,gBAAgB,QAAS,CAAC;AAgCvC,2EAA2E;AAC3E,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX;;;;;;6EAMyE;IACzE,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,SAAS,GAAG,IAAI,GAAG,SAAS,CAAC;IACnC,mCAAmC;IACnC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wDAAwD;IACxD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,kGAAkG;IAClG,UAAU,EAAE,OAAO,CAAC;IACpB,+FAA+F;IAC/F,UAAU,EAAE,OAAO,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAWD,0GAA0G;AAC1G,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,0GAA0G;AAC1G,wBAAgB,eAAe,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,UAAU,GAAG,OAAO,CASrE;AAmBD,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,WAAW,GAAG,WAAW,CAAC;AAE3D,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAiBD;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,SAAU,SAAQ,YAAY;IACzC,QAAQ,CAAC,EAAE,EAAE,aAAa,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAE7B,OAAO,CAAC,KAAK,CAAiB;IAC9B;;;;;;oGAMgG;IAChG,OAAO,CAAC,UAAU,CAAqB;IACvC,OAAO,CAAC,cAAc,CAAqB;IAC3C;;8DAE0D;IAC1D,OAAO,CAAC,sBAAsB,CAA6D;IAC3F;wGACoG;IACpG,OAAO,CAAC,iBAAiB,CAA6B;IACtD;oDACgD;IAChD,OAAO,CAAC,WAAW,CAA6B;IAChD,OAAO,CAAC,oBAAoB,CAAS;IACrC;;6EAEyE;IACzE,OAAO,CAAC,oBAAoB,CAAqB;IACjD,OAAO,CAAC,gBAAgB,CAAqB;IAC7C,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,UAAU,CAAS;IAC3B;sEACkE;IAClE,OAAO,CAAC,mBAAmB,CAAC,CAAS;IACrC,OAAO,CAAC,gBAAgB,CAAmE;IAC3F,OAAO,CAAC,OAAO,CAA0B;IACzC,OAAO,CAAC,UAAU,CAAyB;IAC3C,OAAO,CAAC,aAAa,CAAiC;IACtD;;kEAE8D;IAC9D,OAAO,CAAC,cAAc,CAAqB;IAC3C;;;8EAG0E;IAC1E,OAAO,CAAC,YAAY,CAAkC;IACtD,OAAO,CAAC,UAAU,CAAqB;IACvC;qFACiF;IACjF,OAAO,CAAC,UAAU,CAAC,CAAS;IAC5B,OAAO,CAAC,aAAa,CAAS;IAC9B;;;;;mGAK+F;IAC/F,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAkC;IAChE,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,gBAAgB,CAA6B;IACrD,OAAO,CAAC,yBAAyB,CAAqB;IACtD,OAAO,CAAC,QAAQ,CAAS;gBAEb,MAAM,EAAE,WAAW;IAsD/B,IAAI,EAAE,IAAI,MAAM,CAEf;IAED,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,gFAAgF;IAChF,IAAI,eAAe,IAAI,MAAM,GAAG,SAAS,CAExC;IAED;;oFAEgF;IAC1E,kBAAkB,CAAC,SAAS,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IA0B3D,+EAA+E;IAC/E,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAKjD,4FAA4F;IAC5F,KAAK,CAAC,OAAO,SAAO,GAAG,OAAO,CAAC,IAAI,CAAC;YAItB,WAAW;IAiBnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ3B;;;;iGAI6F;IACvF,SAAS,IAAI,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAkB5D,OAAO,CAAC,MAAM;IA6Fd,OAAO,CAAC,MAAM;IA8Ed,OAAO,CAAC,eAAe;IAYvB,OAAO,CAAC,gBAAgB;IAYxB,OAAO,CAAC,kBAAkB;IAa1B;;;;gDAI4C;IAC5C,OAAO,CAAC,WAAW;IAwBnB;sDACkD;IAClD,SAAS,CAAC,KAAK,GAAE,UAAkB,GAAG,SAAS,EAAE;IAIjD;;;;;;;;;;;;;kEAa8D;IAC9D,YAAY,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO;IAa7C;;;sGAGkG;IAClG,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAI/B;mGAC+F;IAC/F,eAAe,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI;IAS7C,8FAA8F;IAC9F,UAAU,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,GAAE,UAAkB,GAAG,SAAS,EAAE;IAYlE;;;;;;iDAM6C;IAC7C,oBAAoB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,gBAAgB;IA6B/D,OAAO,CAAC,aAAa;IASrB,OAAO,CAAC,OAAO;IAIf;;gGAE4F;IAC5F,OAAO,CAAC,WAAW;IAUnB,UAAU,CAAC,KAAK,GAAE,UAAkB,GAAG,MAAM;IAI7C;;;;;;;;;;;;;;;;;;;OAmBG;IACH,IAAI,YAAY,IAAI,UAAU,CAE7B;IAED;;;;;;;OAOG;IACH,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI;IAIpC;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,WAAW,CAAC,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO;IAI1C,oFAAoF;IACpF,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAIpC;;;;;;;;OAQG;IACH,eAAe,IAAI,MAAM;IAIzB,kFAAkF;IAClF,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAInC;;;;;;;;;OASG;IACH,OAAO,CAAC,eAAe;IAKvB;gCAC4B;IAC5B,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,KAAK,CAAC,GAAG,SAAS;IAK/D;;;0EAGsE;IACtE,oBAAoB,IAAI,MAAM;IAI9B;;;;;;0GAMsG;IACtG,WAAW,IAAI,MAAM;IASrB;;;wFAGoF;IACpF,WAAW,IAAI,IAAI;IAMnB;qFACiF;IACjF,IAAI,SAAS,IAAI,aAAa,CAE7B;IAED,4FAA4F;IAC5F,WAAW,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS;IAItD,4EAA4E;IAC5E,kBAAkB,IAAI,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC;IAIjD;;;;;;;;iDAQ6C;IACvC,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAUlF;;;;;;kCAM8B;IACxB,YAAY,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IA6BtD;;;;;;yCAMqC;IAC/B,aAAa,IAAI,OAAO,CAAC;QAAE,KAAK,EAAE,SAAS,EAAE,CAAC;QAAC,eAAe,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAuB3E,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,YAAY,CAAC;IAOtF;;;;;2DAKuD;IACvD,OAAO,CAAC,mBAAmB;IASrB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAKhE;;wEAEoE;IACpE,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS;IAI3C,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,GAAG,EAAE,YAAY,CAAC;QAAC,IAAI,EAAE,QAAQ,CAAA;KAAE,CAAC;IAUtF;;;;;;;;;;;kFAW8E;IACxE,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,YAAY,CAAC;IAQnL;;;;;;sEAMkE;YACpD,aAAa;IAiC3B;;;;;kGAK8F;YAChF,gBAAgB;IAS9B;;;;;;;4FAOwF;IAClF,OAAO,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,YAAY,CAAC;IAUlF;uGACmG;IAC7F,YAAY,CAAC,IAAI,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,YAAY,CAAC;IAM1E;;;;;;;;;;;;;;;;;;;+FAmB2F;IACrF,aAAa,CAAC,GAAG,EAAE;QACvB,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,GAAG,OAAO,CAAC,YAAY,GAAG;QAAE,aAAa,CAAC,EAAE,MAAM,CAAC;QAAC,eAAe,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAA;KAAE,CAAC;IAgE9F,4CAA4C;IAC5C,MAAM,IAAI,QAAQ,EAAE;IAIpB,8CAA8C;IAC9C,IAAI,MAAM,IAAI,cAAc,CAE3B;IAEK,SAAS,CAAC,MAAM,EAAE,cAAc,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOzE;;;6EAGyE;IACnE,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAO9D;;;;qFAIiF;IACjF,eAAe,IAAI,MAAM,GAAG,SAAS;IAUrC;oFACgF;IAChF,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,OAAO,CAAA;KAAE;IAS9F,2EAA2E;IAC3E,cAAc,IAAI,MAAM,EAAE;IAI1B;;iEAE6D;IACvD,YAAY,IAAI,OAAO,CAC3B;QACE,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,MAAM,EAAE,OAAO,CAAC;QAChB,MAAM,EAAE,OAAO,CAAC;QAChB,eAAe,EAAE,OAAO,CAAC;QACzB,cAAc,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAC;QACvC,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,WAAW,GAAG,QAAQ,CAAC;KAC9B,EAAE,CACJ;IA6DD;;;;;2BAKuB;IACjB,WAAW,CACf,OAAO,EAAE,MAAM,GACd,OAAO,CAAC;QAAE,MAAM,EAAE,OAAO,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAKtF,mEAAmE;IAC7D,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,OAAO,CAAA;KAAE,CAAC;IAO/D,OAAO,CAAC,GAAG;IAIX,OAAO,CAAC,eAAe;IAQvB;;uFAEmF;IACnF,OAAO,CAAC,mBAAmB;IAgB3B,OAAO,CAAC,GAAG;CAGZ"}
|
package/dist/agent.js
CHANGED
|
@@ -43,18 +43,43 @@ function execBearerCmd(argv) {
|
|
|
43
43
|
}
|
|
44
44
|
/** Total order on {@link RecallMark}: by time, then by id, so items sharing a millisecond still queue. */
|
|
45
45
|
export function afterRecallMark(a, b) {
|
|
46
|
+
// #624: recall marks and items are addressed by RECEIVE key (never the wire id), and a minted
|
|
47
|
+
// key is never the empty string, so the tie-break comparator never sees an empty id on either
|
|
48
|
+
// side: the only empty id in a RecallMark is the initial cursor {ts: 0, id: ""}, whose ts
|
|
49
|
+
// differs from every real item and so never reaches the id comparison. (The empty-id guard this
|
|
50
|
+
// comparator once carried was removed after a root chase showed it unreachable: with the wire id
|
|
51
|
+
// out of the marks, no real path can put two empty ids, or an empty id against a real one, into
|
|
52
|
+
// the comparison.)
|
|
46
53
|
return a.ts !== b.ts ? a.ts > b.ts : a.id > b.id;
|
|
47
54
|
}
|
|
48
55
|
const MAX_INBOX = 200;
|
|
56
|
+
/** How many times overflow may evict the SAME directed id before it is acked and given up on.
|
|
57
|
+
* The un-ack reprieve exists so a sacrificed message can be redelivered once there is room; if the
|
|
58
|
+
* inbox is still full on the Nth redelivery, room is not coming and the cycle is pure cost. */
|
|
59
|
+
const OVERFLOW_REDELIVERY_LIMIT = 5;
|
|
60
|
+
/** Bound on the eviction bookkeeping itself, so tracking churn cannot become a leak of its own. */
|
|
61
|
+
const OVERFLOW_EVICTION_CAP = 4 * MAX_INBOX;
|
|
49
62
|
/** How many future-stamped recall ids one session will remember having handed over. See
|
|
50
63
|
* {@link MeshAgent.recallAheadRoom}. */
|
|
51
64
|
const MAX_AHEAD = 256;
|
|
52
65
|
const CLASSIFICATION_CAP = 4096;
|
|
53
66
|
const FOCUS_EXCLUSION_CAP = 4096;
|
|
54
67
|
const PROTECTED_DISPOSITION_CAP = 4096;
|
|
68
|
+
/** Repeated async NATS status errors can arrive once per ordered-consumer retry. They describe one
|
|
69
|
+
* fault, not one hundred useful facts; keep the first visible and summarize at most twice a minute. */
|
|
70
|
+
const ENDPOINT_ERROR_LOG_WINDOW_MS = 30_000;
|
|
71
|
+
import { randomUUID } from "node:crypto";
|
|
55
72
|
function sleep(ms) {
|
|
56
73
|
return new Promise((r) => setTimeout(r, ms));
|
|
57
74
|
}
|
|
75
|
+
/** The dedup key a received message id contributes to ingest's coalescing, or undefined when the id
|
|
76
|
+
* is EMPTY: an empty id is never a dedup key (#624). Undefined means "no identity to coalesce on",
|
|
77
|
+
* so every id-keyed lookup in {@link MeshAgent.ingest} is skipped for such a message and it flows to
|
|
78
|
+
* the buffer on its own merits. Real ids are returned unchanged: their coalescing (live/durable
|
|
79
|
+
* copies of one message, and redelivery after handle) is untouched. */
|
|
80
|
+
function ingestDedupKey(id) {
|
|
81
|
+
return id === "" ? undefined : id;
|
|
82
|
+
}
|
|
58
83
|
/**
|
|
59
84
|
* A thin, mesh-native agent: a {@link CotalEndpoint} plus a buffered inbox and
|
|
60
85
|
* name-based peer resolution. This is the shared core behind the MCP server
|
|
@@ -88,6 +113,9 @@ export class MeshAgent extends EventEmitter {
|
|
|
88
113
|
* permanently degrades unknown ambient to pull-only for this session rather than risk a late
|
|
89
114
|
* live/durable copy changing from quiet to automatic. */
|
|
90
115
|
evictedClassifications = new Map();
|
|
116
|
+
/** How many times overflow has evicted each directed id without it ever being handled. Bounds the
|
|
117
|
+
* un-ack reprieve so a redelivery cycle cannot run forever — see the eviction path in `buffer`. */
|
|
118
|
+
overflowEvictions = new Map();
|
|
91
119
|
/** Surfaced to the host but not yet committed or abandoned, counted per holding frame because
|
|
92
120
|
* frames overlap. See {@link holdInFlight}. */
|
|
93
121
|
inFlightIds = new Map();
|
|
@@ -99,6 +127,10 @@ export class MeshAgent extends EventEmitter {
|
|
|
99
127
|
protectedDropIds = new Set();
|
|
100
128
|
dropUnsafe = false;
|
|
101
129
|
_connected = false;
|
|
130
|
+
/** Latest connection failure, retained until the endpoint binds so a bounded readiness gate can
|
|
131
|
+
* explain why an otherwise healthy host never joined the mesh. */
|
|
132
|
+
lastConnectionError;
|
|
133
|
+
endpointErrorLog = new Map();
|
|
102
134
|
_status = "idle";
|
|
103
135
|
_attention = "open"; // F3: fail-open default; reset to open on SessionStart
|
|
104
136
|
_recallCursor = { ts: 0, id: "" };
|
|
@@ -116,9 +148,14 @@ export class MeshAgent extends EventEmitter {
|
|
|
116
148
|
* published after it ("since you entered focus"). Undefined unless in focus. */
|
|
117
149
|
focusSince;
|
|
118
150
|
enteringFocus = false;
|
|
119
|
-
/**
|
|
120
|
-
*
|
|
121
|
-
*
|
|
151
|
+
/** The receive-key namespace secret (#624): a per-session random value minted at construction,
|
|
152
|
+
* never written to any wire or log. Minted receive keys are `${secret}.${seq}`, so they are
|
|
153
|
+
* DISJOINT from wire ids by construction: an attacker-chosen wire id cannot equal one, so one
|
|
154
|
+
* verdict can never select two entries through a forged collision. Recognition is FUNCTIONAL
|
|
155
|
+
* (the key starts with the secret), so it cannot saturate the way a bounded set would: every
|
|
156
|
+
* minted key stays recognizable for the session's life, with nothing to expire or overflow. */
|
|
157
|
+
recvKeySecret = randomUUID().replace(/-/g, "");
|
|
158
|
+
recvKeySeq = 0;
|
|
122
159
|
focusExcludedIds = new Map();
|
|
123
160
|
focusRecallUnsafeChannels = new Set();
|
|
124
161
|
stopping = false;
|
|
@@ -164,11 +201,18 @@ export class MeshAgent extends EventEmitter {
|
|
|
164
201
|
},
|
|
165
202
|
});
|
|
166
203
|
this.ep.on("message", (m, d, meta) => this.ingest(m, d, meta));
|
|
167
|
-
this.ep.on("error", (e) => this.
|
|
204
|
+
this.ep.on("error", (e) => this.handleEndpointError(e));
|
|
168
205
|
// The endpoint's (re)binds are the single source of truth for connectedness: this fires on
|
|
169
206
|
// initial start, manual reconnect, AND the background self-heal — so a recovery the endpoint
|
|
170
207
|
// did on its own can't leave us thinking we're offline (which would skip stop() → leak).
|
|
171
|
-
this.ep.on("connection", (e) => {
|
|
208
|
+
this.ep.on("connection", (e) => {
|
|
209
|
+
this._connected = e.connected;
|
|
210
|
+
if (e.connected) {
|
|
211
|
+
this.lastConnectionError = undefined;
|
|
212
|
+
this.endpointErrorLog.clear();
|
|
213
|
+
}
|
|
214
|
+
this.emit("connection", e);
|
|
215
|
+
});
|
|
172
216
|
}
|
|
173
217
|
get id() {
|
|
174
218
|
return this.ep.card.id;
|
|
@@ -176,14 +220,53 @@ export class MeshAgent extends EventEmitter {
|
|
|
176
220
|
get connected() {
|
|
177
221
|
return this._connected;
|
|
178
222
|
}
|
|
223
|
+
/** The latest safe diagnostic for a connection that has not become live yet. */
|
|
224
|
+
get connectionIssue() {
|
|
225
|
+
return this.lastConnectionError;
|
|
226
|
+
}
|
|
227
|
+
/** Wait for the endpoint's real post-bind connection signal. `start()` deliberately stays
|
|
228
|
+
* background for connectors whose MCP surface must boot while the broker is absent; a host that
|
|
229
|
+
* advertises mesh readiness uses this bounded gate before making that claim. */
|
|
230
|
+
async waitUntilConnected(timeoutMs = 15_000) {
|
|
231
|
+
if (this._connected)
|
|
232
|
+
return;
|
|
233
|
+
await new Promise((resolve, reject) => {
|
|
234
|
+
let timer;
|
|
235
|
+
let settled = false;
|
|
236
|
+
const finish = (error) => {
|
|
237
|
+
if (settled)
|
|
238
|
+
return;
|
|
239
|
+
settled = true;
|
|
240
|
+
if (timer)
|
|
241
|
+
clearTimeout(timer);
|
|
242
|
+
this.off("connection", onConnection);
|
|
243
|
+
if (error)
|
|
244
|
+
reject(error);
|
|
245
|
+
else
|
|
246
|
+
resolve();
|
|
247
|
+
};
|
|
248
|
+
const onConnection = (event) => {
|
|
249
|
+
if (event.connected)
|
|
250
|
+
finish();
|
|
251
|
+
};
|
|
252
|
+
this.on("connection", onConnection);
|
|
253
|
+
timer = setTimeout(() => {
|
|
254
|
+
const detail = this.connectionIssue ? ` Last error: ${this.connectionIssue}` : "";
|
|
255
|
+
finish(new Error(`mesh did not become ready at ${this.config.servers} within ${timeoutMs}ms.${detail}`));
|
|
256
|
+
}, Math.max(1, timeoutMs));
|
|
257
|
+
// Close the check→listen race if the endpoint connected between the first guard and handler bind.
|
|
258
|
+
if (this._connected)
|
|
259
|
+
finish();
|
|
260
|
+
});
|
|
261
|
+
}
|
|
179
262
|
/** Correlates outgoing messages to the host agent's current context/window. */
|
|
180
263
|
setContextId(contextId) {
|
|
181
264
|
const clean = contextId?.trim();
|
|
182
265
|
this._contextId = clean ? clean : undefined;
|
|
183
266
|
}
|
|
184
|
-
/** Begin connecting
|
|
267
|
+
/** Begin connecting with background retry. Resolves after the first completed mesh join. */
|
|
185
268
|
start(retryMs = 3000) {
|
|
186
|
-
|
|
269
|
+
return this.connectLoop(retryMs);
|
|
187
270
|
}
|
|
188
271
|
async connectLoop(retryMs) {
|
|
189
272
|
while (!this.stopping && !this._connected) {
|
|
@@ -193,7 +276,9 @@ export class MeshAgent extends EventEmitter {
|
|
|
193
276
|
this.log(`connected to ${this.config.servers} as ${this.who()} in space "${this.config.space}" on #${this.config.subscribe.join(", #")}`);
|
|
194
277
|
}
|
|
195
278
|
catch (e) {
|
|
196
|
-
|
|
279
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
280
|
+
this.lastConnectionError = error.message;
|
|
281
|
+
this.log(`mesh unreachable (${error.message}); retrying in ${retryMs}ms`);
|
|
197
282
|
await sleep(retryMs);
|
|
198
283
|
}
|
|
199
284
|
}
|
|
@@ -228,16 +313,25 @@ export class MeshAgent extends EventEmitter {
|
|
|
228
313
|
}
|
|
229
314
|
// ---- inbox ---------------------------------------------------------------
|
|
230
315
|
ingest(m, delivery, meta) {
|
|
316
|
+
// #624: an EMPTY id is never a dedup key. The id-keyed coalescing below exists to collapse the
|
|
317
|
+
// copies of ONE message across the live/durable paths, and identity is what makes that safe. A
|
|
318
|
+
// message that carries an empty id asserts no identity (SPEC §5 requires a unique id; the sender
|
|
319
|
+
// already violated it), so keying the coalescing on "" reads empty-equals-empty as a duplicate
|
|
320
|
+
// and silently drops every empty-id message after the first: measured live, two distinct
|
|
321
|
+
// empty-id messages arrived and only the first ever buffered. Treating an empty id as NO id
|
|
322
|
+
// costs the coalescing for those messages only (a redelivered copy can surface twice), which is
|
|
323
|
+
// the wire contract's at-least-once stance; collapsing distinct messages is not a stance at all.
|
|
324
|
+
const key = ingestDedupKey(m.id);
|
|
231
325
|
// Already SURFACED and drained? This is a post-handle cross-path duplicate (the transition window's
|
|
232
326
|
// second copy, arriving after the first was handled). Don't surface it again; if it's the durable
|
|
233
327
|
// copy, ack it so JetStream stops redelivering — safe because the logical message was already
|
|
234
328
|
// handled (handledIds is recorded at drain time, never at receive time).
|
|
235
|
-
if (this.handledIds.has(
|
|
329
|
+
if (key !== undefined && (this.handledIds.has(key) || this.handledIdsPrev.has(key))) {
|
|
236
330
|
if (delivery.durable)
|
|
237
331
|
delivery.ack();
|
|
238
332
|
return;
|
|
239
333
|
}
|
|
240
|
-
if (this.protectedPullOnlyIds.has(
|
|
334
|
+
if (key !== undefined && (this.protectedPullOnlyIds.has(key) || this.protectedDropIds.has(key))) {
|
|
241
335
|
if (delivery.durable)
|
|
242
336
|
delivery.ack();
|
|
243
337
|
return;
|
|
@@ -251,7 +345,7 @@ export class MeshAgent extends EventEmitter {
|
|
|
251
345
|
// dropped as-is. A durable duplicate also re-announces the still-pending item through the
|
|
252
346
|
// ordinary `incoming` policy path. That turns JetStream redelivery into a timer-free retry for
|
|
253
347
|
// a wake the host dropped, without bypassing quiet/attention or adapter in-flight guards.
|
|
254
|
-
const existing = this.inbox.find((p) => p.item.id ===
|
|
348
|
+
const existing = key === undefined ? undefined : this.inbox.find((p) => p.item.id === key);
|
|
255
349
|
if (existing) {
|
|
256
350
|
if (delivery.durable) {
|
|
257
351
|
existing.ack = delivery.ack;
|
|
@@ -289,7 +383,7 @@ export class MeshAgent extends EventEmitter {
|
|
|
289
383
|
delivery.ack();
|
|
290
384
|
return;
|
|
291
385
|
}
|
|
292
|
-
const remembered = this.evictedClassifications.get(item.id);
|
|
386
|
+
const remembered = item.id === "" ? undefined : this.evictedClassifications.get(item.id); // #624: an empty id never carries a remembered classification
|
|
293
387
|
if (remembered)
|
|
294
388
|
this.evictedClassifications.delete(item.id);
|
|
295
389
|
const snapshottedPullOnly = !item.mentionsMe && (remembered?.pullOnly ?? cm === "quiet");
|
|
@@ -349,13 +443,49 @@ export class MeshAgent extends EventEmitter {
|
|
|
349
443
|
// surfaced batch is made of, so without this an arrival can ack a message a host is still
|
|
350
444
|
// trying to hand to its runtime. Evicting bounds memory; acking is what makes it
|
|
351
445
|
// unrecoverable — gone from the buffer, never marked handled, no longer redeliverable. Left
|
|
352
|
-
// un-acked it redelivers; and if the delivery does succeed, {@link
|
|
446
|
+
// un-acked it redelivers; and if the delivery does succeed, {@link drainInboxDeliveries} marks the
|
|
353
447
|
// now-missing id handled so that redelivery is silently acked.
|
|
354
448
|
//
|
|
355
449
|
// A directed message is never acked on overflow: leaving it un-acked lets JetStream redeliver
|
|
356
450
|
// it once we have room, which turns unrecoverable loss into a delay. Channel ambient is still
|
|
357
451
|
// acked, because replaying it is what the history flood was (#775).
|
|
358
|
-
|
|
452
|
+
//
|
|
453
|
+
// ...but a delay only helps if it ENDS. An un-acked id is one the broker may hand straight
|
|
454
|
+
// back, into an inbox that is still full, to be evicted again - a cycle that spends broker
|
|
455
|
+
// and connector throughput while every seat involved looks healthy. So the reprieve is
|
|
456
|
+
// counted: after OVERFLOW_REDELIVERY_LIMIT evictions of the SAME message we stop hoping for
|
|
457
|
+
// room and ack it, recording the loss loudly. A message lost with a log line beats a mesh
|
|
458
|
+
// that quietly stops moving (#807).
|
|
459
|
+
//
|
|
460
|
+
// The counter is keyed on the RECEIVE key — the wire id for real messages, the minted key
|
|
461
|
+
// for id-less ones — so two id-less deliveries never share one tally (#624). It is cleared
|
|
462
|
+
// whenever the message is actually handled ({@link drainInboxDeliveries}), so one that
|
|
463
|
+
// eventually lands never accumulates toward the cap.
|
|
464
|
+
let giveUp = false;
|
|
465
|
+
if (sacrificingDirected) {
|
|
466
|
+
const seen = (this.overflowEvictions.get(evicted.item.recvKey) ?? 0) + 1;
|
|
467
|
+
if (seen >= OVERFLOW_REDELIVERY_LIMIT) {
|
|
468
|
+
giveUp = true;
|
|
469
|
+
this.overflowEvictions.delete(evicted.item.recvKey);
|
|
470
|
+
// Reported on stderr via the existing log path, NOT emit("error"): an EventEmitter with
|
|
471
|
+
// no "error" listener turns an emit into an unhandled exception that takes the process
|
|
472
|
+
// down, so announcing a dropped message would kill the seat that was trying to survive
|
|
473
|
+
// the flood. Nothing else in this class emits "error" either.
|
|
474
|
+
this.log(`overflow: dropping directed message ${evicted.item.recvKey} after ${seen} evictions - ` +
|
|
475
|
+
`the inbox has stayed full across every redelivery, so the reprieve is not helping`);
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
this.overflowEvictions.set(evicted.item.recvKey, seen);
|
|
479
|
+
if (this.overflowEvictions.size > OVERFLOW_EVICTION_CAP) {
|
|
480
|
+
// Bounded bookkeeping: the map must not become its own leak under a sustained flood.
|
|
481
|
+
// Dropping the oldest entry only forgives a message, never destroys one.
|
|
482
|
+
const oldest = this.overflowEvictions.keys().next();
|
|
483
|
+
if (!oldest.done)
|
|
484
|
+
this.overflowEvictions.delete(oldest.value);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (!this.inFlightIds.has(evicted.item.recvKey) && (!sacrificingDirected || giveUp))
|
|
359
489
|
evicted.ack();
|
|
360
490
|
}
|
|
361
491
|
}
|
|
@@ -364,6 +494,8 @@ export class MeshAgent extends EventEmitter {
|
|
|
364
494
|
rememberEvicted(p) {
|
|
365
495
|
if (p.item.kind !== "channel" || p.item.mentionsMe || !p.item.channel)
|
|
366
496
|
return;
|
|
497
|
+
if (p.item.id === "")
|
|
498
|
+
return; // #624: an empty id is never a key; one delivery's classification must not inherit to another
|
|
367
499
|
if (this.classificationUnsafe)
|
|
368
500
|
return;
|
|
369
501
|
if (!this.evictedClassifications.has(p.item.id) && this.evictedClassifications.size >= CLASSIFICATION_CAP) {
|
|
@@ -386,6 +518,8 @@ export class MeshAgent extends EventEmitter {
|
|
|
386
518
|
this.focusExcludedIds.set(item.id, item.channel);
|
|
387
519
|
}
|
|
388
520
|
protectDisposition(id, disposition) {
|
|
521
|
+
if (id === "")
|
|
522
|
+
return; // #624: an empty id is never a dedup key, so it is never recorded as one
|
|
389
523
|
const ids = disposition === "drop" ? this.protectedDropIds : this.protectedPullOnlyIds;
|
|
390
524
|
if ((disposition === "drop" ? this.dropUnsafe : this.classificationUnsafe) || ids.has(id))
|
|
391
525
|
return;
|
|
@@ -408,6 +542,10 @@ export class MeshAgent extends EventEmitter {
|
|
|
408
542
|
const text = partsToText(m.parts);
|
|
409
543
|
return {
|
|
410
544
|
id: m.id,
|
|
545
|
+
// The wire id when there is one; a minted opaque key when the id is the empty string (#624:
|
|
546
|
+
// an empty id is never a dedup key, so it is never an address either; but the delivery still
|
|
547
|
+
// needs to be individually drainable/ackable, or it can never clear and never commit).
|
|
548
|
+
recvKey: m.id !== "" ? m.id : `${this.recvKeySecret}.${++this.recvKeySeq}`,
|
|
411
549
|
ts: m.ts,
|
|
412
550
|
fromId: m.from.id,
|
|
413
551
|
fromName: m.from.name,
|
|
@@ -443,6 +581,11 @@ export class MeshAgent extends EventEmitter {
|
|
|
443
581
|
* valve free to ack them. Declining to surface costs a deferral: the messages stay buffered and go
|
|
444
582
|
* out on a later frame, once a verdict releases capacity. */
|
|
445
583
|
holdInFlight(ids) {
|
|
584
|
+
// #624: the keys here are RECEIVE keys. An id-less delivery's wire id is "", so raw-id keying
|
|
585
|
+
// merged the counts of DISTINCT empty-id deliveries, and the eviction valve's in-flight check
|
|
586
|
+
// then deferred acking an evicted id-less item while any other id-less batch was held:
|
|
587
|
+
// cross-delivery interference in the safe direction, but interference. Receive keys are unique
|
|
588
|
+
// per delivery, so each is protected exactly for the frames holding it.
|
|
446
589
|
let fresh = 0;
|
|
447
590
|
for (const id of ids)
|
|
448
591
|
if (!this.inFlightIds.has(id))
|
|
@@ -478,31 +621,48 @@ export class MeshAgent extends EventEmitter {
|
|
|
478
621
|
const eligible = this.inbox.filter((p) => this.inScope(p, scope));
|
|
479
622
|
const n = limit && limit > 0 ? Math.min(limit, eligible.length) : eligible.length;
|
|
480
623
|
const selected = eligible.slice(0, n);
|
|
481
|
-
|
|
482
|
-
|
|
624
|
+
// Remove by OBJECT IDENTITY, not by a set of wire ids (#624): the id-less entries share "", so
|
|
625
|
+
// an id set would remove every id-less neighbor beyond the limit and outside the scope while
|
|
626
|
+
// acking only the selected — silent loss by selection. Identity removes exactly what was taken.
|
|
627
|
+
const taken = new Set(selected);
|
|
628
|
+
this.inbox = this.inbox.filter((p) => !taken.has(p));
|
|
483
629
|
return this.commitPending(selected);
|
|
484
630
|
}
|
|
485
|
-
/** Ack exact surfaced
|
|
486
|
-
*
|
|
487
|
-
|
|
488
|
-
|
|
631
|
+
/** Ack exact surfaced deliveries without assuming they still form the physical inbox prefix.
|
|
632
|
+
* Takes RECEIVE keys ({@link InboxItem.recvKey}): the wire id for real messages, a minted key
|
|
633
|
+
* for id-less ones, and selects by them, so a host that surfaced one empty-id item drains THAT
|
|
634
|
+
* delivery, never its neighbors: the sweep the raw id produced (every pending empty-id item
|
|
635
|
+
* acked and marked handled in one call) is closed by construction, not by filtering. Every
|
|
636
|
+
* requested key whose item is present is acked and (for a real id) marked handled, including an
|
|
637
|
+
* item overflow-evicted during the turn. */
|
|
638
|
+
drainInboxDeliveries(keys) {
|
|
639
|
+
const requested = [...new Set(keys)];
|
|
489
640
|
const wanted = new Set(requested);
|
|
490
|
-
const selected = this.inbox.filter((p) => wanted.has(p.item.
|
|
491
|
-
const present = new Set(selected.map((p) => p.item.
|
|
492
|
-
const pullOnly = new Map(selected.map((p) => [p.item.
|
|
641
|
+
const selected = this.inbox.filter((p) => wanted.has(p.item.recvKey));
|
|
642
|
+
const present = new Set(selected.map((p) => p.item.recvKey));
|
|
643
|
+
const pullOnly = new Map(selected.map((p) => [p.item.recvKey, p.pullOnly]));
|
|
493
644
|
for (const id of requested) {
|
|
494
|
-
const remembered = this.evictedClassifications.get(id);
|
|
645
|
+
const remembered = id !== "" ? this.evictedClassifications.get(id) : undefined;
|
|
495
646
|
if (!pullOnly.has(id) && remembered)
|
|
496
647
|
pullOnly.set(id, remembered.pullOnly);
|
|
497
648
|
}
|
|
498
|
-
this.inbox = this.inbox.filter((p) => !present.has(p.item.
|
|
649
|
+
this.inbox = this.inbox.filter((p) => !present.has(p.item.recvKey));
|
|
499
650
|
const items = this.commitPending(selected);
|
|
500
651
|
for (const id of requested) {
|
|
501
|
-
|
|
652
|
+
// A MINTED key (an id-less delivery) is never handled-authority: its wire id is "", which
|
|
653
|
+
// markHandled already refuses, so skipping it here is the same at-least-once stance rather
|
|
654
|
+
// than a new one. Recording the minted key itself would pollute handledIds with a string a
|
|
655
|
+
// future REAL id could legitimately equal, arming the exact suppression this change removes.
|
|
656
|
+
if (!present.has(id) && !id.startsWith(`${this.recvKeySecret}.`)) {
|
|
502
657
|
this.markHandled(id, pullOnly.get(id) ?? false);
|
|
503
|
-
|
|
658
|
+
this.evictedClassifications.delete(id);
|
|
659
|
+
}
|
|
660
|
+
// A message that actually landed is not churning, whatever it survived on the way here: clear
|
|
661
|
+
// its overflow tally so one that is redelivered, evicted, then finally handled never carries
|
|
662
|
+
// history toward the give-up cap. Keyed on the receive key, like the tally itself.
|
|
663
|
+
this.overflowEvictions.delete(id);
|
|
504
664
|
}
|
|
505
|
-
return { items,
|
|
665
|
+
return { items, missingKeys: requested.filter((key) => !present.has(key)) };
|
|
506
666
|
}
|
|
507
667
|
commitPending(taken) {
|
|
508
668
|
for (const p of taken) {
|
|
@@ -519,6 +679,8 @@ export class MeshAgent extends EventEmitter {
|
|
|
519
679
|
* two rotating windows: when the live set fills, it becomes the previous window and a fresh one
|
|
520
680
|
* starts — so memory stays ~2× the cap while the lookup horizon never shrinks below it. */
|
|
521
681
|
markHandled(id, pullOnly = false) {
|
|
682
|
+
if (id === "")
|
|
683
|
+
return; // #624: an empty id is never a dedup key, so it is never recorded as one
|
|
522
684
|
if (pullOnly)
|
|
523
685
|
this.protectDisposition(id, "pull-only");
|
|
524
686
|
this.handledIds.add(id);
|
|
@@ -625,9 +787,10 @@ export class MeshAgent extends EventEmitter {
|
|
|
625
787
|
this._recallCursor = { ts: 0, id: "" };
|
|
626
788
|
this.aheadDelivered.clear();
|
|
627
789
|
}
|
|
628
|
-
/** Buffered receive-time lane for one
|
|
629
|
-
|
|
630
|
-
|
|
790
|
+
/** Buffered receive-time lane for one delivery, addressed by its receive key. Undefined means it
|
|
791
|
+
* is no longer pending. */
|
|
792
|
+
inboxScope(key) {
|
|
793
|
+
const pending = this.inbox.find((p) => p.item.recvKey === key);
|
|
631
794
|
return pending ? (pending.pullOnly ? "pull-only" : "automatic") : undefined;
|
|
632
795
|
}
|
|
633
796
|
/** Count of buffered messages that count as *directed* for a wake decision: real dm/anycast
|
|
@@ -1118,6 +1281,25 @@ export class MeshAgent extends EventEmitter {
|
|
|
1118
1281
|
throw new Error(`not connected to the mesh at ${this.config.servers} — is it running? (pnpm cotal up)`);
|
|
1119
1282
|
}
|
|
1120
1283
|
}
|
|
1284
|
+
/** Keep an ordered-consumer reset storm from painting hundreds of status lines through an
|
|
1285
|
+
* attached Codex TUI. Consumer names are generated per reset, so normalize them before
|
|
1286
|
+
* deduplicating; otherwise every `_71`, `_72`, ... would look like a new fault. */
|
|
1287
|
+
handleEndpointError(error) {
|
|
1288
|
+
const now = Date.now();
|
|
1289
|
+
this.lastConnectionError = error.message;
|
|
1290
|
+
const fingerprint = error.message.replace(/oc_[A-Za-z0-9]+_\d+/g, "oc_*");
|
|
1291
|
+
const prior = this.endpointErrorLog.get(fingerprint);
|
|
1292
|
+
if (prior && now - prior.lastLoggedAt < ENDPOINT_ERROR_LOG_WINDOW_MS) {
|
|
1293
|
+
prior.suppressed++;
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
const suffix = prior?.suppressed ? ` (${prior.suppressed} repeats suppressed)` : "";
|
|
1297
|
+
this.endpointErrorLog.set(fingerprint, { lastLoggedAt: now, suppressed: 0 });
|
|
1298
|
+
// Bound the map even for a server producing novel error text on every request.
|
|
1299
|
+
if (this.endpointErrorLog.size > 16)
|
|
1300
|
+
this.endpointErrorLog.delete(this.endpointErrorLog.keys().next().value);
|
|
1301
|
+
this.log(`endpoint error: ${error.message}${suffix}`);
|
|
1302
|
+
}
|
|
1121
1303
|
log(msg) {
|
|
1122
1304
|
process.stderr.write(`[cotal-connector] ${msg}\n`);
|
|
1123
1305
|
}
|