@parall/daemon 1.37.0 → 1.39.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`,
@@ -27439,7 +27457,13 @@ var ENDPOINTS = {
27439
27457
  // the machine control-plane request/reply bridge that lives in api-server.
27440
27458
  BROWSER_PROFILE_VIEWER_COMMAND: (orgId, profileId) => `${API_BASE}/orgs/${orgId}/browser-profiles/${profileId}/viewer/command`,
27441
27459
  // Clip registry (global, served by clip-service → Pinix Hub proxy)
27442
- CLIP_REGISTRY: () => `${CLIP_BASE}/registry/clips`
27460
+ CLIP_REGISTRY: () => `${CLIP_BASE}/registry/clips`,
27461
+ // Edge device endpoints
27462
+ ORG_EDGE_DEVICES: (orgId) => `/api/v1/orgs/${orgId}/edge/devices`,
27463
+ ORG_EDGE_ONBOARDING: (orgId) => `/api/v1/orgs/${orgId}/edge/onboarding`,
27464
+ ORG_EDGE_PROFILES: (orgId, edgeId) => `/api/v1/orgs/${orgId}/edge/${edgeId}/profiles`,
27465
+ CLIP_CONNECTIONS: (orgId, clipId) => `/api/v1/orgs/${orgId}/clip-registry/${clipId}/connections`,
27466
+ CLIP_CONNECTION: (orgId, connId) => `/api/v1/orgs/${orgId}/clip-connections/${connId}`
27443
27467
  };
27444
27468
  var WS_EVENTS = {
27445
27469
  // Client -> Server
@@ -27498,6 +27522,7 @@ var WS_EVENTS = {
27498
27522
  READ_POSITION_UPDATED: "read_position.updated",
27499
27523
  DISPATCH_NEW: "dispatch.new",
27500
27524
  DISPATCH_RECEIVED: "dispatch.received",
27525
+ DISPATCH_RESOLVED: "dispatch.resolved",
27501
27526
  SCHEDULE_CREATED: "schedule.created",
27502
27527
  SCHEDULE_UPDATED: "schedule.updated",
27503
27528
  SCHEDULE_DELETED: "schedule.deleted",
@@ -27593,8 +27618,8 @@ var ParallClient = class _ParallClient {
27593
27618
  * is authoritative, so wiki vs api routing can't drift from how a caller
27594
27619
  * happens to invoke the client.
27595
27620
  */
27596
- baseUrlFor(path8) {
27597
- return path8.startsWith(WIKI_BASE) ? this.wikiBaseUrl : this.baseUrl;
27621
+ baseUrlFor(path9) {
27622
+ return path9.startsWith(WIKI_BASE) ? this.wikiBaseUrl : this.baseUrl;
27598
27623
  }
27599
27624
  setToken(token) {
27600
27625
  this.token = token;
@@ -27621,10 +27646,10 @@ var ParallClient = class _ParallClient {
27621
27646
  * REFRESH_THRESHOLD_S, refresh it **before** sending the request.
27622
27647
  * No-op when the token is still fresh, missing, or un-parseable.
27623
27648
  */
27624
- async ensureFreshToken(path8) {
27649
+ async ensureFreshToken(path9) {
27625
27650
  if (!this.token || !this.getRefreshToken)
27626
27651
  return;
27627
- const pathSuffix = path8.replace(/^\/api\/v1/, "");
27652
+ const pathSuffix = path9.replace(/^\/api\/v1/, "");
27628
27653
  if (_ParallClient.AUTH_PATHS.has(pathSuffix))
27629
27654
  return;
27630
27655
  const exp = _ParallClient.decodeJwtExp(this.token);
@@ -27656,11 +27681,11 @@ var ParallClient = class _ParallClient {
27656
27681
  this.refreshPromise = null;
27657
27682
  }
27658
27683
  }
27659
- async request(method, path8, body, query, retried = false, opts) {
27684
+ async request(method, path9, body, query, retried = false, opts) {
27660
27685
  if (!retried) {
27661
- await this.ensureFreshToken(path8);
27686
+ await this.ensureFreshToken(path9);
27662
27687
  }
27663
- let url = `${this.baseUrlFor(path8)}${path8}`;
27688
+ let url = `${this.baseUrlFor(path9)}${path9}`;
27664
27689
  if (query) {
27665
27690
  const params = new URLSearchParams();
27666
27691
  for (const [key, value] of Object.entries(query)) {
@@ -27690,12 +27715,12 @@ var ParallClient = class _ParallClient {
27690
27715
  throw _ParallClient.normalizeFetchError(err);
27691
27716
  }
27692
27717
  if (res.status === 401) {
27693
- const pathSuffix = path8.replace(/^\/api\/v1/, "");
27718
+ const pathSuffix = path9.replace(/^\/api\/v1/, "");
27694
27719
  const isAuthPath = _ParallClient.AUTH_PATHS.has(pathSuffix);
27695
27720
  if (!retried && !isAuthPath && this.getRefreshToken) {
27696
27721
  const refreshed = await this.tryRefresh();
27697
27722
  if (refreshed) {
27698
- return this.request(method, path8, body, query, true, opts);
27723
+ return this.request(method, path9, body, query, true, opts);
27699
27724
  }
27700
27725
  }
27701
27726
  if (this.onTokenExpired && !isAuthPath) {
@@ -27706,6 +27731,7 @@ var ParallClient = class _ParallClient {
27706
27731
  const rawErrorBody = await res.json().catch(() => ({}));
27707
27732
  throw buildApiError(res, rawErrorBody);
27708
27733
  }
27734
+ opts?.onStatus?.(res.status);
27709
27735
  if (res.status === 204)
27710
27736
  return void 0;
27711
27737
  if (res.status === 202) {
@@ -27724,15 +27750,15 @@ var ParallClient = class _ParallClient {
27724
27750
  * hit the 100 MiB cap, so a longer 5-minute timeout is used so a
27725
27751
  * 50 MiB blob on a slow connection doesn't get chopped at 15 s.
27726
27752
  */
27727
- async multipartRequest(method, path8, body, retried = false) {
27753
+ async multipartRequest(method, path9, body, retried = false) {
27728
27754
  if (!retried) {
27729
- await this.ensureFreshToken(path8);
27755
+ await this.ensureFreshToken(path9);
27730
27756
  }
27731
27757
  const { "Content-Type": _drop, ...headers } = this.buildHeaders();
27732
27758
  void _drop;
27733
27759
  let res;
27734
27760
  try {
27735
- res = await fetch(`${this.baseUrlFor(path8)}${path8}`, {
27761
+ res = await fetch(`${this.baseUrlFor(path9)}${path9}`, {
27736
27762
  method,
27737
27763
  headers,
27738
27764
  body,
@@ -27742,12 +27768,12 @@ var ParallClient = class _ParallClient {
27742
27768
  throw _ParallClient.normalizeFetchError(err);
27743
27769
  }
27744
27770
  if (res.status === 401) {
27745
- const pathSuffix = path8.replace(/^\/api\/v1/, "");
27771
+ const pathSuffix = path9.replace(/^\/api\/v1/, "");
27746
27772
  const isAuthPath = _ParallClient.AUTH_PATHS.has(pathSuffix);
27747
27773
  if (!retried && !isAuthPath && this.getRefreshToken) {
27748
27774
  const refreshed = await this.tryRefresh();
27749
27775
  if (refreshed) {
27750
- return this.multipartRequest(method, path8, body, true);
27776
+ return this.multipartRequest(method, path9, body, true);
27751
27777
  }
27752
27778
  }
27753
27779
  if (this.onTokenExpired && !isAuthPath) {
@@ -28039,6 +28065,21 @@ var ParallClient = class _ParallClient {
28039
28065
  async sendMessage(orgId, chatId, req) {
28040
28066
  return this.request("POST", ENDPOINTS.CHAT_MESSAGES(orgId, chatId), req);
28041
28067
  }
28068
+ /**
28069
+ * sendMessage variant that also reports whether the server answered with an
28070
+ * idempotent replay (HTTP 200 — the message already existed for this
28071
+ * idempotency/effect key) instead of a fresh create (201). Used by the CLI
28072
+ * to surface "already sent by a previous run (deduplicated)".
28073
+ */
28074
+ async sendMessageDetailed(orgId, chatId, req) {
28075
+ let status = 0;
28076
+ const message = await this.request("POST", ENDPOINTS.CHAT_MESSAGES(orgId, chatId), req, void 0, false, {
28077
+ onStatus: (s) => {
28078
+ status = s;
28079
+ }
28080
+ });
28081
+ return { message, deduplicated: status === 200 };
28082
+ }
28042
28083
  async getMessages(orgId, chatId, params) {
28043
28084
  return this.request("GET", ENDPOINTS.CHAT_MESSAGES(orgId, chatId), void 0, params);
28044
28085
  }
@@ -28426,8 +28467,8 @@ var ParallClient = class _ParallClient {
28426
28467
  async requestMachineUpdate(orgId, machineId, mandatory = false) {
28427
28468
  await this.request("POST", ENDPOINTS.MACHINE_REQUEST_UPDATE(orgId, machineId), { mandatory });
28428
28469
  }
28429
- async browseMachineFilesystem(orgId, machineId, path8) {
28430
- return this.request("POST", ENDPOINTS.MACHINE_BROWSE(orgId, machineId), { path: path8 }, void 0, false, { timeoutMs: 15e3 });
28470
+ async browseMachineFilesystem(orgId, machineId, path9) {
28471
+ return this.request("POST", ENDPOINTS.MACHINE_BROWSE(orgId, machineId), { path: path9 }, void 0, false, { timeoutMs: 15e3 });
28431
28472
  }
28432
28473
  /** Create a new machine key. Returns the raw key string (shown once) + metadata. */
28433
28474
  async createMachineKey(orgId, machineId, name) {
@@ -28500,6 +28541,30 @@ var ParallClient = class _ParallClient {
28500
28541
  const params = maxAgeHours !== void 0 ? { max_age_hours: String(maxAgeHours) } : void 0;
28501
28542
  return this.request("POST", ENDPOINTS.DISPATCH_EXPIRE(orgId), void 0, params);
28502
28543
  }
28544
+ /**
28545
+ * Claim a dispatch lane (explicit consume endpoint). Occupies the
28546
+ * (agent, target, thread) lane and folds claimable WorkItems into it;
28547
+ * `claimed: false` means a healthy incumbent holds the lane.
28548
+ */
28549
+ async claimDispatch(orgId, req) {
28550
+ return this.request("POST", ENDPOINTS.DISPATCH_CLAIM(orgId), req);
28551
+ }
28552
+ /** Fold a pending same-target WorkItem into a live lane (409 STALE_LANE when dethroned). */
28553
+ async steerDispatch(orgId, req) {
28554
+ return this.request("POST", ENDPOINTS.DISPATCH_STEER(orgId), req);
28555
+ }
28556
+ /** End a turn: no_action sweep of the lane's members + lane release + re-drive check. */
28557
+ async completeDispatch(orgId, req) {
28558
+ return this.request("POST", ENDPOINTS.DISPATCH_COMPLETE(orgId), req);
28559
+ }
28560
+ /** Release a lane on graceful shutdown — members return to pending immediately. */
28561
+ async releaseDispatchLane(orgId, lane) {
28562
+ return this.request("POST", ENDPOINTS.DISPATCH_RELEASE(orgId), { lane });
28563
+ }
28564
+ /** Renew a live lane's lease (long-turn keepalive). 409 STALE_LANE when dethroned. */
28565
+ async heartbeatDispatchLane(orgId, req) {
28566
+ return this.request("POST", ENDPOINTS.DISPATCH_HEARTBEAT(orgId), req);
28567
+ }
28503
28568
  async getDispatchByMessages(orgId, chatId, messageIds) {
28504
28569
  const params = { chat_id: chatId, message_ids: messageIds.join(",") };
28505
28570
  return this.request("GET", ENDPOINTS.DISPATCH_BY_MESSAGES(orgId), void 0, params);
@@ -28964,8 +29029,8 @@ var ParallClient = class _ParallClient {
28964
29029
  async deleteWikiRestriction(orgId, wikiId, restrictionId) {
28965
29030
  await this.request("DELETE", ENDPOINTS.WIKI_RESTRICTION(orgId, wikiId, restrictionId));
28966
29031
  }
28967
- async getWikiAccessStatus(orgId, wikiId, path8) {
28968
- return this.request("GET", ENDPOINTS.WIKI_ACCESS_STATUS(orgId, wikiId), void 0, path8 ? { path: path8 } : void 0);
29032
+ async getWikiAccessStatus(orgId, wikiId, path9) {
29033
+ return this.request("GET", ENDPOINTS.WIKI_ACCESS_STATUS(orgId, wikiId), void 0, path9 ? { path: path9 } : void 0);
28969
29034
  }
28970
29035
  async createWikiAccessRequest(orgId, wikiId, data) {
28971
29036
  await this.request("POST", ENDPOINTS.WIKI_ACCESS_REQUESTS(orgId, wikiId), data);
@@ -28974,14 +29039,14 @@ var ParallClient = class _ParallClient {
28974
29039
  async getWikiCommits(orgId, wikiId, params) {
28975
29040
  return this.request("GET", ENDPOINTS.WIKI_COMMITS(orgId, wikiId), void 0, params);
28976
29041
  }
28977
- async getWikiFileCommits(orgId, wikiId, path8, params) {
29042
+ async getWikiFileCommits(orgId, wikiId, path9, params) {
28978
29043
  return this.request("GET", ENDPOINTS.WIKI_FILE_COMMITS(orgId, wikiId), void 0, {
28979
- path: path8,
29044
+ path: path9,
28980
29045
  ...params
28981
29046
  });
28982
29047
  }
28983
- async getWikiBlame(orgId, wikiId, path8, ref) {
28984
- return this.request("GET", ENDPOINTS.WIKI_BLAME(orgId, wikiId), void 0, { path: path8, ref });
29048
+ async getWikiBlame(orgId, wikiId, path9, ref) {
29049
+ return this.request("GET", ENDPOINTS.WIKI_BLAME(orgId, wikiId), void 0, { path: path9, ref });
28985
29050
  }
28986
29051
  // ---- Wiki Operations (audit log) ----
28987
29052
  async getWikiOperations(orgId, wikiId, params) {
@@ -29227,6 +29292,26 @@ var ParallClient = class _ParallClient {
29227
29292
  const resp = await this.request("GET", url);
29228
29293
  return resp.data;
29229
29294
  }
29295
+ // ---- Edge devices ----
29296
+ async listEdgeDevices(orgId) {
29297
+ return this.request("GET", ENDPOINTS.ORG_EDGE_DEVICES(orgId));
29298
+ }
29299
+ async getEdgeOnboarding(orgId) {
29300
+ return this.request("GET", ENDPOINTS.ORG_EDGE_ONBOARDING(orgId));
29301
+ }
29302
+ async listEdgeProfiles(orgId, edgeId) {
29303
+ return this.request("GET", ENDPOINTS.ORG_EDGE_PROFILES(orgId, edgeId));
29304
+ }
29305
+ // ---- Clip connections ----
29306
+ async listClipConnections(orgId, clipId) {
29307
+ return this.request("GET", ENDPOINTS.CLIP_CONNECTIONS(orgId, clipId));
29308
+ }
29309
+ async createClipConnection(orgId, clipId, input) {
29310
+ return this.request("POST", ENDPOINTS.CLIP_CONNECTIONS(orgId, clipId), input);
29311
+ }
29312
+ async deleteClipConnection(orgId, connId) {
29313
+ return this.request("DELETE", ENDPOINTS.CLIP_CONNECTION(orgId, connId));
29314
+ }
29230
29315
  };
29231
29316
  function normalizeWikiChangeset(changeset) {
29232
29317
  return {
@@ -29613,6 +29698,404 @@ var ParallWs = class {
29613
29698
  }
29614
29699
  };
29615
29700
 
29701
+ // ts/agent-core/dist/lane-ledger.js
29702
+ import * as fs from "node:fs";
29703
+ var LedgerUnsupportedError = class extends Error {
29704
+ };
29705
+ function isStaleLane(err) {
29706
+ return err instanceof ApiError && err.status === 409 && err.code === "STALE_LANE";
29707
+ }
29708
+ function isEndpointMissing(err) {
29709
+ return err instanceof ApiError && err.status === 404 && !err.code;
29710
+ }
29711
+ var LaneLedger = class {
29712
+ opts;
29713
+ lanes = /* @__PURE__ */ new Map();
29714
+ constructor(opts) {
29715
+ this.opts = opts;
29716
+ }
29717
+ get contextDir() {
29718
+ return this.opts.contextDir;
29719
+ }
29720
+ /** Only chat message events ride the lane ledger; typed events stay on the legacy ack path. */
29721
+ handles(event) {
29722
+ return event.type === "message" && event.targetId.startsWith("cht_");
29723
+ }
29724
+ laneKeyFor(event) {
29725
+ if (event.type !== "message" && event.dispatchEventId) {
29726
+ return laneKeyForTarget(`dsp:${event.dispatchEventId}`);
29727
+ }
29728
+ return laneKeyForTarget(`prll://${event.targetId}`, event.threadRootId);
29729
+ }
29730
+ getForEvent(event) {
29731
+ return this.lanes.get(this.laneKeyFor(event));
29732
+ }
29733
+ laneContextPath(lane) {
29734
+ return laneContextFilePath(this.opts.contextDir, lane.targetUri, lane.threadRootId);
29735
+ }
29736
+ /**
29737
+ * Claim (or reuse) the lane for a group of same-lane message events and
29738
+ * fold every group member into it. Returns 'foreign' when a healthy
29739
+ * incumbent (another pod) holds the resource — the caller must not
29740
+ * dispatch; the events stay pending server-side and re-drive after the
29741
+ * incumbent completes.
29742
+ */
29743
+ async ensureLane(events) {
29744
+ const trigger = events[events.length - 1];
29745
+ const laneKey = this.laneKeyFor(trigger);
29746
+ let lane = this.lanes.get(laneKey);
29747
+ if (!lane) {
29748
+ const targetUri = `prll://${trigger.targetId}`;
29749
+ let res;
29750
+ try {
29751
+ res = await this.opts.client.claimDispatch(this.opts.orgId, {
29752
+ target_uri: targetUri,
29753
+ thread_root_id: trigger.threadRootId,
29754
+ limit: 100
29755
+ });
29756
+ } catch (err) {
29757
+ if (isEndpointMissing(err))
29758
+ throw new LedgerUnsupportedError("claim endpoint unavailable");
29759
+ throw err;
29760
+ }
29761
+ if (!res.claimed || !res.lane) {
29762
+ this.opts.log?.info(`lane for ${targetUri} held by a healthy incumbent \u2014 leaving events pending for re-drive`);
29763
+ return null;
29764
+ }
29765
+ const leaseUntilMs = Date.parse(res.lease_until ?? "");
29766
+ lane = {
29767
+ laneKey,
29768
+ lane: res.lane,
29769
+ targetUri,
29770
+ threadRootId: trigger.threadRootId,
29771
+ folded: /* @__PURE__ */ new Map(),
29772
+ ...Number.isNaN(leaseUntilMs) ? {} : { leaseUntilMs, leaseTtlMs: Math.max(leaseUntilMs - Date.now(), 6e4) }
29773
+ };
29774
+ for (const ev of res.events ?? []) {
29775
+ lane.folded.set(ev.source_id, ev.id);
29776
+ }
29777
+ this.lanes.set(laneKey, lane);
29778
+ }
29779
+ for (const ev of events) {
29780
+ if (lane.folded.has(ev.messageId))
29781
+ continue;
29782
+ try {
29783
+ const res = await this.opts.client.steerDispatch(this.opts.orgId, {
29784
+ lane: lane.lane,
29785
+ target_uri: lane.targetUri,
29786
+ thread_root_id: lane.threadRootId,
29787
+ ...ev.dispatchEventId ? { dispatch_event_id: ev.dispatchEventId } : { source_type: "message", source_id: ev.messageId }
29788
+ });
29789
+ lane.folded.set(ev.messageId, res.dispatch_event_id);
29790
+ } catch (err) {
29791
+ if (isStaleLane(err)) {
29792
+ this.lanes.delete(laneKey);
29793
+ return null;
29794
+ }
29795
+ this.opts.log?.warn(`steer fold failed for ${ev.messageId} \u2014 failing closed, releasing lane: ${String(err)}`);
29796
+ await this.release(laneKey);
29797
+ return null;
29798
+ }
29799
+ }
29800
+ return lane;
29801
+ }
29802
+ /**
29803
+ * Fold a live mid-turn message into its active lane BEFORE injecting it
29804
+ * into the running turn. Injection without a successful fold is forbidden —
29805
+ * an un-folded injected message would be re-driven after complete and the
29806
+ * model would handle it twice.
29807
+ */
29808
+ async steerLive(event) {
29809
+ const laneKey = this.laneKeyFor(event);
29810
+ const lane = this.lanes.get(laneKey);
29811
+ if (!lane)
29812
+ return false;
29813
+ if (lane.folded.has(event.messageId))
29814
+ return true;
29815
+ try {
29816
+ const res = await this.opts.client.steerDispatch(this.opts.orgId, {
29817
+ lane: lane.lane,
29818
+ target_uri: lane.targetUri,
29819
+ thread_root_id: lane.threadRootId,
29820
+ ...event.dispatchEventId ? { dispatch_event_id: event.dispatchEventId } : { source_type: "message", source_id: event.messageId }
29821
+ });
29822
+ lane.folded.set(event.messageId, res.dispatch_event_id);
29823
+ return true;
29824
+ } catch (err) {
29825
+ if (isStaleLane(err)) {
29826
+ this.lanes.delete(laneKey);
29827
+ } else {
29828
+ this.opts.log?.warn(`live steer failed for ${event.messageId}: ${String(err)}`);
29829
+ }
29830
+ return false;
29831
+ }
29832
+ }
29833
+ /**
29834
+ * Complete the lane when no local work remains for it: the server sweeps
29835
+ * still-leased members as no_action, releases the occupancy row, and
29836
+ * re-drives any same-target pending work. A STALE_LANE answer means a
29837
+ * takeover already owns the resource — local state is dropped either way.
29838
+ */
29839
+ async completeIfIdle(laneKey, hasMoreLocal) {
29840
+ const lane = this.lanes.get(laneKey);
29841
+ if (!lane || hasMoreLocal)
29842
+ return;
29843
+ this.lanes.delete(laneKey);
29844
+ this.removeLaneContext(lane);
29845
+ try {
29846
+ const res = await this.opts.client.completeDispatch(this.opts.orgId, {
29847
+ lane: lane.lane,
29848
+ target_uri: lane.targetUri,
29849
+ thread_root_id: lane.threadRootId
29850
+ });
29851
+ if (res.swept_no_action > 0 || res.redriven) {
29852
+ this.opts.log?.info(`lane complete for ${lane.targetUri}: swept ${res.swept_no_action} no_action, redriven=${res.redriven}`);
29853
+ }
29854
+ } catch (err) {
29855
+ if (isStaleLane(err)) {
29856
+ this.opts.log?.info(`lane complete skipped for ${lane.targetUri} \u2014 taken over`);
29857
+ return;
29858
+ }
29859
+ this.opts.log?.warn(`lane complete failed for ${lane.targetUri}: ${String(err)}`);
29860
+ }
29861
+ }
29862
+ /**
29863
+ * Long-turn keepalive: renew the lane's lease on runtime activity, throttled
29864
+ * so a chatty turn doesn't spam the server. Without this, a legitimately
29865
+ * long turn (> lane TTL) would be dethroned mid-flight and every subsequent
29866
+ * write misfired with STALE_LANE — the design doc's "long turns renew via
29867
+ * step writes". Fire-and-forget: a failed renewal is surfaced by the next
29868
+ * write's incumbency check anyway.
29869
+ */
29870
+ maybeRenew(lane) {
29871
+ const now = Date.now();
29872
+ const ttl = lane.leaseTtlMs ?? 10 * 6e4;
29873
+ const until = lane.leaseUntilMs ?? now;
29874
+ if (until - now > ttl / 2)
29875
+ return;
29876
+ lane.leaseUntilMs = now + ttl;
29877
+ void this.opts.client.heartbeatDispatchLane(this.opts.orgId, {
29878
+ lane: lane.lane,
29879
+ target_uri: lane.targetUri,
29880
+ thread_root_id: lane.threadRootId
29881
+ }).then((res) => {
29882
+ const until2 = Date.parse(res?.lease_until ?? "");
29883
+ if (!Number.isNaN(until2))
29884
+ lane.leaseUntilMs = until2;
29885
+ }).catch((err) => {
29886
+ if (isStaleLane(err)) {
29887
+ this.lanes.delete(lane.laneKey);
29888
+ this.opts.log?.warn(`lane ${lane.targetUri} was taken over during the turn`);
29889
+ return;
29890
+ }
29891
+ this.opts.log?.warn(`lane heartbeat failed for ${lane.targetUri}: ${String(err)}`);
29892
+ });
29893
+ }
29894
+ /**
29895
+ * Release a lane's unresolved members back to pending (dispatch error /
29896
+ * shutdown) so the next pod re-claims immediately instead of waiting out
29897
+ * the lease.
29898
+ */
29899
+ async release(laneKey) {
29900
+ const lane = this.lanes.get(laneKey);
29901
+ if (!lane)
29902
+ return;
29903
+ this.lanes.delete(laneKey);
29904
+ this.removeLaneContext(lane);
29905
+ try {
29906
+ await this.opts.client.releaseDispatchLane(this.opts.orgId, lane.lane);
29907
+ } catch (err) {
29908
+ this.opts.log?.warn(`lane release failed for ${lane.targetUri}: ${String(err)}`);
29909
+ }
29910
+ }
29911
+ async releaseAll() {
29912
+ const keys = [...this.lanes.keys()];
29913
+ for (const key of keys) {
29914
+ await this.release(key);
29915
+ }
29916
+ }
29917
+ /** True when any lane is currently active (used by shutdown logging). */
29918
+ get activeCount() {
29919
+ return this.lanes.size;
29920
+ }
29921
+ /**
29922
+ * Claim the single-member lane of one typed WorkItem (resource = dsp:<id>),
29923
+ * by WorkItem id or by source identity (the live task.assigned event has no
29924
+ * WorkItem id). Returns null when a healthy incumbent (another pod) holds
29925
+ * it or the WorkItem is already resolved — the caller must skip processing.
29926
+ */
29927
+ async claimTyped(ref) {
29928
+ let res;
29929
+ try {
29930
+ res = await this.opts.client.claimDispatch(this.opts.orgId, {
29931
+ dispatch_event_id: ref.dispatchEventId,
29932
+ source_type: ref.dispatchEventId ? void 0 : ref.sourceType,
29933
+ source_id: ref.dispatchEventId ? void 0 : ref.sourceId
29934
+ });
29935
+ } catch (err) {
29936
+ if (isEndpointMissing(err))
29937
+ throw new LedgerUnsupportedError("claim endpoint unavailable");
29938
+ throw err;
29939
+ }
29940
+ if (!res.claimed || !res.lane || !res.events?.length)
29941
+ return null;
29942
+ const workItem = res.events[0];
29943
+ const targetUri = `dsp:${workItem.id}`;
29944
+ const leaseUntilMs = Date.parse(res.lease_until ?? "");
29945
+ const lane = {
29946
+ laneKey: laneKeyForTarget(targetUri),
29947
+ lane: res.lane,
29948
+ targetUri,
29949
+ folded: /* @__PURE__ */ new Map([[workItem.source_id, workItem.id]]),
29950
+ typedDispatchEventId: workItem.id,
29951
+ ...Number.isNaN(leaseUntilMs) ? {} : { leaseUntilMs, leaseTtlMs: Math.max(leaseUntilMs - Date.now(), 6e4) }
29952
+ };
29953
+ this.lanes.set(lane.laneKey, lane);
29954
+ return lane;
29955
+ }
29956
+ /**
29957
+ * Remove the per-lane context file (and its CLI sidecar) when the lane
29958
+ * ends. A leftover file would make a later cross-context send to the same
29959
+ * target bind a dead lane token and misfire with STALE_LANE instead of
29960
+ * taking the plain non-ledger path.
29961
+ */
29962
+ removeLaneContext(lane) {
29963
+ const contextPath = this.laneContextPath(lane);
29964
+ for (const p of [contextPath, contextPath.replace(/\.json$/, ".reply-state.json")]) {
29965
+ try {
29966
+ fs.rmSync(p, { force: true });
29967
+ } catch {
29968
+ }
29969
+ }
29970
+ }
29971
+ };
29972
+
29973
+ // ts/agent-core/dist/gateway-lane-flow.js
29974
+ async function dispatchLaneGroup(host, opts) {
29975
+ const ledger = host.laneLedger;
29976
+ const event = opts.events[opts.events.length - 1];
29977
+ let lane;
29978
+ try {
29979
+ lane = await ledger.ensureLane(opts.events);
29980
+ } catch (err) {
29981
+ if (!(err instanceof LedgerUnsupportedError))
29982
+ throw err;
29983
+ host.disableLedger("claim endpoint missing");
29984
+ await host.emitDispatchReceived(event);
29985
+ const dispatched2 = await host.runDispatch(event, opts.sessionKey, opts.body, opts.earlier, opts.captureText);
29986
+ if (!dispatched2)
29987
+ return "shutdown";
29988
+ for (const ev of opts.events) {
29989
+ host.opts.client.ackDispatch(host.opts.config.org_id, {
29990
+ source_type: ev.ackSourceType ?? "message",
29991
+ source_id: ev.ackSourceId ?? ev.messageId
29992
+ }).catch(() => {
29993
+ });
29994
+ }
29995
+ return "dispatched";
29996
+ }
29997
+ if (!lane) {
29998
+ for (const ev of opts.events) {
29999
+ host.dispatchedMessages.delete(ev.messageId);
30000
+ }
30001
+ return "foreign";
30002
+ }
30003
+ let dispatched = false;
30004
+ try {
30005
+ dispatched = await host.runDispatch(event, opts.sessionKey, opts.body, opts.earlier, opts.captureText);
30006
+ } catch (err) {
30007
+ await ledger.release(lane.laneKey).catch(() => {
30008
+ });
30009
+ throw err;
30010
+ }
30011
+ if (!dispatched) {
30012
+ return "shutdown";
30013
+ }
30014
+ const pendingInjections = host.opts.dispatchAdapter.hasPendingInjections?.(opts.sessionKey) ?? false;
30015
+ await ledger.completeIfIdle(lane.laneKey, pendingInjections || opts.hasMoreLocal());
30016
+ return "dispatched";
30017
+ }
30018
+ async function consumeTypedDispatch(host, ref, run, ack) {
30019
+ if (!host.laneLedger || host.ledgerDisabled) {
30020
+ if (await run(ref.dispatchEventId))
30021
+ ack(ref.dispatchEventId);
30022
+ return;
30023
+ }
30024
+ let lane;
30025
+ try {
30026
+ lane = await host.laneLedger.claimTyped(ref);
30027
+ } catch (err) {
30028
+ if (err instanceof LedgerUnsupportedError) {
30029
+ host.disableLedger("claim endpoint missing");
30030
+ if (await run(ref.dispatchEventId))
30031
+ ack(ref.dispatchEventId);
30032
+ return;
30033
+ }
30034
+ throw err;
30035
+ }
30036
+ if (!lane) {
30037
+ host.opts.log?.info(`typed dispatch ${ref.dispatchEventId ?? `${ref.sourceType}:${ref.sourceId}`} not claimable (held elsewhere or already resolved) \u2014 skipping`);
30038
+ return;
30039
+ }
30040
+ try {
30041
+ if (await run(lane.typedDispatchEventId))
30042
+ ack(lane.typedDispatchEventId);
30043
+ } finally {
30044
+ await host.laneLedger.completeIfIdle(lane.laneKey, false).catch(() => {
30045
+ });
30046
+ }
30047
+ }
30048
+ async function consumeMessageWorkItem(host, item) {
30049
+ if (host.shuttingDown)
30050
+ return;
30051
+ if (!host.tryClaimMessage(item.source_id))
30052
+ return;
30053
+ const ackItem = () => {
30054
+ host.opts.client.ackDispatchByID(host.opts.config.org_id, item.id).catch(() => {
30055
+ });
30056
+ };
30057
+ let msg = null;
30058
+ try {
30059
+ msg = await host.opts.client.getMessage(item.source_id);
30060
+ } catch (err) {
30061
+ const status = err?.status;
30062
+ if (status !== 404) {
30063
+ host.opts.log?.warn(`message fetch failed for ${item.source_id}, leaving pending: ${String(err)}`);
30064
+ host.dispatchedMessages.delete(item.source_id);
30065
+ return;
30066
+ }
30067
+ }
30068
+ if (!msg || msg.sender_id === host.opts.agentUserId) {
30069
+ host.dispatchedMessages.delete(item.source_id);
30070
+ ackItem();
30071
+ return;
30072
+ }
30073
+ const decision = await host.buildMessageDispatchDecision(item.chat_id, msg);
30074
+ if (decision.action === "retry") {
30075
+ host.dispatchedMessages.delete(item.source_id);
30076
+ return;
30077
+ }
30078
+ if (decision.action === "skip") {
30079
+ host.dispatchedMessages.delete(item.source_id);
30080
+ ackItem();
30081
+ return;
30082
+ }
30083
+ decision.event.dispatchEventId = item.id;
30084
+ const laneResolved = host.usesLaneLedger(decision.event);
30085
+ try {
30086
+ const dispatched = await host.handleInboundEvent(decision.event);
30087
+ if (dispatched) {
30088
+ if (!laneResolved)
30089
+ ackItem();
30090
+ } else {
30091
+ host.dispatchedMessages.delete(item.source_id);
30092
+ }
30093
+ } catch (err) {
30094
+ host.dispatchedMessages.delete(item.source_id);
30095
+ throw err;
30096
+ }
30097
+ }
30098
+
29616
30099
  // ts/agent-core/dist/telemetry.js
