@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 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
- missingIds: string[];
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
- /** IDs received under quiet/muted while focused must never reappear through stream recall after a
107
- * mode toggle. If this bounded exclusion history fills, recall for the affected channel fails
108
- * closed and reports the channel as incomplete. */
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 (with background retry). Returns immediately. */
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 ids without assuming they still form the physical inbox prefix. Every
170
- * requested id is marked handled, including an item overflow-evicted during the turn. */
171
- drainInboxIds(ids: readonly string[]): ExactDrainResult;
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 id. Undefined means it is no longer pending. */
260
- inboxScope(id: string): Exclude<InboxScope, "all"> | undefined;
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
@@ -1 +1 @@
1
- {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"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"}
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
- /** IDs received under quiet/muted while focused must never reappear through stream recall after a
120
- * mode toggle. If this bounded exclusion history fills, recall for the affected channel fails
121
- * closed and reports the channel as incomplete. */
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.log(`endpoint error: ${e.message}`));
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) => { this._connected = e.connected; });
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 (with background retry). Returns immediately. */
267
+ /** Begin connecting with background retry. Resolves after the first completed mesh join. */
185
268
  start(retryMs = 3000) {
186
- void this.connectLoop(retryMs);
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
- this.log(`mesh unreachable (${e.message}); retrying in ${retryMs}ms`);
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(m.id) || this.handledIdsPrev.has(m.id)) {
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(m.id) || this.protectedDropIds.has(m.id)) {
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 === m.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 drainInboxIds} marks the
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
- if (!this.inFlightIds.has(evicted.item.id) && !sacrificingDirected)
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
- const ids = new Set(selected.map((p) => p.item.id));
482
- this.inbox = this.inbox.filter((p) => !ids.has(p.item.id));
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 ids without assuming they still form the physical inbox prefix. Every
486
- * requested id is marked handled, including an item overflow-evicted during the turn. */
487
- drainInboxIds(ids) {
488
- const requested = [...new Set(ids)];
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.id));
491
- const present = new Set(selected.map((p) => p.item.id));
492
- const pullOnly = new Map(selected.map((p) => [p.item.id, p.pullOnly]));
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.id));
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
- if (!present.has(id))
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
- this.evictedClassifications.delete(id);
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, missingIds: requested.filter((id) => !present.has(id)) };
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 id. Undefined means it is no longer pending. */
629
- inboxScope(id) {
630
- const pending = this.inbox.find((p) => p.item.id === id);
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
  }