@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 path8 of paths) {
17721
+ for (const path9 of paths) {
17722
17722
  try {
17723
- const result = await fs_1.promises.readFile(path8, { encoding: "utf8" });
17723
+ const result = await fs_1.promises.readFile(path9, { 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, path8) {
21126
+ function appendResourcePathToUrl(url, path9) {
21127
21127
  try {
21128
21128
  new URL(url);
21129
21129
  } catch (_a) {
@@ -21133,11 +21133,11 @@ function appendResourcePathToUrl(url, path8) {
21133
21133
  if (!url.endsWith("/")) {
21134
21134
  url = url + "/";
21135
21135
  }
21136
- url += path8;
21136
+ url += path9;
21137
21137
  try {
21138
21138
  new URL(url);
21139
21139
  } catch (_b) {
21140
- diag2.warn("Configuration: Provided URL appended with '" + path8 + "' is not a valid URL, using 'undefined' instead of '" + url + "'");
21140
+ diag2.warn("Configuration: Provided URL appended with '" + path9 + "' 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(path8) {
27597
- return path8.startsWith(WIKI_BASE) ? this.wikiBaseUrl : this.baseUrl;
27615
+ baseUrlFor(path9) {
27616
+ return path9.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(path8) {
27643
+ async ensureFreshToken(path9) {
27625
27644
  if (!this.token || !this.getRefreshToken)
27626
27645
  return;
27627
- const pathSuffix = path8.replace(/^\/api\/v1/, "");
27646
+ const pathSuffix = path9.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, path8, body, query, retried = false, opts) {
27678
+ async request(method, path9, body, query, retried = false, opts) {
27660
27679
  if (!retried) {
27661
- await this.ensureFreshToken(path8);
27680
+ await this.ensureFreshToken(path9);
27662
27681
  }
27663
- let url = `${this.baseUrlFor(path8)}${path8}`;
27682
+ let url = `${this.baseUrlFor(path9)}${path9}`;
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 = path8.replace(/^\/api\/v1/, "");
27712
+ const pathSuffix = path9.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, path8, body, query, true, opts);
27717
+ return this.request(method, path9, 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, path8, body, retried = false) {
27747
+ async multipartRequest(method, path9, body, retried = false) {
27728
27748
  if (!retried) {
27729
- await this.ensureFreshToken(path8);
27749
+ await this.ensureFreshToken(path9);
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(path8)}${path8}`, {
27755
+ res = await fetch(`${this.baseUrlFor(path9)}${path9}`, {
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 = path8.replace(/^\/api\/v1/, "");
27765
+ const pathSuffix = path9.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, path8, body, true);
27770
+ return this.multipartRequest(method, path9, 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, path8) {
28430
- return this.request("POST", ENDPOINTS.MACHINE_BROWSE(orgId, machineId), { path: path8 }, void 0, false, { timeoutMs: 15e3 });
28464
+ async browseMachineFilesystem(orgId, machineId, path9) {
28465
+ return this.request("POST", ENDPOINTS.MACHINE_BROWSE(orgId, machineId), { path: path9 }, 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, path8) {
28968
- return this.request("GET", ENDPOINTS.WIKI_ACCESS_STATUS(orgId, wikiId), void 0, path8 ? { path: path8 } : void 0);
29026
+ async getWikiAccessStatus(orgId, wikiId, path9) {
29027
+ return this.request("GET", ENDPOINTS.WIKI_ACCESS_STATUS(orgId, wikiId), void 0, path9 ? { path: path9 } : 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, path8, params) {
29036
+ async getWikiFileCommits(orgId, wikiId, path9, params) {
28978
29037
  return this.request("GET", ENDPOINTS.WIKI_FILE_COMMITS(orgId, wikiId), void 0, {
28979
- path: path8,
29038
+ path: path9,
28980
29039
  ...params
28981
29040
  });
28982
29041
  }
28983
- async getWikiBlame(orgId, wikiId, path8, ref) {
28984
- return this.request("GET", ENDPOINTS.WIKI_BLAME(orgId, wikiId), void 0, { path: path8, ref });
29042
+ async getWikiBlame(orgId, wikiId, path9, ref) {
29043
+ return this.request("GET", ENDPOINTS.WIKI_BLAME(orgId, wikiId), void 0, { path: path9, 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/codex-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) {
@@ -32563,15 +33190,15 @@ function requireEnv(env, name) {
32563
33190
  return value;
32564
33191
  }
32565
33192
  function resolvePath(value) {
32566
- return path4.isAbsolute(value) ? value : path4.resolve(process.cwd(), value);
33193
+ return path5.isAbsolute(value) ? value : path5.resolve(process.cwd(), value);
32567
33194
  }
32568
33195
  function resolveCodexAgentConfig(env = process.env) {
32569
33196
  const apiUrl = requireEnv(env, "PRLL_API_URL");
32570
33197
  const apiKey = requireEnv(env, "PRLL_API_KEY");
32571
33198
  const orgId = requireEnv(env, "PRLL_ORG_ID");
32572
- const codexHome = resolvePath(env.PRLL_CODEX_HOME?.trim() || env.CODEX_HOME?.trim() || path4.join(env.HOME || os2.homedir(), ".codex"));
32573
- const stateDir = resolvePath(env.PRLL_STATE_DIR?.trim() || path4.join(env.HOME || os2.homedir(), ".parall-agent"));
32574
- const workspaceDir = resolvePath(env.PRLL_WORKSPACE_DIR?.trim() || path4.join(stateDir, "workspace"));
33199
+ const codexHome = resolvePath(env.PRLL_CODEX_HOME?.trim() || env.CODEX_HOME?.trim() || path5.join(env.HOME || os2.homedir(), ".codex"));
33200
+ const stateDir = resolvePath(env.PRLL_STATE_DIR?.trim() || path5.join(env.HOME || os2.homedir(), ".parall-agent"));
33201
+ const workspaceDir = resolvePath(env.PRLL_WORKSPACE_DIR?.trim() || path5.join(stateDir, "workspace"));
32575
33202
  return {
32576
33203
  apiUrl,
32577
33204
  apiKey,
@@ -32625,28 +33252,31 @@ function buildCodexRuntimeKey(agentUserId) {
32625
33252
  }
32626
33253
  function sessionStateFilePathForRuntime(stateDir, runtimeKey) {
32627
33254
  const fileName = Buffer.from(runtimeKey).toString("base64url");
32628
- return path4.join(stateDir, "threads", `${fileName}.json`);
33255
+ return path5.join(stateDir, "threads", `${fileName}.json`);
32629
33256
  }
32630
33257
  function contextFilePathForSession(stateDir, sessionKey) {
32631
33258
  const fileName = Buffer.from(sessionKey).toString("base64url");
32632
- return path4.join(stateDir, "dispatch-context", `${fileName}.json`);
33259
+ return path5.join(stateDir, "dispatch-context", `${fileName}.json`);
33260
+ }
33261
+ function dispatchContextDirPath(stateDir) {
33262
+ return dispatchLaneContextDir(stateDir);
32633
33263
  }
32634
33264
  function stepIdFilePathForSession(stateDir, sessionKey) {
32635
33265
  const fileName = Buffer.from(sessionKey).toString("base64url");
32636
- return path4.join(stateDir, "step-ids", `${fileName}.txt`);
33266
+ return path5.join(stateDir, "step-ids", `${fileName}.txt`);
32637
33267
  }
32638
33268
 
32639
33269
  // ts/codex-agent/dist/dispatch.js
32640
33270
  import { execSync as execSync2, spawn } from "node:child_process";
32641
33271
  import { randomUUID } from "node:crypto";
32642
- import * as fs5 from "node:fs";
33272
+ import * as fs6 from "node:fs";
32643
33273
 
32644
33274
  // ts/agent-core/dist/internal/attachment-input.js
32645
33275
  import { execSync } from "node:child_process";
32646
33276
  import { constants } from "node:fs";
32647
33277
  import * as fsSync from "node:fs";
32648
- import * as fs4 from "node:fs/promises";
32649
- import * as path5 from "node:path";
33278
+ import * as fs5 from "node:fs/promises";
33279
+ import * as path6 from "node:path";
32650
33280
  var DEFAULT_MAX_TOTAL_IMAGE_BYTES = 20 * 1024 * 1024;
32651
33281
  var DEFAULT_ATTACHMENT_CACHE_MAX_BYTES = 512 * 1024 * 1024;
32652
33282
  var DEFAULT_ATTACHMENT_DOWNLOAD_TIMEOUT_MS = 3e4;
@@ -32676,11 +33306,11 @@ async function prepareLocalImageAttachments(event, context2, opts) {
32676
33306
  };
32677
33307
  }
32678
33308
  const rootDir = await ensureAttachmentRootDir(opts.workspaceDir);
32679
- const messageDir = path5.join(rootDir, sanitizePathSegment(event.messageId));
33309
+ const messageDir = path6.join(rootDir, sanitizePathSegment(event.messageId));
32680
33310
  await ensurePathIsNotSymlink(messageDir);
32681
- await fs4.mkdir(messageDir, { recursive: true });
33311
+ await fs5.mkdir(messageDir, { recursive: true });
32682
33312
  await ensurePathIsNotSymlink(messageDir);
32683
- const activeMessageDir = path5.resolve(messageDir);
33313
+ const activeMessageDir = path6.resolve(messageDir);
32684
33314
  activeAttachmentDirs.add(activeMessageDir);
32685
33315
  const maintenanceCooldownMs = opts.maintenanceCooldownMs ?? DEFAULT_MAINTENANCE_COOLDOWN_MS;
32686
33316
  const maintenancePromise = scheduleAttachmentMaintenance(rootDir, {
@@ -32697,7 +33327,7 @@ async function prepareLocalImageAttachments(event, context2, opts) {
32697
33327
  const notes = [];
32698
33328
  let downloadedBytes = 0;
32699
33329
  for (const att of imageAttachments) {
32700
- const localPath = path5.join(messageDir, localFileName(att.id, att.fileName, att.mimeType));
33330
+ const localPath = path6.join(messageDir, localFileName(att.id, att.fileName, att.mimeType));
32701
33331
  const downloadTimeoutMs = opts.downloadTimeoutMs ?? DEFAULT_ATTACHMENT_DOWNLOAD_TIMEOUT_MS;
32702
33332
  const fetchFresh = async () => {
32703
33333
  const fileInfo = await withTimeout(context2.client.getFileUrl(att.id), downloadTimeoutMs, `file URL lookup timed out after ${downloadTimeoutMs}ms`);
@@ -32754,7 +33384,7 @@ async function appendPreparedLocalAttachmentRefs(body, event, context2, opts) {
32754
33384
  return { body: appendLocalAttachmentRefs(body, attachments), attachments };
32755
33385
  }
32756
33386
  function pinLocalAttachmentPaths(images) {
32757
- const dirs = new Set(images.map((image) => path5.resolve(path5.dirname(image.localPath))));
33387
+ const dirs = new Set(images.map((image) => path6.resolve(path6.dirname(image.localPath))));
32758
33388
  for (const dir of dirs) {
32759
33389
  activeAttachmentDirs.add(dir);
32760
33390
  }
@@ -32769,7 +33399,7 @@ function pinLocalAttachmentPaths(images) {
32769
33399
  };
32770
33400
  }
32771
33401
  function attachmentRootDir(workspaceDir) {
32772
- return path5.join(path5.resolve(workspaceDir), ".parall", "attachments");
33402
+ return path6.join(path6.resolve(workspaceDir), ".parall", "attachments");
32773
33403
  }
32774
33404
  function ensureLocalAttachmentGitExclude(workingDirectory) {
32775
33405
  try {
@@ -32778,8 +33408,8 @@ function ensureLocalAttachmentGitExclude(workingDirectory) {
32778
33408
  encoding: "utf8",
32779
33409
  stdio: ["ignore", "pipe", "ignore"]
32780
33410
  }).trim();
32781
- const excludePath = path5.isAbsolute(rel) ? rel : path5.join(workingDirectory, rel);
32782
- fsSync.mkdirSync(path5.dirname(excludePath), { recursive: true });
33411
+ const excludePath = path6.isAbsolute(rel) ? rel : path6.join(workingDirectory, rel);
33412
+ fsSync.mkdirSync(path6.dirname(excludePath), { recursive: true });
32783
33413
  const existing = fsSync.existsSync(excludePath) ? fsSync.readFileSync(excludePath, "utf8") : "";
32784
33414
  if (existing.split(/\r?\n/).some((line) => line.trim() === ".parall/"))
32785
33415
  return;
@@ -32815,18 +33445,18 @@ function scheduleAttachmentMaintenance(rootDir, opts) {
32815
33445
  return run;
32816
33446
  }
32817
33447
  async function ensureAttachmentRootDir(workspaceDir) {
32818
- const workspaceRoot = path5.resolve(workspaceDir);
32819
- const parallDir = path5.join(workspaceRoot, ".parall");
33448
+ const workspaceRoot = path6.resolve(workspaceDir);
33449
+ const parallDir = path6.join(workspaceRoot, ".parall");
32820
33450
  const rootDir = attachmentRootDir(workspaceRoot);
32821
- await fs4.mkdir(workspaceRoot, { recursive: true });
33451
+ await fs5.mkdir(workspaceRoot, { recursive: true });
32822
33452
  await ensurePathIsNotSymlink(parallDir);
32823
- await fs4.mkdir(parallDir, { recursive: true, mode: 448 });
33453
+ await fs5.mkdir(parallDir, { recursive: true, mode: 448 });
32824
33454
  await ensurePathIsNotSymlink(parallDir);
32825
33455
  await ensurePathIsNotSymlink(rootDir);
32826
- await fs4.mkdir(rootDir, { recursive: true, mode: 448 });
33456
+ await fs5.mkdir(rootDir, { recursive: true, mode: 448 });
32827
33457
  await ensurePathIsNotSymlink(rootDir);
32828
- const realWorkspace = await fs4.realpath(workspaceRoot);
32829
- const realRoot = await fs4.realpath(rootDir);
33458
+ const realWorkspace = await fs5.realpath(workspaceRoot);
33459
+ const realRoot = await fs5.realpath(rootDir);
32830
33460
  if (!isPathInside(realRoot, realWorkspace)) {
32831
33461
  throw new Error(`attachment root escapes workspace: ${rootDir}`);
32832
33462
  }
@@ -32834,7 +33464,7 @@ async function ensureAttachmentRootDir(workspaceDir) {
32834
33464
  }
32835
33465
  async function ensurePathIsNotSymlink(filePath) {
32836
33466
  try {
32837
- const stat = await fs4.lstat(filePath);
33467
+ const stat = await fs5.lstat(filePath);
32838
33468
  if (stat.isSymbolicLink()) {
32839
33469
  throw new Error(`refusing to use symlinked attachment path ${filePath}`);
32840
33470
  }
@@ -32845,8 +33475,8 @@ async function ensurePathIsNotSymlink(filePath) {
32845
33475
  }
32846
33476
  }
32847
33477
  function isPathInside(childPath, parentPath) {
32848
- const rel = path5.relative(parentPath, childPath);
32849
- return rel === "" || !!rel && !rel.startsWith("..") && !path5.isAbsolute(rel);
33478
+ const rel = path6.relative(parentPath, childPath);
33479
+ return rel === "" || !!rel && !rel.startsWith("..") && !path6.isAbsolute(rel);
32850
33480
  }
32851
33481
  async function existingUsableFile(filePath, expectedSize, rootDir) {
32852
33482
  try {
@@ -32857,30 +33487,30 @@ async function existingUsableFile(filePath, expectedSize, rootDir) {
32857
33487
  }
32858
33488
  }
32859
33489
  async function localFileStatInsideRoot(filePath, rootDir) {
32860
- const stat = await fs4.lstat(filePath);
33490
+ const stat = await fs5.lstat(filePath);
32861
33491
  if (stat.isSymbolicLink()) {
32862
33492
  throw new Error(`refusing to use symlinked attachment file ${filePath}`);
32863
33493
  }
32864
33494
  if (!stat.isFile()) {
32865
33495
  throw new Error(`attachment path is not a file ${filePath}`);
32866
33496
  }
32867
- const realRoot = await fs4.realpath(rootDir);
32868
- const realFile = await fs4.realpath(filePath);
33497
+ const realRoot = await fs5.realpath(rootDir);
33498
+ const realFile = await fs5.realpath(filePath);
32869
33499
  if (!isPathInside(realFile, realRoot)) {
32870
33500
  throw new Error(`attachment file escapes workspace: ${filePath}`);
32871
33501
  }
32872
33502
  return stat;
32873
33503
  }
32874
33504
  async function localDirectoryStatInsideRoot(dirPath, rootDir) {
32875
- const stat = await fs4.lstat(dirPath);
33505
+ const stat = await fs5.lstat(dirPath);
32876
33506
  if (stat.isSymbolicLink()) {
32877
33507
  throw new Error(`refusing to use symlinked attachment directory ${dirPath}`);
32878
33508
  }
32879
33509
  if (!stat.isDirectory()) {
32880
33510
  throw new Error(`attachment path is not a directory ${dirPath}`);
32881
33511
  }
32882
- const realRoot = await fs4.realpath(rootDir);
32883
- const realDir = await fs4.realpath(dirPath);
33512
+ const realRoot = await fs5.realpath(rootDir);
33513
+ const realDir = await fs5.realpath(dirPath);
32884
33514
  if (!isPathInside(realDir, realRoot)) {
32885
33515
  throw new Error(`attachment directory escapes workspace: ${dirPath}`);
32886
33516
  }
@@ -32888,7 +33518,7 @@ async function localDirectoryStatInsideRoot(dirPath, rootDir) {
32888
33518
  }
32889
33519
  async function openLocalFileInsideRoot(filePath, rootDir) {
32890
33520
  const checkedStat = await localFileStatInsideRoot(filePath, rootDir);
32891
- const file = await fs4.open(filePath, constants.O_RDONLY | constants.O_NOFOLLOW);
33521
+ const file = await fs5.open(filePath, constants.O_RDONLY | constants.O_NOFOLLOW);
32892
33522
  let keepOpen = false;
32893
33523
  try {
32894
33524
  const openedStat = await file.stat();
@@ -32904,8 +33534,8 @@ async function openLocalFileInsideRoot(filePath, rootDir) {
32904
33534
  }
32905
33535
  }
32906
33536
  async function openLocalTempFileInsideRoot(filePath, rootDir) {
32907
- await localDirectoryStatInsideRoot(path5.dirname(filePath), rootDir);
32908
- const file = await fs4.open(filePath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
33537
+ await localDirectoryStatInsideRoot(path6.dirname(filePath), rootDir);
33538
+ const file = await fs5.open(filePath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
32909
33539
  let keepOpen = false;
32910
33540
  try {
32911
33541
  const checkedStat = await localFileStatInsideRoot(filePath, rootDir);
@@ -32930,7 +33560,7 @@ async function assertLocalFileIdentity(filePath, rootDir, expected) {
32930
33560
  async function removeLocalFileIfInside(filePath, rootDir) {
32931
33561
  try {
32932
33562
  await localFileStatInsideRoot(filePath, rootDir);
32933
- await fs4.rm(filePath, { force: true });
33563
+ await fs5.rm(filePath, { force: true });
32934
33564
  } catch {
32935
33565
  }
32936
33566
  }
@@ -32943,7 +33573,7 @@ function sameFile(a, b) {
32943
33573
  async function cleanupOldAttachmentFiles(rootDir, ttlMs, log2, preserveDirs) {
32944
33574
  let entries;
32945
33575
  try {
32946
- entries = await fs4.readdir(rootDir, { withFileTypes: true });
33576
+ entries = await fs5.readdir(rootDir, { withFileTypes: true });
32947
33577
  } catch {
32948
33578
  return;
32949
33579
  }
@@ -32951,15 +33581,15 @@ async function cleanupOldAttachmentFiles(rootDir, ttlMs, log2, preserveDirs) {
32951
33581
  await Promise.all(entries.map(async (entry) => {
32952
33582
  if (!entry.isDirectory())
32953
33583
  return;
32954
- const fullPath = path5.join(rootDir, entry.name);
33584
+ const fullPath = path6.join(rootDir, entry.name);
32955
33585
  try {
32956
- if (preserveDirs?.has(path5.resolve(fullPath)))
33586
+ if (preserveDirs?.has(path6.resolve(fullPath)))
32957
33587
  return;
32958
- const stat = await fs4.lstat(fullPath);
33588
+ const stat = await fs5.lstat(fullPath);
32959
33589
  if (!stat.isDirectory())
32960
33590
  return;
32961
33591
  if (stat.mtimeMs < cutoff) {
32962
- await fs4.rm(fullPath, { recursive: true, force: true });
33592
+ await fs5.rm(fullPath, { recursive: true, force: true });
32963
33593
  }
32964
33594
  } catch (err) {
32965
33595
  log2?.warn?.(`agent-core: failed to clean attachment temp dir ${fullPath}: ${String(err)}`);
@@ -32971,7 +33601,7 @@ async function pruneAttachmentCache(rootDir, maxBytes, log2, preserveDirs) {
32971
33601
  return;
32972
33602
  let entries;
32973
33603
  try {
32974
- entries = await fs4.readdir(rootDir, { withFileTypes: true });
33604
+ entries = await fs5.readdir(rootDir, { withFileTypes: true });
32975
33605
  } catch {
32976
33606
  return;
32977
33607
  }
@@ -32980,9 +33610,9 @@ async function pruneAttachmentCache(rootDir, maxBytes, log2, preserveDirs) {
32980
33610
  for (const entry of entries) {
32981
33611
  if (!entry.isDirectory())
32982
33612
  continue;
32983
- const fullPath = path5.join(rootDir, entry.name);
33613
+ const fullPath = path6.join(rootDir, entry.name);
32984
33614
  try {
32985
- const stat = await fs4.lstat(fullPath);
33615
+ const stat = await fs5.lstat(fullPath);
32986
33616
  if (!stat.isDirectory())
32987
33617
  continue;
32988
33618
  const size = await directorySize(fullPath);
@@ -32998,10 +33628,10 @@ async function pruneAttachmentCache(rootDir, maxBytes, log2, preserveDirs) {
32998
33628
  for (const dir of dirs) {
32999
33629
  if (total <= maxBytes)
33000
33630
  break;
33001
- if (preserveDirs?.has(path5.resolve(dir.path)))
33631
+ if (preserveDirs?.has(path6.resolve(dir.path)))
33002
33632
  continue;
33003
33633
  try {
33004
- await fs4.rm(dir.path, { recursive: true, force: true });
33634
+ await fs5.rm(dir.path, { recursive: true, force: true });
33005
33635
  total -= dir.size;
33006
33636
  } catch (err) {
33007
33637
  log2?.warn?.(`agent-core: failed to prune attachment cache dir ${dir.path}: ${String(err)}`);
@@ -33010,12 +33640,12 @@ async function pruneAttachmentCache(rootDir, maxBytes, log2, preserveDirs) {
33010
33640
  }
33011
33641
  async function directorySize(dirPath) {
33012
33642
  let total = 0;
33013
- const entries = await fs4.readdir(dirPath, { withFileTypes: true });
33643
+ const entries = await fs5.readdir(dirPath, { withFileTypes: true });
33014
33644
  for (const entry of entries) {
33015
- const fullPath = path5.join(dirPath, entry.name);
33645
+ const fullPath = path6.join(dirPath, entry.name);
33016
33646
  let stat;
33017
33647
  try {
33018
- stat = await fs4.lstat(fullPath);
33648
+ stat = await fs5.lstat(fullPath);
33019
33649
  } catch {
33020
33650
  continue;
33021
33651
  }
@@ -33030,10 +33660,10 @@ async function directorySize(dirPath) {
33030
33660
  return total;
33031
33661
  }
33032
33662
  function activeDirsForRoot(rootDir) {
33033
- const root = path5.resolve(rootDir);
33663
+ const root = path6.resolve(rootDir);
33034
33664
  const dirs = /* @__PURE__ */ new Set();
33035
33665
  for (const dir of activeAttachmentDirs) {
33036
- if (dir === root || dir.startsWith(`${root}${path5.sep}`)) {
33666
+ if (dir === root || dir.startsWith(`${root}${path6.sep}`)) {
33037
33667
  dirs.add(dir);
33038
33668
  }
33039
33669
  }
@@ -33130,9 +33760,9 @@ async function writeResponseToFileWithLimit(res, filePath, maxBytes, rootDir) {
33130
33760
  }
33131
33761
  writtenStat = await file.stat();
33132
33762
  await closeFile();
33133
- await localDirectoryStatInsideRoot(path5.dirname(filePath), rootDir);
33763
+ await localDirectoryStatInsideRoot(path6.dirname(filePath), rootDir);
33134
33764
  await assertLocalFileIdentity(tmpPath, rootDir, writtenStat);
33135
- await fs4.rename(tmpPath, filePath);
33765
+ await fs5.rename(tmpPath, filePath);
33136
33766
  completed = true;
33137
33767
  return written;
33138
33768
  } finally {
@@ -33148,9 +33778,9 @@ async function writeResponseToFileWithLimit(res, filePath, maxBytes, rootDir) {
33148
33778
  }
33149
33779
  }
33150
33780
  function localFileName(attachmentId, fileName, mimeType) {
33151
- const safeName = sanitizePathSegment(path5.basename(fileName || attachmentId));
33152
- const ext = path5.extname(safeName) || extensionForMime(mimeType);
33153
- const stem = path5.basename(safeName, path5.extname(safeName)) || attachmentId;
33781
+ const safeName = sanitizePathSegment(path6.basename(fileName || attachmentId));
33782
+ const ext = path6.extname(safeName) || extensionForMime(mimeType);
33783
+ const stem = path6.basename(safeName, path6.extname(safeName)) || attachmentId;
33154
33784
  return `${sanitizePathSegment(attachmentId)}-${stem}${ext}`;
33155
33785
  }
33156
33786
  function extensionForMime(mimeType) {
@@ -33933,6 +34563,9 @@ var CodexAppServerAdapter = class {
33933
34563
  if (this.opts.contextFilePath) {
33934
34564
  env.PRLL_CONTEXT_FILE = this.opts.contextFilePath;
33935
34565
  }
34566
+ if (this.opts.contextDirPath) {
34567
+ env.PRLL_CONTEXT_DIR = this.opts.contextDirPath;
34568
+ }
33936
34569
  if (!env.PRLL_WIKI_MOUNT_ROOT?.trim()) {
33937
34570
  env.PRLL_WIKI_MOUNT_ROOT = this.opts.workspaceDir;
33938
34571
  }
@@ -34078,7 +34711,7 @@ var TurnSink = class {
34078
34711
  }
34079
34712
  };
34080
34713
  function ensureGitRepo(workingDirectory) {
34081
- fs5.mkdirSync(workingDirectory, { recursive: true });
34714
+ fs6.mkdirSync(workingDirectory, { recursive: true });
34082
34715
  try {
34083
34716
  execSync2("git rev-parse --is-inside-work-tree", { cwd: workingDirectory, stdio: "pipe" });
34084
34717
  ensureLocalAttachmentGitExclude(workingDirectory);
@@ -34148,8 +34781,8 @@ function errToString(err) {
34148
34781
  }
34149
34782
 
34150
34783
  // ts/codex-agent/dist/session-manager.js
34151
- import * as fs6 from "node:fs";
34152
- import * as path6 from "node:path";
34784
+ import * as fs7 from "node:fs";
34785
+ import * as path7 from "node:path";
34153
34786
  var CodexSessionManager = class {
34154
34787
  mainSessionKey;
34155
34788
  stateFilePath;
@@ -34186,14 +34819,14 @@ var CodexSessionManager = class {
34186
34819
  clearMainThread() {
34187
34820
  this.threadIds.delete(this.mainSessionKey);
34188
34821
  try {
34189
- fs6.rmSync(this.stateFilePath, { force: true });
34822
+ fs7.rmSync(this.stateFilePath, { force: true });
34190
34823
  } catch (error) {
34191
34824
  this.logger?.warn(`codex-agent: failed to clear main thread state at ${this.stateFilePath}: ${String(error)}`);
34192
34825
  }
34193
34826
  }
34194
34827
  restore() {
34195
34828
  try {
34196
- const raw = fs6.readFileSync(this.stateFilePath, "utf8");
34829
+ const raw = fs7.readFileSync(this.stateFilePath, "utf8");
34197
34830
  const parsed = JSON.parse(raw);
34198
34831
  if (parsed.runtimeKey === this.mainSessionKey && typeof parsed.threadId === "string" && parsed.threadId.trim()) {
34199
34832
  this.threadIds.set(this.mainSessionKey, parsed.threadId.trim());
@@ -34206,10 +34839,10 @@ var CodexSessionManager = class {
34206
34839
  }
34207
34840
  persist(threadId) {
34208
34841
  try {
34209
- fs6.mkdirSync(path6.dirname(this.stateFilePath), { recursive: true });
34842
+ fs7.mkdirSync(path7.dirname(this.stateFilePath), { recursive: true });
34210
34843
  const tmpPath = `${this.stateFilePath}.tmp`;
34211
- fs6.writeFileSync(tmpPath, JSON.stringify({ runtimeKey: this.mainSessionKey, threadId }, null, 2));
34212
- fs6.renameSync(tmpPath, this.stateFilePath);
34844
+ fs7.writeFileSync(tmpPath, JSON.stringify({ runtimeKey: this.mainSessionKey, threadId }, null, 2));
34845
+ fs7.renameSync(tmpPath, this.stateFilePath);
34213
34846
  } catch (error) {
34214
34847
  this.logger?.warn(`codex-agent: failed to persist thread state at ${this.stateFilePath}: ${String(error)}`);
34215
34848
  }
@@ -34217,8 +34850,8 @@ var CodexSessionManager = class {
34217
34850
  };
34218
34851
 
34219
34852
  // ts/codex-agent/dist/workspace.js
34220
- import * as fs7 from "node:fs";
34221
- import * as path7 from "node:path";
34853
+ import * as fs8 from "node:fs";
34854
+ import * as path8 from "node:path";
34222
34855
 
34223
34856
  // ts/node_modules/.pnpm/smol-toml@1.6.1/node_modules/smol-toml/dist/error.js
34224
34857
  function getLineColFromPtr(string, ptr) {
@@ -34912,12 +35545,12 @@ function parse(toml, { maxDepth = 1e3, integersAsBigInt } = {}) {
34912
35545
 
34913
35546
  // ts/codex-agent/dist/workspace.js
34914
35547
  function ensureWorkspaceTrusted(codexHome, workspaceDir, log2) {
34915
- const configPath = path7.join(codexHome, "config.toml");
34916
- const normalizedPath = path7.resolve(workspaceDir);
35548
+ const configPath = path8.join(codexHome, "config.toml");
35549
+ const normalizedPath = path8.resolve(workspaceDir);
34917
35550
  try {
34918
35551
  let content = "";
34919
35552
  try {
34920
- content = fs7.readFileSync(configPath, "utf8");
35553
+ content = fs8.readFileSync(configPath, "utf8");
34921
35554
  } catch (err) {
34922
35555
  if (err.code !== "ENOENT")
34923
35556
  throw err;
@@ -34957,14 +35590,14 @@ ${trustLine}
34957
35590
  content = content.substring(0, afterHeader) + trustLine + "\n" + content.substring(afterHeader);
34958
35591
  }
34959
35592
  }
34960
- fs7.writeFileSync(configPath, content, "utf8");
35593
+ fs8.writeFileSync(configPath, content, "utf8");
34961
35594
  } else if (projects?.[normalizedPath] !== void 0) {
34962
35595
  log2?.warn(`Codex config.toml has non-canonical header for ${normalizedPath}; skipping trust write`);
34963
35596
  } else if (projects && !content.includes("[projects.")) {
34964
35597
  log2?.warn(`Codex config.toml uses inline table for projects; skipping trust write for ${normalizedPath}`);
34965
35598
  } else {
34966
- fs7.mkdirSync(codexHome, { recursive: true });
34967
- fs7.appendFileSync(configPath, `
35599
+ fs8.mkdirSync(codexHome, { recursive: true });
35600
+ fs8.appendFileSync(configPath, `
34968
35601
  ${sectionHeader}
34969
35602
  ${trustLine}
34970
35603
  `, "utf8");
@@ -34981,12 +35614,12 @@ function isParallProxyMode(env = process.env) {
34981
35614
  return baseUrl === apiUrl || baseUrl.startsWith(`${apiUrl}/`);
34982
35615
  }
34983
35616
  function ensureParallProvider(codexHome, apiUrl, log2) {
34984
- const configPath = path7.join(codexHome, "config.toml");
35617
+ const configPath = path8.join(codexHome, "config.toml");
34985
35618
  const baseUrl = apiUrl.replace(/\/$/, "") + "/api/llm/v1";
34986
35619
  try {
34987
35620
  let content = "";
34988
35621
  try {
34989
- content = fs7.readFileSync(configPath, "utf8");
35622
+ content = fs8.readFileSync(configPath, "utf8");
34990
35623
  } catch (err) {
34991
35624
  if (err.code !== "ENOENT")
34992
35625
  throw err;
@@ -35017,8 +35650,8 @@ function ensureParallProvider(codexHome, apiUrl, log2) {
35017
35650
  } else {
35018
35651
  content = content.trimEnd() + "\n\n" + providerBlock + "\n";
35019
35652
  }
35020
- fs7.mkdirSync(codexHome, { recursive: true });
35021
- fs7.writeFileSync(configPath, content, "utf8");
35653
+ fs8.mkdirSync(codexHome, { recursive: true });
35654
+ fs8.writeFileSync(configPath, content, "utf8");
35022
35655
  } catch (err) {
35023
35656
  throw new Error(`failed to write Parall provider config: ${String(err)}`);
35024
35657
  }
@@ -35035,18 +35668,18 @@ function ensureCodexWorkspace(workspaceDir, log2, agentIdentity) {
35035
35668
  PRLL_REFERENCE_GUIDE,
35036
35669
  buildSkillReferences(workspaceDir)
35037
35670
  ].join("\n\n");
35038
- fs7.mkdirSync(workspaceDir, { recursive: true });
35039
- const parallDir = path7.join(workspaceDir, ".parall");
35040
- fs7.mkdirSync(parallDir, { recursive: true });
35041
- fs7.writeFileSync(path7.join(parallDir, "system-prompt.md"), systemPrompt, "utf8");
35042
- writeSkillFiles(path7.join(parallDir, "skills"));
35043
- const codexConfigDir = path7.join(workspaceDir, ".codex");
35044
- fs7.mkdirSync(codexConfigDir, { recursive: true });
35671
+ fs8.mkdirSync(workspaceDir, { recursive: true });
35672
+ const parallDir = path8.join(workspaceDir, ".parall");
35673
+ fs8.mkdirSync(parallDir, { recursive: true });
35674
+ fs8.writeFileSync(path8.join(parallDir, "system-prompt.md"), systemPrompt, "utf8");
35675
+ writeSkillFiles(path8.join(parallDir, "skills"));
35676
+ const codexConfigDir = path8.join(workspaceDir, ".codex");
35677
+ fs8.mkdirSync(codexConfigDir, { recursive: true });
35045
35678
  const toml = `developer_instructions = """
35046
35679
  ${systemPrompt.replace(/\\/g, "\\\\").replace(/"""/g, '\\"""')}
35047
35680
  """
35048
35681
  `;
35049
- fs7.writeFileSync(path7.join(codexConfigDir, "config.toml"), toml, "utf8");
35682
+ fs8.writeFileSync(path8.join(codexConfigDir, "config.toml"), toml, "utf8");
35050
35683
  }
35051
35684
 
35052
35685
  // ts/codex-agent/dist/index.js
@@ -35139,6 +35772,7 @@ async function main() {
35139
35772
  sessionManager,
35140
35773
  log: agentLog,
35141
35774
  contextFilePath: mainContextFilePath,
35775
+ contextDirPath: dispatchContextDirPath(config.stateDir),
35142
35776
  useParallProvider
35143
35777
  });
35144
35778
  const refreshPlatformConfig = async () => {
@@ -35184,6 +35818,7 @@ async function main() {
35184
35818
  dispatchDeadlineMs: parseDispatchDeadlineMs(process.env.PRLL_DISPATCH_DEADLINE_MS),
35185
35819
  contextFilePathForSession: (sessionKey) => contextFilePathForSession(config.stateDir, sessionKey),
35186
35820
  stepIdFilePathForSession: (sessionKey) => stepIdFilePathForSession(config.stateDir, sessionKey),
35821
+ dispatchContextDir: dispatchContextDirPath(config.stateDir),
35187
35822
  onConfigUpdate: refreshPlatformConfig,
35188
35823
  onSessionReady: refreshPlatformConfig,
35189
35824
  onNewSession: () => {