@parall/agent-core 1.42.0 → 1.42.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.
@@ -122,6 +122,8 @@ export declare class ParallAgentGateway {
122
122
  private laneFlowHost;
123
123
  private dispatchLaneGroup;
124
124
  private consumeTypedDispatch;
125
+ private ackDispatchEvent;
126
+ private clearTypedDispatchDedupe;
125
127
  private buildDispatchContext;
126
128
  private isSessionNotLiveError;
127
129
  private createInputStep;
@@ -144,6 +146,7 @@ export declare class ParallAgentGateway {
144
146
  private buildMessageDispatchDecision;
145
147
  private handleMessage;
146
148
  private handleMessageRedrive;
149
+ private handleTaskAssignmentRedrive;
147
150
  private consumeMessageWorkItem;
148
151
  private handleTaskAssignment;
149
152
  private handleTaskDispatch;
@@ -1 +1 @@
1
- {"version":3,"file":"gateway-base.d.ts","sourceRoot":"","sources":["../src/gateway-base.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"gateway-base.d.ts","sourceRoot":"","sources":["../src/gateway-base.ts"],"names":[],"mappings":"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"}
@@ -255,17 +255,16 @@ export class ParallAgentGateway {
255
255
  ? { dispatchEventId: data.dispatch_event_id }
256
256
  : { sourceType: 'task_activity', sourceId: data.id }, (dispatchEventId) => this.handleTaskAssignment(data, data.id, dispatchEventId), (dispatchEventId) => {
257
257
  if (dispatchEventId) {
258
- this.opts.client
259
- .ackDispatchByID(this.opts.config.org_id, dispatchEventId)
260
- .catch(() => { });
261
- return;
258
+ return this.ackDispatchEvent(dispatchEventId, () => {
259
+ this.dispatchedTasks.delete(`${data.id}:${data.updated_at}`);
260
+ });
262
261
  }
263
- this.opts.client
262
+ return this.opts.client
264
263
  .ackDispatch(this.opts.config.org_id, {
265
264
  source_type: 'task_activity',
266
265
  source_id: data.id,
267
266
  })
268
- .catch(() => { });
267
+ .then(() => undefined, () => undefined);
269
268
  });
270
269
  }
271
270
  catch (err) {
@@ -273,13 +272,21 @@ export class ParallAgentGateway {
273
272
  }
274
273
  });
275
274
  ws.on('dispatch.new', async (data) => {
276
- if (data.event_type === 'task_comment') {
275
+ if (data.event_type === 'task_assign') {
276
+ if (!data.task_id)
277
+ return;
278
+ try {
279
+ await this.handleTaskAssignmentRedrive(data);
280
+ }
281
+ catch (err) {
282
+ this.opts.log?.error(`task assignment re-drive failed for ${data.task_id}: ${String(err)}`);
283
+ }
284
+ }
285
+ else if (data.event_type === 'task_comment') {
277
286
  if (!data.source_id || !data.task_id)
278
287
  return;
279
288
  try {
280
- await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.handleTaskComment(data.source_id, data.task_id ?? '', data.actor_id, data.delivery_reason), () => {
281
- this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => { });
282
- });
289
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.handleTaskComment(data.source_id, data.task_id ?? '', data.actor_id, data.delivery_reason), () => this.ackDispatchEvent(data.id, () => this.clearTypedDispatchDedupe(data)));
283
290
  }
284
291
  catch (err) {
285
292
  this.opts.log?.error(`task comment dispatch failed for ${data.source_id}: ${String(err)}`);
@@ -289,9 +296,7 @@ export class ParallAgentGateway {
289
296
  if (!data.source_id)
290
297
  return;
291
298
  try {
292
- await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.handleWikiComment(data.source_id, data.actor_id, data.delivery_reason), () => {
293
- this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => { });
294
- });
299
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.handleWikiComment(data.source_id, data.actor_id, data.delivery_reason), () => this.ackDispatchEvent(data.id, () => this.clearTypedDispatchDedupe(data)));
295
300
  }
296
301
  catch (err) {
297
302
  this.opts.log?.error(`wiki comment dispatch failed for ${data.source_id}: ${String(err)}`);
@@ -304,9 +309,7 @@ export class ParallAgentGateway {
304
309
  await this.consumeTypedDispatch({ dispatchEventId: data.id }, (dispatchEventId) => this.handleTaskDispatch(data.task_id ?? '', data.source_id ?? data.task_id ?? '', {
305
310
  allowCreator: true,
306
311
  dispatchEventId,
307
- }), () => {
308
- this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => { });
309
- });
312
+ }), () => this.ackDispatchEvent(data.id, () => this.clearTypedDispatchDedupe(data)));
310
313
  }
