@parall/daemon 1.37.0 → 1.38.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.
@@ -17718,9 +17718,9 @@ var require_getMachineId_linux = __commonJS({
17718
17718
  var api_1 = (init_esm(), __toCommonJS(esm_exports));
17719
17719
  async function getMachineId() {
17720
17720
  const paths = ["/etc/machine-id", "/var/lib/dbus/machine-id"];
17721
- for (const path9 of paths) {
17721
+ for (const path10 of paths) {
17722
17722
  try {
17723
- const result = await fs_1.promises.readFile(path9, { encoding: "utf8" });
17723
+ const result = await fs_1.promises.readFile(path10, { encoding: "utf8" });
17724
17724
  return result.trim();
17725
17725
  } catch (e) {
17726
17726
  api_1.diag.debug(`error reading machine id: ${e}`);
@@ -21123,7 +21123,7 @@ function appendRootPathToUrlIfNeeded(url) {
21123
21123
  return void 0;
21124
21124
  }
21125
21125
  }
21126
- function appendResourcePathToUrl(url, path9) {
21126
+ function appendResourcePathToUrl(url, path10) {
21127
21127
  try {
21128
21128
  new URL(url);
21129
21129
  } catch (_a) {
@@ -21133,11 +21133,11 @@ function appendResourcePathToUrl(url, path9) {
21133
21133
  if (!url.endsWith("/")) {
21134
21134
  url = url + "/";
21135
21135
  }
21136
- url += path9;
21136
+ url += path10;
21137
21137
  try {
21138
21138
  new URL(url);
21139
21139
  } catch (_b) {
21140
- diag2.warn("Configuration: Provided URL appended with '" + path9 + "' is not a valid URL, using 'undefined' instead of '" + url + "'");
21140
+ diag2.warn("Configuration: Provided URL appended with '" + path10 + "' is not a valid URL, using 'undefined' instead of '" + url + "'");
21141
21141
  return void 0;
21142
21142
  }
21143
21143
  return url;
@@ -26474,6 +26474,19 @@ function parseProviderConfig(env) {
26474
26474
  }
26475
26475
  }
26476
26476
 
26477
+ // ts/agent-core/dist/lane-key.js
26478
+ import * as path from "node:path";
26479
+ function laneKeyForTarget(targetUri, threadRootId) {
26480
+ return Buffer.from(`${targetUri}
26481
+ ${threadRootId ?? ""}`, "utf8").toString("base64url");
26482
+ }
26483
+ function dispatchLaneContextDir(stateDir) {
26484
+ return path.join(stateDir, "dispatch-lane-context");
26485
+ }
26486
+ function laneContextFilePath(contextDir, targetUri, threadRootId) {
26487
+ return path.join(contextDir, `${laneKeyForTarget(targetUri, threadRootId)}.json`);
26488
+ }
26489
+
26477
26490
  // ts/agent-core/dist/session-state.js
26478
26491
  function normalizeSessionKey(sessionKey) {
26479
26492
  return sessionKey.toLowerCase();
@@ -27102,8 +27115,8 @@ function childLogger(logger, sub) {
27102
27115
 
27103
27116
  // ts/agent-core/dist/gateway-base.js
27104
27117
  import * as os from "node:os";
27105
- import * as fs from "node:fs";
27106
- import * as path from "node:path";
27118
+ import * as fs2 from "node:fs";
27119
+ import * as path2 from "node:path";
27107
27120
 
27108
27121
  // ts/sdk/dist/types.js
27109
27122
  var MENTION_ALL_USER_ID = "all";
@@ -27378,6 +27391,11 @@ var ENDPOINTS = {
27378
27391
  DISPATCH_ACK_BY_ID: (orgId, id) => `${API_BASE}/orgs/${orgId}/dispatch/${id}/ack`,
27379
27392
  DISPATCH_EXPIRE: (orgId) => `${API_BASE}/orgs/${orgId}/dispatch/expire`,
27380
27393
  DISPATCH_BY_MESSAGES: (orgId) => `${API_BASE}/orgs/${orgId}/dispatch/by-messages`,
27394
+ DISPATCH_CLAIM: (orgId) => `${API_BASE}/orgs/${orgId}/dispatch/claim`,
27395
+ DISPATCH_STEER: (orgId) => `${API_BASE}/orgs/${orgId}/dispatch/steer`,
27396
+ DISPATCH_COMPLETE: (orgId) => `${API_BASE}/orgs/${orgId}/dispatch/complete`,
27397
+ DISPATCH_RELEASE: (orgId) => `${API_BASE}/orgs/${orgId}/dispatch/release`,
27398
+ DISPATCH_HEARTBEAT: (orgId) => `${API_BASE}/orgs/${orgId}/dispatch/heartbeat`,
27381
27399
  // Unread
27382
27400
  UNREAD: `${API_BASE}/me/unread`,
27383
27401
  ORG_UNREAD: (orgId) => `${API_BASE}/orgs/${orgId}/unread`,
@@ -27498,6 +27516,7 @@ var WS_EVENTS = {
27498
27516
  READ_POSITION_UPDATED: "read_position.updated",
27499
27517
  DISPATCH_NEW: "dispatch.new",
27500
27518
  DISPATCH_RECEIVED: "dispatch.received",
27519
+ DISPATCH_RESOLVED: "dispatch.resolved",
27501
27520
  SCHEDULE_CREATED: "schedule.created",
27502
27521
  SCHEDULE_UPDATED: "schedule.updated",
27503
27522
  SCHEDULE_DELETED: "schedule.deleted",
@@ -27593,8 +27612,8 @@ var ParallClient = class _ParallClient {
27593
27612
  * is authoritative, so wiki vs api routing can't drift from how a caller
27594
27613
  * happens to invoke the client.
27595
27614
  */
27596
- baseUrlFor(path9) {
27597
- return path9.startsWith(WIKI_BASE) ? this.wikiBaseUrl : this.baseUrl;
27615
+ baseUrlFor(path10) {
27616
+ return path10.startsWith(WIKI_BASE) ? this.wikiBaseUrl : this.baseUrl;
27598
27617
  }
27599
27618
  setToken(token) {
27600
27619
  this.token = token;
@@ -27621,10 +27640,10 @@ var ParallClient = class _ParallClient {
27621
27640
  * REFRESH_THRESHOLD_S, refresh it **before** sending the request.
27622
27641
  * No-op when the token is still fresh, missing, or un-parseable.
27623
27642
  */
27624
- async ensureFreshToken(path9) {
27643
+ async ensureFreshToken(path10) {
27625
27644
  if (!this.token || !this.getRefreshToken)
27626
27645
  return;
27627
- const pathSuffix = path9.replace(/^\/api\/v1/, "");
27646
+ const pathSuffix = path10.replace(/^\/api\/v1/, "");
27628
27647
  if (_ParallClient.AUTH_PATHS.has(pathSuffix))
27629
27648
  return;
27630
27649
  const exp = _ParallClient.decodeJwtExp(this.token);
@@ -27656,11 +27675,11 @@ var ParallClient = class _ParallClient {
27656
27675
  this.refreshPromise = null;
27657
27676
  }
27658
27677
  }
27659
- async request(method, path9, body, query, retried = false, opts) {
27678
+ async request(method, path10, body, query, retried = false, opts) {
27660
27679
  if (!retried) {
27661
- await this.ensureFreshToken(path9);
27680
+ await this.ensureFreshToken(path10);
27662
27681
  }
27663
- let url = `${this.baseUrlFor(path9)}${path9}`;
27682
+ let url = `${this.baseUrlFor(path10)}${path10}`;
27664
27683
  if (query) {
27665
27684
  const params = new URLSearchParams();
27666
27685
  for (const [key, value] of Object.entries(query)) {
@@ -27690,12 +27709,12 @@ var ParallClient = class _ParallClient {
27690
27709
  throw _ParallClient.normalizeFetchError(err);
27691
27710
  }
27692
27711
  if (res.status === 401) {
27693
- const pathSuffix = path9.replace(/^\/api\/v1/, "");
27712
+ const pathSuffix = path10.replace(/^\/api\/v1/, "");
27694
27713
  const isAuthPath = _ParallClient.AUTH_PATHS.has(pathSuffix);
27695
27714
  if (!retried && !isAuthPath && this.getRefreshToken) {
27696
27715
  const refreshed = await this.tryRefresh();
27697
27716
  if (refreshed) {
27698
- return this.request(method, path9, body, query, true, opts);
27717
+ return this.request(method, path10, body, query, true, opts);
27699
27718
  }
27700
27719
  }
27701
27720
  if (this.onTokenExpired && !isAuthPath) {
@@ -27706,6 +27725,7 @@ var ParallClient = class _ParallClient {
27706
27725
  const rawErrorBody = await res.json().catch(() => ({}));
27707
27726
  throw buildApiError(res, rawErrorBody);
27708
27727
  }
27728
+ opts?.onStatus?.(res.status);
27709
27729
  if (res.status === 204)
27710
27730
  return void 0;
27711
27731
  if (res.status === 202) {
@@ -27724,15 +27744,15 @@ var ParallClient = class _ParallClient {
27724
27744
  * hit the 100 MiB cap, so a longer 5-minute timeout is used so a
27725
27745
  * 50 MiB blob on a slow connection doesn't get chopped at 15 s.
27726
27746
  */
27727
- async multipartRequest(method, path9, body, retried = false) {
27747
+ async multipartRequest(method, path10, body, retried = false) {
27728
27748
  if (!retried) {
27729
- await this.ensureFreshToken(path9);
27749
+ await this.ensureFreshToken(path10);
27730
27750
  }
27731
27751
  const { "Content-Type": _drop, ...headers } = this.buildHeaders();
27732
27752
  void _drop;
27733
27753
  let res;
27734
27754
  try {
27735
- res = await fetch(`${this.baseUrlFor(path9)}${path9}`, {
27755
+ res = await fetch(`${this.baseUrlFor(path10)}${path10}`, {
27736
27756
  method,
27737
27757
  headers,
27738
27758
  body,
@@ -27742,12 +27762,12 @@ var ParallClient = class _ParallClient {
27742
27762
  throw _ParallClient.normalizeFetchError(err);
27743
27763
  }
27744
27764
  if (res.status === 401) {
27745
- const pathSuffix = path9.replace(/^\/api\/v1/, "");
27765
+ const pathSuffix = path10.replace(/^\/api\/v1/, "");
27746
27766
  const isAuthPath = _ParallClient.AUTH_PATHS.has(pathSuffix);
27747
27767
  if (!retried && !isAuthPath && this.getRefreshToken) {
27748
27768
  const refreshed = await this.tryRefresh();
27749
27769
  if (refreshed) {
27750
- return this.multipartRequest(method, path9, body, true);
27770
+ return this.multipartRequest(method, path10, body, true);
27751
27771
  }
27752
27772
  }
27753
27773
  if (this.onTokenExpired && !isAuthPath) {
@@ -28039,6 +28059,21 @@ var ParallClient = class _ParallClient {
28039
28059
  async sendMessage(orgId, chatId, req) {
28040
28060
  return this.request("POST", ENDPOINTS.CHAT_MESSAGES(orgId, chatId), req);
28041
28061
  }
28062
+ /**
28063
+ * sendMessage variant that also reports whether the server answered with an
28064
+ * idempotent replay (HTTP 200 — the message already existed for this
28065
+ * idempotency/effect key) instead of a fresh create (201). Used by the CLI
28066
+ * to surface "already sent by a previous run (deduplicated)".
28067
+ */
28068
+ async sendMessageDetailed(orgId, chatId, req) {
28069
+ let status = 0;
28070
+ const message = await this.request("POST", ENDPOINTS.CHAT_MESSAGES(orgId, chatId), req, void 0, false, {
28071
+ onStatus: (s) => {
28072
+ status = s;
28073
+ }
28074
+ });
28075
+ return { message, deduplicated: status === 200 };
28076
+ }
28042
28077
  async getMessages(orgId, chatId, params) {
28043
28078
  return this.request("GET", ENDPOINTS.CHAT_MESSAGES(orgId, chatId), void 0, params);
28044
28079
  }
@@ -28426,8 +28461,8 @@ var ParallClient = class _ParallClient {
28426
28461
  async requestMachineUpdate(orgId, machineId, mandatory = false) {
28427
28462
  await this.request("POST", ENDPOINTS.MACHINE_REQUEST_UPDATE(orgId, machineId), { mandatory });
28428
28463
  }
28429
- async browseMachineFilesystem(orgId, machineId, path9) {
28430
- return this.request("POST", ENDPOINTS.MACHINE_BROWSE(orgId, machineId), { path: path9 }, void 0, false, { timeoutMs: 15e3 });
28464
+ async browseMachineFilesystem(orgId, machineId, path10) {
28465
+ return this.request("POST", ENDPOINTS.MACHINE_BROWSE(orgId, machineId), { path: path10 }, void 0, false, { timeoutMs: 15e3 });
28431
28466
  }
28432
28467
  /** Create a new machine key. Returns the raw key string (shown once) + metadata. */
28433
28468
  async createMachineKey(orgId, machineId, name) {
@@ -28500,6 +28535,30 @@ var ParallClient = class _ParallClient {
28500
28535
  const params = maxAgeHours !== void 0 ? { max_age_hours: String(maxAgeHours) } : void 0;
28501
28536
  return this.request("POST", ENDPOINTS.DISPATCH_EXPIRE(orgId), void 0, params);
28502
28537
  }
28538
+ /**
28539
+ * Claim a dispatch lane (explicit consume endpoint). Occupies the
28540
+ * (agent, target, thread) lane and folds claimable WorkItems into it;
28541
+ * `claimed: false` means a healthy incumbent holds the lane.
28542
+ */
28543
+ async claimDispatch(orgId, req) {
28544
+ return this.request("POST", ENDPOINTS.DISPATCH_CLAIM(orgId), req);
28545
+ }
28546
+ /** Fold a pending same-target WorkItem into a live lane (409 STALE_LANE when dethroned). */
28547
+ async steerDispatch(orgId, req) {
28548
+ return this.request("POST", ENDPOINTS.DISPATCH_STEER(orgId), req);
28549
+ }
28550
+ /** End a turn: no_action sweep of the lane's members + lane release + re-drive check. */
28551
+ async completeDispatch(orgId, req) {
28552
+ return this.request("POST", ENDPOINTS.DISPATCH_COMPLETE(orgId), req);
28553
+ }
28554
+ /** Release a lane on graceful shutdown — members return to pending immediately. */
28555
+ async releaseDispatchLane(orgId, lane) {
28556
+ return this.request("POST", ENDPOINTS.DISPATCH_RELEASE(orgId), { lane });
28557
+ }
28558
+ /** Renew a live lane's lease (long-turn keepalive). 409 STALE_LANE when dethroned. */
28559
+ async heartbeatDispatchLane(orgId, req) {
28560
+ return this.request("POST", ENDPOINTS.DISPATCH_HEARTBEAT(orgId), req);
28561
+ }
28503
28562
  async getDispatchByMessages(orgId, chatId, messageIds) {
28504
28563
  const params = { chat_id: chatId, message_ids: messageIds.join(",") };
28505
28564
  return this.request("GET", ENDPOINTS.DISPATCH_BY_MESSAGES(orgId), void 0, params);
@@ -28964,8 +29023,8 @@ var ParallClient = class _ParallClient {
28964
29023
  async deleteWikiRestriction(orgId, wikiId, restrictionId) {
28965
29024
  await this.request("DELETE", ENDPOINTS.WIKI_RESTRICTION(orgId, wikiId, restrictionId));
28966
29025
  }
28967
- async getWikiAccessStatus(orgId, wikiId, path9) {
28968
- return this.request("GET", ENDPOINTS.WIKI_ACCESS_STATUS(orgId, wikiId), void 0, path9 ? { path: path9 } : void 0);
29026
+ async getWikiAccessStatus(orgId, wikiId, path10) {
29027
+ return this.request("GET", ENDPOINTS.WIKI_ACCESS_STATUS(orgId, wikiId), void 0, path10 ? { path: path10 } : void 0);
28969
29028
  }
28970
29029
  async createWikiAccessRequest(orgId, wikiId, data) {
28971
29030
  await this.request("POST", ENDPOINTS.WIKI_ACCESS_REQUESTS(orgId, wikiId), data);
@@ -28974,14 +29033,14 @@ var ParallClient = class _ParallClient {
28974
29033
  async getWikiCommits(orgId, wikiId, params) {
28975
29034
  return this.request("GET", ENDPOINTS.WIKI_COMMITS(orgId, wikiId), void 0, params);
28976
29035
  }
28977
- async getWikiFileCommits(orgId, wikiId, path9, params) {
29036
+ async getWikiFileCommits(orgId, wikiId, path10, params) {
28978
29037
  return this.request("GET", ENDPOINTS.WIKI_FILE_COMMITS(orgId, wikiId), void 0, {
28979
- path: path9,
29038
+ path: path10,
28980
29039
  ...params
28981
29040
  });
28982
29041
  }
28983
- async getWikiBlame(orgId, wikiId, path9, ref) {
28984
- return this.request("GET", ENDPOINTS.WIKI_BLAME(orgId, wikiId), void 0, { path: path9, ref });
29042
+ async getWikiBlame(orgId, wikiId, path10, ref) {
29043
+ return this.request("GET", ENDPOINTS.WIKI_BLAME(orgId, wikiId), void 0, { path: path10, ref });
28985
29044
  }
28986
29045
  // ---- Wiki Operations (audit log) ----
28987
29046
  async getWikiOperations(orgId, wikiId, params) {
@@ -29613,6 +29672,404 @@ var ParallWs = class {
29613
29672
  }
29614
29673
  };
29615
29674
 
29675
+ // ts/agent-core/dist/lane-ledger.js
29676
+ import * as fs from "node:fs";
29677
+ var LedgerUnsupportedError = class extends Error {
29678
+ };
29679
+ function isStaleLane(err) {
29680
+ return err instanceof ApiError && err.status === 409 && err.code === "STALE_LANE";
29681
+ }
29682
+ function isEndpointMissing(err) {
29683
+ return err instanceof ApiError && err.status === 404 && !err.code;
29684
+ }
29685
+ var LaneLedger = class {
29686
+ opts;
29687
+ lanes = /* @__PURE__ */ new Map();
29688
+ constructor(opts) {
29689
+ this.opts = opts;
29690
+ }
29691
+ get contextDir() {
29692
+ return this.opts.contextDir;
29693
+ }
29694
+ /** Only chat message events ride the lane ledger; typed events stay on the legacy ack path. */
29695
+ handles(event) {
29696
+ return event.type === "message" && event.targetId.startsWith("cht_");
29697
+ }
29698
+ laneKeyFor(event) {
29699
+ if (event.type !== "message" && event.dispatchEventId) {
29700
+ return laneKeyForTarget(`dsp:${event.dispatchEventId}`);
29701
+ }
29702
+ return laneKeyForTarget(`prll://${event.targetId}`, event.threadRootId);
29703
+ }
29704
+ getForEvent(event) {
29705
+ return this.lanes.get(this.laneKeyFor(event));
29706
+ }
29707
+ laneContextPath(lane) {
29708
+ return laneContextFilePath(this.opts.contextDir, lane.targetUri, lane.threadRootId);
29709
+ }
29710
+ /**
29711
+ * Claim (or reuse) the lane for a group of same-lane message events and
29712
+ * fold every group member into it. Returns 'foreign' when a healthy
29713
+ * incumbent (another pod) holds the resource — the caller must not
29714
+ * dispatch; the events stay pending server-side and re-drive after the
29715
+ * incumbent completes.
29716
+ */
29717
+ async ensureLane(events) {
29718
+ const trigger = events[events.length - 1];
29719
+ const laneKey = this.laneKeyFor(trigger);
29720
+ let lane = this.lanes.get(laneKey);
29721
+ if (!lane) {
29722
+ const targetUri = `prll://${trigger.targetId}`;
29723
+ let res;
29724
+ try {
29725
+ res = await this.opts.client.claimDispatch(this.opts.orgId, {
29726
+ target_uri: targetUri,
29727
+ thread_root_id: trigger.threadRootId,
29728
+ limit: 100
29729
+ });
29730
+ } catch (err) {
29731
+ if (isEndpointMissing(err))
29732
+ throw new LedgerUnsupportedError("claim endpoint unavailable");
29733
+ throw err;
29734
+ }
29735
+ if (!res.claimed || !res.lane) {
29736
+ this.opts.log?.info(`lane for ${targetUri} held by a healthy incumbent \u2014 leaving events pending for re-drive`);
29737
+ return null;
29738
+ }
29739
+ const leaseUntilMs = Date.parse(res.lease_until ?? "");
29740
+ lane = {
29741
+ laneKey,
29742
+ lane: res.lane,
29743
+ targetUri,
29744
+ threadRootId: trigger.threadRootId,
29745
+ folded: /* @__PURE__ */ new Map(),
29746
+ ...Number.isNaN(leaseUntilMs) ? {} : { leaseUntilMs, leaseTtlMs: Math.max(leaseUntilMs - Date.now(), 6e4) }
29747
+ };
29748
+ for (const ev of res.events ?? []) {
29749
+ lane.folded.set(ev.source_id, ev.id);
29750
+ }
29751
+ this.lanes.set(laneKey, lane);
29752
+ }
29753
+ for (const ev of events) {
29754
+ if (lane.folded.has(ev.messageId))
29755
+ continue;
29756
+ try {
29757
+ const res = await this.opts.client.steerDispatch(this.opts.orgId, {
29758
+ lane: lane.lane,
29759
+ target_uri: lane.targetUri,
29760
+ thread_root_id: lane.threadRootId,
29761
+ ...ev.dispatchEventId ? { dispatch_event_id: ev.dispatchEventId } : { source_type: "message", source_id: ev.messageId }
29762
+ });
29763
+ lane.folded.set(ev.messageId, res.dispatch_event_id);
29764
+ } catch (err) {
29765
+ if (isStaleLane(err)) {
29766
+ this.lanes.delete(laneKey);
29767
+ return null;
29768
+ }
29769
+ this.opts.log?.warn(`steer fold failed for ${ev.messageId} \u2014 failing closed, releasing lane: ${String(err)}`);
29770
+ await this.release(laneKey);
29771
+ return null;
29772
+ }
29773
+ }
29774
+ return lane;
29775
+ }
29776
+ /**
29777
+ * Fold a live mid-turn message into its active lane BEFORE injecting it
29778
+ * into the running turn. Injection without a successful fold is forbidden —
29779
+ * an un-folded injected message would be re-driven after complete and the
29780
+ * model would handle it twice.
29781
+ */
29782
+ async steerLive(event) {
29783
+ const laneKey = this.laneKeyFor(event);
29784
+ const lane = this.lanes.get(laneKey);
29785
+ if (!lane)
29786
+ return false;
29787
+ if (lane.folded.has(event.messageId))
29788
+ return true;
29789
+ try {
29790
+ const res = await this.opts.client.steerDispatch(this.opts.orgId, {
29791
+ lane: lane.lane,
29792
+ target_uri: lane.targetUri,
29793
+ thread_root_id: lane.threadRootId,
29794
+ ...event.dispatchEventId ? { dispatch_event_id: event.dispatchEventId } : { source_type: "message", source_id: event.messageId }
29795
+ });
29796
+ lane.folded.set(event.messageId, res.dispatch_event_id);
29797
+ return true;
29798
+ } catch (err) {
29799
+ if (isStaleLane(err)) {
29800
+ this.lanes.delete(laneKey);
29801
+ } else {
29802
+ this.opts.log?.warn(`live steer failed for ${event.messageId}: ${String(err)}`);
29803
+ }
29804
+ return false;
29805
+ }
29806
+ }
29807
+ /**
29808
+ * Complete the lane when no local work remains for it: the server sweeps
29809
+ * still-leased members as no_action, releases the occupancy row, and
29810
+ * re-drives any same-target pending work. A STALE_LANE answer means a
29811
+ * takeover already owns the resource — local state is dropped either way.
29812
+ */
29813
+ async completeIfIdle(laneKey, hasMoreLocal) {
29814
+ const lane = this.lanes.get(laneKey);
29815
+ if (!lane || hasMoreLocal)
29816
+ return;
29817
+ this.lanes.delete(laneKey);
29818
+ this.removeLaneContext(lane);
29819
+ try {
29820
+ const res = await this.opts.client.completeDispatch(this.opts.orgId, {
29821
+ lane: lane.lane,
29822
+ target_uri: lane.targetUri,
29823
+ thread_root_id: lane.threadRootId
29824
+ });
29825
+ if (res.swept_no_action > 0 || res.redriven) {
29826
+ this.opts.log?.info(`lane complete for ${lane.targetUri}: swept ${res.swept_no_action} no_action, redriven=${res.redriven}`);
29827
+ }
29828
+ } catch (err) {
29829
+ if (isStaleLane(err)) {
29830
+ this.opts.log?.info(`lane complete skipped for ${lane.targetUri} \u2014 taken over`);
29831
+ return;
29832
+ }
29833
+ this.opts.log?.warn(`lane complete failed for ${lane.targetUri}: ${String(err)}`);
29834
+ }
29835
+ }
29836
+ /**
29837
+ * Long-turn keepalive: renew the lane's lease on runtime activity, throttled
29838
+ * so a chatty turn doesn't spam the server. Without this, a legitimately
29839
+ * long turn (> lane TTL) would be dethroned mid-flight and every subsequent
29840
+ * write misfired with STALE_LANE — the design doc's "long turns renew via
29841
+ * step writes". Fire-and-forget: a failed renewal is surfaced by the next
29842
+ * write's incumbency check anyway.
29843
+ */
29844
+ maybeRenew(lane) {
29845
+ const now = Date.now();
29846
+ const ttl = lane.leaseTtlMs ?? 10 * 6e4;
29847
+ const until = lane.leaseUntilMs ?? now;
29848
+ if (until - now > ttl / 2)
29849
+ return;
29850
+ lane.leaseUntilMs = now + ttl;
29851
+ void this.opts.client.heartbeatDispatchLane(this.opts.orgId, {
29852
+ lane: lane.lane,
29853
+ target_uri: lane.targetUri,
29854
+ thread_root_id: lane.threadRootId
29855
+ }).then((res) => {
29856
+ const until2 = Date.parse(res?.lease_until ?? "");
29857
+ if (!Number.isNaN(until2))
29858
+ lane.leaseUntilMs = until2;
29859
+ }).catch((err) => {
29860
+ if (isStaleLane(err)) {
29861
+ this.lanes.delete(lane.laneKey);
29862
+ this.opts.log?.warn(`lane ${lane.targetUri} was taken over during the turn`);
29863
+ return;
29864
+ }
29865
+ this.opts.log?.warn(`lane heartbeat failed for ${lane.targetUri}: ${String(err)}`);
29866
+ });
29867
+ }
29868
+ /**
29869
+ * Release a lane's unresolved members back to pending (dispatch error /
29870
+ * shutdown) so the next pod re-claims immediately instead of waiting out
29871
+ * the lease.
29872
+ */
29873
+ async release(laneKey) {
29874
+ const lane = this.lanes.get(laneKey);
29875
+ if (!lane)
29876
+ return;
29877
+ this.lanes.delete(laneKey);
29878
+ this.removeLaneContext(lane);
29879
+ try {
29880
+ await this.opts.client.releaseDispatchLane(this.opts.orgId, lane.lane);
29881
+ } catch (err) {
29882
+ this.opts.log?.warn(`lane release failed for ${lane.targetUri}: ${String(err)}`);
29883
+ }
29884
+ }
29885
+ async releaseAll() {
29886
+ const keys = [...this.lanes.keys()];
29887
+ for (const key of keys) {
29888
+ await this.release(key);
29889
+ }
29890
+ }
29891
+ /** True when any lane is currently active (used by shutdown logging). */
29892
+ get activeCount() {
29893
+ return this.lanes.size;
29894
+ }
29895
+ /**
29896
+ * Claim the single-member lane of one typed WorkItem (resource = dsp:<id>),
29897
+ * by WorkItem id or by source identity (the live task.assigned event has no
29898
+ * WorkItem id). Returns null when a healthy incumbent (another pod) holds
29899
+ * it or the WorkItem is already resolved — the caller must skip processing.
29900
+ */
29901
+ async claimTyped(ref) {
29902
+ let res;
29903
+ try {
29904
+ res = await this.opts.client.claimDispatch(this.opts.orgId, {
29905
+ dispatch_event_id: ref.dispatchEventId,
29906
+ source_type: ref.dispatchEventId ? void 0 : ref.sourceType,
29907
+ source_id: ref.dispatchEventId ? void 0 : ref.sourceId
29908
+ });
29909
+ } catch (err) {
29910
+ if (isEndpointMissing(err))
29911
+ throw new LedgerUnsupportedError("claim endpoint unavailable");
29912
+ throw err;
29913
+ }
29914
+ if (!res.claimed || !res.lane || !res.events?.length)
29915
+ return null;
29916
+ const workItem = res.events[0];
29917
+ const targetUri = `dsp:${workItem.id}`;
29918
+ const leaseUntilMs = Date.parse(res.lease_until ?? "");
29919
+ const lane = {
29920
+ laneKey: laneKeyForTarget(targetUri),
29921
+ lane: res.lane,
29922
+ targetUri,
29923
+ folded: /* @__PURE__ */ new Map([[workItem.source_id, workItem.id]]),
29924
+ typedDispatchEventId: workItem.id,
29925
+ ...Number.isNaN(leaseUntilMs) ? {} : { leaseUntilMs, leaseTtlMs: Math.max(leaseUntilMs - Date.now(), 6e4) }
29926
+ };
29927
+ this.lanes.set(lane.laneKey, lane);
29928
+ return lane;
29929
+ }
29930
+ /**
29931
+ * Remove the per-lane context file (and its CLI sidecar) when the lane
29932
+ * ends. A leftover file would make a later cross-context send to the same
29933
+ * target bind a dead lane token and misfire with STALE_LANE instead of
29934
+ * taking the plain non-ledger path.
29935
+ */
29936
+ removeLaneContext(lane) {
29937
+ const contextPath = this.laneContextPath(lane);
29938
+ for (const p of [contextPath, contextPath.replace(/\.json$/, ".reply-state.json")]) {
29939
+ try {
29940
+ fs.rmSync(p, { force: true });
29941
+ } catch {
29942
+ }
29943
+ }
29944
+ }
29945
+ };
29946
+
29947
+ // ts/agent-core/dist/gateway-lane-flow.js
29948
+ async function dispatchLaneGroup(host, opts) {
29949
+ const ledger = host.laneLedger;
29950
+ const event = opts.events[opts.events.length - 1];
29951
+ let lane;
29952
+ try {
29953
+ lane = await ledger.ensureLane(opts.events);
29954
+ } catch (err) {
29955
+ if (!(err instanceof LedgerUnsupportedError))
29956
+ throw err;
29957
+ host.disableLedger("claim endpoint missing");
29958
+ await host.emitDispatchReceived(event);
29959
+ const dispatched2 = await host.runDispatch(event, opts.sessionKey, opts.body, opts.earlier, opts.captureText);
29960
+ if (!dispatched2)
29961
+ return "shutdown";
29962
+ for (const ev of opts.events) {
29963
+ host.opts.client.ackDispatch(host.opts.config.org_id, {
29964
+ source_type: ev.ackSourceType ?? "message",
29965
+ source_id: ev.ackSourceId ?? ev.messageId
29966
+ }).catch(() => {
29967
+ });
29968
+ }
29969
+ return "dispatched";
29970
+ }
29971
+ if (!lane) {
29972
+ for (const ev of opts.events) {
29973
+ host.dispatchedMessages.delete(ev.messageId);
29974
+ }
29975
+ return "foreign";
29976
+ }
29977
+ let dispatched = false;
29978
+ try {
29979
+ dispatched = await host.runDispatch(event, opts.sessionKey, opts.body, opts.earlier, opts.captureText);
29980
+ } catch (err) {
29981
+ await ledger.release(lane.laneKey).catch(() => {
29982
+ });
29983
+ throw err;
29984
+ }
29985
+ if (!dispatched) {
29986
+ return "shutdown";
29987
+ }
29988
+ const pendingInjections = host.opts.dispatchAdapter.hasPendingInjections?.(opts.sessionKey) ?? false;
29989
+ await ledger.completeIfIdle(lane.laneKey, pendingInjections || opts.hasMoreLocal());
29990
+ return "dispatched";
29991
+ }
29992
+ async function consumeTypedDispatch(host, ref, run, ack) {
29993
+ if (!host.laneLedger || host.ledgerDisabled) {
29994
+ if (await run(ref.dispatchEventId))
29995
+ ack(ref.dispatchEventId);
29996
+ return;
29997
+ }
29998
+ let lane;
29999
+ try {
30000
+ lane = await host.laneLedger.claimTyped(ref);
30001
+ } catch (err) {
30002
+ if (err instanceof LedgerUnsupportedError) {
30003
+ host.disableLedger("claim endpoint missing");
30004
+ if (await run(ref.dispatchEventId))
30005
+ ack(ref.dispatchEventId);
30006
+ return;
30007
+ }
30008
+ throw err;
30009
+ }
30010
+ if (!lane) {
30011
+ host.opts.log?.info(`typed dispatch ${ref.dispatchEventId ?? `${ref.sourceType}:${ref.sourceId}`} not claimable (held elsewhere or already resolved) \u2014 skipping`);
30012
+ return;
30013
+ }
30014
+ try {
30015
+ if (await run(lane.typedDispatchEventId))
30016
+ ack(lane.typedDispatchEventId);
30017
+ } finally {
30018
+ await host.laneLedger.completeIfIdle(lane.laneKey, false).catch(() => {
30019
+ });
30020
+ }
30021
+ }
30022
+ async function consumeMessageWorkItem(host, item) {
30023
+ if (host.shuttingDown)
30024
+ return;
30025
+ if (!host.tryClaimMessage(item.source_id))
30026
+ return;
30027
+ const ackItem = () => {
30028
+ host.opts.client.ackDispatchByID(host.opts.config.org_id, item.id).catch(() => {
30029
+ });
30030
+ };
30031
+ let msg = null;
30032
+ try {
30033
+ msg = await host.opts.client.getMessage(item.source_id);
30034
+ } catch (err) {
30035
+ const status = err?.status;
30036
+ if (status !== 404) {
30037
+ host.opts.log?.warn(`message fetch failed for ${item.source_id}, leaving pending: ${String(err)}`);
30038
+ host.dispatchedMessages.delete(item.source_id);
30039
+ return;
30040
+ }
30041
+ }
30042
+ if (!msg || msg.sender_id === host.opts.agentUserId) {
30043
+ host.dispatchedMessages.delete(item.source_id);
30044
+ ackItem();
30045
+ return;
30046
+ }
30047
+ const decision = await host.buildMessageDispatchDecision(item.chat_id, msg);
30048
+ if (decision.action === "retry") {
30049
+ host.dispatchedMessages.delete(item.source_id);
30050
+ return;
30051
+ }
30052
+ if (decision.action === "skip") {
30053
+ host.dispatchedMessages.delete(item.source_id);
30054
+ ackItem();
30055
+ return;
30056
+ }
30057
+ decision.event.dispatchEventId = item.id;
30058
+ const laneResolved = host.usesLaneLedger(decision.event);
30059
+ try {
30060
+ const dispatched = await host.handleInboundEvent(decision.event);
30061
+ if (dispatched) {
30062
+ if (!laneResolved)
30063
+ ackItem();
30064
+ } else {
30065
+ host.dispatchedMessages.delete(item.source_id);
30066
+ }
30067
+ } catch (err) {
30068
+ host.dispatchedMessages.delete(item.source_id);
30069
+ throw err;
30070
+ }
30071
+ }
30072
+
29616
30073
  // ts/agent-core/dist/telemetry.js
29617
30074
  init_esm();
29618
30075
  var import_api_logs = __toESM(require_src(), 1);
@@ -29914,6 +30371,14 @@ var ParallAgentGateway = class {
29914
30371
  inFlightDispatches = 0;
29915
30372
  drainResolvers = [];
29916
30373
  pendingRestartNotification = null;
30374
+ laneLedger;
30375
+ // Sticky fallback: flipped when the server predates the ledger (claim
30376
+ // endpoint 404) so every subsequent dispatch uses the legacy flow.
30377
+ ledgerDisabled = false;
30378
+ // Group key of the group currently being dispatched on main — lane-aware
30379
+ // (targetId + thread), unlike mainCurrentTargetId which stays chat-level
30380
+ // for fork routing decisions.
30381
+ mainCurrentGroupKey;
29917
30382
  DISPATCHED_MESSAGES_CAP = 5e3;
29918
30383
  // SHUTDOWN_DEADLINE_MS is read by waitForDrain via the configured value
29919
30384
  // below — kept as instance state so per-runtime configs can override it
@@ -29923,6 +30388,14 @@ var ParallAgentGateway = class {
29923
30388
  DISPATCH_DEADLINE_MS;
29924
30389
  constructor(opts) {
29925
30390
  this.opts = opts;
30391
+ if (opts.dispatchContextDir) {
30392
+ this.laneLedger = new LaneLedger({
30393
+ client: opts.client,
30394
+ orgId: opts.config.org_id,
30395
+ contextDir: opts.dispatchContextDir,
30396
+ log: opts.log
30397
+ });
30398
+ }
29926
30399
  this.SHUTDOWN_DEADLINE_MS = opts.shutdownDeadlineMs ?? 6e4;
29927
30400
  this.FORK_DEADLINE_MS = opts.forkDeadlineMs ?? 2 * 60 * 6e4;
29928
30401
  this.DISPATCH_DEADLINE_MS = opts.dispatchDeadlineMs ?? 20 * 6e4;
@@ -29992,14 +30465,18 @@ var ParallAgentGateway = class {
29992
30465
  if (data.status !== "todo" && data.status !== "in_progress")
29993
30466
  return;
29994
30467
  try {
29995
- const dispatched = await this.handleTaskAssignment(data, data.id);
29996
- if (dispatched) {
30468
+ 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) => {
30469
+ if (dispatchEventId) {
30470
+ this.opts.client.ackDispatchByID(this.opts.config.org_id, dispatchEventId).catch(() => {
30471
+ });
30472
+ return;
30473
+ }
29997
30474
  this.opts.client.ackDispatch(this.opts.config.org_id, {
29998
30475
  source_type: "task_activity",
29999
30476
  source_id: data.id
30000
30477
  }).catch(() => {
30001
30478
  });
30002
- }
30479
+ });
30003
30480
  } catch (err) {
30004
30481
  this.opts.log?.error(`task dispatch failed for ${data.id}: ${String(err)}`);
30005
30482
  }
@@ -30009,11 +30486,10 @@ var ParallAgentGateway = class {
30009
30486
  if (!data.source_id || !data.task_id)
30010
30487
  return;
30011
30488
  try {
30012
- const dispatched = await this.handleTaskComment(data.source_id, data.task_id, data.actor_id, data.delivery_reason);
30013
- if (dispatched) {
30489
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.handleTaskComment(data.source_id, data.task_id ?? "", data.actor_id, data.delivery_reason), () => {
30014
30490
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
30015
30491
  });
30016
- }
30492
+ });
30017
30493
  } catch (err) {
30018
30494
  this.opts.log?.error(`task comment dispatch failed for ${data.source_id}: ${String(err)}`);
30019
30495
  }
@@ -30021,11 +30497,10 @@ var ParallAgentGateway = class {
30021
30497
  if (!data.source_id)
30022
30498
  return;
30023
30499
  try {
30024
- const dispatched = await this.handleWikiComment(data.source_id, data.actor_id, data.delivery_reason);
30025
- if (dispatched) {
30500
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.handleWikiComment(data.source_id, data.actor_id, data.delivery_reason), () => {
30026
30501
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
30027
30502
  });
30028
- }
30503
+ });
30029
30504
  } catch (err) {
30030
30505
  this.opts.log?.error(`wiki comment dispatch failed for ${data.source_id}: ${String(err)}`);
30031
30506
  }
@@ -30033,11 +30508,13 @@ var ParallAgentGateway = class {
30033
30508
  if (!data.task_id)
30034
30509
  return;
30035
30510
  try {
30036
- const dispatched = await this.handleTaskDispatch(data.task_id, data.source_id ?? data.task_id, { allowCreator: true });
30037
- if (dispatched) {
30511
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, (dispatchEventId) => this.handleTaskDispatch(data.task_id ?? "", data.source_id ?? data.task_id ?? "", {
30512
+ allowCreator: true,
30513
+ dispatchEventId
30514
+ }), () => {
30038
30515
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
30039
30516
  });
30040
- }
30517
+ });
30041
30518
  } catch (err) {
30042
30519
  this.opts.log?.error(`task update dispatch failed for ${data.task_id}: ${String(err)}`);
30043
30520
  }
@@ -30045,11 +30522,10 @@ var ParallAgentGateway = class {
30045
30522
  if (!data.source_id)
30046
30523
  return;
30047
30524
  try {
30048
- const dispatched = await this.fetchAndHandleScheduleFire(data.source_id, data.actor_id);
30049
- if (dispatched) {
30525
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.fetchAndHandleScheduleFire(data.source_id, data.actor_id), () => {
30050
30526
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
30051
30527
  });
30052
- }
30528
+ });
30053
30529
  } catch (err) {
30054
30530
  this.opts.log?.error(`schedule fire dispatch failed for ${data.source_id}: ${String(err)}`);
30055
30531
  }
@@ -30057,11 +30533,10 @@ var ParallAgentGateway = class {
30057
30533
  if (!data.source_id)
30058
30534
  return;
30059
30535
  try {
30060
- const dispatched = await this.fetchAndHandleExternalTriggerRun(data.source_id);
30061
- if (dispatched) {
30536
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.fetchAndHandleExternalTriggerRun(data.source_id), () => {
30062
30537
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
30063
30538
  });
30064
- }
30539
+ });
30065
30540
  } catch (err) {
30066
30541
  this.opts.log?.error(`external trigger dispatch failed for ${data.source_id}: ${String(err)}`);
30067
30542
  }
@@ -30069,11 +30544,10 @@ var ParallAgentGateway = class {
30069
30544
  if (!data.source_id)
30070
30545
  return;
30071
30546
  try {
30072
- const dispatched = await this.fetchAndHandleChannelMessage(data.source_id);
30073
- if (dispatched) {
30547
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.fetchAndHandleChannelMessage(data.source_id), () => {
30074
30548
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
30075
30549
  });
30076
- }
30550
+ });
30077
30551
  } catch (err) {
30078
30552
  this.opts.log?.error(`channel message dispatch failed for ${data.source_id}: ${String(err)}`);
30079
30553
  }
@@ -30081,14 +30555,19 @@ var ParallAgentGateway = class {
30081
30555
  if (!data.source_id)
30082
30556
  return;
30083
30557
  try {
30084
- const dispatched = await this.fetchAndHandleApprovalDecided(data.source_id, data.actor_id, data.chat_id ?? null);
30085
- if (dispatched) {
30558
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.fetchAndHandleApprovalDecided(data.source_id, data.actor_id, data.chat_id ?? null), () => {
30086
30559
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
30087
30560
  });
30088
- }
30561
+ });
30089
30562
  } catch (err) {
30090
30563
  this.opts.log?.error(`approval decided dispatch failed for ${data.source_id}: ${String(err)}`);
30091
30564
  }
30565
+ } else if (data.event_type === "message" && this.laneLedger && data.source_id && data.chat_id) {
30566
+ try {
30567
+ await this.handleMessageRedrive(data);
30568
+ } catch (err) {
30569
+ this.opts.log?.error(`message re-drive failed for ${data.source_id}: ${String(err)}`);
30570
+ }
30092
30571
  } else if (data.event_type !== "message" && data.event_type !== "task_assign") {
30093
30572
  this.opts.log?.info(`dispatch.new with unhandled event_type=${String(data.event_type)} (id=${data.id}) \u2014 no-op`);
30094
30573
  }
@@ -30124,6 +30603,39 @@ var ParallAgentGateway = class {
30124
30603
  source_id: sourceId
30125
30604
  });
30126
30605
  }
30606
+ /** True when this event's lifecycle is owned by the dispatch lane ledger. */
30607
+ usesLaneLedger(event) {
30608
+ return this.laneLedger != null && !this.ledgerDisabled && this.laneLedger.handles(event);
30609
+ }
30610
+ disableLedger(reason) {
30611
+ if (this.ledgerDisabled)
30612
+ return;
30613
+ this.ledgerDisabled = true;
30614
+ this.opts.log?.warn(`dispatch ledger unavailable (${reason}) \u2014 falling back to legacy received/ack flow`);
30615
+ }
30616
+ /**
30617
+ * Buffer grouping key. Lane-ledger message events group by full lane
30618
+ * identity (chat + thread) so a channel lane and a thread lane in the same
30619
+ * chat dispatch as separate turns with separate claims; everything else
30620
+ * keeps the historical chat-level grouping.
30621
+ */
30622
+ dispatchGroupKey(event) {
30623
+ if (this.usesLaneLedger(event)) {
30624
+ return this.laneLedger.laneKeyFor(event);
30625
+ }
30626
+ return event.targetId;
30627
+ }
30628
+ // Lane-flow protocols live in gateway-lane-flow.ts; these thin delegates
30629
+ // keep call sites and tests on the class surface.
30630
+ laneFlowHost() {
30631
+ return this;
30632
+ }
30633
+ dispatchLaneGroup(opts) {
30634
+ return dispatchLaneGroup(this.laneFlowHost(), opts);
30635
+ }
30636
+ consumeTypedDispatch(ref, run, ack) {
30637
+ return consumeTypedDispatch(this.laneFlowHost(), ref, run, ack);
30638
+ }
30127
30639
  buildDispatchContext(event, sessionKey) {
30128
30640
  const binding = this.sessionBindings.get(sessionKey);
30129
30641
  return {
@@ -30140,6 +30652,7 @@ var ParallAgentGateway = class {
30140
30652
  noReply: event.noReply ?? false,
30141
30653
  contextFilePath: this.opts.contextFilePathForSession?.(sessionKey),
30142
30654
  stepIdFilePath: this.opts.stepIdFilePathForSession?.(sessionKey),
30655
+ contextDirPath: this.opts.dispatchContextDir,
30143
30656
  client: this.opts.client,
30144
30657
  log: this.opts.log
30145
30658
  };
@@ -30179,7 +30692,7 @@ var ParallAgentGateway = class {
30179
30692
  this.opts.log?.warn(`failed to create input step: ${String(err)}`);
30180
30693
  }
30181
30694
  }
30182
- async createRuntimeStep(sessionId, event, runtimeEvent, stepIdFilePath, contextFilePath) {
30695
+ async createRuntimeStep(sessionId, event, runtimeEvent, stepIdFilePath, contextFilePath, laneContextFilePath2) {
30183
30696
  const target = resolveStepTarget(event);
30184
30697
  try {
30185
30698
  switch (runtimeEvent.type) {
@@ -30225,6 +30738,9 @@ var ParallAgentGateway = class {
30225
30738
  } else if (stepIdFilePath) {
30226
30739
  this.writeStepIdFile(stepIdFilePath, step.id);
30227
30740
  }
30741
+ if (laneContextFilePath2) {
30742
+ this.updateContextFileStepId(laneContextFilePath2, step.id);
30743
+ }
30228
30744
  break;
30229
30745
  }
30230
30746
  case "tool_result":
@@ -30247,6 +30763,9 @@ var ParallAgentGateway = class {
30247
30763
  } else if (stepIdFilePath) {
30248
30764
  this.clearStepIdFile(stepIdFilePath);
30249
30765
  }
30766
+ if (laneContextFilePath2) {
30767
+ this.updateContextFileStepId(laneContextFilePath2, null);
30768
+ }
30250
30769
  break;
30251
30770
  case "error":
30252
30771
  await this.opts.client.createAgentStep(this.opts.config.org_id, this.opts.agentUserId, sessionId, {
@@ -30266,28 +30785,28 @@ var ParallAgentGateway = class {
30266
30785
  }
30267
30786
  writeContextFile(filePath, ctx) {
30268
30787
  try {
30269
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
30270
- fs.writeFileSync(filePath, JSON.stringify(ctx), "utf8");
30788
+ fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
30789
+ fs2.writeFileSync(filePath, JSON.stringify(ctx), "utf8");
30271
30790
  } catch (err) {
30272
30791
  this.opts.log?.warn(`failed to write context file ${filePath}: ${String(err)}`);
30273
30792
  }
30274
30793
  }
30275
30794
  updateContextFileStepId(filePath, stepId) {
30276
30795
  try {
30277
- const raw = fs.readFileSync(filePath, "utf8");
30796
+ const raw = fs2.readFileSync(filePath, "utf8");
30278
30797
  const ctx = JSON.parse(raw);
30279
30798
  ctx.step_id = stepId;
30280
- fs.writeFileSync(filePath, JSON.stringify(ctx), "utf8");
30799
+ fs2.writeFileSync(filePath, JSON.stringify(ctx), "utf8");
30281
30800
  } catch (err) {
30282
30801
  this.opts.log?.warn(`failed to update context file step_id ${filePath}: ${String(err)}`);
30283
30802
  }
30284
30803
  }
30285
30804
  updateContextFileSessionId(filePath, sessionId) {
30286
30805
  try {
30287
- const raw = fs.readFileSync(filePath, "utf8");
30806
+ const raw = fs2.readFileSync(filePath, "utf8");
30288
30807
  const ctx = JSON.parse(raw);
30289
30808
  ctx.session_id = sessionId;
30290
- fs.writeFileSync(filePath, JSON.stringify(ctx), "utf8");
30809
+ fs2.writeFileSync(filePath, JSON.stringify(ctx), "utf8");
30291
30810
  } catch (err) {
30292
30811
  this.opts.log?.warn(`failed to update context file session_id ${filePath}: ${String(err)}`);
30293
30812
  }
@@ -30295,8 +30814,8 @@ var ParallAgentGateway = class {
30295
30814
  /** @deprecated Use writeContextFile / updateContextFileStepId. */
30296
30815
  writeStepIdFile(filePath, stepId) {
30297
30816
  try {
30298
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
30299
- fs.writeFileSync(filePath, stepId, "utf8");
30817
+ fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
30818
+ fs2.writeFileSync(filePath, stepId, "utf8");
30300
30819
  } catch (err) {
30301
30820
  this.opts.log?.warn(`failed to write step id file ${filePath}: ${String(err)}`);
30302
30821
  }
@@ -30304,7 +30823,7 @@ var ParallAgentGateway = class {
30304
30823
  /** @deprecated Use writeContextFile / updateContextFileStepId. */
30305
30824
  clearStepIdFile(filePath) {
30306
30825
  try {
30307
- fs.writeFileSync(filePath, "", "utf8");
30826
+ fs2.writeFileSync(filePath, "", "utf8");
30308
30827
  } catch {
30309
30828
  }
30310
30829
  }
@@ -30313,7 +30832,7 @@ var ParallAgentGateway = class {
30313
30832
  await this.createInputStep(sessionId, event);
30314
30833
  }
30315
30834
  }
30316
- async bindRuntimeSession(sessionKey, runtimeEvent, contextFilePath) {
30835
+ async bindRuntimeSession(sessionKey, runtimeEvent, contextFilePath, laneContextFilePath2) {
30317
30836
  const runtimeLaneKey = runtimeEvent.runtimeLaneKey || sessionKey;
30318
30837
  const existing = this.sessionBindings.get(sessionKey);
30319
30838
  if (existing && existing.runtimeLaneKey === runtimeLaneKey && existing.runtimeSessionId === runtimeEvent.runtimeSessionId) {
@@ -30357,6 +30876,9 @@ var ParallAgentGateway = class {
30357
30876
  if (contextFilePath) {
30358
30877
  this.updateContextFileSessionId(contextFilePath, session.id);
30359
30878
  }
30879
+ if (laneContextFilePath2) {
30880
+ this.updateContextFileSessionId(laneContextFilePath2, session.id);
30881
+ }
30360
30882
  await this.opts.onSessionBinding?.(binding);
30361
30883
  return binding;
30362
30884
  }
@@ -30383,14 +30905,27 @@ var ParallAgentGateway = class {
30383
30905
  const dispatchContext = this.buildDispatchContext(event, sessionKey);
30384
30906
  const contextFilePath = dispatchContext.contextFilePath;
30385
30907
  const stepIdFilePath = dispatchContext.stepIdFilePath;
30908
+ const activeLane = this.ledgerDisabled ? void 0 : this.laneLedger?.getForEvent(event);
30909
+ const laneContextFilePath2 = activeLane ? this.laneLedger?.laneContextPath(activeLane) : void 0;
30910
+ const contextBody = {
30911
+ session_id: dispatchContext.sessionId ?? null,
30912
+ chat_id: dispatchContext.chatId ?? null,
30913
+ trigger_message_id: dispatchContext.triggerMessageId ?? null,
30914
+ no_reply: dispatchContext.noReply,
30915
+ step_id: null,
30916
+ dispatch_event_id: activeLane?.typedDispatchEventId ?? activeLane?.folded.get(event.messageId) ?? null,
30917
+ lane: activeLane?.lane ?? null,
30918
+ target_uri: activeLane?.targetUri ?? null,
30919
+ thread_root_id: activeLane?.threadRootId ?? null,
30920
+ // Typed binding hint for the CLI: which task this dispatch is about
30921
+ // (parall task update attaches the typed effect only on a match).
30922
+ task_id: event.type === "task" ? event.targetId : null
30923
+ };
30386
30924
  if (contextFilePath) {
30387
- this.writeContextFile(contextFilePath, {
30388
- session_id: dispatchContext.sessionId ?? null,
30389
- chat_id: dispatchContext.chatId ?? null,
30390
- trigger_message_id: dispatchContext.triggerMessageId ?? null,
30391
- no_reply: dispatchContext.noReply,
30392
- step_id: null
30393
- });
30925
+ this.writeContextFile(contextFilePath, contextBody);
30926
+ }
30927
+ if (laneContextFilePath2) {
30928
+ this.writeContextFile(laneContextFilePath2, contextBody);
30394
30929
  }
30395
30930
  this.inFlightDispatches++;
30396
30931
  const deadlineTimer = this.DISPATCH_DEADLINE_MS > 0 ? setTimeout(() => {
@@ -30417,7 +30952,7 @@ var ParallAgentGateway = class {
30417
30952
  })) {
30418
30953
  if (runtimeEvent.type === "runtime_session") {
30419
30954
  const priorAgentSessionId = binding?.agentSessionId;
30420
- binding = await this.bindRuntimeSession(sessionKey, runtimeEvent, contextFilePath);
30955
+ binding = await this.bindRuntimeSession(sessionKey, runtimeEvent, contextFilePath, laneContextFilePath2);
30421
30956
  if (event.targetType === "channel_conversation" && binding.agentSessionId !== priorAgentSessionId) {
30422
30957
  try {
30423
30958
  await this.opts.client.setChannelConversationSession(this.opts.config.org_id, event.targetId, binding.agentSessionId);
@@ -30449,6 +30984,9 @@ var ParallAgentGateway = class {
30449
30984
  await this.createInputStep(binding.agentSessionId, event);
30450
30985
  inputStepsCreated = true;
30451
30986
  }
30987
+ if (activeLane && !this.ledgerDisabled) {
30988
+ this.laneLedger?.maybeRenew(activeLane);
30989
+ }
30452
30990
  if (captureText && runtimeEvent.type === "text" && runtimeEvent.text) {
30453
30991
  captureText.push(runtimeEvent.text);
30454
30992
  }
@@ -30465,7 +31003,7 @@ var ParallAgentGateway = class {
30465
31003
  } else if (runtimeEvent.type === "tool_result" && pendingSendCallIds.delete(runtimeEvent.callId)) {
30466
31004
  recordMessageSend(sessionKey, !runtimeEvent.error);
30467
31005
  }
30468
- await this.createRuntimeStep(binding.agentSessionId, event, runtimeEvent, stepIdFilePath, contextFilePath);
31006
+ await this.createRuntimeStep(binding.agentSessionId, event, runtimeEvent, stepIdFilePath, contextFilePath, laneContextFilePath2);
30469
31007
  }
30470
31008
  if (!binding) {
30471
31009
  binding = this.sessionBindings.get(sessionKey);
@@ -30487,7 +31025,7 @@ var ParallAgentGateway = class {
30487
31025
  await this.createRuntimeStep(binding.agentSessionId, event, {
30488
31026
  type: "error",
30489
31027
  message: `Dispatch failed: ${String(err)}`
30490
- }, stepIdFilePath, contextFilePath);
31028
+ }, stepIdFilePath, contextFilePath, laneContextFilePath2);
30491
31029
  } catch (stepErr) {
30492
31030
  if (this.isSessionNotLiveError(stepErr))
30493
31031
  staleDetected = true;
@@ -30530,6 +31068,9 @@ var ParallAgentGateway = class {
30530
31068
  } else if (stepIdFilePath) {
30531
31069
  this.clearStepIdFile(stepIdFilePath);
30532
31070
  }
31071
+ if (laneContextFilePath2) {
31072
+ this.updateContextFileStepId(laneContextFilePath2, null);
31073
+ }
30533
31074
  this.inFlightDispatches--;
30534
31075
  if (this.inFlightDispatches === 0 && this.drainResolvers.length > 0) {
30535
31076
  const resolvers = this.drainResolvers.splice(0);
@@ -30587,13 +31128,39 @@ var ParallAgentGateway = class {
30587
31128
  item.resolve(false);
30588
31129
  break;
30589
31130
  }
30590
- const items = fork.queue.splice(0);
31131
+ let items;
31132
+ const head = fork.queue[0];
31133
+ if (head && this.usesLaneLedger(head.event)) {
31134
+ const headKey = this.dispatchGroupKey(head.event);
31135
+ const splitAt = fork.queue.findIndex((it) => this.dispatchGroupKey(it.event) !== headKey);
31136
+ items = splitAt === -1 ? fork.queue.splice(0) : fork.queue.splice(0, splitAt);
31137
+ } else {
31138
+ items = fork.queue.splice(0);
31139
+ }
30591
31140
  const events = items.map((item) => item.event);
30592
31141
  const last = events[events.length - 1];
30593
31142
  const earlier = events.slice(0, -1);
30594
31143
  try {
30595
31144
  const batchText = [];
30596
- const dispatched = await this.runDispatch(last, fork.fork.sessionKey, buildForkScopePrefix(last) + buildEventBody(last), earlier, batchText);
31145
+ let dispatched;
31146
+ if (this.usesLaneLedger(last)) {
31147
+ const outcome = await this.dispatchLaneGroup({
31148
+ events,
31149
+ sessionKey: fork.fork.sessionKey,
31150
+ body: buildForkScopePrefix(last) + buildEventBody(last),
31151
+ earlier,
31152
+ captureText: batchText,
31153
+ hasMoreLocal: () => fork.queue.length > 0
31154
+ });
31155
+ if (outcome === "foreign") {
31156
+ for (const item of items)
31157
+ item.resolve(false);
31158
+ break;
31159
+ }
31160
+ dispatched = outcome === "dispatched";
31161
+ } else {
31162
+ dispatched = await this.runDispatch(last, fork.fork.sessionKey, buildForkScopePrefix(last) + buildEventBody(last), earlier, batchText);
31163
+ }
30597
31164
  if (!dispatched) {
30598
31165
  for (const item of items) {
30599
31166
  item.resolve(false);
@@ -30696,9 +31263,9 @@ var ParallAgentGateway = class {
30696
31263
  this.opts.log?.info(`drainMainBuffer halted (shutting down) \u2014 ${this.dispatchState.mainBuffer.length} buffered, ${this.dispatchState.pendingForkResults.length} pending fork results left for catch-up`);
30697
31264
  break;
30698
31265
  }
30699
- const targetId = this.dispatchState.mainBuffer[0].targetId;
31266
+ const groupKey = this.dispatchGroupKey(this.dispatchState.mainBuffer[0]);
30700
31267
  const events = [];
30701
- while (this.dispatchState.mainBuffer[0]?.targetId === targetId) {
31268
+ while (this.dispatchState.mainBuffer[0] && this.dispatchGroupKey(this.dispatchState.mainBuffer[0]) === groupKey) {
30702
31269
  events.push(this.dispatchState.mainBuffer.shift());
30703
31270
  }
30704
31271
  const event = events[events.length - 1];
@@ -30707,7 +31274,36 @@ var ParallAgentGateway = class {
30707
31274
  const pendingFork = hasPendingInjections ? [] : this.dispatchState.pendingForkResults.splice(0);
30708
31275
  const forkPrefix = buildForkResultPrefix(pendingFork);
30709
31276
  this.dispatchState.mainCurrentTargetId = event.targetId;
31277
+ this.mainCurrentGroupKey = groupKey;
30710
31278
  this.dispatchState.mainPreDispatchBranchPoint = this.opts.dispatchAdapter.getBranchPoint?.(this.opts.runtimeKey);
31279
+ if (this.usesLaneLedger(event)) {
31280
+ let outcome;
31281
+ try {
31282
+ outcome = await this.dispatchLaneGroup({
31283
+ events,
31284
+ sessionKey: this.opts.runtimeKey,
31285
+ body: forkPrefix + buildEventBody(event),
31286
+ earlier,
31287
+ hasMoreLocal: () => this.dispatchState.mainBuffer.some((e) => this.dispatchGroupKey(e) === groupKey)
31288
+ });
31289
+ } catch (err) {
31290
+ this.opts.log?.error(`lane dispatch failed for ${event.messageId}: ${String(err)}`);
31291
+ this.dispatchState.pendingForkResults.unshift(...pendingFork);
31292
+ for (const ev of events)
31293
+ this.dispatchedMessages.delete(ev.messageId);
31294
+ continue;
31295
+ }
31296
+ if (outcome === "shutdown") {
31297
+ this.dispatchState.mainBuffer.unshift(...events);
31298
+ this.dispatchState.pendingForkResults.unshift(...pendingFork);
31299
+ break;
31300
+ }
31301
+ if (outcome === "foreign") {
31302
+ this.dispatchState.pendingForkResults.unshift(...pendingFork);
31303
+ continue;
31304
+ }
31305
+ continue;
31306
+ }
30711
31307
  try {
30712
31308
  await this.emitDispatchReceived(event);
30713
31309
  } catch (err) {
@@ -30736,6 +31332,7 @@ var ParallAgentGateway = class {
30736
31332
  this.draining = false;
30737
31333
  this.dispatchState.mainDispatching = false;
30738
31334
  this.dispatchState.mainCurrentTargetId = void 0;
31335
+ this.mainCurrentGroupKey = void 0;
30739
31336
  this.dispatchState.mainPreDispatchBranchPoint = void 0;
30740
31337
  if (!this.shuttingDown && this.dispatchState.mainBuffer.length > 0) {
30741
31338
  setTimeout(() => {
@@ -30755,13 +31352,38 @@ var ParallAgentGateway = class {
30755
31352
  const forkPrefix = buildForkResultPrefix(pendingFork);
30756
31353
  this.dispatchState.mainDispatching = true;
30757
31354
  this.dispatchState.mainCurrentTargetId = event.targetId;
31355
+ this.mainCurrentGroupKey = this.dispatchGroupKey(event);
30758
31356
  this.dispatchState.mainPreDispatchBranchPoint = this.opts.dispatchAdapter.getBranchPoint?.(this.opts.runtimeKey);
31357
+ if (this.usesLaneLedger(event)) {
31358
+ let outcome = "shutdown";
31359
+ try {
31360
+ try {
31361
+ outcome = await this.dispatchLaneGroup({
31362
+ events: [event],
31363
+ sessionKey: this.opts.runtimeKey,
31364
+ body: forkPrefix + buildEventBody(event),
31365
+ earlier: [],
31366
+ hasMoreLocal: () => this.dispatchState.mainBuffer.some((e) => this.dispatchGroupKey(e) === this.dispatchGroupKey(event))
31367
+ });
31368
+ } catch (err) {
31369
+ this.dispatchState.pendingForkResults.unshift(...pendingFork);
31370
+ throw err;
31371
+ }
31372
+ if (outcome !== "dispatched") {
31373
+ this.dispatchState.pendingForkResults.unshift(...pendingFork);
31374
+ }
31375
+ } finally {
31376
+ await this.drainMainBuffer();
31377
+ }
31378
+ return outcome === "dispatched";
31379
+ }
30759
31380
  try {
30760
31381
  await this.emitDispatchReceived(event);
30761
31382
  } catch (err) {
30762
31383
  this.opts.log?.warn?.(`mark-received failed, leaving unacked for retry: ${String(err)}`);
30763
31384
  this.dispatchState.mainDispatching = false;
30764
31385
  this.dispatchState.mainCurrentTargetId = void 0;
31386
+ this.mainCurrentGroupKey = void 0;
30765
31387
  this.dispatchState.mainPreDispatchBranchPoint = void 0;
30766
31388
  this.dispatchState.pendingForkResults.unshift(...pendingFork);
30767
31389
  return false;
@@ -30782,7 +31404,11 @@ var ParallAgentGateway = class {
30782
31404
  return false;
30783
31405
  }
30784
31406
  this.dispatchState.mainBuffer.push(event);
30785
- if (this.dispatchState.mainCurrentTargetId === event.targetId && await this.opts.dispatchAdapter.enqueueDuringDispatch?.(this.opts.runtimeKey, buildEventBody(event))) {
31407
+ if (this.usesLaneLedger(event)) {
31408
+ if (this.mainCurrentGroupKey === this.dispatchGroupKey(event) && await this.laneLedger?.steerLive(event) && await this.opts.dispatchAdapter.enqueueDuringDispatch?.(this.opts.runtimeKey, buildEventBody(event))) {
31409
+ this.opts.log?.info(`steer folded+injected for ${event.messageId} (will drain for bookkeeping)`);
31410
+ }
31411
+ } else if (this.dispatchState.mainCurrentTargetId === event.targetId && await this.opts.dispatchAdapter.enqueueDuringDispatch?.(this.opts.runtimeKey, buildEventBody(event))) {
30786
31412
  this.opts.log?.info(`steer injected for ${event.messageId} (will drain for bookkeeping)`);
30787
31413
  }
30788
31414
  if (!this.dispatchState.mainDispatching && !this.draining && this.dispatchState.mainBuffer.length > 0) {
@@ -30953,8 +31579,10 @@ var ParallAgentGateway = class {
30953
31579
  try {
30954
31580
  const dispatched = await this.handleInboundEvent(event);
30955
31581
  if (dispatched) {
30956
- this.opts.client.ackDispatch(this.opts.config.org_id, { source_type: "message", source_id: data.id }).catch(() => {
30957
- });
31582
+ if (!this.usesLaneLedger(event)) {
31583
+ this.opts.client.ackDispatch(this.opts.config.org_id, { source_type: "message", source_id: data.id }).catch(() => {
31584
+ });
31585
+ }
30958
31586
  } else {
30959
31587
  this.dispatchedMessages.delete(data.id);
30960
31588
  }
@@ -30963,7 +31591,21 @@ var ParallAgentGateway = class {
30963
31591
  this.dispatchedMessages.delete(data.id);
30964
31592
  }
30965
31593
  }
30966
- async handleTaskAssignment(task, ackSourceId) {
31594
+ // Ledger re-drive consumption: dispatch.new message hints re-enter the
31595
+ // shared WorkItem consumption path (same protocol as catch-up).
31596
+ async handleMessageRedrive(item) {
31597
+ if (!item.chat_id || !item.source_id)
31598
+ return;
31599
+ await this.consumeMessageWorkItem({
31600
+ id: item.id,
31601
+ source_id: item.source_id,
31602
+ chat_id: item.chat_id
31603
+ });
31604
+ }
31605
+ consumeMessageWorkItem(item) {
31606
+ return consumeMessageWorkItem(this.laneFlowHost(), item);
31607
+ }
31608
+ async handleTaskAssignment(task, ackSourceId, dispatchEventId) {
30967
31609
  if (this.shuttingDown)
30968
31610
  return false;
30969
31611
  const dedupeKey = `${task.id}:${task.updated_at}`;
@@ -30992,7 +31634,8 @@ var ParallAgentGateway = class {
30992
31634
  body: parts.join("\n"),
30993
31635
  sentAt: task.updated_at ?? task.created_at,
30994
31636
  ackSourceType: "task_activity",
30995
- ackSourceId
31637
+ ackSourceId,
31638
+ dispatchEventId
30996
31639
  };
30997
31640
  const dispatched = await this.handleInboundEvent(event);
30998
31641
  if (!dispatched) {
@@ -31016,7 +31659,7 @@ var ParallAgentGateway = class {
31016
31659
  this.opts.log?.info(`skipping stale task dispatch ${ackSourceId ?? taskId} \u2014 assigned to ${task.assignee_id}, creator ${task.creator_id}`);
31017
31660
  return true;
31018
31661
  }
31019
- return this.handleTaskAssignment(task, ackSourceId);
31662
+ return this.handleTaskAssignment(task, ackSourceId, opts.dispatchEventId);
31020
31663
  }
31021
31664
  async handleTaskComment(commentId, taskId, actorId, deliveryReason) {
31022
31665
  if (this.shuttingDown)
@@ -31424,74 +32067,46 @@ var ParallAgentGateway = class {
31424
32067
  }
31425
32068
  processed++;
31426
32069
  try {
31427
- let dispatched = false;
32070
+ const ackItem = () => {
32071
+ this.opts.client.ackDispatchByID(this.opts.config.org_id, item.id).catch(() => {
32072
+ });
32073
+ };
31428
32074
  if (item.event_type === "task_assign" && item.task_id) {
31429
32075
  try {
31430
- dispatched = await this.handleTaskDispatch(item.task_id, item.source_id ?? item.task_id);
32076
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, (dispatchEventId) => this.handleTaskDispatch(item.task_id ?? "", item.source_id ?? item.task_id ?? "", {
32077
+ dispatchEventId
32078
+ }), ackItem);
31431
32079
  } catch (err) {
31432
32080
  this.opts.log?.warn(`catch-up task fetch failed for ${item.task_id}, leaving pending: ${String(err)}`);
31433
32081
  continue;
31434
32082
  }
31435
32083
  } else if (item.event_type === "task_update" && item.task_id) {
31436
32084
  try {
31437
- dispatched = await this.handleTaskDispatch(item.task_id, item.source_id ?? item.task_id, { allowCreator: true });
32085
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, (dispatchEventId) => this.handleTaskDispatch(item.task_id ?? "", item.source_id ?? item.task_id ?? "", {
32086
+ allowCreator: true,
32087
+ dispatchEventId
32088
+ }), ackItem);
31438
32089
  } catch (err) {
31439
32090
  this.opts.log?.warn(`catch-up task fetch failed for ${item.task_id}, leaving pending: ${String(err)}`);
31440
32091
  continue;
31441
32092
  }
31442
32093
  } else if (item.event_type === "task_comment" && item.source_id && item.task_id) {
31443
- dispatched = await this.handleTaskComment(item.source_id, item.task_id, item.actor_id, item.delivery_reason);
32094
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.handleTaskComment(item.source_id, item.task_id ?? "", item.actor_id, item.delivery_reason), ackItem);
31444
32095
  } else if (item.event_type === "wiki_comment" && item.source_id) {
31445
- dispatched = await this.handleWikiComment(item.source_id, item.actor_id, item.delivery_reason);
32096
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.handleWikiComment(item.source_id, item.actor_id, item.delivery_reason), ackItem);
31446
32097
  } else if (item.event_type === "schedule.fire" && item.source_id) {
31447
- dispatched = await this.fetchAndHandleScheduleFire(item.source_id, item.actor_id);
32098
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.fetchAndHandleScheduleFire(item.source_id, item.actor_id), ackItem);
31448
32099
  } else if (item.event_type === "external_trigger" && item.source_id) {
31449
- dispatched = await this.fetchAndHandleExternalTriggerRun(item.source_id);
32100
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.fetchAndHandleExternalTriggerRun(item.source_id), ackItem);
31450
32101
  } else if (item.event_type === "channel_message" && item.source_id) {
31451
- dispatched = await this.fetchAndHandleChannelMessage(item.source_id);
32102
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.fetchAndHandleChannelMessage(item.source_id), ackItem);
31452
32103
  } else if (item.event_type === "approval_decided" && item.source_id) {
31453
- dispatched = await this.fetchAndHandleApprovalDecided(item.source_id, item.actor_id, item.chat_id ?? null);
32104
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.fetchAndHandleApprovalDecided(item.source_id, item.actor_id, item.chat_id ?? null), ackItem);
31454
32105
  } else if (item.event_type === "message" && item.source_id && item.chat_id) {
31455
- if (!this.tryClaimMessage(item.source_id))
31456
- continue;
31457
- let msg = null;
31458
- let msgFetchFailed = false;
31459
- try {
31460
- msg = await this.opts.client.getMessage(item.source_id);
31461
- } catch (err) {
31462
- const status = err?.status;
31463
- if (status === 404) {
31464
- msg = null;
31465
- } else {
31466
- msgFetchFailed = true;
31467
- this.opts.log?.warn(`catch-up message fetch failed for ${item.source_id}, leaving pending: ${String(err)}`);
31468
- }
31469
- }
31470
- if (msgFetchFailed) {
31471
- this.dispatchedMessages.delete(item.source_id);
31472
- continue;
31473
- }
31474
- if (!msg || msg.sender_id === this.opts.agentUserId) {
31475
- this.dispatchedMessages.delete(item.source_id);
31476
- this.opts.client.ackDispatchByID(this.opts.config.org_id, item.id).catch(() => {
31477
- });
31478
- continue;
31479
- }
31480
- const decision = await this.buildMessageDispatchDecision(item.chat_id, msg);
31481
- if (decision.action === "retry") {
31482
- this.dispatchedMessages.delete(item.source_id);
31483
- continue;
31484
- }
31485
- if (decision.action === "skip") {
31486
- this.dispatchedMessages.delete(item.source_id);
31487
- this.opts.client.ackDispatchByID(this.opts.config.org_id, item.id).catch(() => {
31488
- });
31489
- continue;
31490
- }
31491
- dispatched = await this.handleInboundEvent(decision.event);
31492
- }
31493
- if (dispatched) {
31494
- this.opts.client.ackDispatchByID(this.opts.config.org_id, item.id).catch(() => {
32106
+ await this.consumeMessageWorkItem({
32107
+ id: item.id,
32108
+ source_id: item.source_id,
32109
+ chat_id: item.chat_id
31495
32110
  });
31496
32111
  }
31497
32112
  } catch (err) {
@@ -31557,6 +32172,14 @@ ${fullSummary}` : fullSummary;
31557
32172
  this.abortFork(targetId, "ws reconnect");
31558
32173
  }
31559
32174
  }
32175
+ if (this.laneLedger && this.inFlightDispatches === 0 && this.laneLedger.activeCount > 0) {
32176
+ log2?.info(`releasing ${this.laneLedger.activeCount} stale lane(s) on reconnect`);
32177
+ await this.laneLedger.releaseAll();
32178
+ }
32179
+ if (this.laneLedger && this.ledgerDisabled) {
32180
+ log2?.info("re-probing dispatch ledger after reconnect (was disabled)");
32181
+ this.ledgerDisabled = false;
32182
+ }
31560
32183
  const intervalSec = data.heartbeat_interval > 0 ? data.heartbeat_interval : 30;
31561
32184
  try {
31562
32185
  const count = await fetchAllChats(client, config.org_id, this.chatInfoMap);
@@ -31627,6 +32250,10 @@ ${fullSummary}` : fullSummary;
31627
32250
  }
31628
32251
  if (this.heartbeatTimer)
31629
32252
  clearInterval(this.heartbeatTimer);
32253
+ if (this.laneLedger && this.laneLedger.activeCount > 0) {
32254
+ this.opts.log?.info(`releasing ${this.laneLedger.activeCount} lane(s) on shutdown`);
32255
+ await this.laneLedger.releaseAll();
32256
+ }
31630
32257
  await this.opts.onBeforeDisconnect?.();
31631
32258
  this.opts.ws.disconnect();
31632
32259
  this.opts.log?.info(`disconnected`);
@@ -31634,8 +32261,8 @@ ${fullSummary}` : fullSummary;
31634
32261
  };
31635
32262
 
31636
32263
  // ts/agent-core/dist/platform-config.js
31637
- import * as fs2 from "node:fs";
31638
- import * as path2 from "node:path";
32264
+ import * as fs3 from "node:fs";
32265
+ import * as path3 from "node:path";
31639
32266
  function isPlatformManagedProfile(profile) {
31640
32267
  return profile?.machine_id != null || profile?.model_management === "platform";
31641
32268
  }
@@ -31652,11 +32279,11 @@ function deriveModelIsPin(defaults, profile) {
31652
32279
  var CACHE_FILENAME = "parall-platform-config.json";
31653
32280
  var SUPPORTED_SCHEMA_VERSION = 1;
31654
32281
  function cachePath(stateDir) {
31655
- return path2.join(stateDir, CACHE_FILENAME);
32282
+ return path3.join(stateDir, CACHE_FILENAME);
31656
32283
  }
31657
32284
  function loadCache(stateDir) {
31658
32285
  try {
31659
- const raw = fs2.readFileSync(cachePath(stateDir), "utf-8");
32286
+ const raw = fs3.readFileSync(cachePath(stateDir), "utf-8");
31660
32287
  return JSON.parse(raw);
31661
32288
  } catch {
31662
32289
  return null;
@@ -31671,9 +32298,9 @@ function saveCache(stateDir, response) {
31671
32298
  };
31672
32299
  const filePath = cachePath(stateDir);
31673
32300
  const tmpPath = `${filePath}.tmp`;
31674
- fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
31675
- fs2.writeFileSync(tmpPath, JSON.stringify(cached, null, 2), "utf-8");
31676
- fs2.renameSync(tmpPath, filePath);
32301
+ fs3.mkdirSync(path3.dirname(filePath), { recursive: true });
32302
+ fs3.writeFileSync(tmpPath, JSON.stringify(cached, null, 2), "utf-8");
32303
+ fs3.renameSync(tmpPath, filePath);
31677
32304
  }
31678
32305
  function runtimeModelName(canonicalModel, runtimeType, config) {
31679
32306
  const runtime = runtimeType?.trim();
@@ -31764,8 +32391,8 @@ function createPlatformConfigManager(opts) {
31764
32391
  }
31765
32392
 
31766
32393
  // ts/agent-core/dist/skills/index.js
31767
- import * as fs3 from "node:fs";
31768
- import * as path3 from "node:path";
32394
+ import * as fs4 from "node:fs";
32395
+ import * as path4 from "node:path";
31769
32396
 
31770
32397
  // ts/agent-core/dist/skills/parall-platform.js
31771
32398
  var PARALL_PLATFORM_SKILL = `# Parall Platform
@@ -32538,13 +33165,13 @@ var SKILLS = [
32538
33165
  }
32539
33166
  ];
32540
33167
  function writeSkillFiles(targetDir) {
32541
- fs3.mkdirSync(targetDir, { recursive: true });
33168
+ fs4.mkdirSync(targetDir, { recursive: true });
32542
33169
  for (const skill of SKILLS) {
32543
- fs3.writeFileSync(path3.join(targetDir, `${skill.name}.md`), skill.content, "utf8");
33170
+ fs4.writeFileSync(path4.join(targetDir, `${skill.name}.md`), skill.content, "utf8");
32544
33171
  }
32545
33172
  }
32546
33173
  function buildSkillReferences(workspaceDir) {
32547
- const dir = path3.join(workspaceDir, ".parall", "skills");
33174
+ const dir = path4.join(workspaceDir, ".parall", "skills");
32548
33175
  const lines = SKILLS.map((s) => `- ${s.description.split(":")[0]}: \`${dir}/${s.name}.md\``);
32549
33176
  return `## Platform Skills (read on demand)
32550
33177
 
@@ -32554,7 +33181,7 @@ ${lines.join("\n")}
32554
33181
 
32555
33182
  // ts/claude-agent/dist/config.js
32556
33183
  import * as os2 from "node:os";
32557
- import * as path4 from "node:path";
33184
+ import * as path5 from "node:path";
32558
33185
  function requireEnv(env, name) {
32559
33186
  const value = env[name]?.trim();
32560
33187
  if (!value) {
@@ -32568,15 +33195,15 @@ function parseList(value) {
32568
33195
  return value.split(/[,\n]/).map((item) => item.trim()).filter(Boolean);
32569
33196
  }
32570
33197
  function resolvePath(value) {
32571
- return path4.isAbsolute(value) ? value : path4.resolve(process.cwd(), value);
33198
+ return path5.isAbsolute(value) ? value : path5.resolve(process.cwd(), value);
32572
33199
  }
32573
33200
  function resolveClaudeAgentConfig(env = process.env) {
32574
33201
  const apiUrl = requireEnv(env, "PRLL_API_URL");
32575
33202
  const apiKey = requireEnv(env, "PRLL_API_KEY");
32576
33203
  const orgId = requireEnv(env, "PRLL_ORG_ID");
32577
33204
  const claudeHome = resolvePath(env.PRLL_CLAUDE_HOME?.trim() || env.HOME || os2.homedir());
32578
- const stateDir = resolvePath(env.PRLL_STATE_DIR?.trim() || path4.join(claudeHome, ".parall-agent"));
32579
- const workspaceDir = resolvePath(env.PRLL_WORKSPACE_DIR?.trim() || path4.join(stateDir, "workspace"));
33205
+ const stateDir = resolvePath(env.PRLL_STATE_DIR?.trim() || path5.join(claudeHome, ".parall-agent"));
33206
+ const workspaceDir = resolvePath(env.PRLL_WORKSPACE_DIR?.trim() || path5.join(stateDir, "workspace"));
32580
33207
  const additionalDirs = parseList(env.PRLL_CLAUDE_ADD_DIRS).map(resolvePath);
32581
33208
  return {
32582
33209
  apiUrl,
@@ -32611,20 +33238,23 @@ function buildClaudeRuntimeKey(agentUserId) {
32611
33238
  }
32612
33239
  function sessionStateFilePathForRuntime(stateDir, runtimeKey) {
32613
33240
  const fileName = Buffer.from(runtimeKey).toString("base64url");
32614
- return path4.join(stateDir, "sessions", `${fileName}.json`);
33241
+ return path5.join(stateDir, "sessions", `${fileName}.json`);
32615
33242
  }
32616
33243
  function contextFilePathForSession(stateDir, sessionKey) {
32617
33244
  const fileName = Buffer.from(sessionKey).toString("base64url");
32618
- return path4.join(stateDir, "dispatch-context", `${fileName}.json`);
33245
+ return path5.join(stateDir, "dispatch-context", `${fileName}.json`);
33246
+ }
33247
+ function dispatchContextDirPath(stateDir) {
33248
+ return dispatchLaneContextDir(stateDir);
32619
33249
  }
32620
33250
  function stepIdFilePathForSession(stateDir, sessionKey) {
32621
33251
  const fileName = Buffer.from(sessionKey).toString("base64url");
32622
- return path4.join(stateDir, "step-ids", `${fileName}.txt`);
33252
+ return path5.join(stateDir, "step-ids", `${fileName}.txt`);
32623
33253
  }
32624
33254
 
32625
33255
  // ts/claude-agent/dist/dispatch.js
32626
- import * as fs5 from "node:fs";
32627
- import * as path6 from "node:path";
33256
+ import * as fs6 from "node:fs";
33257
+ import * as path7 from "node:path";
32628
33258
  import { randomUUID } from "node:crypto";
32629
33259
  import { execSync as execSync2, spawn } from "node:child_process";
32630
33260
 
@@ -32632,8 +33262,8 @@ import { execSync as execSync2, spawn } from "node:child_process";
32632
33262
  import { execSync } from "node:child_process";
32633
33263
  import { constants } from "node:fs";
32634
33264
  import * as fsSync from "node:fs";
32635
- import * as fs4 from "node:fs/promises";
32636
- import * as path5 from "node:path";
33265
+ import * as fs5 from "node:fs/promises";
33266
+ import * as path6 from "node:path";
32637
33267
  var DEFAULT_MAX_TOTAL_IMAGE_BYTES = 20 * 1024 * 1024;
32638
33268
  var DEFAULT_ATTACHMENT_CACHE_MAX_BYTES = 512 * 1024 * 1024;
32639
33269
  var DEFAULT_ATTACHMENT_DOWNLOAD_TIMEOUT_MS = 3e4;
@@ -32663,11 +33293,11 @@ async function prepareLocalImageAttachments(event, context2, opts) {
32663
33293
  };
32664
33294
  }
32665
33295
  const rootDir = await ensureAttachmentRootDir(opts.workspaceDir);
32666
- const messageDir = path5.join(rootDir, sanitizePathSegment(event.messageId));
33296
+ const messageDir = path6.join(rootDir, sanitizePathSegment(event.messageId));
32667
33297
  await ensurePathIsNotSymlink(messageDir);
32668
- await fs4.mkdir(messageDir, { recursive: true });
33298
+ await fs5.mkdir(messageDir, { recursive: true });
32669
33299
  await ensurePathIsNotSymlink(messageDir);
32670
- const activeMessageDir = path5.resolve(messageDir);
33300
+ const activeMessageDir = path6.resolve(messageDir);
32671
33301
  activeAttachmentDirs.add(activeMessageDir);
32672
33302
  const maintenanceCooldownMs = opts.maintenanceCooldownMs ?? DEFAULT_MAINTENANCE_COOLDOWN_MS;
32673
33303
  const maintenancePromise = scheduleAttachmentMaintenance(rootDir, {
@@ -32684,7 +33314,7 @@ async function prepareLocalImageAttachments(event, context2, opts) {
32684
33314
  const notes = [];
32685
33315
  let downloadedBytes = 0;
32686
33316
  for (const att of imageAttachments) {
32687
- const localPath = path5.join(messageDir, localFileName(att.id, att.fileName, att.mimeType));
33317
+ const localPath = path6.join(messageDir, localFileName(att.id, att.fileName, att.mimeType));
32688
33318
  const downloadTimeoutMs = opts.downloadTimeoutMs ?? DEFAULT_ATTACHMENT_DOWNLOAD_TIMEOUT_MS;
32689
33319
  const fetchFresh = async () => {
32690
33320
  const fileInfo = await withTimeout(context2.client.getFileUrl(att.id), downloadTimeoutMs, `file URL lookup timed out after ${downloadTimeoutMs}ms`);
@@ -32741,7 +33371,7 @@ async function appendPreparedLocalAttachmentRefs(body, event, context2, opts) {
32741
33371
  return { body: appendLocalAttachmentRefs(body, attachments), attachments };
32742
33372
  }
32743
33373
  function pinLocalAttachmentPaths(images) {
32744
- const dirs = new Set(images.map((image) => path5.resolve(path5.dirname(image.localPath))));
33374
+ const dirs = new Set(images.map((image) => path6.resolve(path6.dirname(image.localPath))));
32745
33375
  for (const dir of dirs) {
32746
33376
  activeAttachmentDirs.add(dir);
32747
33377
  }
@@ -32756,7 +33386,7 @@ function pinLocalAttachmentPaths(images) {
32756
33386
  };
32757
33387
  }
32758
33388
  function attachmentRootDir(workspaceDir) {
32759
- return path5.join(path5.resolve(workspaceDir), ".parall", "attachments");
33389
+ return path6.join(path6.resolve(workspaceDir), ".parall", "attachments");
32760
33390
  }
32761
33391
  function ensureLocalAttachmentGitExclude(workingDirectory) {
32762
33392
  try {
@@ -32765,8 +33395,8 @@ function ensureLocalAttachmentGitExclude(workingDirectory) {
32765
33395
  encoding: "utf8",
32766
33396
  stdio: ["ignore", "pipe", "ignore"]
32767
33397
  }).trim();
32768
- const excludePath = path5.isAbsolute(rel) ? rel : path5.join(workingDirectory, rel);
32769
- fsSync.mkdirSync(path5.dirname(excludePath), { recursive: true });
33398
+ const excludePath = path6.isAbsolute(rel) ? rel : path6.join(workingDirectory, rel);
33399
+ fsSync.mkdirSync(path6.dirname(excludePath), { recursive: true });
32770
33400
  const existing = fsSync.existsSync(excludePath) ? fsSync.readFileSync(excludePath, "utf8") : "";
32771
33401
  if (existing.split(/\r?\n/).some((line) => line.trim() === ".parall/"))
32772
33402
  return;
@@ -32802,18 +33432,18 @@ function scheduleAttachmentMaintenance(rootDir, opts) {
32802
33432
  return run;
32803
33433
  }
32804
33434
  async function ensureAttachmentRootDir(workspaceDir) {
32805
- const workspaceRoot = path5.resolve(workspaceDir);
32806
- const parallDir = path5.join(workspaceRoot, ".parall");
33435
+ const workspaceRoot = path6.resolve(workspaceDir);
33436
+ const parallDir = path6.join(workspaceRoot, ".parall");
32807
33437
  const rootDir = attachmentRootDir(workspaceRoot);
32808
- await fs4.mkdir(workspaceRoot, { recursive: true });
33438
+ await fs5.mkdir(workspaceRoot, { recursive: true });
32809
33439
  await ensurePathIsNotSymlink(parallDir);
32810
- await fs4.mkdir(parallDir, { recursive: true, mode: 448 });
33440
+ await fs5.mkdir(parallDir, { recursive: true, mode: 448 });
32811
33441
  await ensurePathIsNotSymlink(parallDir);
32812
33442
  await ensurePathIsNotSymlink(rootDir);
32813
- await fs4.mkdir(rootDir, { recursive: true, mode: 448 });
33443
+ await fs5.mkdir(rootDir, { recursive: true, mode: 448 });
32814
33444
  await ensurePathIsNotSymlink(rootDir);
32815
- const realWorkspace = await fs4.realpath(workspaceRoot);
32816
- const realRoot = await fs4.realpath(rootDir);
33445
+ const realWorkspace = await fs5.realpath(workspaceRoot);
33446
+ const realRoot = await fs5.realpath(rootDir);
32817
33447
  if (!isPathInside(realRoot, realWorkspace)) {
32818
33448
  throw new Error(`attachment root escapes workspace: ${rootDir}`);
32819
33449
  }
@@ -32821,7 +33451,7 @@ async function ensureAttachmentRootDir(workspaceDir) {
32821
33451
  }
32822
33452
  async function ensurePathIsNotSymlink(filePath) {
32823
33453
  try {
32824
- const stat = await fs4.lstat(filePath);
33454
+ const stat = await fs5.lstat(filePath);
32825
33455
  if (stat.isSymbolicLink()) {
32826
33456
  throw new Error(`refusing to use symlinked attachment path ${filePath}`);
32827
33457
  }
@@ -32832,8 +33462,8 @@ async function ensurePathIsNotSymlink(filePath) {
32832
33462
  }
32833
33463
  }
32834
33464
  function isPathInside(childPath, parentPath) {
32835
- const rel = path5.relative(parentPath, childPath);
32836
- return rel === "" || !!rel && !rel.startsWith("..") && !path5.isAbsolute(rel);
33465
+ const rel = path6.relative(parentPath, childPath);
33466
+ return rel === "" || !!rel && !rel.startsWith("..") && !path6.isAbsolute(rel);
32837
33467
  }
32838
33468
  async function existingUsableFile(filePath, expectedSize, rootDir) {
32839
33469
  try {
@@ -32844,30 +33474,30 @@ async function existingUsableFile(filePath, expectedSize, rootDir) {
32844
33474
  }
32845
33475
  }
32846
33476
  async function localFileStatInsideRoot(filePath, rootDir) {
32847
- const stat = await fs4.lstat(filePath);
33477
+ const stat = await fs5.lstat(filePath);
32848
33478
  if (stat.isSymbolicLink()) {
32849
33479
  throw new Error(`refusing to use symlinked attachment file ${filePath}`);
32850
33480
  }
32851
33481
  if (!stat.isFile()) {
32852
33482
  throw new Error(`attachment path is not a file ${filePath}`);
32853
33483
  }
32854
- const realRoot = await fs4.realpath(rootDir);
32855
- const realFile = await fs4.realpath(filePath);
33484
+ const realRoot = await fs5.realpath(rootDir);
33485
+ const realFile = await fs5.realpath(filePath);
32856
33486
  if (!isPathInside(realFile, realRoot)) {
32857
33487
  throw new Error(`attachment file escapes workspace: ${filePath}`);
32858
33488
  }
32859
33489
  return stat;
32860
33490
  }
32861
33491
  async function localDirectoryStatInsideRoot(dirPath, rootDir) {
32862
- const stat = await fs4.lstat(dirPath);
33492
+ const stat = await fs5.lstat(dirPath);
32863
33493
  if (stat.isSymbolicLink()) {
32864
33494
  throw new Error(`refusing to use symlinked attachment directory ${dirPath}`);
32865
33495
  }
32866
33496
  if (!stat.isDirectory()) {
32867
33497
  throw new Error(`attachment path is not a directory ${dirPath}`);
32868
33498
  }
32869
- const realRoot = await fs4.realpath(rootDir);
32870
- const realDir = await fs4.realpath(dirPath);
33499
+ const realRoot = await fs5.realpath(rootDir);
33500
+ const realDir = await fs5.realpath(dirPath);
32871
33501
  if (!isPathInside(realDir, realRoot)) {
32872
33502
  throw new Error(`attachment directory escapes workspace: ${dirPath}`);
32873
33503
  }
@@ -32875,7 +33505,7 @@ async function localDirectoryStatInsideRoot(dirPath, rootDir) {
32875
33505
  }
32876
33506
  async function openLocalFileInsideRoot(filePath, rootDir) {
32877
33507
  const checkedStat = await localFileStatInsideRoot(filePath, rootDir);
32878
- const file = await fs4.open(filePath, constants.O_RDONLY | constants.O_NOFOLLOW);
33508
+ const file = await fs5.open(filePath, constants.O_RDONLY | constants.O_NOFOLLOW);
32879
33509
  let keepOpen = false;
32880
33510
  try {
32881
33511
  const openedStat = await file.stat();
@@ -32891,8 +33521,8 @@ async function openLocalFileInsideRoot(filePath, rootDir) {
32891
33521
  }
32892
33522
  }
32893
33523
  async function openLocalTempFileInsideRoot(filePath, rootDir) {
32894
- await localDirectoryStatInsideRoot(path5.dirname(filePath), rootDir);
32895
- const file = await fs4.open(filePath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
33524
+ await localDirectoryStatInsideRoot(path6.dirname(filePath), rootDir);
33525
+ const file = await fs5.open(filePath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
32896
33526
  let keepOpen = false;
32897
33527
  try {
32898
33528
  const checkedStat = await localFileStatInsideRoot(filePath, rootDir);
@@ -32917,7 +33547,7 @@ async function assertLocalFileIdentity(filePath, rootDir, expected) {
32917
33547
  async function removeLocalFileIfInside(filePath, rootDir) {
32918
33548
  try {
32919
33549
  await localFileStatInsideRoot(filePath, rootDir);
32920
- await fs4.rm(filePath, { force: true });
33550
+ await fs5.rm(filePath, { force: true });
32921
33551
  } catch {
32922
33552
  }
32923
33553
  }
@@ -32930,7 +33560,7 @@ function sameFile(a, b) {
32930
33560
  async function cleanupOldAttachmentFiles(rootDir, ttlMs, log2, preserveDirs) {
32931
33561
  let entries;
32932
33562
  try {
32933
- entries = await fs4.readdir(rootDir, { withFileTypes: true });
33563
+ entries = await fs5.readdir(rootDir, { withFileTypes: true });
32934
33564
  } catch {
32935
33565
  return;
32936
33566
  }
@@ -32938,15 +33568,15 @@ async function cleanupOldAttachmentFiles(rootDir, ttlMs, log2, preserveDirs) {
32938
33568
  await Promise.all(entries.map(async (entry) => {
32939
33569
  if (!entry.isDirectory())
32940
33570
  return;
32941
- const fullPath = path5.join(rootDir, entry.name);
33571
+ const fullPath = path6.join(rootDir, entry.name);
32942
33572
  try {
32943
- if (preserveDirs?.has(path5.resolve(fullPath)))
33573
+ if (preserveDirs?.has(path6.resolve(fullPath)))
32944
33574
  return;
32945
- const stat = await fs4.lstat(fullPath);
33575
+ const stat = await fs5.lstat(fullPath);
32946
33576
  if (!stat.isDirectory())
32947
33577
  return;
32948
33578
  if (stat.mtimeMs < cutoff) {
32949
- await fs4.rm(fullPath, { recursive: true, force: true });
33579
+ await fs5.rm(fullPath, { recursive: true, force: true });
32950
33580
  }
32951
33581
  } catch (err) {
32952
33582
  log2?.warn?.(`agent-core: failed to clean attachment temp dir ${fullPath}: ${String(err)}`);
@@ -32958,7 +33588,7 @@ async function pruneAttachmentCache(rootDir, maxBytes, log2, preserveDirs) {
32958
33588
  return;
32959
33589
  let entries;
32960
33590
  try {
32961
- entries = await fs4.readdir(rootDir, { withFileTypes: true });
33591
+ entries = await fs5.readdir(rootDir, { withFileTypes: true });
32962
33592
  } catch {
32963
33593
  return;
32964
33594
  }
@@ -32967,9 +33597,9 @@ async function pruneAttachmentCache(rootDir, maxBytes, log2, preserveDirs) {
32967
33597
  for (const entry of entries) {
32968
33598
  if (!entry.isDirectory())
32969
33599
  continue;
32970
- const fullPath = path5.join(rootDir, entry.name);
33600
+ const fullPath = path6.join(rootDir, entry.name);
32971
33601
  try {
32972
- const stat = await fs4.lstat(fullPath);
33602
+ const stat = await fs5.lstat(fullPath);
32973
33603
  if (!stat.isDirectory())
32974
33604
  continue;
32975
33605
  const size = await directorySize(fullPath);
@@ -32985,10 +33615,10 @@ async function pruneAttachmentCache(rootDir, maxBytes, log2, preserveDirs) {
32985
33615
  for (const dir of dirs) {
32986
33616
  if (total <= maxBytes)
32987
33617
  break;
32988
- if (preserveDirs?.has(path5.resolve(dir.path)))
33618
+ if (preserveDirs?.has(path6.resolve(dir.path)))
32989
33619
  continue;
32990
33620
  try {
32991
- await fs4.rm(dir.path, { recursive: true, force: true });
33621
+ await fs5.rm(dir.path, { recursive: true, force: true });
32992
33622
  total -= dir.size;
32993
33623
  } catch (err) {
32994
33624
  log2?.warn?.(`agent-core: failed to prune attachment cache dir ${dir.path}: ${String(err)}`);
@@ -32997,12 +33627,12 @@ async function pruneAttachmentCache(rootDir, maxBytes, log2, preserveDirs) {
32997
33627
  }
32998
33628
  async function directorySize(dirPath) {
32999
33629
  let total = 0;
33000
- const entries = await fs4.readdir(dirPath, { withFileTypes: true });
33630
+ const entries = await fs5.readdir(dirPath, { withFileTypes: true });
33001
33631
  for (const entry of entries) {
33002
- const fullPath = path5.join(dirPath, entry.name);
33632
+ const fullPath = path6.join(dirPath, entry.name);
33003
33633
  let stat;
33004
33634
  try {
33005
- stat = await fs4.lstat(fullPath);
33635
+ stat = await fs5.lstat(fullPath);
33006
33636
  } catch {
33007
33637
  continue;
33008
33638
  }
@@ -33017,10 +33647,10 @@ async function directorySize(dirPath) {
33017
33647
  return total;
33018
33648
  }
33019
33649
  function activeDirsForRoot(rootDir) {
33020
- const root = path5.resolve(rootDir);
33650
+ const root = path6.resolve(rootDir);
33021
33651
  const dirs = /* @__PURE__ */ new Set();
33022
33652
  for (const dir of activeAttachmentDirs) {
33023
- if (dir === root || dir.startsWith(`${root}${path5.sep}`)) {
33653
+ if (dir === root || dir.startsWith(`${root}${path6.sep}`)) {
33024
33654
  dirs.add(dir);
33025
33655
  }
33026
33656
  }
@@ -33117,9 +33747,9 @@ async function writeResponseToFileWithLimit(res, filePath, maxBytes, rootDir) {
33117
33747
  }
33118
33748
  writtenStat = await file.stat();
33119
33749
  await closeFile();
33120
- await localDirectoryStatInsideRoot(path5.dirname(filePath), rootDir);
33750
+ await localDirectoryStatInsideRoot(path6.dirname(filePath), rootDir);
33121
33751
  await assertLocalFileIdentity(tmpPath, rootDir, writtenStat);
33122
- await fs4.rename(tmpPath, filePath);
33752
+ await fs5.rename(tmpPath, filePath);
33123
33753
  completed = true;
33124
33754
  return written;
33125
33755
  } finally {
@@ -33135,9 +33765,9 @@ async function writeResponseToFileWithLimit(res, filePath, maxBytes, rootDir) {
33135
33765
  }
33136
33766
  }
33137
33767
  function localFileName(attachmentId, fileName, mimeType) {
33138
- const safeName = sanitizePathSegment(path5.basename(fileName || attachmentId));
33139
- const ext = path5.extname(safeName) || extensionForMime(mimeType);
33140
- const stem = path5.basename(safeName, path5.extname(safeName)) || attachmentId;
33768
+ const safeName = sanitizePathSegment(path6.basename(fileName || attachmentId));
33769
+ const ext = path6.extname(safeName) || extensionForMime(mimeType);
33770
+ const stem = path6.basename(safeName, path6.extname(safeName)) || attachmentId;
33141
33771
  return `${sanitizePathSegment(attachmentId)}-${stem}${ext}`;
33142
33772
  }
33143
33773
  function extensionForMime(mimeType) {
@@ -33417,7 +34047,11 @@ function buildSpawnEnv(parentEnv, claudeHome, context2, opts) {
33417
34047
  PRLL_TRIGGER_MESSAGE_ID: context2.triggerMessageId ?? "",
33418
34048
  PRLL_NO_REPLY: context2.noReply ? "1" : "",
33419
34049
  PRLL_CONTEXT_FILE: context2.contextFilePath ?? "",
33420
- PRLL_STEP_ID_FILE: context2.stepIdFilePath ?? ""
34050
+ PRLL_STEP_ID_FILE: context2.stepIdFilePath ?? "",
34051
+ // Per-lane dispatch context directory (stable for the bridge's lifetime,
34052
+ // so pinning it at spawn is safe even for long-lived subprocesses). The
34053
+ // CLI keys into it by send target to derive dispatch effect keys.
34054
+ PRLL_CONTEXT_DIR: context2.contextDirPath ?? ""
33421
34055
  };
33422
34056
  if (!result.PRLL_WIKI_MOUNT_ROOT?.trim() && opts.wikiMountRoot) {
33423
34057
  result.PRLL_WIKI_MOUNT_ROOT = opts.wikiMountRoot;
@@ -33523,8 +34157,8 @@ var ClaudeCodeAdapter = class {
33523
34157
  if (!sessionId)
33524
34158
  return void 0;
33525
34159
  const projectSlug = this.opts.workspaceDir.replace(/[/.]/g, "-");
33526
- const filePath = path6.join(this.opts.claudeHome, ".claude", "projects", projectSlug, `${sessionId}.jsonl`);
33527
- return fs5.existsSync(filePath) ? filePath : void 0;
34160
+ const filePath = path7.join(this.opts.claudeHome, ".claude", "projects", projectSlug, `${sessionId}.jsonl`);
34161
+ return fs6.existsSync(filePath) ? filePath : void 0;
33528
34162
  }
33529
34163
  forkSession({ sessionKey }) {
33530
34164
  return this.opts.sessionManager.createForkSession(sessionKey);
@@ -33810,7 +34444,7 @@ var ClaudeCodeAdapter = class {
33810
34444
  if (this.opts.disallowedTools.length > 0) {
33811
34445
  args.push("--disallowedTools", this.opts.disallowedTools.join(","));
33812
34446
  }
33813
- args.push("--append-system-prompt-file", path6.join(this.opts.workspaceDir, ".parall", "system-prompt.md"));
34447
+ args.push("--append-system-prompt-file", path7.join(this.opts.workspaceDir, ".parall", "system-prompt.md"));
33814
34448
  if (this.opts.appendSystemPrompt) {
33815
34449
  args.push("--append-system-prompt", this.opts.appendSystemPrompt);
33816
34450
  }
@@ -33835,14 +34469,15 @@ var ClaudeCodeAdapter = class {
33835
34469
  noReply: false,
33836
34470
  contextFilePath: this.opts.contextFilePathForSession?.(sessionKey) ?? "",
33837
34471
  stepIdFilePath: this.opts.stepIdFilePathForSession?.(sessionKey) ?? "",
34472
+ contextDirPath: this.opts.contextDirPath ?? "",
33838
34473
  client: void 0
33839
34474
  };
33840
34475
  }
33841
34476
  };
33842
34477
 
33843
34478
  // ts/claude-agent/dist/session-manager.js
33844
- import * as fs6 from "node:fs";
33845
- import * as path7 from "node:path";
34479
+ import * as fs7 from "node:fs";
34480
+ import * as path8 from "node:path";
33846
34481
  import { randomUUID as randomUUID2 } from "node:crypto";
33847
34482
  var ClaudeSessionManager = class _ClaudeSessionManager {
33848
34483
  mainSessionKey;
@@ -33991,13 +34626,13 @@ var ClaudeSessionManager = class _ClaudeSessionManager {
33991
34626
  clearMainSession() {
33992
34627
  this.sessionIds.delete(this.mainSessionKey);
33993
34628
  try {
33994
- fs6.unlinkSync(this.stateFilePath);
34629
+ fs7.unlinkSync(this.stateFilePath);
33995
34630
  } catch {
33996
34631
  }
33997
34632
  }
33998
34633
  restore() {
33999
34634
  try {
34000
- const raw = fs6.readFileSync(this.stateFilePath, "utf8");
34635
+ const raw = fs7.readFileSync(this.stateFilePath, "utf8");
34001
34636
  const parsed = JSON.parse(raw);
34002
34637
  if (parsed.runtimeKey === this.mainSessionKey && typeof parsed.sessionId === "string" && parsed.sessionId.trim()) {
34003
34638
  this.sessionIds.set(this.mainSessionKey, parsed.sessionId.trim());
@@ -34007,8 +34642,8 @@ var ClaudeSessionManager = class _ClaudeSessionManager {
34007
34642
  }
34008
34643
  persist(sessionId) {
34009
34644
  try {
34010
- fs6.mkdirSync(path7.dirname(this.stateFilePath), { recursive: true });
34011
- fs6.writeFileSync(this.stateFilePath, JSON.stringify({
34645
+ fs7.mkdirSync(path8.dirname(this.stateFilePath), { recursive: true });
34646
+ fs7.writeFileSync(this.stateFilePath, JSON.stringify({
34012
34647
  runtimeKey: this.mainSessionKey,
34013
34648
  sessionId
34014
34649
  }, null, 2));
@@ -34019,8 +34654,8 @@ var ClaudeSessionManager = class _ClaudeSessionManager {
34019
34654
  };
34020
34655
 
34021
34656
  // ts/claude-agent/dist/workspace.js
34022
- import * as fs7 from "node:fs";
34023
- import * as path8 from "node:path";
34657
+ import * as fs8 from "node:fs";
34658
+ import * as path9 from "node:path";
34024
34659
  function ensureClaudeWorkspace(workspaceDir, log2, agentIdentity) {
34025
34660
  const systemPrompt = [
34026
34661
  buildIdentity(agentIdentity),
@@ -34029,12 +34664,12 @@ function ensureClaudeWorkspace(workspaceDir, log2, agentIdentity) {
34029
34664
  PRLL_REFERENCE_GUIDE,
34030
34665
  buildSkillReferences(workspaceDir)
34031
34666
  ].join("\n\n");
34032
- fs7.mkdirSync(workspaceDir, { recursive: true });
34033
- fs7.mkdirSync(path8.join(workspaceDir, ".claude"), { recursive: true });
34034
- const parallDir = path8.join(workspaceDir, ".parall");
34035
- fs7.mkdirSync(parallDir, { recursive: true });
34036
- fs7.writeFileSync(path8.join(parallDir, "system-prompt.md"), systemPrompt, "utf8");
34037
- writeSkillFiles(path8.join(parallDir, "skills"));
34667
+ fs8.mkdirSync(workspaceDir, { recursive: true });
34668
+ fs8.mkdirSync(path9.join(workspaceDir, ".claude"), { recursive: true });
34669
+ const parallDir = path9.join(workspaceDir, ".parall");
34670
+ fs8.mkdirSync(parallDir, { recursive: true });
34671
+ fs8.writeFileSync(path9.join(parallDir, "system-prompt.md"), systemPrompt, "utf8");
34672
+ writeSkillFiles(path9.join(parallDir, "skills"));
34038
34673
  ensureLocalAttachmentGitExclude(workspaceDir);
34039
34674
  }
34040
34675
 
@@ -34135,7 +34770,8 @@ async function main() {
34135
34770
  apiKey: config.apiKey,
34136
34771
  orgId: config.orgId,
34137
34772
  contextFilePathForSession: (sessionKey) => contextFilePathForSession(config.stateDir, sessionKey),
34138
- stepIdFilePathForSession: (sessionKey) => stepIdFilePathForSession(config.stateDir, sessionKey)
34773
+ stepIdFilePathForSession: (sessionKey) => stepIdFilePathForSession(config.stateDir, sessionKey),
34774
+ contextDirPath: dispatchContextDirPath(config.stateDir)
34139
34775
  });
34140
34776
  if (resolvedEffort) {
34141
34777
  adapter.updateConfig({ effort: resolvedEffort });
@@ -34183,6 +34819,7 @@ async function main() {
34183
34819
  dispatchDeadlineMs: parseDispatchDeadlineMs(process.env.PRLL_DISPATCH_DEADLINE_MS),
34184
34820
  contextFilePathForSession: (sessionKey) => contextFilePathForSession(config.stateDir, sessionKey),
34185
34821
  stepIdFilePathForSession: (sessionKey) => stepIdFilePathForSession(config.stateDir, sessionKey),
34822
+ dispatchContextDir: dispatchContextDirPath(config.stateDir),
34186
34823
  onConfigUpdate: refreshPlatformConfig,
34187
34824
  onSessionReady: refreshPlatformConfig,
34188
34825
  // Long-lived `claude` subprocesses outlive a single dispatch, so the