@parall/daemon 1.43.0 → 1.44.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.
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "1.43.0",
3
- "built_at": "2026-07-13T04:04:48.217Z",
2
+ "version": "1.44.0",
3
+ "built_at": "2026-07-14T07:01:33.876Z",
4
4
  "min_node_version": "20.0.0",
5
5
  "files": {
6
6
  "bb-browser-daemon.js": {
@@ -16,29 +16,29 @@
16
16
  "size": 18
17
17
  },
18
18
  "parall-browser-pod.js": {
19
- "sha256": "464b249ca4be9ed4ebc520220f11b94fe78c4ceedda8cbfd3dee7c7e02ae4c38",
20
- "size": 1699655
19
+ "sha256": "81dbdef2606b3295d94101d672f514f6fab6d7c105716b4fe9f917bb9b404b1a",
20
+ "size": 1699775
21
21
  },
22
22
  "parall-channel-exec.js": {
23
23
  "sha256": "32c438151df40ccda7491c57bfa0cd9d6c4d762755353c6c751d5793af57b971",
24
24
  "size": 7097
25
25
  },
26
26
  "parall-claude-agent.js": {
27
- "sha256": "084f01f1e64aa582dde65ee1eaf3d85a99b7960451a55b1157cb2ad61816d7bd",
28
- "size": 1639412
27
+ "sha256": "f7dadfab076bcb55a7aea0d79fdd59b93200dad8c8ef674ac1879e53720fd946",
28
+ "size": 1644021
29
29
  },
30
30
  "parall-codex-agent.js": {
31
- "sha256": "4fa941ad4643333e97feb88dd728ec0badebc5cba21206dbe22d08a40cd76cf6",
32
- "size": 1676742
31
+ "sha256": "4f77525ca21caa389106feb7aef1f630613ec91dedbf366ed52fc3c70b391607",
32
+ "size": 1681351
33
33
  },
34
34
  "parall-daemon.js": {
35
- "sha256": "82f1047c975cb47e31063863a752163a756b857c962d63a7df9acbcdfe0c450d",
36
- "size": 1726872
35
+ "sha256": "b880b10ae5b932968178ff1f216ad13859afdd02cca332fdd900279dc3ba87ea",
36
+ "size": 1727004
37
37
  },
38
38
  "parall-openclaw-agent.js": {
39
39
  "sha256": "505433b4bf3a4b8dbb7b96978d8693d6c48079f4513282ff1ee59a6d13b3eddf",
40
40
  "size": 9801
41
41
  }
42
42
  },
43
- "signature": "YRCOk2+FiAfFASvIWGt7sDPkt+iZE1yfUfVnu793pKZhbSmjKedFXwq6WdUM+lpM6WqL09/JPSf9d3gmDNc9AA=="
43
+ "signature": "WChU4cNRetS9GZz1kISiZkkXjvwR5fSi4QeMiIyZx9r1FBio4CuLqLI7aUFoSEyTzTDSAH9mJS7jdV/csZvfBQ=="
44
44
  }
