@parall/parall 1.37.0 → 1.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.bundle.mjs +856 -232
  2. package/package.json +3 -3
@@ -17717,9 +17717,9 @@ var require_getMachineId_linux = __commonJS({
17717
17717
  var api_1 = (init_esm(), __toCommonJS(esm_exports));
17718
17718
  async function getMachineId() {
17719
17719
  const paths = ["/etc/machine-id", "/var/lib/dbus/machine-id"];
17720
- for (const path7 of paths) {
17720
+ for (const path8 of paths) {
17721
17721
  try {
17722
- const result = await fs_1.promises.readFile(path7, { encoding: "utf8" });
17722
+ const result = await fs_1.promises.readFile(path8, { encoding: "utf8" });
17723
17723
  return result.trim();
17724
17724
  } catch (e) {
17725
17725
  api_1.diag.debug(`error reading machine id: ${e}`);
@@ -21122,7 +21122,7 @@ function appendRootPathToUrlIfNeeded(url) {
21122
21122
  return void 0;
21123
21123
  }
21124
21124
  }
21125
- function appendResourcePathToUrl(url, path7) {
21125
+ function appendResourcePathToUrl(url, path8) {
21126
21126
  try {
21127
21127
  new URL(url);
21128
21128
  } catch (_a) {
@@ -21132,11 +21132,11 @@ function appendResourcePathToUrl(url, path7) {
21132
21132
  if (!url.endsWith("/")) {
21133
21133
  url = url + "/";
21134
21134
  }
21135
- url += path7;
21135
+ url += path8;
21136
21136
  try {
21137
21137
  new URL(url);
21138
21138
  } catch (_b) {
21139
- diag2.warn("Configuration: Provided URL appended with '" + path7 + "' is not a valid URL, using 'undefined' instead of '" + url + "'");
21139
+ diag2.warn("Configuration: Provided URL appended with '" + path8 + "' is not a valid URL, using 'undefined' instead of '" + url + "'");
21140
21140
  return void 0;
21141
21141
  }
21142
21142
  return url;
@@ -26473,6 +26473,16 @@ function resolveParallAccount(params) {
26473
26473
  };
26474
26474
  }
26475
26475
 
26476
+ // ../agent-core/dist/lane-key.js
26477
+ import * as path from "node:path";
26478
+ function laneKeyForTarget(targetUri, threadRootId) {
26479
+ return Buffer.from(`${targetUri}
26480
+ ${threadRootId ?? ""}`, "utf8").toString("base64url");
26481
+ }
26482
+ function laneContextFilePath(contextDir, targetUri, threadRootId) {
26483
+ return path.join(contextDir, `${laneKeyForTarget(targetUri, threadRootId)}.json`);
26484
+ }
26485
+
26476
26486
  // ../agent-core/dist/session-state.js
26477
26487
  function normalizeSessionKey(sessionKey) {
26478
26488
  return sessionKey.toLowerCase();
@@ -27021,8 +27031,8 @@ function isParallNoReplyCommand(command) {
27021
27031
 
27022
27032
  // ../agent-core/dist/gateway-base.js
27023
27033
  import * as os from "node:os";
27024
- import * as fs from "node:fs";
27025
- import * as path from "node:path";
27034
+ import * as fs2 from "node:fs";
27035
+ import * as path2 from "node:path";
27026
27036
 
27027
27037
  // ../sdk/dist/types.js
27028
27038
  var MENTION_ALL_USER_ID = "all";
@@ -27297,6 +27307,11 @@ var ENDPOINTS = {
27297
27307
  DISPATCH_ACK_BY_ID: (orgId, id) => `${API_BASE}/orgs/${orgId}/dispatch/${id}/ack`,
27298
27308
  DISPATCH_EXPIRE: (orgId) => `${API_BASE}/orgs/${orgId}/dispatch/expire`,
27299
27309
  DISPATCH_BY_MESSAGES: (orgId) => `${API_BASE}/orgs/${orgId}/dispatch/by-messages`,
27310
+ DISPATCH_CLAIM: (orgId) => `${API_BASE}/orgs/${orgId}/dispatch/claim`,
27311
+ DISPATCH_STEER: (orgId) => `${API_BASE}/orgs/${orgId}/dispatch/steer`,
27312
+ DISPATCH_COMPLETE: (orgId) => `${API_BASE}/orgs/${orgId}/dispatch/complete`,
27313
+ DISPATCH_RELEASE: (orgId) => `${API_BASE}/orgs/${orgId}/dispatch/release`,
27314
+ DISPATCH_HEARTBEAT: (orgId) => `${API_BASE}/orgs/${orgId}/dispatch/heartbeat`,
27300
27315
  // Unread
27301
27316
  UNREAD: `${API_BASE}/me/unread`,
27302
27317
  ORG_UNREAD: (orgId) => `${API_BASE}/orgs/${orgId}/unread`,
@@ -27417,6 +27432,7 @@ var WS_EVENTS = {
27417
27432
  READ_POSITION_UPDATED: "read_position.updated",
27418
27433
  DISPATCH_NEW: "dispatch.new",
27419
27434
  DISPATCH_RECEIVED: "dispatch.received",
27435
+ DISPATCH_RESOLVED: "dispatch.resolved",
27420
27436
  SCHEDULE_CREATED: "schedule.created",
27421
27437
  SCHEDULE_UPDATED: "schedule.updated",
27422
27438
  SCHEDULE_DELETED: "schedule.deleted",
@@ -27512,8 +27528,8 @@ var ParallClient = class _ParallClient {
27512
27528
  * is authoritative, so wiki vs api routing can't drift from how a caller
27513
27529
  * happens to invoke the client.
27514
27530
  */
27515
- baseUrlFor(path7) {
27516
- return path7.startsWith(WIKI_BASE) ? this.wikiBaseUrl : this.baseUrl;
27531
+ baseUrlFor(path8) {
27532
+ return path8.startsWith(WIKI_BASE) ? this.wikiBaseUrl : this.baseUrl;
27517
27533
  }
27518
27534
  setToken(token) {
27519
27535
  this.token = token;
@@ -27540,10 +27556,10 @@ var ParallClient = class _ParallClient {
27540
27556
  * REFRESH_THRESHOLD_S, refresh it **before** sending the request.
27541
27557
  * No-op when the token is still fresh, missing, or un-parseable.
27542
27558
  */
27543
- async ensureFreshToken(path7) {
27559
+ async ensureFreshToken(path8) {
27544
27560
  if (!this.token || !this.getRefreshToken)
27545
27561
  return;
27546
- const pathSuffix = path7.replace(/^\/api\/v1/, "");
27562
+ const pathSuffix = path8.replace(/^\/api\/v1/, "");
27547
27563
  if (_ParallClient.AUTH_PATHS.has(pathSuffix))
27548
27564
  return;
27549
27565
  const exp = _ParallClient.decodeJwtExp(this.token);
@@ -27575,11 +27591,11 @@ var ParallClient = class _ParallClient {
27575
27591
  this.refreshPromise = null;
27576
27592
  }
27577
27593
  }
27578
- async request(method, path7, body, query, retried = false, opts) {
27594
+ async request(method, path8, body, query, retried = false, opts) {
27579
27595
  if (!retried) {
27580
- await this.ensureFreshToken(path7);
27596
+ await this.ensureFreshToken(path8);
27581
27597
  }
27582
- let url = `${this.baseUrlFor(path7)}${path7}`;
27598
+ let url = `${this.baseUrlFor(path8)}${path8}`;
27583
27599
  if (query) {
27584
27600
  const params = new URLSearchParams();
27585
27601
  for (const [key, value] of Object.entries(query)) {
@@ -27609,12 +27625,12 @@ var ParallClient = class _ParallClient {
27609
27625
  throw _ParallClient.normalizeFetchError(err);
27610
27626
  }
27611
27627
  if (res.status === 401) {
27612
- const pathSuffix = path7.replace(/^\/api\/v1/, "");
27628
+ const pathSuffix = path8.replace(/^\/api\/v1/, "");
27613
27629
  const isAuthPath = _ParallClient.AUTH_PATHS.has(pathSuffix);
27614
27630
  if (!retried && !isAuthPath && this.getRefreshToken) {
27615
27631
  const refreshed = await this.tryRefresh();
27616
27632
  if (refreshed) {
27617
- return this.request(method, path7, body, query, true, opts);
27633
+ return this.request(method, path8, body, query, true, opts);
27618
27634
  }
27619
27635
  }
27620
27636
  if (this.onTokenExpired && !isAuthPath) {
@@ -27625,6 +27641,7 @@ var ParallClient = class _ParallClient {
27625
27641
  const rawErrorBody = await res.json().catch(() => ({}));
27626
27642
  throw buildApiError(res, rawErrorBody);
27627
27643
  }
27644
+ opts?.onStatus?.(res.status);
27628
27645
  if (res.status === 204)
27629
27646
  return void 0;
27630
27647
  if (res.status === 202) {
@@ -27643,15 +27660,15 @@ var ParallClient = class _ParallClient {
27643
27660
  * hit the 100 MiB cap, so a longer 5-minute timeout is used so a
27644
27661
  * 50 MiB blob on a slow connection doesn't get chopped at 15 s.
27645
27662
  */
27646
- async multipartRequest(method, path7, body, retried = false) {
27663
+ async multipartRequest(method, path8, body, retried = false) {
27647
27664
  if (!retried) {
27648
- await this.ensureFreshToken(path7);
27665
+ await this.ensureFreshToken(path8);
27649
27666
  }
27650
27667
  const { "Content-Type": _drop, ...headers } = this.buildHeaders();
27651
27668
  void _drop;
27652
27669
  let res;
27653
27670
  try {
27654
- res = await fetch(`${this.baseUrlFor(path7)}${path7}`, {
27671
+ res = await fetch(`${this.baseUrlFor(path8)}${path8}`, {
27655
27672
  method,
27656
27673
  headers,
27657
27674
  body,
@@ -27661,12 +27678,12 @@ var ParallClient = class _ParallClient {
27661
27678
  throw _ParallClient.normalizeFetchError(err);
27662
27679
  }
27663
27680
  if (res.status === 401) {
27664
- const pathSuffix = path7.replace(/^\/api\/v1/, "");
27681
+ const pathSuffix = path8.replace(/^\/api\/v1/, "");
27665
27682
  const isAuthPath = _ParallClient.AUTH_PATHS.has(pathSuffix);
27666
27683
  if (!retried && !isAuthPath && this.getRefreshToken) {
27667
27684
  const refreshed = await this.tryRefresh();
27668
27685
  if (refreshed) {
27669
- return this.multipartRequest(method, path7, body, true);
27686
+ return this.multipartRequest(method, path8, body, true);
27670
27687
  }
27671
27688
  }
27672
27689
  if (this.onTokenExpired && !isAuthPath) {
@@ -27958,6 +27975,21 @@ var ParallClient = class _ParallClient {
27958
27975
  async sendMessage(orgId, chatId, req) {
27959
27976
  return this.request("POST", ENDPOINTS.CHAT_MESSAGES(orgId, chatId), req);
27960
27977
  }
27978
+ /**
27979
+ * sendMessage variant that also reports whether the server answered with an
27980
+ * idempotent replay (HTTP 200 — the message already existed for this
27981
+ * idempotency/effect key) instead of a fresh create (201). Used by the CLI
27982
+ * to surface "already sent by a previous run (deduplicated)".
27983
+ */
27984
+ async sendMessageDetailed(orgId, chatId, req) {
27985
+ let status = 0;
27986
+ const message = await this.request("POST", ENDPOINTS.CHAT_MESSAGES(orgId, chatId), req, void 0, false, {
27987
+ onStatus: (s) => {
27988
+ status = s;
27989
+ }
27990
+ });
27991
+ return { message, deduplicated: status === 200 };
27992
+ }
27961
27993
  async getMessages(orgId, chatId, params) {
27962
27994
  return this.request("GET", ENDPOINTS.CHAT_MESSAGES(orgId, chatId), void 0, params);
27963
27995
  }
@@ -28345,8 +28377,8 @@ var ParallClient = class _ParallClient {
28345
28377
  async requestMachineUpdate(orgId, machineId, mandatory = false) {
28346
28378
  await this.request("POST", ENDPOINTS.MACHINE_REQUEST_UPDATE(orgId, machineId), { mandatory });
28347
28379
  }
28348
- async browseMachineFilesystem(orgId, machineId, path7) {
28349
- return this.request("POST", ENDPOINTS.MACHINE_BROWSE(orgId, machineId), { path: path7 }, void 0, false, { timeoutMs: 15e3 });
28380
+ async browseMachineFilesystem(orgId, machineId, path8) {
28381
+ return this.request("POST", ENDPOINTS.MACHINE_BROWSE(orgId, machineId), { path: path8 }, void 0, false, { timeoutMs: 15e3 });
28350
28382
  }
28351
28383
  /** Create a new machine key. Returns the raw key string (shown once) + metadata. */
28352
28384
  async createMachineKey(orgId, machineId, name) {
@@ -28419,6 +28451,30 @@ var ParallClient = class _ParallClient {
28419
28451
  const params = maxAgeHours !== void 0 ? { max_age_hours: String(maxAgeHours) } : void 0;
28420
28452
  return this.request("POST", ENDPOINTS.DISPATCH_EXPIRE(orgId), void 0, params);
28421
28453
  }
28454
+ /**
28455
+ * Claim a dispatch lane (explicit consume endpoint). Occupies the
28456
+ * (agent, target, thread) lane and folds claimable WorkItems into it;
28457
+ * `claimed: false` means a healthy incumbent holds the lane.
28458
+ */
28459
+ async claimDispatch(orgId, req) {
28460
+ return this.request("POST", ENDPOINTS.DISPATCH_CLAIM(orgId), req);
28461
+ }
28462
+ /** Fold a pending same-target WorkItem into a live lane (409 STALE_LANE when dethroned). */
28463
+ async steerDispatch(orgId, req) {
28464
+ return this.request("POST", ENDPOINTS.DISPATCH_STEER(orgId), req);
28465
+ }
28466
+ /** End a turn: no_action sweep of the lane's members + lane release + re-drive check. */
28467
+ async completeDispatch(orgId, req) {
28468
+ return this.request("POST", ENDPOINTS.DISPATCH_COMPLETE(orgId), req);
28469
+ }
28470
+ /** Release a lane on graceful shutdown — members return to pending immediately. */
28471
+ async releaseDispatchLane(orgId, lane) {
28472
+ return this.request("POST", ENDPOINTS.DISPATCH_RELEASE(orgId), { lane });
28473
+ }
28474
+ /** Renew a live lane's lease (long-turn keepalive). 409 STALE_LANE when dethroned. */
28475
+ async heartbeatDispatchLane(orgId, req) {
28476
+ return this.request("POST", ENDPOINTS.DISPATCH_HEARTBEAT(orgId), req);
28477
+ }
28422
28478
  async getDispatchByMessages(orgId, chatId, messageIds) {
28423
28479
  const params = { chat_id: chatId, message_ids: messageIds.join(",") };
28424
28480
  return this.request("GET", ENDPOINTS.DISPATCH_BY_MESSAGES(orgId), void 0, params);
@@ -28883,8 +28939,8 @@ var ParallClient = class _ParallClient {
28883
28939
  async deleteWikiRestriction(orgId, wikiId, restrictionId) {
28884
28940
  await this.request("DELETE", ENDPOINTS.WIKI_RESTRICTION(orgId, wikiId, restrictionId));
28885
28941
  }
28886
- async getWikiAccessStatus(orgId, wikiId, path7) {
28887
- return this.request("GET", ENDPOINTS.WIKI_ACCESS_STATUS(orgId, wikiId), void 0, path7 ? { path: path7 } : void 0);
28942
+ async getWikiAccessStatus(orgId, wikiId, path8) {
28943
+ return this.request("GET", ENDPOINTS.WIKI_ACCESS_STATUS(orgId, wikiId), void 0, path8 ? { path: path8 } : void 0);
28888
28944
  }
28889
28945
  async createWikiAccessRequest(orgId, wikiId, data) {
28890
28946
  await this.request("POST", ENDPOINTS.WIKI_ACCESS_REQUESTS(orgId, wikiId), data);
@@ -28893,14 +28949,14 @@ var ParallClient = class _ParallClient {
28893
28949
  async getWikiCommits(orgId, wikiId, params) {
28894
28950
  return this.request("GET", ENDPOINTS.WIKI_COMMITS(orgId, wikiId), void 0, params);
28895
28951
  }
28896
- async getWikiFileCommits(orgId, wikiId, path7, params) {
28952
+ async getWikiFileCommits(orgId, wikiId, path8, params) {
28897
28953
  return this.request("GET", ENDPOINTS.WIKI_FILE_COMMITS(orgId, wikiId), void 0, {
28898
- path: path7,
28954
+ path: path8,
28899
28955
  ...params
28900
28956
  });
28901
28957
  }
28902
- async getWikiBlame(orgId, wikiId, path7, ref) {
28903
- return this.request("GET", ENDPOINTS.WIKI_BLAME(orgId, wikiId), void 0, { path: path7, ref });
28958
+ async getWikiBlame(orgId, wikiId, path8, ref) {
28959
+ return this.request("GET", ENDPOINTS.WIKI_BLAME(orgId, wikiId), void 0, { path: path8, ref });
28904
28960
  }
28905
28961
  // ---- Wiki Operations (audit log) ----
28906
28962
  async getWikiOperations(orgId, wikiId, params) {
@@ -29532,6 +29588,404 @@ var ParallWs = class {
29532
29588
  }
29533
29589
  };
29534
29590
 
29591
+ // ../agent-core/dist/lane-ledger.js
29592
+ import * as fs from "node:fs";
29593
+ var LedgerUnsupportedError = class extends Error {
29594
+ };
29595
+ function isStaleLane(err) {
29596
+ return err instanceof ApiError && err.status === 409 && err.code === "STALE_LANE";
29597
+ }
29598
+ function isEndpointMissing(err) {
29599
+ return err instanceof ApiError && err.status === 404 && !err.code;
29600
+ }
29601
+ var LaneLedger = class {
29602
+ opts;
29603
+ lanes = /* @__PURE__ */ new Map();
29604
+ constructor(opts) {
29605
+ this.opts = opts;
29606
+ }
29607
+ get contextDir() {
29608
+ return this.opts.contextDir;
29609
+ }
29610
+ /** Only chat message events ride the lane ledger; typed events stay on the legacy ack path. */
29611
+ handles(event) {
29612
+ return event.type === "message" && event.targetId.startsWith("cht_");
29613
+ }
29614
+ laneKeyFor(event) {
29615
+ if (event.type !== "message" && event.dispatchEventId) {
29616
+ return laneKeyForTarget(`dsp:${event.dispatchEventId}`);
29617
+ }
29618
+ return laneKeyForTarget(`prll://${event.targetId}`, event.threadRootId);
29619
+ }
29620
+ getForEvent(event) {
29621
+ return this.lanes.get(this.laneKeyFor(event));
29622
+ }
29623
+ laneContextPath(lane) {
29624
+ return laneContextFilePath(this.opts.contextDir, lane.targetUri, lane.threadRootId);
29625
+ }
29626
+ /**
29627
+ * Claim (or reuse) the lane for a group of same-lane message events and
29628
+ * fold every group member into it. Returns 'foreign' when a healthy
29629
+ * incumbent (another pod) holds the resource — the caller must not
29630
+ * dispatch; the events stay pending server-side and re-drive after the
29631
+ * incumbent completes.
29632
+ */
29633
+ async ensureLane(events) {
29634
+ const trigger = events[events.length - 1];
29635
+ const laneKey = this.laneKeyFor(trigger);
29636
+ let lane = this.lanes.get(laneKey);
29637
+ if (!lane) {
29638
+ const targetUri = `prll://${trigger.targetId}`;
29639
+ let res;
29640
+ try {
29641
+ res = await this.opts.client.claimDispatch(this.opts.orgId, {
29642
+ target_uri: targetUri,
29643
+ thread_root_id: trigger.threadRootId,
29644
+ limit: 100
29645
+ });
29646
+ } catch (err) {
29647
+ if (isEndpointMissing(err))
29648
+ throw new LedgerUnsupportedError("claim endpoint unavailable");
29649
+ throw err;
29650
+ }
29651
+ if (!res.claimed || !res.lane) {
29652
+ this.opts.log?.info(`lane for ${targetUri} held by a healthy incumbent \u2014 leaving events pending for re-drive`);
29653
+ return null;
29654
+ }
29655
+ const leaseUntilMs = Date.parse(res.lease_until ?? "");
29656
+ lane = {
29657
+ laneKey,
29658
+ lane: res.lane,
29659
+ targetUri,
29660
+ threadRootId: trigger.threadRootId,
29661
+ folded: /* @__PURE__ */ new Map(),
29662
+ ...Number.isNaN(leaseUntilMs) ? {} : { leaseUntilMs, leaseTtlMs: Math.max(leaseUntilMs - Date.now(), 6e4) }
29663
+ };
29664
+ for (const ev of res.events ?? []) {
29665
+ lane.folded.set(ev.source_id, ev.id);
29666
+ }
29667
+ this.lanes.set(laneKey, lane);
29668
+ }
29669
+ for (const ev of events) {
29670
+ if (lane.folded.has(ev.messageId))
29671
+ continue;
29672
+ try {
29673
+ const res = await this.opts.client.steerDispatch(this.opts.orgId, {
29674
+ lane: lane.lane,
29675
+ target_uri: lane.targetUri,
29676
+ thread_root_id: lane.threadRootId,
29677
+ ...ev.dispatchEventId ? { dispatch_event_id: ev.dispatchEventId } : { source_type: "message", source_id: ev.messageId }
29678
+ });
29679
+ lane.folded.set(ev.messageId, res.dispatch_event_id);
29680
+ } catch (err) {
29681
+ if (isStaleLane(err)) {
29682
+ this.lanes.delete(laneKey);
29683
+ return null;
29684
+ }
29685
+ this.opts.log?.warn(`steer fold failed for ${ev.messageId} \u2014 failing closed, releasing lane: ${String(err)}`);
29686
+ await this.release(laneKey);
29687
+ return null;
29688
+ }
29689
+ }
29690
+ return lane;
29691
+ }
29692
+ /**
29693
+ * Fold a live mid-turn message into its active lane BEFORE injecting it
29694
+ * into the running turn. Injection without a successful fold is forbidden —
29695
+ * an un-folded injected message would be re-driven after complete and the
29696
+ * model would handle it twice.
29697
+ */
29698
+ async steerLive(event) {
29699
+ const laneKey = this.laneKeyFor(event);
29700
+ const lane = this.lanes.get(laneKey);
29701
+ if (!lane)
29702
+ return false;
29703
+ if (lane.folded.has(event.messageId))
29704
+ return true;
29705
+ try {
29706
+ const res = await this.opts.client.steerDispatch(this.opts.orgId, {
29707
+ lane: lane.lane,
29708
+ target_uri: lane.targetUri,
29709
+ thread_root_id: lane.threadRootId,
29710
+ ...event.dispatchEventId ? { dispatch_event_id: event.dispatchEventId } : { source_type: "message", source_id: event.messageId }
29711
+ });
29712
+ lane.folded.set(event.messageId, res.dispatch_event_id);
29713
+ return true;
29714
+ } catch (err) {
29715
+ if (isStaleLane(err)) {
29716
+ this.lanes.delete(laneKey);
29717
+ } else {
29718
+ this.opts.log?.warn(`live steer failed for ${event.messageId}: ${String(err)}`);
29719
+ }
29720
+ return false;
29721
+ }
29722
+ }
29723
+ /**
29724
+ * Complete the lane when no local work remains for it: the server sweeps
29725
+ * still-leased members as no_action, releases the occupancy row, and
29726
+ * re-drives any same-target pending work. A STALE_LANE answer means a
29727
+ * takeover already owns the resource — local state is dropped either way.
29728
+ */
29729
+ async completeIfIdle(laneKey, hasMoreLocal) {
29730
+ const lane = this.lanes.get(laneKey);
29731
+ if (!lane || hasMoreLocal)
29732
+ return;
29733
+ this.lanes.delete(laneKey);
29734
+ this.removeLaneContext(lane);
29735
+ try {
29736
+ const res = await this.opts.client.completeDispatch(this.opts.orgId, {
29737
+ lane: lane.lane,
29738
+ target_uri: lane.targetUri,
29739
+ thread_root_id: lane.threadRootId
29740
+ });
29741
+ if (res.swept_no_action > 0 || res.redriven) {
29742
+ this.opts.log?.info(`lane complete for ${lane.targetUri}: swept ${res.swept_no_action} no_action, redriven=${res.redriven}`);
29743
+ }
29744
+ } catch (err) {
29745
+ if (isStaleLane(err)) {
29746
+ this.opts.log?.info(`lane complete skipped for ${lane.targetUri} \u2014 taken over`);
29747
+ return;
29748
+ }
29749
+ this.opts.log?.warn(`lane complete failed for ${lane.targetUri}: ${String(err)}`);
29750
+ }
29751
+ }
29752
+ /**
29753
+ * Long-turn keepalive: renew the lane's lease on runtime activity, throttled
29754
+ * so a chatty turn doesn't spam the server. Without this, a legitimately
29755
+ * long turn (> lane TTL) would be dethroned mid-flight and every subsequent
29756
+ * write misfired with STALE_LANE — the design doc's "long turns renew via
29757
+ * step writes". Fire-and-forget: a failed renewal is surfaced by the next
29758
+ * write's incumbency check anyway.
29759
+ */
29760
+ maybeRenew(lane) {
29761
+ const now = Date.now();
29762
+ const ttl = lane.leaseTtlMs ?? 10 * 6e4;
29763
+ const until = lane.leaseUntilMs ?? now;
29764
+ if (until - now > ttl / 2)
29765
+ return;
29766
+ lane.leaseUntilMs = now + ttl;
29767
+ void this.opts.client.heartbeatDispatchLane(this.opts.orgId, {
29768
+ lane: lane.lane,
29769
+ target_uri: lane.targetUri,
29770
+ thread_root_id: lane.threadRootId
29771
+ }).then((res) => {
29772
+ const until2 = Date.parse(res?.lease_until ?? "");
29773
+ if (!Number.isNaN(until2))
29774
+ lane.leaseUntilMs = until2;
29775
+ }).catch((err) => {
29776
+ if (isStaleLane(err)) {
29777
+ this.lanes.delete(lane.laneKey);
29778
+ this.opts.log?.warn(`lane ${lane.targetUri} was taken over during the turn`);
29779
+ return;
29780
+ }
29781
+ this.opts.log?.warn(`lane heartbeat failed for ${lane.targetUri}: ${String(err)}`);
29782
+ });
29783
+ }
29784
+ /**
29785
+ * Release a lane's unresolved members back to pending (dispatch error /
29786
+ * shutdown) so the next pod re-claims immediately instead of waiting out
29787
+ * the lease.
29788
+ */
29789
+ async release(laneKey) {
29790
+ const lane = this.lanes.get(laneKey);
29791
+ if (!lane)
29792
+ return;
29793
+ this.lanes.delete(laneKey);
29794
+ this.removeLaneContext(lane);
29795
+ try {
29796
+ await this.opts.client.releaseDispatchLane(this.opts.orgId, lane.lane);
29797
+ } catch (err) {
29798
+ this.opts.log?.warn(`lane release failed for ${lane.targetUri}: ${String(err)}`);
29799
+ }
29800
+ }
29801
+ async releaseAll() {
29802
+ const keys = [...this.lanes.keys()];
29803
+ for (const key of keys) {
29804
+ await this.release(key);
29805
+ }
29806
+ }
29807
+ /** True when any lane is currently active (used by shutdown logging). */
29808
+ get activeCount() {
29809
+ return this.lanes.size;
29810
+ }
29811
+ /**
29812
+ * Claim the single-member lane of one typed WorkItem (resource = dsp:<id>),
29813
+ * by WorkItem id or by source identity (the live task.assigned event has no
29814
+ * WorkItem id). Returns null when a healthy incumbent (another pod) holds
29815
+ * it or the WorkItem is already resolved — the caller must skip processing.
29816
+ */
29817
+ async claimTyped(ref) {
29818
+ let res;
29819
+ try {
29820
+ res = await this.opts.client.claimDispatch(this.opts.orgId, {
29821
+ dispatch_event_id: ref.dispatchEventId,
29822
+ source_type: ref.dispatchEventId ? void 0 : ref.sourceType,
29823
+ source_id: ref.dispatchEventId ? void 0 : ref.sourceId
29824
+ });
29825
+ } catch (err) {
29826
+ if (isEndpointMissing(err))
29827
+ throw new LedgerUnsupportedError("claim endpoint unavailable");
29828
+ throw err;
29829
+ }
29830
+ if (!res.claimed || !res.lane || !res.events?.length)
29831
+ return null;
29832
+ const workItem = res.events[0];
29833
+ const targetUri = `dsp:${workItem.id}`;
29834
+ const leaseUntilMs = Date.parse(res.lease_until ?? "");
29835
+ const lane = {
29836
+ laneKey: laneKeyForTarget(targetUri),
29837
+ lane: res.lane,
29838
+ targetUri,
29839
+ folded: /* @__PURE__ */ new Map([[workItem.source_id, workItem.id]]),
29840
+ typedDispatchEventId: workItem.id,
29841
+ ...Number.isNaN(leaseUntilMs) ? {} : { leaseUntilMs, leaseTtlMs: Math.max(leaseUntilMs - Date.now(), 6e4) }
29842
+ };
29843
+ this.lanes.set(lane.laneKey, lane);
29844
+ return lane;
29845
+ }
29846
+ /**
29847
+ * Remove the per-lane context file (and its CLI sidecar) when the lane
29848
+ * ends. A leftover file would make a later cross-context send to the same
29849
+ * target bind a dead lane token and misfire with STALE_LANE instead of
29850
+ * taking the plain non-ledger path.
29851
+ */
29852
+ removeLaneContext(lane) {
29853
+ const contextPath = this.laneContextPath(lane);
29854
+ for (const p of [contextPath, contextPath.replace(/\.json$/, ".reply-state.json")]) {
29855
+ try {
29856
+ fs.rmSync(p, { force: true });
29857
+ } catch {
29858
+ }
29859
+ }
29860
+ }
29861
+ };
29862
+
29863
+ // ../agent-core/dist/gateway-lane-flow.js
29864
+ async function dispatchLaneGroup(host, opts) {
29865
+ const ledger = host.laneLedger;
29866
+ const event = opts.events[opts.events.length - 1];
29867
+ let lane;
29868
+ try {
29869
+ lane = await ledger.ensureLane(opts.events);
29870
+ } catch (err) {
29871
+ if (!(err instanceof LedgerUnsupportedError))
29872
+ throw err;
29873
+ host.disableLedger("claim endpoint missing");
29874
+ await host.emitDispatchReceived(event);
29875
+ const dispatched2 = await host.runDispatch(event, opts.sessionKey, opts.body, opts.earlier, opts.captureText);
29876
+ if (!dispatched2)
29877
+ return "shutdown";
29878
+ for (const ev of opts.events) {
29879
+ host.opts.client.ackDispatch(host.opts.config.org_id, {
29880
+ source_type: ev.ackSourceType ?? "message",
29881
+ source_id: ev.ackSourceId ?? ev.messageId
29882
+ }).catch(() => {
29883
+ });
29884
+ }
29885
+ return "dispatched";
29886
+ }
29887
+ if (!lane) {
29888
+ for (const ev of opts.events) {
29889
+ host.dispatchedMessages.delete(ev.messageId);
29890
+ }
29891
+ return "foreign";
29892
+ }
29893
+ let dispatched = false;
29894
+ try {
29895
+ dispatched = await host.runDispatch(event, opts.sessionKey, opts.body, opts.earlier, opts.captureText);
29896
+ } catch (err) {
29897
+ await ledger.release(lane.laneKey).catch(() => {
29898
+ });
29899
+ throw err;
29900
+ }
29901
+ if (!dispatched) {
29902
+ return "shutdown";
29903
+ }
29904
+ const pendingInjections = host.opts.dispatchAdapter.hasPendingInjections?.(opts.sessionKey) ?? false;
29905
+ await ledger.completeIfIdle(lane.laneKey, pendingInjections || opts.hasMoreLocal());
29906
+ return "dispatched";
29907
+ }
29908
+ async function consumeTypedDispatch(host, ref, run, ack) {
29909
+ if (!host.laneLedger || host.ledgerDisabled) {
29910
+ if (await run(ref.dispatchEventId))
29911
+ ack(ref.dispatchEventId);
29912
+ return;
29913
+ }
29914
+ let lane;
29915
+ try {
29916
+ lane = await host.laneLedger.claimTyped(ref);
29917
+ } catch (err) {
29918
+ if (err instanceof LedgerUnsupportedError) {
29919
+ host.disableLedger("claim endpoint missing");
29920
+ if (await run(ref.dispatchEventId))
29921
+ ack(ref.dispatchEventId);
29922
+ return;
29923
+ }
29924
+ throw err;
29925
+ }
29926
+ if (!lane) {
29927
+ host.opts.log?.info(`typed dispatch ${ref.dispatchEventId ?? `${ref.sourceType}:${ref.sourceId}`} not claimable (held elsewhere or already resolved) \u2014 skipping`);
29928
+ return;
29929
+ }
29930
+ try {
29931
+ if (await run(lane.typedDispatchEventId))
29932
+ ack(lane.typedDispatchEventId);
29933
+ } finally {
29934
+ await host.laneLedger.completeIfIdle(lane.laneKey, false).catch(() => {
29935
+ });
29936
+ }
29937
+ }
29938
+ async function consumeMessageWorkItem(host, item) {
29939
+ if (host.shuttingDown)
29940
+ return;
29941
+ if (!host.tryClaimMessage(item.source_id))
29942
+ return;
29943
+ const ackItem = () => {
29944
+ host.opts.client.ackDispatchByID(host.opts.config.org_id, item.id).catch(() => {
29945
+ });
29946
+ };
29947
+ let msg = null;
29948
+ try {
29949
+ msg = await host.opts.client.getMessage(item.source_id);
29950
+ } catch (err) {
29951
+ const status = err?.status;
29952
+ if (status !== 404) {
29953
+ host.opts.log?.warn(`message fetch failed for ${item.source_id}, leaving pending: ${String(err)}`);
29954
+ host.dispatchedMessages.delete(item.source_id);
29955
+ return;
29956
+ }
29957
+ }
29958
+ if (!msg || msg.sender_id === host.opts.agentUserId) {
29959
+ host.dispatchedMessages.delete(item.source_id);
29960
+ ackItem();
29961
+ return;
29962
+ }
29963
+ const decision = await host.buildMessageDispatchDecision(item.chat_id, msg);
29964
+ if (decision.action === "retry") {
29965
+ host.dispatchedMessages.delete(item.source_id);
29966
+ return;
29967
+ }
29968
+ if (decision.action === "skip") {
29969
+ host.dispatchedMessages.delete(item.source_id);
29970
+ ackItem();
29971
+ return;
29972
+ }
29973
+ decision.event.dispatchEventId = item.id;
29974
+ const laneResolved = host.usesLaneLedger(decision.event);
29975
+ try {
29976
+ const dispatched = await host.handleInboundEvent(decision.event);
29977
+ if (dispatched) {
29978
+ if (!laneResolved)
29979
+ ackItem();
29980
+ } else {
29981
+ host.dispatchedMessages.delete(item.source_id);
29982
+ }
29983
+ } catch (err) {
29984
+ host.dispatchedMessages.delete(item.source_id);
29985
+ throw err;
29986
+ }
29987
+ }
29988
+
29535
29989
  // ../agent-core/dist/telemetry.js
29536
29990
  init_esm();
29537
29991
  var import_api_logs = __toESM(require_src(), 1);
@@ -29833,6 +30287,14 @@ var ParallAgentGateway = class {
29833
30287
  inFlightDispatches = 0;
29834
30288
  drainResolvers = [];
29835
30289
  pendingRestartNotification = null;
30290
+ laneLedger;
30291
+ // Sticky fallback: flipped when the server predates the ledger (claim
30292
+ // endpoint 404) so every subsequent dispatch uses the legacy flow.
30293
+ ledgerDisabled = false;
30294
+ // Group key of the group currently being dispatched on main — lane-aware
30295
+ // (targetId + thread), unlike mainCurrentTargetId which stays chat-level
30296
+ // for fork routing decisions.
30297
+ mainCurrentGroupKey;
29836
30298
  DISPATCHED_MESSAGES_CAP = 5e3;
29837
30299
  // SHUTDOWN_DEADLINE_MS is read by waitForDrain via the configured value
29838
30300
  // below — kept as instance state so per-runtime configs can override it
@@ -29842,6 +30304,14 @@ var ParallAgentGateway = class {
29842
30304
  DISPATCH_DEADLINE_MS;
29843
30305
  constructor(opts) {
29844
30306
  this.opts = opts;
30307
+ if (opts.dispatchContextDir) {
30308
+ this.laneLedger = new LaneLedger({
30309
+ client: opts.client,
30310
+ orgId: opts.config.org_id,
30311
+ contextDir: opts.dispatchContextDir,
30312
+ log: opts.log
30313
+ });
30314
+ }
29845
30315
  this.SHUTDOWN_DEADLINE_MS = opts.shutdownDeadlineMs ?? 6e4;
29846
30316
  this.FORK_DEADLINE_MS = opts.forkDeadlineMs ?? 2 * 60 * 6e4;
29847
30317
  this.DISPATCH_DEADLINE_MS = opts.dispatchDeadlineMs ?? 20 * 6e4;
@@ -29911,14 +30381,18 @@ var ParallAgentGateway = class {
29911
30381
  if (data.status !== "todo" && data.status !== "in_progress")
29912
30382
  return;
29913
30383
  try {
29914
- const dispatched = await this.handleTaskAssignment(data, data.id);
29915
- if (dispatched) {
30384
+ 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) => {
30385
+ if (dispatchEventId) {
30386
+ this.opts.client.ackDispatchByID(this.opts.config.org_id, dispatchEventId).catch(() => {
30387
+ });
30388
+ return;
30389
+ }
29916
30390
  this.opts.client.ackDispatch(this.opts.config.org_id, {
29917
30391
  source_type: "task_activity",
29918
30392
  source_id: data.id
29919
30393
  }).catch(() => {
29920
30394
  });
29921
- }
30395
+ });
29922
30396
  } catch (err) {
29923
30397
  this.opts.log?.error(`task dispatch failed for ${data.id}: ${String(err)}`);
29924
30398
  }
@@ -29928,11 +30402,10 @@ var ParallAgentGateway = class {
29928
30402
  if (!data.source_id || !data.task_id)
29929
30403
  return;
29930
30404
  try {
29931
- const dispatched = await this.handleTaskComment(data.source_id, data.task_id, data.actor_id, data.delivery_reason);
29932
- if (dispatched) {
30405
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.handleTaskComment(data.source_id, data.task_id ?? "", data.actor_id, data.delivery_reason), () => {
29933
30406
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
29934
30407
  });
29935
- }
30408
+ });
29936
30409
  } catch (err) {
29937
30410
  this.opts.log?.error(`task comment dispatch failed for ${data.source_id}: ${String(err)}`);
29938
30411
  }
@@ -29940,11 +30413,10 @@ var ParallAgentGateway = class {
29940
30413
  if (!data.source_id)
29941
30414
  return;
29942
30415
  try {
29943
- const dispatched = await this.handleWikiComment(data.source_id, data.actor_id, data.delivery_reason);
29944
- if (dispatched) {
30416
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.handleWikiComment(data.source_id, data.actor_id, data.delivery_reason), () => {
29945
30417
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
29946
30418
  });
29947
- }
30419
+ });
29948
30420
  } catch (err) {
29949
30421
  this.opts.log?.error(`wiki comment dispatch failed for ${data.source_id}: ${String(err)}`);
29950
30422
  }
@@ -29952,11 +30424,13 @@ var ParallAgentGateway = class {
29952
30424
  if (!data.task_id)
29953
30425
  return;
29954
30426
  try {
29955
- const dispatched = await this.handleTaskDispatch(data.task_id, data.source_id ?? data.task_id, { allowCreator: true });
29956
- if (dispatched) {
30427
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, (dispatchEventId) => this.handleTaskDispatch(data.task_id ?? "", data.source_id ?? data.task_id ?? "", {
30428
+ allowCreator: true,
30429
+ dispatchEventId
30430
+ }), () => {
29957
30431
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
29958
30432
  });
29959
- }
30433
+ });
29960
30434
  } catch (err) {
29961
30435
  this.opts.log?.error(`task update dispatch failed for ${data.task_id}: ${String(err)}`);
29962
30436
  }
@@ -29964,11 +30438,10 @@ var ParallAgentGateway = class {
29964
30438
  if (!data.source_id)
29965
30439
  return;
29966
30440
  try {
29967
- const dispatched = await this.fetchAndHandleScheduleFire(data.source_id, data.actor_id);
29968
- if (dispatched) {
30441
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.fetchAndHandleScheduleFire(data.source_id, data.actor_id), () => {
29969
30442
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
29970
30443
  });
29971
- }
30444
+ });
29972
30445
  } catch (err) {
29973
30446
  this.opts.log?.error(`schedule fire dispatch failed for ${data.source_id}: ${String(err)}`);
29974
30447
  }
@@ -29976,11 +30449,10 @@ var ParallAgentGateway = class {
29976
30449
  if (!data.source_id)
29977
30450
  return;
29978
30451
  try {
29979
- const dispatched = await this.fetchAndHandleExternalTriggerRun(data.source_id);
29980
- if (dispatched) {
30452
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.fetchAndHandleExternalTriggerRun(data.source_id), () => {
29981
30453
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
29982
30454
  });
29983
- }
30455
+ });
29984
30456
  } catch (err) {
29985
30457
  this.opts.log?.error(`external trigger dispatch failed for ${data.source_id}: ${String(err)}`);
29986
30458
  }
@@ -29988,11 +30460,10 @@ var ParallAgentGateway = class {
29988
30460
  if (!data.source_id)
29989
30461
  return;
29990
30462
  try {
29991
- const dispatched = await this.fetchAndHandleChannelMessage(data.source_id);
29992
- if (dispatched) {
30463
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.fetchAndHandleChannelMessage(data.source_id), () => {
29993
30464
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
29994
30465
  });
29995
- }
30466
+ });
29996
30467
  } catch (err) {
29997
30468
  this.opts.log?.error(`channel message dispatch failed for ${data.source_id}: ${String(err)}`);
29998
30469
  }
@@ -30000,14 +30471,19 @@ var ParallAgentGateway = class {
30000
30471
  if (!data.source_id)
30001
30472
  return;
30002
30473
  try {
30003
- const dispatched = await this.fetchAndHandleApprovalDecided(data.source_id, data.actor_id, data.chat_id ?? null);
30004
- if (dispatched) {
30474
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.fetchAndHandleApprovalDecided(data.source_id, data.actor_id, data.chat_id ?? null), () => {
30005
30475
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
30006
30476
  });
30007
- }
30477
+ });
30008
30478
  } catch (err) {
30009
30479
  this.opts.log?.error(`approval decided dispatch failed for ${data.source_id}: ${String(err)}`);
30010
30480
  }
30481
+ } else if (data.event_type === "message" && this.laneLedger && data.source_id && data.chat_id) {
30482
+ try {
30483
+ await this.handleMessageRedrive(data);
30484
+ } catch (err) {
30485
+ this.opts.log?.error(`message re-drive failed for ${data.source_id}: ${String(err)}`);
30486
+ }
30011
30487
  } else if (data.event_type !== "message" && data.event_type !== "task_assign") {
30012
30488
  this.opts.log?.info(`dispatch.new with unhandled event_type=${String(data.event_type)} (id=${data.id}) \u2014 no-op`);
30013
30489
  }
@@ -30043,6 +30519,39 @@ var ParallAgentGateway = class {
30043
30519
  source_id: sourceId
30044
30520
  });
30045
30521
  }
30522
+ /** True when this event's lifecycle is owned by the dispatch lane ledger. */
30523
+ usesLaneLedger(event) {
30524
+ return this.laneLedger != null && !this.ledgerDisabled && this.laneLedger.handles(event);
30525
+ }
30526
+ disableLedger(reason) {
30527
+ if (this.ledgerDisabled)
30528
+ return;
30529
+ this.ledgerDisabled = true;
30530
+ this.opts.log?.warn(`dispatch ledger unavailable (${reason}) \u2014 falling back to legacy received/ack flow`);
30531
+ }
30532
+ /**
30533
+ * Buffer grouping key. Lane-ledger message events group by full lane
30534
+ * identity (chat + thread) so a channel lane and a thread lane in the same
30535
+ * chat dispatch as separate turns with separate claims; everything else
30536
+ * keeps the historical chat-level grouping.
30537
+ */
30538
+ dispatchGroupKey(event) {
30539
+ if (this.usesLaneLedger(event)) {
30540
+ return this.laneLedger.laneKeyFor(event);
30541
+ }
30542
+ return event.targetId;
30543
+ }
30544
+ // Lane-flow protocols live in gateway-lane-flow.ts; these thin delegates
30545
+ // keep call sites and tests on the class surface.
30546
+ laneFlowHost() {
30547
+ return this;
30548
+ }
30549
+ dispatchLaneGroup(opts) {
30550
+ return dispatchLaneGroup(this.laneFlowHost(), opts);
30551
+ }
30552
+ consumeTypedDispatch(ref, run, ack) {
30553
+ return consumeTypedDispatch(this.laneFlowHost(), ref, run, ack);
30554
+ }
30046
30555
  buildDispatchContext(event, sessionKey) {
30047
30556
  const binding = this.sessionBindings.get(sessionKey);
30048
30557
  return {
@@ -30059,6 +30568,7 @@ var ParallAgentGateway = class {
30059
30568
  noReply: event.noReply ?? false,
30060
30569
  contextFilePath: this.opts.contextFilePathForSession?.(sessionKey),
30061
30570
  stepIdFilePath: this.opts.stepIdFilePathForSession?.(sessionKey),
30571
+ contextDirPath: this.opts.dispatchContextDir,
30062
30572
  client: this.opts.client,
30063
30573
  log: this.opts.log
30064
30574
  };
@@ -30098,7 +30608,7 @@ var ParallAgentGateway = class {
30098
30608
  this.opts.log?.warn(`failed to create input step: ${String(err)}`);
30099
30609
  }
30100
30610
  }
30101
- async createRuntimeStep(sessionId, event, runtimeEvent, stepIdFilePath, contextFilePath) {
30611
+ async createRuntimeStep(sessionId, event, runtimeEvent, stepIdFilePath, contextFilePath, laneContextFilePath2) {
30102
30612
  const target = resolveStepTarget(event);
30103
30613
  try {
30104
30614
  switch (runtimeEvent.type) {
@@ -30144,6 +30654,9 @@ var ParallAgentGateway = class {
30144
30654
  } else if (stepIdFilePath) {
30145
30655
  this.writeStepIdFile(stepIdFilePath, step.id);
30146
30656
  }
30657
+ if (laneContextFilePath2) {
30658
+ this.updateContextFileStepId(laneContextFilePath2, step.id);
30659
+ }
30147
30660
  break;
30148
30661
  }
30149
30662
  case "tool_result":
@@ -30166,6 +30679,9 @@ var ParallAgentGateway = class {
30166
30679
  } else if (stepIdFilePath) {
30167
30680
  this.clearStepIdFile(stepIdFilePath);
30168
30681
  }
30682
+ if (laneContextFilePath2) {
30683
+ this.updateContextFileStepId(laneContextFilePath2, null);
30684
+ }
30169
30685
  break;
30170
30686
  case "error":
30171
30687
  await this.opts.client.createAgentStep(this.opts.config.org_id, this.opts.agentUserId, sessionId, {
@@ -30185,28 +30701,28 @@ var ParallAgentGateway = class {
30185
30701
  }
30186
30702
  writeContextFile(filePath, ctx) {
30187
30703
  try {
30188
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
30189
- fs.writeFileSync(filePath, JSON.stringify(ctx), "utf8");
30704
+ fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
30705
+ fs2.writeFileSync(filePath, JSON.stringify(ctx), "utf8");
30190
30706
  } catch (err) {
30191
30707
  this.opts.log?.warn(`failed to write context file ${filePath}: ${String(err)}`);
30192
30708
  }
30193
30709
  }
30194
30710
  updateContextFileStepId(filePath, stepId) {
30195
30711
  try {
30196
- const raw = fs.readFileSync(filePath, "utf8");
30712
+ const raw = fs2.readFileSync(filePath, "utf8");
30197
30713
  const ctx = JSON.parse(raw);
30198
30714
  ctx.step_id = stepId;
30199
- fs.writeFileSync(filePath, JSON.stringify(ctx), "utf8");
30715
+ fs2.writeFileSync(filePath, JSON.stringify(ctx), "utf8");
30200
30716
  } catch (err) {
30201
30717
  this.opts.log?.warn(`failed to update context file step_id ${filePath}: ${String(err)}`);
30202
30718
  }
30203
30719
  }
30204
30720
  updateContextFileSessionId(filePath, sessionId) {
30205
30721
  try {
30206
- const raw = fs.readFileSync(filePath, "utf8");
30722
+ const raw = fs2.readFileSync(filePath, "utf8");
30207
30723
  const ctx = JSON.parse(raw);
30208
30724
  ctx.session_id = sessionId;
30209
- fs.writeFileSync(filePath, JSON.stringify(ctx), "utf8");
30725
+ fs2.writeFileSync(filePath, JSON.stringify(ctx), "utf8");
30210
30726
  } catch (err) {
30211
30727
  this.opts.log?.warn(`failed to update context file session_id ${filePath}: ${String(err)}`);
30212
30728
  }
@@ -30214,8 +30730,8 @@ var ParallAgentGateway = class {
30214
30730
  /** @deprecated Use writeContextFile / updateContextFileStepId. */
30215
30731
  writeStepIdFile(filePath, stepId) {
30216
30732
  try {
30217
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
30218
- fs.writeFileSync(filePath, stepId, "utf8");
30733
+ fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
30734
+ fs2.writeFileSync(filePath, stepId, "utf8");
30219
30735
  } catch (err) {
30220
30736
  this.opts.log?.warn(`failed to write step id file ${filePath}: ${String(err)}`);
30221
30737
  }
@@ -30223,7 +30739,7 @@ var ParallAgentGateway = class {
30223
30739
  /** @deprecated Use writeContextFile / updateContextFileStepId. */
30224
30740
  clearStepIdFile(filePath) {
30225
30741
  try {
30226
- fs.writeFileSync(filePath, "", "utf8");
30742
+ fs2.writeFileSync(filePath, "", "utf8");
30227
30743
  } catch {
30228
30744
  }
30229
30745
  }
@@ -30232,7 +30748,7 @@ var ParallAgentGateway = class {
30232
30748
  await this.createInputStep(sessionId, event);
30233
30749
  }
30234
30750
  }
30235
- async bindRuntimeSession(sessionKey, runtimeEvent, contextFilePath) {
30751
+ async bindRuntimeSession(sessionKey, runtimeEvent, contextFilePath, laneContextFilePath2) {
30236
30752
  const runtimeLaneKey = runtimeEvent.runtimeLaneKey || sessionKey;
30237
30753
  const existing = this.sessionBindings.get(sessionKey);
30238
30754
  if (existing && existing.runtimeLaneKey === runtimeLaneKey && existing.runtimeSessionId === runtimeEvent.runtimeSessionId) {
@@ -30276,6 +30792,9 @@ var ParallAgentGateway = class {
30276
30792
  if (contextFilePath) {
30277
30793
  this.updateContextFileSessionId(contextFilePath, session.id);
30278
30794
  }
30795
+ if (laneContextFilePath2) {
30796
+ this.updateContextFileSessionId(laneContextFilePath2, session.id);
30797
+ }
30279
30798
  await this.opts.onSessionBinding?.(binding);
30280
30799
  return binding;
30281
30800
  }
@@ -30302,14 +30821,27 @@ var ParallAgentGateway = class {
30302
30821
  const dispatchContext = this.buildDispatchContext(event, sessionKey);
30303
30822
  const contextFilePath = dispatchContext.contextFilePath;
30304
30823
  const stepIdFilePath = dispatchContext.stepIdFilePath;
30824
+ const activeLane = this.ledgerDisabled ? void 0 : this.laneLedger?.getForEvent(event);
30825
+ const laneContextFilePath2 = activeLane ? this.laneLedger?.laneContextPath(activeLane) : void 0;
30826
+ const contextBody = {
30827
+ session_id: dispatchContext.sessionId ?? null,
30828
+ chat_id: dispatchContext.chatId ?? null,
30829
+ trigger_message_id: dispatchContext.triggerMessageId ?? null,
30830
+ no_reply: dispatchContext.noReply,
30831
+ step_id: null,
30832
+ dispatch_event_id: activeLane?.typedDispatchEventId ?? activeLane?.folded.get(event.messageId) ?? null,
30833
+ lane: activeLane?.lane ?? null,
30834
+ target_uri: activeLane?.targetUri ?? null,
30835
+ thread_root_id: activeLane?.threadRootId ?? null,
30836
+ // Typed binding hint for the CLI: which task this dispatch is about
30837
+ // (parall task update attaches the typed effect only on a match).
30838
+ task_id: event.type === "task" ? event.targetId : null
30839
+ };
30305
30840
  if (contextFilePath) {
30306
- this.writeContextFile(contextFilePath, {
30307
- session_id: dispatchContext.sessionId ?? null,
30308
- chat_id: dispatchContext.chatId ?? null,
30309
- trigger_message_id: dispatchContext.triggerMessageId ?? null,
30310
- no_reply: dispatchContext.noReply,
30311
- step_id: null
30312
- });
30841
+ this.writeContextFile(contextFilePath, contextBody);
30842
+ }
30843
+ if (laneContextFilePath2) {
30844
+ this.writeContextFile(laneContextFilePath2, contextBody);
30313
30845
  }
30314
30846
  this.inFlightDispatches++;
30315
30847
  const deadlineTimer = this.DISPATCH_DEADLINE_MS > 0 ? setTimeout(() => {
@@ -30336,7 +30868,7 @@ var ParallAgentGateway = class {
30336
30868
  })) {
30337
30869
  if (runtimeEvent.type === "runtime_session") {
30338
30870
  const priorAgentSessionId = binding?.agentSessionId;
30339
- binding = await this.bindRuntimeSession(sessionKey, runtimeEvent, contextFilePath);
30871
+ binding = await this.bindRuntimeSession(sessionKey, runtimeEvent, contextFilePath, laneContextFilePath2);
30340
30872
  if (event.targetType === "channel_conversation" && binding.agentSessionId !== priorAgentSessionId) {
30341
30873
  try {
30342
30874
  await this.opts.client.setChannelConversationSession(this.opts.config.org_id, event.targetId, binding.agentSessionId);
@@ -30368,6 +30900,9 @@ var ParallAgentGateway = class {
30368
30900
  await this.createInputStep(binding.agentSessionId, event);
30369
30901
  inputStepsCreated = true;
30370
30902
  }
30903
+ if (activeLane && !this.ledgerDisabled) {
30904
+ this.laneLedger?.maybeRenew(activeLane);
30905
+ }
30371
30906
  if (captureText && runtimeEvent.type === "text" && runtimeEvent.text) {
30372
30907
  captureText.push(runtimeEvent.text);
30373
30908
  }
@@ -30384,7 +30919,7 @@ var ParallAgentGateway = class {
30384
30919
  } else if (runtimeEvent.type === "tool_result" && pendingSendCallIds.delete(runtimeEvent.callId)) {
30385
30920
  recordMessageSend(sessionKey, !runtimeEvent.error);
30386
30921
  }
30387
- await this.createRuntimeStep(binding.agentSessionId, event, runtimeEvent, stepIdFilePath, contextFilePath);
30922
+ await this.createRuntimeStep(binding.agentSessionId, event, runtimeEvent, stepIdFilePath, contextFilePath, laneContextFilePath2);
30388
30923
  }
30389
30924
  if (!binding) {
30390
30925
  binding = this.sessionBindings.get(sessionKey);
@@ -30406,7 +30941,7 @@ var ParallAgentGateway = class {
30406
30941
  await this.createRuntimeStep(binding.agentSessionId, event, {
30407
30942
  type: "error",
30408
30943
  message: `Dispatch failed: ${String(err)}`
30409
- }, stepIdFilePath, contextFilePath);
30944
+ }, stepIdFilePath, contextFilePath, laneContextFilePath2);
30410
30945
  } catch (stepErr) {
30411
30946
  if (this.isSessionNotLiveError(stepErr))
30412
30947
  staleDetected = true;
@@ -30449,6 +30984,9 @@ var ParallAgentGateway = class {
30449
30984
  } else if (stepIdFilePath) {
30450
30985
  this.clearStepIdFile(stepIdFilePath);
30451
30986
  }
30987
+ if (laneContextFilePath2) {
30988
+ this.updateContextFileStepId(laneContextFilePath2, null);
30989
+ }
30452
30990
  this.inFlightDispatches--;
30453
30991
  if (this.inFlightDispatches === 0 && this.drainResolvers.length > 0) {
30454
30992
  const resolvers = this.drainResolvers.splice(0);
@@ -30506,13 +31044,39 @@ var ParallAgentGateway = class {
30506
31044
  item.resolve(false);
30507
31045
  break;
30508
31046
  }
30509
- const items = fork.queue.splice(0);
31047
+ let items;
31048
+ const head = fork.queue[0];
31049
+ if (head && this.usesLaneLedger(head.event)) {
31050
+ const headKey = this.dispatchGroupKey(head.event);
31051
+ const splitAt = fork.queue.findIndex((it) => this.dispatchGroupKey(it.event) !== headKey);
31052
+ items = splitAt === -1 ? fork.queue.splice(0) : fork.queue.splice(0, splitAt);
31053
+ } else {
31054
+ items = fork.queue.splice(0);
31055
+ }
30510
31056
  const events = items.map((item) => item.event);
30511
31057
  const last = events[events.length - 1];
30512
31058
  const earlier = events.slice(0, -1);
30513
31059
  try {
30514
31060
  const batchText = [];
30515
- const dispatched = await this.runDispatch(last, fork.fork.sessionKey, buildForkScopePrefix(last) + buildEventBody(last), earlier, batchText);
31061
+ let dispatched;
31062
+ if (this.usesLaneLedger(last)) {
31063
+ const outcome = await this.dispatchLaneGroup({
31064
+ events,
31065
+ sessionKey: fork.fork.sessionKey,
31066
+ body: buildForkScopePrefix(last) + buildEventBody(last),
31067
+ earlier,
31068
+ captureText: batchText,
31069
+ hasMoreLocal: () => fork.queue.length > 0
31070
+ });
31071
+ if (outcome === "foreign") {
31072
+ for (const item of items)
31073
+ item.resolve(false);
31074
+ break;
31075
+ }
31076
+ dispatched = outcome === "dispatched";
31077
+ } else {
31078
+ dispatched = await this.runDispatch(last, fork.fork.sessionKey, buildForkScopePrefix(last) + buildEventBody(last), earlier, batchText);
31079
+ }
30516
31080
  if (!dispatched) {
30517
31081
  for (const item of items) {
30518
31082
  item.resolve(false);
@@ -30615,9 +31179,9 @@ var ParallAgentGateway = class {
30615
31179
  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`);
30616
31180
  break;
30617
31181
  }
30618
- const targetId = this.dispatchState.mainBuffer[0].targetId;
31182
+ const groupKey = this.dispatchGroupKey(this.dispatchState.mainBuffer[0]);
30619
31183
  const events = [];
30620
- while (this.dispatchState.mainBuffer[0]?.targetId === targetId) {
31184
+ while (this.dispatchState.mainBuffer[0] && this.dispatchGroupKey(this.dispatchState.mainBuffer[0]) === groupKey) {
30621
31185
  events.push(this.dispatchState.mainBuffer.shift());
30622
31186
  }
30623
31187
  const event = events[events.length - 1];
@@ -30626,7 +31190,36 @@ var ParallAgentGateway = class {
30626
31190
  const pendingFork = hasPendingInjections ? [] : this.dispatchState.pendingForkResults.splice(0);
30627
31191
  const forkPrefix = buildForkResultPrefix(pendingFork);
30628
31192
  this.dispatchState.mainCurrentTargetId = event.targetId;
31193
+ this.mainCurrentGroupKey = groupKey;
30629
31194
  this.dispatchState.mainPreDispatchBranchPoint = this.opts.dispatchAdapter.getBranchPoint?.(this.opts.runtimeKey);
31195
+ if (this.usesLaneLedger(event)) {
31196
+ let outcome;
31197
+ try {
31198
+ outcome = await this.dispatchLaneGroup({
31199
+ events,
31200
+ sessionKey: this.opts.runtimeKey,
31201
+ body: forkPrefix + buildEventBody(event),
31202
+ earlier,
31203
+ hasMoreLocal: () => this.dispatchState.mainBuffer.some((e) => this.dispatchGroupKey(e) === groupKey)
31204
+ });
31205
+ } catch (err) {
31206
+ this.opts.log?.error(`lane dispatch failed for ${event.messageId}: ${String(err)}`);
31207
+ this.dispatchState.pendingForkResults.unshift(...pendingFork);
31208
+ for (const ev of events)
31209
+ this.dispatchedMessages.delete(ev.messageId);
31210
+ continue;
31211
+ }
31212
+ if (outcome === "shutdown") {
31213
+ this.dispatchState.mainBuffer.unshift(...events);
31214
+ this.dispatchState.pendingForkResults.unshift(...pendingFork);
31215
+ break;
31216
+ }
31217
+ if (outcome === "foreign") {
31218
+ this.dispatchState.pendingForkResults.unshift(...pendingFork);
31219
+ continue;
31220
+ }
31221
+ continue;
31222
+ }
30630
31223
  try {
30631
31224
  await this.emitDispatchReceived(event);
30632
31225
  } catch (err) {
@@ -30655,6 +31248,7 @@ var ParallAgentGateway = class {
30655
31248
  this.draining = false;
30656
31249
  this.dispatchState.mainDispatching = false;
30657
31250
  this.dispatchState.mainCurrentTargetId = void 0;
31251
+ this.mainCurrentGroupKey = void 0;
30658
31252
  this.dispatchState.mainPreDispatchBranchPoint = void 0;
30659
31253
  if (!this.shuttingDown && this.dispatchState.mainBuffer.length > 0) {
30660
31254
  setTimeout(() => {
@@ -30674,13 +31268,38 @@ var ParallAgentGateway = class {
30674
31268
  const forkPrefix = buildForkResultPrefix(pendingFork);
30675
31269
  this.dispatchState.mainDispatching = true;
30676
31270
  this.dispatchState.mainCurrentTargetId = event.targetId;
31271
+ this.mainCurrentGroupKey = this.dispatchGroupKey(event);
30677
31272
  this.dispatchState.mainPreDispatchBranchPoint = this.opts.dispatchAdapter.getBranchPoint?.(this.opts.runtimeKey);
31273
+ if (this.usesLaneLedger(event)) {
31274
+ let outcome = "shutdown";
31275
+ try {
31276
+ try {
31277
+ outcome = await this.dispatchLaneGroup({
31278
+ events: [event],
31279
+ sessionKey: this.opts.runtimeKey,
31280
+ body: forkPrefix + buildEventBody(event),
31281
+ earlier: [],
31282
+ hasMoreLocal: () => this.dispatchState.mainBuffer.some((e) => this.dispatchGroupKey(e) === this.dispatchGroupKey(event))
31283
+ });
31284
+ } catch (err) {
31285
+ this.dispatchState.pendingForkResults.unshift(...pendingFork);
31286
+ throw err;
31287
+ }
31288
+ if (outcome !== "dispatched") {
31289
+ this.dispatchState.pendingForkResults.unshift(...pendingFork);
31290
+ }
31291
+ } finally {
31292
+ await this.drainMainBuffer();
31293
+ }
31294
+ return outcome === "dispatched";
31295
+ }
30678
31296
  try {
30679
31297
  await this.emitDispatchReceived(event);
30680
31298
  } catch (err) {
30681
31299
  this.opts.log?.warn?.(`mark-received failed, leaving unacked for retry: ${String(err)}`);
30682
31300
  this.dispatchState.mainDispatching = false;
30683
31301
  this.dispatchState.mainCurrentTargetId = void 0;
31302
+ this.mainCurrentGroupKey = void 0;
30684
31303
  this.dispatchState.mainPreDispatchBranchPoint = void 0;
30685
31304
  this.dispatchState.pendingForkResults.unshift(...pendingFork);
30686
31305
  return false;
@@ -30701,7 +31320,11 @@ var ParallAgentGateway = class {
30701
31320
  return false;
30702
31321
  }
30703
31322
  this.dispatchState.mainBuffer.push(event);
30704
- if (this.dispatchState.mainCurrentTargetId === event.targetId && await this.opts.dispatchAdapter.enqueueDuringDispatch?.(this.opts.runtimeKey, buildEventBody(event))) {
31323
+ if (this.usesLaneLedger(event)) {
31324
+ if (this.mainCurrentGroupKey === this.dispatchGroupKey(event) && await this.laneLedger?.steerLive(event) && await this.opts.dispatchAdapter.enqueueDuringDispatch?.(this.opts.runtimeKey, buildEventBody(event))) {
31325
+ this.opts.log?.info(`steer folded+injected for ${event.messageId} (will drain for bookkeeping)`);
31326
+ }
31327
+ } else if (this.dispatchState.mainCurrentTargetId === event.targetId && await this.opts.dispatchAdapter.enqueueDuringDispatch?.(this.opts.runtimeKey, buildEventBody(event))) {
30705
31328
  this.opts.log?.info(`steer injected for ${event.messageId} (will drain for bookkeeping)`);
30706
31329
  }
30707
31330
  if (!this.dispatchState.mainDispatching && !this.draining && this.dispatchState.mainBuffer.length > 0) {
@@ -30872,8 +31495,10 @@ var ParallAgentGateway = class {
30872
31495
  try {
30873
31496
  const dispatched = await this.handleInboundEvent(event);
30874
31497
  if (dispatched) {
30875
- this.opts.client.ackDispatch(this.opts.config.org_id, { source_type: "message", source_id: data.id }).catch(() => {
30876
- });
31498
+ if (!this.usesLaneLedger(event)) {
31499
+ this.opts.client.ackDispatch(this.opts.config.org_id, { source_type: "message", source_id: data.id }).catch(() => {
31500
+ });
31501
+ }
30877
31502
  } else {
30878
31503
  this.dispatchedMessages.delete(data.id);
30879
31504
  }
@@ -30882,7 +31507,21 @@ var ParallAgentGateway = class {
30882
31507
  this.dispatchedMessages.delete(data.id);
30883
31508
  }
30884
31509
  }
30885
- async handleTaskAssignment(task, ackSourceId) {
31510
+ // Ledger re-drive consumption: dispatch.new message hints re-enter the
31511
+ // shared WorkItem consumption path (same protocol as catch-up).
31512
+ async handleMessageRedrive(item) {
31513
+ if (!item.chat_id || !item.source_id)
31514
+ return;
31515
+ await this.consumeMessageWorkItem({
31516
+ id: item.id,
31517
+ source_id: item.source_id,
31518
+ chat_id: item.chat_id
31519
+ });
31520
+ }
31521
+ consumeMessageWorkItem(item) {
31522
+ return consumeMessageWorkItem(this.laneFlowHost(), item);
31523
+ }
31524
+ async handleTaskAssignment(task, ackSourceId, dispatchEventId) {
30886
31525
  if (this.shuttingDown)
30887
31526
  return false;
30888
31527
  const dedupeKey = `${task.id}:${task.updated_at}`;
@@ -30911,7 +31550,8 @@ var ParallAgentGateway = class {
30911
31550
  body: parts.join("\n"),
30912
31551
  sentAt: task.updated_at ?? task.created_at,
30913
31552
  ackSourceType: "task_activity",
30914
- ackSourceId
31553
+ ackSourceId,
31554
+ dispatchEventId
30915
31555
  };
30916
31556
  const dispatched = await this.handleInboundEvent(event);
30917
31557
  if (!dispatched) {
@@ -30935,7 +31575,7 @@ var ParallAgentGateway = class {
30935
31575
  this.opts.log?.info(`skipping stale task dispatch ${ackSourceId ?? taskId} \u2014 assigned to ${task.assignee_id}, creator ${task.creator_id}`);
30936
31576
  return true;
30937
31577
  }
30938
- return this.handleTaskAssignment(task, ackSourceId);
31578
+ return this.handleTaskAssignment(task, ackSourceId, opts.dispatchEventId);
30939
31579
  }
30940
31580
  async handleTaskComment(commentId, taskId, actorId, deliveryReason) {
30941
31581
  if (this.shuttingDown)
@@ -31343,74 +31983,46 @@ var ParallAgentGateway = class {
31343
31983
  }
31344
31984
  processed++;
31345
31985
  try {
31346
- let dispatched = false;
31986
+ const ackItem = () => {
31987
+ this.opts.client.ackDispatchByID(this.opts.config.org_id, item.id).catch(() => {
31988
+ });
31989
+ };
31347
31990
  if (item.event_type === "task_assign" && item.task_id) {
31348
31991
  try {
31349
- dispatched = await this.handleTaskDispatch(item.task_id, item.source_id ?? item.task_id);
31992
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, (dispatchEventId) => this.handleTaskDispatch(item.task_id ?? "", item.source_id ?? item.task_id ?? "", {
31993
+ dispatchEventId
31994
+ }), ackItem);
31350
31995
  } catch (err) {
31351
31996
  this.opts.log?.warn(`catch-up task fetch failed for ${item.task_id}, leaving pending: ${String(err)}`);
31352
31997
  continue;
31353
31998
  }
31354
31999
  } else if (item.event_type === "task_update" && item.task_id) {
31355
32000
  try {
31356
- dispatched = await this.handleTaskDispatch(item.task_id, item.source_id ?? item.task_id, { allowCreator: true });
32001
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, (dispatchEventId) => this.handleTaskDispatch(item.task_id ?? "", item.source_id ?? item.task_id ?? "", {
32002
+ allowCreator: true,
32003
+ dispatchEventId
32004
+ }), ackItem);
31357
32005
  } catch (err) {
31358
32006
  this.opts.log?.warn(`catch-up task fetch failed for ${item.task_id}, leaving pending: ${String(err)}`);
31359
32007
  continue;
31360
32008
  }
31361
32009
  } else if (item.event_type === "task_comment" && item.source_id && item.task_id) {
31362
- dispatched = await this.handleTaskComment(item.source_id, item.task_id, item.actor_id, item.delivery_reason);
32010
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.handleTaskComment(item.source_id, item.task_id ?? "", item.actor_id, item.delivery_reason), ackItem);
31363
32011
  } else if (item.event_type === "wiki_comment" && item.source_id) {
31364
- dispatched = await this.handleWikiComment(item.source_id, item.actor_id, item.delivery_reason);
32012
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.handleWikiComment(item.source_id, item.actor_id, item.delivery_reason), ackItem);
31365
32013
  } else if (item.event_type === "schedule.fire" && item.source_id) {
31366
- dispatched = await this.fetchAndHandleScheduleFire(item.source_id, item.actor_id);
32014
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.fetchAndHandleScheduleFire(item.source_id, item.actor_id), ackItem);
31367
32015
  } else if (item.event_type === "external_trigger" && item.source_id) {
31368
- dispatched = await this.fetchAndHandleExternalTriggerRun(item.source_id);
32016
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.fetchAndHandleExternalTriggerRun(item.source_id), ackItem);
31369
32017
  } else if (item.event_type === "channel_message" && item.source_id) {
31370
- dispatched = await this.fetchAndHandleChannelMessage(item.source_id);
32018
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.fetchAndHandleChannelMessage(item.source_id), ackItem);
31371
32019
  } else if (item.event_type === "approval_decided" && item.source_id) {
31372
- dispatched = await this.fetchAndHandleApprovalDecided(item.source_id, item.actor_id, item.chat_id ?? null);
32020
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.fetchAndHandleApprovalDecided(item.source_id, item.actor_id, item.chat_id ?? null), ackItem);
31373
32021
  } else if (item.event_type === "message" && item.source_id && item.chat_id) {
31374
- if (!this.tryClaimMessage(item.source_id))
31375
- continue;
31376
- let msg = null;
31377
- let msgFetchFailed = false;
31378
- try {
31379
- msg = await this.opts.client.getMessage(item.source_id);
31380
- } catch (err) {
31381
- const status = err?.status;
31382
- if (status === 404) {
31383
- msg = null;
31384
- } else {
31385
- msgFetchFailed = true;
31386
- this.opts.log?.warn(`catch-up message fetch failed for ${item.source_id}, leaving pending: ${String(err)}`);
31387
- }
31388
- }
31389
- if (msgFetchFailed) {
31390
- this.dispatchedMessages.delete(item.source_id);
31391
- continue;
31392
- }
31393
- if (!msg || msg.sender_id === this.opts.agentUserId) {
31394
- this.dispatchedMessages.delete(item.source_id);
31395
- this.opts.client.ackDispatchByID(this.opts.config.org_id, item.id).catch(() => {
31396
- });
31397
- continue;
31398
- }
31399
- const decision = await this.buildMessageDispatchDecision(item.chat_id, msg);
31400
- if (decision.action === "retry") {
31401
- this.dispatchedMessages.delete(item.source_id);
31402
- continue;
31403
- }
31404
- if (decision.action === "skip") {
31405
- this.dispatchedMessages.delete(item.source_id);
31406
- this.opts.client.ackDispatchByID(this.opts.config.org_id, item.id).catch(() => {
31407
- });
31408
- continue;
31409
- }
31410
- dispatched = await this.handleInboundEvent(decision.event);
31411
- }
31412
- if (dispatched) {
31413
- this.opts.client.ackDispatchByID(this.opts.config.org_id, item.id).catch(() => {
32022
+ await this.consumeMessageWorkItem({
32023
+ id: item.id,
32024
+ source_id: item.source_id,
32025
+ chat_id: item.chat_id
31414
32026
  });
31415
32027
  }
31416
32028
  } catch (err) {
@@ -31476,6 +32088,14 @@ ${fullSummary}` : fullSummary;
31476
32088
  this.abortFork(targetId, "ws reconnect");
31477
32089
  }
31478
32090
  }
32091
+ if (this.laneLedger && this.inFlightDispatches === 0 && this.laneLedger.activeCount > 0) {
32092
+ log?.info(`releasing ${this.laneLedger.activeCount} stale lane(s) on reconnect`);
32093
+ await this.laneLedger.releaseAll();
32094
+ }
32095
+ if (this.laneLedger && this.ledgerDisabled) {
32096
+ log?.info("re-probing dispatch ledger after reconnect (was disabled)");
32097
+ this.ledgerDisabled = false;
32098
+ }
31479
32099
  const intervalSec = data.heartbeat_interval > 0 ? data.heartbeat_interval : 30;
31480
32100
  try {
31481
32101
  const count = await fetchAllChats(client, config.org_id, this.chatInfoMap);
@@ -31546,6 +32166,10 @@ ${fullSummary}` : fullSummary;
31546
32166
  }
31547
32167
  if (this.heartbeatTimer)
31548
32168
  clearInterval(this.heartbeatTimer);
32169
+ if (this.laneLedger && this.laneLedger.activeCount > 0) {
32170
+ this.opts.log?.info(`releasing ${this.laneLedger.activeCount} lane(s) on shutdown`);
32171
+ await this.laneLedger.releaseAll();
32172
+ }
31549
32173
  await this.opts.onBeforeDisconnect?.();
31550
32174
  this.opts.ws.disconnect();
31551
32175
  this.opts.log?.info(`disconnected`);
@@ -31556,8 +32180,8 @@ ${fullSummary}` : fullSummary;
31556
32180
  import { execSync } from "node:child_process";
31557
32181
  import { constants } from "node:fs";
31558
32182
  import * as fsSync from "node:fs";
31559
- import * as fs2 from "node:fs/promises";
31560
- import * as path2 from "node:path";
32183
+ import * as fs3 from "node:fs/promises";
32184
+ import * as path3 from "node:path";
31561
32185
  var DEFAULT_MAX_TOTAL_IMAGE_BYTES = 20 * 1024 * 1024;
31562
32186
  var DEFAULT_ATTACHMENT_CACHE_MAX_BYTES = 512 * 1024 * 1024;
31563
32187
  var DEFAULT_ATTACHMENT_DOWNLOAD_TIMEOUT_MS = 3e4;
@@ -31587,11 +32211,11 @@ async function prepareLocalImageAttachments(event, context2, opts) {
31587
32211
  };
31588
32212
  }
31589
32213
  const rootDir = await ensureAttachmentRootDir(opts.workspaceDir);
31590
- const messageDir = path2.join(rootDir, sanitizePathSegment(event.messageId));
32214
+ const messageDir = path3.join(rootDir, sanitizePathSegment(event.messageId));
31591
32215
  await ensurePathIsNotSymlink(messageDir);
31592
- await fs2.mkdir(messageDir, { recursive: true });
32216
+ await fs3.mkdir(messageDir, { recursive: true });
31593
32217
  await ensurePathIsNotSymlink(messageDir);
31594
- const activeMessageDir = path2.resolve(messageDir);
32218
+ const activeMessageDir = path3.resolve(messageDir);
31595
32219
  activeAttachmentDirs.add(activeMessageDir);
31596
32220
  const maintenanceCooldownMs = opts.maintenanceCooldownMs ?? DEFAULT_MAINTENANCE_COOLDOWN_MS;
31597
32221
  const maintenancePromise = scheduleAttachmentMaintenance(rootDir, {
@@ -31608,7 +32232,7 @@ async function prepareLocalImageAttachments(event, context2, opts) {
31608
32232
  const notes = [];
31609
32233
  let downloadedBytes = 0;
31610
32234
  for (const att of imageAttachments) {
31611
- const localPath = path2.join(messageDir, localFileName(att.id, att.fileName, att.mimeType));
32235
+ const localPath = path3.join(messageDir, localFileName(att.id, att.fileName, att.mimeType));
31612
32236
  const downloadTimeoutMs = opts.downloadTimeoutMs ?? DEFAULT_ATTACHMENT_DOWNLOAD_TIMEOUT_MS;
31613
32237
  const fetchFresh = async () => {
31614
32238
  const fileInfo = await withTimeout(context2.client.getFileUrl(att.id), downloadTimeoutMs, `file URL lookup timed out after ${downloadTimeoutMs}ms`);
@@ -31665,7 +32289,7 @@ async function appendPreparedLocalAttachmentRefs(body, event, context2, opts) {
31665
32289
  return { body: appendLocalAttachmentRefs(body, attachments), attachments };
31666
32290
  }
31667
32291
  function pinLocalAttachmentPaths(images) {
31668
- const dirs = new Set(images.map((image) => path2.resolve(path2.dirname(image.localPath))));
32292
+ const dirs = new Set(images.map((image) => path3.resolve(path3.dirname(image.localPath))));
31669
32293
  for (const dir of dirs) {
31670
32294
  activeAttachmentDirs.add(dir);
31671
32295
  }
@@ -31680,7 +32304,7 @@ function pinLocalAttachmentPaths(images) {
31680
32304
  };
31681
32305
  }
31682
32306
  function attachmentRootDir(workspaceDir) {
31683
- return path2.join(path2.resolve(workspaceDir), ".parall", "attachments");
32307
+ return path3.join(path3.resolve(workspaceDir), ".parall", "attachments");
31684
32308
  }
31685
32309
  function ensureLocalAttachmentGitExclude(workingDirectory) {
31686
32310
  try {
@@ -31689,8 +32313,8 @@ function ensureLocalAttachmentGitExclude(workingDirectory) {
31689
32313
  encoding: "utf8",
31690
32314
  stdio: ["ignore", "pipe", "ignore"]
31691
32315
  }).trim();
31692
- const excludePath = path2.isAbsolute(rel) ? rel : path2.join(workingDirectory, rel);
31693
- fsSync.mkdirSync(path2.dirname(excludePath), { recursive: true });
32316
+ const excludePath = path3.isAbsolute(rel) ? rel : path3.join(workingDirectory, rel);
32317
+ fsSync.mkdirSync(path3.dirname(excludePath), { recursive: true });
31694
32318
  const existing = fsSync.existsSync(excludePath) ? fsSync.readFileSync(excludePath, "utf8") : "";
31695
32319
  if (existing.split(/\r?\n/).some((line) => line.trim() === ".parall/"))
31696
32320
  return;
@@ -31726,18 +32350,18 @@ function scheduleAttachmentMaintenance(rootDir, opts) {
31726
32350
  return run;
31727
32351
  }
31728
32352
  async function ensureAttachmentRootDir(workspaceDir) {
31729
- const workspaceRoot = path2.resolve(workspaceDir);
31730
- const parallDir = path2.join(workspaceRoot, ".parall");
32353
+ const workspaceRoot = path3.resolve(workspaceDir);
32354
+ const parallDir = path3.join(workspaceRoot, ".parall");
31731
32355
  const rootDir = attachmentRootDir(workspaceRoot);
31732
- await fs2.mkdir(workspaceRoot, { recursive: true });
32356
+ await fs3.mkdir(workspaceRoot, { recursive: true });
31733
32357
  await ensurePathIsNotSymlink(parallDir);
31734
- await fs2.mkdir(parallDir, { recursive: true, mode: 448 });
32358
+ await fs3.mkdir(parallDir, { recursive: true, mode: 448 });
31735
32359
  await ensurePathIsNotSymlink(parallDir);
31736
32360
  await ensurePathIsNotSymlink(rootDir);
31737
- await fs2.mkdir(rootDir, { recursive: true, mode: 448 });
32361
+ await fs3.mkdir(rootDir, { recursive: true, mode: 448 });
31738
32362
  await ensurePathIsNotSymlink(rootDir);
31739
- const realWorkspace = await fs2.realpath(workspaceRoot);
31740
- const realRoot = await fs2.realpath(rootDir);
32363
+ const realWorkspace = await fs3.realpath(workspaceRoot);
32364
+ const realRoot = await fs3.realpath(rootDir);
31741
32365
  if (!isPathInside(realRoot, realWorkspace)) {
31742
32366
  throw new Error(`attachment root escapes workspace: ${rootDir}`);
31743
32367
  }
@@ -31745,7 +32369,7 @@ async function ensureAttachmentRootDir(workspaceDir) {
31745
32369
  }
31746
32370
  async function ensurePathIsNotSymlink(filePath) {
31747
32371
  try {
31748
- const stat = await fs2.lstat(filePath);
32372
+ const stat = await fs3.lstat(filePath);
31749
32373
  if (stat.isSymbolicLink()) {
31750
32374
  throw new Error(`refusing to use symlinked attachment path ${filePath}`);
31751
32375
  }
@@ -31756,8 +32380,8 @@ async function ensurePathIsNotSymlink(filePath) {
31756
32380
  }
31757
32381
  }
31758
32382
  function isPathInside(childPath, parentPath) {
31759
- const rel = path2.relative(parentPath, childPath);
31760
- return rel === "" || !!rel && !rel.startsWith("..") && !path2.isAbsolute(rel);
32383
+ const rel = path3.relative(parentPath, childPath);
32384
+ return rel === "" || !!rel && !rel.startsWith("..") && !path3.isAbsolute(rel);
31761
32385
  }
31762
32386
  async function existingUsableFile(filePath, expectedSize, rootDir) {
31763
32387
  try {
@@ -31768,30 +32392,30 @@ async function existingUsableFile(filePath, expectedSize, rootDir) {
31768
32392
  }
31769
32393
  }
31770
32394
  async function localFileStatInsideRoot(filePath, rootDir) {
31771
- const stat = await fs2.lstat(filePath);
32395
+ const stat = await fs3.lstat(filePath);
31772
32396
  if (stat.isSymbolicLink()) {
31773
32397
  throw new Error(`refusing to use symlinked attachment file ${filePath}`);
31774
32398
  }
31775
32399
  if (!stat.isFile()) {
31776
32400
  throw new Error(`attachment path is not a file ${filePath}`);
31777
32401
  }
31778
- const realRoot = await fs2.realpath(rootDir);
31779
- const realFile = await fs2.realpath(filePath);
32402
+ const realRoot = await fs3.realpath(rootDir);
32403
+ const realFile = await fs3.realpath(filePath);
31780
32404
  if (!isPathInside(realFile, realRoot)) {
31781
32405
  throw new Error(`attachment file escapes workspace: ${filePath}`);
31782
32406
  }
31783
32407
  return stat;
31784
32408
  }
31785
32409
  async function localDirectoryStatInsideRoot(dirPath, rootDir) {
31786
- const stat = await fs2.lstat(dirPath);
32410
+ const stat = await fs3.lstat(dirPath);
31787
32411
  if (stat.isSymbolicLink()) {
31788
32412
  throw new Error(`refusing to use symlinked attachment directory ${dirPath}`);
31789
32413
  }
31790
32414
  if (!stat.isDirectory()) {
31791
32415
  throw new Error(`attachment path is not a directory ${dirPath}`);
31792
32416
  }
31793
- const realRoot = await fs2.realpath(rootDir);
31794
- const realDir = await fs2.realpath(dirPath);
32417
+ const realRoot = await fs3.realpath(rootDir);
32418
+ const realDir = await fs3.realpath(dirPath);
31795
32419
  if (!isPathInside(realDir, realRoot)) {
31796
32420
  throw new Error(`attachment directory escapes workspace: ${dirPath}`);
31797
32421
  }
@@ -31799,7 +32423,7 @@ async function localDirectoryStatInsideRoot(dirPath, rootDir) {
31799
32423
  }
31800
32424
  async function openLocalFileInsideRoot(filePath, rootDir) {
31801
32425
  const checkedStat = await localFileStatInsideRoot(filePath, rootDir);
31802
- const file = await fs2.open(filePath, constants.O_RDONLY | constants.O_NOFOLLOW);
32426
+ const file = await fs3.open(filePath, constants.O_RDONLY | constants.O_NOFOLLOW);
31803
32427
  let keepOpen = false;
31804
32428
  try {
31805
32429
  const openedStat = await file.stat();
@@ -31815,8 +32439,8 @@ async function openLocalFileInsideRoot(filePath, rootDir) {
31815
32439
  }
31816
32440
  }
31817
32441
  async function openLocalTempFileInsideRoot(filePath, rootDir) {
31818
- await localDirectoryStatInsideRoot(path2.dirname(filePath), rootDir);
31819
- const file = await fs2.open(filePath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
32442
+ await localDirectoryStatInsideRoot(path3.dirname(filePath), rootDir);
32443
+ const file = await fs3.open(filePath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
31820
32444
  let keepOpen = false;
31821
32445
  try {
31822
32446
  const checkedStat = await localFileStatInsideRoot(filePath, rootDir);
@@ -31841,7 +32465,7 @@ async function assertLocalFileIdentity(filePath, rootDir, expected) {
31841
32465
  async function removeLocalFileIfInside(filePath, rootDir) {
31842
32466
  try {
31843
32467
  await localFileStatInsideRoot(filePath, rootDir);
31844
- await fs2.rm(filePath, { force: true });
32468
+ await fs3.rm(filePath, { force: true });
31845
32469
  } catch {
31846
32470
  }
31847
32471
  }
@@ -31854,7 +32478,7 @@ function sameFile(a, b) {
31854
32478
  async function cleanupOldAttachmentFiles(rootDir, ttlMs, log, preserveDirs) {
31855
32479
  let entries;
31856
32480
  try {
31857
- entries = await fs2.readdir(rootDir, { withFileTypes: true });
32481
+ entries = await fs3.readdir(rootDir, { withFileTypes: true });
31858
32482
  } catch {
31859
32483
  return;
31860
32484
  }
@@ -31862,15 +32486,15 @@ async function cleanupOldAttachmentFiles(rootDir, ttlMs, log, preserveDirs) {
31862
32486
  await Promise.all(entries.map(async (entry) => {
31863
32487
  if (!entry.isDirectory())
31864
32488
  return;
31865
- const fullPath = path2.join(rootDir, entry.name);
32489
+ const fullPath = path3.join(rootDir, entry.name);
31866
32490
  try {
31867
- if (preserveDirs?.has(path2.resolve(fullPath)))
32491
+ if (preserveDirs?.has(path3.resolve(fullPath)))
31868
32492
  return;
31869
- const stat = await fs2.lstat(fullPath);
32493
+ const stat = await fs3.lstat(fullPath);
31870
32494
  if (!stat.isDirectory())
31871
32495
  return;
31872
32496
  if (stat.mtimeMs < cutoff) {
31873
- await fs2.rm(fullPath, { recursive: true, force: true });
32497
+ await fs3.rm(fullPath, { recursive: true, force: true });
31874
32498
  }
31875
32499
  } catch (err) {
31876
32500
  log?.warn?.(`agent-core: failed to clean attachment temp dir ${fullPath}: ${String(err)}`);
@@ -31882,7 +32506,7 @@ async function pruneAttachmentCache(rootDir, maxBytes, log, preserveDirs) {
31882
32506
  return;
31883
32507
  let entries;
31884
32508
  try {
31885
- entries = await fs2.readdir(rootDir, { withFileTypes: true });
32509
+ entries = await fs3.readdir(rootDir, { withFileTypes: true });
31886
32510
  } catch {
31887
32511
  return;
31888
32512
  }
@@ -31891,9 +32515,9 @@ async function pruneAttachmentCache(rootDir, maxBytes, log, preserveDirs) {
31891
32515
  for (const entry of entries) {
31892
32516
  if (!entry.isDirectory())
31893
32517
  continue;
31894
- const fullPath = path2.join(rootDir, entry.name);
32518
+ const fullPath = path3.join(rootDir, entry.name);
31895
32519
  try {
31896
- const stat = await fs2.lstat(fullPath);
32520
+ const stat = await fs3.lstat(fullPath);
31897
32521
  if (!stat.isDirectory())
31898
32522
  continue;
31899
32523
  const size = await directorySize(fullPath);
@@ -31909,10 +32533,10 @@ async function pruneAttachmentCache(rootDir, maxBytes, log, preserveDirs) {
31909
32533
  for (const dir of dirs) {
31910
32534
  if (total <= maxBytes)
31911
32535
  break;
31912
- if (preserveDirs?.has(path2.resolve(dir.path)))
32536
+ if (preserveDirs?.has(path3.resolve(dir.path)))
31913
32537
  continue;
31914
32538
  try {
31915
- await fs2.rm(dir.path, { recursive: true, force: true });
32539
+ await fs3.rm(dir.path, { recursive: true, force: true });
31916
32540
  total -= dir.size;
31917
32541
  } catch (err) {
31918
32542
  log?.warn?.(`agent-core: failed to prune attachment cache dir ${dir.path}: ${String(err)}`);
@@ -31921,12 +32545,12 @@ async function pruneAttachmentCache(rootDir, maxBytes, log, preserveDirs) {
31921
32545
  }
31922
32546
  async function directorySize(dirPath) {
31923
32547
  let total = 0;
31924
- const entries = await fs2.readdir(dirPath, { withFileTypes: true });
32548
+ const entries = await fs3.readdir(dirPath, { withFileTypes: true });
31925
32549
  for (const entry of entries) {
31926
- const fullPath = path2.join(dirPath, entry.name);
32550
+ const fullPath = path3.join(dirPath, entry.name);
31927
32551
  let stat;
31928
32552
  try {
31929
- stat = await fs2.lstat(fullPath);
32553
+ stat = await fs3.lstat(fullPath);
31930
32554
  } catch {
31931
32555
  continue;
31932
32556
  }
@@ -31941,10 +32565,10 @@ async function directorySize(dirPath) {
31941
32565
  return total;
31942
32566
  }
31943
32567
  function activeDirsForRoot(rootDir) {
31944
- const root = path2.resolve(rootDir);
32568
+ const root = path3.resolve(rootDir);
31945
32569
  const dirs = /* @__PURE__ */ new Set();
31946
32570
  for (const dir of activeAttachmentDirs) {
31947
- if (dir === root || dir.startsWith(`${root}${path2.sep}`)) {
32571
+ if (dir === root || dir.startsWith(`${root}${path3.sep}`)) {
31948
32572
  dirs.add(dir);
31949
32573
  }
31950
32574
  }
@@ -32041,9 +32665,9 @@ async function writeResponseToFileWithLimit(res, filePath, maxBytes, rootDir) {
32041
32665
  }
32042
32666
  writtenStat = await file.stat();
32043
32667
  await closeFile();
32044
- await localDirectoryStatInsideRoot(path2.dirname(filePath), rootDir);
32668
+ await localDirectoryStatInsideRoot(path3.dirname(filePath), rootDir);
32045
32669
  await assertLocalFileIdentity(tmpPath, rootDir, writtenStat);
32046
- await fs2.rename(tmpPath, filePath);
32670
+ await fs3.rename(tmpPath, filePath);
32047
32671
  completed = true;
32048
32672
  return written;
32049
32673
  } finally {
@@ -32059,9 +32683,9 @@ async function writeResponseToFileWithLimit(res, filePath, maxBytes, rootDir) {
32059
32683
  }
32060
32684
  }
32061
32685
  function localFileName(attachmentId, fileName, mimeType) {
32062
- const safeName = sanitizePathSegment(path2.basename(fileName || attachmentId));
32063
- const ext = path2.extname(safeName) || extensionForMime(mimeType);
32064
- const stem = path2.basename(safeName, path2.extname(safeName)) || attachmentId;
32686
+ const safeName = sanitizePathSegment(path3.basename(fileName || attachmentId));
32687
+ const ext = path3.extname(safeName) || extensionForMime(mimeType);
32688
+ const stem = path3.basename(safeName, path3.extname(safeName)) || attachmentId;
32065
32689
  return `${sanitizePathSegment(attachmentId)}-${stem}${ext}`;
32066
32690
  }
32067
32691
  function extensionForMime(mimeType) {
@@ -32127,7 +32751,7 @@ function parseContentLength(value) {
32127
32751
  // dist/gateway.js
32128
32752
  import * as crypto2 from "node:crypto";
32129
32753
  import * as os2 from "node:os";
32130
- import * as path6 from "node:path";
32754
+ import * as path7 from "node:path";
32131
32755
 
32132
32756
  // dist/runtime.js
32133
32757
  var runtime = null;
@@ -32185,15 +32809,15 @@ function buildOrchestratorSessionKey(accountId) {
32185
32809
  }
32186
32810
 
32187
32811
  // dist/config-manager.js
32188
- import * as fs3 from "node:fs";
32189
- import * as path3 from "node:path";
32812
+ import * as fs4 from "node:fs";
32813
+ import * as path4 from "node:path";
32190
32814
  var CACHE_FILENAME = "parall-platform-config.json";
32191
32815
  function cachePath(stateDir) {
32192
- return path3.join(stateDir, CACHE_FILENAME);
32816
+ return path4.join(stateDir, CACHE_FILENAME);
32193
32817
  }
32194
32818
  function loadCachedConfig(stateDir) {
32195
32819
  try {
32196
- const raw = fs3.readFileSync(cachePath(stateDir), "utf-8");
32820
+ const raw = fs4.readFileSync(cachePath(stateDir), "utf-8");
32197
32821
  return JSON.parse(raw);
32198
32822
  } catch {
32199
32823
  return null;
@@ -32207,14 +32831,14 @@ function saveCachedConfig(stateDir, config) {
32207
32831
  };
32208
32832
  const filePath = cachePath(stateDir);
32209
32833
  const tmpPath = `${filePath}.tmp`;
32210
- fs3.mkdirSync(path3.dirname(filePath), { recursive: true });
32211
- fs3.writeFileSync(tmpPath, JSON.stringify(cached, null, 2), "utf-8");
32212
- fs3.renameSync(tmpPath, filePath);
32834
+ fs4.mkdirSync(path4.dirname(filePath), { recursive: true });
32835
+ fs4.writeFileSync(tmpPath, JSON.stringify(cached, null, 2), "utf-8");
32836
+ fs4.renameSync(tmpPath, filePath);
32213
32837
  }
32214
32838
  function applyToOpenClawConfig(configPath, platformConfig, credentials) {
32215
32839
  let existing = {};
32216
32840
  try {
32217
- const raw = fs3.readFileSync(configPath, "utf-8");
32841
+ const raw = fs4.readFileSync(configPath, "utf-8");
32218
32842
  existing = JSON.parse(raw);
32219
32843
  } catch {
32220
32844
  }
@@ -32286,9 +32910,9 @@ function applyToOpenClawConfig(configPath, platformConfig, credentials) {
32286
32910
  agents.defaults = cleanedExisting;
32287
32911
  existing.agents = agents;
32288
32912
  const tmpPath = `${configPath}.tmp`;
32289
- fs3.mkdirSync(path3.dirname(configPath), { recursive: true });
32290
- fs3.writeFileSync(tmpPath, JSON.stringify(existing, null, 2), "utf-8");
32291
- fs3.renameSync(tmpPath, configPath);
32913
+ fs4.mkdirSync(path4.dirname(configPath), { recursive: true });
32914
+ fs4.writeFileSync(tmpPath, JSON.stringify(existing, null, 2), "utf-8");
32915
+ fs4.renameSync(tmpPath, configPath);
32292
32916
  }
32293
32917
  async function fetchAndApplyPlatformConfig(opts) {
32294
32918
  const { client, stateDir, configPath, credentials, log } = opts;
@@ -32327,7 +32951,7 @@ async function fetchAndApplyPlatformConfig(opts) {
32327
32951
 
32328
32952
  // dist/wiki-helper.js
32329
32953
  import { spawn, spawnSync } from "node:child_process";
32330
- import path4 from "node:path";
32954
+ import path5 from "node:path";
32331
32955
  var DEFAULT_SYNC_TIMEOUT_MS = 9e4;
32332
32956
  var DEFAULT_WATCH_INTERVAL_SEC = 30;
32333
32957
  function isCommandMissing(error) {
@@ -32342,7 +32966,7 @@ function resolveParallCli() {
32342
32966
  return _cli;
32343
32967
  }
32344
32968
  function resolveMountRoot(stateDir) {
32345
- return process.env.PRLL_WIKI_MOUNT_ROOT?.trim() || path4.join(stateDir, "workspace");
32969
+ return process.env.PRLL_WIKI_MOUNT_ROOT?.trim() || path5.join(stateDir, "workspace");
32346
32970
  }
32347
32971
  function resolveWatchIntervalSec() {
32348
32972
  const raw = process.env.PRLL_WIKI_REFRESH_INTERVAL_SEC?.trim();
@@ -32458,7 +33082,7 @@ async function startWikiHelper(params) {
32458
33082
  // dist/oc-session.js
32459
33083
  import { randomUUID } from "node:crypto";
32460
33084
  import { existsSync as existsSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
32461
- import { join as join3, resolve as resolve2 } from "node:path";
33085
+ import { join as join4, resolve as resolve2 } from "node:path";
32462
33086
  var CURRENT_SESSION_VERSION = 3;
32463
33087
  function generateId(existing) {
32464
33088
  for (let i = 0; i < 100; i++) {
@@ -32604,7 +33228,7 @@ var SessionManager = class _SessionManager {
32604
33228
  this.leafId = null;
32605
33229
  this.flushed = false;
32606
33230
  const ts = timestamp.replace(/[:.]/g, "-");
32607
- this.sessionFile = join3(this.sessionDir, `${ts}_${this.sessionId}.jsonl`);
33231
+ this.sessionFile = join4(this.sessionDir, `${ts}_${this.sessionId}.jsonl`);
32608
33232
  }
32609
33233
  buildIndex() {
32610
33234
  this.byId.clear();
@@ -32651,14 +33275,14 @@ var SessionManager = class _SessionManager {
32651
33275
  }
32652
33276
  // -- Branching -------------------------------------------------------------
32653
33277
  getBranch(fromId) {
32654
- const path7 = [];
33278
+ const path8 = [];
32655
33279
  const startId = fromId ?? this.leafId;
32656
33280
  let current = startId ? this.byId.get(startId) : void 0;
32657
33281
  while (current) {
32658
- path7.unshift(current);
33282
+ path8.unshift(current);
32659
33283
  current = current.parentId ? this.byId.get(current.parentId) : void 0;
32660
33284
  }
32661
- return path7;
33285
+ return path8;
32662
33286
  }
32663
33287
  createBranchedSession(leafId) {
32664
33288
  const branch = this.getBranch(leafId);
@@ -32668,7 +33292,7 @@ var SessionManager = class _SessionManager {
32668
33292
  const newId = randomUUID();
32669
33293
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
32670
33294
  const ts = timestamp.replace(/[:.]/g, "-");
32671
- const newFile = join3(this.sessionDir, `${ts}_${newId}.jsonl`);
33295
+ const newFile = join4(this.sessionDir, `${ts}_${newId}.jsonl`);
32672
33296
  const header = {
32673
33297
  type: "session",
32674
33298
  version: CURRENT_SESSION_VERSION,
@@ -32714,50 +33338,50 @@ var SessionManager = class _SessionManager {
32714
33338
  return newFile;
32715
33339
  }
32716
33340
  // -- Factory ---------------------------------------------------------------
32717
- static open(path7) {
32718
- const entries = loadEntries(path7);
33341
+ static open(path8) {
33342
+ const entries = loadEntries(path8);
32719
33343
  const header = entries.find((e) => e.type === "session");
32720
33344
  const cwd = header?.cwd ?? process.cwd();
32721
- const dir = resolve2(path7, "..");
32722
- return new _SessionManager(cwd, dir, path7);
33345
+ const dir = resolve2(path8, "..");
33346
+ return new _SessionManager(cwd, dir, path8);
32723
33347
  }
32724
33348
  };
32725
33349
 
32726
33350
  // dist/fork.js
32727
- import * as fs4 from "node:fs";
32728
- import * as path5 from "node:path";
33351
+ import * as fs5 from "node:fs";
33352
+ import * as path6 from "node:path";
32729
33353
  import * as crypto from "node:crypto";
32730
33354
  function readStoreEntry(sessionsDir, sessionKey) {
32731
- const storeFile = path5.join(sessionsDir, "sessions.json");
33355
+ const storeFile = path6.join(sessionsDir, "sessions.json");
32732
33356
  try {
32733
- const store = JSON.parse(fs4.readFileSync(storeFile, "utf-8"));
33357
+ const store = JSON.parse(fs5.readFileSync(storeFile, "utf-8"));
32734
33358
  return store[sessionKey] ?? store[sessionKey.toLowerCase()] ?? null;
32735
33359
  } catch {
32736
33360
  return null;
32737
33361
  }
32738
33362
  }
32739
33363
  function writeStoreEntry(sessionsDir, sessionKey, entry) {
32740
- const storeFile = path5.join(sessionsDir, "sessions.json");
33364
+ const storeFile = path6.join(sessionsDir, "sessions.json");
32741
33365
  try {
32742
33366
  let store = {};
32743
33367
  try {
32744
- store = JSON.parse(fs4.readFileSync(storeFile, "utf-8"));
33368
+ store = JSON.parse(fs5.readFileSync(storeFile, "utf-8"));
32745
33369
  } catch {
32746
33370
  }
32747
33371
  store[sessionKey.toLowerCase()] = entry;
32748
- fs4.writeFileSync(storeFile, JSON.stringify(store, null, 2), { encoding: "utf-8" });
33372
+ fs5.writeFileSync(storeFile, JSON.stringify(store, null, 2), { encoding: "utf-8" });
32749
33373
  return true;
32750
33374
  } catch {
32751
33375
  return false;
32752
33376
  }
32753
33377
  }
32754
33378
  function deleteStoreEntry(sessionsDir, sessionKey) {
32755
- const storeFile = path5.join(sessionsDir, "sessions.json");
33379
+ const storeFile = path6.join(sessionsDir, "sessions.json");
32756
33380
  try {
32757
- const store = JSON.parse(fs4.readFileSync(storeFile, "utf-8"));
33381
+ const store = JSON.parse(fs5.readFileSync(storeFile, "utf-8"));
32758
33382
  delete store[sessionKey];
32759
33383
  delete store[sessionKey.toLowerCase()];
32760
- fs4.writeFileSync(storeFile, JSON.stringify(store, null, 2), { encoding: "utf-8" });
33384
+ fs5.writeFileSync(storeFile, JSON.stringify(store, null, 2), { encoding: "utf-8" });
32761
33385
  } catch {
32762
33386
  }
32763
33387
  }
@@ -32769,17 +33393,17 @@ function resolveTranscriptFile(sessionsDir, sessionKey) {
32769
33393
  if (!entry?.sessionId)
32770
33394
  return null;
32771
33395
  if (entry.sessionFile) {
32772
- const resolved = path5.isAbsolute(entry.sessionFile) ? entry.sessionFile : path5.join(sessionsDir, entry.sessionFile);
32773
- if (fs4.existsSync(resolved))
33396
+ const resolved = path6.isAbsolute(entry.sessionFile) ? entry.sessionFile : path6.join(sessionsDir, entry.sessionFile);
33397
+ if (fs5.existsSync(resolved))
32774
33398
  return resolved;
32775
33399
  }
32776
- const conventional = path5.join(sessionsDir, `${entry.sessionId}.jsonl`);
32777
- if (fs4.existsSync(conventional))
33400
+ const conventional = path6.join(sessionsDir, `${entry.sessionId}.jsonl`);
33401
+ if (fs5.existsSync(conventional))
32778
33402
  return conventional;
32779
33403
  try {
32780
- const files = fs4.readdirSync(sessionsDir);
33404
+ const files = fs5.readdirSync(sessionsDir);
32781
33405
  const match = files.find((file) => file.includes(entry.sessionId) && file.endsWith(".jsonl"));
32782
- return match ? path5.join(sessionsDir, match) : null;
33406
+ return match ? path6.join(sessionsDir, match) : null;
32783
33407
  } catch {
32784
33408
  return null;
32785
33409
  }
@@ -32790,7 +33414,7 @@ function resolveSessionId(sessionsDir, sessionKey) {
32790
33414
  }
32791
33415
  function forkOrchestratorSession(opts) {
32792
33416
  const { orchestratorSessionKey, accountId, transcriptFile, sessionsDir } = opts;
32793
- if (!fs4.existsSync(transcriptFile))
33417
+ if (!fs5.existsSync(transcriptFile))
32794
33418
  return null;
32795
33419
  try {
32796
33420
  const manager = SessionManager.open(transcriptFile);
@@ -32810,7 +33434,7 @@ function forkOrchestratorSession(opts) {
32810
33434
  sessionId = crypto.randomUUID();
32811
33435
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
32812
33436
  const fileTimestamp = timestamp.replace(/[:.]/g, "-");
32813
- sessionFile = path5.join(manager.getSessionDir(), `${fileTimestamp}_${sessionId}.jsonl`);
33437
+ sessionFile = path6.join(manager.getSessionDir(), `${fileTimestamp}_${sessionId}.jsonl`);
32814
33438
  const header = {
32815
33439
  type: "session",
32816
33440
  version: CURRENT_SESSION_VERSION,
@@ -32819,7 +33443,7 @@ function forkOrchestratorSession(opts) {
32819
33443
  cwd: manager.getCwd(),
32820
33444
  parentSession: transcriptFile
32821
33445
  };
32822
- fs4.writeFileSync(sessionFile, `${JSON.stringify(header)}
33446
+ fs5.writeFileSync(sessionFile, `${JSON.stringify(header)}
32823
33447
  `, {
32824
33448
  encoding: "utf-8",
32825
33449
  mode: 384,
@@ -32829,7 +33453,7 @@ function forkOrchestratorSession(opts) {
32829
33453
  const forkSessionKey = `${orchestratorSessionKey}:fork:${sessionId}`;
32830
33454
  const wrote = writeStoreEntry(sessionsDir, forkSessionKey, {
32831
33455
  sessionId,
32832
- sessionFile: path5.relative(sessionsDir, sessionFile),
33456
+ sessionFile: path6.relative(sessionsDir, sessionFile),
32833
33457
  updatedAt: Date.now(),
32834
33458
  spawnedBy: orchestratorSessionKey,
32835
33459
  parentSessionKey: orchestratorSessionKey,
@@ -32844,8 +33468,8 @@ function forkOrchestratorSession(opts) {
32844
33468
  }
32845
33469
  function cleanupForkSession(opts) {
32846
33470
  try {
32847
- if (fs4.existsSync(opts.sessionFile)) {
32848
- fs4.unlinkSync(opts.sessionFile);
33471
+ if (fs5.existsSync(opts.sessionFile)) {
33472
+ fs5.unlinkSync(opts.sessionFile);
32849
33473
  }
32850
33474
  } catch {
32851
33475
  }
@@ -33155,8 +33779,8 @@ var parallGateway = {
33155
33779
  const telemetry = await initAgentTelemetry("parall-openclaw-agent", "openclaw");
33156
33780
  const otelLog = createOtelLogger("agent", "openclaw-agent");
33157
33781
  try {
33158
- const stateDir = process.env.OPENCLAW_STATE_DIR || path6.join(process.env.HOME || "/data", ".openclaw");
33159
- const openclawConfigPath = path6.join(stateDir, "openclaw.json");
33782
+ const stateDir = process.env.OPENCLAW_STATE_DIR || path7.join(process.env.HOME || "/data", ".openclaw");
33783
+ const openclawConfigPath = path7.join(stateDir, "openclaw.json");
33160
33784
  const configManagerOpts = {
33161
33785
  client,
33162
33786
  stateDir,
@@ -33192,7 +33816,7 @@ var parallGateway = {
33192
33816
  wsUrl
33193
33817
  });
33194
33818
  const orchestratorKey = buildOrchestratorSessionKey(ctx.accountId);
33195
- const sessionsDir = path6.join(stateDir, "agents", "main", "sessions");
33819
+ const sessionsDir = path7.join(stateDir, "agents", "main", "sessions");
33196
33820
  const workspaceDir = process.cwd();
33197
33821
  ensureLocalAttachmentGitExclude(workspaceDir);
33198
33822
  const dispatchAdapter = createOpenClawDispatchAdapter({