29617
30100
  init_esm();
29618
30101
  var import_api_logs = __toESM(require_src(), 1);
@@ -29914,6 +30397,14 @@ var ParallAgentGateway = class {
29914
30397
  inFlightDispatches = 0;
29915
30398
  drainResolvers = [];
29916
30399
  pendingRestartNotification = null;
30400
+ laneLedger;
30401
+ // Sticky fallback: flipped when the server predates the ledger (claim
30402
+ // endpoint 404) so every subsequent dispatch uses the legacy flow.
30403
+ ledgerDisabled = false;
30404
+ // Group key of the group currently being dispatched on main — lane-aware
30405
+ // (targetId + thread), unlike mainCurrentTargetId which stays chat-level
30406
+ // for fork routing decisions.
30407
+ mainCurrentGroupKey;
29917
30408
  DISPATCHED_MESSAGES_CAP = 5e3;
29918
30409
  // SHUTDOWN_DEADLINE_MS is read by waitForDrain via the configured value
29919
30410
  // below — kept as instance state so per-runtime configs can override it
@@ -29923,6 +30414,14 @@ var ParallAgentGateway = class {
29923
30414
  DISPATCH_DEADLINE_MS;
29924
30415
  constructor(opts) {
29925
30416
  this.opts = opts;
30417
+ if (opts.dispatchContextDir) {
30418
+ this.laneLedger = new LaneLedger({
30419
+ client: opts.client,
30420
+ orgId: opts.config.org_id,
30421
+ contextDir: opts.dispatchContextDir,
30422
+ log: opts.log
30423
+ });
30424
+ }
29926
30425
  this.SHUTDOWN_DEADLINE_MS = opts.shutdownDeadlineMs ?? 6e4;
29927
30426
  this.FORK_DEADLINE_MS = opts.forkDeadlineMs ?? 2 * 60 * 6e4;
29928
30427
  this.DISPATCH_DEADLINE_MS = opts.dispatchDeadlineMs ?? 20 * 6e4;
@@ -29992,14 +30491,18 @@ var ParallAgentGateway = class {
29992
30491
  if (data.status !== "todo" && data.status !== "in_progress")
29993
30492
  return;
29994
30493
  try {
29995
- const dispatched = await this.handleTaskAssignment(data, data.id);
29996
- if (dispatched) {
30494
+ 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) => {
30495
+ if (dispatchEventId) {
30496
+ this.opts.client.ackDispatchByID(this.opts.config.org_id, dispatchEventId).catch(() => {
30497
+ });
30498
+ return;
30499
+ }
29997
30500
  this.opts.client.ackDispatch(this.opts.config.org_id, {
29998
30501
  source_type: "task_activity",
29999
30502
  source_id: data.id
30000
30503
  }).catch(() => {
30001
30504
  });
30002
- }
30505
+ });
30003
30506
  } catch (err) {
30004
30507
  this.opts.log?.error(`task dispatch failed for ${data.id}: ${String(err)}`);
30005
30508
  }
@@ -30009,11 +30512,10 @@ var ParallAgentGateway = class {
30009
30512
  if (!data.source_id || !data.task_id)
30010
30513
  return;
30011
30514
  try {
30012
- const dispatched = await this.handleTaskComment(data.source_id, data.task_id, data.actor_id, data.delivery_reason);
30013
- if (dispatched) {
30515
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.handleTaskComment(data.source_id, data.task_id ?? "", data.actor_id, data.delivery_reason), () => {
30014
30516
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
30015
30517
  });
30016
- }
30518
+ });
30017
30519
  } catch (err) {
30018
30520
  this.opts.log?.error(`task comment dispatch failed for ${data.source_id}: ${String(err)}`);
30019
30521
  }