@@ -36443,7 +36443,6 @@ var ParallClient = class _ParallClient {
36443
36443
  async steerDispatch(orgId, req) {
36444
36444
  return this.request("POST", ENDPOINTS.DISPATCH_STEER(orgId), req);
36445
36445
  }
36446
- /** End a turn: no_action sweep of the lane's members + lane release + re-drive check. */
36447
36446
  async completeDispatch(orgId, req) {
36448
36447
  return this.request("POST", ENDPOINTS.DISPATCH_COMPLETE(orgId), req);
36449
36448
  }
@@ -36451,6 +36450,10 @@ var ParallClient = class _ParallClient {
36451
36450
  * End a turn for a lane-less runtime: resolve the turn's folded WorkItems
36452
36451
  * by source — broad-cover to the turn's reply Effect when one exists,
36453
36452
  * no_action sweep otherwise. Idempotent.
36453
+ *
36454
+ * @deprecated Legacy ok-only alias — use {@link completeDispatch} with the
36455
+ * `sources` form, which also carries `turn_outcome`. The endpoint retires
36456
+ * at S3b (dispatch-convergence-design.md §3).
36454
36457
  */
36455
36458
  async completeDispatchSources(orgId, req) {
36456
36459
  return this.request("POST", ENDPOINTS.DISPATCH_COMPLETE_SOURCES(orgId), req);
@@ -27944,7 +27944,6 @@ var ParallClient = class _ParallClient {
27944
27944
  async steerDispatch(orgId, req) {
27945
27945
  return this.request("POST", ENDPOINTS.DISPATCH_STEER(orgId), req);
27946
27946
  }
27947
- /** End a turn: no_action sweep of the lane's members + lane release + re-drive check. */
27948
27947
  async completeDispatch(orgId, req) {
27949
27948
  return this.request("POST", ENDPOINTS.DISPATCH_COMPLETE(orgId), req);
27950
27949
  }
@@ -27952,6 +27951,10 @@ var ParallClient = class _ParallClient {
27952
27951
  * End a turn for a lane-less runtime: resolve the turn's folded WorkItems
27953
27952
  * by source — broad-cover to the turn's reply Effect when one exists,
27954
27953
  * no_action sweep otherwise. Idempotent.
27954
+ *
27955
+ * @deprecated Legacy ok-only alias — use {@link completeDispatch} with the
27956
+ * `sources` form, which also carries `turn_outcome`. The endpoint retires
27957
+ * at S3b (dispatch-convergence-design.md §3).
27955
27958
  */
27956
27959
  async completeDispatchSources(orgId, req) {
27957
27960
  return this.request("POST", ENDPOINTS.DISPATCH_COMPLETE_SOURCES(orgId), req);
@@ -29157,7 +29160,11 @@ var LaneLedger = class {
29157
29160
  throw err;
29158
29161
  }
29159
29162
  if (!res.claimed || !res.lane) {
29160
- this.opts.log?.info(`lane for ${targetUri} held by a healthy incumbent \u2014 leaving events pending for re-drive`);
29163
+ if (res.reason === "empty") {
29164
+ this.opts.log?.warn(`claim for ${targetUri} came back empty \u2014 nothing foldable; leaving to the reconciler`);
29165
+ } else {
29166
+ this.opts.log?.info(`lane for ${targetUri} held by a healthy incumbent \u2014 leaving events pending for re-drive`);
29167
+ }
29161
29168
  return null;
29162
29169
  }
29163
29170
  const leaseUntilMs = Date.parse(res.lease_until ?? "");
@@ -29234,6 +29241,18 @@ var LaneLedger = class {
29234
29241
  * re-drives any same-target pending work. A STALE_LANE answer means a
29235
29242
  * takeover already owns the resource — local state is dropped either way.
29236
29243
  */
29244
+ /**
29245
+ * Record that the turn on this lane surfaced a runtime error. The flow
29246
+ * settles an errored lane immediately (dispatchLaneGroup returns 'failed'
29247
+ * after a forced complete), so the bit normally lives for one turn only —
29248
+ * it is the transport between the gateway's per-session error signal and
29249
+ * this lane's complete request.
29250
+ */
29251
+ markTurnError(laneKey) {
29252
+ const lane = this.lanes.get(laneKey);
29253
+ if (lane)
29254
+ lane.turnError = true;
29255
+ }
29237
29256
  async completeIfIdle(laneKey, hasMoreLocal) {
29238
29257
  const lane = this.lanes.get(laneKey);
29239
29258
  if (!lane || hasMoreLocal)
@@ -29244,7 +29263,10 @@ var LaneLedger = class {
29244
29263
  const res = await this.opts.client.completeDispatch(this.opts.orgId, {
29245
29264
  lane: lane.lane,
29246
29265
  target_uri: lane.targetUri,
29247
- thread_root_id: lane.threadRootId
29266
+ thread_root_id: lane.threadRootId,
29267
+ // An error turn releases its members for retry instead of sweeping
29268
+ // them as handled (ignored by older servers).
29269
+ turn_outcome: lane.turnError ? "error" : "ok"
29248
29270
  });
29249
29271
  if (res.swept_no_action > 0 || res.redriven) {
29250
29272
  this.opts.log?.info(`lane complete for ${lane.targetUri}: swept ${res.swept_no_action} no_action, redriven=${res.redriven}`);
@@ -29257,6 +29279,17 @@ var LaneLedger = class {
29257
29279
  this.opts.log?.warn(`lane complete failed for ${lane.targetUri}: ${String(err)}`);
29258
29280
  }
29259
29281
  }
29282
+ /**
29283
+ * Renew one lane by its key — the external runtime-activity hook for
29284
+ * adapters whose tool traffic bypasses the RuntimeEvent stream (openclaw
29285
+ * hooks). Scoped to the session's own lane: renewing every lane would let
29286
+ * one busy fork keep an unrelated stalled fork's lane leased forever.
29287
+ */
29288
+ renewByKey(laneKey) {
29289
+ const lane = this.lanes.get(laneKey);
29290
+ if (lane)
29291
+ this.maybeRenew(lane);
29292
+ }
29260
29293
  /**
29261
29294
  * Long-turn keepalive: renew the lane's lease on runtime activity, throttled
29262
29295
  * so a chatty turn doesn't spam the server. Without this, a legitimately
@@ -29398,17 +29431,34 @@ async function dispatchLaneGroup(host, opts) {
29398
29431
  }
29399
29432
  return "foreign";
29400
29433
  }
29434
+ host.noteSessionLane(opts.sessionKey, lane.laneKey);
29401
29435
  let dispatched = false;
29402
29436
  try {
29403
29437
  dispatched = await host.runDispatch(event, opts.sessionKey, opts.body, opts.earlier, opts.captureText);
29404
29438
  } catch (err) {
29439
+ host.noteSessionLane(opts.sessionKey, null);
29405
29440
  await ledger.release(lane.laneKey).catch(() => {
29406
29441
  });
29407
29442
  throw err;
29443
+ } finally {
29444
+ if (dispatched)
29445
+ host.noteSessionLane(opts.sessionKey, null);
29408
29446
  }
29409
29447
  if (!dispatched) {
29410
29448
  return "shutdown";
29411
29449
  }
29450
+ if (host.consumeTurnError(opts.sessionKey)) {
29451
+ ledger.markTurnError(lane.laneKey);
29452
+ for (const msgId of lane.folded.keys()) {
29453
+ host.dispatchedMessages.delete(msgId);
29454
+ }
29455
+ try {
29456
+ host.opts.dispatchAdapter.abortDispatch?.(opts.sessionKey);
29457
+ } catch {
29458
+ }
29459
+ await ledger.completeIfIdle(lane.laneKey, false);
29460
+ return "failed";
29461
+ }
29412
29462
  const pendingInjections = host.opts.dispatchAdapter.hasPendingInjections?.(opts.sessionKey) ?? false;
29413
29463
  await ledger.completeIfIdle(lane.laneKey, pendingInjections || opts.hasMoreLocal());
29414
29464
  return "dispatched";
@@ -30791,6 +30841,44 @@ var ParallAgentGateway = class {
30791
30841
  this.dispatchedMessages.add(id);
30792
30842
  return true;
30793
30843
  }
30844
+ /**
30845
+ * Lane currently being dispatched per session — lets external activity
30846
+ * signals renew exactly the caller's lane (renewing all lanes would keep
30847
+ * an unrelated stalled fork's lane leased forever).
30848
+ */
30849
+ sessionActiveLanes = /* @__PURE__ */ new Map();
30850
+ noteSessionLane(sessionKey, laneKey) {
30851
+ if (laneKey == null)
30852
+ this.sessionActiveLanes.delete(sessionKey);
30853
+ else
30854
+ this.sessionActiveLanes.set(sessionKey, laneKey);
30855
+ }
30856
+ /**
30857
+ * External runtime-activity signal for adapters whose tool activity does
30858
+ * not flow through the RuntimeEvent stream (openclaw hooks call this from
30859
+ * the tool-call lifecycle): renews the session's OWN active ledger lane so
30860
+ * a long tool call cannot outlive the lease and get dethroned mid-turn.
30861
+ * No-op without an active ledger lane for the session.
30862
+ */
30863
+ touchRuntimeActivity(sessionKey) {
30864
+ if (this.ledgerDisabled)
30865
+ return;
30866
+ const laneKey = this.sessionActiveLanes.get(sessionKey);
30867
+ if (laneKey)
30868
+ this.laneLedger?.renewByKey(laneKey);
30869
+ }
30870
+ /** Sessions whose in-flight turn surfaced a runtime error event. */
30871
+ turnErrorSessions = /* @__PURE__ */ new Set();
30872
+ /**
30873
+ * Consume (read-and-clear) the error marker for sessionKey's last turn.
30874
+ * Feeds complete's turn_outcome so an error turn's lane members are
30875
+ * released for retry instead of no_action-swept (design §3). Consuming
30876
+ * (rather than peeking) keeps one-shot fork session keys from accumulating
30877
+ * in the set forever.
30878
+ */
30879
+ consumeTurnError(sessionKey) {
30880
+ return this.turnErrorSessions.delete(sessionKey);
30881
+ }
30794
30882
  async emitDispatchReceived(event) {
30795
30883
  const sourceType = event.ackSourceType ?? (event.type === "task" ? "task_activity" : "message");
30796
30884
  const sourceId = event.ackSourceId ?? event.messageId;
@@ -31140,6 +31228,7 @@ var ParallAgentGateway = class {
31140
31228
  this.pendingRestartNotification = null;
31141
31229
  }
31142
31230
  resetDispatchMetrics(sessionKey);
31231
+ this.turnErrorSessions.delete(sessionKey);
31143
31232
  return runWithSessionKey(sessionKey, async () => {
31144
31233
  let dispatchSpan = null;
31145
31234
  setSessionChatId(sessionKey, event.targetId);
@@ -31247,6 +31336,9 @@ var ParallAgentGateway = class {
31247
31336
  } else if (runtimeEvent.type === "tool_result" && pendingSendCallIds.delete(runtimeEvent.callId)) {
31248
31337
  recordMessageSend(sessionKey, !runtimeEvent.error);
31249
31338
  }
31339
+ if (runtimeEvent.type === "error") {
31340
+ this.turnErrorSessions.add(sessionKey);
31341
+ }
31250
31342
  await this.createRuntimeStep(binding.agentSessionId, event, runtimeEvent, stepIdFilePath, contextFilePath, laneContextFilePath2);
31251
31343
  }
31252
31344
  if (!binding) {
@@ -31394,13 +31486,23 @@ var ParallAgentGateway = class {
31394
31486
  body: buildForkScopePrefix(last) + buildEventBody(last),
31395
31487
  earlier,
31396
31488
  captureText: batchText,
31397
- hasMoreLocal: () => fork.queue.length > 0
31489
+ // Per-LANE residue check (parity with the main-buffer path):
31490
+ // the fork queue can hold several lanes (channel + thread of
31491
+ // the same chat). A whole-queue check would defer THIS lane's
31492
+ // complete behind another lane's items and never revisit it —
31493
+ // its members would sit received until lease expiry.
31494
+ hasMoreLocal: () => fork.queue.some((it) => this.dispatchGroupKey(it.event) === this.dispatchGroupKey(last))
31398
31495
  });
31399
31496
  if (outcome === "foreign") {
31400
31497
  for (const item of items)
31401
31498
  item.resolve(false);
31402
31499
  break;
31403
31500
  }
31501
+ if (outcome === "failed") {
31502
+ for (const item of items)
31503
+ item.resolve(false);
31504
+ continue;
31505
+ }
31404
31506
  dispatched = outcome === "dispatched";
31405
31507
  } else {
31406
31508
  dispatched = await this.runDispatch(last, fork.fork.sessionKey, buildForkScopePrefix(last) + buildEventBody(last), earlier, batchText);
@@ -31434,6 +31536,7 @@ var ParallAgentGateway = class {
31434
31536
  remaining.resolve(false);
31435
31537
  }
31436
31538
  } finally {
31539
+ this.turnErrorSessions.delete(fork.fork.sessionKey);
31437
31540
  if (fork.deadlineTimer) {
31438
31541
  clearTimeout(fork.deadlineTimer);
31439
31542
  fork.deadlineTimer = null;
@@ -31546,6 +31649,10 @@ var ParallAgentGateway = class {
31546
31649
  this.dispatchState.pendingForkResults.unshift(...pendingFork);
31547
31650
  continue;
31548
31651
  }
31652
+ if (outcome === "failed") {
31653
+ this.dispatchState.pendingForkResults.unshift(...pendingFork);
31654
+ continue;
31655
+ }
31549
31656
  continue;
31550
31657
  }
31551
31658
  try {
@@ -31649,7 +31756,7 @@ var ParallAgentGateway = class {
31649
31756
  }
31650
31757
  this.dispatchState.mainBuffer.push(event);
31651
31758
  if (this.usesLaneLedger(event)) {
31652
- if (this.mainCurrentGroupKey === this.dispatchGroupKey(event) && await this.laneLedger?.steerLive(event) && await this.opts.dispatchAdapter.enqueueDuringDispatch?.(this.opts.runtimeKey, buildEventBody(event))) {
31759
+ 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))) {
31653
31760
  this.opts.log?.info(`steer folded+injected for ${event.messageId} (will drain for bookkeeping)`);
31654
31761
  }
31655
31762
  } else if (this.dispatchState.mainCurrentTargetId === event.targetId && await this.opts.dispatchAdapter.enqueueDuringDispatch?.(this.opts.runtimeKey, buildEventBody(event))) {
@@ -31676,12 +31783,14 @@ var ParallAgentGateway = class {
31676
31783
  this.dispatchState.mainBuffer.push(event);
31677
31784
  return false;
31678
31785
  }
31679
- try {
31680
- await this.emitDispatchReceived(event);
31681
- } catch (err) {
31682
- this.opts.log?.warn?.(`mark-received failed for fork dispatch, leaving unacked for retry: ${String(err)}`);
31683
- this.dispatchState.mainBuffer.push(event);
31684
- return false;
31786
+ if (!this.usesLaneLedger(event)) {
31787
+ try {
31788
+ await this.emitDispatchReceived(event);
31789
+ } catch (err) {
31790
+ this.opts.log?.warn?.(`mark-received failed for fork dispatch, leaving unacked for retry: ${String(err)}`);
31791
+ this.dispatchState.mainBuffer.push(event);
31792
+ return false;
31793
+ }
31685
31794
  }
31686
31795
  const fork = await this.opts.dispatchAdapter.forkSession({
31687
31796
  sessionKey: this.opts.runtimeKey,
@@ -27944,7 +27944,6 @@ var ParallClient = class _ParallClient {
27944
27944
  async steerDispatch(orgId, req) {
27945
27945
  return this.request("POST", ENDPOINTS.DISPATCH_STEER(orgId), req);
27946
27946
  }
27947
- /** End a turn: no_action sweep of the lane's members + lane release + re-drive check. */
27948
27947
  async completeDispatch(orgId, req) {
27949
27948
  return this.request("POST", ENDPOINTS.DISPATCH_COMPLETE(orgId), req);
27950
27949
  }
@@ -27952,6 +27951,10 @@ var ParallClient = class _ParallClient {
27952
27951
  * End a turn for a lane-less runtime: resolve the turn's folded WorkItems
27953
27952
  * by source — broad-cover to the turn's reply Effect when one exists,
27954
27953
  * no_action sweep otherwise. Idempotent.
27954
+ *
27955
+ * @deprecated Legacy ok-only alias — use {@link completeDispatch} with the
27956
+ * `sources` form, which also carries `turn_outcome`. The endpoint retires
27957
+ * at S3b (dispatch-convergence-design.md §3).
27955
27958
  */
27956
27959
  async completeDispatchSources(orgId, req) {
27957
27960
  return this.request("POST", ENDPOINTS.DISPATCH_COMPLETE_SOURCES(orgId), req);
@@ -29157,7 +29160,11 @@ var LaneLedger = class {
29157
29160
  throw err;
29158
29161
  }
29159
29162
  if (!res.claimed || !res.lane) {
29160
- this.opts.log?.info(`lane for ${targetUri} held by a healthy incumbent \u2014 leaving events pending for re-drive`);
29163
+ if (res.reason === "empty") {
29164
+ this.opts.log?.warn(`claim for ${targetUri} came back empty \u2014 nothing foldable; leaving to the reconciler`);
29165
+ } else {
29166
+ this.opts.log?.info(`lane for ${targetUri} held by a healthy incumbent \u2014 leaving events pending for re-drive`);
29167
+ }
29161
29168
  return null;
29162
29169
  }
29163
29170
  const leaseUntilMs = Date.parse(res.lease_until ?? "");
@@ -29234,6 +29241,18 @@ var LaneLedger = class {
29234
29241
  * re-drives any same-target pending work. A STALE_LANE answer means a
29235
29242
  * takeover already owns the resource — local state is dropped either way.
29236
29243
  */
29244
+ /**
29245
+ * Record that the turn on this lane surfaced a runtime error. The flow
29246
+ * settles an errored lane immediately (dispatchLaneGroup returns 'failed'
29247
+ * after a forced complete), so the bit normally lives for one turn only —
29248
+ * it is the transport between the gateway's per-session error signal and
29249
+ * this lane's complete request.
29250
+ */
29251
+ markTurnError(laneKey) {
29252
+ const lane = this.lanes.get(laneKey);
29253
+ if (lane)
29254
+ lane.turnError = true;
29255
+ }
29237
29256
  async completeIfIdle(laneKey, hasMoreLocal) {
29238
29257
  const lane = this.lanes.get(laneKey);
29239
29258
  if (!lane || hasMoreLocal)
@@ -29244,7 +29263,10 @@ var LaneLedger = class {
29244
29263
  const res = await this.opts.client.completeDispatch(this.opts.orgId, {
29245
29264
  lane: lane.lane,
29246
29265
  target_uri: lane.targetUri,
29247
- thread_root_id: lane.threadRootId
29266
+ thread_root_id: lane.threadRootId,
29267
+ // An error turn releases its members for retry instead of sweeping
29268
+ // them as handled (ignored by older servers).
29269
+ turn_outcome: lane.turnError ? "error" : "ok"
29248
29270
  });
29249
29271
  if (res.swept_no_action > 0 || res.redriven) {
29250
29272
  this.opts.log?.info(`lane complete for ${lane.targetUri}: swept ${res.swept_no_action} no_action, redriven=${res.redriven}`);
@@ -29257,6 +29279,17 @@ var LaneLedger = class {
29257
29279
  this.opts.log?.warn(`lane complete failed for ${lane.targetUri}: ${String(err)}`);
29258
29280
  }
29259
29281
  }
29282
+ /**
29283
+ * Renew one lane by its key — the external runtime-activity hook for
29284
+ * adapters whose tool traffic bypasses the RuntimeEvent stream (openclaw
29285
+ * hooks). Scoped to the session's own lane: renewing every lane would let
29286
+ * one busy fork keep an unrelated stalled fork's lane leased forever.
29287
+ */
29288
+ renewByKey(laneKey) {
29289
+ const lane = this.lanes.get(laneKey);
29290
+ if (lane)
29291
+ this.maybeRenew(lane);
29292
+ }
29260
29293
  /**
29261
29294
  * Long-turn keepalive: renew the lane's lease on runtime activity, throttled
29262
29295
  * so a chatty turn doesn't spam the server. Without this, a legitimately
@@ -29398,17 +29431,34 @@ async function dispatchLaneGroup(host, opts) {
29398
29431
  }
29399
29432
  return "foreign";
29400
29433
  }
29434
+ host.noteSessionLane(opts.sessionKey, lane.laneKey);
29401
29435
  let dispatched = false;
29402
29436
  try {
29403
29437
  dispatched = await host.runDispatch(event, opts.sessionKey, opts.body, opts.earlier, opts.captureText);
29404
29438
  } catch (err) {
29439
+ host.noteSessionLane(opts.sessionKey, null);
29405
29440
  await ledger.release(lane.laneKey).catch(() => {
29406
29441
  });
29407
29442
  throw err;
29443
+ } finally {
29444
+ if (dispatched)
29445
+ host.noteSessionLane(opts.sessionKey, null);
29408
29446
  }
29409
29447
  if (!dispatched) {
29410
29448
  return "shutdown";
29411
29449
  }
29450
+ if (host.consumeTurnError(opts.sessionKey)) {
29451
+ ledger.markTurnError(lane.laneKey);
29452
+ for (const msgId of lane.folded.keys()) {
29453
+ host.dispatchedMessages.delete(msgId);
29454
+ }
29455
+ try {
29456
+ host.opts.dispatchAdapter.abortDispatch?.(opts.sessionKey);
29457
+ } catch {
29458
+ }
29459
+ await ledger.completeIfIdle(lane.laneKey, false);
29460
+ return "failed";
29461
+ }
29412
29462
  const pendingInjections = host.opts.dispatchAdapter.hasPendingInjections?.(opts.sessionKey) ?? false;
29413
29463
  await ledger.completeIfIdle(lane.laneKey, pendingInjections || opts.hasMoreLocal());
29414
29464
  return "dispatched";
@@ -30791,6 +30841,44 @@ var ParallAgentGateway = class {
30791
30841
  this.dispatchedMessages.add(id);
30792
30842
  return true;
30793
30843
  }
30844
+ /**
30845
+ * Lane currently being dispatched per session — lets external activity
30846
+ * signals renew exactly the caller's lane (renewing all lanes would keep
30847
+ * an unrelated stalled fork's lane leased forever).
30848
+ */
30849
+ sessionActiveLanes = /* @__PURE__ */ new Map();
30850
+ noteSessionLane(sessionKey, laneKey) {
30851
+ if (laneKey == null)
30852
+ this.sessionActiveLanes.delete(sessionKey);
30853
+ else
30854
+ this.sessionActiveLanes.set(sessionKey, laneKey);
30855
+ }
30856
+ /**
30857
+ * External runtime-activity signal for adapters whose tool activity does
30858
+ * not flow through the RuntimeEvent stream (openclaw hooks call this from
30859
+ * the tool-call lifecycle): renews the session's OWN active ledger lane so
30860
+ * a long tool call cannot outlive the lease and get dethroned mid-turn.
30861
+ * No-op without an active ledger lane for the session.
30862
+ */
30863
+ touchRuntimeActivity(sessionKey) {
30864
+ if (this.ledgerDisabled)
30865
+ return;
30866
+ const laneKey = this.sessionActiveLanes.get(sessionKey);
30867
+ if (laneKey)
30868
+ this.laneLedger?.renewByKey(laneKey);
30869
+ }
30870
+ /** Sessions whose in-flight turn surfaced a runtime error event. */
30871
+ turnErrorSessions = /* @__PURE__ */ new Set();
30872
+ /**
30873
+ * Consume (read-and-clear) the error marker for sessionKey's last turn.
30874
+ * Feeds complete's turn_outcome so an error turn's lane members are
30875
+ * released for retry instead of no_action-swept (design §3). Consuming
30876
+ * (rather than peeking) keeps one-shot fork session keys from accumulating
30877
+ * in the set forever.
30878
+ */
30879
+ consumeTurnError(sessionKey) {
30880
+ return this.turnErrorSessions.delete(sessionKey);
30881
+ }
30794
30882
  async emitDispatchReceived(event) {
30795
30883
  const sourceType = event.ackSourceType ?? (event.type === "task" ? "task_activity" : "message");
30796
30884
  const sourceId = event.ackSourceId ?? event.messageId;
@@ -31140,6 +31228,7 @@ var ParallAgentGateway = class {
31140
31228
  this.pendingRestartNotification = null;
31141
31229
  }
31142
31230
  resetDispatchMetrics(sessionKey);
31231
+ this.turnErrorSessions.delete(sessionKey);
31143
31232
  return runWithSessionKey(sessionKey, async () => {
31144
31233
  let dispatchSpan = null;
31145
31234
  setSessionChatId(sessionKey, event.targetId);
@@ -31247,6 +31336,9 @@ var ParallAgentGateway = class {
31247
31336
  } else if (runtimeEvent.type === "tool_result" && pendingSendCallIds.delete(runtimeEvent.callId)) {
31248
31337
  recordMessageSend(sessionKey, !runtimeEvent.error);
31249
31338
  }
31339
+ if (runtimeEvent.type === "error") {
31340
+ this.turnErrorSessions.add(sessionKey);
31341
+ }
31250
31342
  await this.createRuntimeStep(binding.agentSessionId, event, runtimeEvent, stepIdFilePath, contextFilePath, laneContextFilePath2);
31251
31343
  }
31252
31344
  if (!binding) {
@@ -31394,13 +31486,23 @@ var ParallAgentGateway = class {
31394
31486
  body: buildForkScopePrefix(last) + buildEventBody(last),
31395
31487
  earlier,
31396
31488
  captureText: batchText,
31397
- hasMoreLocal: () => fork.queue.length > 0
31489
+ // Per-LANE residue check (parity with the main-buffer path):
31490
+ // the fork queue can hold several lanes (channel + thread of
31491
+ // the same chat). A whole-queue check would defer THIS lane's
31492
+ // complete behind another lane's items and never revisit it —
31493
+ // its members would sit received until lease expiry.
31494
+ hasMoreLocal: () => fork.queue.some((it) => this.dispatchGroupKey(it.event) === this.dispatchGroupKey(last))
31398
31495
  });
31399
31496
  if (outcome === "foreign") {
31400
31497
  for (const item of items)
31401
31498
  item.resolve(false);
31402
31499
  break;
31403
31500
  }
31501
+ if (outcome === "failed") {
31502
+ for (const item of items)
31503
+ item.resolve(false);
31504
+ continue;
31505
+ }
31404
31506
  dispatched = outcome === "dispatched";
31405
31507
  } else {
31406
31508
  dispatched = await this.runDispatch(last, fork.fork.sessionKey, buildForkScopePrefix(last) + buildEventBody(last), earlier, batchText);
@@ -31434,6 +31536,7 @@ var ParallAgentGateway = class {
31434
31536
  remaining.resolve(false);
31435
31537
  }
31436
31538
  } finally {
31539
+ this.turnErrorSessions.delete(fork.fork.sessionKey);
31437
31540
  if (fork.deadlineTimer) {
31438
31541
  clearTimeout(fork.deadlineTimer);
31439
31542
  fork.deadlineTimer = null;
@@ -31546,6 +31649,10 @@ var ParallAgentGateway = class {
31546
31649
  this.dispatchState.pendingForkResults.unshift(...pendingFork);
31547
31650
  continue;
31548
31651
  }
31652
+ if (outcome === "failed") {
31653
+ this.dispatchState.pendingForkResults.unshift(...pendingFork);
31654
+ continue;
31655
+ }
31549
31656
  continue;
31550
31657
  }
31551
31658
  try {
@@ -31649,7 +31756,7 @@ var ParallAgentGateway = class {
31649
31756
  }
31650
31757
  this.dispatchState.mainBuffer.push(event);
31651
31758
  if (this.usesLaneLedger(event)) {
31652
- if (this.mainCurrentGroupKey === this.dispatchGroupKey(event) && await this.laneLedger?.steerLive(event) && await this.opts.dispatchAdapter.enqueueDuringDispatch?.(this.opts.runtimeKey, buildEventBody(event))) {
31759
+ 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))) {
31653
31760
  this.opts.log?.info(`steer folded+injected for ${event.messageId} (will drain for bookkeeping)`);
31654
31761
  }
31655
31762
  } else if (this.dispatchState.mainCurrentTargetId === event.targetId && await this.opts.dispatchAdapter.enqueueDuringDispatch?.(this.opts.runtimeKey, buildEventBody(event))) {
@@ -31676,12 +31783,14 @@ var ParallAgentGateway = class {
31676
31783
  this.dispatchState.mainBuffer.push(event);
31677
31784
  return false;
31678
31785
  }
31679
- try {
31680
- await this.emitDispatchReceived(event);
31681
- } catch (err) {
31682
- this.opts.log?.warn?.(`mark-received failed for fork dispatch, leaving unacked for retry: ${String(err)}`);
31683
- this.dispatchState.mainBuffer.push(event);
31684
- return false;
31786
+ if (!this.usesLaneLedger(event)) {
31787
+ try {
31788
+ await this.emitDispatchReceived(event);
31789
+ } catch (err) {
31790
+ this.opts.log?.warn?.(`mark-received failed for fork dispatch, leaving unacked for retry: ${String(err)}`);
31791
+ this.dispatchState.mainBuffer.push(event);
31792
+ return false;
31793
+ }
31685
31794
  }
31686
31795
  const fork = await this.opts.dispatchAdapter.forkSession({
31687
31796
  sessionKey: this.opts.runtimeKey,
@@ -1625,7 +1625,6 @@ var init_client = __esm({
1625
1625
  async steerDispatch(orgId, req) {
1626
1626
  return this.request("POST", ENDPOINTS.DISPATCH_STEER(orgId), req);
1627
1627
  }
1628
- /** End a turn: no_action sweep of the lane's members + lane release + re-drive check. */
1629
1628
  async completeDispatch(orgId, req) {
1630
1629
  return this.request("POST", ENDPOINTS.DISPATCH_COMPLETE(orgId), req);
1631
1630
  }
@@ -1633,6 +1632,10 @@ var init_client = __esm({
1633
1632
  * End a turn for a lane-less runtime: resolve the turn's folded WorkItems
1634
1633
  * by source — broad-cover to the turn's reply Effect when one exists,
1635
1634
  * no_action sweep otherwise. Idempotent.
1635
+ *
1636
+ * @deprecated Legacy ok-only alias — use {@link completeDispatch} with the
1637
+ * `sources` form, which also carries `turn_outcome`. The endpoint retires
1638
+ * at S3b (dispatch-convergence-design.md §3).
1636
1639
  */
1637
1640
  async completeDispatchSources(orgId, req) {
1638
1641
  return this.request("POST", ENDPOINTS.DISPATCH_COMPLETE_SOURCES(orgId), req);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/daemon",
3
- "version": "1.43.0",
3
+ "version": "1.44.0",
4
4
  "description": "Parall local agent runtime — daemon supervisor + bridge runtimes, bundled as standalone JS files",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -32,11 +32,11 @@
32
32
  "dependencies": {
33
33
  "@aws-sdk/client-s3": "3.984.0",
34
34
  "@pinixai/bb-browser-pro": "0.15.0",
35
- "@parall/agent-core": "1.43.0",
36
- "@parall/sdk": "1.43.0",
37
- "@parall/claude-agent": "1.43.0",
38
- "@parall/codex-agent": "1.43.0",
39
- "@parall/openclaw-agent": "1.43.0"
35
+ "@parall/agent-core": "1.44.0",
36
+ "@parall/claude-agent": "1.44.0",
37
+ "@parall/sdk": "1.44.0",
38
+ "@parall/codex-agent": "1.44.0",
39
+ "@parall/openclaw-agent": "1.44.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/node": "^22.0.0",