@parall/parall 1.43.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.
@@ -26479,13 +26479,13 @@ function laneKeyForTarget(targetUri, threadRootId) {
26479
26479
  return Buffer.from(`${targetUri}
26480
26480
  ${threadRootId ?? ""}`, "utf8").toString("base64url");
26481
26481
  }
26482
+ function dispatchLaneContextDir(stateDir) {
26483
+ return path.join(stateDir, "dispatch-lane-context");
26484
+ }
26482
26485
  function laneContextFilePath(contextDir, targetUri, threadRootId) {
26483
26486
  return path.join(contextDir, `${laneKeyForTarget(targetUri, threadRootId)}.json`);
26484
26487
  }
26485
26488
 
26486
- // ../agent-core/dist/lane-ledger.js
26487
- import * as fs from "node:fs";
26488
-
26489
26489
  // ../sdk/dist/types.js
26490
26490
  var MENTION_ALL_USER_ID = "all";
26491
26491
 
@@ -27940,7 +27940,6 @@ var ParallClient = class _ParallClient {
27940
27940
  async steerDispatch(orgId, req) {
27941
27941
  return this.request("POST", ENDPOINTS.DISPATCH_STEER(orgId), req);
27942
27942
  }
27943
- /** End a turn: no_action sweep of the lane's members + lane release + re-drive check. */
27944
27943
  async completeDispatch(orgId, req) {
27945
27944
  return this.request("POST", ENDPOINTS.DISPATCH_COMPLETE(orgId), req);
27946
27945
  }
@@ -27948,6 +27947,10 @@ var ParallClient = class _ParallClient {
27948
27947
  * End a turn for a lane-less runtime: resolve the turn's folded WorkItems
27949
27948
  * by source — broad-cover to the turn's reply Effect when one exists,
27950
27949
  * no_action sweep otherwise. Idempotent.
27950
+ *
27951
+ * @deprecated Legacy ok-only alias — use {@link completeDispatch} with the
27952
+ * `sources` form, which also carries `turn_outcome`. The endpoint retires
27953
+ * at S3b (dispatch-convergence-design.md §3).
27951
27954
  */
27952
27955
  async completeDispatchSources(orgId, req) {
27953
27956
  return this.request("POST", ENDPOINTS.DISPATCH_COMPLETE_SOURCES(orgId), req);
@@ -29094,6 +29097,7 @@ var ParallWs = class {
29094
29097
  };
29095
29098
 
29096
29099
  // ../agent-core/dist/lane-ledger.js
29100
+ import * as fs from "node:fs";
29097
29101
  var LedgerUnsupportedError = class extends Error {
29098
29102
  };
29099
29103
  function isStaleLane(err) {
@@ -29153,7 +29157,11 @@ var LaneLedger = class {
29153
29157
  throw err;
29154
29158
  }
29155
29159
  if (!res.claimed || !res.lane) {
29156
- this.opts.log?.info(`lane for ${targetUri} held by a healthy incumbent \u2014 leaving events pending for re-drive`);
29160
+ if (res.reason === "empty") {
29161
+ this.opts.log?.warn(`claim for ${targetUri} came back empty \u2014 nothing foldable; leaving to the reconciler`);
29162
+ } else {
29163
+ this.opts.log?.info(`lane for ${targetUri} held by a healthy incumbent \u2014 leaving events pending for re-drive`);
29164
+ }
29157
29165
  return null;
29158
29166
  }
29159
29167
  const leaseUntilMs = Date.parse(res.lease_until ?? "");
@@ -29230,6 +29238,18 @@ var LaneLedger = class {
29230
29238
  * re-drives any same-target pending work. A STALE_LANE answer means a
29231
29239
  * takeover already owns the resource — local state is dropped either way.
29232
29240
  */
29241
+ /**
29242
+ * Record that the turn on this lane surfaced a runtime error. The flow
29243
+ * settles an errored lane immediately (dispatchLaneGroup returns 'failed'
29244
+ * after a forced complete), so the bit normally lives for one turn only —
29245
+ * it is the transport between the gateway's per-session error signal and
29246
+ * this lane's complete request.
29247
+ */
29248
+ markTurnError(laneKey) {
29249
+ const lane = this.lanes.get(laneKey);
29250
+ if (lane)
29251
+ lane.turnError = true;
29252
+ }
29233
29253
  async completeIfIdle(laneKey, hasMoreLocal) {
29234
29254
  const lane = this.lanes.get(laneKey);
29235
29255
  if (!lane || hasMoreLocal)
@@ -29240,7 +29260,10 @@ var LaneLedger = class {
29240
29260
  const res = await this.opts.client.completeDispatch(this.opts.orgId, {
29241
29261
  lane: lane.lane,
29242
29262
  target_uri: lane.targetUri,
29243
- thread_root_id: lane.threadRootId
29263
+ thread_root_id: lane.threadRootId,
29264
+ // An error turn releases its members for retry instead of sweeping
29265
+ // them as handled (ignored by older servers).
29266
+ turn_outcome: lane.turnError ? "error" : "ok"
29244
29267
  });
29245
29268
  if (res.swept_no_action > 0 || res.redriven) {
29246
29269
  this.opts.log?.info(`lane complete for ${lane.targetUri}: swept ${res.swept_no_action} no_action, redriven=${res.redriven}`);
@@ -29253,6 +29276,17 @@ var LaneLedger = class {
29253
29276
  this.opts.log?.warn(`lane complete failed for ${lane.targetUri}: ${String(err)}`);
29254
29277
  }
29255
29278
  }
29279
+ /**
29280
+ * Renew one lane by its key — the external runtime-activity hook for
29281
+ * adapters whose tool traffic bypasses the RuntimeEvent stream (openclaw
29282
+ * hooks). Scoped to the session's own lane: renewing every lane would let
29283
+ * one busy fork keep an unrelated stalled fork's lane leased forever.
29284
+ */
29285
+ renewByKey(laneKey) {
29286
+ const lane = this.lanes.get(laneKey);
29287
+ if (lane)
29288
+ this.maybeRenew(lane);
29289
+ }
29256
29290
  /**
29257
29291
  * Long-turn keepalive: renew the lane's lease on runtime activity, throttled
29258
29292
  * so a chatty turn doesn't spam the server. Without this, a legitimately
@@ -29347,6 +29381,20 @@ var LaneLedger = class {
29347
29381
  this.lanes.set(lane.laneKey, lane);
29348
29382
  return lane;
29349
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
+ }
29350
29398
  /**
29351
29399
  * Remove the per-lane context file (and its CLI sidecar) when the lane
29352
29400
  * ends. A leftover file would make a later cross-context send to the same
@@ -29394,25 +29442,136 @@ async function dispatchLaneGroup(host, opts) {
29394
29442
  }
29395
29443
  return "foreign";
29396
29444
  }
29445
+ host.noteSessionLane(opts.sessionKey, lane.laneKey);
29397
29446
  let dispatched = false;
29398
29447
  try {
29399
29448
  dispatched = await host.runDispatch(event, opts.sessionKey, opts.body, opts.earlier, opts.captureText);
29400
29449
  } catch (err) {
29450
+ host.noteSessionLane(opts.sessionKey, null);
29401
29451
  await ledger.release(lane.laneKey).catch(() => {
29402
29452
  });
29403
29453
  throw err;
29454
+ } finally {
29455
+ if (dispatched)
29456
+ host.noteSessionLane(opts.sessionKey, null);
29404
29457
  }
29405
29458
  if (!dispatched) {
29406
29459
  return "shutdown";
29407
29460
  }
29461
+ if (host.consumeTurnError(opts.sessionKey)) {
29462
+ ledger.markTurnError(lane.laneKey);
29463
+ for (const msgId of lane.folded.keys()) {
29464
+ host.dispatchedMessages.delete(msgId);
29465
+ }
29466
+ try {
29467
+ host.opts.dispatchAdapter.abortDispatch?.(opts.sessionKey);
29468
+ } catch {
29469
+ }
29470
+ await ledger.completeIfIdle(lane.laneKey, false);
29471
+ return "failed";
29472
+ }
29408
29473
  const pendingInjections = host.opts.dispatchAdapter.hasPendingInjections?.(opts.sessionKey) ?? false;
29409
29474
  await ledger.completeIfIdle(lane.laneKey, pendingInjections || opts.hasMoreLocal());
29410
29475
  return "dispatched";
29411
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
+ }
29412
29571
  var TYPED_BACKOFF_BASE_MS = 2e3;
29413
29572
  var TYPED_BACKOFF_CAP_MS = 5 * 6e4;
29414
29573
  var TYPED_BACKOFF_MAP_CAP = 512;
29415
- async function consumeTypedDispatch(host, ref, run, ack) {
29574
+ async function consumeTypedDispatch(host, ref, run, hooks) {
29416
29575
  const backoffKey = ref.dispatchEventId ?? `${ref.sourceType}:${ref.sourceId}`;
29417
29576
  const armed = host.typedRedriveBackoff.get(backoffKey);
29418
29577
  if (armed) {
@@ -29428,8 +29587,8 @@ async function consumeTypedDispatch(host, ref, run, ack) {
29428
29587
  return;
29429
29588
  }
29430
29589
  const settleAck = (ackResult) => ackResult !== false;
29431
- const settle = (acked2) => {
29432
- if (acked2) {
29590
+ const settle = (resolved2) => {
29591
+ if (resolved2) {
29433
29592
  host.typedRedriveBackoff.delete(backoffKey);
29434
29593
  return;
29435
29594
  }
@@ -29443,13 +29602,13 @@ async function consumeTypedDispatch(host, ref, run, ack) {
29443
29602
  host.typedRedriveBackoff.set(backoffKey, { failures, until: Date.now() + backoffMs });
29444
29603
  };
29445
29604
  const runLegacy = async () => {
29446
- let acked2 = false;
29605
+ let acked = false;
29447
29606
  try {
29448
29607
  if (await run(ref.dispatchEventId)) {
29449
- acked2 = settleAck(await ack(ref.dispatchEventId));
29608
+ acked = settleAck(await hooks.legacyAck(ref.dispatchEventId));
29450
29609
  }
29451
29610
  } finally {
29452
- settle(acked2);
29611
+ settle(acked);
29453
29612
  }
29454
29613
  };
29455
29614
  if (!host.laneLedger || host.ledgerDisabled) {
@@ -29471,15 +29630,39 @@ async function consumeTypedDispatch(host, ref, run, ack) {
29471
29630
  host.opts.log?.info(`typed dispatch ${ref.dispatchEventId ?? `${ref.sourceType}:${ref.sourceId}`} not claimable (held elsewhere or already resolved) \u2014 skipping`);
29472
29631
  return;
29473
29632
  }
29474
- let acked = false;
29633
+ let resolved = false;
29634
+ let viaLegacyAck = false;
29475
29635
  try {
29476
29636
  if (await run(lane.typedDispatchEventId)) {
29477
- 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
+ }
29478
29657
  }
29479
29658
  } finally {
29480
- settle(acked);
29481
- await host.laneLedger.completeIfIdle(lane.laneKey, false).catch(() => {
29482
- });
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
+ }
29483
29666
  }
29484
29667
  }
29485
29668
  async function consumeMessageWorkItem(host, item) {
@@ -29488,6 +29671,16 @@ async function consumeMessageWorkItem(host, item) {
29488
29671
  if (!host.tryClaimMessage(item.source_id))
29489
29672
  return;
29490
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
+ }
29491
29684
  host.opts.client.ackDispatchByID(host.opts.config.org_id, item.id).catch(() => {
29492
29685
  });
29493
29686
  };
@@ -30608,20 +30801,23 @@ var ParallAgentGateway = class {
30608
30801
  if (data.status !== "todo" && data.status !== "in_progress")
30609
30802
  return;
30610
30803
  try {
30611
- 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) => {
30612
- if (dispatchEventId) {
30613
- 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) => {
30614
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;
30615
30818
  });
30616
- }
30617
- return this.opts.client.ackDispatch(this.opts.config.org_id, {
30618
- source_type: "task_activity",
30619
- source_id: data.id
30620
- }).then(() => true, (err) => {
30621
- this.dispatchedTasks.delete(`${data.id}:${data.updated_at}`);
30622
- this.opts.log?.warn(`dispatch ack failed for task ${data.id}, releasing for re-drive: ${String(err)}`);
30623
- return false;
30624
- });
30819
+ },
30820
+ clearDedupe: () => this.dispatchedTasks.delete(`${data.id}:${data.updated_at}`)
30625
30821
  });
30626
30822
  } catch (err) {
30627
30823
  this.opts.log?.error(`task dispatch failed for ${data.id}: ${String(err)}`);
@@ -30640,7 +30836,10 @@ var ParallAgentGateway = class {
30640
30836
  if (!data.source_id || !data.task_id)
30641
30837
  return;
30642
30838
  try {
30643
- 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
+ });
30644
30843
  } catch (err) {
30645
30844
  this.opts.log?.error(`task comment dispatch failed for ${data.source_id}: ${String(err)}`);
30646
30845
  }
@@ -30648,7 +30847,10 @@ var ParallAgentGateway = class {
30648
30847
  if (!data.source_id)
30649
30848
  return;
30650
30849
  try {
30651
- 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
+ });
30652
30854
  } catch (err) {
30653
30855
  this.opts.log?.error(`wiki comment dispatch failed for ${data.source_id}: ${String(err)}`);
30654
30856
  }
@@ -30659,7 +30861,10 @@ var ParallAgentGateway = class {
30659
30861
  await this.consumeTypedDispatch({ dispatchEventId: data.id }, (dispatchEventId) => this.handleTaskDispatch(data.task_id ?? "", data.source_id ?? data.task_id ?? "", {
30660
30862
  allowCreator: true,
30661
30863
  dispatchEventId
30662
- }), () => this.ackDispatchEvent(data.id, () => this.clearTypedDispatchDedupe(data)));
30864
+ }), {
30865
+ legacyAck: () => this.ackDispatchEvent(data.id, () => this.clearTypedDispatchDedupe(data)),
30866
+ clearDedupe: () => this.clearTypedDispatchDedupe(data)
30867
+ });
30663
30868
  } catch (err) {
30664
30869
  this.opts.log?.error(`task update dispatch failed for ${data.task_id}: ${String(err)}`);
30665
30870
  }
@@ -30667,7 +30872,10 @@ var ParallAgentGateway = class {
30667
30872
  if (!data.source_id)
30668
30873
  return;
30669
30874
  try {
30670
- 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
+ });
30671
30879
  } catch (err) {
30672
30880
  this.opts.log?.error(`schedule fire dispatch failed for ${data.source_id}: ${String(err)}`);
30673
30881
  }
@@ -30675,7 +30883,10 @@ var ParallAgentGateway = class {
30675
30883
  if (!data.source_id)
30676
30884
  return;
30677
30885
  try {
30678
- 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
+ });
30679
30890
  } catch (err) {
30680
30891
  this.opts.log?.error(`external trigger dispatch failed for ${data.source_id}: ${String(err)}`);
30681
30892
  }
@@ -30683,7 +30894,10 @@ var ParallAgentGateway = class {
30683
30894
  if (!data.source_id)
30684
30895
  return;
30685
30896
  try {
30686
- 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
+ });
30687
30901
  } catch (err) {
30688
30902
  this.opts.log?.error(`channel message dispatch failed for ${data.source_id}: ${String(err)}`);
30689
30903
  }
@@ -30691,7 +30905,10 @@ var ParallAgentGateway = class {
30691
30905
  if (!data.source_id)
30692
30906
  return;
30693
30907
  try {
30694
- 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
+ });
30695
30912
  } catch (err) {
30696
30913
  this.opts.log?.error(`approval decided dispatch failed for ${data.source_id}: ${String(err)}`);
30697
30914
  }
@@ -30728,6 +30945,44 @@ var ParallAgentGateway = class {
30728
30945
  this.dispatchedMessages.add(id);
30729
30946
  return true;
30730
30947
  }
30948
+ /**
30949
+ * Lane currently being dispatched per session — lets external activity
30950
+ * signals renew exactly the caller's lane (renewing all lanes would keep
30951
+ * an unrelated stalled fork's lane leased forever).
30952
+ */
30953
+ sessionActiveLanes = /* @__PURE__ */ new Map();
30954
+ noteSessionLane(sessionKey, laneKey) {
30955
+ if (laneKey == null)
30956
+ this.sessionActiveLanes.delete(sessionKey);
30957
+ else
30958
+ this.sessionActiveLanes.set(sessionKey, laneKey);
30959
+ }
30960
+ /**
30961
+ * External runtime-activity signal for adapters whose tool activity does
30962
+ * not flow through the RuntimeEvent stream (openclaw hooks call this from
30963
+ * the tool-call lifecycle): renews the session's OWN active ledger lane so
30964
+ * a long tool call cannot outlive the lease and get dethroned mid-turn.
30965
+ * No-op without an active ledger lane for the session.
30966
+ */
30967
+ touchRuntimeActivity(sessionKey) {
30968
+ if (this.ledgerDisabled)
30969
+ return;
30970
+ const laneKey = this.sessionActiveLanes.get(sessionKey);
30971
+ if (laneKey)
30972
+ this.laneLedger?.renewByKey(laneKey);
30973
+ }
30974
+ /** Sessions whose in-flight turn surfaced a runtime error event. */
30975
+ turnErrorSessions = /* @__PURE__ */ new Set();
30976
+ /**
30977
+ * Consume (read-and-clear) the error marker for sessionKey's last turn.
30978
+ * Feeds complete's turn_outcome so an error turn's lane members are
30979
+ * released for retry instead of no_action-swept (design §3). Consuming
30980
+ * (rather than peeking) keeps one-shot fork session keys from accumulating
30981
+ * in the set forever.
30982
+ */
30983
+ consumeTurnError(sessionKey) {
30984
+ return this.turnErrorSessions.delete(sessionKey);
30985
+ }
30731
30986
  async emitDispatchReceived(event) {
30732
30987
  const sourceType = event.ackSourceType ?? (event.type === "task" ? "task_activity" : "message");
30733
30988
  const sourceId = event.ackSourceId ?? event.messageId;
@@ -30736,6 +30991,20 @@ var ParallAgentGateway = class {
30736
30991
  source_id: sourceId
30737
30992
  });
30738
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
+ }
30739
31008
  /** True when this event's lifecycle is owned by the dispatch lane ledger. */
30740
31009
  usesLaneLedger(event) {
30741
31010
  return this.laneLedger != null && !this.ledgerDisabled && this.laneLedger.handles(event);
@@ -30766,15 +31035,16 @@ var ParallAgentGateway = class {
30766
31035
  dispatchLaneGroup(opts) {
30767
31036
  return dispatchLaneGroup(this.laneFlowHost(), opts);
30768
31037
  }
30769
- consumeTypedDispatch(ref, run, ack) {
30770
- return consumeTypedDispatch(this.laneFlowHost(), ref, run, ack);
31038
+ consumeTypedDispatch(ref, run, hooks) {
31039
+ return consumeTypedDispatch(this.laneFlowHost(), ref, run, hooks);
30771
31040
  }
30772
- // Typed completion must wait until the administrative ack has either
30773
- // committed or failed. Errors stay best-effort: a failed ack leaves the row
30774
- // received, so Complete releases and re-drives it safely. The boolean
30775
- // outcome feeds the typed-consume backoff an ack that failed must count
30776
- // as a failed consume, or an ack outage would clear the backoff entry and
30777
- // 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.
30778
31048
  ackDispatchEvent(dispatchEventId, onFailure) {
30779
31049
  return this.opts.client.ackDispatchByID(this.opts.config.org_id, dispatchEventId).then(() => true, (err) => {
30780
31050
  onFailure?.();
@@ -30782,6 +31052,9 @@ var ParallAgentGateway = class {
30782
31052
  return false;
30783
31053
  });
30784
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).
30785
31058
  clearTypedDispatchDedupe(item) {
30786
31059
  switch (item.event_type) {
30787
31060
  case "task_assign":
@@ -31077,6 +31350,7 @@ var ParallAgentGateway = class {
31077
31350
  this.pendingRestartNotification = null;
31078
31351
  }
31079
31352
  resetDispatchMetrics(sessionKey);
31353
+ this.turnErrorSessions.delete(sessionKey);
31080
31354
  return runWithSessionKey(sessionKey, async () => {
31081
31355
  let dispatchSpan = null;
31082
31356
  setSessionChatId(sessionKey, event.targetId);
@@ -31184,6 +31458,9 @@ var ParallAgentGateway = class {
31184
31458
  } else if (runtimeEvent.type === "tool_result" && pendingSendCallIds.delete(runtimeEvent.callId)) {
31185
31459
  recordMessageSend(sessionKey, !runtimeEvent.error);
31186
31460
  }
31461
+ if (runtimeEvent.type === "error") {
31462
+ this.turnErrorSessions.add(sessionKey);
31463
+ }
31187
31464
  await this.createRuntimeStep(binding.agentSessionId, event, runtimeEvent, stepIdFilePath, contextFilePath, laneContextFilePath2);
31188
31465
  }
31189
31466
  if (!binding) {
@@ -31331,16 +31608,32 @@ var ParallAgentGateway = class {
31331
31608
  body: buildForkScopePrefix(last) + buildEventBody(last),
31332
31609
  earlier,
31333
31610
  captureText: batchText,
31334
- hasMoreLocal: () => fork.queue.length > 0
31611
+ // Per-LANE residue check (parity with the main-buffer path):
31612
+ // the fork queue can hold several lanes (channel + thread of
31613
+ // the same chat). A whole-queue check would defer THIS lane's
31614
+ // complete behind another lane's items and never revisit it —
31615
+ // its members would sit received until lease expiry.
31616
+ hasMoreLocal: () => fork.queue.some((it) => this.dispatchGroupKey(it.event) === this.dispatchGroupKey(last))
31335
31617
  });
31336
31618
  if (outcome === "foreign") {
31337
31619
  for (const item of items)
31338
31620
  item.resolve(false);
31339
31621
  break;
31340
31622
  }
31623
+ if (outcome === "failed") {
31624
+ for (const item of items)
31625
+ item.resolve(false);
31626
+ continue;
31627
+ }
31341
31628
  dispatched = outcome === "dispatched";
31342
31629
  } else {
31343
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
+ }
31344
31637
  }
31345
31638
  if (!dispatched) {
31346
31639
  for (const item of items) {
@@ -31371,6 +31664,7 @@ var ParallAgentGateway = class {
31371
31664
  remaining.resolve(false);
31372
31665
  }
31373
31666
  } finally {
31667
+ this.turnErrorSessions.delete(fork.fork.sessionKey);
31374
31668
  if (fork.deadlineTimer) {
31375
31669
  clearTimeout(fork.deadlineTimer);
31376
31670
  fork.deadlineTimer = null;
@@ -31483,15 +31777,22 @@ var ParallAgentGateway = class {
31483
31777
  this.dispatchState.pendingForkResults.unshift(...pendingFork);
31484
31778
  continue;
31485
31779
  }
31780
+ if (outcome === "failed") {
31781
+ this.dispatchState.pendingForkResults.unshift(...pendingFork);
31782
+ continue;
31783
+ }
31486
31784
  continue;
31487
31785
  }
31488
- try {
31489
- await this.emitDispatchReceived(event);
31490
- } catch (err) {
31491
- this.opts.log?.warn?.(`mark-received failed for buffered dispatch, leaving unacked for retry: ${String(err)}`);
31492
- this.dispatchState.mainBuffer.unshift(...events);
31493
- this.dispatchState.pendingForkResults.unshift(...pendingFork);
31494
- 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
+ }
31495
31796
  }
31496
31797
  const dispatched = await this.runDispatch(event, this.opts.runtimeKey, forkPrefix + buildEventBody(event), earlier);
31497
31798
  if (!dispatched) {
@@ -31499,6 +31800,10 @@ var ParallAgentGateway = class {
31499
31800
  this.dispatchState.pendingForkResults.unshift(...pendingFork);
31500
31801
  break;
31501
31802
  }
31803
+ if (typedRefs) {
31804
+ await settleDrainedTypedGroup(this.laneFlowHost(), events, typedRefs, this.consumeTurnError(this.opts.runtimeKey));
31805
+ continue;
31806
+ }
31502
31807
  for (const bufferedEvent of events) {
31503
31808
  const sourceType = bufferedEvent.ackSourceType ?? (bufferedEvent.type === "task" ? "task_activity" : "message");
31504
31809
  const sourceId = bufferedEvent.ackSourceId ?? bufferedEvent.messageId;
@@ -31558,20 +31863,27 @@ var ParallAgentGateway = class {
31558
31863
  }
31559
31864
  return outcome === "dispatched";
31560
31865
  }
31561
- try {
31562
- await this.emitDispatchReceived(event);
31563
- } catch (err) {
31564
- this.opts.log?.warn?.(`mark-received failed, leaving unacked for retry: ${String(err)}`);
31565
- this.dispatchState.mainDispatching = false;
31566
- this.dispatchState.mainCurrentTargetId = void 0;
31567
- this.mainCurrentGroupKey = void 0;
31568
- this.dispatchState.mainPreDispatchBranchPoint = void 0;
31569
- this.dispatchState.pendingForkResults.unshift(...pendingFork);
31570
- 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
+ }
31571
31879
  }
31572
31880
  let dispatched = false;
31573
31881
  try {
31574
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
+ }
31575
31887
  if (!dispatched) {
31576
31888
  this.dispatchState.pendingForkResults.unshift(...pendingFork);
31577
31889
  }
@@ -31586,7 +31898,7 @@ var ParallAgentGateway = class {
31586
31898
  }
31587
31899
  this.dispatchState.mainBuffer.push(event);
31588
31900
  if (this.usesLaneLedger(event)) {
31589
- if (this.mainCurrentGroupKey === this.dispatchGroupKey(event) && await this.laneLedger?.steerLive(event) && await this.opts.dispatchAdapter.enqueueDuringDispatch?.(this.opts.runtimeKey, buildEventBody(event))) {
31901
+ if (this.mainCurrentGroupKey === this.dispatchGroupKey(event) && this.opts.dispatchAdapter.enqueueDuringDispatch != null && await this.laneLedger?.steerLive(event) && await this.opts.dispatchAdapter.enqueueDuringDispatch(this.opts.runtimeKey, buildEventBody(event))) {
31590
31902
  this.opts.log?.info(`steer folded+injected for ${event.messageId} (will drain for bookkeeping)`);
31591
31903
  }
31592
31904
  } else if (this.dispatchState.mainCurrentTargetId === event.targetId && await this.opts.dispatchAdapter.enqueueDuringDispatch?.(this.opts.runtimeKey, buildEventBody(event))) {
@@ -31613,12 +31925,14 @@ var ParallAgentGateway = class {
31613
31925
  this.dispatchState.mainBuffer.push(event);
31614
31926
  return false;
31615
31927
  }
31616
- try {
31617
- await this.emitDispatchReceived(event);
31618
- } catch (err) {
31619
- this.opts.log?.warn?.(`mark-received failed for fork dispatch, leaving unacked for retry: ${String(err)}`);
31620
- this.dispatchState.mainBuffer.push(event);
31621
- return false;
31928
+ if (!this.usesLaneLedger(event) && !this.typedLedgerEventIds([event])) {
31929
+ try {
31930
+ await this.emitDispatchReceived(event);
31931
+ } catch (err) {
31932
+ this.opts.log?.warn?.(`mark-received failed for fork dispatch, leaving unacked for retry: ${String(err)}`);
31933
+ this.dispatchState.mainBuffer.push(event);
31934
+ return false;
31935
+ }
31622
31936
  }
31623
31937
  const fork = await this.opts.dispatchAdapter.forkSession({
31624
31938
  sessionKey: this.opts.runtimeKey,
@@ -31792,7 +32106,10 @@ var ParallAgentGateway = class {
31792
32106
  return;
31793
32107
  await this.consumeTypedDispatch({ dispatchEventId: item.id }, (dispatchEventId) => this.handleTaskDispatch(item.task_id ?? "", item.source_id ?? item.task_id ?? "", {
31794
32108
  dispatchEventId
31795
- }), (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
+ });
31796
32113
  }
31797
32114
  consumeMessageWorkItem(item) {
31798
32115
  return consumeMessageWorkItem(this.laneFlowHost(), item);
@@ -31853,7 +32170,7 @@ var ParallAgentGateway = class {
31853
32170
  }
31854
32171
  return this.handleTaskAssignment(task, ackSourceId, opts.dispatchEventId);
31855
32172
  }
31856
- async handleTaskComment(commentId, taskId, actorId, deliveryReason) {
32173
+ async handleTaskComment(commentId, taskId, actorId, deliveryReason, dispatchEventId) {
31857
32174
  if (this.shuttingDown)
31858
32175
  return false;
31859
32176
  const dedupeKey = `comment:${commentId}`;
@@ -31909,7 +32226,8 @@ var ParallAgentGateway = class {
31909
32226
  body: parts.join("\n"),
31910
32227
  deliveryReason: deliveryReason ?? void 0,
31911
32228
  ackSourceType: "comment",
31912
- ackSourceId: commentId
32229
+ ackSourceId: commentId,
32230
+ dispatchEventId
31913
32231
  };
31914
32232
  let dispatched;
31915
32233
  try {
@@ -31923,7 +32241,7 @@ var ParallAgentGateway = class {
31923
32241
  }
31924
32242
  return dispatched;
31925
32243
  }
31926
- async handleWikiComment(commentId, actorId, deliveryReason) {
32244
+ async handleWikiComment(commentId, actorId, deliveryReason, dispatchEventId) {
31927
32245
  if (this.shuttingDown)
31928
32246
  return false;
31929
32247
  const dedupeKey = `comment:${commentId}`;
@@ -31969,7 +32287,8 @@ var ParallAgentGateway = class {
31969
32287
  deliveryReason: deliveryReason ?? void 0,
31970
32288
  replyTargetUri: comment.target_uri,
31971
32289
  ackSourceType: "comment",
31972
- ackSourceId: commentId
32290
+ ackSourceId: commentId,
32291
+ dispatchEventId
31973
32292
  };
31974
32293
  let dispatched;
31975
32294
  try {
@@ -31991,7 +32310,7 @@ var ParallAgentGateway = class {
31991
32310
  * access to already-delivered run snapshots, and the runtime must not crash
31992
32311
  * or retry forever in that case.
31993
32312
  */
31994
- async fetchAndHandleScheduleFire(runId, actorId) {
32313
+ async fetchAndHandleScheduleFire(runId, actorId, dispatchEventId) {
31995
32314
  let run = null;
31996
32315
  try {
31997
32316
  run = await this.opts.client.getScheduleRun(this.opts.config.org_id, runId);
@@ -32006,9 +32325,9 @@ var ParallAgentGateway = class {
32006
32325
  }
32007
32326
  if (!run)
32008
32327
  return true;
32009
- return this.handleScheduleFire(run, actorId);
32328
+ return this.handleScheduleFire(run, actorId, dispatchEventId);
32010
32329
  }
32011
- async handleScheduleFire(run, actorId) {
32330
+ async handleScheduleFire(run, actorId, dispatchEventId) {
32012
32331
  if (this.shuttingDown)
32013
32332
  return false;
32014
32333
  const dedupeKey = `schedule_run:${run.id}`;
@@ -32031,7 +32350,8 @@ var ParallAgentGateway = class {
32031
32350
  scheduledFireAt: run.scheduled_fire_at,
32032
32351
  attachedUri: run.fired_attached_uri ?? void 0,
32033
32352
  ackSourceType: "schedule_run",
32034
- ackSourceId: run.id
32353
+ ackSourceId: run.id,
32354
+ dispatchEventId
32035
32355
  };
32036
32356
  let dispatched;
32037
32357
  try {
@@ -32045,7 +32365,7 @@ var ParallAgentGateway = class {
32045
32365
  }
32046
32366
  return dispatched;
32047
32367
  }
32048
- async fetchAndHandleExternalTriggerRun(runId) {
32368
+ async fetchAndHandleExternalTriggerRun(runId, dispatchEventId) {
32049
32369
  let run = null;
32050
32370
  try {
32051
32371
  run = await this.opts.client.getExternalTriggerRun(this.opts.config.org_id, runId);
@@ -32060,14 +32380,14 @@ var ParallAgentGateway = class {
32060
32380
  }
32061
32381
  if (!run)
32062
32382
  return true;
32063
- return this.handleExternalTriggerRun(run);
32383
+ return this.handleExternalTriggerRun(run, dispatchEventId);
32064
32384
  }
32065
32385
  // fetchAndHandleChannelMessage resolves a channel_message dispatch to its
32066
32386
  // durable ChannelMessage + conversation and hands it to the inbound
32067
32387
  // pipeline. targetId = the ChannelConversation id, so per-conversation
32068
32388
  // multi-turn continuity rides the same per-target session mechanics as
32069
32389
  // chats. Design: docs/engineering-design/external-im-channel-design.md.
32070
- async fetchAndHandleChannelMessage(messageId) {
32390
+ async fetchAndHandleChannelMessage(messageId, dispatchEventId) {
32071
32391
  if (this.shuttingDown)
32072
32392
  return false;
32073
32393
  const claimKey = `channel_message:${messageId}`;
@@ -32120,7 +32440,8 @@ var ParallAgentGateway = class {
32120
32440
  channelExternalMessageId: msg.external_message_id,
32121
32441
  channelCliCapable: cliCapable,
32122
32442
  ackSourceType: "channel_message",
32123
- ackSourceId: msg.id
32443
+ ackSourceId: msg.id,
32444
+ dispatchEventId
32124
32445
  };
32125
32446
  let dispatched;
32126
32447
  try {
@@ -32134,7 +32455,7 @@ var ParallAgentGateway = class {
32134
32455
  }
32135
32456
  return dispatched;
32136
32457
  }
32137
- async handleExternalTriggerRun(run) {
32458
+ async handleExternalTriggerRun(run, dispatchEventId) {
32138
32459
  if (this.shuttingDown)
32139
32460
  return false;
32140
32461
  const dedupeKey = `external_trigger_run:${run.id}`;
@@ -32159,7 +32480,8 @@ var ParallAgentGateway = class {
32159
32480
  externalIngressEventType: run.ingress_event_type || void 0,
32160
32481
  attachedUri,
32161
32482
  ackSourceType: "external_trigger_run",
32162
- ackSourceId: run.id
32483
+ ackSourceId: run.id,
32484
+ dispatchEventId
32163
32485
  };
32164
32486
  let dispatched;
32165
32487
  try {
@@ -32173,7 +32495,7 @@ var ParallAgentGateway = class {
32173
32495
  }
32174
32496
  return dispatched;
32175
32497
  }
32176
- async fetchAndHandleApprovalDecided(approvalId, actorId, chatId) {
32498
+ async fetchAndHandleApprovalDecided(approvalId, actorId, chatId, dispatchEventId) {
32177
32499
  let approval = null;
32178
32500
  try {
32179
32501
  approval = await this.opts.client.getApproval(approvalId);
@@ -32204,7 +32526,12 @@ var ParallAgentGateway = class {
32204
32526
  senderId: actorId ?? approval.decided_by ?? "system",
32205
32527
  senderName: "approver",
32206
32528
  messageId: approval.id,
32207
- 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
32208
32535
  };
32209
32536
  let dispatched;
32210
32537
  try {
@@ -32251,7 +32578,17 @@ var ParallAgentGateway = class {
32251
32578
  break;
32252
32579
  if (overflowMode && processed >= CATCHUP_MAX) {
32253
32580
  try {
32254
- 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
+ }
32255
32592
  skipped++;
32256
32593
  const key = item.event_type;
32257
32594
  skippedByType.set(key, (skippedByType.get(key) ?? 0) + 1);
@@ -32262,12 +32599,15 @@ var ParallAgentGateway = class {
32262
32599
  }
32263
32600
  processed++;
32264
32601
  try {
32265
- 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
+ };
32266
32606
  if (item.event_type === "task_assign" && item.task_id) {
32267
32607
  try {
32268
32608
  await this.consumeTypedDispatch({ dispatchEventId: item.id }, (dispatchEventId) => this.handleTaskDispatch(item.task_id ?? "", item.source_id ?? item.task_id ?? "", {
32269
32609
  dispatchEventId
32270
- }), ackItem);
32610
+ }), typedHooks);
32271
32611
  } catch (err) {
32272
32612
  this.opts.log?.warn(`catch-up task fetch failed for ${item.task_id}, leaving pending: ${String(err)}`);
32273
32613
  continue;
@@ -32277,23 +32617,23 @@ var ParallAgentGateway = class {
32277
32617
  await this.consumeTypedDispatch({ dispatchEventId: item.id }, (dispatchEventId) => this.handleTaskDispatch(item.task_id ?? "", item.source_id ?? item.task_id ?? "", {
32278
32618
  allowCreator: true,
32279
32619
  dispatchEventId
32280
- }), ackItem);
32620
+ }), typedHooks);
32281
32621
  } catch (err) {
32282
32622
  this.opts.log?.warn(`catch-up task fetch failed for ${item.task_id}, leaving pending: ${String(err)}`);
32283
32623
  continue;
32284
32624
  }
32285
32625
  } else if (item.event_type === "task_comment" && item.source_id && item.task_id) {
32286
- 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);
32287
32627
  } else if (item.event_type === "wiki_comment" && item.source_id) {
32288
- 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);
32289
32629
  } else if (item.event_type === "schedule.fire" && item.source_id) {
32290
- 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);
32291
32631
  } else if (item.event_type === "external_trigger" && item.source_id) {
32292
- 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);
32293
32633
  } else if (item.event_type === "channel_message" && item.source_id) {
32294
- 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);
32295
32635
  } else if (item.event_type === "approval_decided" && item.source_id) {
32296
- 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);
32297
32637
  } else if (item.event_type === "message" && item.source_id && item.chat_id) {
32298
32638
  await this.consumeMessageWorkItem({
32299
32639
  id: item.id,
@@ -33873,6 +34213,10 @@ function cleanupForkSession(opts) {
33873
34213
  }
33874
34214
 
33875
34215
  // dist/gateway.js
34216
+ function sessionContextFilePath(stateDir, sessionKey) {
34217
+ const fileName = Buffer.from(sessionKey).toString("base64url");
34218
+ return path8.join(stateDir, "dispatch-context", `${fileName}.json`);
34219
+ }
33876
34220
  function resolveWsUrl(account) {
33877
34221
  if (account.config.ws_url)
33878
34222
  return account.config.ws_url;
@@ -34242,6 +34586,18 @@ var parallGateway = {
34242
34586
  runtimeKey: orchestratorKey,
34243
34587
  runtimeRef: { hostname: os2.hostname(), pid: process.pid },
34244
34588
  dispatchAdapter,
34589
+ // Opts openclaw into the dispatch ledger (claim/fold/complete +
34590
+ // idempotent reply effects) — the same shared-gateway machinery
34591
+ // claude/codex ride; openclaw stays buffer-only (no mid-turn steer),
34592
+ // which the ledger does not require. Replies already flow through
34593
+ // @parall/cli, which reads PRLL_CONTEXT_DIR (injected in hooks.ts)
34594
+ // to bind dispatch_lane + reply effect keys.
34595
+ // Design: docs/engineering-design/dispatch-convergence-design.md §7 (S2).
34596
+ dispatchContextDir: dispatchLaneContextDir(stateDir),
34597
+ // Per-session context file (PRLL_CONTEXT_FILE contract) — the CLI's
34598
+ // TYPED dispatch binding (parall tasks update → task_update effect)
34599
+ // reads lane/event/task fields from this file, not the lane dir.
34600
+ contextFilePathForSession: (sessionKey) => sessionContextFilePath(stateDir, sessionKey),
34245
34601
  log: otelLog,
34246
34602
  // Live capability view for hint routing: the channel reply hint
34247
34603
  // points at the vendor CLI only while the grant is active.
@@ -34265,7 +34621,13 @@ var parallGateway = {
34265
34621
  runtimeRef: { hostname: os2.hostname(), pid: process.pid },
34266
34622
  wikiMountRoot,
34267
34623
  ws,
34268
- orchestratorSessionKey: orchestratorKey
34624
+ orchestratorSessionKey: orchestratorKey,
34625
+ dispatchContextDir: dispatchLaneContextDir(stateDir),
34626
+ contextFilePathForSession: (sessionKey) => sessionContextFilePath(stateDir, sessionKey),
34627
+ // Lease-renewal bridge: openclaw tool calls flow through hooks,
34628
+ // not the RuntimeEvent stream, so hook activity must renew the
34629
+ // ledger lanes or a long exec outlives the lease (STALE_LANE).
34630
+ touchRuntimeActivity: (sessionKey) => gateway.touchRuntimeActivity(sessionKey)
34269
34631
  });
34270
34632
  },
34271
34633
  onSessionBinding: async ({ sessionKey, agentSessionId }) => {
@@ -34450,10 +34812,41 @@ function registerParallHooks(api) {
34450
34812
  };
34451
34813
  });
34452
34814
  const pendingSendCalls = /* @__PURE__ */ new Map();
34815
+ const laneRenewals = /* @__PURE__ */ new Map();
34816
+ const LANE_RENEW_INTERVAL_MS = 18e4;
34817
+ const beginToolRenewal = (sessionKey) => {
34818
+ const accountId = extractAccountIdFromSessionKey(sessionKey);
34819
+ const state = accountId ? getParallAccountState(accountId) : void 0;
34820
+ if (!state?.touchRuntimeActivity)
34821
+ return;
34822
+ state.touchRuntimeActivity(sessionKey);
34823
+ const entry = laneRenewals.get(sessionKey) ?? { count: 0 };
34824
+ entry.count++;
34825
+ if (!entry.timer) {
34826
+ entry.timer = setInterval(() => state.touchRuntimeActivity?.(sessionKey), LANE_RENEW_INTERVAL_MS);
34827
+ entry.timer.unref?.();
34828
+ }
34829
+ laneRenewals.set(sessionKey, entry);
34830
+ };
34831
+ const endToolRenewal = (sessionKey) => {
34832
+ const entry = laneRenewals.get(sessionKey);
34833
+ if (entry) {
34834
+ entry.count--;
34835
+ if (entry.count <= 0) {
34836
+ if (entry.timer)
34837
+ clearInterval(entry.timer);
34838
+ laneRenewals.delete(sessionKey);
34839
+ }
34840
+ }
34841
+ const accountId = extractAccountIdFromSessionKey(sessionKey);
34842
+ const state = accountId ? getParallAccountState(accountId) : void 0;
34843
+ state?.touchRuntimeActivity?.(sessionKey);
34844
+ };
34453
34845
  api.on("before_tool_call", async (event, ctx) => {
34454
34846
  const sessionKey = ctx.sessionKey;
34455
34847
  if (sessionKey) {
34456
34848
  recordToolCall(sessionKey);
34849
+ beginToolRenewal(sessionKey);
34457
34850
  if (event.toolName === "exec" && event.toolCallId) {
34458
34851
  const command = event.params?.command;
34459
34852
  if (isParallSendCommand(command)) {
@@ -34522,6 +34915,12 @@ function registerParallHooks(api) {
34522
34915
  injectedEnv.PRLL_TRIGGER_MESSAGE_ID = triggerMsgId;
34523
34916
  if (state.wikiMountRoot)
34524
34917
  injectedEnv.PRLL_WIKI_MOUNT_ROOT = state.wikiMountRoot;
34918
+ if (state.dispatchContextDir)
34919
+ injectedEnv.PRLL_CONTEXT_DIR = state.dispatchContextDir;
34920
+ if (state.contextFilePathForSession) {
34921
+ injectedEnv.PRLL_CONTEXT_FILE = state.contextFilePathForSession(sessionKey);
34922
+ }
34923
+ state.touchRuntimeActivity?.(sessionKey);
34525
34924
  injectedEnv.OPENCLAW_SESSION_KEY = sessionKey;
34526
34925
  if (event.toolCallId)
34527
34926
  injectedEnv.OPENCLAW_TOOL_CALL_ID = event.toolCallId;
@@ -34533,6 +34932,8 @@ function registerParallHooks(api) {
34533
34932
  };
34534
34933
  });
34535
34934
  api.on("after_tool_call", async (event, ctx) => {
34935
+ if (ctx.sessionKey)
34936
+ endToolRenewal(ctx.sessionKey);
34536
34937
  if (ctx.sessionKey && event.toolCallId && pendingSendCalls.delete(event.toolCallId)) {
34537
34938
  recordMessageSend(ctx.sessionKey, !event.error);
34538
34939
  }