@@ -30021,11 +30523,10 @@ var ParallAgentGateway = class {
30021
30523
  if (!data.source_id)
30022
30524
  return;
30023
30525
  try {
30024
- const dispatched = await this.handleWikiComment(data.source_id, data.actor_id, data.delivery_reason);
30025
- if (dispatched) {
30526
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.handleWikiComment(data.source_id, data.actor_id, data.delivery_reason), () => {
30026
30527
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
30027
30528
  });
30028
- }
30529
+ });
30029
30530
  } catch (err) {
30030
30531
  this.opts.log?.error(`wiki comment dispatch failed for ${data.source_id}: ${String(err)}`);
30031
30532
  }
@@ -30033,11 +30534,13 @@ var ParallAgentGateway = class {
30033
30534
  if (!data.task_id)
30034
30535
  return;
30035
30536
  try {
30036
- const dispatched = await this.handleTaskDispatch(data.task_id, data.source_id ?? data.task_id, { allowCreator: true });
30037
- if (dispatched) {
30537
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, (dispatchEventId) => this.handleTaskDispatch(data.task_id ?? "", data.source_id ?? data.task_id ?? "", {
30538
+ allowCreator: true,
30539
+ dispatchEventId
30540
+ }), () => {
30038
30541
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
30039
30542
  });
30040
- }
30543
+ });
30041
30544
  } catch (err) {
30042
30545
  this.opts.log?.error(`task update dispatch failed for ${data.task_id}: ${String(err)}`);
30043
30546
  }
@@ -30045,11 +30548,10 @@ var ParallAgentGateway = class {
30045
30548
  if (!data.source_id)
30046
30549
  return;
30047
30550
  try {
30048
- const dispatched = await this.fetchAndHandleScheduleFire(data.source_id, data.actor_id);
30049
- if (dispatched) {
30551
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.fetchAndHandleScheduleFire(data.source_id, data.actor_id), () => {
30050
30552
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
30051
30553
  });
30052
- }
30554
+ });
30053
30555
  } catch (err) {
30054
30556
  this.opts.log?.error(`schedule fire dispatch failed for ${data.source_id}: ${String(err)}`);
30055
30557
  }
@@ -30057,11 +30559,10 @@ var ParallAgentGateway = class {
30057
30559
  if (!data.source_id)
30058
30560
  return;
30059
30561
  try {
30060
- const dispatched = await this.fetchAndHandleExternalTriggerRun(data.source_id);
30061
- if (dispatched) {
30562
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.fetchAndHandleExternalTriggerRun(data.source_id), () => {
30062
30563
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
30063
30564
  });
30064
- }
30565
+ });
30065
30566
  } catch (err) {
30066
30567
  this.opts.log?.error(`external trigger dispatch failed for ${data.source_id}: ${String(err)}`);
30067
30568
  }
