@parall/parall 1.44.0 → 1.45.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.
@@ -26486,9 +26486,6 @@ function laneContextFilePath(contextDir, targetUri, threadRootId) {
26486
26486
  return path.join(contextDir, `${laneKeyForTarget(targetUri, threadRootId)}.json`);
26487
26487
  }
26488
26488
 
26489
- // ../agent-core/dist/lane-ledger.js
26490
- import * as fs from "node:fs";
26491
-
26492
26489
  // ../sdk/dist/types.js
26493
26490
  var MENTION_ALL_USER_ID = "all";
26494
26491
 
@@ -29100,6 +29097,7 @@ var ParallWs = class {
29100
29097
  };
29101
29098
 
29102
29099
  // ../agent-core/dist/lane-ledger.js
29100
+ import * as fs from "node:fs";
29103
29101
  var LedgerUnsupportedError = class extends Error {
29104
29102
  };
29105
29103
  function isStaleLane(err) {
@@ -29383,6 +29381,20 @@ var LaneLedger = class {
29383
29381
  this.lanes.set(lane.laneKey, lane);
29384
29382
  return lane;
29385
29383
  }
29384
+ /**
29385
+ * Drop a lane's local record without any server call — for a typed lane
29386
+ * whose member the by-id complete just resolved (the server dropped the
29387
+ * vacated lane row in the same transaction). Calling completeIfIdle
29388
+ * instead would fire a lane-form Complete at a lane that no longer exists
29389
+ * and burn an RPC on the guaranteed STALE_LANE answer.
29390
+ */
29391
+ dropLocal(laneKey) {
29392
+ const lane = this.lanes.get(laneKey);
29393
+ if (!lane)
29394
+ return;
29395
+ this.lanes.delete(laneKey);
29396
+ this.removeLaneContext(lane);
29397
+ }
29386
29398
  /**
29387
29399
  * Remove the per-lane context file (and its CLI sidecar) when the lane
29388
29400
  * ends. A leftover file would make a later cross-context send to the same
@@ -29462,10 +29474,104 @@ async function dispatchLaneGroup(host, opts) {
29462
29474
  await ledger.completeIfIdle(lane.laneKey, pendingInjections || opts.hasMoreLocal());
29463
29475
  return "dispatched";
29464
29476
  }
29477
+ function typedLedgerEventIds(host, events) {
29478
+ if (!host.laneLedger || host.ledgerDisabled)
29479
+ return null;
29480
+ const ids = [];
29481
+ for (const ev of events) {
29482
+ if (!ev.dispatchEventId || host.usesLaneLedger(ev))
29483
+ return null;
29484
+ ids.push(ev.dispatchEventId);
29485
+ }
29486
+ return ids.length > 0 ? ids : null;
29487
+ }
29488
+ function isByIDCompleteUnsupported(err) {
29489
+ return err instanceof ApiError && err.status === 400;
29490
+ }
29491
+ async function resolveDispatchByID(host, dispatchEventId, lane) {
29492
+ try {
29493
+ await host.opts.client.completeDispatch(host.opts.config.org_id, {
29494
+ dispatch_event_id: dispatchEventId,
29495
+ ...lane ? { lane } : {},
29496
+ turn_outcome: "ok"
29497
+ });
29498
+ return "ok";
29499
+ } catch (err) {
29500
+ if (err instanceof ApiError && err.status === 409)
29501
+ return "stale";
29502
+ if (isByIDCompleteUnsupported(err))
29503
+ return "unsupported";
29504
+ host.opts.log?.warn(`by-id complete failed for ${dispatchEventId} \u2014 leaving for re-drive: ${String(err)}`);
29505
+ return "failed";
29506
+ }
29507
+ }
29508
+ function clearTypedDedupeForEvent(host, event) {
29509
+ const sourceId = event.ackSourceId;
29510
+ if (!sourceId)
29511
+ return;
29512
+ switch (event.ackSourceType) {
29513
+ case "task_activity": {
29514
+ const prefix = `${event.targetId}:`;
29515
+ for (const key of host.dispatchedTasks) {
29516
+ if (key.startsWith(prefix))
29517
+ host.dispatchedTasks.delete(key);
29518
+ }
29519
+ break;
29520
+ }
29521
+ case "comment":
29522
+ host.dispatchedTasks.delete(`comment:${sourceId}`);
29523
+ break;
29524
+ case "schedule_run":
29525
+ host.dispatchedTasks.delete(`schedule_run:${sourceId}`);
29526
+ break;
29527
+ case "external_trigger_run":
29528
+ host.dispatchedTasks.delete(`external_trigger_run:${sourceId}`);
29529
+ break;
29530
+ case "channel_message":
29531
+ host.dispatchedMessages.delete(`channel_message:${sourceId}`);
29532
+ break;
29533
+ case "approval":
29534
+ host.dispatchedTasks.delete(`approval:${sourceId}`);
29535
+ break;
29536
+ }
29537
+ }
29538
+ async function settleDrainedTypedGroup(host, events, ids, turnErrored) {
29539
+ if (turnErrored) {
29540
+ host.opts.log?.info(`buffered typed turn for ${events[events.length - 1]?.messageId} surfaced a runtime error \u2014 leaving for re-drive`);
29541
+ for (const event of events)
29542
+ clearTypedDedupeForEvent(host, event);
29543
+ return;
29544
+ }
29545
+ const legacyAckFrom = async (start) => {
29546
+ for (const [j, id] of ids.slice(start).entries()) {
29547
+ try {
29548
+ await host.opts.client.ackDispatchByID(host.opts.config.org_id, id);
29549
+ } catch {
29550
+ clearTypedDedupeForEvent(host, events[start + j]);
29551
+ }
29552
+ }
29553
+ };
29554
+ if (host.typedByIdCompleteUnsupported) {
29555
+ await legacyAckFrom(0);
29556
+ return;
29557
+ }
29558
+ for (const [i, id] of ids.entries()) {
29559
+ const outcome = await resolveDispatchByID(host, id);
29560
+ if (outcome === "unsupported") {
29561
+ host.typedByIdCompleteUnsupported = true;
29562
+ host.opts.log?.warn("server predates the by-id dispatch complete \u2014 falling back to legacy typed acks");
29563
+ await legacyAckFrom(i);
29564
+ return;
29565
+ }
29566
+ if (outcome !== "ok") {
29567
+ clearTypedDedupeForEvent(host, events[i]);
29568
+ }
29569
+ }
29570
+ }
29465
29571
  var TYPED_BACKOFF_BASE_MS = 2e3;
29466
29572
  var TYPED_BACKOFF_CAP_MS = 5 * 6e4;
29467
29573
  var TYPED_BACKOFF_MAP_CAP = 512;
29468
- async function consumeTypedDispatch(host, ref, run, ack) {
29574
+ async function consumeTypedDispatch(host, ref, run, hooks) {
29469
29575
  const backoffKey = ref.dispatchEventId ?? `${ref.sourceType}:${ref.sourceId}`;
29470
29576
  const armed = host.typedRedriveBackoff.get(backoffKey);
29471
29577
  if (armed) {
@@ -29481,8 +29587,8 @@ async function consumeTypedDispatch(host, ref, run, ack) {
29481
29587
  return;
29482
29588
  }
29483
29589
  const settleAck = (ackResult) => ackResult !== false;
29484
- const settle = (acked2) => {
29485
- if (acked2) {
29590
+ const settle = (resolved2) => {
29591
+ if (resolved2) {
29486
29592
  host.typedRedriveBackoff.delete(backoffKey);
29487
29593
  return;
29488
29594
  }
@@ -29496,13 +29602,13 @@ async function consumeTypedDispatch(host, ref, run, ack) {
29496
29602
  host.typedRedriveBackoff.set(backoffKey, { failures, until: Date.now() + backoffMs });
29497
29603
  };
29498
29604
  const runLegacy = async () => {
29499
- let acked2 = false;
29605
+ let acked = false;
29500
29606
  try {
29501
29607
  if (await run(ref.dispatchEventId)) {
29502
- acked2 = settleAck(await ack(ref.dispatchEventId));
29608
+ acked = settleAck(await hooks.legacyAck(ref.dispatchEventId));
29503
29609
  }
29504
29610
  } finally {
29505
- settle(acked2);
29611
+ settle(acked);
29506
29612
  }
29507
29613
  };
29508
29614
  if (!host.laneLedger || host.ledgerDisabled) {
@@ -29524,15 +29630,39 @@ async function consumeTypedDispatch(host, ref, run, ack) {
29524
29630
  host.opts.log?.info(`typed dispatch ${ref.dispatchEventId ?? `${ref.sourceType}:${ref.sourceId}`} not claimable (held elsewhere or already resolved) \u2014 skipping`);
29525
29631
  return;
29526
29632
  }
29527
- let acked = false;
29633
+ let resolved = false;
29634
+ let viaLegacyAck = false;
29528
29635
  try {
29529
29636
  if (await run(lane.typedDispatchEventId)) {
29530
- acked = settleAck(await ack(lane.typedDispatchEventId));
29637
+ if (host.typedByIdCompleteUnsupported || !lane.typedDispatchEventId) {
29638
+ viaLegacyAck = true;
29639
+ resolved = settleAck(await hooks.legacyAck(lane.typedDispatchEventId));
29640
+ } else {
29641
+ const outcome = await resolveDispatchByID(host, lane.typedDispatchEventId, lane.lane);
29642
+ if (outcome === "unsupported") {
29643
+ host.typedByIdCompleteUnsupported = true;
29644
+ host.opts.log?.warn("server predates the by-id dispatch complete \u2014 falling back to the legacy typed ack");
29645
+ viaLegacyAck = true;
29646
+ resolved = settleAck(await hooks.legacyAck(lane.typedDispatchEventId));
29647
+ } else if (outcome === "stale") {
29648
+ hooks.clearDedupe?.();
29649
+ resolved = true;
29650
+ } else {
29651
+ resolved = outcome === "ok";
29652
+ if (!resolved) {
29653
+ hooks.clearDedupe?.();
29654
+ }
29655
+ }
29656
+ }
29531
29657
  }
29532
29658
  } finally {
29533
- settle(acked);
29534
- await host.laneLedger.completeIfIdle(lane.laneKey, false).catch(() => {
29535
- });
29659
+ settle(resolved);
29660
+ if (resolved && !viaLegacyAck) {
29661
+ host.laneLedger.dropLocal(lane.laneKey);
29662
+ } else {
29663
+ await host.laneLedger.completeIfIdle(lane.laneKey, false).catch(() => {
29664
+ });
29665
+ }
29536
29666
  }
29537
29667
  }
29538
29668
  async function consumeMessageWorkItem(host, item) {
@@ -29541,6 +29671,16 @@ async function consumeMessageWorkItem(host, item) {
29541
29671
  if (!host.tryClaimMessage(item.source_id))
29542
29672
  return;
29543
29673
  const ackItem = () => {
29674
+ if (host.laneLedger && !host.ledgerDisabled && !host.typedByIdCompleteUnsupported) {
29675
+ void resolveDispatchByID(host, item.id).then((outcome) => {
29676
+ if (outcome === "unsupported") {
29677
+ host.typedByIdCompleteUnsupported = true;
29678
+ host.opts.client.ackDispatchByID(host.opts.config.org_id, item.id).catch(() => {
29679
+ });
29680
+ }
29681
+ });
29682
+ return;
29683
+ }
29544
29684
  host.opts.client.ackDispatchByID(host.opts.config.org_id, item.id).catch(() => {
29545
29685
  });
29546
29686
  };
@@ -30661,20 +30801,23 @@ var ParallAgentGateway = class {
30661
30801
  if (data.status !== "todo" && data.status !== "in_progress")
30662
30802
  return;
30663
30803
  try {
30664
- await this.consumeTypedDispatch(data.dispatch_event_id ? { dispatchEventId: data.dispatch_event_id } : { sourceType: "task_activity", sourceId: data.id }, (dispatchEventId) => this.handleTaskAssignment(data, data.id, dispatchEventId), (dispatchEventId) => {
30665
- if (dispatchEventId) {
30666
- return this.ackDispatchEvent(dispatchEventId, () => {
30804
+ await this.consumeTypedDispatch(data.dispatch_event_id ? { dispatchEventId: data.dispatch_event_id } : { sourceType: "task_activity", sourceId: data.id }, (dispatchEventId) => this.handleTaskAssignment(data, data.id, dispatchEventId), {
30805
+ legacyAck: (dispatchEventId) => {
30806
+ if (dispatchEventId) {
30807
+ return this.ackDispatchEvent(dispatchEventId, () => {
30808
+ this.dispatchedTasks.delete(`${data.id}:${data.updated_at}`);
30809
+ });
30810
+ }
30811
+ return this.opts.client.ackDispatch(this.opts.config.org_id, {
30812
+ source_type: "task_activity",
30813
+ source_id: data.id
30814
+ }).then(() => true, (err) => {
30667
30815
  this.dispatchedTasks.delete(`${data.id}:${data.updated_at}`);
30816
+ this.opts.log?.warn(`dispatch ack failed for task ${data.id}, releasing for re-drive: ${String(err)}`);
30817
+ return false;
30668
30818
  });
30669
- }
30670
- return this.opts.client.ackDispatch(this.opts.config.org_id, {
30671
- source_type: "task_activity",
30672
- source_id: data.id
30673
- }).then(() => true, (err) => {
30674
- this.dispatchedTasks.delete(`${data.id}:${data.updated_at}`);
30675
- this.opts.log?.warn(`dispatch ack failed for task ${data.id}, releasing for re-drive: ${String(err)}`);
30676
- return false;
30677
- });
30819
+ },
30820
+ clearDedupe: () => this.dispatchedTasks.delete(`${data.id}:${data.updated_at}`)
30678
30821
  });
30679
30822
  } catch (err) {
30680
30823
  this.opts.log?.error(`task dispatch failed for ${data.id}: ${String(err)}`);
@@ -30693,7 +30836,10 @@ var ParallAgentGateway = class {
30693
30836
  if (!data.source_id || !data.task_id)
30694
30837
  return;
30695
30838
  try {
30696
- 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)));
30839
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, (dispatchEventId) => this.handleTaskComment(data.source_id, data.task_id ?? "", data.actor_id, data.delivery_reason, dispatchEventId), {
30840
+ legacyAck: () => this.ackDispatchEvent(data.id, () => this.clearTypedDispatchDedupe(data)),
30841
+ clearDedupe: () => this.clearTypedDispatchDedupe(data)
30842
+ });
30697
30843
  } catch (err) {
30698
30844
  this.opts.log?.error(`task comment dispatch failed for ${data.source_id}: ${String(err)}`);
30699
30845
  }
@@ -30701,7 +30847,10 @@ var ParallAgentGateway = class {
30701
30847
  if (!data.source_id)
30702
30848
  return;
30703
30849
  try {
30704
- await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.handleWikiComment(data.source_id, data.actor_id, data.delivery_reason), () => this.ackDispatchEvent(data.id, () => this.clearTypedDispatchDedupe(data)));
30850
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, (dispatchEventId) => this.handleWikiComment(data.source_id, data.actor_id, data.delivery_reason, dispatchEventId), {
30851
+ legacyAck: () => this.ackDispatchEvent(data.id, () => this.clearTypedDispatchDedupe(data)),
30852
+ clearDedupe: () => this.clearTypedDispatchDedupe(data)
30853
+ });
30705
30854
  } catch (err) {
30706
30855
  this.opts.log?.error(`wiki comment dispatch failed for ${data.source_id}: ${String(err)}`);
30707
30856
  }
@@ -30712,7 +30861,10 @@ var ParallAgentGateway = class {
30712
30861
  await this.consumeTypedDispatch({ dispatchEventId: data.id }, (dispatchEventId) => this.handleTaskDispatch(data.task_id ?? "", data.source_id ?? data.task_id ?? "", {
30713
30862
  allowCreator: true,
30714
30863
  dispatchEventId
30715
- }), () => this.ackDispatchEvent(data.id, () => this.clearTypedDispatchDedupe(data)));
30864
+ }), {
30865
+ legacyAck: () => this.ackDispatchEvent(data.id, () => this.clearTypedDispatchDedupe(data)),
30866
+ clearDedupe: () => this.clearTypedDispatchDedupe(data)
30867
+ });
30716
30868
  } catch (err) {
30717
30869
  this.opts.log?.error(`task update dispatch failed for ${data.task_id}: ${String(err)}`);
30718
30870
  }
@@ -30720,7 +30872,10 @@ var ParallAgentGateway = class {
30720
30872
  if (!data.source_id)
30721
30873
  return;
30722
30874
  try {
30723
- await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.fetchAndHandleScheduleFire(data.source_id, data.actor_id), () => this.ackDispatchEvent(data.id, () => this.clearTypedDispatchDedupe(data)));
30875
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, (dispatchEventId) => this.fetchAndHandleScheduleFire(data.source_id, data.actor_id, dispatchEventId), {
30876
+ legacyAck: () => this.ackDispatchEvent(data.id, () => this.clearTypedDispatchDedupe(data)),
30877
+ clearDedupe: () => this.clearTypedDispatchDedupe(data)
30878
+ });
30724
30879
  } catch (err) {
30725
30880
  this.opts.log?.error(`schedule fire dispatch failed for ${data.source_id}: ${String(err)}`);
30726
30881
  }
@@ -30728,7 +30883,10 @@ var ParallAgentGateway = class {
30728
30883
  if (!data.source_id)
30729
30884
  return;
30730
30885
  try {
30731
- await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.fetchAndHandleExternalTriggerRun(data.source_id), () => this.ackDispatchEvent(data.id, () => this.clearTypedDispatchDedupe(data)));
30886
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, (dispatchEventId) => this.fetchAndHandleExternalTriggerRun(data.source_id, dispatchEventId), {
30887
+ legacyAck: () => this.ackDispatchEvent(data.id, () => this.clearTypedDispatchDedupe(data)),
30888
+ clearDedupe: () => this.clearTypedDispatchDedupe(data)
30889
+ });
30732
30890
  } catch (err) {
30733
30891
  this.opts.log?.error(`external trigger dispatch failed for ${data.source_id}: ${String(err)}`);
30734
30892
  }
@@ -30736,7 +30894,10 @@ var ParallAgentGateway = class {
30736
30894
  if (!data.source_id)
30737
30895
  return;
30738
30896
  try {
30739
- await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.fetchAndHandleChannelMessage(data.source_id), () => this.ackDispatchEvent(data.id, () => this.clearTypedDispatchDedupe(data)));
30897
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, (dispatchEventId) => this.fetchAndHandleChannelMessage(data.source_id, dispatchEventId), {
30898
+ legacyAck: () => this.ackDispatchEvent(data.id, () => this.clearTypedDispatchDedupe(data)),
30899
+ clearDedupe: () => this.clearTypedDispatchDedupe(data)
30900
+ });
30740
30901
  } catch (err) {
30741
30902
  this.opts.log?.error(`channel message dispatch failed for ${data.source_id}: ${String(err)}`);
30742
30903
  }
@@ -30744,7 +30905,10 @@ var ParallAgentGateway = class {
30744
30905
  if (!data.source_id)
30745
30906
  return;
30746
30907
  try {
30747
- 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)));
30908
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, (dispatchEventId) => this.fetchAndHandleApprovalDecided(data.source_id, data.actor_id, data.chat_id ?? null, dispatchEventId), {
30909
+ legacyAck: () => this.ackDispatchEvent(data.id, () => this.clearTypedDispatchDedupe(data)),
30910
+ clearDedupe: () => this.clearTypedDispatchDedupe(data)
30911
+ });
30748
30912
  } catch (err) {
30749
30913
  this.opts.log?.error(`approval decided dispatch failed for ${data.source_id}: ${String(err)}`);
30750
30914
  }
@@ -30827,6 +30991,20 @@ var ParallAgentGateway = class {
30827
30991
  source_id: sourceId
30828
30992
  });
30829
30993
  }
30994
+ /**
30995
+ * Sticky: the server answered a by-id complete with 400 (predates the
30996
+ * form). Typed resolution falls back to the legacy ack for the rest of
30997
+ * the process lifetime.
30998
+ */
30999
+ typedByIdCompleteUnsupported = false;
31000
+ // Typed-face ledger helpers live in gateway-lane-flow.ts; thin delegates
31001
+ // keep the site code and tests on the class surface.
31002
+ typedLedgerEventIds(events) {
31003
+ return typedLedgerEventIds(this.laneFlowHost(), events);
31004
+ }
31005
+ resolveDispatchByID(dispatchEventId, lane) {
31006
+ return resolveDispatchByID(this.laneFlowHost(), dispatchEventId, lane);
31007
+ }
30830
31008
  /** True when this event's lifecycle is owned by the dispatch lane ledger. */
30831
31009
  usesLaneLedger(event) {
30832
31010
  return this.laneLedger != null && !this.ledgerDisabled && this.laneLedger.handles(event);
@@ -30857,15 +31035,16 @@ var ParallAgentGateway = class {
30857
31035
  dispatchLaneGroup(opts) {
30858
31036
  return dispatchLaneGroup(this.laneFlowHost(), opts);
30859
31037
  }
30860
- consumeTypedDispatch(ref, run, ack) {
30861
- return consumeTypedDispatch(this.laneFlowHost(), ref, run, ack);
31038
+ consumeTypedDispatch(ref, run, hooks) {
31039
+ return consumeTypedDispatch(this.laneFlowHost(), ref, run, hooks);
30862
31040
  }
30863
- // Typed completion must wait until the administrative ack has either
30864
- // committed or failed. Errors stay best-effort: a failed ack leaves the row
30865
- // received, so Complete releases and re-drives it safely. The boolean
30866
- // outcome feeds the typed-consume backoff an ack that failed must count
30867
- // as a failed consume, or an ack outage would clear the backoff entry and
30868
- // let the release re-drive spin at wire speed.
31041
+ // Legacy administrative ack (ledger-disabled fallback only). Typed
31042
+ // completion must wait until the ack has either committed or failed.
31043
+ // Errors stay best-effort: a failed ack leaves the row received, so
31044
+ // Complete releases and re-drives it safely. The boolean outcome feeds the
31045
+ // typed-consume backoff an ack that failed must count as a failed
31046
+ // consume, or an ack outage would clear the backoff entry and let the
31047
+ // release re-drive spin at wire speed.
30869
31048
  ackDispatchEvent(dispatchEventId, onFailure) {
30870
31049
  return this.opts.client.ackDispatchByID(this.opts.config.org_id, dispatchEventId).then(() => true, (err) => {
30871
31050
  onFailure?.();
@@ -30873,6 +31052,9 @@ var ParallAgentGateway = class {
30873
31052
  return false;
30874
31053
  });
30875
31054
  }
31055
+ // PARITY: this switch and gateway-lane-flow's clearTypedDedupeForEvent must
31056
+ // handle the same typed source families — extend BOTH when adding a typed
31057
+ // event type (same dedupe entries, keyed from different event shapes).
30876
31058
  clearTypedDispatchDedupe(item) {
30877
31059
  switch (item.event_type) {
30878
31060
  case "task_assign":
@@ -31446,6 +31628,12 @@ var ParallAgentGateway = class {
31446
31628
  dispatched = outcome === "dispatched";
31447
31629
  } else {
31448
31630
  dispatched = await this.runDispatch(last, fork.fork.sessionKey, buildForkScopePrefix(last) + buildEventBody(last), earlier, batchText);
31631
+ if (dispatched && this.typedLedgerEventIds(events) && this.consumeTurnError(fork.fork.sessionKey)) {
31632
+ this.opts.log?.info(`typed fork turn for ${last.messageId} surfaced a runtime error \u2014 releasing for retry`);
31633
+ for (const item of items)
31634
+ item.resolve(false);
31635
+ continue;
31636
+ }
31449
31637
  }
31450
31638
  if (!dispatched) {
31451
31639
  for (const item of items) {
@@ -31595,13 +31783,16 @@ var ParallAgentGateway = class {
31595
31783
  }
31596
31784
  continue;
31597
31785
  }
31598
- try {
31599
- await this.emitDispatchReceived(event);
31600
- } catch (err) {
31601
- this.opts.log?.warn?.(`mark-received failed for buffered dispatch, leaving unacked for retry: ${String(err)}`);
31602
- this.dispatchState.mainBuffer.unshift(...events);
31603
- this.dispatchState.pendingForkResults.unshift(...pendingFork);
31604
- break;
31786
+ const typedRefs = this.typedLedgerEventIds(events);
31787
+ if (!typedRefs) {
31788
+ try {
31789
+ await this.emitDispatchReceived(event);
31790
+ } catch (err) {
31791
+ this.opts.log?.warn?.(`mark-received failed for buffered dispatch, leaving unacked for retry: ${String(err)}`);
31792
+ this.dispatchState.mainBuffer.unshift(...events);
31793
+ this.dispatchState.pendingForkResults.unshift(...pendingFork);
31794
+ break;
31795
+ }
31605
31796
  }
31606
31797
  const dispatched = await this.runDispatch(event, this.opts.runtimeKey, forkPrefix + buildEventBody(event), earlier);
31607
31798
  if (!dispatched) {
@@ -31609,6 +31800,10 @@ var ParallAgentGateway = class {
31609
31800
  this.dispatchState.pendingForkResults.unshift(...pendingFork);
31610
31801
  break;
31611
31802
  }
31803
+ if (typedRefs) {
31804
+ await settleDrainedTypedGroup(this.laneFlowHost(), events, typedRefs, this.consumeTurnError(this.opts.runtimeKey));
31805
+ continue;
31806
+ }
31612
31807
  for (const bufferedEvent of events) {
31613
31808
  const sourceType = bufferedEvent.ackSourceType ?? (bufferedEvent.type === "task" ? "task_activity" : "message");
31614
31809
  const sourceId = bufferedEvent.ackSourceId ?? bufferedEvent.messageId;
@@ -31668,20 +31863,27 @@ var ParallAgentGateway = class {
31668
31863
  }
31669
31864
  return outcome === "dispatched";
31670
31865
  }
31671
- try {
31672
- await this.emitDispatchReceived(event);
31673
- } catch (err) {
31674
- this.opts.log?.warn?.(`mark-received failed, leaving unacked for retry: ${String(err)}`);
31675
- this.dispatchState.mainDispatching = false;
31676
- this.dispatchState.mainCurrentTargetId = void 0;
31677
- this.mainCurrentGroupKey = void 0;
31678
- this.dispatchState.mainPreDispatchBranchPoint = void 0;
31679
- this.dispatchState.pendingForkResults.unshift(...pendingFork);
31680
- return false;
31866
+ const typedRefs = this.typedLedgerEventIds([event]);
31867
+ if (!typedRefs) {
31868
+ try {
31869
+ await this.emitDispatchReceived(event);
31870
+ } catch (err) {
31871
+ this.opts.log?.warn?.(`mark-received failed, leaving unacked for retry: ${String(err)}`);
31872
+ this.dispatchState.mainDispatching = false;
31873
+ this.dispatchState.mainCurrentTargetId = void 0;
31874
+ this.mainCurrentGroupKey = void 0;
31875
+ this.dispatchState.mainPreDispatchBranchPoint = void 0;
31876
+ this.dispatchState.pendingForkResults.unshift(...pendingFork);
31877
+ return false;
31878
+ }
31681
31879
  }
31682
31880
  let dispatched = false;
31683
31881
  try {
31684
31882
  dispatched = await this.runDispatch(event, this.opts.runtimeKey, forkPrefix + buildEventBody(event));
31883
+ if (dispatched && typedRefs && this.consumeTurnError(this.opts.runtimeKey)) {
31884
+ this.opts.log?.info(`typed dispatch turn for ${event.messageId} surfaced a runtime error \u2014 releasing for retry`);
31885
+ dispatched = false;
31886
+ }
31685
31887
  if (!dispatched) {
31686
31888
  this.dispatchState.pendingForkResults.unshift(...pendingFork);
31687
31889
  }
@@ -31723,7 +31925,7 @@ var ParallAgentGateway = class {
31723
31925
  this.dispatchState.mainBuffer.push(event);
31724
31926
  return false;
31725
31927
  }
31726
- if (!this.usesLaneLedger(event)) {
31928
+ if (!this.usesLaneLedger(event) && !this.typedLedgerEventIds([event])) {
31727
31929
  try {
31728
31930
  await this.emitDispatchReceived(event);
31729
31931
  } catch (err) {
@@ -31904,7 +32106,10 @@ var ParallAgentGateway = class {
31904
32106
  return;
31905
32107
  await this.consumeTypedDispatch({ dispatchEventId: item.id }, (dispatchEventId) => this.handleTaskDispatch(item.task_id ?? "", item.source_id ?? item.task_id ?? "", {
31906
32108
  dispatchEventId
31907
- }), (dispatchEventId) => this.ackDispatchEvent(dispatchEventId ?? item.id, () => this.clearTypedDispatchDedupe(item)));
32109
+ }), {
32110
+ legacyAck: (dispatchEventId) => this.ackDispatchEvent(dispatchEventId ?? item.id, () => this.clearTypedDispatchDedupe(item)),
32111
+ clearDedupe: () => this.clearTypedDispatchDedupe(item)
32112
+ });
31908
32113
  }
31909
32114
  consumeMessageWorkItem(item) {
31910
32115
  return consumeMessageWorkItem(this.laneFlowHost(), item);
@@ -31965,7 +32170,7 @@ var ParallAgentGateway = class {
31965
32170
  }
31966
32171
  return this.handleTaskAssignment(task, ackSourceId, opts.dispatchEventId);
31967
32172
  }
31968
- async handleTaskComment(commentId, taskId, actorId, deliveryReason) {
32173
+ async handleTaskComment(commentId, taskId, actorId, deliveryReason, dispatchEventId) {
31969
32174
  if (this.shuttingDown)
31970
32175
  return false;
31971
32176
  const dedupeKey = `comment:${commentId}`;
@@ -32021,7 +32226,8 @@ var ParallAgentGateway = class {
32021
32226
  body: parts.join("\n"),
32022
32227
  deliveryReason: deliveryReason ?? void 0,
32023
32228
  ackSourceType: "comment",
32024
- ackSourceId: commentId
32229
+ ackSourceId: commentId,
32230
+ dispatchEventId
32025
32231
  };
32026
32232
  let dispatched;
32027
32233
  try {
@@ -32035,7 +32241,7 @@ var ParallAgentGateway = class {
32035
32241
  }
32036
32242
  return dispatched;
32037
32243
  }
32038
- async handleWikiComment(commentId, actorId, deliveryReason) {
32244
+ async handleWikiComment(commentId, actorId, deliveryReason, dispatchEventId) {
32039
32245
  if (this.shuttingDown)
32040
32246
  return false;
32041
32247
  const dedupeKey = `comment:${commentId}`;
@@ -32081,7 +32287,8 @@ var ParallAgentGateway = class {
32081
32287
  deliveryReason: deliveryReason ?? void 0,
32082
32288
  replyTargetUri: comment.target_uri,
32083
32289
  ackSourceType: "comment",
32084
- ackSourceId: commentId
32290
+ ackSourceId: commentId,
32291
+ dispatchEventId
32085
32292
  };
32086
32293
  let dispatched;
32087
32294
  try {
@@ -32103,7 +32310,7 @@ var ParallAgentGateway = class {
32103
32310
  * access to already-delivered run snapshots, and the runtime must not crash
32104
32311
  * or retry forever in that case.
32105
32312
  */
32106
- async fetchAndHandleScheduleFire(runId, actorId) {
32313
+ async fetchAndHandleScheduleFire(runId, actorId, dispatchEventId) {
32107
32314
  let run = null;
32108
32315
  try {
32109
32316
  run = await this.opts.client.getScheduleRun(this.opts.config.org_id, runId);
@@ -32118,9 +32325,9 @@ var ParallAgentGateway = class {
32118
32325
  }
32119
32326
  if (!run)
32120
32327
  return true;
32121
- return this.handleScheduleFire(run, actorId);
32328
+ return this.handleScheduleFire(run, actorId, dispatchEventId);
32122
32329
  }
32123
- async handleScheduleFire(run, actorId) {
32330
+ async handleScheduleFire(run, actorId, dispatchEventId) {
32124
32331
  if (this.shuttingDown)
32125
32332
  return false;
32126
32333
  const dedupeKey = `schedule_run:${run.id}`;
@@ -32143,7 +32350,8 @@ var ParallAgentGateway = class {
32143
32350
  scheduledFireAt: run.scheduled_fire_at,
32144
32351
  attachedUri: run.fired_attached_uri ?? void 0,
32145
32352
  ackSourceType: "schedule_run",
32146
- ackSourceId: run.id
32353
+ ackSourceId: run.id,
32354
+ dispatchEventId
32147
32355
  };
32148
32356
  let dispatched;
32149
32357
  try {
@@ -32157,7 +32365,7 @@ var ParallAgentGateway = class {
32157
32365
  }
32158
32366
  return dispatched;
32159
32367
  }
32160
- async fetchAndHandleExternalTriggerRun(runId) {
32368
+ async fetchAndHandleExternalTriggerRun(runId, dispatchEventId) {
32161
32369
  let run = null;
32162
32370
  try {
32163
32371
  run = await this.opts.client.getExternalTriggerRun(this.opts.config.org_id, runId);
@@ -32172,14 +32380,14 @@ var ParallAgentGateway = class {
32172
32380
  }
32173
32381
  if (!run)
32174
32382
  return true;
32175
- return this.handleExternalTriggerRun(run);
32383
+ return this.handleExternalTriggerRun(run, dispatchEventId);
32176
32384
  }
32177
32385
  // fetchAndHandleChannelMessage resolves a channel_message dispatch to its
32178
32386
  // durable ChannelMessage + conversation and hands it to the inbound
32179
32387
  // pipeline. targetId = the ChannelConversation id, so per-conversation
32180
32388
  // multi-turn continuity rides the same per-target session mechanics as
32181
32389
  // chats. Design: docs/engineering-design/external-im-channel-design.md.
32182
- async fetchAndHandleChannelMessage(messageId) {
32390
+ async fetchAndHandleChannelMessage(messageId, dispatchEventId) {
32183
32391
  if (this.shuttingDown)
32184
32392
  return false;
32185
32393
  const claimKey = `channel_message:${messageId}`;
@@ -32232,7 +32440,8 @@ var ParallAgentGateway = class {
32232
32440
  channelExternalMessageId: msg.external_message_id,
32233
32441
  channelCliCapable: cliCapable,
32234
32442
  ackSourceType: "channel_message",
32235
- ackSourceId: msg.id
32443
+ ackSourceId: msg.id,
32444
+ dispatchEventId
32236
32445
  };
32237
32446
  let dispatched;
32238
32447
  try {
@@ -32246,7 +32455,7 @@ var ParallAgentGateway = class {
32246
32455
  }
32247
32456
  return dispatched;
32248
32457
  }
32249
- async handleExternalTriggerRun(run) {
32458
+ async handleExternalTriggerRun(run, dispatchEventId) {
32250
32459
  if (this.shuttingDown)
32251
32460
  return false;
32252
32461
  const dedupeKey = `external_trigger_run:${run.id}`;
@@ -32271,7 +32480,8 @@ var ParallAgentGateway = class {
32271
32480
  externalIngressEventType: run.ingress_event_type || void 0,
32272
32481
  attachedUri,
32273
32482
  ackSourceType: "external_trigger_run",
32274
- ackSourceId: run.id
32483
+ ackSourceId: run.id,
32484
+ dispatchEventId
32275
32485
  };
32276
32486
  let dispatched;
32277
32487
  try {
@@ -32285,7 +32495,7 @@ var ParallAgentGateway = class {
32285
32495
  }
32286
32496
  return dispatched;
32287
32497
  }
32288
- async fetchAndHandleApprovalDecided(approvalId, actorId, chatId) {
32498
+ async fetchAndHandleApprovalDecided(approvalId, actorId, chatId, dispatchEventId) {
32289
32499
  let approval = null;
32290
32500
  try {
32291
32501
  approval = await this.opts.client.getApproval(approvalId);
@@ -32316,7 +32526,12 @@ var ParallAgentGateway = class {
32316
32526
  senderId: actorId ?? approval.decided_by ?? "system",
32317
32527
  senderName: "approver",
32318
32528
  messageId: approval.id,
32319
- body
32529
+ body,
32530
+ // The WorkItem's real source pair. Without it the legacy fallback
32531
+ // guessed ('message', approval_id) — a pair no row matches.
32532
+ ackSourceType: "approval",
32533
+ ackSourceId: approval.id,
32534
+ dispatchEventId
32320
32535
  };
32321
32536
  let dispatched;
32322
32537
  try {
@@ -32363,7 +32578,17 @@ var ParallAgentGateway = class {
32363
32578
  break;
32364
32579
  if (overflowMode && processed >= CATCHUP_MAX) {
32365
32580
  try {
32366
- await this.opts.client.ackDispatchByID(this.opts.config.org_id, item.id);
32581
+ if (this.laneLedger && !this.ledgerDisabled && !this.typedByIdCompleteUnsupported) {
32582
+ const outcome = await this.resolveDispatchByID(item.id);
32583
+ if (outcome === "unsupported") {
32584
+ this.typedByIdCompleteUnsupported = true;
32585
+ await this.opts.client.ackDispatchByID(this.opts.config.org_id, item.id);
32586
+ } else if (outcome === "failed") {
32587
+ throw new Error("by-id complete failed");
32588
+ }
32589
+ } else {
32590
+ await this.opts.client.ackDispatchByID(this.opts.config.org_id, item.id);
32591
+ }
32367
32592
  skipped++;
32368
32593
  const key = item.event_type;
32369
32594
  skippedByType.set(key, (skippedByType.get(key) ?? 0) + 1);
@@ -32374,12 +32599,15 @@ var ParallAgentGateway = class {
32374
32599
  }
32375
32600
  processed++;
32376
32601
  try {
32377
- const ackItem = () => this.ackDispatchEvent(item.id, () => this.clearTypedDispatchDedupe(item));
32602
+ const typedHooks = {
32603
+ legacyAck: () => this.ackDispatchEvent(item.id, () => this.clearTypedDispatchDedupe(item)),
32604
+ clearDedupe: () => this.clearTypedDispatchDedupe(item)
32605
+ };
32378
32606
  if (item.event_type === "task_assign" && item.task_id) {
32379
32607
  try {
32380
32608
  await this.consumeTypedDispatch({ dispatchEventId: item.id }, (dispatchEventId) => this.handleTaskDispatch(item.task_id ?? "", item.source_id ?? item.task_id ?? "", {
32381
32609
  dispatchEventId
32382
- }), ackItem);
32610
+ }), typedHooks);
32383
32611
  } catch (err) {
32384
32612
  this.opts.log?.warn(`catch-up task fetch failed for ${item.task_id}, leaving pending: ${String(err)}`);
32385
32613
  continue;
@@ -32389,23 +32617,23 @@ var ParallAgentGateway = class {
32389
32617
  await this.consumeTypedDispatch({ dispatchEventId: item.id }, (dispatchEventId) => this.handleTaskDispatch(item.task_id ?? "", item.source_id ?? item.task_id ?? "", {
32390
32618
  allowCreator: true,
32391
32619
  dispatchEventId
32392
- }), ackItem);
32620
+ }), typedHooks);
32393
32621
  } catch (err) {
32394
32622
  this.opts.log?.warn(`catch-up task fetch failed for ${item.task_id}, leaving pending: ${String(err)}`);
32395
32623
  continue;
32396
32624
  }
32397
32625
  } else if (item.event_type === "task_comment" && item.source_id && item.task_id) {
32398
- await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.handleTaskComment(item.source_id, item.task_id ?? "", item.actor_id, item.delivery_reason), ackItem);
32626
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, (dispatchEventId) => this.handleTaskComment(item.source_id, item.task_id ?? "", item.actor_id, item.delivery_reason, dispatchEventId), typedHooks);
32399
32627
  } else if (item.event_type === "wiki_comment" && item.source_id) {
32400
- await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.handleWikiComment(item.source_id, item.actor_id, item.delivery_reason), ackItem);
32628
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, (dispatchEventId) => this.handleWikiComment(item.source_id, item.actor_id, item.delivery_reason, dispatchEventId), typedHooks);
32401
32629
  } else if (item.event_type === "schedule.fire" && item.source_id) {
32402
- await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.fetchAndHandleScheduleFire(item.source_id, item.actor_id), ackItem);
32630
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, (dispatchEventId) => this.fetchAndHandleScheduleFire(item.source_id, item.actor_id, dispatchEventId), typedHooks);
32403
32631
  } else if (item.event_type === "external_trigger" && item.source_id) {
32404
- await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.fetchAndHandleExternalTriggerRun(item.source_id), ackItem);
32632
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, (dispatchEventId) => this.fetchAndHandleExternalTriggerRun(item.source_id, dispatchEventId), typedHooks);
32405
32633
  } else if (item.event_type === "channel_message" && item.source_id) {
32406
- await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.fetchAndHandleChannelMessage(item.source_id), ackItem);
32634
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, (dispatchEventId) => this.fetchAndHandleChannelMessage(item.source_id, dispatchEventId), typedHooks);
32407
32635
  } else if (item.event_type === "approval_decided" && item.source_id) {
32408
- await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.fetchAndHandleApprovalDecided(item.source_id, item.actor_id, item.chat_id ?? null), ackItem);
32636
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, (dispatchEventId) => this.fetchAndHandleApprovalDecided(item.source_id, item.actor_id, item.chat_id ?? null, dispatchEventId), typedHooks);
32409
32637
  } else if (item.event_type === "message" && item.source_id && item.chat_id) {
32410
32638
  await this.consumeMessageWorkItem({
32411
32639
  id: item.id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/parall",
3
- "version": "1.44.0",
3
+ "version": "1.45.0",
4
4
  "description": "OpenClaw channel plugin for Parall IM",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -16,8 +16,8 @@
16
16
  "openclaw.plugin.json"
17
17
  ],
18
18
  "dependencies": {
19
- "@parall/agent-core": "1.44.0",
20
- "@parall/sdk": "1.44.0"
19
+ "@parall/sdk": "1.45.0",
20
+ "@parall/agent-core": "1.45.0"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/node": "^22.0.0",
@@ -40,7 +40,7 @@ parall schedules create \
40
40
  --run-at <FUTURE_RFC3339_TIME>
41
41
  ```
42
42
 
43
- `--target-ids` is who receives the fire (usually yourself when you're self-scheduling; another agent or human when delegating). `--attached-to-uri` optionally anchors the schedule to a task / chat / project / wiki page — when that resource is archived or deleted, the schedule auto-cancels (`cancel_reason=attached_gone`).
43
+ `--target-ids` is who receives the fire (usually yourself when you're self-scheduling; another agent or human when delegating). `--attached-to-uri` optionally anchors the schedule to a task / chat / project / wiki page — when that resource is archived or deleted, the schedule auto-cancels (`status_reason=attached_gone`). A schedule whose agent targets all sit on a terminated machine is auto-paused by the platform (`status_reason=attendee_unreachable`) instead of firing into a void; resuming a recurring schedule while the machine is still terminated just pauses it again on the next slot (a one-shot resumed past its catch-up window instead follows the normal missed semantics and completes).
44
44
 
45
45
  ### Reminders for someone else
46
46
 
@@ -39,16 +39,32 @@ parall tasks list --assignee-id prll://usr_xxx # first page only (default 20)
39
39
  parall tasks subtasks prll://tsk_xxx # children of a single parent task
40
40
 
41
41
  # Create a task (add --parent-id to make it a SUBTASK of another task)
42
- parall tasks create --title "Task title" [--assignee-id prll://usr_xxx] [--parent-id prll://tsk_xxx] [--project-id prll://prj_xxx]
42
+ parall tasks create --title "Task title" [--assignee-id prll://usr_xxx] [--parent-id prll://tsk_xxx] [--project-id prll://prj_xxx] [--due-date 2026-08-01]
43
43
 
44
- # Update task status
45
- parall tasks update prll://tsk_xxx --status in_progress
46
- parall tasks update prll://tsk_xxx --status done
44
+ # Update task status — add --placement end so the task lands at the end of
45
+ # its NEW status column (a bare --status keeps the old column's sort_order)
46
+ parall tasks update prll://tsk_xxx --status in_progress --placement end
47
+ parall tasks update prll://tsk_xxx --status done --placement end
48
+
49
+ # Due date — a plain YYYY-MM-DD date (no timestamps); "none" clears it
50
+ parall tasks update prll://tsk_xxx --due-date 2026-08-01
51
+ parall tasks update prll://tsk_xxx --due-date none
52
+
53
+ # Move a task to the end of its status column
54
+ parall tasks update prll://tsk_xxx --placement end
47
55
 
48
56
  # Add a comment
49
57
  parall tasks comments add prll://tsk_xxx --body "Progress update..."
50
58
  ```
51
59
 
60
+ Ordering: to append a task to the end of a status column, always use
61
+ `--placement end` — the server resolves the position atomically. This
62
+ includes status changes: a bare `--status` keeps the task's old
63
+ `sort_order`, which may collide inside the new column. Do NOT compute a
64
+ `sort_order` value yourself from listed tasks (your view may be stale or
65
+ partial). `--sort-order` is only for pinpoint insertion between two cards
66
+ you just listed, and it cannot be combined with `--placement`.
67
+
52
68
  Subtasks are just tasks with a parent: create one with `tasks create --parent-id`,
53
69
  re-parent with `tasks update --parent-id`, list a parent's children with
54
70
  `tasks subtasks`. `tasks list` without `--parent-id` already returns both
@@ -83,7 +99,7 @@ watcher.
83
99
  When you receive `[Event: task.assigned]`:
84
100
 
85
101
  1. Acknowledge with a comment: `tasks comments add prll://tsk_xxx --body "On it"`
86
- 2. Update status: `tasks update prll://tsk_xxx --status in_progress`
102
+ 2. Update status: `tasks update prll://tsk_xxx --status in_progress --placement end`
87
103
  3. Do the work
88
104
  4. Report results in a comment. If a gate remains — review, merge, deploy,
89
105
  requester acceptance — set `in_review` and name the gate; set `done`