311
314
  catch (err) {
312
315
  this.opts.log?.error(`task update dispatch failed for ${data.task_id}: ${String(err)}`);
@@ -316,9 +319,7 @@ export class ParallAgentGateway {
316
319
  if (!data.source_id)
317
320
  return;
318
321
  try {
319
- await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.fetchAndHandleScheduleFire(data.source_id, data.actor_id), () => {
320
- this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => { });
321
- });
322
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.fetchAndHandleScheduleFire(data.source_id, data.actor_id), () => this.ackDispatchEvent(data.id, () => this.clearTypedDispatchDedupe(data)));
322
323
  }
323
324
  catch (err) {
324
325
  this.opts.log?.error(`schedule fire dispatch failed for ${data.source_id}: ${String(err)}`);
@@ -328,9 +329,7 @@ export class ParallAgentGateway {
328
329
  if (!data.source_id)
329
330
  return;
330
331
  try {
331
- await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.fetchAndHandleExternalTriggerRun(data.source_id), () => {
332
- this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => { });
333
- });
332
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.fetchAndHandleExternalTriggerRun(data.source_id), () => this.ackDispatchEvent(data.id, () => this.clearTypedDispatchDedupe(data)));
334
333
  }
335
334
  catch (err) {
336
335
  this.opts.log?.error(`external trigger dispatch failed for ${data.source_id}: ${String(err)}`);
@@ -340,9 +339,7 @@ export class ParallAgentGateway {
340
339
  if (!data.source_id)
341
340
  return;
342
341
  try {
343
- await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.fetchAndHandleChannelMessage(data.source_id), () => {
344
- this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => { });
345
- });
342
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.fetchAndHandleChannelMessage(data.source_id), () => this.ackDispatchEvent(data.id, () => this.clearTypedDispatchDedupe(data)));
346
343
  }
347
344
  catch (err) {
348
345
  this.opts.log?.error(`channel message dispatch failed for ${data.source_id}: ${String(err)}`);
@@ -352,9 +349,7 @@ export class ParallAgentGateway {
352
349
  if (!data.source_id)
353
350
  return;
354
351
  try {
355
- await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.fetchAndHandleApprovalDecided(data.source_id, data.actor_id, data.chat_id ?? null), () => {
356
- this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => { });
357
- });
352
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.fetchAndHandleApprovalDecided(data.source_id, data.actor_id, data.chat_id ?? null), () => this.ackDispatchEvent(data.id, () => this.clearTypedDispatchDedupe(data)));
358
353
  }
359
354
  catch (err) {
360
355
  this.opts.log?.error(`approval decided dispatch failed for ${data.source_id}: ${String(err)}`);
@@ -374,14 +369,11 @@ export class ParallAgentGateway {
374
369
  this.opts.log?.error(`message re-drive failed for ${data.source_id}: ${String(err)}`);
375
370
  }
376
371
  }
377
- else if (data.event_type !== 'message' && data.event_type !== 'task_assign') {
372
+ else if (data.event_type !== 'message') {
378
373
  // Truly unknown event_type — log so a newly-added dispatch type
379
- // not yet wired here surfaces during runtime testing. "message"
380
- // and "task_assign" are deliberately excluded: dispatch.new
381
- // carries them too, but they are owned by dedicated WS handlers
382
- // (message.new, task.assigned) above and would otherwise spam
383
- // info-level logs for every inbound chat message / task
384
- // assignment on a busy agent.
374
+ // not yet wired here surfaces during runtime testing. "message" is
375
+ // deliberately excluded because its first delivery is owned by the
376
+ // message.new handler; recovered task_assign rows are handled above.
385
377
  this.opts.log?.info(`dispatch.new with unhandled event_type=${String(data.event_type)} (id=${data.id}) — no-op`);
386
378
  }
387
379
  });