@@ -30069,11 +30570,10 @@ var ParallAgentGateway = class {
30069
30570
  if (!data.source_id)
30070
30571
  return;
30071
30572
  try {
30072
- const dispatched = await this.fetchAndHandleChannelMessage(data.source_id);
30073
- if (dispatched) {
30573
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.fetchAndHandleChannelMessage(data.source_id), () => {
30074
30574
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
30075
30575
  });
30076
- }
30576
+ });
30077
30577
  } catch (err) {
30078
30578
  this.opts.log?.error(`channel message dispatch failed for ${data.source_id}: ${String(err)}`);
30079
30579
  }
@@ -30081,14 +30581,19 @@ var ParallAgentGateway = class {
30081
30581
  if (!data.source_id)
30082
30582
  return;
30083
30583
  try {
30084
- const dispatched = await this.fetchAndHandleApprovalDecided(data.source_id, data.actor_id, data.chat_id ?? null);
30085
- if (dispatched) {
30584
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.fetchAndHandleApprovalDecided(data.source_id, data.actor_id, data.chat_id ?? null), () => {
30086
30585
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
30087
30586
  });
30088
- }
30587
+ });
30089
30588
  } catch (err) {
30090
30589
  this.opts.log?.error(`approval decided dispatch failed for ${data.source_id}: ${String(err)}`);
30091
30590
  }
30591
+ } else if (data.event_type === "message" && this.laneLedger && data.source_id && data.chat_id) {
30592
+ try {
30593
+ await this.handleMessageRedrive(data);
30594
+ } catch (err) {
30595
+ this.opts.log?.error(`message re-drive failed for ${data.source_id}: ${String(err)}`);
30596
+ }
30092
30597
  } else if (data.event_type !== "message" && data.event_type !== "task_assign") {
30093
30598
  this.opts.log?.info(`dispatch.new with unhandled event_type=${String(data.event_type)} (id=${data.id}) \u2014 no-op`);
30094
30599
  }
@@ -30124,6 +30629,39 @@ var ParallAgentGateway = class {
30124
30629
  source_id: sourceId
30125
30630
  });
30126
30631
  }
