@parall/agent-core 1.44.0 → 1.46.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.
Files changed (62) hide show
  1. package/dist/channel-capability.d.ts +2 -0
  2. package/dist/channel-capability.d.ts.map +1 -1
  3. package/dist/channel-capability.js +15 -0
  4. package/dist/dispatch-adapter.d.ts +9 -0
  5. package/dist/dispatch-adapter.d.ts.map +1 -1
  6. package/dist/event-format.d.ts.map +1 -1
  7. package/dist/event-format.js +27 -7
  8. package/dist/fork-session-finalizer.d.ts +65 -0
  9. package/dist/fork-session-finalizer.d.ts.map +1 -0
  10. package/dist/fork-session-finalizer.js +70 -0
  11. package/dist/gateway-base.d.ts +55 -0
  12. package/dist/gateway-base.d.ts.map +1 -1
  13. package/dist/gateway-base.js +640 -263
  14. package/dist/gateway-lane-flow.d.ts +75 -5
  15. package/dist/gateway-lane-flow.d.ts.map +1 -1
  16. package/dist/gateway-lane-flow.js +240 -18
  17. package/dist/http-keepalive.d.ts +4 -0
  18. package/dist/http-keepalive.d.ts.map +1 -0
  19. package/dist/http-keepalive.js +33 -0
  20. package/dist/index.d.ts +2 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +7 -1
  23. package/dist/lane-ledger.d.ts +8 -0
  24. package/dist/lane-ledger.d.ts.map +1 -1
  25. package/dist/lane-ledger.js +14 -0
  26. package/dist/session-lifecycle.d.ts +198 -0
  27. package/dist/session-lifecycle.d.ts.map +1 -0
  28. package/dist/session-lifecycle.js +446 -0
  29. package/dist/skills/parall-clips.d.ts +1 -1
  30. package/dist/skills/parall-clips.d.ts.map +1 -1
  31. package/dist/skills/parall-clips.js +3 -0
  32. package/dist/skills/parall-schedules.d.ts +1 -1
  33. package/dist/skills/parall-schedules.d.ts.map +1 -1
  34. package/dist/skills/parall-schedules.js +1 -1
  35. package/dist/skills/parall-tasks.d.ts +1 -1
  36. package/dist/skills/parall-tasks.d.ts.map +1 -1
  37. package/dist/skills/parall-tasks.js +21 -5
  38. package/dist/step-persister.d.ts +66 -0
  39. package/dist/step-persister.d.ts.map +1 -0
  40. package/dist/step-persister.js +116 -0
  41. package/dist/step-retry-queue.d.ts +91 -0
  42. package/dist/step-retry-queue.d.ts.map +1 -0
  43. package/dist/step-retry-queue.js +259 -0
  44. package/dist/types.d.ts +1 -1
  45. package/dist/types.d.ts.map +1 -1
  46. package/package.json +3 -2
  47. package/src/channel-capability.ts +16 -0
  48. package/src/dispatch-adapter.ts +10 -0
  49. package/src/event-format.ts +27 -7
  50. package/src/fork-session-finalizer.ts +122 -0
  51. package/src/gateway-base.ts +747 -331
  52. package/src/gateway-lane-flow.ts +275 -18
  53. package/src/http-keepalive.ts +36 -0
  54. package/src/index.ts +7 -1
  55. package/src/lane-ledger.ts +14 -0
  56. package/src/session-lifecycle.ts +552 -0
  57. package/src/skills/parall-clips.ts +3 -0
  58. package/src/skills/parall-schedules.ts +1 -1
  59. package/src/skills/parall-tasks.ts +21 -5
  60. package/src/step-persister.ts +161 -0
  61. package/src/step-retry-queue.ts +296 -0
  62. package/src/types.ts +2 -1