@@ -453,6 +445,53 @@ export class ParallAgentGateway {
453
445
  consumeTypedDispatch(ref, run, ack) {
454
446
  return consumeTypedDispatch(this.laneFlowHost(), ref, run, ack);
455
447
  }
448
+ // Typed completion must wait until the administrative ack has either
449
+ // committed or failed. Errors stay best-effort: a failed ack leaves the row
450
+ // received, so Complete releases and re-drives it safely.
451
+ ackDispatchEvent(dispatchEventId, onFailure) {
452
+ return this.opts.client.ackDispatchByID(this.opts.config.org_id, dispatchEventId).then(() => undefined, (err) => {
453
+ // Complete will return the still-received item to pending and publish
454
+ // an immediate hint. Free its hot-path claim first, otherwise that
455
+ // same-process re-drive is rejected forever by the local dedupe set.
456
+ onFailure?.();
457
+ this.opts.log?.warn(`dispatch ack failed for ${dispatchEventId}, releasing for re-drive: ${String(err)}`);
458
+ });
459
+ }
460
+ clearTypedDispatchDedupe(item) {
461
+ switch (item.event_type) {
462
+ case 'task_assign':
463
+ case 'task_update':
464
+ if (item.task_id) {
465
+ const prefix = `${item.task_id}:`;
466
+ for (const key of this.dispatchedTasks) {
467
+ if (key.startsWith(prefix))
468
+ this.dispatchedTasks.delete(key);
469
+ }
470
+ }
471
+ break;
472
+ case 'task_comment':
473
+ case 'wiki_comment':
474
+ if (item.source_id)
475
+ this.dispatchedTasks.delete(`comment:${item.source_id}`);
476
+ break;
477
+ case 'schedule.fire':
478
+ if (item.source_id)
479
+ this.dispatchedTasks.delete(`schedule_run:${item.source_id}`);
480
+ break;
481
+ case 'external_trigger':
482
+ if (item.source_id)
483
+ this.dispatchedTasks.delete(`external_trigger_run:${item.source_id}`);
484
+ break;
485
+ case 'channel_message':
486
+ if (item.source_id)
487
+ this.dispatchedMessages.delete(`channel_message:${item.source_id}`);
488
+ break;
489
+ case 'approval_decided':
490
+ if (item.source_id)
491
+ this.dispatchedTasks.delete(`approval:${item.source_id}`);
492
+ break;
493
+ }
494
+ }
456
495
  buildDispatchContext(event, sessionKey) {
457
496
  const binding = this.sessionBindings.get(sessionKey);
458
497
  return {
@@ -1605,6 +1644,17 @@ export class ParallAgentGateway {
1605
1644
  chat_id: item.chat_id,
1606
1645
  });
1607
1646
  }
1647
+ // A task assignment's first delivery rides task.assigned, but a typed lane
1648
+ // released without an effect is re-driven as dispatch.new. Reuse the exact
1649
+ // typed claim path used by catch-up so that recovery does not require a
1650
+ // runtime reconnect.
1651
+ async handleTaskAssignmentRedrive(item) {
1652
+ if (!item.task_id)
1653
+ return;
1654
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, (dispatchEventId) => this.handleTaskDispatch(item.task_id ?? '', item.source_id ?? item.task_id ?? '', {
1655
+ dispatchEventId,
1656
+ }), (dispatchEventId) => this.ackDispatchEvent(dispatchEventId ?? item.id, () => this.clearTypedDispatchDedupe(item)));
1657
+ }
1608
1658
  consumeMessageWorkItem(item) {
1609
1659
  return consumeMessageWorkItem(this.laneFlowHost(), item);
1610
1660
  }
@@ -2116,9 +2166,7 @@ export class ParallAgentGateway {
2116
2166
  }
2117
2167
  processed++;