30632
+ /** True when this event's lifecycle is owned by the dispatch lane ledger. */
30633
+ usesLaneLedger(event) {
30634
+ return this.laneLedger != null && !this.ledgerDisabled && this.laneLedger.handles(event);
30635
+ }
30636
+ disableLedger(reason) {
30637
+ if (this.ledgerDisabled)
30638
+ return;
30639
+ this.ledgerDisabled = true;
30640
+ this.opts.log?.warn(`dispatch ledger unavailable (${reason}) \u2014 falling back to legacy received/ack flow`);
30641
+ }
30642
+ /**
30643
+ * Buffer grouping key. Lane-ledger message events group by full lane
30644
+ * identity (chat + thread) so a channel lane and a thread lane in the same
30645
+ * chat dispatch as separate turns with separate claims; everything else
30646
+ * keeps the historical chat-level grouping.
30647
+ */
30648
+ dispatchGroupKey(event) {
30649
+ if (this.usesLaneLedger(event)) {
30650
+ return this.laneLedger.laneKeyFor(event);
30651
+ }
30652
+ return event.targetId;
30653
+ }
30654
+ // Lane-flow protocols live in gateway-lane-flow.ts; these thin delegates
30655
+ // keep call sites and tests on the class surface.
30656
+ laneFlowHost() {
30657
+ return this;
30658
+ }
30659
+ dispatchLaneGroup(opts) {
30660
+ return dispatchLaneGroup(this.laneFlowHost(), opts);
30661
+ }
30662
+ consumeTypedDispatch(ref, run, ack) {
30663
+ return consumeTypedDispatch(this.laneFlowHost(), ref, run, ack);
30664
+ }
30127
30665
  buildDispatchContext(event, sessionKey) {
30128
30666
  const binding = this.sessionBindings.get(sessionKey);
30129
30667
  return {
@@ -30140,6 +30678,7 @@ var ParallAgentGateway = class {
30140
30678
  noReply: event.noReply ?? false,
30141
30679
  contextFilePath: this.opts.contextFilePathForSession?.(sessionKey),
30142
30680
  stepIdFilePath: this.opts.stepIdFilePathForSession?.(sessionKey),
30681
+ contextDirPath: this.opts.dispatchContextDir,
30143
30682
  client: this.opts.client,
30144
30683
  log: this.opts.log
30145
30684
  };
@@ -30179,7 +30718,7 @@ var ParallAgentGateway = class {
30179
30718
  this.opts.log?.warn(`failed to create input step: ${String(err)}`);
30180
30719
  }
30181
30720
  }
30182
- async createRuntimeStep(sessionId, event, runtimeEvent, stepIdFilePath, contextFilePath) {
30721
+ async createRuntimeStep(sessionId, event, runtimeEvent, stepIdFilePath, contextFilePath, laneContextFilePath2) {
30183
30722
  const target = resolveStepTarget(event);
30184
30723
  try {
30185
30724
  switch (runtimeEvent.type) {
@@ -30225,6 +30764,9 @@ var ParallAgentGateway = class {
30225
30764
  } else if (stepIdFilePath) {
30226
30765
  this.writeStepIdFile(stepIdFilePath, step.id);
30227
30766
  }
30767
+ if (laneContextFilePath2) {
30768
+ this.updateContextFileStepId(laneContextFilePath2, step.id);
30769
+ }
30228
30770
  break;
30229
30771
  }
30230
30772
  case "tool_result":
@@ -30247,6 +30789,9 @@ var ParallAgentGateway = class {
30247
30789
  } else if (stepIdFilePath) {
30248
30790
  this.clearStepIdFile(stepIdFilePath);
30249
30791
  }
30792
+ if (laneContextFilePath2) {
30793
+ this.updateContextFileStepId(laneContextFilePath2, null);
30794
+ }
30250
30795
  break;
30251
30796
  case "error":
30252
30797
  await this.opts.client.createAgentStep(this.opts.config.org_id, this.opts.agentUserId, sessionId, {
@@ -30266,28 +30811,28 @@ var ParallAgentGateway = class {
30266
30811
  }
30267
30812
  writeContextFile(filePath, ctx) {
30268
30813
  try {
30269
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
30270
- fs.writeFileSync(filePath, JSON.stringify(ctx), "utf8");
30814
+ fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
30815
+ fs2.writeFileSync(filePath, JSON.stringify(ctx), "utf8");
30271
30816
  } catch (err) {
30272
30817
  this.opts.log?.warn(`failed to write context file ${filePath}: ${String(err)}`);
30273
30818
  }
30274
30819
  }
30275
30820
  updateContextFileStepId(filePath, stepId) {
30276
30821
  try {
30277
- const raw = fs.readFileSync(filePath, "utf8");
30822
+ const raw = fs2.readFileSync(filePath, "utf8");
30278
30823
  const ctx = JSON.parse(raw);
30279
30824
  ctx.step_id = stepId;
30280
- fs.writeFileSync(filePath, JSON.stringify(ctx), "utf8");
30825
+ fs2.writeFileSync(filePath, JSON.stringify(ctx), "utf8");
30281
30826
  } catch (err) {
30282
30827
  this.opts.log?.warn(`failed to update context file step_id ${filePath}: ${String(err)}`);
30283
30828
  }
30284
30829
  }
30285
30830
  updateContextFileSessionId(filePath, sessionId) {
30286
30831
  try {
30287
- const raw = fs.readFileSync(filePath, "utf8");
30832
+ const raw = fs2.readFileSync(filePath, "utf8");
30288
30833
  const ctx = JSON.parse(raw);
30289
30834
  ctx.session_id = sessionId;
30290
- fs.writeFileSync(filePath, JSON.stringify(ctx), "utf8");
30835
+ fs2.writeFileSync(filePath, JSON.stringify(ctx), "utf8");
30291
30836
  } catch (err) {
30292
30837
  this.opts.log?.warn(`failed to update context file session_id ${filePath}: ${String(err)}`);
30293
30838
  }
@@ -30295,8 +30840,8 @@ var ParallAgentGateway = class {
30295
30840
  /** @deprecated Use writeContextFile / updateContextFileStepId. */
30296
30841
  writeStepIdFile(filePath, stepId) {
30297
30842
  try {
30298
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
30299
- fs.writeFileSync(filePath, stepId, "utf8");
30843
+ fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
30844
+ fs2.writeFileSync(filePath, stepId, "utf8");
30300
30845
  } catch (err) {
30301
30846
  this.opts.log?.warn(`failed to write step id file ${filePath}: ${String(err)}`);
30302
30847
  }
@@ -30304,7 +30849,7 @@ var ParallAgentGateway = class {
30304
30849
  /** @deprecated Use writeContextFile / updateContextFileStepId. */
30305
30850
  clearStepIdFile(filePath) {
30306
30851
  try {
30307
- fs.writeFileSync(filePath, "", "utf8");
30852
+ fs2.writeFileSync(filePath, "", "utf8");
30308
30853
  } catch {
30309
30854
  }
30310
30855
  }
@@ -30313,7 +30858,7 @@ var ParallAgentGateway = class {
30313
30858
  await this.createInputStep(sessionId, event);
30314
30859
  }
30315
30860
  }
30316
- async bindRuntimeSession(sessionKey, runtimeEvent, contextFilePath) {
30861
+ async bindRuntimeSession(sessionKey, runtimeEvent, contextFilePath, laneContextFilePath2) {
30317
30862
  const runtimeLaneKey = runtimeEvent.runtimeLaneKey || sessionKey;
30318
30863
  const existing = this.sessionBindings.get(sessionKey);
30319
30864
  if (existing && existing.runtimeLaneKey === runtimeLaneKey && existing.runtimeSessionId === runtimeEvent.runtimeSessionId) {
@@ -30357,6 +30902,9 @@ var ParallAgentGateway = class {
30357
30902
  if (contextFilePath) {
30358
30903
  this.updateContextFileSessionId(contextFilePath, session.id);
30359
30904
  }
30905
+ if (laneContextFilePath2) {
30906
+ this.updateContextFileSessionId(laneContextFilePath2, session.id);
30907
+ }
30360
30908
  await this.opts.onSessionBinding?.(binding);
30361
30909
  return binding;
30362
30910
  }
@@ -30383,14 +30931,27 @@ var ParallAgentGateway = class {
30383
30931
  const dispatchContext = this.buildDispatchContext(event, sessionKey);
30384
30932
  const contextFilePath = dispatchContext.contextFilePath;
30385
30933
  const stepIdFilePath = dispatchContext.stepIdFilePath;
30934
+ const activeLane = this.ledgerDisabled ? void 0 : this.laneLedger?.getForEvent(event);
30935
+ const laneContextFilePath2 = activeLane ? this.laneLedger?.laneContextPath(activeLane) : void 0;
30936
+ const contextBody = {
30937
+ session_id: dispatchContext.sessionId ?? null,
30938
+ chat_id: dispatchContext.chatId ?? null,
30939
+ trigger_message_id: dispatchContext.triggerMessageId ?? null,
30940
+ no_reply: dispatchContext.noReply,
30941
+ step_id: null,
30942
+ dispatch_event_id: activeLane?.typedDispatchEventId ?? activeLane?.folded.get(event.messageId) ?? null,
30943
+ lane: activeLane?.lane ?? null,
30944
+ target_uri: activeLane?.targetUri ?? null,
30945
+ thread_root_id: activeLane?.threadRootId ?? null,
30946
+ // Typed binding hint for the CLI: which task this dispatch is about
30947
+ // (parall task update attaches the typed effect only on a match).
30948
+ task_id: event.type === "task" ? event.targetId : null
30949
+ };
30386
30950
  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
- });
30951
+ this.writeContextFile(contextFilePath, contextBody);
30952
+ }
30953
+ if (laneContextFilePath2) {
30954
+ this.writeContextFile(laneContextFilePath2, contextBody);
30394
30955
  }
30395
30956
  this.inFlightDispatches++;
30396
30957
  const deadlineTimer = this.DISPATCH_DEADLINE_MS > 0 ? setTimeout(() => {
@@ -30417,7 +30978,7 @@ var ParallAgentGateway = class {
30417
30978
  })) {
30418
30979
  if (runtimeEvent.type === "runtime_session") {
30419
30980
  const priorAgentSessionId = binding?.agentSessionId;
30420
- binding = await this.bindRuntimeSession(sessionKey, runtimeEvent, contextFilePath);
30981
+ binding = await this.bindRuntimeSession(sessionKey, runtimeEvent, contextFilePath, laneContextFilePath2);
30421
30982
  if (event.targetType === "channel_conversation" && binding.agentSessionId !== priorAgentSessionId) {
30422
30983
  try {
30423
30984
  await this.opts.client.setChannelConversationSession(this.opts.config.org_id, event.targetId, binding.agentSessionId);
@@ -30449,6 +31010,9 @@ var ParallAgentGateway = class {
30449
31010
  await this.createInputStep(binding.agentSessionId, event);
30450
31011
  inputStepsCreated = true;
30451
31012
  }
31013
+ if (activeLane && !this.ledgerDisabled) {
31014
+ this.laneLedger?.maybeRenew(activeLane);
31015
+ }
30452
31016
  if (captureText && runtimeEvent.type === "text" && runtimeEvent.text) {
30453
31017
  captureText.push(runtimeEvent.text);
30454
31018
  }
@@ -30465,7 +31029,7 @@ var ParallAgentGateway = class {
30465
31029
  } else if (runtimeEvent.type === "tool_result" && pendingSendCallIds.delete(runtimeEvent.callId)) {
30466
31030
  recordMessageSend(sessionKey, !runtimeEvent.error);
30467
31031
  }
30468
- await this.createRuntimeStep(binding.agentSessionId, event, runtimeEvent, stepIdFilePath, contextFilePath);
31032
+ await this.createRuntimeStep(binding.agentSessionId, event, runtimeEvent, stepIdFilePath, contextFilePath, laneContextFilePath2);
30469
31033
  }
30470
31034
  if (!binding) {
30471
31035
  binding = this.sessionBindings.get(sessionKey);
@@ -30487,7 +31051,7 @@ var ParallAgentGateway = class {
30487
31051
  await this.createRuntimeStep(binding.agentSessionId, event, {
30488
31052
  type: "error",
30489
31053
  message: `Dispatch failed: ${String(err)}`
30490
- }, stepIdFilePath, contextFilePath);
31054
+ }, stepIdFilePath, contextFilePath, laneContextFilePath2);
30491
31055
  } catch (stepErr) {
30492
31056
  if (this.isSessionNotLiveError(stepErr))
30493
31057
  staleDetected = true;
@@ -30530,6 +31094,9 @@ var ParallAgentGateway = class {
30530
31094
  } else if (stepIdFilePath) {
30531
31095
  this.clearStepIdFile(stepIdFilePath);
30532
31096
  }
31097
+ if (laneContextFilePath2) {
31098
+ this.updateContextFileStepId(laneContextFilePath2, null);
31099
+ }
30533
31100
  this.inFlightDispatches--;
30534
31101
  if (this.inFlightDispatches === 0 && this.drainResolvers.length > 0) {
30535
31102
  const resolvers = this.drainResolvers.splice(0);
@@ -30587,13 +31154,39 @@ var ParallAgentGateway = class {
30587
31154
  item.resolve(false);
30588
31155
  break;
30589
31156
  }
30590
- const items = fork.queue.splice(0);
31157
+ let items;
31158
+ const head = fork.queue[0];
31159
+ if (head && this.usesLaneLedger(head.event)) {
31160
+ const headKey = this.dispatchGroupKey(head.event);
31161
+ const splitAt = fork.queue.findIndex((it) => this.dispatchGroupKey(it.event) !== headKey);
31162
+ items = splitAt === -1 ? fork.queue.splice(0) : fork.queue.splice(0, splitAt);
31163
+ } else {
31164
+ items = fork.queue.splice(0);
31165
+ }
30591
31166
  const events = items.map((item) => item.event);
30592
31167
  const last = events[events.length - 1];
30593
31168
  const earlier = events.slice(0, -1);
30594
31169
  try {
30595
31170
  const batchText = [];
30596
- const dispatched = await this.runDispatch(last, fork.fork.sessionKey, buildForkScopePrefix(last) + buildEventBody(last), earlier, batchText);
31171
+ let dispatched;
31172
+ if (this.usesLaneLedger(last)) {
31173
+ const outcome = await this.dispatchLaneGroup({
31174
+ events,
31175
+ sessionKey: fork.fork.sessionKey,
31176
+ body: buildForkScopePrefix(last) + buildEventBody(last),
31177
+ earlier,
31178
+ captureText: batchText,
31179
+ hasMoreLocal: () => fork.queue.length > 0
31180
+ });
31181
+ if (outcome === "foreign") {
31182
+ for (const item of items)
31183
+ item.resolve(false);
31184
+ break;
31185
+ }
31186
+ dispatched = outcome === "dispatched";
31187
+ } else {
31188
+ dispatched = await this.runDispatch(last, fork.fork.sessionKey, buildForkScopePrefix(last) + buildEventBody(last), earlier, batchText);
31189
+ }
30597
31190
  if (!dispatched) {
30598
31191
  for (const item of items) {
30599
31192
  item.resolve(false);
@@ -30696,9 +31289,9 @@ var ParallAgentGateway = class {
30696
31289
  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
31290
  break;
30698
31291
  }
30699
- const targetId = this.dispatchState.mainBuffer[0].targetId;
31292
+ const groupKey = this.dispatchGroupKey(this.dispatchState.mainBuffer[0]);
30700
31293
  const events = [];
30701
- while (this.dispatchState.mainBuffer[0]?.targetId === targetId) {
31294
+ while (this.dispatchState.mainBuffer[0] && this.dispatchGroupKey(this.dispatchState.mainBuffer[0]) === groupKey) {
30702
31295
  events.push(this.dispatchState.mainBuffer.shift());
30703
31296
  }
30704
31297
  const event = events[events.length - 1];
@@ -30707,7 +31300,36 @@ var ParallAgentGateway = class {
30707
31300
  const pendingFork = hasPendingInjections ? [] : this.dispatchState.pendingForkResults.splice(0);
30708
31301
  const forkPrefix = buildForkResultPrefix(pendingFork);
30709
31302
  this.dispatchState.mainCurrentTargetId = event.targetId;
31303
+ this.mainCurrentGroupKey = groupKey;
30710
31304
  this.dispatchState.mainPreDispatchBranchPoint = this.opts.dispatchAdapter.getBranchPoint?.(this.opts.runtimeKey);
31305
+ if (this.usesLaneLedger(event)) {
31306
+ let outcome;
31307
+ try {
31308
+ outcome = await this.dispatchLaneGroup({
31309
+ events,
31310
+ sessionKey: this.opts.runtimeKey,
31311
+ body: forkPrefix + buildEventBody(event),
31312
+ earlier,
31313
+ hasMoreLocal: () => this.dispatchState.mainBuffer.some((e) => this.dispatchGroupKey(e) === groupKey)
31314
+ });
31315
+ } catch (err) {
31316
+ this.opts.log?.error(`lane dispatch failed for ${event.messageId}: ${String(err)}`);
31317
+ this.dispatchState.pendingForkResults.unshift(...pendingFork);
31318
+ for (const ev of events)
31319
+ this.dispatchedMessages.delete(ev.messageId);
31320
+ continue;
31321
+ }
31322
+ if (outcome === "shutdown") {
31323
+ this.dispatchState.mainBuffer.unshift(...events);
31324
+ this.dispatchState.pendingForkResults.unshift(...pendingFork);
31325
+ break;
31326
+ }
31327
+ if (outcome === "foreign") {
31328
+ this.dispatchState.pendingForkResults.unshift(...pendingFork);
31329
+ continue;
31330
+ }
31331
+ continue;
31332
+ }
30711
31333
  try {
30712
31334
  await this.emitDispatchReceived(event);
30713
31335
  } catch (err) {
@@ -30736,6 +31358,7 @@ var ParallAgentGateway = class {
30736
31358
  this.draining = false;
30737
31359
  this.dispatchState.mainDispatching = false;
30738
31360
  this.dispatchState.mainCurrentTargetId = void 0;
31361
+ this.mainCurrentGroupKey = void 0;
30739
31362
  this.dispatchState.mainPreDispatchBranchPoint = void 0;
30740
31363
  if (!this.shuttingDown && this.dispatchState.mainBuffer.length > 0) {
30741
31364
  setTimeout(() => {
@@ -30755,13 +31378,38 @@ var ParallAgentGateway = class {
30755
31378
  const forkPrefix = buildForkResultPrefix(pendingFork);
30756
31379
  this.dispatchState.mainDispatching = true;
30757
31380
  this.dispatchState.mainCurrentTargetId = event.targetId;
31381
+ this.mainCurrentGroupKey = this.dispatchGroupKey(event);
30758
31382
  this.dispatchState.mainPreDispatchBranchPoint = this.opts.dispatchAdapter.getBranchPoint?.(this.opts.runtimeKey);
31383
+ if (this.usesLaneLedger(event)) {
31384
+ let outcome = "shutdown";
31385
+ try {
31386
+ try {
31387
+ outcome = await this.dispatchLaneGroup({
31388
+ events: [event],
31389
+ sessionKey: this.opts.runtimeKey,
31390
+ body: forkPrefix + buildEventBody(event),
31391
+ earlier: [],
31392
+ hasMoreLocal: () => this.dispatchState.mainBuffer.some((e) => this.dispatchGroupKey(e) === this.dispatchGroupKey(event))
31393
+ });
31394
+ } catch (err) {
31395
+ this.dispatchState.pendingForkResults.unshift(...pendingFork);
31396
+ throw err;
31397
+ }
31398
+ if (outcome !== "dispatched") {
31399
+ this.dispatchState.pendingForkResults.unshift(...pendingFork);
31400
+ }
31401
+ } finally {
31402
+ await this.drainMainBuffer();
31403
+ }
31404
+ return outcome === "dispatched";
31405
+ }
30759
31406
  try {
30760
31407
  await this.emitDispatchReceived(event);
30761
31408
  } catch (err) {
30762
31409
  this.opts.log?.warn?.(`mark-received failed, leaving unacked for retry: ${String(err)}`);
30763
31410
  this.dispatchState.mainDispatching = false;
30764
31411
  this.dispatchState.mainCurrentTargetId = void 0;
31412
+ this.mainCurrentGroupKey = void 0;
30765
31413
  this.dispatchState.mainPreDispatchBranchPoint = void 0;
30766
31414
  this.dispatchState.pendingForkResults.unshift(...pendingFork);
30767
31415
  return false;
@@ -30782,7 +31430,11 @@ var ParallAgentGateway = class {
30782
31430
  return false;
30783
31431
  }
30784
31432
  this.dispatchState.mainBuffer.push(event);
30785
- if (this.dispatchState.mainCurrentTargetId === event.targetId && await this.opts.dispatchAdapter.enqueueDuringDispatch?.(this.opts.runtimeKey, buildEventBody(event))) {
31433
+ if (this.usesLaneLedger(event)) {
31434
+ if (this.mainCurrentGroupKey === this.dispatchGroupKey(event) && await this.laneLedger?.steerLive(event) && await this.opts.dispatchAdapter.enqueueDuringDispatch?.(this.opts.runtimeKey, buildEventBody(event))) {
31435
+ this.opts.log?.info(`steer folded+injected for ${event.messageId} (will drain for bookkeeping)`);
31436
+ }
31437
+ } else if (this.dispatchState.mainCurrentTargetId === event.targetId && await this.opts.dispatchAdapter.enqueueDuringDispatch?.(this.opts.runtimeKey, buildEventBody(event))) {
30786
31438
  this.opts.log?.info(`steer injected for ${event.messageId} (will drain for bookkeeping)`);
30787
31439
  }
30788
31440
  if (!this.dispatchState.mainDispatching && !this.draining && this.dispatchState.mainBuffer.length > 0) {
@@ -30953,8 +31605,10 @@ var ParallAgentGateway = class {
30953
31605
  try {
30954
31606
  const dispatched = await this.handleInboundEvent(event);
30955
31607
  if (dispatched) {
30956
- this.opts.client.ackDispatch(this.opts.config.org_id, { source_type: "message", source_id: data.id }).catch(() => {
30957
- });
31608
+ if (!this.usesLaneLedger(event)) {
31609
+ this.opts.client.ackDispatch(this.opts.config.org_id, { source_type: "message", source_id: data.id }).catch(() => {
31610
+ });
31611
+ }
30958
31612
  } else {
30959
31613
  this.dispatchedMessages.delete(data.id);
30960
31614
  }
@@ -30963,7 +31617,21 @@ var ParallAgentGateway = class {
30963
31617
  this.dispatchedMessages.delete(data.id);
30964
31618
  }
30965
31619
  }
30966
- async handleTaskAssignment(task, ackSourceId) {
31620
+ // Ledger re-drive consumption: dispatch.new message hints re-enter the
31621
+ // shared WorkItem consumption path (same protocol as catch-up).
31622
+ async handleMessageRedrive(item) {
31623
+ if (!item.chat_id || !item.source_id)
31624
+ return;
31625
+ await this.consumeMessageWorkItem({
31626
+ id: item.id,
31627
+ source_id: item.source_id,
31628
+ chat_id: item.chat_id
31629
+ });
31630
+ }
31631
+ consumeMessageWorkItem(item) {
31632
+ return consumeMessageWorkItem(this.laneFlowHost(), item);
31633
+ }
31634
+ async handleTaskAssignment(task, ackSourceId, dispatchEventId) {
30967
31635
  if (this.shuttingDown)
30968
31636
  return false;
30969
31637
  const dedupeKey = `${task.id}:${task.updated_at}`;
@@ -30992,7 +31660,8 @@ var ParallAgentGateway = class {
30992
31660
  body: parts.join("\n"),
30993
31661
  sentAt: task.updated_at ?? task.created_at,
30994
31662
  ackSourceType: "task_activity",
30995
- ackSourceId
31663
+ ackSourceId,
31664
+ dispatchEventId
30996
31665
  };
30997
31666
  const dispatched = await this.handleInboundEvent(event);
30998
31667
  if (!dispatched) {
@@ -31016,7 +31685,7 @@ var ParallAgentGateway = class {
31016
31685
  this.opts.log?.info(`skipping stale task dispatch ${ackSourceId ?? taskId} \u2014 assigned to ${task.assignee_id}, creator ${task.creator_id}`);
31017
31686
  return true;
31018
31687
  }
31019
- return this.handleTaskAssignment(task, ackSourceId);
31688
+ return this.handleTaskAssignment(task, ackSourceId, opts.dispatchEventId);
31020
31689
  }
31021
31690
  async handleTaskComment(commentId, taskId, actorId, deliveryReason) {
31022
31691
  if (this.shuttingDown)
@@ -31424,74 +32093,46 @@ var ParallAgentGateway = class {
31424
32093
  }
31425
32094
  processed++;
31426
32095
  try {
31427
- let dispatched = false;
32096
+ const ackItem = () => {
32097
+ this.opts.client.ackDispatchByID(this.opts.config.org_id, item.id).catch(() => {
32098
+ });
32099
+ };
31428
32100
  if (item.event_type === "task_assign" && item.task_id) {
31429
32101
  try {
31430
- dispatched = await this.handleTaskDispatch(item.task_id, item.source_id ?? item.task_id);
32102
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, (dispatchEventId) => this.handleTaskDispatch(item.task_id ?? "", item.source_id ?? item.task_id ?? "", {
32103
+ dispatchEventId
32104
+ }), ackItem);
31431
32105
  } catch (err) {
31432
32106
  this.opts.log?.warn(`catch-up task fetch failed for ${item.task_id}, leaving pending: ${String(err)}`);
31433
32107
  continue;
31434
32108
  }
31435
32109
  } else if (item.event_type === "task_update" && item.task_id) {
31436
32110
  try {
31437
- dispatched = await this.handleTaskDispatch(item.task_id, item.source_id ?? item.task_id, { allowCreator: true });
32111
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, (dispatchEventId) => this.handleTaskDispatch(item.task_id ?? "", item.source_id ?? item.task_id ?? "", {
32112
+ allowCreator: true,
32113
+ dispatchEventId
32114
+ }), ackItem);
31438
32115
  } catch (err) {
31439
32116
  this.opts.log?.warn(`catch-up task fetch failed for ${item.task_id}, leaving pending: ${String(err)}`);
31440
32117
  continue;
31441
32118
  }
31442
32119
  } 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);
32120
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.handleTaskComment(item.source_id, item.task_id ?? "", item.actor_id, item.delivery_reason), ackItem);
31444
32121
  } else if (item.event_type === "wiki_comment" && item.source_id) {
31445
- dispatched = await this.handleWikiComment(item.source_id, item.actor_id, item.delivery_reason);
32122
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.handleWikiComment(item.source_id, item.actor_id, item.delivery_reason), ackItem);
31446
32123
  } else if (item.event_type === "schedule.fire" && item.source_id) {
31447
- dispatched = await this.fetchAndHandleScheduleFire(item.source_id, item.actor_id);
32124
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.fetchAndHandleScheduleFire(item.source_id, item.actor_id), ackItem);
31448
32125
  } else if (item.event_type === "external_trigger" && item.source_id) {
31449
- dispatched = await this.fetchAndHandleExternalTriggerRun(item.source_id);
32126
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.fetchAndHandleExternalTriggerRun(item.source_id), ackItem);
31450
32127
  } else if (item.event_type === "channel_message" && item.source_id) {
31451
- dispatched = await this.fetchAndHandleChannelMessage(item.source_id);
32128
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.fetchAndHandleChannelMessage(item.source_id), ackItem);
31452
32129
  } 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);
32130
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.fetchAndHandleApprovalDecided(item.source_id, item.actor_id, item.chat_id ?? null), ackItem);
31454
32131
  } 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(() => {
32132
+ await this.consumeMessageWorkItem({
32133
+ id: item.id,
32134
+ source_id: item.source_id,
32135
+ chat_id: item.chat_id
31495
32136
  });
31496
32137
  }
31497
32138
  } catch (err) {
@@ -31557,6 +32198,14 @@ ${fullSummary}` : fullSummary;
31557
32198
  this.abortFork(targetId, "ws reconnect");
31558
32199
  }
31559
32200
  }
32201
+ if (this.laneLedger && this.inFlightDispatches === 0 && this.laneLedger.activeCount > 0) {
32202
+ log2?.info(`releasing ${this.laneLedger.activeCount} stale lane(s) on reconnect`);
32203
+ await this.laneLedger.releaseAll();
32204
+ }
32205
+ if (this.laneLedger && this.ledgerDisabled) {
32206
+ log2?.info("re-probing dispatch ledger after reconnect (was disabled)");
32207
+ this.ledgerDisabled = false;
32208
+ }
31560
32209
  const intervalSec = data.heartbeat_interval > 0 ? data.heartbeat_interval : 30;
31561
32210
  try {
31562
32211
  const count = await fetchAllChats(client, config.org_id, this.chatInfoMap);
@@ -31627,6 +32276,10 @@ ${fullSummary}` : fullSummary;
31627
32276
  }
31628
32277
  if (this.heartbeatTimer)
31629
32278
  clearInterval(this.heartbeatTimer);
32279
+ if (this.laneLedger && this.laneLedger.activeCount > 0) {
32280
+ this.opts.log?.info(`releasing ${this.laneLedger.activeCount} lane(s) on shutdown`);
32281
+ await this.laneLedger.releaseAll();
32282
+ }
31630
32283
  await this.opts.onBeforeDisconnect?.();
31631
32284
  this.opts.ws.disconnect();
31632
32285
  this.opts.log?.info(`disconnected`);
@@ -31634,8 +32287,8 @@ ${fullSummary}` : fullSummary;
31634
32287
  };
31635
32288
 
31636
32289
  // ts/agent-core/dist/platform-config.js
31637
- import * as fs2 from "node:fs";
31638
- import * as path2 from "node:path";
32290
+ import * as fs3 from "node:fs";
32291
+ import * as path3 from "node:path";
31639
32292
  function isPlatformManagedProfile(profile) {
31640
32293
  return profile?.machine_id != null || profile?.model_management === "platform";
31641
32294
  }
@@ -31652,11 +32305,11 @@ function deriveModelIsPin(defaults, profile) {
31652
32305
  var CACHE_FILENAME = "parall-platform-config.json";
31653
32306
  var SUPPORTED_SCHEMA_VERSION = 1;
31654
32307
  function cachePath(stateDir) {
31655
- return path2.join(stateDir, CACHE_FILENAME);
32308
+ return path3.join(stateDir, CACHE_FILENAME);
31656
32309
  }
31657
32310
  function loadCache(stateDir) {
31658
32311
  try {
31659
- const raw = fs2.readFileSync(cachePath(stateDir), "utf-8");
32312
+ const raw = fs3.readFileSync(cachePath(stateDir), "utf-8");
31660
32313
  return JSON.parse(raw);
31661
32314
  } catch {
31662
32315
  return null;
@@ -31671,9 +32324,9 @@ function saveCache(stateDir, response) {
31671
32324
  };
31672
32325
  const filePath = cachePath(stateDir);
31673
32326
  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);
32327
+ fs3.mkdirSync(path3.dirname(filePath), { recursive: true });
32328
+ fs3.writeFileSync(tmpPath, JSON.stringify(cached, null, 2), "utf-8");
32329
+ fs3.renameSync(tmpPath, filePath);
31677
32330
  }
31678
32331
  function runtimeModelName(canonicalModel, runtimeType, config) {
31679
32332
  const runtime = runtimeType?.trim();
@@ -31764,8 +32417,8 @@ function createPlatformConfigManager(opts) {
31764
32417
  }
31765
32418
 
31766
32419
  // ts/agent-core/dist/skills/index.js
31767
- import * as fs3 from "node:fs";
31768
- import * as path3 from "node:path";
32420
+ import * as fs4 from "node:fs";
32421
+ import * as path4 from "node:path";
31769
32422
 
31770
32423
  // ts/agent-core/dist/skills/parall-platform.js
31771
32424
  var PARALL_PLATFORM_SKILL = `# Parall Platform
@@ -32538,13 +33191,13 @@ var SKILLS = [
32538
33191
  }
32539
33192
  ];
32540
33193
  function writeSkillFiles(targetDir) {
32541
- fs3.mkdirSync(targetDir, { recursive: true });
33194
+ fs4.mkdirSync(targetDir, { recursive: true });
32542
33195
  for (const skill of SKILLS) {
32543
- fs3.writeFileSync(path3.join(targetDir, `${skill.name}.md`), skill.content, "utf8");
33196
+ fs4.writeFileSync(path4.join(targetDir, `${skill.name}.md`), skill.content, "utf8");
32544
33197
  }
32545
33198
  }
32546
33199
  function buildSkillReferences(workspaceDir) {
32547
- const dir = path3.join(workspaceDir, ".parall", "skills");
33200
+ const dir = path4.join(workspaceDir, ".parall", "skills");
32548
33201
  const lines = SKILLS.map((s) => `- ${s.description.split(":")[0]}: \`${dir}/${s.name}.md\``);
32549
33202
  return `## Platform Skills (read on demand)
32550
33203
 
@@ -32554,7 +33207,7 @@ ${lines.join("\n")}
32554
33207
 
32555
33208
  // ts/codex-agent/dist/config.js
32556
33209
  import * as os2 from "node:os";
32557
- import * as path4 from "node:path";
33210
+ import * as path5 from "node:path";
32558
33211
  function requireEnv(env, name) {
32559
33212
  const value = env[name]?.trim();
32560
33213
  if (!value) {
@@ -32563,15 +33216,15 @@ function requireEnv(env, name) {
32563
33216
  return value;
32564
33217
  }
32565
33218
  function resolvePath(value) {
32566
- return path4.isAbsolute(value) ? value : path4.resolve(process.cwd(), value);
33219
+ return path5.isAbsolute(value) ? value : path5.resolve(process.cwd(), value);
32567
33220
  }
32568
33221
  function resolveCodexAgentConfig(env = process.env) {
32569
33222
  const apiUrl = requireEnv(env, "PRLL_API_URL");
32570
33223
  const apiKey = requireEnv(env, "PRLL_API_KEY");
32571
33224
  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"));
33225
+ const codexHome = resolvePath(env.PRLL_CODEX_HOME?.trim() || env.CODEX_HOME?.trim() || path5.join(env.HOME || os2.homedir(), ".codex"));
33226
+ const stateDir = resolvePath(env.PRLL_STATE_DIR?.trim() || path5.join(env.HOME || os2.homedir(), ".parall-agent"));
33227
+ const workspaceDir = resolvePath(env.PRLL_WORKSPACE_DIR?.trim() || path5.join(stateDir, "workspace"));
32575
33228
  return {
32576
33229
  apiUrl,
32577
33230
  apiKey,
@@ -32625,28 +33278,31 @@ function buildCodexRuntimeKey(agentUserId) {
32625
33278
  }
32626
33279
  function sessionStateFilePathForRuntime(stateDir, runtimeKey) {
32627
33280
  const fileName = Buffer.from(runtimeKey).toString("base64url");
32628
- return path4.join(stateDir, "threads", `${fileName}.json`);
33281
+ return path5.join(stateDir, "threads", `${fileName}.json`);
32629
33282
  }
32630
33283
  function contextFilePathForSession(stateDir, sessionKey) {
32631
33284
  const fileName = Buffer.from(sessionKey).toString("base64url");
32632
- return path4.join(stateDir, "dispatch-context", `${fileName}.json`);
33285
+ return path5.join(stateDir, "dispatch-context", `${fileName}.json`);
33286
+ }
33287
+ function dispatchContextDirPath(stateDir) {
33288
+ return dispatchLaneContextDir(stateDir);
32633
33289
  }
32634
33290
  function stepIdFilePathForSession(stateDir, sessionKey) {
32635
33291
  const fileName = Buffer.from(sessionKey).toString("base64url");
32636
- return path4.join(stateDir, "step-ids", `${fileName}.txt`);
33292
+ return path5.join(stateDir, "step-ids", `${fileName}.txt`);
32637
33293
  }
32638
33294
 
32639
33295
  // ts/codex-agent/dist/dispatch.js
32640
33296
  import { execSync as execSync2, spawn } from "node:child_process";
32641
33297
  import { randomUUID } from "node:crypto";
32642
- import * as fs5 from "node:fs";
33298
+ import * as fs6 from "node:fs";
32643
33299
 
32644
33300
  // ts/agent-core/dist/internal/attachment-input.js
32645
33301
  import { execSync } from "node:child_process";
32646
33302
  import { constants } from "node:fs";
32647
33303
  import * as fsSync from "node:fs";
32648
- import * as fs4 from "node:fs/promises";
32649
- import * as path5 from "node:path";
33304
+ import * as fs5 from "node:fs/promises";
33305
+ import * as path6 from "node:path";
32650
33306
  var DEFAULT_MAX_TOTAL_IMAGE_BYTES = 20 * 1024 * 1024;
32651
33307
  var DEFAULT_ATTACHMENT_CACHE_MAX_BYTES = 512 * 1024 * 1024;
32652
33308
  var DEFAULT_ATTACHMENT_DOWNLOAD_TIMEOUT_MS = 3e4;
@@ -32676,11 +33332,11 @@ async function prepareLocalImageAttachments(event, context2, opts) {
32676
33332
  };
32677
33333
  }
32678
33334
  const rootDir = await ensureAttachmentRootDir(opts.workspaceDir);
32679
- const messageDir = path5.join(rootDir, sanitizePathSegment(event.messageId));
33335
+ const messageDir = path6.join(rootDir, sanitizePathSegment(event.messageId));
32680
33336
  await ensurePathIsNotSymlink(messageDir);
32681
- await fs4.mkdir(messageDir, { recursive: true });
33337
+ await fs5.mkdir(messageDir, { recursive: true });
32682
33338
  await ensurePathIsNotSymlink(messageDir);
32683
- const activeMessageDir = path5.resolve(messageDir);
33339
+ const activeMessageDir = path6.resolve(messageDir);
32684
33340
  activeAttachmentDirs.add(activeMessageDir);
32685
33341
  const maintenanceCooldownMs = opts.maintenanceCooldownMs ?? DEFAULT_MAINTENANCE_COOLDOWN_MS;
32686
33342
  const maintenancePromise = scheduleAttachmentMaintenance(rootDir, {
@@ -32697,7 +33353,7 @@ async function prepareLocalImageAttachments(event, context2, opts) {
32697
33353
  const notes = [];
32698
33354
  let downloadedBytes = 0;
32699
33355
  for (const att of imageAttachments) {
32700
- const localPath = path5.join(messageDir, localFileName(att.id, att.fileName, att.mimeType));
33356
+ const localPath = path6.join(messageDir, localFileName(att.id, att.fileName, att.mimeType));
32701
33357
  const downloadTimeoutMs = opts.downloadTimeoutMs ?? DEFAULT_ATTACHMENT_DOWNLOAD_TIMEOUT_MS;
32702
33358
  const fetchFresh = async () => {
32703
33359
  const fileInfo = await withTimeout(context2.client.getFileUrl(att.id), downloadTimeoutMs, `file URL lookup timed out after ${downloadTimeoutMs}ms`);
@@ -32754,7 +33410,7 @@ async function appendPreparedLocalAttachmentRefs(body, event, context2, opts) {
32754
33410
  return { body: appendLocalAttachmentRefs(body, attachments), attachments };
32755
33411
  }
32756
33412
  function pinLocalAttachmentPaths(images) {
32757
- const dirs = new Set(images.map((image) => path5.resolve(path5.dirname(image.localPath))));
33413
+ const dirs = new Set(images.map((image) => path6.resolve(path6.dirname(image.localPath))));
32758
33414
  for (const dir of dirs) {
32759
33415
  activeAttachmentDirs.add(dir);
32760
33416
  }
@@ -32769,7 +33425,7 @@ function pinLocalAttachmentPaths(images) {
32769
33425
  };
32770
33426
  }
32771
33427
  function attachmentRootDir(workspaceDir) {
32772
- return path5.join(path5.resolve(workspaceDir), ".parall", "attachments");
33428
+ return path6.join(path6.resolve(workspaceDir), ".parall", "attachments");
32773
33429
  }
32774
33430
  function ensureLocalAttachmentGitExclude(workingDirectory) {
32775
33431
  try {
@@ -32778,8 +33434,8 @@ function ensureLocalAttachmentGitExclude(workingDirectory) {
32778
33434
  encoding: "utf8",
32779
33435
  stdio: ["ignore", "pipe", "ignore"]
32780
33436
  }).trim();
32781
- const excludePath = path5.isAbsolute(rel) ? rel : path5.join(workingDirectory, rel);
32782
- fsSync.mkdirSync(path5.dirname(excludePath), { recursive: true });
33437
+ const excludePath = path6.isAbsolute(rel) ? rel : path6.join(workingDirectory, rel);
33438
+ fsSync.mkdirSync(path6.dirname(excludePath), { recursive: true });
32783
33439
  const existing = fsSync.existsSync(excludePath) ? fsSync.readFileSync(excludePath, "utf8") : "";
32784
33440
  if (existing.split(/\r?\n/).some((line) => line.trim() === ".parall/"))
32785
33441
  return;
@@ -32815,18 +33471,18 @@ function scheduleAttachmentMaintenance(rootDir, opts) {
32815
33471
  return run;
32816
33472
  }
32817
33473
  async function ensureAttachmentRootDir(workspaceDir) {
32818
- const workspaceRoot = path5.resolve(workspaceDir);
32819
- const parallDir = path5.join(workspaceRoot, ".parall");
33474
+ const workspaceRoot = path6.resolve(workspaceDir);
33475
+ const parallDir = path6.join(workspaceRoot, ".parall");
32820
33476
  const rootDir = attachmentRootDir(workspaceRoot);
32821
- await fs4.mkdir(workspaceRoot, { recursive: true });
33477
+ await fs5.mkdir(workspaceRoot, { recursive: true });
32822
33478
  await ensurePathIsNotSymlink(parallDir);
32823
- await fs4.mkdir(parallDir, { recursive: true, mode: 448 });
33479
+ await fs5.mkdir(parallDir, { recursive: true, mode: 448 });
32824
33480
  await ensurePathIsNotSymlink(parallDir);
32825
33481
  await ensurePathIsNotSymlink(rootDir);
32826
- await fs4.mkdir(rootDir, { recursive: true, mode: 448 });
33482
+ await fs5.mkdir(rootDir, { recursive: true, mode: 448 });
32827
33483
  await ensurePathIsNotSymlink(rootDir);
32828
- const realWorkspace = await fs4.realpath(workspaceRoot);
32829
- const realRoot = await fs4.realpath(rootDir);
33484
+ const realWorkspace = await fs5.realpath(workspaceRoot);
33485
+ const realRoot = await fs5.realpath(rootDir);
32830
33486
  if (!isPathInside(realRoot, realWorkspace)) {
32831
33487
  throw new Error(`attachment root escapes workspace: ${rootDir}`);
32832
33488
  }
@@ -32834,7 +33490,7 @@ async function ensureAttachmentRootDir(workspaceDir) {
32834
33490
  }
32835
33491
  async function ensurePathIsNotSymlink(filePath) {
32836
33492
  try {
32837
- const stat = await fs4.lstat(filePath);
33493
+ const stat = await fs5.lstat(filePath);
32838
33494
  if (stat.isSymbolicLink()) {
32839
33495
  throw new Error(`refusing to use symlinked attachment path ${filePath}`);
32840
33496
  }
@@ -32845,8 +33501,8 @@ async function ensurePathIsNotSymlink(filePath) {
32845
33501
  }
32846
33502
  }
32847
33503
  function isPathInside(childPath, parentPath) {
32848
- const rel = path5.relative(parentPath, childPath);
32849
- return rel === "" || !!rel && !rel.startsWith("..") && !path5.isAbsolute(rel);
33504
+ const rel = path6.relative(parentPath, childPath);
33505
+ return rel === "" || !!rel && !rel.startsWith("..") && !path6.isAbsolute(rel);
32850
33506
  }
32851
33507
  async function existingUsableFile(filePath, expectedSize, rootDir) {
32852
33508
  try {
@@ -32857,30 +33513,30 @@ async function existingUsableFile(filePath, expectedSize, rootDir) {
32857
33513
  }
32858
33514
  }
32859
33515
  async function localFileStatInsideRoot(filePath, rootDir) {
32860
- const stat = await fs4.lstat(filePath);
33516
+ const stat = await fs5.lstat(filePath);
32861
33517
  if (stat.isSymbolicLink()) {
32862
33518
  throw new Error(`refusing to use symlinked attachment file ${filePath}`);
32863
33519
  }
32864
33520
  if (!stat.isFile()) {
32865
33521
  throw new Error(`attachment path is not a file ${filePath}`);
32866
33522
  }
32867
- const realRoot = await fs4.realpath(rootDir);
32868
- const realFile = await fs4.realpath(filePath);
33523
+ const realRoot = await fs5.realpath(rootDir);
33524
+ const realFile = await fs5.realpath(filePath);
32869
33525
  if (!isPathInside(realFile, realRoot)) {
32870
33526
  throw new Error(`attachment file escapes workspace: ${filePath}`);
32871
33527
  }
32872
33528
  return stat;
32873
33529
  }
32874
33530
  async function localDirectoryStatInsideRoot(dirPath, rootDir) {
32875
- const stat = await fs4.lstat(dirPath);
33531
+ const stat = await fs5.lstat(dirPath);
32876
33532
  if (stat.isSymbolicLink()) {
32877
33533
  throw new Error(`refusing to use symlinked attachment directory ${dirPath}`);
32878
33534
  }
32879
33535
  if (!stat.isDirectory()) {
32880
33536
  throw new Error(`attachment path is not a directory ${dirPath}`);
32881
33537
  }
32882
- const realRoot = await fs4.realpath(rootDir);
32883
- const realDir = await fs4.realpath(dirPath);
33538
+ const realRoot = await fs5.realpath(rootDir);
33539
+ const realDir = await fs5.realpath(dirPath);
32884
33540
  if (!isPathInside(realDir, realRoot)) {
32885
33541
  throw new Error(`attachment directory escapes workspace: ${dirPath}`);
32886
33542
  }
@@ -32888,7 +33544,7 @@ async function localDirectoryStatInsideRoot(dirPath, rootDir) {
32888
33544
  }
32889
33545
  async function openLocalFileInsideRoot(filePath, rootDir) {
32890
33546
  const checkedStat = await localFileStatInsideRoot(filePath, rootDir);
32891
- const file = await fs4.open(filePath, constants.O_RDONLY | constants.O_NOFOLLOW);
33547
+ const file = await fs5.open(filePath, constants.O_RDONLY | constants.O_NOFOLLOW);
32892
33548
  let keepOpen = false;
32893
33549
  try {
32894
33550
  const openedStat = await file.stat();
@@ -32904,8 +33560,8 @@ async function openLocalFileInsideRoot(filePath, rootDir) {
32904
33560
  }
32905
33561
  }
32906
33562
  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);
33563
+ await localDirectoryStatInsideRoot(path6.dirname(filePath), rootDir);
33564
+ const file = await fs5.open(filePath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
32909
33565
  let keepOpen = false;
32910
33566
  try {
32911
33567
  const checkedStat = await localFileStatInsideRoot(filePath, rootDir);
@@ -32930,7 +33586,7 @@ async function assertLocalFileIdentity(filePath, rootDir, expected) {
32930
33586
  async function removeLocalFileIfInside(filePath, rootDir) {
32931
33587
  try {
32932
33588
  await localFileStatInsideRoot(filePath, rootDir);
32933
- await fs4.rm(filePath, { force: true });
33589
+ await fs5.rm(filePath, { force: true });
32934
33590
  } catch {
32935
33591
  }
32936
33592
  }
@@ -32943,7 +33599,7 @@ function sameFile(a, b) {
32943
33599
  async function cleanupOldAttachmentFiles(rootDir, ttlMs, log2, preserveDirs) {
32944
33600
  let entries;
32945
33601
  try {
32946
- entries = await fs4.readdir(rootDir, { withFileTypes: true });
33602
+ entries = await fs5.readdir(rootDir, { withFileTypes: true });
32947
33603
  } catch {
32948
33604
  return;
32949
33605
  }
@@ -32951,15 +33607,15 @@ async function cleanupOldAttachmentFiles(rootDir, ttlMs, log2, preserveDirs) {
32951
33607
  await Promise.all(entries.map(async (entry) => {
32952
33608
  if (!entry.isDirectory())
32953
33609
  return;
32954
- const fullPath = path5.join(rootDir, entry.name);
33610
+ const fullPath = path6.join(rootDir, entry.name);
32955
33611
  try {
32956
- if (preserveDirs?.has(path5.resolve(fullPath)))
33612
+ if (preserveDirs?.has(path6.resolve(fullPath)))
32957
33613
  return;
32958
- const stat = await fs4.lstat(fullPath);
33614
+ const stat = await fs5.lstat(fullPath);
32959
33615
  if (!stat.isDirectory())
32960
33616
  return;
32961
33617
  if (stat.mtimeMs < cutoff) {
32962
- await fs4.rm(fullPath, { recursive: true, force: true });
33618
+ await fs5.rm(fullPath, { recursive: true, force: true });
32963
33619
  }
32964
33620
  } catch (err) {
32965
33621
  log2?.warn?.(`agent-core: failed to clean attachment temp dir ${fullPath}: ${String(err)}`);
@@ -32971,7 +33627,7 @@ async function pruneAttachmentCache(rootDir, maxBytes, log2, preserveDirs) {
32971
33627
  return;
32972
33628
  let entries;
32973
33629
  try {
32974
- entries = await fs4.readdir(rootDir, { withFileTypes: true });
33630
+ entries = await fs5.readdir(rootDir, { withFileTypes: true });
32975
33631
  } catch {
32976
33632
  return;
32977
33633
  }
@@ -32980,9 +33636,9 @@ async function pruneAttachmentCache(rootDir, maxBytes, log2, preserveDirs) {
32980
33636
  for (const entry of entries) {
32981
33637
  if (!entry.isDirectory())
32982
33638
  continue;
32983
- const fullPath = path5.join(rootDir, entry.name);
33639
+ const fullPath = path6.join(rootDir, entry.name);
32984
33640
  try {
32985
- const stat = await fs4.lstat(fullPath);
33641
+ const stat = await fs5.lstat(fullPath);
32986
33642
  if (!stat.isDirectory())
32987
33643
  continue;
32988
33644
  const size = await directorySize(fullPath);
@@ -32998,10 +33654,10 @@ async function pruneAttachmentCache(rootDir, maxBytes, log2, preserveDirs) {
32998
33654
  for (const dir of dirs) {
32999
33655
  if (total <= maxBytes)
33000
33656
  break;
33001
- if (preserveDirs?.has(path5.resolve(dir.path)))
33657
+ if (preserveDirs?.has(path6.resolve(dir.path)))
33002
33658
  continue;
33003
33659
  try {
33004
- await fs4.rm(dir.path, { recursive: true, force: true });
33660
+ await fs5.rm(dir.path, { recursive: true, force: true });
33005
33661
  total -= dir.size;
33006
33662
  } catch (err) {
33007
33663
  log2?.warn?.(`agent-core: failed to prune attachment cache dir ${dir.path}: ${String(err)}`);
@@ -33010,12 +33666,12 @@ async function pruneAttachmentCache(rootDir, maxBytes, log2, preserveDirs) {
33010
33666
  }
33011
33667
  async function directorySize(dirPath) {
33012
33668
  let total = 0;
33013
- const entries = await fs4.readdir(dirPath, { withFileTypes: true });
33669
+ const entries = await fs5.readdir(dirPath, { withFileTypes: true });
33014
33670
  for (const entry of entries) {
33015
- const fullPath = path5.join(dirPath, entry.name);
33671
+ const fullPath = path6.join(dirPath, entry.name);
33016
33672
  let stat;
33017
33673
  try {
33018
- stat = await fs4.lstat(fullPath);
33674
+ stat = await fs5.lstat(fullPath);
33019
33675
  } catch {
33020
33676
  continue;
33021
33677
  }
@@ -33030,10 +33686,10 @@ async function directorySize(dirPath) {
33030
33686
  return total;
33031
33687
  }
33032
33688
  function activeDirsForRoot(rootDir) {
33033
- const root = path5.resolve(rootDir);
33689
+ const root = path6.resolve(rootDir);
33034
33690
  const dirs = /* @__PURE__ */ new Set();
33035
33691
  for (const dir of activeAttachmentDirs) {
33036
- if (dir === root || dir.startsWith(`${root}${path5.sep}`)) {
33692
+ if (dir === root || dir.startsWith(`${root}${path6.sep}`)) {
33037
33693
  dirs.add(dir);
33038
33694
  }
33039
33695
  }
@@ -33130,9 +33786,9 @@ async function writeResponseToFileWithLimit(res, filePath, maxBytes, rootDir) {
33130
33786
  }
33131
33787
  writtenStat = await file.stat();
33132
33788
  await closeFile();
33133
- await localDirectoryStatInsideRoot(path5.dirname(filePath), rootDir);
33789
+ await localDirectoryStatInsideRoot(path6.dirname(filePath), rootDir);
33134
33790
  await assertLocalFileIdentity(tmpPath, rootDir, writtenStat);
33135
- await fs4.rename(tmpPath, filePath);
33791
+ await fs5.rename(tmpPath, filePath);
33136
33792
  completed = true;
33137
33793
  return written;
33138
33794
  } finally {
@@ -33148,9 +33804,9 @@ async function writeResponseToFileWithLimit(res, filePath, maxBytes, rootDir) {
33148
33804
  }
33149
33805
  }
33150
33806
  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;
33807
+ const safeName = sanitizePathSegment(path6.basename(fileName || attachmentId));
33808
+ const ext = path6.extname(safeName) || extensionForMime(mimeType);
33809
+ const stem = path6.basename(safeName, path6.extname(safeName)) || attachmentId;
33154
33810
  return `${sanitizePathSegment(attachmentId)}-${stem}${ext}`;
33155
33811
  }
33156
33812
  function extensionForMime(mimeType) {
@@ -33933,6 +34589,9 @@ var CodexAppServerAdapter = class {
33933
34589
  if (this.opts.contextFilePath) {
33934
34590
  env.PRLL_CONTEXT_FILE = this.opts.contextFilePath;
33935
34591
  }
34592
+ if (this.opts.contextDirPath) {
34593
+ env.PRLL_CONTEXT_DIR = this.opts.contextDirPath;
34594
+ }
33936
34595
  if (!env.PRLL_WIKI_MOUNT_ROOT?.trim()) {
33937
34596
  env.PRLL_WIKI_MOUNT_ROOT = this.opts.workspaceDir;
33938
34597
  }
@@ -34078,7 +34737,7 @@ var TurnSink = class {
34078
34737
  }
34079
34738
  };
34080
34739
  function ensureGitRepo(workingDirectory) {
34081
- fs5.mkdirSync(workingDirectory, { recursive: true });
34740
+ fs6.mkdirSync(workingDirectory, { recursive: true });
34082
34741
  try {
34083
34742
  execSync2("git rev-parse --is-inside-work-tree", { cwd: workingDirectory, stdio: "pipe" });
34084
34743
  ensureLocalAttachmentGitExclude(workingDirectory);
@@ -34148,8 +34807,8 @@ function errToString(err) {
34148
34807
  }
34149
34808
 
34150
34809
  // ts/codex-agent/dist/session-manager.js
34151
- import * as fs6 from "node:fs";
34152
- import * as path6 from "node:path";
34810
+ import * as fs7 from "node:fs";
34811
+ import * as path7 from "node:path";
34153
34812
  var CodexSessionManager = class {
34154
34813
  mainSessionKey;
34155
34814
  stateFilePath;
@@ -34186,14 +34845,14 @@ var CodexSessionManager = class {
34186
34845
  clearMainThread() {
34187
34846
  this.threadIds.delete(this.mainSessionKey);
34188
34847
  try {
34189
- fs6.rmSync(this.stateFilePath, { force: true });
34848
+ fs7.rmSync(this.stateFilePath, { force: true });
34190
34849
  } catch (error) {
34191
34850
  this.logger?.warn(`codex-agent: failed to clear main thread state at ${this.stateFilePath}: ${String(error)}`);
34192
34851
  }
34193
34852
  }
34194
34853
  restore() {
34195
34854
  try {
34196
- const raw = fs6.readFileSync(this.stateFilePath, "utf8");
34855
+ const raw = fs7.readFileSync(this.stateFilePath, "utf8");
34197
34856
  const parsed = JSON.parse(raw);
34198
34857
  if (parsed.runtimeKey === this.mainSessionKey && typeof parsed.threadId === "string" && parsed.threadId.trim()) {
34199
34858
  this.threadIds.set(this.mainSessionKey, parsed.threadId.trim());
@@ -34206,10 +34865,10 @@ var CodexSessionManager = class {
34206
34865
  }
34207
34866
  persist(threadId) {
34208
34867
  try {
34209
- fs6.mkdirSync(path6.dirname(this.stateFilePath), { recursive: true });
34868
+ fs7.mkdirSync(path7.dirname(this.stateFilePath), { recursive: true });
34210
34869
  const tmpPath = `${this.stateFilePath}.tmp`;
34211
- fs6.writeFileSync(tmpPath, JSON.stringify({ runtimeKey: this.mainSessionKey, threadId }, null, 2));
34212
- fs6.renameSync(tmpPath, this.stateFilePath);
34870
+ fs7.writeFileSync(tmpPath, JSON.stringify({ runtimeKey: this.mainSessionKey, threadId }, null, 2));
34871
+ fs7.renameSync(tmpPath, this.stateFilePath);
34213
34872
  } catch (error) {
34214
34873
  this.logger?.warn(`codex-agent: failed to persist thread state at ${this.stateFilePath}: ${String(error)}`);
34215
34874
  }
@@ -34217,8 +34876,8 @@ var CodexSessionManager = class {
34217
34876
  };
34218
34877
 
34219
34878
  // ts/codex-agent/dist/workspace.js
34220
- import * as fs7 from "node:fs";
34221
- import * as path7 from "node:path";
34879
+ import * as fs8 from "node:fs";
34880
+ import * as path8 from "node:path";
34222
34881
 
34223
34882
  // ts/node_modules/.pnpm/smol-toml@1.6.1/node_modules/smol-toml/dist/error.js
34224
34883
  function getLineColFromPtr(string, ptr) {
@@ -34912,12 +35571,12 @@ function parse(toml, { maxDepth = 1e3, integersAsBigInt } = {}) {
34912
35571
 
34913
35572
  // ts/codex-agent/dist/workspace.js
34914
35573
  function ensureWorkspaceTrusted(codexHome, workspaceDir, log2) {
34915
- const configPath = path7.join(codexHome, "config.toml");
34916
- const normalizedPath = path7.resolve(workspaceDir);
35574
+ const configPath = path8.join(codexHome, "config.toml");
35575
+ const normalizedPath = path8.resolve(workspaceDir);
34917
35576
  try {
34918
35577
  let content = "";
34919
35578
  try {
34920
- content = fs7.readFileSync(configPath, "utf8");
35579
+ content = fs8.readFileSync(configPath, "utf8");
34921
35580
  } catch (err) {
34922
35581
  if (err.code !== "ENOENT")
34923
35582
  throw err;
@@ -34957,14 +35616,14 @@ ${trustLine}
34957
35616
  content = content.substring(0, afterHeader) + trustLine + "\n" + content.substring(afterHeader);
34958
35617
  }
34959
35618
  }
34960
- fs7.writeFileSync(configPath, content, "utf8");
35619
+ fs8.writeFileSync(configPath, content, "utf8");
34961
35620
  } else if (projects?.[normalizedPath] !== void 0) {
34962
35621
  log2?.warn(`Codex config.toml has non-canonical header for ${normalizedPath}; skipping trust write`);
34963
35622
  } else if (projects && !content.includes("[projects.")) {
34964
35623
  log2?.warn(`Codex config.toml uses inline table for projects; skipping trust write for ${normalizedPath}`);
34965
35624
  } else {
34966
- fs7.mkdirSync(codexHome, { recursive: true });
34967
- fs7.appendFileSync(configPath, `
35625
+ fs8.mkdirSync(codexHome, { recursive: true });
35626
+ fs8.appendFileSync(configPath, `
34968
35627
  ${sectionHeader}
34969
35628
  ${trustLine}
34970
35629
  `, "utf8");
@@ -34981,12 +35640,12 @@ function isParallProxyMode(env = process.env) {
34981
35640
  return baseUrl === apiUrl || baseUrl.startsWith(`${apiUrl}/`);
34982
35641
  }
34983
35642
  function ensureParallProvider(codexHome, apiUrl, log2) {
34984
- const configPath = path7.join(codexHome, "config.toml");
35643
+ const configPath = path8.join(codexHome, "config.toml");
34985
35644
  const baseUrl = apiUrl.replace(/\/$/, "") + "/api/llm/v1";
34986
35645
  try {
34987
35646
  let content = "";
34988
35647
  try {
34989
- content = fs7.readFileSync(configPath, "utf8");
35648
+ content = fs8.readFileSync(configPath, "utf8");
34990
35649
  } catch (err) {
34991
35650
  if (err.code !== "ENOENT")
34992
35651
  throw err;
@@ -35017,8 +35676,8 @@ function ensureParallProvider(codexHome, apiUrl, log2) {
35017
35676
  } else {
35018
35677
  content = content.trimEnd() + "\n\n" + providerBlock + "\n";
35019
35678
  }
35020
- fs7.mkdirSync(codexHome, { recursive: true });
35021
- fs7.writeFileSync(configPath, content, "utf8");
35679
+ fs8.mkdirSync(codexHome, { recursive: true });
35680
+ fs8.writeFileSync(configPath, content, "utf8");
35022
35681
  } catch (err) {
35023
35682
  throw new Error(`failed to write Parall provider config: ${String(err)}`);
35024
35683
  }
@@ -35035,18 +35694,18 @@ function ensureCodexWorkspace(workspaceDir, log2, agentIdentity) {
35035
35694
  PRLL_REFERENCE_GUIDE,
35036
35695
  buildSkillReferences(workspaceDir)
35037
35696
  ].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 });
35697
+ fs8.mkdirSync(workspaceDir, { recursive: true });
35698
+ const parallDir = path8.join(workspaceDir, ".parall");
35699
+ fs8.mkdirSync(parallDir, { recursive: true });
35700
+ fs8.writeFileSync(path8.join(parallDir, "system-prompt.md"), systemPrompt, "utf8");
35701
+ writeSkillFiles(path8.join(parallDir, "skills"));
35702
+ const codexConfigDir = path8.join(workspaceDir, ".codex");
35703
+ fs8.mkdirSync(codexConfigDir, { recursive: true });
35045
35704
  const toml = `developer_instructions = """
35046
35705
  ${systemPrompt.replace(/\\/g, "\\\\").replace(/"""/g, '\\"""')}
35047
35706
  """
35048
35707
  `;
35049
- fs7.writeFileSync(path7.join(codexConfigDir, "config.toml"), toml, "utf8");
35708
+ fs8.writeFileSync(path8.join(codexConfigDir, "config.toml"), toml, "utf8");
35050
35709
  }
35051
35710
 
35052
35711
  // ts/codex-agent/dist/index.js
@@ -35139,6 +35798,7 @@ async function main() {
35139
35798
  sessionManager,
35140
35799
  log: agentLog,
35141
35800
  contextFilePath: mainContextFilePath,
35801
+ contextDirPath: dispatchContextDirPath(config.stateDir),
35142
35802
  useParallProvider
35143
35803
  });
35144
35804
  const refreshPlatformConfig = async () => {
@@ -35184,6 +35844,7 @@ async function main() {
35184
35844
  dispatchDeadlineMs: parseDispatchDeadlineMs(process.env.PRLL_DISPATCH_DEADLINE_MS),
35185
35845
  contextFilePathForSession: (sessionKey) => contextFilePathForSession(config.stateDir, sessionKey),
35186
35846
  stepIdFilePathForSession: (sessionKey) => stepIdFilePathForSession(config.stateDir, sessionKey),
35847
+ dispatchContextDir: dispatchContextDirPath(config.stateDir),
35187
35848
  onConfigUpdate: refreshPlatformConfig,
35188
35849
  onSessionReady: refreshPlatformConfig,
35189
35850
  onNewSession: () => {