@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 path9 of paths) {
17721
+ for (const path10 of paths) {
17722
17722
  try {
17723
- const result = await fs_1.promises.readFile(path9, { encoding: "utf8" });
17723
+ const result = await fs_1.promises.readFile(path10, { encoding: "utf8" });
17724
17724
  return result.trim();
17725
17725
  } catch (e) {
17726
17726
  api_1.diag.debug(`error reading machine id: ${e}`);
@@ -21123,7 +21123,7 @@ function appendRootPathToUrlIfNeeded(url) {
21123
21123
  return void 0;
21124
21124
  }
21125
21125
  }
21126
- function appendResourcePathToUrl(url, path9) {
21126
+ function appendResourcePathToUrl(url, path10) {
21127
21127
  try {
21128
21128
  new URL(url);
21129
21129
  } catch (_a) {
@@ -21133,11 +21133,11 @@ function appendResourcePathToUrl(url, path9) {
21133
21133
  if (!url.endsWith("/")) {
21134
21134
  url = url + "/";
21135
21135
  }
21136
- url += path9;
21136
+ url += path10;
21137
21137
  try {
21138
21138
  new URL(url);
21139
21139
  } catch (_b) {
21140
- diag2.warn("Configuration: Provided URL appended with '" + path9 + "' is not a valid URL, using 'undefined' instead of '" + url + "'");
21140
+ diag2.warn("Configuration: Provided URL appended with '" + path10 + "' is not a valid URL, using 'undefined' instead of '" + url + "'");
21141
21141
  return void 0;
21142
21142
  }
21143
21143
  return url;
@@ -26474,6 +26474,19 @@ function parseProviderConfig(env) {
26474
26474
  }
26475
26475
  }
26476
26476
 
26477
+ // ts/agent-core/dist/lane-key.js
26478
+ import * as path from "node:path";
26479
+ function laneKeyForTarget(targetUri, threadRootId) {
26480
+ return Buffer.from(`${targetUri}
26481
+ ${threadRootId ?? ""}`, "utf8").toString("base64url");
26482
+ }
26483
+ function dispatchLaneContextDir(stateDir) {
26484
+ return path.join(stateDir, "dispatch-lane-context");
26485
+ }
26486
+ function laneContextFilePath(contextDir, targetUri, threadRootId) {
26487
+ return path.join(contextDir, `${laneKeyForTarget(targetUri, threadRootId)}.json`);
26488
+ }
26489
+
26477
26490
  // ts/agent-core/dist/session-state.js
26478
26491
  function normalizeSessionKey(sessionKey) {
26479
26492
  return sessionKey.toLowerCase();
@@ -27102,8 +27115,8 @@ function childLogger(logger, sub) {
27102
27115
 
27103
27116
  // ts/agent-core/dist/gateway-base.js
27104
27117
  import * as os from "node:os";
27105
- import * as fs from "node:fs";
27106
- import * as path from "node:path";
27118
+ import * as fs2 from "node:fs";
27119
+ import * as path2 from "node:path";
27107
27120
 
27108
27121
  // ts/sdk/dist/types.js
27109
27122
  var MENTION_ALL_USER_ID = "all";
@@ -27378,6 +27391,11 @@ var ENDPOINTS = {
27378
27391
  DISPATCH_ACK_BY_ID: (orgId, id) => `${API_BASE}/orgs/${orgId}/dispatch/${id}/ack`,
27379
27392
  DISPATCH_EXPIRE: (orgId) => `${API_BASE}/orgs/${orgId}/dispatch/expire`,
27380
27393
  DISPATCH_BY_MESSAGES: (orgId) => `${API_BASE}/orgs/${orgId}/dispatch/by-messages`,
27394
+ DISPATCH_CLAIM: (orgId) => `${API_BASE}/orgs/${orgId}/dispatch/claim`,
27395
+ DISPATCH_STEER: (orgId) => `${API_BASE}/orgs/${orgId}/dispatch/steer`,
27396
+ DISPATCH_COMPLETE: (orgId) => `${API_BASE}/orgs/${orgId}/dispatch/complete`,
27397
+ DISPATCH_RELEASE: (orgId) => `${API_BASE}/orgs/${orgId}/dispatch/release`,
27398
+ DISPATCH_HEARTBEAT: (orgId) => `${API_BASE}/orgs/${orgId}/dispatch/heartbeat`,
27381
27399
  // Unread
27382
27400
  UNREAD: `${API_BASE}/me/unread`,
27383
27401
  ORG_UNREAD: (orgId) => `${API_BASE}/orgs/${orgId}/unread`,
@@ -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(path9) {
27597
- return path9.startsWith(WIKI_BASE) ? this.wikiBaseUrl : this.baseUrl;
27621
+ baseUrlFor(path10) {
27622
+ return path10.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(path9) {
27649
+ async ensureFreshToken(path10) {
27625
27650
  if (!this.token || !this.getRefreshToken)
27626
27651
  return;
27627
- const pathSuffix = path9.replace(/^\/api\/v1/, "");
27652
+ const pathSuffix = path10.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, path9, body, query, retried = false, opts) {
27684
+ async request(method, path10, body, query, retried = false, opts) {
27660
27685
  if (!retried) {
27661
- await this.ensureFreshToken(path9);
27686
+ await this.ensureFreshToken(path10);
27662
27687
  }
27663
- let url = `${this.baseUrlFor(path9)}${path9}`;
27688
+ let url = `${this.baseUrlFor(path10)}${path10}`;
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 = path9.replace(/^\/api\/v1/, "");
27718
+ const pathSuffix = path10.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, path9, body, query, true, opts);
27723
+ return this.request(method, path10, 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, path9, body, retried = false) {
27753
+ async multipartRequest(method, path10, body, retried = false) {
27728
27754
  if (!retried) {
27729
- await this.ensureFreshToken(path9);
27755
+ await this.ensureFreshToken(path10);
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(path9)}${path9}`, {
27761
+ res = await fetch(`${this.baseUrlFor(path10)}${path10}`, {
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 = path9.replace(/^\/api\/v1/, "");
27771
+ const pathSuffix = path10.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, path9, body, true);
27776
+ return this.multipartRequest(method, path10, 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, path9) {
28430
- return this.request("POST", ENDPOINTS.MACHINE_BROWSE(orgId, machineId), { path: path9 }, void 0, false, { timeoutMs: 15e3 });
28470
+ async browseMachineFilesystem(orgId, machineId, path10) {
28471
+ return this.request("POST", ENDPOINTS.MACHINE_BROWSE(orgId, machineId), { path: path10 }, 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, path9) {
28968
- return this.request("GET", ENDPOINTS.WIKI_ACCESS_STATUS(orgId, wikiId), void 0, path9 ? { path: path9 } : void 0);
29032
+ async getWikiAccessStatus(orgId, wikiId, path10) {
29033
+ return this.request("GET", ENDPOINTS.WIKI_ACCESS_STATUS(orgId, wikiId), void 0, path10 ? { path: path10 } : 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, path9, params) {
29042
+ async getWikiFileCommits(orgId, wikiId, path10, params) {
28978
29043
  return this.request("GET", ENDPOINTS.WIKI_FILE_COMMITS(orgId, wikiId), void 0, {
28979
- path: path9,
29044
+ path: path10,
28980
29045
  ...params
28981
29046
  });
28982
29047
  }
28983
- async getWikiBlame(orgId, wikiId, path9, ref) {
28984
- return this.request("GET", ENDPOINTS.WIKI_BLAME(orgId, wikiId), void 0, { path: path9, ref });
29048
+ async getWikiBlame(orgId, wikiId, path10, ref) {
29049
+ return this.request("GET", ENDPOINTS.WIKI_BLAME(orgId, wikiId), void 0, { path: path10, 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/claude-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) {
@@ -32568,15 +33221,15 @@ function parseList(value) {
32568
33221
  return value.split(/[,\n]/).map((item) => item.trim()).filter(Boolean);
32569
33222
  }
32570
33223
  function resolvePath(value) {
32571
- return path4.isAbsolute(value) ? value : path4.resolve(process.cwd(), value);
33224
+ return path5.isAbsolute(value) ? value : path5.resolve(process.cwd(), value);
32572
33225
  }
32573
33226
  function resolveClaudeAgentConfig(env = process.env) {
32574
33227
  const apiUrl = requireEnv(env, "PRLL_API_URL");
32575
33228
  const apiKey = requireEnv(env, "PRLL_API_KEY");
32576
33229
  const orgId = requireEnv(env, "PRLL_ORG_ID");
32577
33230
  const claudeHome = resolvePath(env.PRLL_CLAUDE_HOME?.trim() || env.HOME || os2.homedir());
32578
- const stateDir = resolvePath(env.PRLL_STATE_DIR?.trim() || path4.join(claudeHome, ".parall-agent"));
32579
- const workspaceDir = resolvePath(env.PRLL_WORKSPACE_DIR?.trim() || path4.join(stateDir, "workspace"));
33231
+ const stateDir = resolvePath(env.PRLL_STATE_DIR?.trim() || path5.join(claudeHome, ".parall-agent"));
33232
+ const workspaceDir = resolvePath(env.PRLL_WORKSPACE_DIR?.trim() || path5.join(stateDir, "workspace"));
32580
33233
  const additionalDirs = parseList(env.PRLL_CLAUDE_ADD_DIRS).map(resolvePath);
32581
33234
  return {
32582
33235
  apiUrl,
@@ -32611,20 +33264,23 @@ function buildClaudeRuntimeKey(agentUserId) {
32611
33264
  }
32612
33265
  function sessionStateFilePathForRuntime(stateDir, runtimeKey) {
32613
33266
  const fileName = Buffer.from(runtimeKey).toString("base64url");
32614
- return path4.join(stateDir, "sessions", `${fileName}.json`);
33267
+ return path5.join(stateDir, "sessions", `${fileName}.json`);
32615
33268
  }
32616
33269
  function contextFilePathForSession(stateDir, sessionKey) {
32617
33270
  const fileName = Buffer.from(sessionKey).toString("base64url");
32618
- return path4.join(stateDir, "dispatch-context", `${fileName}.json`);
33271
+ return path5.join(stateDir, "dispatch-context", `${fileName}.json`);
33272
+ }
33273
+ function dispatchContextDirPath(stateDir) {
33274
+ return dispatchLaneContextDir(stateDir);
32619
33275
  }
32620
33276
  function stepIdFilePathForSession(stateDir, sessionKey) {
32621
33277
  const fileName = Buffer.from(sessionKey).toString("base64url");
32622
- return path4.join(stateDir, "step-ids", `${fileName}.txt`);
33278
+ return path5.join(stateDir, "step-ids", `${fileName}.txt`);
32623
33279
  }
32624
33280
 
32625
33281
  // ts/claude-agent/dist/dispatch.js
32626
- import * as fs5 from "node:fs";
32627
- import * as path6 from "node:path";
33282
+ import * as fs6 from "node:fs";
33283
+ import * as path7 from "node:path";
32628
33284
  import { randomUUID } from "node:crypto";
32629
33285
  import { execSync as execSync2, spawn } from "node:child_process";
32630
33286
 
@@ -32632,8 +33288,8 @@ import { execSync as execSync2, spawn } from "node:child_process";
32632
33288
  import { execSync } from "node:child_process";
32633
33289
  import { constants } from "node:fs";
32634
33290
  import * as fsSync from "node:fs";
32635
- import * as fs4 from "node:fs/promises";
32636
- import * as path5 from "node:path";
33291
+ import * as fs5 from "node:fs/promises";
33292
+ import * as path6 from "node:path";
32637
33293
  var DEFAULT_MAX_TOTAL_IMAGE_BYTES = 20 * 1024 * 1024;
32638
33294
  var DEFAULT_ATTACHMENT_CACHE_MAX_BYTES = 512 * 1024 * 1024;
32639
33295
  var DEFAULT_ATTACHMENT_DOWNLOAD_TIMEOUT_MS = 3e4;
@@ -32663,11 +33319,11 @@ async function prepareLocalImageAttachments(event, context2, opts) {
32663
33319
  };
32664
33320
  }
32665
33321
  const rootDir = await ensureAttachmentRootDir(opts.workspaceDir);
32666
- const messageDir = path5.join(rootDir, sanitizePathSegment(event.messageId));
33322
+ const messageDir = path6.join(rootDir, sanitizePathSegment(event.messageId));
32667
33323
  await ensurePathIsNotSymlink(messageDir);
32668
- await fs4.mkdir(messageDir, { recursive: true });
33324
+ await fs5.mkdir(messageDir, { recursive: true });
32669
33325
  await ensurePathIsNotSymlink(messageDir);
32670
- const activeMessageDir = path5.resolve(messageDir);
33326
+ const activeMessageDir = path6.resolve(messageDir);
32671
33327
  activeAttachmentDirs.add(activeMessageDir);
32672
33328
  const maintenanceCooldownMs = opts.maintenanceCooldownMs ?? DEFAULT_MAINTENANCE_COOLDOWN_MS;
32673
33329
  const maintenancePromise = scheduleAttachmentMaintenance(rootDir, {
@@ -32684,7 +33340,7 @@ async function prepareLocalImageAttachments(event, context2, opts) {
32684
33340
  const notes = [];
32685
33341
  let downloadedBytes = 0;
32686
33342
  for (const att of imageAttachments) {
32687
- const localPath = path5.join(messageDir, localFileName(att.id, att.fileName, att.mimeType));
33343
+ const localPath = path6.join(messageDir, localFileName(att.id, att.fileName, att.mimeType));
32688
33344
  const downloadTimeoutMs = opts.downloadTimeoutMs ?? DEFAULT_ATTACHMENT_DOWNLOAD_TIMEOUT_MS;
32689
33345
  const fetchFresh = async () => {
32690
33346
  const fileInfo = await withTimeout(context2.client.getFileUrl(att.id), downloadTimeoutMs, `file URL lookup timed out after ${downloadTimeoutMs}ms`);
@@ -32741,7 +33397,7 @@ async function appendPreparedLocalAttachmentRefs(body, event, context2, opts) {
32741
33397
  return { body: appendLocalAttachmentRefs(body, attachments), attachments };
32742
33398
  }
32743
33399
  function pinLocalAttachmentPaths(images) {
32744
- const dirs = new Set(images.map((image) => path5.resolve(path5.dirname(image.localPath))));
33400
+ const dirs = new Set(images.map((image) => path6.resolve(path6.dirname(image.localPath))));
32745
33401
  for (const dir of dirs) {
32746
33402
  activeAttachmentDirs.add(dir);
32747
33403
  }
@@ -32756,7 +33412,7 @@ function pinLocalAttachmentPaths(images) {
32756
33412
  };
32757
33413
  }
32758
33414
  function attachmentRootDir(workspaceDir) {
32759
- return path5.join(path5.resolve(workspaceDir), ".parall", "attachments");
33415
+ return path6.join(path6.resolve(workspaceDir), ".parall", "attachments");
32760
33416
  }
32761
33417
  function ensureLocalAttachmentGitExclude(workingDirectory) {
32762
33418
  try {
@@ -32765,8 +33421,8 @@ function ensureLocalAttachmentGitExclude(workingDirectory) {
32765
33421
  encoding: "utf8",
32766
33422
  stdio: ["ignore", "pipe", "ignore"]
32767
33423
  }).trim();
32768
- const excludePath = path5.isAbsolute(rel) ? rel : path5.join(workingDirectory, rel);
32769
- fsSync.mkdirSync(path5.dirname(excludePath), { recursive: true });
33424
+ const excludePath = path6.isAbsolute(rel) ? rel : path6.join(workingDirectory, rel);
33425
+ fsSync.mkdirSync(path6.dirname(excludePath), { recursive: true });
32770
33426
  const existing = fsSync.existsSync(excludePath) ? fsSync.readFileSync(excludePath, "utf8") : "";
32771
33427
  if (existing.split(/\r?\n/).some((line) => line.trim() === ".parall/"))
32772
33428
  return;
@@ -32802,18 +33458,18 @@ function scheduleAttachmentMaintenance(rootDir, opts) {
32802
33458
  return run;
32803
33459
  }
32804
33460
  async function ensureAttachmentRootDir(workspaceDir) {
32805
- const workspaceRoot = path5.resolve(workspaceDir);
32806
- const parallDir = path5.join(workspaceRoot, ".parall");
33461
+ const workspaceRoot = path6.resolve(workspaceDir);
33462
+ const parallDir = path6.join(workspaceRoot, ".parall");
32807
33463
  const rootDir = attachmentRootDir(workspaceRoot);
32808
- await fs4.mkdir(workspaceRoot, { recursive: true });
33464
+ await fs5.mkdir(workspaceRoot, { recursive: true });
32809
33465
  await ensurePathIsNotSymlink(parallDir);
32810
- await fs4.mkdir(parallDir, { recursive: true, mode: 448 });
33466
+ await fs5.mkdir(parallDir, { recursive: true, mode: 448 });
32811
33467
  await ensurePathIsNotSymlink(parallDir);
32812
33468
  await ensurePathIsNotSymlink(rootDir);
32813
- await fs4.mkdir(rootDir, { recursive: true, mode: 448 });
33469
+ await fs5.mkdir(rootDir, { recursive: true, mode: 448 });
32814
33470
  await ensurePathIsNotSymlink(rootDir);
32815
- const realWorkspace = await fs4.realpath(workspaceRoot);
32816
- const realRoot = await fs4.realpath(rootDir);
33471
+ const realWorkspace = await fs5.realpath(workspaceRoot);
33472
+ const realRoot = await fs5.realpath(rootDir);
32817
33473
  if (!isPathInside(realRoot, realWorkspace)) {
32818
33474
  throw new Error(`attachment root escapes workspace: ${rootDir}`);
32819
33475
  }
@@ -32821,7 +33477,7 @@ async function ensureAttachmentRootDir(workspaceDir) {
32821
33477
  }
32822
33478
  async function ensurePathIsNotSymlink(filePath) {
32823
33479
  try {
32824
- const stat = await fs4.lstat(filePath);
33480
+ const stat = await fs5.lstat(filePath);
32825
33481
  if (stat.isSymbolicLink()) {
32826
33482
  throw new Error(`refusing to use symlinked attachment path ${filePath}`);
32827
33483
  }
@@ -32832,8 +33488,8 @@ async function ensurePathIsNotSymlink(filePath) {
32832
33488
  }
32833
33489
  }
32834
33490
  function isPathInside(childPath, parentPath) {
32835
- const rel = path5.relative(parentPath, childPath);
32836
- return rel === "" || !!rel && !rel.startsWith("..") && !path5.isAbsolute(rel);
33491
+ const rel = path6.relative(parentPath, childPath);
33492
+ return rel === "" || !!rel && !rel.startsWith("..") && !path6.isAbsolute(rel);
32837
33493
  }
32838
33494
  async function existingUsableFile(filePath, expectedSize, rootDir) {
32839
33495
  try {
@@ -32844,30 +33500,30 @@ async function existingUsableFile(filePath, expectedSize, rootDir) {
32844
33500
  }
32845
33501
  }
32846
33502
  async function localFileStatInsideRoot(filePath, rootDir) {
32847
- const stat = await fs4.lstat(filePath);
33503
+ const stat = await fs5.lstat(filePath);
32848
33504
  if (stat.isSymbolicLink()) {
32849
33505
  throw new Error(`refusing to use symlinked attachment file ${filePath}`);
32850
33506
  }
32851
33507
  if (!stat.isFile()) {
32852
33508
  throw new Error(`attachment path is not a file ${filePath}`);
32853
33509
  }
32854
- const realRoot = await fs4.realpath(rootDir);
32855
- const realFile = await fs4.realpath(filePath);
33510
+ const realRoot = await fs5.realpath(rootDir);
33511
+ const realFile = await fs5.realpath(filePath);
32856
33512
  if (!isPathInside(realFile, realRoot)) {
32857
33513
  throw new Error(`attachment file escapes workspace: ${filePath}`);
32858
33514
  }
32859
33515
  return stat;
32860
33516
  }
32861
33517
  async function localDirectoryStatInsideRoot(dirPath, rootDir) {
32862
- const stat = await fs4.lstat(dirPath);
33518
+ const stat = await fs5.lstat(dirPath);
32863
33519
  if (stat.isSymbolicLink()) {
32864
33520
  throw new Error(`refusing to use symlinked attachment directory ${dirPath}`);
32865
33521
  }
32866
33522
  if (!stat.isDirectory()) {
32867
33523
  throw new Error(`attachment path is not a directory ${dirPath}`);
32868
33524
  }
32869
- const realRoot = await fs4.realpath(rootDir);
32870
- const realDir = await fs4.realpath(dirPath);
33525
+ const realRoot = await fs5.realpath(rootDir);
33526
+ const realDir = await fs5.realpath(dirPath);
32871
33527
  if (!isPathInside(realDir, realRoot)) {
32872
33528
  throw new Error(`attachment directory escapes workspace: ${dirPath}`);
32873
33529
  }
@@ -32875,7 +33531,7 @@ async function localDirectoryStatInsideRoot(dirPath, rootDir) {
32875
33531
  }
32876
33532
  async function openLocalFileInsideRoot(filePath, rootDir) {
32877
33533
  const checkedStat = await localFileStatInsideRoot(filePath, rootDir);
32878
- const file = await fs4.open(filePath, constants.O_RDONLY | constants.O_NOFOLLOW);
33534
+ const file = await fs5.open(filePath, constants.O_RDONLY | constants.O_NOFOLLOW);
32879
33535
  let keepOpen = false;
32880
33536
  try {
32881
33537
  const openedStat = await file.stat();
@@ -32891,8 +33547,8 @@ async function openLocalFileInsideRoot(filePath, rootDir) {
32891
33547
  }
32892
33548
  }
32893
33549
  async function openLocalTempFileInsideRoot(filePath, rootDir) {
32894
- await localDirectoryStatInsideRoot(path5.dirname(filePath), rootDir);
32895
- const file = await fs4.open(filePath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
33550
+ await localDirectoryStatInsideRoot(path6.dirname(filePath), rootDir);
33551
+ const file = await fs5.open(filePath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
32896
33552
  let keepOpen = false;
32897
33553
  try {
32898
33554
  const checkedStat = await localFileStatInsideRoot(filePath, rootDir);
@@ -32917,7 +33573,7 @@ async function assertLocalFileIdentity(filePath, rootDir, expected) {
32917
33573
  async function removeLocalFileIfInside(filePath, rootDir) {
32918
33574
  try {
32919
33575
  await localFileStatInsideRoot(filePath, rootDir);
32920
- await fs4.rm(filePath, { force: true });
33576
+ await fs5.rm(filePath, { force: true });
32921
33577
  } catch {
32922
33578
  }
32923
33579
  }
@@ -32930,7 +33586,7 @@ function sameFile(a, b) {
32930
33586
  async function cleanupOldAttachmentFiles(rootDir, ttlMs, log2, preserveDirs) {
32931
33587
  let entries;
32932
33588
  try {
32933
- entries = await fs4.readdir(rootDir, { withFileTypes: true });
33589
+ entries = await fs5.readdir(rootDir, { withFileTypes: true });
32934
33590
  } catch {
32935
33591
  return;
32936
33592
  }
@@ -32938,15 +33594,15 @@ async function cleanupOldAttachmentFiles(rootDir, ttlMs, log2, preserveDirs) {
32938
33594
  await Promise.all(entries.map(async (entry) => {
32939
33595
  if (!entry.isDirectory())
32940
33596
  return;
32941
- const fullPath = path5.join(rootDir, entry.name);
33597
+ const fullPath = path6.join(rootDir, entry.name);
32942
33598
  try {
32943
- if (preserveDirs?.has(path5.resolve(fullPath)))
33599
+ if (preserveDirs?.has(path6.resolve(fullPath)))
32944
33600
  return;
32945
- const stat = await fs4.lstat(fullPath);
33601
+ const stat = await fs5.lstat(fullPath);
32946
33602
  if (!stat.isDirectory())
32947
33603
  return;
32948
33604
  if (stat.mtimeMs < cutoff) {
32949
- await fs4.rm(fullPath, { recursive: true, force: true });
33605
+ await fs5.rm(fullPath, { recursive: true, force: true });
32950
33606
  }
32951
33607
  } catch (err) {
32952
33608
  log2?.warn?.(`agent-core: failed to clean attachment temp dir ${fullPath}: ${String(err)}`);
@@ -32958,7 +33614,7 @@ async function pruneAttachmentCache(rootDir, maxBytes, log2, preserveDirs) {
32958
33614
  return;
32959
33615
  let entries;
32960
33616
  try {
32961
- entries = await fs4.readdir(rootDir, { withFileTypes: true });
33617
+ entries = await fs5.readdir(rootDir, { withFileTypes: true });
32962
33618
  } catch {
32963
33619
  return;
32964
33620
  }
@@ -32967,9 +33623,9 @@ async function pruneAttachmentCache(rootDir, maxBytes, log2, preserveDirs) {
32967
33623
  for (const entry of entries) {
32968
33624
  if (!entry.isDirectory())
32969
33625
  continue;
32970
- const fullPath = path5.join(rootDir, entry.name);
33626
+ const fullPath = path6.join(rootDir, entry.name);
32971
33627
  try {
32972
- const stat = await fs4.lstat(fullPath);
33628
+ const stat = await fs5.lstat(fullPath);
32973
33629
  if (!stat.isDirectory())
32974
33630
  continue;
32975
33631
  const size = await directorySize(fullPath);
@@ -32985,10 +33641,10 @@ async function pruneAttachmentCache(rootDir, maxBytes, log2, preserveDirs) {
32985
33641
  for (const dir of dirs) {
32986
33642
  if (total <= maxBytes)
32987
33643
  break;
32988
- if (preserveDirs?.has(path5.resolve(dir.path)))
33644
+ if (preserveDirs?.has(path6.resolve(dir.path)))
32989
33645
  continue;
32990
33646
  try {
32991
- await fs4.rm(dir.path, { recursive: true, force: true });
33647
+ await fs5.rm(dir.path, { recursive: true, force: true });
32992
33648
  total -= dir.size;
32993
33649
  } catch (err) {
32994
33650
  log2?.warn?.(`agent-core: failed to prune attachment cache dir ${dir.path}: ${String(err)}`);
@@ -32997,12 +33653,12 @@ async function pruneAttachmentCache(rootDir, maxBytes, log2, preserveDirs) {
32997
33653
  }
32998
33654
  async function directorySize(dirPath) {
32999
33655
  let total = 0;
33000
- const entries = await fs4.readdir(dirPath, { withFileTypes: true });
33656
+ const entries = await fs5.readdir(dirPath, { withFileTypes: true });
33001
33657
  for (const entry of entries) {
33002
- const fullPath = path5.join(dirPath, entry.name);
33658
+ const fullPath = path6.join(dirPath, entry.name);
33003
33659
  let stat;
33004
33660
  try {
33005
- stat = await fs4.lstat(fullPath);
33661
+ stat = await fs5.lstat(fullPath);
33006
33662
  } catch {
33007
33663
  continue;
33008
33664
  }
@@ -33017,10 +33673,10 @@ async function directorySize(dirPath) {
33017
33673
  return total;
33018
33674
  }
33019
33675
  function activeDirsForRoot(rootDir) {
33020
- const root = path5.resolve(rootDir);
33676
+ const root = path6.resolve(rootDir);
33021
33677
  const dirs = /* @__PURE__ */ new Set();
33022
33678
  for (const dir of activeAttachmentDirs) {
33023
- if (dir === root || dir.startsWith(`${root}${path5.sep}`)) {
33679
+ if (dir === root || dir.startsWith(`${root}${path6.sep}`)) {
33024
33680
  dirs.add(dir);
33025
33681
  }
33026
33682
  }
@@ -33117,9 +33773,9 @@ async function writeResponseToFileWithLimit(res, filePath, maxBytes, rootDir) {
33117
33773
  }
33118
33774
  writtenStat = await file.stat();
33119
33775
  await closeFile();
33120
- await localDirectoryStatInsideRoot(path5.dirname(filePath), rootDir);
33776
+ await localDirectoryStatInsideRoot(path6.dirname(filePath), rootDir);
33121
33777
  await assertLocalFileIdentity(tmpPath, rootDir, writtenStat);
33122
- await fs4.rename(tmpPath, filePath);
33778
+ await fs5.rename(tmpPath, filePath);
33123
33779
  completed = true;
33124
33780
  return written;
33125
33781
  } finally {
@@ -33135,9 +33791,9 @@ async function writeResponseToFileWithLimit(res, filePath, maxBytes, rootDir) {
33135
33791
  }
33136
33792
  }
33137
33793
  function localFileName(attachmentId, fileName, mimeType) {
33138
- const safeName = sanitizePathSegment(path5.basename(fileName || attachmentId));
33139
- const ext = path5.extname(safeName) || extensionForMime(mimeType);
33140
- const stem = path5.basename(safeName, path5.extname(safeName)) || attachmentId;
33794
+ const safeName = sanitizePathSegment(path6.basename(fileName || attachmentId));
33795
+ const ext = path6.extname(safeName) || extensionForMime(mimeType);
33796
+ const stem = path6.basename(safeName, path6.extname(safeName)) || attachmentId;
33141
33797
  return `${sanitizePathSegment(attachmentId)}-${stem}${ext}`;
33142
33798
  }
33143
33799
  function extensionForMime(mimeType) {
@@ -33417,7 +34073,11 @@ function buildSpawnEnv(parentEnv, claudeHome, context2, opts) {
33417
34073
  PRLL_TRIGGER_MESSAGE_ID: context2.triggerMessageId ?? "",
33418
34074
  PRLL_NO_REPLY: context2.noReply ? "1" : "",
33419
34075
  PRLL_CONTEXT_FILE: context2.contextFilePath ?? "",
33420
- PRLL_STEP_ID_FILE: context2.stepIdFilePath ?? ""
34076
+ PRLL_STEP_ID_FILE: context2.stepIdFilePath ?? "",
34077
+ // Per-lane dispatch context directory (stable for the bridge's lifetime,
34078
+ // so pinning it at spawn is safe even for long-lived subprocesses). The
34079
+ // CLI keys into it by send target to derive dispatch effect keys.
34080
+ PRLL_CONTEXT_DIR: context2.contextDirPath ?? ""
33421
34081
  };
33422
34082
  if (!result.PRLL_WIKI_MOUNT_ROOT?.trim() && opts.wikiMountRoot) {
33423
34083
  result.PRLL_WIKI_MOUNT_ROOT = opts.wikiMountRoot;
@@ -33523,8 +34183,8 @@ var ClaudeCodeAdapter = class {
33523
34183
  if (!sessionId)
33524
34184
  return void 0;
33525
34185
  const projectSlug = this.opts.workspaceDir.replace(/[/.]/g, "-");
33526
- const filePath = path6.join(this.opts.claudeHome, ".claude", "projects", projectSlug, `${sessionId}.jsonl`);
33527
- return fs5.existsSync(filePath) ? filePath : void 0;
34186
+ const filePath = path7.join(this.opts.claudeHome, ".claude", "projects", projectSlug, `${sessionId}.jsonl`);
34187
+ return fs6.existsSync(filePath) ? filePath : void 0;
33528
34188
  }
33529
34189
  forkSession({ sessionKey }) {
33530
34190
  return this.opts.sessionManager.createForkSession(sessionKey);
@@ -33810,7 +34470,7 @@ var ClaudeCodeAdapter = class {
33810
34470
  if (this.opts.disallowedTools.length > 0) {
33811
34471
  args.push("--disallowedTools", this.opts.disallowedTools.join(","));
33812
34472
  }
33813
- args.push("--append-system-prompt-file", path6.join(this.opts.workspaceDir, ".parall", "system-prompt.md"));
34473
+ args.push("--append-system-prompt-file", path7.join(this.opts.workspaceDir, ".parall", "system-prompt.md"));
33814
34474
  if (this.opts.appendSystemPrompt) {
33815
34475
  args.push("--append-system-prompt", this.opts.appendSystemPrompt);
33816
34476
  }
@@ -33835,14 +34495,15 @@ var ClaudeCodeAdapter = class {
33835
34495
  noReply: false,
33836
34496
  contextFilePath: this.opts.contextFilePathForSession?.(sessionKey) ?? "",
33837
34497
  stepIdFilePath: this.opts.stepIdFilePathForSession?.(sessionKey) ?? "",
34498
+ contextDirPath: this.opts.contextDirPath ?? "",
33838
34499
  client: void 0
33839
34500
  };
33840
34501
  }
33841
34502
  };
33842
34503
 
33843
34504
  // ts/claude-agent/dist/session-manager.js
33844
- import * as fs6 from "node:fs";
33845
- import * as path7 from "node:path";
34505
+ import * as fs7 from "node:fs";
34506
+ import * as path8 from "node:path";
33846
34507
  import { randomUUID as randomUUID2 } from "node:crypto";
33847
34508
  var ClaudeSessionManager = class _ClaudeSessionManager {
33848
34509
  mainSessionKey;
@@ -33991,13 +34652,13 @@ var ClaudeSessionManager = class _ClaudeSessionManager {
33991
34652
  clearMainSession() {
33992
34653
  this.sessionIds.delete(this.mainSessionKey);
33993
34654
  try {
33994
- fs6.unlinkSync(this.stateFilePath);
34655
+ fs7.unlinkSync(this.stateFilePath);
33995
34656
  } catch {
33996
34657
  }
33997
34658
  }
33998
34659
  restore() {
33999
34660
  try {
34000
- const raw = fs6.readFileSync(this.stateFilePath, "utf8");
34661
+ const raw = fs7.readFileSync(this.stateFilePath, "utf8");
34001
34662
  const parsed = JSON.parse(raw);
34002
34663
  if (parsed.runtimeKey === this.mainSessionKey && typeof parsed.sessionId === "string" && parsed.sessionId.trim()) {
34003
34664
  this.sessionIds.set(this.mainSessionKey, parsed.sessionId.trim());
@@ -34007,8 +34668,8 @@ var ClaudeSessionManager = class _ClaudeSessionManager {
34007
34668
  }
34008
34669
  persist(sessionId) {
34009
34670
  try {
34010
- fs6.mkdirSync(path7.dirname(this.stateFilePath), { recursive: true });
34011
- fs6.writeFileSync(this.stateFilePath, JSON.stringify({
34671
+ fs7.mkdirSync(path8.dirname(this.stateFilePath), { recursive: true });
34672
+ fs7.writeFileSync(this.stateFilePath, JSON.stringify({
34012
34673
  runtimeKey: this.mainSessionKey,
34013
34674
  sessionId
34014
34675
  }, null, 2));
@@ -34019,8 +34680,8 @@ var ClaudeSessionManager = class _ClaudeSessionManager {
34019
34680
  };
34020
34681
 
34021
34682
  // ts/claude-agent/dist/workspace.js
34022
- import * as fs7 from "node:fs";
34023
- import * as path8 from "node:path";
34683
+ import * as fs8 from "node:fs";
34684
+ import * as path9 from "node:path";
34024
34685
  function ensureClaudeWorkspace(workspaceDir, log2, agentIdentity) {
34025
34686
  const systemPrompt = [
34026
34687
  buildIdentity(agentIdentity),
@@ -34029,12 +34690,12 @@ function ensureClaudeWorkspace(workspaceDir, log2, agentIdentity) {
34029
34690
  PRLL_REFERENCE_GUIDE,
34030
34691
  buildSkillReferences(workspaceDir)
34031
34692
  ].join("\n\n");
34032
- fs7.mkdirSync(workspaceDir, { recursive: true });
34033
- fs7.mkdirSync(path8.join(workspaceDir, ".claude"), { recursive: true });
34034
- const parallDir = path8.join(workspaceDir, ".parall");
34035
- fs7.mkdirSync(parallDir, { recursive: true });
34036
- fs7.writeFileSync(path8.join(parallDir, "system-prompt.md"), systemPrompt, "utf8");
34037
- writeSkillFiles(path8.join(parallDir, "skills"));
34693
+ fs8.mkdirSync(workspaceDir, { recursive: true });
34694
+ fs8.mkdirSync(path9.join(workspaceDir, ".claude"), { recursive: true });
34695
+ const parallDir = path9.join(workspaceDir, ".parall");
34696
+ fs8.mkdirSync(parallDir, { recursive: true });
34697
+ fs8.writeFileSync(path9.join(parallDir, "system-prompt.md"), systemPrompt, "utf8");
34698
+ writeSkillFiles(path9.join(parallDir, "skills"));
34038
34699
  ensureLocalAttachmentGitExclude(workspaceDir);
34039
34700
  }
34040
34701
 
@@ -34135,7 +34796,8 @@ async function main() {
34135
34796
  apiKey: config.apiKey,
34136
34797
  orgId: config.orgId,
34137
34798
  contextFilePathForSession: (sessionKey) => contextFilePathForSession(config.stateDir, sessionKey),
34138
- stepIdFilePathForSession: (sessionKey) => stepIdFilePathForSession(config.stateDir, sessionKey)
34799
+ stepIdFilePathForSession: (sessionKey) => stepIdFilePathForSession(config.stateDir, sessionKey),
34800
+ contextDirPath: dispatchContextDirPath(config.stateDir)
34139
34801
  });
34140
34802
  if (resolvedEffort) {
34141
34803
  adapter.updateConfig({ effort: resolvedEffort });
@@ -34183,6 +34845,7 @@ async function main() {
34183
34845
  dispatchDeadlineMs: parseDispatchDeadlineMs(process.env.PRLL_DISPATCH_DEADLINE_MS),
34184
34846
  contextFilePathForSession: (sessionKey) => contextFilePathForSession(config.stateDir, sessionKey),
34185
34847
  stepIdFilePathForSession: (sessionKey) => stepIdFilePathForSession(config.stateDir, sessionKey),
34848
+ dispatchContextDir: dispatchContextDirPath(config.stateDir),
34186
34849
  onConfigUpdate: refreshPlatformConfig,
34187
34850
  onSessionReady: refreshPlatformConfig,
34188
34851
  // Long-lived `claude` subprocesses outlive a single dispatch, so the