2118
2168
  try {
2119
- const ackItem = () => {
2120
- this.opts.client.ackDispatchByID(this.opts.config.org_id, item.id).catch(() => { });
2121
- };
2169
+ const ackItem = () => this.ackDispatchEvent(item.id, () => this.clearTypedDispatchDedupe(item));
2122
2170
  if (item.event_type === 'task_assign' && item.task_id) {
2123
2171
  try {
2124
2172
  await this.consumeTypedDispatch({ dispatchEventId: item.id }, (dispatchEventId) => this.handleTaskDispatch(item.task_id ?? '', item.source_id ?? item.task_id ?? '', {
@@ -57,7 +57,7 @@ export declare function consumeTypedDispatch(host: LaneFlowHost, ref: {
57
57
  dispatchEventId?: string;
58
58
  sourceType?: string;
59
59
  sourceId?: string;
60
- }, run: (dispatchEventId?: string) => Promise<boolean>, ack: (dispatchEventId?: string) => void): Promise<void>;
60
+ }, run: (dispatchEventId?: string) => Promise<boolean>, ack: (dispatchEventId?: string) => void | Promise<void>): Promise<void>;
61
61
  /**
62
62
  * Shared consumption of a message WorkItem referenced by id — the ONE
63
63
  * 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":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC5E,OAAO,KAAK,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAEtF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;GAKG;AAEH,0EAA0E;AAC1E,MAAM,WAAW,YAAY;IAC3B,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,cAAc,EAAE,OAAO,CAAC;IACxB,YAAY,EAAE,OAAO,CAAC;IACtB,kBAAkB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE;QACJ,MAAM,EAAE,YAAY,CAAC;QACrB,GAAG,CAAC,EAAE,aAAa,CAAC;QACpB,MAAM,EAAE;YAAE,MAAM,EAAE,MAAM,CAAA;SAAE,CAAC;QAC3B,eAAe,EAAE,eAAe,CAAC;QACjC,WAAW,EAAE,MAAM,CAAC;KACrB,CAAC;IACF,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,cAAc,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC;IAC5C,oBAAoB,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,WAAW,CACT,KAAK,EAAE,WAAW,EAClB,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,EACpB,aAAa,CAAC,EAAE,WAAW,EAAE,EAC7B,WAAW,CAAC,EAAE,MAAM,EAAE,GACrB,OAAO,CAAC,OAAO,CAAC,CAAC;IACpB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IACrC,4BAA4B,CAC1B,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,uBAAuB,CAAC,CAAC;IACpC,kBAAkB,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1D;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,YAAY,EAClB,IAAI,EAAE;IACJ,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,YAAY,EAAE,MAAM,OAAO,CAAC;CAC7B,GACA,OAAO,CAAC,YAAY,GAAG,SAAS,GAAG,UAAU,CAAC,CA6DhD;AAED;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,YAAY,EAClB,GAAG,EAAE;IAAE,eAAe,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,EACzE,GAAG,EAAE,CAAC,eAAe,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,EACnD,GAAG,EAAE,CAAC,eAAe,CAAC,EAAE,MAAM,KAAK,IAAI,GACtC,OAAO,CAAC,IAAI,CAAC,CA8Bf;AAED;;;;;;;GAOG;AACH,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,YAAY,EAClB,IAAI,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACvD,OAAO,CAAC,IAAI,CAAC,CAuDf"}
1
+ {"version":3,"file":"gateway-lane-flow.d.ts","sourceRoot":"","sources":["../src/gateway-lane-flow.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC5E,OAAO,KAAK,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAEtF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;GAKG;AAEH,0EAA0E;AAC1E,MAAM,WAAW,YAAY;IAC3B,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,cAAc,EAAE,OAAO,CAAC;IACxB,YAAY,EAAE,OAAO,CAAC;IACtB,kBAAkB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE;QACJ,MAAM,EAAE,YAAY,CAAC;QACrB,GAAG,CAAC,EAAE,aAAa,CAAC;QACpB,MAAM,EAAE;YAAE,MAAM,EAAE,MAAM,CAAA;SAAE,CAAC;QAC3B,eAAe,EAAE,eAAe,CAAC;QACjC,WAAW,EAAE,MAAM,CAAC;KACrB,CAAC;IACF,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,cAAc,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC;IAC5C,oBAAoB,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,WAAW,CACT,KAAK,EAAE,WAAW,EAClB,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,EACpB,aAAa,CAAC,EAAE,WAAW,EAAE,EAC7B,WAAW,CAAC,EAAE,MAAM,EAAE,GACrB,OAAO,CAAC,OAAO,CAAC,CAAC;IACpB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IACrC,4BAA4B,CAC1B,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,uBAAuB,CAAC,CAAC;IACpC,kBAAkB,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1D;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,YAAY,EAClB,IAAI,EAAE;IACJ,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,YAAY,EAAE,MAAM,OAAO,CAAC;CAC7B,GACA,OAAO,CAAC,YAAY,GAAG,SAAS,GAAG,UAAU,CAAC,CA6DhD;AAED;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,YAAY,EAClB,GAAG,EAAE;IAAE,eAAe,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,EACzE,GAAG,EAAE,CAAC,eAAe,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,EACnD,GAAG,EAAE,CAAC,eAAe,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GACtD,OAAO,CAAC,IAAI,CAAC,CAiCf;AAED;;;;;;;GAOG;AACH,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,YAAY,EAClB,IAAI,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACvD,OAAO,CAAC,IAAI,CAAC,CAuDf"}
@@ -68,7 +68,7 @@ export async function dispatchLaneGroup(host, opts) {
68
68
  export async function consumeTypedDispatch(host, ref, run, ack) {
69
69
  if (!host.laneLedger || host.ledgerDisabled) {
70
70
  if (await run(ref.dispatchEventId))
71
- ack(ref.dispatchEventId);
71
+ await ack(ref.dispatchEventId);
72
72
  return;
73
73
  }
74
74
  let lane;
@@ -79,7 +79,7 @@ export async function consumeTypedDispatch(host, ref, run, ack) {
79
79
  if (err instanceof LedgerUnsupportedError) {
80
80
  host.disableLedger('claim endpoint missing');
81
81
  if (await run(ref.dispatchEventId))
82
- ack(ref.dispatchEventId);
82
+ await ack(ref.dispatchEventId);
83
83
  return;
84
84
  }
85
85
  throw err;
@@ -89,8 +89,11 @@ export async function consumeTypedDispatch(host, ref, run, ack) {
89
89
  return;
90
90
  }
91
91
  try {
92
+ // Ack must settle before Complete. A fire-and-forget ack races the lane
93
+ // release: Complete can return the still-received typed item to pending
94
+ // and publish a re-drive while its successful ack is still in flight.
92
95
  if (await run(lane.typedDispatchEventId))
93
- ack(lane.typedDispatchEventId);
96
+ await ack(lane.typedDispatchEventId);
94
97
  }
95
98
  finally {
96
99
  // Release the occupancy row. A buffered dispatch may outlive this guard
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/agent-core",
3
- "version": "1.42.0",
3
+ "version": "1.42.1",
4
4
  "description": "Shared agent runtime orchestration helpers for Parall",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -35,7 +35,7 @@
35
35
  "@opentelemetry/sdk-logs": "^0.57.0",
36
36
  "@opentelemetry/sdk-metrics": "^1.30.0",
37
37
  "@opentelemetry/sdk-trace-node": "^1.30.0",
38
- "@parall/sdk": "1.42.0"
38
+ "@parall/sdk": "1.42.1"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/node": "^22.0.0",
@@ -438,17 +438,19 @@ export class ParallAgentGateway {
438
438
  (dispatchEventId) => this.handleTaskAssignment(data, data.id, dispatchEventId),
439
439
  (dispatchEventId) => {
440
440
  if (dispatchEventId) {
441
- this.opts.client
442
- .ackDispatchByID(this.opts.config.org_id, dispatchEventId)
443
- .catch(() => {});
444
- return;
441
+ return this.ackDispatchEvent(dispatchEventId, () => {
442
+ this.dispatchedTasks.delete(`${data.id}:${data.updated_at}`);
443
+ });
445
444
  }
446
- this.opts.client
445
+ return this.opts.client
447
446
  .ackDispatch(this.opts.config.org_id, {
448
447
  source_type: 'task_activity',
449
448
  source_id: data.id,
450
449
  })
451
- .catch(() => {});
450
+ .then(
451
+ () => undefined,
452
+ () => undefined,
453
+ );
452
454
  },
453
455
  );
454
456
  } catch (err) {
@@ -457,7 +459,16 @@ export class ParallAgentGateway {
457
459
  });
458
460
 
459
461
  ws.on('dispatch.new', async (data: DispatchNewData) => {
460
- if (data.event_type === 'task_comment') {
462
+ if (data.event_type === 'task_assign') {
463
+ if (!data.task_id) return;
464
+ try {
465
+ await this.handleTaskAssignmentRedrive(data);
466
+ } catch (err) {
467
+ this.opts.log?.error(
468
+ `task assignment re-drive failed for ${data.task_id}: ${String(err)}`,
469
+ );
470
+ }
471
+ } else if (data.event_type === 'task_comment') {
461
472
  if (!data.source_id || !data.task_id) return;
462
473
  try {
463
474
  await this.consumeTypedDispatch(
@@ -469,9 +480,7 @@ export class ParallAgentGateway {
469
480
  data.actor_id,
470
481
  data.delivery_reason,
471
482
  ),
472
- () => {
473
- this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {});
474
- },
483
+ () => this.ackDispatchEvent(data.id, () => this.clearTypedDispatchDedupe(data)),
475
484
  );
476
485
  } catch (err) {
477
486
  this.opts.log?.error(
@@ -484,9 +493,7 @@ export class ParallAgentGateway {
484
493
  await this.consumeTypedDispatch(
485
494
  { dispatchEventId: data.id },
486
495
  () => this.handleWikiComment(data.source_id, data.actor_id, data.delivery_reason),
487
- () => {
488
- this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {});
489
- },
496
+ () => this.ackDispatchEvent(data.id, () => this.clearTypedDispatchDedupe(data)),
490
497
  );
491
498
  } catch (err) {
492
499
  this.opts.log?.error(
@@ -503,9 +510,7 @@ export class ParallAgentGateway {
503
510
  allowCreator: true,
504
511
  dispatchEventId,
505
512
  }),
506
- () => {
507
- this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {});
508
- },
513
+ () => this.ackDispatchEvent(data.id, () => this.clearTypedDispatchDedupe(data)),
509
514
  );
510
515
  } catch (err) {
511
516
  this.opts.log?.error(`task update dispatch failed for ${data.task_id}: ${String(err)}`);
@@ -516,9 +521,7 @@ export class ParallAgentGateway {
516
521
  await this.consumeTypedDispatch(
517
522
  { dispatchEventId: data.id },
518
523
  () => this.fetchAndHandleScheduleFire(data.source_id, data.actor_id),
519
- () => {
520
- this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {});
521
- },
524
+ () => this.ackDispatchEvent(data.id, () => this.clearTypedDispatchDedupe(data)),
522
525
  );
523
526
  } catch (err) {
524
527
  this.opts.log?.error(
@@ -531,9 +534,7 @@ export class ParallAgentGateway {
531
534
  await this.consumeTypedDispatch(
532
535
  { dispatchEventId: data.id },
533
536
  () => this.fetchAndHandleExternalTriggerRun(data.source_id),
534
- () => {
535
- this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {});
536
- },
537
+ () => this.ackDispatchEvent(data.id, () => this.clearTypedDispatchDedupe(data)),
537
538
  );
538
539
  } catch (err) {
539
540
  this.opts.log?.error(
@@ -546,9 +547,7 @@ export class ParallAgentGateway {
546
547
  await this.consumeTypedDispatch(
547
548
  { dispatchEventId: data.id },
548
549
  () => this.fetchAndHandleChannelMessage(data.source_id),
549
- () => {
550
- this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {});
551
- },
550
+ () => this.ackDispatchEvent(data.id, () => this.clearTypedDispatchDedupe(data)),
552
551
  );
553
552
  } catch (err) {
554
553
  this.opts.log?.error(
@@ -566,9 +565,7 @@ export class ParallAgentGateway {
566
565
  data.actor_id,
567
566
  data.chat_id ?? null,
568
567
  ),
569
- () => {
570
- this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {});
571
- },
568
+ () => this.ackDispatchEvent(data.id, () => this.clearTypedDispatchDedupe(data)),
572
569
  );
573
570
  } catch (err) {
574
571
  this.opts.log?.error(
@@ -589,14 +586,11 @@ export class ParallAgentGateway {
589
586
  } catch (err) {
590
587
  this.opts.log?.error(`message re-drive failed for ${data.source_id}: ${String(err)}`);
591
588
  }
592
- } else if (data.event_type !== 'message' && data.event_type !== 'task_assign') {
589
+ } else if (data.event_type !== 'message') {
593
590
  // Truly unknown event_type — log so a newly-added dispatch type
594
- // not yet wired here surfaces during runtime testing. "message"
595
- // and "task_assign" are deliberately excluded: dispatch.new
596
- // carries them too, but they are owned by dedicated WS handlers
597
- // (message.new, task.assigned) above and would otherwise spam
598
- // info-level logs for every inbound chat message / task
599
- // assignment on a busy agent.
591
+ // not yet wired here surfaces during runtime testing. "message" is
592
+ // deliberately excluded because its first delivery is owned by the
593
+ // message.new handler; recovered task_assign rows are handled above.
600
594
  this.opts.log?.info(
601
595
  `dispatch.new with unhandled event_type=${String(data.event_type)} (id=${data.id}) — no-op`,
602
596
  );
@@ -686,11 +680,59 @@ export class ParallAgentGateway {
686
680
  private consumeTypedDispatch(
687
681
  ref: { dispatchEventId?: string; sourceType?: string; sourceId?: string },
688
682
  run: (dispatchEventId?: string) => Promise<boolean>,
689
- ack: (dispatchEventId?: string) => void,
683
+ ack: (dispatchEventId?: string) => void | Promise<void>,
690
684
  ): Promise<void> {
691
685
  return consumeTypedDispatch(this.laneFlowHost(), ref, run, ack);
692
686
  }
693
687
 
688
+ // Typed completion must wait until the administrative ack has either
689
+ // committed or failed. Errors stay best-effort: a failed ack leaves the row
690
+ // received, so Complete releases and re-drives it safely.
691
+ private ackDispatchEvent(dispatchEventId: string, onFailure?: () => void): Promise<void> {
692
+ return this.opts.client.ackDispatchByID(this.opts.config.org_id, dispatchEventId).then(
693
+ () => undefined,
694
+ (err) => {
695
+ // Complete will return the still-received item to pending and publish
696
+ // an immediate hint. Free its hot-path claim first, otherwise that
697
+ // same-process re-drive is rejected forever by the local dedupe set.
698
+ onFailure?.();
699
+ this.opts.log?.warn(
700
+ `dispatch ack failed for ${dispatchEventId}, releasing for re-drive: ${String(err)}`,
701
+ );
702
+ },
703
+ );
704
+ }
705
+
706
+ private clearTypedDispatchDedupe(item: DispatchNewData): void {
707
+ switch (item.event_type) {
708
+ case 'task_assign':
709
+ case 'task_update':
710
+ if (item.task_id) {
711
+ const prefix = `${item.task_id}:`;
712
+ for (const key of this.dispatchedTasks) {
713
+ if (key.startsWith(prefix)) this.dispatchedTasks.delete(key);
714
+ }
715
+ }
716
+ break;
717
+ case 'task_comment':
718
+ case 'wiki_comment':
719
+ if (item.source_id) this.dispatchedTasks.delete(`comment:${item.source_id}`);
720
+ break;
721
+ case 'schedule.fire':
722
+ if (item.source_id) this.dispatchedTasks.delete(`schedule_run:${item.source_id}`);
723
+ break;
724
+ case 'external_trigger':
725
+ if (item.source_id) this.dispatchedTasks.delete(`external_trigger_run:${item.source_id}`);
726
+ break;
727
+ case 'channel_message':
728
+ if (item.source_id) this.dispatchedMessages.delete(`channel_message:${item.source_id}`);
729
+ break;
730
+ case 'approval_decided':
731
+ if (item.source_id) this.dispatchedTasks.delete(`approval:${item.source_id}`);
732
+ break;
733
+ }
734
+ }
735
+
694
736
  private buildDispatchContext(event: ParallEvent, sessionKey: string): DispatchContext {
695
737
  const binding = this.sessionBindings.get(sessionKey);
696
738
  return {
@@ -2023,6 +2065,25 @@ export class ParallAgentGateway {
2023
2065
  });
2024
2066
  }
2025
2067
 
2068
+ // A task assignment's first delivery rides task.assigned, but a typed lane
2069
+ // released without an effect is re-driven as dispatch.new. Reuse the exact
2070
+ // typed claim path used by catch-up so that recovery does not require a
2071
+ // runtime reconnect.
2072
+ private async handleTaskAssignmentRedrive(item: DispatchNewData): Promise<void> {
2073
+ if (!item.task_id) return;
2074
+ await this.consumeTypedDispatch(
2075
+ { dispatchEventId: item.id },
2076
+ (dispatchEventId) =>
2077
+ this.handleTaskDispatch(item.task_id ?? '', item.source_id ?? item.task_id ?? '', {
2078
+ dispatchEventId,
2079
+ }),
2080
+ (dispatchEventId) =>
2081
+ this.ackDispatchEvent(dispatchEventId ?? item.id, () =>
2082
+ this.clearTypedDispatchDedupe(item),
2083
+ ),
2084
+ );
2085
+ }
2086
+
2026
2087
  private consumeMessageWorkItem(item: {
2027
2088
  id: string;
2028
2089
  source_id: string;
@@ -2596,9 +2657,8 @@ export class ParallAgentGateway {
2596
2657
 
2597
2658
  processed++;
2598
2659
  try {
2599
- const ackItem = () => {
2600
- this.opts.client.ackDispatchByID(this.opts.config.org_id, item.id).catch(() => {});
2601
- };
2660
+ const ackItem = () =>
2661
+ this.ackDispatchEvent(item.id, () => this.clearTypedDispatchDedupe(item));
2602
2662
  if (item.event_type === 'task_assign' && item.task_id) {
2603
2663
  try {
2604
2664
  await this.consumeTypedDispatch(
@@ -133,10 +133,10 @@ export async function consumeTypedDispatch(
133
133
  host: LaneFlowHost,
134
134
  ref: { dispatchEventId?: string; sourceType?: string; sourceId?: string },
135
135
  run: (dispatchEventId?: string) => Promise<boolean>,
136
- ack: (dispatchEventId?: string) => void,
136
+ ack: (dispatchEventId?: string) => void | Promise<void>,
137
137
  ): Promise<void> {
138
138
  if (!host.laneLedger || host.ledgerDisabled) {
139
- if (await run(ref.dispatchEventId)) ack(ref.dispatchEventId);
139
+ if (await run(ref.dispatchEventId)) await ack(ref.dispatchEventId);
140
140
  return;
141
141
  }
142
142
  let lane: Awaited<ReturnType<LaneLedger['claimTyped']>>;
@@ -145,7 +145,7 @@ export async function consumeTypedDispatch(
145
145
  } catch (err) {
146
146
  if (err instanceof LedgerUnsupportedError) {
147
147
  host.disableLedger('claim endpoint missing');
148
- if (await run(ref.dispatchEventId)) ack(ref.dispatchEventId);
148
+ if (await run(ref.dispatchEventId)) await ack(ref.dispatchEventId);
149
149
  return;
150
150
  }
151
151
  throw err;
@@ -157,7 +157,10 @@ export async function consumeTypedDispatch(
157
157
  return;
158
158
  }
159
159
  try {
160
- if (await run(lane.typedDispatchEventId)) ack(lane.typedDispatchEventId);
160
+ // Ack must settle before Complete. A fire-and-forget ack races the lane
161
+ // release: Complete can return the still-received typed item to pending
162
+ // and publish a re-drive while its successful ack is still in flight.
163
+ if (await run(lane.typedDispatchEventId)) await ack(lane.typedDispatchEventId);
161
164
  } finally {
162
165
  // Release the occupancy row. A buffered dispatch may outlive this guard
163
166
  // (lane TTL) — acceptable at-least-once; the ledger's resolution paths