@@ -13,8 +13,15 @@ import type { ParallEvent } from './types.js';
13
13
  export interface LaneFlowHost {
14
14
  laneLedger?: LaneLedger;
15
15
  ledgerDisabled: boolean;
16
+ /**
17
+ * Sticky: the server answered a by-id complete with 400 (predates the
18
+ * form, v1.44). Typed resolution falls back to the legacy ack for the
19
+ * rest of the process; claims stay on the ledger (that face is older).
20
+ */
21
+ typedByIdCompleteUnsupported: boolean;
16
22
  shuttingDown: boolean;
17
23
  dispatchedMessages: Set<string>;
24
+ dispatchedTasks: Set<string>;
18
25
  /**
19
26
  * Per-WorkItem failure backoff for typed dispatch consumption. A consume
20
27
  * that ends without an ack re-arms the entry; the next attempt for the
@@ -60,14 +67,77 @@ export declare function dispatchLaneGroup(host: LaneFlowHost, opts: {
60
67
  captureText?: string[];
61
68
  hasMoreLocal: () => boolean;
62
69
  }): Promise<'dispatched' | 'foreign' | 'shutdown' | 'failed'>;
70
+ /**
71
+ * The WorkItem ids of a typed event group whose lifecycle the ledger owns
72
+ * (claimed into dsp lanes by consumeTypedDispatch), or null when the group
73
+ * must stay on the legacy received/ack surface — ledger unavailable, an id
74
+ * missing, or a member that rides a message lane. Non-null means: skip
75
+ * legacy received+ack, resolve via the by-id complete, and fold the
76
+ * turn-error signal into the outcome.
77
+ */
78
+ export declare function typedLedgerEventIds(host: LaneFlowHost, events: ParallEvent[]): string[] | null;
79
+ /** Outcome of a by-id complete attempt. */
80
+ export type TypedResolveOutcome = 'ok' | 'stale' | 'unsupported' | 'failed';
81
+ /**
82
+ * By-id complete (turn_outcome ok): terminal resolution of ONE WorkItem —
83
+ * exact where the source pair is ambiguous across task siblings. `lane`
84
+ * fences the call while the row is claim-owned (a dethroned caller gets
85
+ * 'stale' and must leave the row to its successor); omit it for the
86
+ * wrapper-less pending close (buffered drain, administrative drops).
87
+ */
88
+ export declare function resolveDispatchByID(host: LaneFlowHost, dispatchEventId: string, lane?: string): Promise<TypedResolveOutcome>;
89
+ /**
90
+ * Free a typed event's in-memory hot-path dedupe claim by its source pair —
91
+ * the ParallEvent-keyed mirror of the gateway's clearTypedDispatchDedupe
92
+ * (which keys on the wire DispatchNewData). Used when a buffered typed
93
+ * dispatch ran but its resolution failed: the member re-drives, and a stale
94
+ * local claim would make this pod reject the retry forever.
95
+ *
96
+ * PARITY: the switch below and clearTypedDispatchDedupe's must handle the
97
+ * same typed source families — when adding a new typed event type, extend
98
+ * BOTH (they key the same dedupe entries from different event shapes).
99
+ */
100
+ export declare function clearTypedDedupeForEvent(host: LaneFlowHost, event: ParallEvent): void;
101
+ /**
102
+ * Settle a drained typed group — the one wrapper-less dispatch path: these
103
+ * events buffered behind a busy main (fork-less adapter), their consume
104
+ * guards returned false and released the claims, so the drain site owns the
105
+ * terminal resolution. Rows are pending — the by-id close resolves them
106
+ * without a fence; a row a successor lane meanwhile claimed answers stale and
107
+ * is that owner's to finish. `turnErrored` (consumed at the call site before
108
+ * the drain can start another turn) leaves the rows for the renotify pacing
109
+ * instead. Either way a row left live gets its local dedupe claim freed, or
110
+ * this pod would reject the retry forever.
111
+ */
112
+ export declare function settleDrainedTypedGroup(host: LaneFlowHost, events: ParallEvent[], ids: string[], turnErrored: boolean): Promise<void>;
113
+ /** Consumption hooks for one typed dispatch. */
114
+ export interface TypedConsumeHooks {
115
+ /**
116
+ * Legacy administrative ack — invoked ONLY on the ledger-disabled fallback
117
+ * path (old server). On the ledger path the WorkItem resolves through the
118
+ * sources-form complete instead; this callback never fires there.
119
+ */
120
+ legacyAck: (dispatchEventId?: string) => boolean | void | Promise<boolean | void>;
121
+ /**
122
+ * Free the item's in-memory hot-path dedupe claim. Invoked when the handler
123
+ * ran but the resolution could not commit (sources-form complete failed) —
124
+ * the member is released for re-drive, and a stale local claim would make
125
+ * this pod reject its own retry forever.
126
+ */
127
+ clearDedupe?: () => void;
128
+ }
63
129
  /**
64
130
  * Consume one typed dispatch (task/comment/schedule/trigger/approval) under
65
131
  * its typed-lane occupancy guard: claim the dsp:<id> lane (skip when another
66
- * pod holds it or the WorkItem is already resolved), run the handler, ack on
67
- * success (the doc's option (b): notification delivered, tracked elsewhere),
68
- * then release the lane. Legacy run+ack flow when the ledger is unavailable.
132
+ * pod holds it or the WorkItem is already resolved), run the handler, then
133
+ * settle. A successful run resolves the WorkItem terminally via the
134
+ * sources-form complete (turn_outcome ok the server sweeps it, or lets a
135
+ * typed Effect committed during the turn stand, and drops the vacated lane);
136
+ * a failed / errored / unresolved run releases the member back to pending on
137
+ * the redrive budget via the lane complete. Legacy run+ack flow when the
138
+ * ledger is unavailable.
69
139
  *
70
- * Repeated failures back off: a consume that ends un-acked re-arms the
140
+ * Repeated failures back off: a consume that ends unresolved re-arms the
71
141
  * WorkItem's backoff entry, and the next attempt sleeps out the remaining
72
142
  * window before claiming. Without this, complete's release re-drives the
73
143
  * item instantly and a persistently-failing consume (e.g. buffered behind a
@@ -78,7 +148,7 @@ export declare function consumeTypedDispatch(host: LaneFlowHost, ref: {
78
148
  dispatchEventId?: string;
79
149
  sourceType?: string;
80
150
  sourceId?: string;
81
- }, run: (dispatchEventId?: string) => Promise<boolean>, ack: (dispatchEventId?: string) => boolean | void | Promise<boolean | void>): Promise<void>;
151
+ }, run: (dispatchEventId?: string) => Promise<boolean>, hooks: TypedConsumeHooks): Promise<void>;
82
152
  /**
83
153
  * Shared consumption of a message WorkItem referenced by id — the ONE
84
154
  * protocol for dispatch catch-up pages and post-complete re-drive hints
@@ -1 +1 @@
1
- {"version":3,"file":"gateway-lane-flow.d.ts","sourceRoot":"","sources":["../src/gateway-lane-flow.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"gateway-lane-flow.d.ts","sourceRoot":"","sources":["../src/gateway-lane-flow.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { ApiError } from '@parall/sdk';
1
2
  import { LedgerUnsupportedError } from './lane-ledger.js';
2
3
  /**
3
4
  * Dispatch one group of same-lane message events under the ledger contract:
@@ -101,6 +102,161 @@ export async function dispatchLaneGroup(host, opts) {
101
102
  await ledger.completeIfIdle(lane.laneKey, pendingInjections || opts.hasMoreLocal());
102
103
  return 'dispatched';
103
104
  }
105
+ /**
106
+ * The WorkItem ids of a typed event group whose lifecycle the ledger owns
107
+ * (claimed into dsp lanes by consumeTypedDispatch), or null when the group
108
+ * must stay on the legacy received/ack surface — ledger unavailable, an id
109
+ * missing, or a member that rides a message lane. Non-null means: skip
110
+ * legacy received+ack, resolve via the by-id complete, and fold the
111
+ * turn-error signal into the outcome.
112
+ */
113
+ export function typedLedgerEventIds(host, events) {
114
+ if (!host.laneLedger || host.ledgerDisabled)
115
+ return null;
116
+ const ids = [];
117
+ for (const ev of events) {
118
+ if (!ev.dispatchEventId || host.usesLaneLedger(ev))
119
+ return null;
120
+ ids.push(ev.dispatchEventId);
121
+ }
122
+ return ids.length > 0 ? ids : null;
123
+ }
124
+ /**
125
+ * A pre-by-id server (v1.44) routes a by-id body into its lane-form
126
+ * validation and answers 400 INVALID_REQUEST. Any 400 here means the server
127
+ * did not understand the form — a well-formed by-id call never 400s on a
128
+ * current server — so the caller falls back to the legacy typed ack for the
129
+ * rest of the process (sticky; the bridge restarts on the next update).
130
+ */
131
+ function isByIDCompleteUnsupported(err) {
132
+ return err instanceof ApiError && err.status === 400;
133
+ }
134
+ /**
135
+ * By-id complete (turn_outcome ok): terminal resolution of ONE WorkItem —
136
+ * exact where the source pair is ambiguous across task siblings. `lane`
137
+ * fences the call while the row is claim-owned (a dethroned caller gets
138
+ * 'stale' and must leave the row to its successor); omit it for the
139
+ * wrapper-less pending close (buffered drain, administrative drops).
140
+ */
141
+ export async function resolveDispatchByID(host, dispatchEventId, lane) {
142
+ try {
143
+ await host.opts.client.completeDispatch(host.opts.config.org_id, {
144
+ dispatch_event_id: dispatchEventId,
145
+ ...(lane ? { lane } : {}),
146
+ turn_outcome: 'ok',
147
+ });
148
+ return 'ok';
149
+ }
150
+ catch (err) {
151
+ if (err instanceof ApiError && err.status === 409)
152
+ return 'stale';
153
+ if (isByIDCompleteUnsupported(err))
154
+ return 'unsupported';
155
+ host.opts.log?.warn(`by-id complete failed for ${dispatchEventId} — leaving for re-drive: ${String(err)}`);
156
+ return 'failed';
157
+ }
158
+ }
159
+ /**
160
+ * Free a typed event's in-memory hot-path dedupe claim by its source pair —
161
+ * the ParallEvent-keyed mirror of the gateway's clearTypedDispatchDedupe
162
+ * (which keys on the wire DispatchNewData). Used when a buffered typed
163
+ * dispatch ran but its resolution failed: the member re-drives, and a stale
164
+ * local claim would make this pod reject the retry forever.
165
+ *
166
+ * PARITY: the switch below and clearTypedDispatchDedupe's must handle the
167
+ * same typed source families — when adding a new typed event type, extend
168
+ * BOTH (they key the same dedupe entries from different event shapes).
169
+ */
170
+ export function clearTypedDedupeForEvent(host, event) {
171
+ const sourceId = event.ackSourceId;
172
+ if (!sourceId)
173
+ return;
174
+ switch (event.ackSourceType) {
175
+ case 'task_activity': {
176
+ // Task dedupe keys are `${task_id}:${updated_at}`; the task id is the
177
+ // event target. Prefix-clear mirrors clearTypedDispatchDedupe.
178
+ const prefix = `${event.targetId}:`;
179
+ for (const key of host.dispatchedTasks) {
180
+ if (key.startsWith(prefix))
181
+ host.dispatchedTasks.delete(key);
182
+ }
183
+ break;
184
+ }
185
+ case 'comment':
186
+ host.dispatchedTasks.delete(`comment:${sourceId}`);
187
+ break;
188
+ case 'schedule_run':
189
+ host.dispatchedTasks.delete(`schedule_run:${sourceId}`);
190
+ break;
191
+ case 'external_trigger_run':
192
+ host.dispatchedTasks.delete(`external_trigger_run:${sourceId}`);
193
+ break;
194
+ case 'channel_message':
195
+ host.dispatchedMessages.delete(`channel_message:${sourceId}`);
196
+ break;
197
+ case 'approval':
198
+ host.dispatchedTasks.delete(`approval:${sourceId}`);
199
+ break;
200
+ }
201
+ }
202
+ /**
203
+ * Settle a drained typed group — the one wrapper-less dispatch path: these
204
+ * events buffered behind a busy main (fork-less adapter), their consume
205
+ * guards returned false and released the claims, so the drain site owns the
206
+ * terminal resolution. Rows are pending — the by-id close resolves them
207
+ * without a fence; a row a successor lane meanwhile claimed answers stale and
208
+ * is that owner's to finish. `turnErrored` (consumed at the call site before
209
+ * the drain can start another turn) leaves the rows for the renotify pacing
210
+ * instead. Either way a row left live gets its local dedupe claim freed, or
211
+ * this pod would reject the retry forever.
212
+ */
213
+ export async function settleDrainedTypedGroup(host, events, ids, turnErrored) {
214
+ if (turnErrored) {
215
+ host.opts.log?.info(`buffered typed turn for ${events[events.length - 1]?.messageId} surfaced a runtime error — leaving for re-drive`);
216
+ for (const event of events)
217
+ clearTypedDedupeForEvent(host, event);
218
+ return;
219
+ }
220
+ // The legacy ack BY ID, never by source — task siblings share a source
221
+ // pair, and a by-source ack would sweep the undispatched one. Awaited, and
222
+ // the dedupe claim is freed on EVERY outcome: a claim's lifetime is the
223
+ // buffer stay (#1149). On failure the row is still pending and its retry
224
+ // must not be self-rejected; on success the row is terminal — but a
225
+ // shared-key sibling WorkItem (task assign+update fetch the same
226
+ // `task:updated_at` pair) must not be rejected by a leftover claim either.
227
+ const legacyAckFrom = async (start) => {
228
+ for (const [j, id] of ids.slice(start).entries()) {
229
+ try {
230
+ await host.opts.client.ackDispatchByID(host.opts.config.org_id, id);
231
+ }
232
+ catch {
233
+ // Transient ack failure: the row stays live server-side and its
234
+ // re-drive re-converges — keep settling the REST of the group (a
235
+ // propagated error would strand every later id's retained claim).
236
+ }
237
+ finally {
238
+ clearTypedDedupeForEvent(host, events[start + j]);
239
+ }
240
+ }
241
+ };
242
+ if (host.typedByIdCompleteUnsupported) {
243
+ // Sticky: the server already answered one by-id probe with 400 — go
244
+ // straight to the legacy acks instead of re-probing per group.
245
+ await legacyAckFrom(0);
246
+ return;
247
+ }
248
+ for (const [i, id] of ids.entries()) {
249
+ const outcome = await resolveDispatchByID(host, id);
250
+ if (outcome === 'unsupported') {
251
+ host.typedByIdCompleteUnsupported = true;
252
+ host.opts.log?.warn('server predates the by-id dispatch complete — falling back to legacy typed acks');
253
+ await legacyAckFrom(i);
254
+ return;
255
+ }
256
+ // Freed on every outcome — see legacyAckFrom's contract note.
257
+ clearTypedDedupeForEvent(host, events[i]);
258
+ }
259
+ }
104
260
  /** Failure backoff pacing for typed dispatch retries (base 2s, cap 5min). */
105
261
  const TYPED_BACKOFF_BASE_MS = 2_000;
106
262
  const TYPED_BACKOFF_CAP_MS = 5 * 60_000;
@@ -108,18 +264,22 @@ const TYPED_BACKOFF_MAP_CAP = 512;
108
264
  /**
109
265
  * Consume one typed dispatch (task/comment/schedule/trigger/approval) under
110
266
  * its typed-lane occupancy guard: claim the dsp:<id> lane (skip when another
111
- * pod holds it or the WorkItem is already resolved), run the handler, ack on
112
- * success (the doc's option (b): notification delivered, tracked elsewhere),
113
- * then release the lane. Legacy run+ack flow when the ledger is unavailable.
267
+ * pod holds it or the WorkItem is already resolved), run the handler, then
268
+ * settle. A successful run resolves the WorkItem terminally via the
269
+ * sources-form complete (turn_outcome ok the server sweeps it, or lets a
270
+ * typed Effect committed during the turn stand, and drops the vacated lane);
271
+ * a failed / errored / unresolved run releases the member back to pending on
272
+ * the redrive budget via the lane complete. Legacy run+ack flow when the
273
+ * ledger is unavailable.
114
274
  *
115
- * Repeated failures back off: a consume that ends un-acked re-arms the
275
+ * Repeated failures back off: a consume that ends unresolved re-arms the
116
276
  * WorkItem's backoff entry, and the next attempt sleeps out the remaining
117
277
  * window before claiming. Without this, complete's release re-drives the
118
278
  * item instantly and a persistently-failing consume (e.g. buffered behind a
119
279
  * saturated fork pool) spins at wire speed — the client half of the
120
280
  * 2026-07-11 poison loop (the server half is the redrive budget).
121
281
  */
122
- export async function consumeTypedDispatch(host, ref, run, ack) {
282
+ export async function consumeTypedDispatch(host, ref, run, hooks) {
123
283
  const backoffKey = ref.dispatchEventId ?? `${ref.sourceType}:${ref.sourceId}`;
124
284
  const armed = host.typedRedriveBackoff.get(backoffKey);
125
285
  if (armed) {
@@ -142,8 +302,8 @@ export async function consumeTypedDispatch(host, ref, run, ack) {
142
302
  // consume — clearing backoff there would let an ack outage re-create the
143
303
  // wire-speed release/re-drive loop against a pre-budget server.
144
304
  const settleAck = (ackResult) => ackResult !== false;
145
- const settle = (acked) => {
146
- if (acked) {
305
+ const settle = (resolved) => {
306
+ if (resolved) {
147
307
  host.typedRedriveBackoff.delete(backoffKey);
148
308
  return;
149
309
  }
@@ -168,7 +328,7 @@ export async function consumeTypedDispatch(host, ref, run, ack) {
168
328
  let acked = false;
169
329
  try {
170
330
  if (await run(ref.dispatchEventId)) {
171
- acked = settleAck(await ack(ref.dispatchEventId));
331
+ acked = settleAck(await hooks.legacyAck(ref.dispatchEventId));
172
332
  }
173
333
  }
174
334
  finally {
@@ -197,21 +357,68 @@ export async function consumeTypedDispatch(host, ref, run, ack) {
197
357
  host.opts.log?.info(`typed dispatch ${ref.dispatchEventId ?? `${ref.sourceType}:${ref.sourceId}`} not claimable (held elsewhere or already resolved) — skipping`);
198
358
  return;
199
359
  }
200
- let acked = false;
360
+ let resolved = false;
361
+ let viaLegacyAck = false;
201
362
  try {
202
- // Ack must settle before Complete. A fire-and-forget ack races the lane
203
- // release: Complete can return the still-received typed item to pending
204
- // and publish a re-drive while its successful ack is still in flight.
363
+ // A successful run resolves the member terminally through the by-id
364
+ // complete one fenced call that sweeps exactly this WorkItem (a typed
365
+ // Effect committed during the turn already made it terminal idempotent
366
+ // no-op) AND drops the vacated lane. Resolution must settle inside this
367
+ // guard: a fire-and-forget resolve would race the finally's release and
368
+ // spuriously re-drive handled work.
205
369
  if (await run(lane.typedDispatchEventId)) {
206
- acked = settleAck(await ack(lane.typedDispatchEventId));
370
+ if (host.typedByIdCompleteUnsupported || !lane.typedDispatchEventId) {
371
+ // Pre-by-id server (or a defensive claim shape without the id):
372
+ // the legacy administrative ack is still live there.
373
+ viaLegacyAck = true;
374
+ resolved = settleAck(await hooks.legacyAck(lane.typedDispatchEventId));
375
+ }
376
+ else {
377
+ const outcome = await resolveDispatchByID(host, lane.typedDispatchEventId, lane.lane);
378
+ if (outcome === 'unsupported') {
379
+ host.typedByIdCompleteUnsupported = true;
380
+ host.opts.log?.warn('server predates the by-id dispatch complete — falling back to the legacy typed ack');
381
+ viaLegacyAck = true;
382
+ resolved = settleAck(await hooks.legacyAck(lane.typedDispatchEventId));
383
+ }
384
+ else if (outcome === 'stale') {
385
+ // A successor lane owns the row — its turn resolves it. Nothing to
386
+ // spin on locally; our claim record is already dethroned. Free the
387
+ // local dedupe claim though: if the successor later FAILS and
388
+ // releases the row, the re-drive may land back on this pod, and a
389
+ // stale local claim would self-reject the retry into the budget.
390
+ hooks.clearDedupe?.();
391
+ resolved = true;
392
+ }
393
+ else {
394
+ resolved = outcome === 'ok';
395
+ if (!resolved) {
396
+ // The member is about to be released for re-drive — free its
397
+ // hot-path dedupe claim first, or this pod rejects its own retry
398
+ // forever (same contract as the legacy ack-failure path).
399
+ hooks.clearDedupe?.();
400
+ }
401
+ }
402
+ }
207
403
  }
208
404
  }
209
405
  finally {
210
- settle(acked);
211
- // Release the occupancy row. A buffered dispatch may outlive this guard
212
- // (lane TTL) acceptable at-least-once; the ledger's resolution paths
213
- // still dedupe the persistent side effects.
214
- await host.laneLedger.completeIfIdle(lane.laneKey, false).catch(() => { });
406
+ settle(resolved);
407
+ if (resolved && !viaLegacyAck) {
408
+ // The by-id complete already dropped (or a takeover already owns) the
409
+ // server-side lane row; only the local record and context file remain.
410
+ host.laneLedger.dropLocal(lane.laneKey);
411
+ }
412
+ else {
413
+ // Two jobs, one call: an unresolved turn (unconsumed / failed /
414
+ // errored / resolution-failed) releases the member back to pending on
415
+ // the redrive budget; a legacy-acked turn still needs its server-side
416
+ // lane row dropped (the ack resolves the row but not the occupancy).
417
+ // A buffered dispatch may outlive this guard (lane TTL) — acceptable
418
+ // at-least-once; the ledger's resolution paths still dedupe the
419
+ // persistent side effects.
420
+ await host.laneLedger.completeIfIdle(lane.laneKey, false).catch(() => { });
421
+ }
215
422
  }
216
423
  }
217
424
  /**
@@ -227,7 +434,22 @@ export async function consumeMessageWorkItem(host, item) {
227
434
  return;
228
435
  if (!host.tryClaimMessage(item.source_id))
229
436
  return;
437
+ // Administrative drop (deleted source / self-sender / skip decision): on
438
+ // the ledger path the by-id complete closes the pending row terminally and
439
+ // refuses (409) a row a live lane owns — the owner's turn resolves it. The
440
+ // legacy ack remains the ledger-disabled / pre-by-id fallback.
441
+ // Fire-and-forget either way: a failed drop leaves the row live and the
442
+ // next re-drive converges on the same drop.
230
443
  const ackItem = () => {
444
+ if (host.laneLedger && !host.ledgerDisabled && !host.typedByIdCompleteUnsupported) {
445
+ void resolveDispatchByID(host, item.id).then((outcome) => {
446
+ if (outcome === 'unsupported') {
447
+ host.typedByIdCompleteUnsupported = true;
448
+ host.opts.client.ackDispatchByID(host.opts.config.org_id, item.id).catch(() => { });
449
+ }
450
+ });
451
+ return;
452
+ }
231
453
  host.opts.client.ackDispatchByID(host.opts.config.org_id, item.id).catch(() => { });
232
454
  };
233
455
  let msg = null;
@@ -0,0 +1,4 @@
1
+ export declare function configureHttpKeepAlive(opts?: {
2
+ keepAliveTimeoutMs?: number;
3
+ }): void;
4
+ //# sourceMappingURL=http-keepalive.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-keepalive.d.ts","sourceRoot":"","sources":["../src/http-keepalive.ts"],"names":[],"mappings":"AAsBA,wBAAgB,sBAAsB,CAAC,IAAI,CAAC,EAAE;IAAE,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAanF"}
@@ -0,0 +1,33 @@
1
+ import { Agent, setGlobalDispatcher } from 'undici';
2
+ /**
3
+ * Keep bridge→api HTTP connections long-lived.
4
+ *
5
+ * Node's built-in fetch closes idle pooled connections after ~4s when the
6
+ * server sends no Keep-Alive hint (our NLB+Envoy edge sends none), so any two
7
+ * bridge API calls spaced more than a few seconds apart pay a fresh TCP+TLS
8
+ * handshake. On a degraded long-haul link that handshake is exactly what
9
+ * fails — established connections (the WS, the runtime's own model stream)
10
+ * ride through such windows while every bridge call times out (2026-07-10:
11
+ * step creation losses). A 2-minute idle timeout keeps one warm connection
12
+ * across an active turn's step cadence, comfortably below the NLB's 350s
13
+ * idle cutoff.
14
+ *
15
+ * Uses undici's global-dispatcher registry, which Node's built-in fetch
16
+ * shares (verified on Node 22 / undici 7) — so this covers every fetch in
17
+ * the process: ParallClient, lane-ledger heartbeats, OTLP export.
18
+ * Node-only; call once from a bridge/daemon entrypoint before any fetch.
19
+ */
20
+ let installed = false;
21
+ export function configureHttpKeepAlive(opts) {
22
+ // Idempotent: a second call (e.g. a process hosting several bridge
23
+ // entrypoints) must not replace — and leak — the already-installed Agent.
24
+ if (installed)
25
+ return;
26
+ installed = true;
27
+ const keepAliveTimeout = opts?.keepAliveTimeoutMs ?? 120_000;
28
+ setGlobalDispatcher(new Agent({
29
+ keepAliveTimeout,
30
+ // Also cap the server-hinted value: never idle past the edge's cutoff.
31
+ keepAliveMaxTimeout: keepAliveTimeout,
32
+ }));
33
+ }
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ export * from './provider-config.js';
2
2
  export * from './types.js';
3
3
  export * from './lane-key.js';
4
4
  export type { LaneFlowHost } from './gateway-lane-flow.js';
5
- export { consumeTypedDispatch } from './gateway-lane-flow.js';
5
+ export { consumeTypedDispatch, settleDrainedTypedGroup } from './gateway-lane-flow.js';
6
6
  export * from './session-state.js';
7
7
  export * from './routing.js';
8
8
  export * from './event-format.js';
@@ -11,6 +11,7 @@ export * from './bridge-workspace.js';
11
11
  export * from './dispatch-adapter.js';
12
12
  export { createLogger, childLogger } from './logger.js';
13
13
  export * from './gateway-base.js';
14
+ export { configureHttpKeepAlive } from './http-keepalive.js';
14
15
  export * from './platform-config.js';
15
16
  export * from './channel-capability.js';
16
17
  export * from './channel-token.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,YAAY,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,cAAc,oBAAoB,CAAC;AACnC,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACxD,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,yBAAyB,CAAC;AACxC,cAAc,oBAAoB,CAAC;AACnC,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAClF,YAAY,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,EACf,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,YAAY,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,oBAAoB,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACvF,cAAc,oBAAoB,CAAC;AACnC,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACxD,cAAc,mBAAmB,CAAC;AAClC,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAM7D,cAAc,sBAAsB,CAAC;AACrC,cAAc,yBAAyB,CAAC;AACxC,cAAc,oBAAoB,CAAC;AACnC,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAClF,YAAY,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,EACf,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC"}
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export * from './provider-config.js';
2
2
  export * from './types.js';
3
3
  export * from './lane-key.js';
4
- export { consumeTypedDispatch } from './gateway-lane-flow.js';
4
+ export { consumeTypedDispatch, settleDrainedTypedGroup } from './gateway-lane-flow.js';
5
5
  export * from './session-state.js';
6
6
  export * from './routing.js';
7
7
  export * from './event-format.js';
@@ -10,6 +10,12 @@ export * from './bridge-workspace.js';
10
10
  export * from './dispatch-adapter.js';
11
11
  export { createLogger, childLogger } from './logger.js';
12
12
  export * from './gateway-base.js';
13
+ export { configureHttpKeepAlive } from './http-keepalive.js';
14
+ // StepPersister / StepRetryQueue / SessionLifecycleCoordinator /
15
+ // ForkSessionFinalizer are internal gateway collaborators — deliberately NOT
16
+ // re-exported from the package root (no external consumer; a root export
17
+ // would become an accidental long-term compat promise). Tests import their
18
+ // built modules directly (e.g. `../dist/step-retry-queue.js`).
13
19
  export * from './platform-config.js';
14
20
  export * from './channel-capability.js';
15
21
  export * from './channel-token.js';
@@ -123,6 +123,14 @@ export declare class LaneLedger {
123
123
  sourceType?: string;
124
124
  sourceId?: string;
125
125
  }): Promise<ActiveLane | null>;
126
+ /**
127
+ * Drop a lane's local record without any server call — for a typed lane
128
+ * whose member the by-id complete just resolved (the server dropped the
129
+ * vacated lane row in the same transaction). Calling completeIfIdle
130
+ * instead would fire a lane-form Complete at a lane that no longer exists
131
+ * and burn an RPC on the guaranteed STALE_LANE answer.
132
+ */
133
+ dropLocal(laneKey: string): void;
126
134
  /**
127
135
  * Remove the per-lane context file (and its CLI sidecar) when the lane
128
136
  * ends. A leftover file would make a later cross-context send to the same
@@ -1 +1 @@
1
- {"version":3,"file":"lane-ledger.d.ts","sourceRoot":"","sources":["../src/lane-ledger.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,sDAAsD;AACtD,MAAM,MAAM,UAAU,GAAG;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,8EAA8E;IAC9E,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,6EAA6E;IAC7E,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,6EAA6E;IAC7E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAEF;;;;GAIG;AACH,qBAAa,sBAAuB,SAAQ,KAAK;CAAG;AAEpD,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,SAAS,CAAC;AAqBrD;;;;;;;;;;;GAWG;AACH,qBAAa,UAAU;IAInB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAHvB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiC;gBAGpC,IAAI,EAAE;QACrB,MAAM,EAAE,YAAY,CAAC;QACrB,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;QACnB,GAAG,CAAC,EAAE,aAAa,CAAC;KACrB;IAGH,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED,+FAA+F;IAC/F,OAAO,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO;IAIpC,UAAU,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM;IAOtC,WAAW,CAAC,KAAK,EAAE,WAAW,GAAG,UAAU,GAAG,SAAS;IAIvD,eAAe,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM;IAIzC;;;;;;OAMG;IACG,UAAU,CAAC,MAAM,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAsFnE;;;;;OAKG;IACG,SAAS,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC;IA0BrD;;;;;OAKG;IACH;;;;;;OAMG;IACH,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAK9B,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IA6B3E;;;;;OAKG;IACH,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAKjC;;;;;;;OAOG;IACH,UAAU,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI;IA6BlC;;;;OAIG;IACG,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYvC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAOjC,yEAAyE;IACzE,IAAI,WAAW,IAAI,MAAM,CAExB;IAED;;;;;OAKG;IACG,UAAU,CAAC,GAAG,EAAE;QACpB,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IA8B9B;;;;;OAKG;IACH,OAAO,CAAC,iBAAiB;CAW1B"}
1
+ {"version":3,"file":"lane-ledger.d.ts","sourceRoot":"","sources":["../src/lane-ledger.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,sDAAsD;AACtD,MAAM,MAAM,UAAU,GAAG;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,8EAA8E;IAC9E,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,6EAA6E;IAC7E,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,6EAA6E;IAC7E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAEF;;;;GAIG;AACH,qBAAa,sBAAuB,SAAQ,KAAK;CAAG;AAEpD,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,SAAS,CAAC;AAqBrD;;;;;;;;;;;GAWG;AACH,qBAAa,UAAU;IAInB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAHvB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiC;gBAGpC,IAAI,EAAE;QACrB,MAAM,EAAE,YAAY,CAAC;QACrB,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;QACnB,GAAG,CAAC,EAAE,aAAa,CAAC;KACrB;IAGH,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED,+FAA+F;IAC/F,OAAO,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO;IAIpC,UAAU,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM;IAOtC,WAAW,CAAC,KAAK,EAAE,WAAW,GAAG,UAAU,GAAG,SAAS;IAIvD,eAAe,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM;IAIzC;;;;;;OAMG;IACG,UAAU,CAAC,MAAM,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAsFnE;;;;;OAKG;IACG,SAAS,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC;IA0BrD;;;;;OAKG;IACH;;;;;;OAMG;IACH,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAK9B,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IA6B3E;;;;;OAKG;IACH,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAKjC;;;;;;;OAOG;IACH,UAAU,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI;IA6BlC;;;;OAIG;IACG,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYvC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAOjC,yEAAyE;IACzE,IAAI,WAAW,IAAI,MAAM,CAExB;IAED;;;;;OAKG;IACG,UAAU,CAAC,GAAG,EAAE;QACpB,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IA8B9B;;;;;;OAMG;IACH,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAOhC;;;;;OAKG;IACH,OAAO,CAAC,iBAAiB;CAW1B"}
@@ -347,6 +347,20 @@ export class LaneLedger {
347
347
  this.lanes.set(lane.laneKey, lane);
348
348
  return lane;
349
349
  }
350
+ /**
351
+ * Drop a lane's local record without any server call — for a typed lane
352
+ * whose member the by-id complete just resolved (the server dropped the
353
+ * vacated lane row in the same transaction). Calling completeIfIdle
354
+ * instead would fire a lane-form Complete at a lane that no longer exists
355
+ * and burn an RPC on the guaranteed STALE_LANE answer.
356
+ */
357
+ dropLocal(laneKey) {
358
+ const lane = this.lanes.get(laneKey);
359
+ if (!lane)
360
+ return;
361
+ this.lanes.delete(laneKey);
362
+ this.removeLaneContext(lane);
363
+ }
350
364
  /**
351
365
  * Remove the per-lane context file (and its CLI sidecar) when the lane
352
366
  * ends. A leftover file would make a later cross-context send to the same