@parall/parall 1.37.0 → 1.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.bundle.mjs +883 -233
  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`,
@@ -27358,7 +27373,13 @@ var ENDPOINTS = {
27358
27373
  // the machine control-plane request/reply bridge that lives in api-server.
27359
27374
  BROWSER_PROFILE_VIEWER_COMMAND: (orgId, profileId) => `${API_BASE}/orgs/${orgId}/browser-profiles/${profileId}/viewer/command`,
27360
27375
  // Clip registry (global, served by clip-service → Pinix Hub proxy)
27361
- CLIP_REGISTRY: () => `${CLIP_BASE}/registry/clips`
27376
+ CLIP_REGISTRY: () => `${CLIP_BASE}/registry/clips`,
27377
+ // Edge device endpoints
27378
+ ORG_EDGE_DEVICES: (orgId) => `/api/v1/orgs/${orgId}/edge/devices`,
27379
+ ORG_EDGE_ONBOARDING: (orgId) => `/api/v1/orgs/${orgId}/edge/onboarding`,
27380
+ ORG_EDGE_PROFILES: (orgId, edgeId) => `/api/v1/orgs/${orgId}/edge/${edgeId}/profiles`,
27381
+ CLIP_CONNECTIONS: (orgId, clipId) => `/api/v1/orgs/${orgId}/clip-registry/${clipId}/connections`,
27382
+ CLIP_CONNECTION: (orgId, connId) => `/api/v1/orgs/${orgId}/clip-connections/${connId}`
27362
27383
  };
27363
27384
  var WS_EVENTS = {
27364
27385
  // Client -> Server
@@ -27417,6 +27438,7 @@ var WS_EVENTS = {
27417
27438
  READ_POSITION_UPDATED: "read_position.updated",
27418
27439
  DISPATCH_NEW: "dispatch.new",
27419
27440
  DISPATCH_RECEIVED: "dispatch.received",
27441
+ DISPATCH_RESOLVED: "dispatch.resolved",
27420
27442
  SCHEDULE_CREATED: "schedule.created",
27421
27443
  SCHEDULE_UPDATED: "schedule.updated",
27422
27444
  SCHEDULE_DELETED: "schedule.deleted",
@@ -27512,8 +27534,8 @@ var ParallClient = class _ParallClient {
27512
27534
  * is authoritative, so wiki vs api routing can't drift from how a caller
27513
27535
  * happens to invoke the client.
27514
27536
  */
27515
- baseUrlFor(path7) {
27516
- return path7.startsWith(WIKI_BASE) ? this.wikiBaseUrl : this.baseUrl;
27537
+ baseUrlFor(path8) {
27538
+ return path8.startsWith(WIKI_BASE) ? this.wikiBaseUrl : this.baseUrl;
27517
27539
  }
27518
27540
  setToken(token) {
27519
27541
  this.token = token;
@@ -27540,10 +27562,10 @@ var ParallClient = class _ParallClient {
27540
27562
  * REFRESH_THRESHOLD_S, refresh it **before** sending the request.
27541
27563
  * No-op when the token is still fresh, missing, or un-parseable.
27542
27564
  */
27543
- async ensureFreshToken(path7) {
27565
+ async ensureFreshToken(path8) {
27544
27566
  if (!this.token || !this.getRefreshToken)
27545
27567
  return;
27546
- const pathSuffix = path7.replace(/^\/api\/v1/, "");
27568
+ const pathSuffix = path8.replace(/^\/api\/v1/, "");
27547
27569
  if (_ParallClient.AUTH_PATHS.has(pathSuffix))
27548
27570
  return;
27549
27571
  const exp = _ParallClient.decodeJwtExp(this.token);
@@ -27575,11 +27597,11 @@ var ParallClient = class _ParallClient {
27575
27597
  this.refreshPromise = null;
27576
27598
  }
27577
27599
  }
27578
- async request(method, path7, body, query, retried = false, opts) {
27600
+ async request(method, path8, body, query, retried = false, opts) {
27579
27601
  if (!retried) {
27580
- await this.ensureFreshToken(path7);
27602
+ await this.ensureFreshToken(path8);
27581
27603
  }
27582
- let url = `${this.baseUrlFor(path7)}${path7}`;
27604
+ let url = `${this.baseUrlFor(path8)}${path8}`;
27583
27605
  if (query) {
27584
27606
  const params = new URLSearchParams();
27585
27607
  for (const [key, value] of Object.entries(query)) {
@@ -27609,12 +27631,12 @@ var ParallClient = class _ParallClient {
27609
27631
  throw _ParallClient.normalizeFetchError(err);
27610
27632
  }
27611
27633
  if (res.status === 401) {
27612
- const pathSuffix = path7.replace(/^\/api\/v1/, "");
27634
+ const pathSuffix = path8.replace(/^\/api\/v1/, "");
27613
27635
  const isAuthPath = _ParallClient.AUTH_PATHS.has(pathSuffix);
27614
27636
  if (!retried && !isAuthPath && this.getRefreshToken) {
27615
27637
  const refreshed = await this.tryRefresh();
27616
27638
  if (refreshed) {
27617
- return this.request(method, path7, body, query, true, opts);
27639
+ return this.request(method, path8, body, query, true, opts);
27618
27640
  }
27619
27641
  }
27620
27642
  if (this.onTokenExpired && !isAuthPath) {
@@ -27625,6 +27647,7 @@ var ParallClient = class _ParallClient {
27625
27647
  const rawErrorBody = await res.json().catch(() => ({}));
27626
27648
  throw buildApiError(res, rawErrorBody);
27627
27649
  }
27650
+ opts?.onStatus?.(res.status);
27628
27651
  if (res.status === 204)
27629
27652
  return void 0;
27630
27653
  if (res.status === 202) {
@@ -27643,15 +27666,15 @@ var ParallClient = class _ParallClient {
27643
27666
  * hit the 100 MiB cap, so a longer 5-minute timeout is used so a
27644
27667
  * 50 MiB blob on a slow connection doesn't get chopped at 15 s.
27645
27668
  */
27646
- async multipartRequest(method, path7, body, retried = false) {
27669
+ async multipartRequest(method, path8, body, retried = false) {
27647
27670
  if (!retried) {
27648
- await this.ensureFreshToken(path7);
27671
+ await this.ensureFreshToken(path8);
27649
27672
  }
27650
27673
  const { "Content-Type": _drop, ...headers } = this.buildHeaders();
27651
27674
  void _drop;
27652
27675
  let res;
27653
27676
  try {
27654
- res = await fetch(`${this.baseUrlFor(path7)}${path7}`, {
27677
+ res = await fetch(`${this.baseUrlFor(path8)}${path8}`, {
27655
27678
  method,
27656
27679
  headers,
27657
27680
  body,
@@ -27661,12 +27684,12 @@ var ParallClient = class _ParallClient {
27661
27684
  throw _ParallClient.normalizeFetchError(err);
27662
27685
  }
27663
27686
  if (res.status === 401) {
27664
- const pathSuffix = path7.replace(/^\/api\/v1/, "");
27687
+ const pathSuffix = path8.replace(/^\/api\/v1/, "");
27665
27688
  const isAuthPath = _ParallClient.AUTH_PATHS.has(pathSuffix);
27666
27689
  if (!retried && !isAuthPath && this.getRefreshToken) {
27667
27690
  const refreshed = await this.tryRefresh();
27668
27691
  if (refreshed) {
27669
- return this.multipartRequest(method, path7, body, true);
27692
+ return this.multipartRequest(method, path8, body, true);
27670
27693
  }
27671
27694
  }
27672
27695
  if (this.onTokenExpired && !isAuthPath) {
@@ -27958,6 +27981,21 @@ var ParallClient = class _ParallClient {
27958
27981
  async sendMessage(orgId, chatId, req) {
27959
27982
  return this.request("POST", ENDPOINTS.CHAT_MESSAGES(orgId, chatId), req);
27960
27983
  }
27984
+ /**
27985
+ * sendMessage variant that also reports whether the server answered with an
27986
+ * idempotent replay (HTTP 200 — the message already existed for this
27987
+ * idempotency/effect key) instead of a fresh create (201). Used by the CLI
27988
+ * to surface "already sent by a previous run (deduplicated)".
27989
+ */
27990
+ async sendMessageDetailed(orgId, chatId, req) {
27991
+ let status = 0;
27992
+ const message = await this.request("POST", ENDPOINTS.CHAT_MESSAGES(orgId, chatId), req, void 0, false, {
27993
+ onStatus: (s) => {
27994
+ status = s;
27995
+ }
27996
+ });
27997
+ return { message, deduplicated: status === 200 };
27998
+ }
27961
27999
  async getMessages(orgId, chatId, params) {
27962
28000
  return this.request("GET", ENDPOINTS.CHAT_MESSAGES(orgId, chatId), void 0, params);
27963
28001
  }
@@ -28345,8 +28383,8 @@ var ParallClient = class _ParallClient {
28345
28383
  async requestMachineUpdate(orgId, machineId, mandatory = false) {
28346
28384
  await this.request("POST", ENDPOINTS.MACHINE_REQUEST_UPDATE(orgId, machineId), { mandatory });
28347
28385
  }
28348
- async browseMachineFilesystem(orgId, machineId, path7) {
28349
- return this.request("POST", ENDPOINTS.MACHINE_BROWSE(orgId, machineId), { path: path7 }, void 0, false, { timeoutMs: 15e3 });
28386
+ async browseMachineFilesystem(orgId, machineId, path8) {
28387
+ return this.request("POST", ENDPOINTS.MACHINE_BROWSE(orgId, machineId), { path: path8 }, void 0, false, { timeoutMs: 15e3 });
28350
28388
  }
28351
28389
  /** Create a new machine key. Returns the raw key string (shown once) + metadata. */
28352
28390
  async createMachineKey(orgId, machineId, name) {
@@ -28419,6 +28457,30 @@ var ParallClient = class _ParallClient {
28419
28457
  const params = maxAgeHours !== void 0 ? { max_age_hours: String(maxAgeHours) } : void 0;
28420
28458
  return this.request("POST", ENDPOINTS.DISPATCH_EXPIRE(orgId), void 0, params);
28421
28459
  }
28460
+ /**
28461
+ * Claim a dispatch lane (explicit consume endpoint). Occupies the
28462
+ * (agent, target, thread) lane and folds claimable WorkItems into it;
28463
+ * `claimed: false` means a healthy incumbent holds the lane.
28464
+ */
28465
+ async claimDispatch(orgId, req) {
28466
+ return this.request("POST", ENDPOINTS.DISPATCH_CLAIM(orgId), req);
28467
+ }
28468
+ /** Fold a pending same-target WorkItem into a live lane (409 STALE_LANE when dethroned). */
28469
+ async steerDispatch(orgId, req) {
28470
+ return this.request("POST", ENDPOINTS.DISPATCH_STEER(orgId), req);
28471
+ }
28472
+ /** End a turn: no_action sweep of the lane's members + lane release + re-drive check. */
28473
+ async completeDispatch(orgId, req) {
28474
+ return this.request("POST", ENDPOINTS.DISPATCH_COMPLETE(orgId), req);
28475
+ }
28476
+ /** Release a lane on graceful shutdown — members return to pending immediately. */
28477
+ async releaseDispatchLane(orgId, lane) {
28478
+ return this.request("POST", ENDPOINTS.DISPATCH_RELEASE(orgId), { lane });
28479
+ }
28480
+ /** Renew a live lane's lease (long-turn keepalive). 409 STALE_LANE when dethroned. */
28481
+ async heartbeatDispatchLane(orgId, req) {
28482
+ return this.request("POST", ENDPOINTS.DISPATCH_HEARTBEAT(orgId), req);
28483
+ }
28422
28484
  async getDispatchByMessages(orgId, chatId, messageIds) {
28423
28485
  const params = { chat_id: chatId, message_ids: messageIds.join(",") };
28424
28486
  return this.request("GET", ENDPOINTS.DISPATCH_BY_MESSAGES(orgId), void 0, params);
@@ -28883,8 +28945,8 @@ var ParallClient = class _ParallClient {
28883
28945
  async deleteWikiRestriction(orgId, wikiId, restrictionId) {
28884
28946
  await this.request("DELETE", ENDPOINTS.WIKI_RESTRICTION(orgId, wikiId, restrictionId));
28885
28947
  }
28886
- async getWikiAccessStatus(orgId, wikiId, path7) {
28887
- return this.request("GET", ENDPOINTS.WIKI_ACCESS_STATUS(orgId, wikiId), void 0, path7 ? { path: path7 } : void 0);
28948
+ async getWikiAccessStatus(orgId, wikiId, path8) {
28949
+ return this.request("GET", ENDPOINTS.WIKI_ACCESS_STATUS(orgId, wikiId), void 0, path8 ? { path: path8 } : void 0);
28888
28950
  }
28889
28951
  async createWikiAccessRequest(orgId, wikiId, data) {
28890
28952
  await this.request("POST", ENDPOINTS.WIKI_ACCESS_REQUESTS(orgId, wikiId), data);
@@ -28893,14 +28955,14 @@ var ParallClient = class _ParallClient {
28893
28955
  async getWikiCommits(orgId, wikiId, params) {
28894
28956
  return this.request("GET", ENDPOINTS.WIKI_COMMITS(orgId, wikiId), void 0, params);
28895
28957
  }
28896
- async getWikiFileCommits(orgId, wikiId, path7, params) {
28958
+ async getWikiFileCommits(orgId, wikiId, path8, params) {
28897
28959
  return this.request("GET", ENDPOINTS.WIKI_FILE_COMMITS(orgId, wikiId), void 0, {
28898
- path: path7,
28960
+ path: path8,
28899
28961
  ...params
28900
28962
  });
28901
28963
  }
28902
- async getWikiBlame(orgId, wikiId, path7, ref) {
28903
- return this.request("GET", ENDPOINTS.WIKI_BLAME(orgId, wikiId), void 0, { path: path7, ref });
28964
+ async getWikiBlame(orgId, wikiId, path8, ref) {
28965
+ return this.request("GET", ENDPOINTS.WIKI_BLAME(orgId, wikiId), void 0, { path: path8, ref });
28904
28966
  }
28905
28967
  // ---- Wiki Operations (audit log) ----
28906
28968
  async getWikiOperations(orgId, wikiId, params) {
@@ -29146,6 +29208,26 @@ var ParallClient = class _ParallClient {
29146
29208
  const resp = await this.request("GET", url);
29147
29209
  return resp.data;
29148
29210
  }
29211
+ // ---- Edge devices ----
29212
+ async listEdgeDevices(orgId) {
29213
+ return this.request("GET", ENDPOINTS.ORG_EDGE_DEVICES(orgId));
29214
+ }
29215
+ async getEdgeOnboarding(orgId) {
29216
+ return this.request("GET", ENDPOINTS.ORG_EDGE_ONBOARDING(orgId));
29217
+ }
29218
+ async listEdgeProfiles(orgId, edgeId) {
29219
+ return this.request("GET", ENDPOINTS.ORG_EDGE_PROFILES(orgId, edgeId));
29220
+ }
29221
+ // ---- Clip connections ----
29222
+ async listClipConnections(orgId, clipId) {
29223
+ return this.request("GET", ENDPOINTS.CLIP_CONNECTIONS(orgId, clipId));
29224
+ }
29225
+ async createClipConnection(orgId, clipId, input) {
29226
+ return this.request("POST", ENDPOINTS.CLIP_CONNECTIONS(orgId, clipId), input);
29227
+ }
29228
+ async deleteClipConnection(orgId, connId) {
29229
+ return this.request("DELETE", ENDPOINTS.CLIP_CONNECTION(orgId, connId));
29230
+ }
29149
29231
  };
29150
29232
  function normalizeWikiChangeset(changeset) {
29151
29233
  return {
@@ -29532,6 +29614,404 @@ var ParallWs = class {
29532
29614
  }
29533
29615
  };
29534
29616
 
29617
+ // ../agent-core/dist/lane-ledger.js
29618
+ import * as fs from "node:fs";
29619
+ var LedgerUnsupportedError = class extends Error {
29620
+ };
29621
+ function isStaleLane(err) {
29622
+ return err instanceof ApiError && err.status === 409 && err.code === "STALE_LANE";
29623
+ }
29624
+ function isEndpointMissing(err) {
29625
+ return err instanceof ApiError && err.status === 404 && !err.code;
29626
+ }
29627
+ var LaneLedger = class {
29628
+ opts;
29629
+ lanes = /* @__PURE__ */ new Map();
29630
+ constructor(opts) {
29631
+ this.opts = opts;
29632
+ }
29633
+ get contextDir() {
29634
+ return this.opts.contextDir;
29635
+ }
29636
+ /** Only chat message events ride the lane ledger; typed events stay on the legacy ack path. */
29637
+ handles(event) {
29638
+ return event.type === "message" && event.targetId.startsWith("cht_");
29639
+ }
29640
+ laneKeyFor(event) {
29641
+ if (event.type !== "message" && event.dispatchEventId) {
29642
+ return laneKeyForTarget(`dsp:${event.dispatchEventId}`);
29643
+ }
29644
+ return laneKeyForTarget(`prll://${event.targetId}`, event.threadRootId);
29645
+ }
29646
+ getForEvent(event) {
29647
+ return this.lanes.get(this.laneKeyFor(event));
29648
+ }
29649
+ laneContextPath(lane) {
29650
+ return laneContextFilePath(this.opts.contextDir, lane.targetUri, lane.threadRootId);
29651
+ }
29652
+ /**
29653
+ * Claim (or reuse) the lane for a group of same-lane message events and
29654
+ * fold every group member into it. Returns 'foreign' when a healthy
29655
+ * incumbent (another pod) holds the resource — the caller must not
29656
+ * dispatch; the events stay pending server-side and re-drive after the
29657
+ * incumbent completes.
29658
+ */
29659
+ async ensureLane(events) {
29660
+ const trigger = events[events.length - 1];
29661
+ const laneKey = this.laneKeyFor(trigger);
29662
+ let lane = this.lanes.get(laneKey);
29663
+ if (!lane) {
29664
+ const targetUri = `prll://${trigger.targetId}`;
29665
+ let res;
29666
+ try {
29667
+ res = await this.opts.client.claimDispatch(this.opts.orgId, {
29668
+ target_uri: targetUri,
29669
+ thread_root_id: trigger.threadRootId,
29670
+ limit: 100
29671
+ });
29672
+ } catch (err) {
29673
+ if (isEndpointMissing(err))
29674
+ throw new LedgerUnsupportedError("claim endpoint unavailable");
29675
+ throw err;
29676
+ }
29677
+ if (!res.claimed || !res.lane) {
29678
+ this.opts.log?.info(`lane for ${targetUri} held by a healthy incumbent \u2014 leaving events pending for re-drive`);
29679
+ return null;
29680
+ }
29681
+ const leaseUntilMs = Date.parse(res.lease_until ?? "");
29682
+ lane = {
29683
+ laneKey,
29684
+ lane: res.lane,
29685
+ targetUri,
29686
+ threadRootId: trigger.threadRootId,
29687
+ folded: /* @__PURE__ */ new Map(),
29688
+ ...Number.isNaN(leaseUntilMs) ? {} : { leaseUntilMs, leaseTtlMs: Math.max(leaseUntilMs - Date.now(), 6e4) }
29689
+ };
29690
+ for (const ev of res.events ?? []) {
29691
+ lane.folded.set(ev.source_id, ev.id);
29692
+ }
29693
+ this.lanes.set(laneKey, lane);
29694
+ }
29695
+ for (const ev of events) {
29696
+ if (lane.folded.has(ev.messageId))
29697
+ continue;
29698
+ try {
29699
+ const res = await this.opts.client.steerDispatch(this.opts.orgId, {
29700
+ lane: lane.lane,
29701
+ target_uri: lane.targetUri,
29702
+ thread_root_id: lane.threadRootId,
29703
+ ...ev.dispatchEventId ? { dispatch_event_id: ev.dispatchEventId } : { source_type: "message", source_id: ev.messageId }
29704
+ });
29705
+ lane.folded.set(ev.messageId, res.dispatch_event_id);
29706
+ } catch (err) {
29707
+ if (isStaleLane(err)) {
29708
+ this.lanes.delete(laneKey);
29709
+ return null;
29710
+ }
29711
+ this.opts.log?.warn(`steer fold failed for ${ev.messageId} \u2014 failing closed, releasing lane: ${String(err)}`);
29712
+ await this.release(laneKey);
29713
+ return null;
29714
+ }
29715
+ }
29716
+ return lane;
29717
+ }
29718
+ /**
29719
+ * Fold a live mid-turn message into its active lane BEFORE injecting it
29720
+ * into the running turn. Injection without a successful fold is forbidden —
29721
+ * an un-folded injected message would be re-driven after complete and the
29722
+ * model would handle it twice.
29723
+ */
29724
+ async steerLive(event) {
29725
+ const laneKey = this.laneKeyFor(event);
29726
+ const lane = this.lanes.get(laneKey);
29727
+ if (!lane)
29728
+ return false;
29729
+ if (lane.folded.has(event.messageId))
29730
+ return true;
29731
+ try {
29732
+ const res = await this.opts.client.steerDispatch(this.opts.orgId, {
29733
+ lane: lane.lane,
29734
+ target_uri: lane.targetUri,
29735
+ thread_root_id: lane.threadRootId,
29736
+ ...event.dispatchEventId ? { dispatch_event_id: event.dispatchEventId } : { source_type: "message", source_id: event.messageId }
29737
+ });
29738
+ lane.folded.set(event.messageId, res.dispatch_event_id);
29739
+ return true;
29740
+ } catch (err) {
29741
+ if (isStaleLane(err)) {
29742
+ this.lanes.delete(laneKey);
29743
+ } else {
29744
+ this.opts.log?.warn(`live steer failed for ${event.messageId}: ${String(err)}`);
29745
+ }
29746
+ return false;
29747
+ }
29748
+ }
29749
+ /**
29750
+ * Complete the lane when no local work remains for it: the server sweeps
29751
+ * still-leased members as no_action, releases the occupancy row, and
29752
+ * re-drives any same-target pending work. A STALE_LANE answer means a
29753
+ * takeover already owns the resource — local state is dropped either way.
29754
+ */
29755
+ async completeIfIdle(laneKey, hasMoreLocal) {
29756
+ const lane = this.lanes.get(laneKey);
29757
+ if (!lane || hasMoreLocal)
29758
+ return;
29759
+ this.lanes.delete(laneKey);
29760
+ this.removeLaneContext(lane);
29761
+ try {
29762
+ const res = await this.opts.client.completeDispatch(this.opts.orgId, {
29763
+ lane: lane.lane,
29764
+ target_uri: lane.targetUri,
29765
+ thread_root_id: lane.threadRootId
29766
+ });
29767
+ if (res.swept_no_action > 0 || res.redriven) {
29768
+ this.opts.log?.info(`lane complete for ${lane.targetUri}: swept ${res.swept_no_action} no_action, redriven=${res.redriven}`);
29769
+ }
29770
+ } catch (err) {
29771
+ if (isStaleLane(err)) {
29772
+ this.opts.log?.info(`lane complete skipped for ${lane.targetUri} \u2014 taken over`);
29773
+ return;
29774
+ }
29775
+ this.opts.log?.warn(`lane complete failed for ${lane.targetUri}: ${String(err)}`);
29776
+ }
29777
+ }
29778
+ /**
29779
+ * Long-turn keepalive: renew the lane's lease on runtime activity, throttled
29780
+ * so a chatty turn doesn't spam the server. Without this, a legitimately
29781
+ * long turn (> lane TTL) would be dethroned mid-flight and every subsequent
29782
+ * write misfired with STALE_LANE — the design doc's "long turns renew via
29783
+ * step writes". Fire-and-forget: a failed renewal is surfaced by the next
29784
+ * write's incumbency check anyway.
29785
+ */
29786
+ maybeRenew(lane) {
29787
+ const now = Date.now();
29788
+ const ttl = lane.leaseTtlMs ?? 10 * 6e4;
29789
+ const until = lane.leaseUntilMs ?? now;
29790
+ if (until - now > ttl / 2)
29791
+ return;
29792
+ lane.leaseUntilMs = now + ttl;
29793
+ void this.opts.client.heartbeatDispatchLane(this.opts.orgId, {
29794
+ lane: lane.lane,
29795
+ target_uri: lane.targetUri,
29796
+ thread_root_id: lane.threadRootId
29797
+ }).then((res) => {
29798
+ const until2 = Date.parse(res?.lease_until ?? "");
29799
+ if (!Number.isNaN(until2))
29800
+ lane.leaseUntilMs = until2;
29801
+ }).catch((err) => {
29802
+ if (isStaleLane(err)) {
29803
+ this.lanes.delete(lane.laneKey);
29804
+ this.opts.log?.warn(`lane ${lane.targetUri} was taken over during the turn`);
29805
+ return;
29806
+ }
29807
+ this.opts.log?.warn(`lane heartbeat failed for ${lane.targetUri}: ${String(err)}`);
29808
+ });
29809
+ }
29810
+ /**
29811
+ * Release a lane's unresolved members back to pending (dispatch error /
29812
+ * shutdown) so the next pod re-claims immediately instead of waiting out
29813
+ * the lease.
29814
+ */
29815
+ async release(laneKey) {
29816
+ const lane = this.lanes.get(laneKey);
29817
+ if (!lane)
29818
+ return;
29819
+ this.lanes.delete(laneKey);
29820
+ this.removeLaneContext(lane);
29821
+ try {
29822
+ await this.opts.client.releaseDispatchLane(this.opts.orgId, lane.lane);
29823
+ } catch (err) {
29824
+ this.opts.log?.warn(`lane release failed for ${lane.targetUri}: ${String(err)}`);
29825
+ }
29826
+ }
29827
+ async releaseAll() {
29828
+ const keys = [...this.lanes.keys()];
29829
+ for (const key of keys) {
29830
+ await this.release(key);
29831
+ }
29832
+ }
29833
+ /** True when any lane is currently active (used by shutdown logging). */
29834
+ get activeCount() {
29835
+ return this.lanes.size;
29836
+ }
29837
+ /**
29838
+ * Claim the single-member lane of one typed WorkItem (resource = dsp:<id>),
29839
+ * by WorkItem id or by source identity (the live task.assigned event has no
29840
+ * WorkItem id). Returns null when a healthy incumbent (another pod) holds
29841
+ * it or the WorkItem is already resolved — the caller must skip processing.
29842
+ */
29843
+ async claimTyped(ref) {
29844
+ let res;
29845
+ try {
29846
+ res = await this.opts.client.claimDispatch(this.opts.orgId, {
29847
+ dispatch_event_id: ref.dispatchEventId,
29848
+ source_type: ref.dispatchEventId ? void 0 : ref.sourceType,
29849
+ source_id: ref.dispatchEventId ? void 0 : ref.sourceId
29850
+ });
29851
+ } catch (err) {
29852
+ if (isEndpointMissing(err))
29853
+ throw new LedgerUnsupportedError("claim endpoint unavailable");
29854
+ throw err;
29855
+ }
29856
+ if (!res.claimed || !res.lane || !res.events?.length)
29857
+ return null;
29858
+ const workItem = res.events[0];
29859
+ const targetUri = `dsp:${workItem.id}`;
29860
+ const leaseUntilMs = Date.parse(res.lease_until ?? "");
29861
+ const lane = {
29862
+ laneKey: laneKeyForTarget(targetUri),
29863
+ lane: res.lane,
29864
+ targetUri,
29865
+ folded: /* @__PURE__ */ new Map([[workItem.source_id, workItem.id]]),
29866
+ typedDispatchEventId: workItem.id,
29867
+ ...Number.isNaN(leaseUntilMs) ? {} : { leaseUntilMs, leaseTtlMs: Math.max(leaseUntilMs - Date.now(), 6e4) }
29868
+ };
29869
+ this.lanes.set(lane.laneKey, lane);
29870
+ return lane;
29871
+ }
29872
+ /**
29873
+ * Remove the per-lane context file (and its CLI sidecar) when the lane
29874
+ * ends. A leftover file would make a later cross-context send to the same
29875
+ * target bind a dead lane token and misfire with STALE_LANE instead of
29876
+ * taking the plain non-ledger path.
29877
+ */
29878
+ removeLaneContext(lane) {
29879
+ const contextPath = this.laneContextPath(lane);
29880
+ for (const p of [contextPath, contextPath.replace(/\.json$/, ".reply-state.json")]) {
29881
+ try {
29882
+ fs.rmSync(p, { force: true });
29883
+ } catch {
29884
+ }
29885
+ }
29886
+ }
29887
+ };
29888
+
29889
+ // ../agent-core/dist/gateway-lane-flow.js
29890
+ async function dispatchLaneGroup(host, opts) {
29891
+ const ledger = host.laneLedger;
29892
+ const event = opts.events[opts.events.length - 1];
29893
+ let lane;
29894
+ try {
29895
+ lane = await ledger.ensureLane(opts.events);
29896
+ } catch (err) {
29897
+ if (!(err instanceof LedgerUnsupportedError))
29898
+ throw err;
29899
+ host.disableLedger("claim endpoint missing");
29900
+ await host.emitDispatchReceived(event);
29901
+ const dispatched2 = await host.runDispatch(event, opts.sessionKey, opts.body, opts.earlier, opts.captureText);
29902
+ if (!dispatched2)
29903
+ return "shutdown";
29904
+ for (const ev of opts.events) {
29905
+ host.opts.client.ackDispatch(host.opts.config.org_id, {
29906
+ source_type: ev.ackSourceType ?? "message",
29907
+ source_id: ev.ackSourceId ?? ev.messageId
29908
+ }).catch(() => {
29909
+ });
29910
+ }
29911
+ return "dispatched";
29912
+ }
29913
+ if (!lane) {
29914
+ for (const ev of opts.events) {
29915
+ host.dispatchedMessages.delete(ev.messageId);
29916
+ }
29917
+ return "foreign";
29918
+ }
29919
+ let dispatched = false;
29920
+ try {
29921
+ dispatched = await host.runDispatch(event, opts.sessionKey, opts.body, opts.earlier, opts.captureText);
29922
+ } catch (err) {
29923
+ await ledger.release(lane.laneKey).catch(() => {
29924
+ });
29925
+ throw err;
29926
+ }
29927
+ if (!dispatched) {
29928
+ return "shutdown";
29929
+ }
29930
+ const pendingInjections = host.opts.dispatchAdapter.hasPendingInjections?.(opts.sessionKey) ?? false;
29931
+ await ledger.completeIfIdle(lane.laneKey, pendingInjections || opts.hasMoreLocal());
29932
+ return "dispatched";
29933
+ }
29934
+ async function consumeTypedDispatch(host, ref, run, ack) {
29935
+ if (!host.laneLedger || host.ledgerDisabled) {
29936
+ if (await run(ref.dispatchEventId))
29937
+ ack(ref.dispatchEventId);
29938
+ return;
29939
+ }
29940
+ let lane;
29941
+ try {
29942
+ lane = await host.laneLedger.claimTyped(ref);
29943
+ } catch (err) {
29944
+ if (err instanceof LedgerUnsupportedError) {
29945
+ host.disableLedger("claim endpoint missing");
29946
+ if (await run(ref.dispatchEventId))
29947
+ ack(ref.dispatchEventId);
29948
+ return;
29949
+ }
29950
+ throw err;
29951
+ }
29952
+ if (!lane) {
29953
+ host.opts.log?.info(`typed dispatch ${ref.dispatchEventId ?? `${ref.sourceType}:${ref.sourceId}`} not claimable (held elsewhere or already resolved) \u2014 skipping`);
29954
+ return;
29955
+ }
29956
+ try {
29957
+ if (await run(lane.typedDispatchEventId))
29958
+ ack(lane.typedDispatchEventId);
29959
+ } finally {
29960
+ await host.laneLedger.completeIfIdle(lane.laneKey, false).catch(() => {
29961
+ });
29962
+ }
29963
+ }
29964
+ async function consumeMessageWorkItem(host, item) {
29965
+ if (host.shuttingDown)
29966
+ return;
29967
+ if (!host.tryClaimMessage(item.source_id))
29968
+ return;
29969
+ const ackItem = () => {
29970
+ host.opts.client.ackDispatchByID(host.opts.config.org_id, item.id).catch(() => {
29971
+ });
29972
+ };
29973
+ let msg = null;
29974
+ try {
29975
+ msg = await host.opts.client.getMessage(item.source_id);
29976
+ } catch (err) {
29977
+ const status = err?.status;
29978
+ if (status !== 404) {
29979
+ host.opts.log?.warn(`message fetch failed for ${item.source_id}, leaving pending: ${String(err)}`);
29980
+ host.dispatchedMessages.delete(item.source_id);
29981
+ return;
29982
+ }
29983
+ }
29984
+ if (!msg || msg.sender_id === host.opts.agentUserId) {
29985
+ host.dispatchedMessages.delete(item.source_id);
29986
+ ackItem();
29987
+ return;
29988
+ }
29989
+ const decision = await host.buildMessageDispatchDecision(item.chat_id, msg);
29990
+ if (decision.action === "retry") {
29991
+ host.dispatchedMessages.delete(item.source_id);
29992
+ return;
29993
+ }
29994
+ if (decision.action === "skip") {
29995
+ host.dispatchedMessages.delete(item.source_id);
29996
+ ackItem();
29997
+ return;
29998
+ }
29999
+ decision.event.dispatchEventId = item.id;
30000
+ const laneResolved = host.usesLaneLedger(decision.event);
30001
+ try {
30002
+ const dispatched = await host.handleInboundEvent(decision.event);
30003
+ if (dispatched) {
30004
+ if (!laneResolved)
30005
+ ackItem();
30006
+ } else {
30007
+ host.dispatchedMessages.delete(item.source_id);
30008
+ }
30009
+ } catch (err) {
30010
+ host.dispatchedMessages.delete(item.source_id);
30011
+ throw err;
30012
+ }
30013
+ }
30014
+
29535
30015
  // ../agent-core/dist/telemetry.js
29536
30016
  init_esm();
29537
30017
  var import_api_logs = __toESM(require_src(), 1);
@@ -29833,6 +30313,14 @@ var ParallAgentGateway = class {
29833
30313
  inFlightDispatches = 0;
29834
30314
  drainResolvers = [];
29835
30315
  pendingRestartNotification = null;
30316
+ laneLedger;
30317
+ // Sticky fallback: flipped when the server predates the ledger (claim
30318
+ // endpoint 404) so every subsequent dispatch uses the legacy flow.
30319
+ ledgerDisabled = false;
30320
+ // Group key of the group currently being dispatched on main — lane-aware
30321
+ // (targetId + thread), unlike mainCurrentTargetId which stays chat-level
30322
+ // for fork routing decisions.
30323
+ mainCurrentGroupKey;
29836
30324
  DISPATCHED_MESSAGES_CAP = 5e3;
29837
30325
  // SHUTDOWN_DEADLINE_MS is read by waitForDrain via the configured value
29838
30326
  // below — kept as instance state so per-runtime configs can override it
@@ -29842,6 +30330,14 @@ var ParallAgentGateway = class {
29842
30330
  DISPATCH_DEADLINE_MS;
29843
30331
  constructor(opts) {
29844
30332
  this.opts = opts;
30333
+ if (opts.dispatchContextDir) {
30334
+ this.laneLedger = new LaneLedger({
30335
+ client: opts.client,
30336
+ orgId: opts.config.org_id,
30337
+ contextDir: opts.dispatchContextDir,
30338
+ log: opts.log
30339
+ });
30340
+ }
29845
30341
  this.SHUTDOWN_DEADLINE_MS = opts.shutdownDeadlineMs ?? 6e4;
29846
30342
  this.FORK_DEADLINE_MS = opts.forkDeadlineMs ?? 2 * 60 * 6e4;
29847
30343
  this.DISPATCH_DEADLINE_MS = opts.dispatchDeadlineMs ?? 20 * 6e4;
@@ -29911,14 +30407,18 @@ var ParallAgentGateway = class {
29911
30407
  if (data.status !== "todo" && data.status !== "in_progress")
29912
30408
  return;
29913
30409
  try {
29914
- const dispatched = await this.handleTaskAssignment(data, data.id);
29915
- if (dispatched) {
30410
+ 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) => {
30411
+ if (dispatchEventId) {
30412
+ this.opts.client.ackDispatchByID(this.opts.config.org_id, dispatchEventId).catch(() => {
30413
+ });
30414
+ return;
30415
+ }
29916
30416
  this.opts.client.ackDispatch(this.opts.config.org_id, {
29917
30417
  source_type: "task_activity",
29918
30418
  source_id: data.id
29919
30419
  }).catch(() => {
29920
30420
  });
29921
- }
30421
+ });
29922
30422
  } catch (err) {
29923
30423
  this.opts.log?.error(`task dispatch failed for ${data.id}: ${String(err)}`);
29924
30424
  }
@@ -29928,11 +30428,10 @@ var ParallAgentGateway = class {
29928
30428
  if (!data.source_id || !data.task_id)
29929
30429
  return;
29930
30430
  try {
29931
- const dispatched = await this.handleTaskComment(data.source_id, data.task_id, data.actor_id, data.delivery_reason);
29932
- if (dispatched) {
30431
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.handleTaskComment(data.source_id, data.task_id ?? "", data.actor_id, data.delivery_reason), () => {
29933
30432
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
29934
30433
  });
29935
- }
30434
+ });
29936
30435
  } catch (err) {
29937
30436
  this.opts.log?.error(`task comment dispatch failed for ${data.source_id}: ${String(err)}`);
29938
30437
  }
@@ -29940,11 +30439,10 @@ var ParallAgentGateway = class {
29940
30439
  if (!data.source_id)
29941
30440
  return;
29942
30441
  try {
29943
- const dispatched = await this.handleWikiComment(data.source_id, data.actor_id, data.delivery_reason);
29944
- if (dispatched) {
30442
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.handleWikiComment(data.source_id, data.actor_id, data.delivery_reason), () => {
29945
30443
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
29946
30444
  });
29947
- }
30445
+ });
29948
30446
  } catch (err) {
29949
30447
  this.opts.log?.error(`wiki comment dispatch failed for ${data.source_id}: ${String(err)}`);
29950
30448
  }
@@ -29952,11 +30450,13 @@ var ParallAgentGateway = class {
29952
30450
  if (!data.task_id)
29953
30451
  return;
29954
30452
  try {
29955
- const dispatched = await this.handleTaskDispatch(data.task_id, data.source_id ?? data.task_id, { allowCreator: true });
29956
- if (dispatched) {
30453
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, (dispatchEventId) => this.handleTaskDispatch(data.task_id ?? "", data.source_id ?? data.task_id ?? "", {
30454
+ allowCreator: true,
30455
+ dispatchEventId
30456
+ }), () => {
29957
30457
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
29958
30458
  });
29959
- }
30459
+ });
29960
30460
  } catch (err) {
29961
30461
  this.opts.log?.error(`task update dispatch failed for ${data.task_id}: ${String(err)}`);
29962
30462
  }
@@ -29964,11 +30464,10 @@ var ParallAgentGateway = class {
29964
30464
  if (!data.source_id)
29965
30465
  return;
29966
30466
  try {
29967
- const dispatched = await this.fetchAndHandleScheduleFire(data.source_id, data.actor_id);
29968
- if (dispatched) {
30467
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.fetchAndHandleScheduleFire(data.source_id, data.actor_id), () => {
29969
30468
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
29970
30469
  });
29971
- }
30470
+ });
29972
30471
  } catch (err) {
29973
30472
  this.opts.log?.error(`schedule fire dispatch failed for ${data.source_id}: ${String(err)}`);
29974
30473
  }
@@ -29976,11 +30475,10 @@ var ParallAgentGateway = class {
29976
30475
  if (!data.source_id)
29977
30476
  return;
29978
30477
  try {
29979
- const dispatched = await this.fetchAndHandleExternalTriggerRun(data.source_id);
29980
- if (dispatched) {
30478
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.fetchAndHandleExternalTriggerRun(data.source_id), () => {
29981
30479
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
29982
30480
  });
29983
- }
30481
+ });
29984
30482
  } catch (err) {
29985
30483
  this.opts.log?.error(`external trigger dispatch failed for ${data.source_id}: ${String(err)}`);
29986
30484
  }
@@ -29988,11 +30486,10 @@ var ParallAgentGateway = class {
29988
30486
  if (!data.source_id)
29989
30487
  return;
29990
30488
  try {
29991
- const dispatched = await this.fetchAndHandleChannelMessage(data.source_id);
29992
- if (dispatched) {
30489
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.fetchAndHandleChannelMessage(data.source_id), () => {
29993
30490
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
29994
30491
  });
29995
- }
30492
+ });
29996
30493
  } catch (err) {
29997
30494
  this.opts.log?.error(`channel message dispatch failed for ${data.source_id}: ${String(err)}`);
29998
30495
  }
@@ -30000,14 +30497,19 @@ var ParallAgentGateway = class {
30000
30497
  if (!data.source_id)
30001
30498
  return;
30002
30499
  try {
30003
- const dispatched = await this.fetchAndHandleApprovalDecided(data.source_id, data.actor_id, data.chat_id ?? null);
30004
- if (dispatched) {
30500
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.fetchAndHandleApprovalDecided(data.source_id, data.actor_id, data.chat_id ?? null), () => {
30005
30501
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
30006
30502
  });
30007
- }
30503
+ });
30008
30504
  } catch (err) {
30009
30505
  this.opts.log?.error(`approval decided dispatch failed for ${data.source_id}: ${String(err)}`);
30010
30506
  }
30507
+ } else if (data.event_type === "message" && this.laneLedger && data.source_id && data.chat_id) {
30508
+ try {
30509
+ await this.handleMessageRedrive(data);
30510
+ } catch (err) {
30511
+ this.opts.log?.error(`message re-drive failed for ${data.source_id}: ${String(err)}`);
30512
+ }
30011
30513
  } else if (data.event_type !== "message" && data.event_type !== "task_assign") {
30012
30514
  this.opts.log?.info(`dispatch.new with unhandled event_type=${String(data.event_type)} (id=${data.id}) \u2014 no-op`);
30013
30515
  }
@@ -30043,6 +30545,39 @@ var ParallAgentGateway = class {
30043
30545
  source_id: sourceId
30044
30546
  });
30045
30547
  }
30548
+ /** True when this event's lifecycle is owned by the dispatch lane ledger. */
30549
+ usesLaneLedger(event) {
30550
+ return this.laneLedger != null && !this.ledgerDisabled && this.laneLedger.handles(event);
30551
+ }
30552
+ disableLedger(reason) {
30553
+ if (this.ledgerDisabled)
30554
+ return;
30555
+ this.ledgerDisabled = true;
30556
+ this.opts.log?.warn(`dispatch ledger unavailable (${reason}) \u2014 falling back to legacy received/ack flow`);
30557
+ }
30558
+ /**
30559
+ * Buffer grouping key. Lane-ledger message events group by full lane
30560
+ * identity (chat + thread) so a channel lane and a thread lane in the same
30561
+ * chat dispatch as separate turns with separate claims; everything else
30562
+ * keeps the historical chat-level grouping.
30563
+ */
30564
+ dispatchGroupKey(event) {
30565
+ if (this.usesLaneLedger(event)) {
30566
+ return this.laneLedger.laneKeyFor(event);
30567
+ }
30568
+ return event.targetId;
30569
+ }
30570
+ // Lane-flow protocols live in gateway-lane-flow.ts; these thin delegates
30571
+ // keep call sites and tests on the class surface.
30572
+ laneFlowHost() {
30573
+ return this;
30574
+ }
30575
+ dispatchLaneGroup(opts) {
30576
+ return dispatchLaneGroup(this.laneFlowHost(), opts);
30577
+ }
30578
+ consumeTypedDispatch(ref, run, ack) {
30579
+ return consumeTypedDispatch(this.laneFlowHost(), ref, run, ack);
30580
+ }
30046
30581
  buildDispatchContext(event, sessionKey) {
30047
30582
  const binding = this.sessionBindings.get(sessionKey);
30048
30583
  return {
@@ -30059,6 +30594,7 @@ var ParallAgentGateway = class {
30059
30594
  noReply: event.noReply ?? false,
30060
30595
  contextFilePath: this.opts.contextFilePathForSession?.(sessionKey),
30061
30596
  stepIdFilePath: this.opts.stepIdFilePathForSession?.(sessionKey),
30597
+ contextDirPath: this.opts.dispatchContextDir,
30062
30598
  client: this.opts.client,
30063
30599
  log: this.opts.log
30064
30600
  };
@@ -30098,7 +30634,7 @@ var ParallAgentGateway = class {
30098
30634
  this.opts.log?.warn(`failed to create input step: ${String(err)}`);
30099
30635
  }
30100
30636
  }
30101
- async createRuntimeStep(sessionId, event, runtimeEvent, stepIdFilePath, contextFilePath) {
30637
+ async createRuntimeStep(sessionId, event, runtimeEvent, stepIdFilePath, contextFilePath, laneContextFilePath2) {
30102
30638
  const target = resolveStepTarget(event);
30103
30639
  try {
30104
30640
  switch (runtimeEvent.type) {
@@ -30144,6 +30680,9 @@ var ParallAgentGateway = class {
30144
30680
  } else if (stepIdFilePath) {
30145
30681
  this.writeStepIdFile(stepIdFilePath, step.id);
30146
30682
  }
30683
+ if (laneContextFilePath2) {
30684
+ this.updateContextFileStepId(laneContextFilePath2, step.id);
30685
+ }
30147
30686
  break;
30148
30687
  }
30149
30688
  case "tool_result":
@@ -30166,6 +30705,9 @@ var ParallAgentGateway = class {
30166
30705
  } else if (stepIdFilePath) {
30167
30706
  this.clearStepIdFile(stepIdFilePath);
30168
30707
  }
30708
+ if (laneContextFilePath2) {
30709
+ this.updateContextFileStepId(laneContextFilePath2, null);
30710
+ }
30169
30711
  break;
30170
30712
  case "error":
30171
30713
  await this.opts.client.createAgentStep(this.opts.config.org_id, this.opts.agentUserId, sessionId, {
@@ -30185,28 +30727,28 @@ var ParallAgentGateway = class {
30185
30727
  }
30186
30728
  writeContextFile(filePath, ctx) {
30187
30729
  try {
30188
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
30189
- fs.writeFileSync(filePath, JSON.stringify(ctx), "utf8");
30730
+ fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
30731
+ fs2.writeFileSync(filePath, JSON.stringify(ctx), "utf8");
30190
30732
  } catch (err) {
30191
30733
  this.opts.log?.warn(`failed to write context file ${filePath}: ${String(err)}`);
30192
30734
  }
30193
30735
  }
30194
30736
  updateContextFileStepId(filePath, stepId) {
30195
30737
  try {
30196
- const raw = fs.readFileSync(filePath, "utf8");
30738
+ const raw = fs2.readFileSync(filePath, "utf8");
30197
30739
  const ctx = JSON.parse(raw);
30198
30740
  ctx.step_id = stepId;
30199
- fs.writeFileSync(filePath, JSON.stringify(ctx), "utf8");
30741
+ fs2.writeFileSync(filePath, JSON.stringify(ctx), "utf8");
30200
30742
  } catch (err) {
30201
30743
  this.opts.log?.warn(`failed to update context file step_id ${filePath}: ${String(err)}`);
30202
30744
  }
30203
30745
  }
30204
30746
  updateContextFileSessionId(filePath, sessionId) {
30205
30747
  try {
30206
- const raw = fs.readFileSync(filePath, "utf8");
30748
+ const raw = fs2.readFileSync(filePath, "utf8");
30207
30749
  const ctx = JSON.parse(raw);
30208
30750
  ctx.session_id = sessionId;
30209
- fs.writeFileSync(filePath, JSON.stringify(ctx), "utf8");
30751
+ fs2.writeFileSync(filePath, JSON.stringify(ctx), "utf8");
30210
30752
  } catch (err) {
30211
30753
  this.opts.log?.warn(`failed to update context file session_id ${filePath}: ${String(err)}`);
30212
30754
  }
@@ -30214,8 +30756,8 @@ var ParallAgentGateway = class {
30214
30756
  /** @deprecated Use writeContextFile / updateContextFileStepId. */
30215
30757
  writeStepIdFile(filePath, stepId) {
30216
30758
  try {
30217
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
30218
- fs.writeFileSync(filePath, stepId, "utf8");
30759
+ fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
30760
+ fs2.writeFileSync(filePath, stepId, "utf8");
30219
30761
  } catch (err) {
30220
30762
  this.opts.log?.warn(`failed to write step id file ${filePath}: ${String(err)}`);
30221
30763
  }
@@ -30223,7 +30765,7 @@ var ParallAgentGateway = class {
30223
30765
  /** @deprecated Use writeContextFile / updateContextFileStepId. */
30224
30766
  clearStepIdFile(filePath) {
30225
30767
  try {
30226
- fs.writeFileSync(filePath, "", "utf8");
30768
+ fs2.writeFileSync(filePath, "", "utf8");
30227
30769
  } catch {
30228
30770
  }
30229
30771
  }
@@ -30232,7 +30774,7 @@ var ParallAgentGateway = class {
30232
30774
  await this.createInputStep(sessionId, event);
30233
30775
  }
30234
30776
  }
30235
- async bindRuntimeSession(sessionKey, runtimeEvent, contextFilePath) {
30777
+ async bindRuntimeSession(sessionKey, runtimeEvent, contextFilePath, laneContextFilePath2) {
30236
30778
  const runtimeLaneKey = runtimeEvent.runtimeLaneKey || sessionKey;
30237
30779
  const existing = this.sessionBindings.get(sessionKey);
30238
30780
  if (existing && existing.runtimeLaneKey === runtimeLaneKey && existing.runtimeSessionId === runtimeEvent.runtimeSessionId) {
@@ -30276,6 +30818,9 @@ var ParallAgentGateway = class {
30276
30818
  if (contextFilePath) {
30277
30819
  this.updateContextFileSessionId(contextFilePath, session.id);
30278
30820
  }
30821
+ if (laneContextFilePath2) {
30822
+ this.updateContextFileSessionId(laneContextFilePath2, session.id);
30823
+ }
30279
30824
  await this.opts.onSessionBinding?.(binding);
30280
30825
  return binding;
30281
30826
  }
@@ -30302,14 +30847,27 @@ var ParallAgentGateway = class {
30302
30847
  const dispatchContext = this.buildDispatchContext(event, sessionKey);
30303
30848
  const contextFilePath = dispatchContext.contextFilePath;
30304
30849
  const stepIdFilePath = dispatchContext.stepIdFilePath;
30850
+ const activeLane = this.ledgerDisabled ? void 0 : this.laneLedger?.getForEvent(event);
30851
+ const laneContextFilePath2 = activeLane ? this.laneLedger?.laneContextPath(activeLane) : void 0;
30852
+ const contextBody = {
30853
+ session_id: dispatchContext.sessionId ?? null,
30854
+ chat_id: dispatchContext.chatId ?? null,
30855
+ trigger_message_id: dispatchContext.triggerMessageId ?? null,
30856
+ no_reply: dispatchContext.noReply,
30857
+ step_id: null,
30858
+ dispatch_event_id: activeLane?.typedDispatchEventId ?? activeLane?.folded.get(event.messageId) ?? null,
30859
+ lane: activeLane?.lane ?? null,
30860
+ target_uri: activeLane?.targetUri ?? null,
30861
+ thread_root_id: activeLane?.threadRootId ?? null,
30862
+ // Typed binding hint for the CLI: which task this dispatch is about
30863
+ // (parall task update attaches the typed effect only on a match).
30864
+ task_id: event.type === "task" ? event.targetId : null
30865
+ };
30305
30866
  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
- });
30867
+ this.writeContextFile(contextFilePath, contextBody);
30868
+ }
30869
+ if (laneContextFilePath2) {
30870
+ this.writeContextFile(laneContextFilePath2, contextBody);
30313
30871
  }
30314
30872
  this.inFlightDispatches++;
30315
30873
  const deadlineTimer = this.DISPATCH_DEADLINE_MS > 0 ? setTimeout(() => {
@@ -30336,7 +30894,7 @@ var ParallAgentGateway = class {
30336
30894
  })) {
30337
30895
  if (runtimeEvent.type === "runtime_session") {
30338
30896
  const priorAgentSessionId = binding?.agentSessionId;
30339
- binding = await this.bindRuntimeSession(sessionKey, runtimeEvent, contextFilePath);
30897
+ binding = await this.bindRuntimeSession(sessionKey, runtimeEvent, contextFilePath, laneContextFilePath2);
30340
30898
  if (event.targetType === "channel_conversation" && binding.agentSessionId !== priorAgentSessionId) {
30341
30899
  try {
30342
30900
  await this.opts.client.setChannelConversationSession(this.opts.config.org_id, event.targetId, binding.agentSessionId);
@@ -30368,6 +30926,9 @@ var ParallAgentGateway = class {
30368
30926
  await this.createInputStep(binding.agentSessionId, event);
30369
30927
  inputStepsCreated = true;
30370
30928
  }
30929
+ if (activeLane && !this.ledgerDisabled) {
30930
+ this.laneLedger?.maybeRenew(activeLane);
30931
+ }
30371
30932
  if (captureText && runtimeEvent.type === "text" && runtimeEvent.text) {
30372
30933
  captureText.push(runtimeEvent.text);
30373
30934
  }
@@ -30384,7 +30945,7 @@ var ParallAgentGateway = class {
30384
30945
  } else if (runtimeEvent.type === "tool_result" && pendingSendCallIds.delete(runtimeEvent.callId)) {
30385
30946
  recordMessageSend(sessionKey, !runtimeEvent.error);
30386
30947
  }
30387
- await this.createRuntimeStep(binding.agentSessionId, event, runtimeEvent, stepIdFilePath, contextFilePath);
30948
+ await this.createRuntimeStep(binding.agentSessionId, event, runtimeEvent, stepIdFilePath, contextFilePath, laneContextFilePath2);
30388
30949
  }
30389
30950
  if (!binding) {
30390
30951
  binding = this.sessionBindings.get(sessionKey);
@@ -30406,7 +30967,7 @@ var ParallAgentGateway = class {
30406
30967
  await this.createRuntimeStep(binding.agentSessionId, event, {
30407
30968
  type: "error",
30408
30969
  message: `Dispatch failed: ${String(err)}`
30409
- }, stepIdFilePath, contextFilePath);
30970
+ }, stepIdFilePath, contextFilePath, laneContextFilePath2);
30410
30971
  } catch (stepErr) {
30411
30972
  if (this.isSessionNotLiveError(stepErr))
30412
30973
  staleDetected = true;
@@ -30449,6 +31010,9 @@ var ParallAgentGateway = class {
30449
31010
  } else if (stepIdFilePath) {
30450
31011
  this.clearStepIdFile(stepIdFilePath);
30451
31012
  }
31013
+ if (laneContextFilePath2) {
31014
+ this.updateContextFileStepId(laneContextFilePath2, null);
31015
+ }
30452
31016
  this.inFlightDispatches--;
30453
31017
  if (this.inFlightDispatches === 0 && this.drainResolvers.length > 0) {
30454
31018
  const resolvers = this.drainResolvers.splice(0);
@@ -30506,13 +31070,39 @@ var ParallAgentGateway = class {
30506
31070
  item.resolve(false);
30507
31071
  break;
30508
31072
  }
30509
- const items = fork.queue.splice(0);
31073
+ let items;
31074
+ const head = fork.queue[0];
31075
+ if (head && this.usesLaneLedger(head.event)) {
31076
+ const headKey = this.dispatchGroupKey(head.event);
31077
+ const splitAt = fork.queue.findIndex((it) => this.dispatchGroupKey(it.event) !== headKey);
31078
+ items = splitAt === -1 ? fork.queue.splice(0) : fork.queue.splice(0, splitAt);
31079
+ } else {
31080
+ items = fork.queue.splice(0);
31081
+ }
30510
31082
  const events = items.map((item) => item.event);
30511
31083
  const last = events[events.length - 1];
30512
31084
  const earlier = events.slice(0, -1);
30513
31085
  try {
30514
31086
  const batchText = [];
30515
- const dispatched = await this.runDispatch(last, fork.fork.sessionKey, buildForkScopePrefix(last) + buildEventBody(last), earlier, batchText);
31087
+ let dispatched;
31088
+ if (this.usesLaneLedger(last)) {
31089
+ const outcome = await this.dispatchLaneGroup({
31090
+ events,
31091
+ sessionKey: fork.fork.sessionKey,
31092
+ body: buildForkScopePrefix(last) + buildEventBody(last),
31093
+ earlier,
31094
+ captureText: batchText,
31095
+ hasMoreLocal: () => fork.queue.length > 0
31096
+ });
31097
+ if (outcome === "foreign") {
31098
+ for (const item of items)
31099
+ item.resolve(false);
31100
+ break;
31101
+ }
31102
+ dispatched = outcome === "dispatched";
31103
+ } else {
31104
+ dispatched = await this.runDispatch(last, fork.fork.sessionKey, buildForkScopePrefix(last) + buildEventBody(last), earlier, batchText);
31105
+ }
30516
31106
  if (!dispatched) {
30517
31107
  for (const item of items) {
30518
31108
  item.resolve(false);
@@ -30615,9 +31205,9 @@ var ParallAgentGateway = class {
30615
31205
  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
31206
  break;
30617
31207
  }
30618
- const targetId = this.dispatchState.mainBuffer[0].targetId;
31208
+ const groupKey = this.dispatchGroupKey(this.dispatchState.mainBuffer[0]);
30619
31209
  const events = [];
30620
- while (this.dispatchState.mainBuffer[0]?.targetId === targetId) {
31210
+ while (this.dispatchState.mainBuffer[0] && this.dispatchGroupKey(this.dispatchState.mainBuffer[0]) === groupKey) {
30621
31211
  events.push(this.dispatchState.mainBuffer.shift());
30622
31212
  }
30623
31213
  const event = events[events.length - 1];
@@ -30626,7 +31216,36 @@ var ParallAgentGateway = class {
30626
31216
  const pendingFork = hasPendingInjections ? [] : this.dispatchState.pendingForkResults.splice(0);
30627
31217
  const forkPrefix = buildForkResultPrefix(pendingFork);
30628
31218
  this.dispatchState.mainCurrentTargetId = event.targetId;
31219
+ this.mainCurrentGroupKey = groupKey;
30629
31220
  this.dispatchState.mainPreDispatchBranchPoint = this.opts.dispatchAdapter.getBranchPoint?.(this.opts.runtimeKey);
31221
+ if (this.usesLaneLedger(event)) {
31222
+ let outcome;
31223
+ try {
31224
+ outcome = await this.dispatchLaneGroup({
31225
+ events,
31226
+ sessionKey: this.opts.runtimeKey,
31227
+ body: forkPrefix + buildEventBody(event),
31228
+ earlier,
31229
+ hasMoreLocal: () => this.dispatchState.mainBuffer.some((e) => this.dispatchGroupKey(e) === groupKey)
31230
+ });
31231
+ } catch (err) {
31232
+ this.opts.log?.error(`lane dispatch failed for ${event.messageId}: ${String(err)}`);
31233
+ this.dispatchState.pendingForkResults.unshift(...pendingFork);
31234
+ for (const ev of events)
31235
+ this.dispatchedMessages.delete(ev.messageId);
31236
+ continue;
31237
+ }
31238
+ if (outcome === "shutdown") {
31239
+ this.dispatchState.mainBuffer.unshift(...events);
31240
+ this.dispatchState.pendingForkResults.unshift(...pendingFork);
31241
+ break;
31242
+ }
31243
+ if (outcome === "foreign") {
31244
+ this.dispatchState.pendingForkResults.unshift(...pendingFork);
31245
+ continue;
31246
+ }
31247
+ continue;
31248
+ }
30630
31249
  try {
30631
31250
  await this.emitDispatchReceived(event);
30632
31251
  } catch (err) {
@@ -30655,6 +31274,7 @@ var ParallAgentGateway = class {
30655
31274
  this.draining = false;
30656
31275
  this.dispatchState.mainDispatching = false;
30657
31276
  this.dispatchState.mainCurrentTargetId = void 0;
31277
+ this.mainCurrentGroupKey = void 0;
30658
31278
  this.dispatchState.mainPreDispatchBranchPoint = void 0;
30659
31279
  if (!this.shuttingDown && this.dispatchState.mainBuffer.length > 0) {
30660
31280
  setTimeout(() => {
@@ -30674,13 +31294,38 @@ var ParallAgentGateway = class {
30674
31294
  const forkPrefix = buildForkResultPrefix(pendingFork);
30675
31295
  this.dispatchState.mainDispatching = true;
30676
31296
  this.dispatchState.mainCurrentTargetId = event.targetId;
31297
+ this.mainCurrentGroupKey = this.dispatchGroupKey(event);
30677
31298
  this.dispatchState.mainPreDispatchBranchPoint = this.opts.dispatchAdapter.getBranchPoint?.(this.opts.runtimeKey);
31299
+ if (this.usesLaneLedger(event)) {
31300
+ let outcome = "shutdown";
31301
+ try {
31302
+ try {
31303
+ outcome = await this.dispatchLaneGroup({
31304
+ events: [event],
31305
+ sessionKey: this.opts.runtimeKey,
31306
+ body: forkPrefix + buildEventBody(event),
31307
+ earlier: [],
31308
+ hasMoreLocal: () => this.dispatchState.mainBuffer.some((e) => this.dispatchGroupKey(e) === this.dispatchGroupKey(event))
31309
+ });
31310
+ } catch (err) {
31311
+ this.dispatchState.pendingForkResults.unshift(...pendingFork);
31312
+ throw err;
31313
+ }
31314
+ if (outcome !== "dispatched") {
31315
+ this.dispatchState.pendingForkResults.unshift(...pendingFork);
31316
+ }
31317
+ } finally {
31318
+ await this.drainMainBuffer();
31319
+ }
31320
+ return outcome === "dispatched";
31321
+ }
30678
31322
  try {
30679
31323
  await this.emitDispatchReceived(event);
30680
31324
  } catch (err) {
30681
31325
  this.opts.log?.warn?.(`mark-received failed, leaving unacked for retry: ${String(err)}`);
30682
31326
  this.dispatchState.mainDispatching = false;
30683
31327
  this.dispatchState.mainCurrentTargetId = void 0;
31328
+ this.mainCurrentGroupKey = void 0;
30684
31329
  this.dispatchState.mainPreDispatchBranchPoint = void 0;
30685
31330
  this.dispatchState.pendingForkResults.unshift(...pendingFork);
30686
31331
  return false;
@@ -30701,7 +31346,11 @@ var ParallAgentGateway = class {
30701
31346
  return false;
30702
31347
  }
30703
31348
  this.dispatchState.mainBuffer.push(event);
30704
- if (this.dispatchState.mainCurrentTargetId === event.targetId && await this.opts.dispatchAdapter.enqueueDuringDispatch?.(this.opts.runtimeKey, buildEventBody(event))) {
31349
+ if (this.usesLaneLedger(event)) {
31350
+ if (this.mainCurrentGroupKey === this.dispatchGroupKey(event) && await this.laneLedger?.steerLive(event) && await this.opts.dispatchAdapter.enqueueDuringDispatch?.(this.opts.runtimeKey, buildEventBody(event))) {
31351
+ this.opts.log?.info(`steer folded+injected for ${event.messageId} (will drain for bookkeeping)`);
31352
+ }
31353
+ } else if (this.dispatchState.mainCurrentTargetId === event.targetId && await this.opts.dispatchAdapter.enqueueDuringDispatch?.(this.opts.runtimeKey, buildEventBody(event))) {
30705
31354
  this.opts.log?.info(`steer injected for ${event.messageId} (will drain for bookkeeping)`);
30706
31355
  }
30707
31356
  if (!this.dispatchState.mainDispatching && !this.draining && this.dispatchState.mainBuffer.length > 0) {
@@ -30872,8 +31521,10 @@ var ParallAgentGateway = class {
30872
31521
  try {
30873
31522
  const dispatched = await this.handleInboundEvent(event);
30874
31523
  if (dispatched) {
30875
- this.opts.client.ackDispatch(this.opts.config.org_id, { source_type: "message", source_id: data.id }).catch(() => {
30876
- });
31524
+ if (!this.usesLaneLedger(event)) {
31525
+ this.opts.client.ackDispatch(this.opts.config.org_id, { source_type: "message", source_id: data.id }).catch(() => {
31526
+ });
31527
+ }
30877
31528
  } else {
30878
31529
  this.dispatchedMessages.delete(data.id);
30879
31530
  }
@@ -30882,7 +31533,21 @@ var ParallAgentGateway = class {
30882
31533
  this.dispatchedMessages.delete(data.id);
30883
31534
  }
30884
31535
  }
30885
- async handleTaskAssignment(task, ackSourceId) {
31536
+ // Ledger re-drive consumption: dispatch.new message hints re-enter the
31537
+ // shared WorkItem consumption path (same protocol as catch-up).
31538
+ async handleMessageRedrive(item) {
31539
+ if (!item.chat_id || !item.source_id)
31540
+ return;
31541
+ await this.consumeMessageWorkItem({
31542
+ id: item.id,
31543
+ source_id: item.source_id,
31544
+ chat_id: item.chat_id
31545
+ });
31546
+ }
31547
+ consumeMessageWorkItem(item) {
31548
+ return consumeMessageWorkItem(this.laneFlowHost(), item);
31549
+ }
31550
+ async handleTaskAssignment(task, ackSourceId, dispatchEventId) {
30886
31551
  if (this.shuttingDown)
30887
31552
  return false;
30888
31553
  const dedupeKey = `${task.id}:${task.updated_at}`;
@@ -30911,7 +31576,8 @@ var ParallAgentGateway = class {
30911
31576
  body: parts.join("\n"),
30912
31577
  sentAt: task.updated_at ?? task.created_at,
30913
31578
  ackSourceType: "task_activity",
30914
- ackSourceId
31579
+ ackSourceId,
31580
+ dispatchEventId
30915
31581
  };
30916
31582
  const dispatched = await this.handleInboundEvent(event);
30917
31583
  if (!dispatched) {
@@ -30935,7 +31601,7 @@ var ParallAgentGateway = class {
30935
31601
  this.opts.log?.info(`skipping stale task dispatch ${ackSourceId ?? taskId} \u2014 assigned to ${task.assignee_id}, creator ${task.creator_id}`);
30936
31602
  return true;
30937
31603
  }
30938
- return this.handleTaskAssignment(task, ackSourceId);
31604
+ return this.handleTaskAssignment(task, ackSourceId, opts.dispatchEventId);
30939
31605
  }
30940
31606
  async handleTaskComment(commentId, taskId, actorId, deliveryReason) {
30941
31607
  if (this.shuttingDown)
@@ -31343,74 +32009,46 @@ var ParallAgentGateway = class {
31343
32009
  }
31344
32010
  processed++;
31345
32011
  try {
31346
- let dispatched = false;
32012
+ const ackItem = () => {
32013
+ this.opts.client.ackDispatchByID(this.opts.config.org_id, item.id).catch(() => {
32014
+ });
32015
+ };
31347
32016
  if (item.event_type === "task_assign" && item.task_id) {
31348
32017
  try {
31349
- dispatched = await this.handleTaskDispatch(item.task_id, item.source_id ?? item.task_id);
32018
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, (dispatchEventId) => this.handleTaskDispatch(item.task_id ?? "", item.source_id ?? item.task_id ?? "", {
32019
+ dispatchEventId
32020
+ }), ackItem);
31350
32021
  } catch (err) {
31351
32022
  this.opts.log?.warn(`catch-up task fetch failed for ${item.task_id}, leaving pending: ${String(err)}`);
31352
32023
  continue;
31353
32024
  }
31354
32025
  } else if (item.event_type === "task_update" && item.task_id) {
31355
32026
  try {
31356
- dispatched = await this.handleTaskDispatch(item.task_id, item.source_id ?? item.task_id, { allowCreator: true });
32027
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, (dispatchEventId) => this.handleTaskDispatch(item.task_id ?? "", item.source_id ?? item.task_id ?? "", {
32028
+ allowCreator: true,
32029
+ dispatchEventId
32030
+ }), ackItem);
31357
32031
  } catch (err) {
31358
32032
  this.opts.log?.warn(`catch-up task fetch failed for ${item.task_id}, leaving pending: ${String(err)}`);
31359
32033
  continue;
31360
32034
  }
31361
32035
  } 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);
32036
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.handleTaskComment(item.source_id, item.task_id ?? "", item.actor_id, item.delivery_reason), ackItem);
31363
32037
  } else if (item.event_type === "wiki_comment" && item.source_id) {
31364
- dispatched = await this.handleWikiComment(item.source_id, item.actor_id, item.delivery_reason);
32038
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.handleWikiComment(item.source_id, item.actor_id, item.delivery_reason), ackItem);
31365
32039
  } else if (item.event_type === "schedule.fire" && item.source_id) {
31366
- dispatched = await this.fetchAndHandleScheduleFire(item.source_id, item.actor_id);
32040
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.fetchAndHandleScheduleFire(item.source_id, item.actor_id), ackItem);
31367
32041
  } else if (item.event_type === "external_trigger" && item.source_id) {
31368
- dispatched = await this.fetchAndHandleExternalTriggerRun(item.source_id);
32042
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.fetchAndHandleExternalTriggerRun(item.source_id), ackItem);
31369
32043
  } else if (item.event_type === "channel_message" && item.source_id) {
31370
- dispatched = await this.fetchAndHandleChannelMessage(item.source_id);
32044
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.fetchAndHandleChannelMessage(item.source_id), ackItem);
31371
32045
  } 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);
32046
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.fetchAndHandleApprovalDecided(item.source_id, item.actor_id, item.chat_id ?? null), ackItem);
31373
32047
  } 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(() => {
32048
+ await this.consumeMessageWorkItem({
32049
+ id: item.id,
32050
+ source_id: item.source_id,
32051
+ chat_id: item.chat_id
31414
32052
  });
31415
32053
  }
31416
32054
  } catch (err) {
@@ -31476,6 +32114,14 @@ ${fullSummary}` : fullSummary;
31476
32114
  this.abortFork(targetId, "ws reconnect");
31477
32115
  }
31478
32116
  }
32117
+ if (this.laneLedger && this.inFlightDispatches === 0 && this.laneLedger.activeCount > 0) {
32118
+ log?.info(`releasing ${this.laneLedger.activeCount} stale lane(s) on reconnect`);
32119
+ await this.laneLedger.releaseAll();
32120
+ }
32121
+ if (this.laneLedger && this.ledgerDisabled) {
32122
+ log?.info("re-probing dispatch ledger after reconnect (was disabled)");
32123
+ this.ledgerDisabled = false;
32124
+ }
31479
32125
  const intervalSec = data.heartbeat_interval > 0 ? data.heartbeat_interval : 30;
31480
32126
  try {
31481
32127
  const count = await fetchAllChats(client, config.org_id, this.chatInfoMap);
@@ -31546,6 +32192,10 @@ ${fullSummary}` : fullSummary;
31546
32192
  }
31547
32193
  if (this.heartbeatTimer)
31548
32194
  clearInterval(this.heartbeatTimer);
32195
+ if (this.laneLedger && this.laneLedger.activeCount > 0) {
32196
+ this.opts.log?.info(`releasing ${this.laneLedger.activeCount} lane(s) on shutdown`);
32197
+ await this.laneLedger.releaseAll();
32198
+ }
31549
32199
  await this.opts.onBeforeDisconnect?.();
31550
32200
  this.opts.ws.disconnect();
31551
32201
  this.opts.log?.info(`disconnected`);
@@ -31556,8 +32206,8 @@ ${fullSummary}` : fullSummary;
31556
32206
  import { execSync } from "node:child_process";
31557
32207
  import { constants } from "node:fs";
31558
32208
  import * as fsSync from "node:fs";
31559
- import * as fs2 from "node:fs/promises";
31560
- import * as path2 from "node:path";
32209
+ import * as fs3 from "node:fs/promises";
32210
+ import * as path3 from "node:path";
31561
32211
  var DEFAULT_MAX_TOTAL_IMAGE_BYTES = 20 * 1024 * 1024;
31562
32212
  var DEFAULT_ATTACHMENT_CACHE_MAX_BYTES = 512 * 1024 * 1024;
31563
32213
  var DEFAULT_ATTACHMENT_DOWNLOAD_TIMEOUT_MS = 3e4;
@@ -31587,11 +32237,11 @@ async function prepareLocalImageAttachments(event, context2, opts) {
31587
32237
  };
31588
32238
  }
31589
32239
  const rootDir = await ensureAttachmentRootDir(opts.workspaceDir);
31590
- const messageDir = path2.join(rootDir, sanitizePathSegment(event.messageId));
32240
+ const messageDir = path3.join(rootDir, sanitizePathSegment(event.messageId));
31591
32241
  await ensurePathIsNotSymlink(messageDir);
31592
- await fs2.mkdir(messageDir, { recursive: true });
32242
+ await fs3.mkdir(messageDir, { recursive: true });
31593
32243
  await ensurePathIsNotSymlink(messageDir);
31594
- const activeMessageDir = path2.resolve(messageDir);
32244
+ const activeMessageDir = path3.resolve(messageDir);
31595
32245
  activeAttachmentDirs.add(activeMessageDir);
31596
32246
  const maintenanceCooldownMs = opts.maintenanceCooldownMs ?? DEFAULT_MAINTENANCE_COOLDOWN_MS;
31597
32247
  const maintenancePromise = scheduleAttachmentMaintenance(rootDir, {
@@ -31608,7 +32258,7 @@ async function prepareLocalImageAttachments(event, context2, opts) {
31608
32258
  const notes = [];
31609
32259
  let downloadedBytes = 0;
31610
32260
  for (const att of imageAttachments) {
31611
- const localPath = path2.join(messageDir, localFileName(att.id, att.fileName, att.mimeType));
32261
+ const localPath = path3.join(messageDir, localFileName(att.id, att.fileName, att.mimeType));
31612
32262
  const downloadTimeoutMs = opts.downloadTimeoutMs ?? DEFAULT_ATTACHMENT_DOWNLOAD_TIMEOUT_MS;
31613
32263
  const fetchFresh = async () => {
31614
32264
  const fileInfo = await withTimeout(context2.client.getFileUrl(att.id), downloadTimeoutMs, `file URL lookup timed out after ${downloadTimeoutMs}ms`);
@@ -31665,7 +32315,7 @@ async function appendPreparedLocalAttachmentRefs(body, event, context2, opts) {
31665
32315
  return { body: appendLocalAttachmentRefs(body, attachments), attachments };
31666
32316
  }
31667
32317
  function pinLocalAttachmentPaths(images) {
31668
- const dirs = new Set(images.map((image) => path2.resolve(path2.dirname(image.localPath))));
32318
+ const dirs = new Set(images.map((image) => path3.resolve(path3.dirname(image.localPath))));
31669
32319
  for (const dir of dirs) {
31670
32320
  activeAttachmentDirs.add(dir);
31671
32321
  }
@@ -31680,7 +32330,7 @@ function pinLocalAttachmentPaths(images) {
31680
32330
  };
31681
32331
  }
31682
32332
  function attachmentRootDir(workspaceDir) {
31683
- return path2.join(path2.resolve(workspaceDir), ".parall", "attachments");
32333
+ return path3.join(path3.resolve(workspaceDir), ".parall", "attachments");
31684
32334
  }
31685
32335
  function ensureLocalAttachmentGitExclude(workingDirectory) {
31686
32336
  try {
@@ -31689,8 +32339,8 @@ function ensureLocalAttachmentGitExclude(workingDirectory) {
31689
32339
  encoding: "utf8",
31690
32340
  stdio: ["ignore", "pipe", "ignore"]
31691
32341
  }).trim();
31692
- const excludePath = path2.isAbsolute(rel) ? rel : path2.join(workingDirectory, rel);
31693
- fsSync.mkdirSync(path2.dirname(excludePath), { recursive: true });
32342
+ const excludePath = path3.isAbsolute(rel) ? rel : path3.join(workingDirectory, rel);
32343
+ fsSync.mkdirSync(path3.dirname(excludePath), { recursive: true });
31694
32344
  const existing = fsSync.existsSync(excludePath) ? fsSync.readFileSync(excludePath, "utf8") : "";
31695
32345
  if (existing.split(/\r?\n/).some((line) => line.trim() === ".parall/"))
31696
32346
  return;
@@ -31726,18 +32376,18 @@ function scheduleAttachmentMaintenance(rootDir, opts) {
31726
32376
  return run;
31727
32377
  }
31728
32378
  async function ensureAttachmentRootDir(workspaceDir) {
31729
- const workspaceRoot = path2.resolve(workspaceDir);
31730
- const parallDir = path2.join(workspaceRoot, ".parall");
32379
+ const workspaceRoot = path3.resolve(workspaceDir);
32380
+ const parallDir = path3.join(workspaceRoot, ".parall");
31731
32381
  const rootDir = attachmentRootDir(workspaceRoot);
31732
- await fs2.mkdir(workspaceRoot, { recursive: true });
32382
+ await fs3.mkdir(workspaceRoot, { recursive: true });
31733
32383
  await ensurePathIsNotSymlink(parallDir);
31734
- await fs2.mkdir(parallDir, { recursive: true, mode: 448 });
32384
+ await fs3.mkdir(parallDir, { recursive: true, mode: 448 });
31735
32385
  await ensurePathIsNotSymlink(parallDir);
31736
32386
  await ensurePathIsNotSymlink(rootDir);
31737
- await fs2.mkdir(rootDir, { recursive: true, mode: 448 });
32387
+ await fs3.mkdir(rootDir, { recursive: true, mode: 448 });
31738
32388
  await ensurePathIsNotSymlink(rootDir);
31739
- const realWorkspace = await fs2.realpath(workspaceRoot);
31740
- const realRoot = await fs2.realpath(rootDir);
32389
+ const realWorkspace = await fs3.realpath(workspaceRoot);
32390
+ const realRoot = await fs3.realpath(rootDir);
31741
32391
  if (!isPathInside(realRoot, realWorkspace)) {
31742
32392
  throw new Error(`attachment root escapes workspace: ${rootDir}`);
31743
32393
  }
@@ -31745,7 +32395,7 @@ async function ensureAttachmentRootDir(workspaceDir) {
31745
32395
  }
31746
32396
  async function ensurePathIsNotSymlink(filePath) {
31747
32397
  try {
31748
- const stat = await fs2.lstat(filePath);
32398
+ const stat = await fs3.lstat(filePath);
31749
32399
  if (stat.isSymbolicLink()) {
31750
32400
  throw new Error(`refusing to use symlinked attachment path ${filePath}`);
31751
32401
  }
@@ -31756,8 +32406,8 @@ async function ensurePathIsNotSymlink(filePath) {
31756
32406
  }
31757
32407
  }
31758
32408
  function isPathInside(childPath, parentPath) {
31759
- const rel = path2.relative(parentPath, childPath);
31760
- return rel === "" || !!rel && !rel.startsWith("..") && !path2.isAbsolute(rel);
32409
+ const rel = path3.relative(parentPath, childPath);
32410
+ return rel === "" || !!rel && !rel.startsWith("..") && !path3.isAbsolute(rel);
31761
32411
  }
31762
32412
  async function existingUsableFile(filePath, expectedSize, rootDir) {
31763
32413
  try {
@@ -31768,30 +32418,30 @@ async function existingUsableFile(filePath, expectedSize, rootDir) {
31768
32418
  }
31769
32419
  }
31770
32420
  async function localFileStatInsideRoot(filePath, rootDir) {
31771
- const stat = await fs2.lstat(filePath);
32421
+ const stat = await fs3.lstat(filePath);
31772
32422
  if (stat.isSymbolicLink()) {
31773
32423
  throw new Error(`refusing to use symlinked attachment file ${filePath}`);
31774
32424
  }
31775
32425
  if (!stat.isFile()) {
31776
32426
  throw new Error(`attachment path is not a file ${filePath}`);
31777
32427
  }
31778
- const realRoot = await fs2.realpath(rootDir);
31779
- const realFile = await fs2.realpath(filePath);
32428
+ const realRoot = await fs3.realpath(rootDir);
32429
+ const realFile = await fs3.realpath(filePath);
31780
32430
  if (!isPathInside(realFile, realRoot)) {
31781
32431
  throw new Error(`attachment file escapes workspace: ${filePath}`);
31782
32432
  }
31783
32433
  return stat;
31784
32434
  }
31785
32435
  async function localDirectoryStatInsideRoot(dirPath, rootDir) {
31786
- const stat = await fs2.lstat(dirPath);
32436
+ const stat = await fs3.lstat(dirPath);
31787
32437
  if (stat.isSymbolicLink()) {
31788
32438
  throw new Error(`refusing to use symlinked attachment directory ${dirPath}`);
31789
32439
  }
31790
32440
  if (!stat.isDirectory()) {
31791
32441
  throw new Error(`attachment path is not a directory ${dirPath}`);
31792
32442
  }
31793
- const realRoot = await fs2.realpath(rootDir);
31794
- const realDir = await fs2.realpath(dirPath);
32443
+ const realRoot = await fs3.realpath(rootDir);
32444
+ const realDir = await fs3.realpath(dirPath);
31795
32445
  if (!isPathInside(realDir, realRoot)) {
31796
32446
  throw new Error(`attachment directory escapes workspace: ${dirPath}`);
31797
32447
  }
@@ -31799,7 +32449,7 @@ async function localDirectoryStatInsideRoot(dirPath, rootDir) {
31799
32449
  }
31800
32450
  async function openLocalFileInsideRoot(filePath, rootDir) {
31801
32451
  const checkedStat = await localFileStatInsideRoot(filePath, rootDir);
31802
- const file = await fs2.open(filePath, constants.O_RDONLY | constants.O_NOFOLLOW);
32452
+ const file = await fs3.open(filePath, constants.O_RDONLY | constants.O_NOFOLLOW);
31803
32453
  let keepOpen = false;
31804
32454
  try {
31805
32455
  const openedStat = await file.stat();
@@ -31815,8 +32465,8 @@ async function openLocalFileInsideRoot(filePath, rootDir) {
31815
32465
  }
31816
32466
  }
31817
32467
  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);
32468
+ await localDirectoryStatInsideRoot(path3.dirname(filePath), rootDir);
32469
+ const file = await fs3.open(filePath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
31820
32470
  let keepOpen = false;
31821
32471
  try {
31822
32472
  const checkedStat = await localFileStatInsideRoot(filePath, rootDir);
@@ -31841,7 +32491,7 @@ async function assertLocalFileIdentity(filePath, rootDir, expected) {
31841
32491
  async function removeLocalFileIfInside(filePath, rootDir) {
31842
32492
  try {
31843
32493
  await localFileStatInsideRoot(filePath, rootDir);
31844
- await fs2.rm(filePath, { force: true });
32494
+ await fs3.rm(filePath, { force: true });
31845
32495
  } catch {
31846
32496
  }
31847
32497
  }
@@ -31854,7 +32504,7 @@ function sameFile(a, b) {
31854
32504
  async function cleanupOldAttachmentFiles(rootDir, ttlMs, log, preserveDirs) {
31855
32505
  let entries;
31856
32506
  try {
31857
- entries = await fs2.readdir(rootDir, { withFileTypes: true });
32507
+ entries = await fs3.readdir(rootDir, { withFileTypes: true });
31858
32508
  } catch {
31859
32509
  return;
31860
32510
  }
@@ -31862,15 +32512,15 @@ async function cleanupOldAttachmentFiles(rootDir, ttlMs, log, preserveDirs) {
31862
32512
  await Promise.all(entries.map(async (entry) => {
31863
32513
  if (!entry.isDirectory())
31864
32514
  return;
31865
- const fullPath = path2.join(rootDir, entry.name);
32515
+ const fullPath = path3.join(rootDir, entry.name);
31866
32516
  try {
31867
- if (preserveDirs?.has(path2.resolve(fullPath)))
32517
+ if (preserveDirs?.has(path3.resolve(fullPath)))
31868
32518
  return;
31869
- const stat = await fs2.lstat(fullPath);
32519
+ const stat = await fs3.lstat(fullPath);
31870
32520
  if (!stat.isDirectory())
31871
32521
  return;
31872
32522
  if (stat.mtimeMs < cutoff) {
31873
- await fs2.rm(fullPath, { recursive: true, force: true });
32523
+ await fs3.rm(fullPath, { recursive: true, force: true });
31874
32524
  }
31875
32525
  } catch (err) {
31876
32526
  log?.warn?.(`agent-core: failed to clean attachment temp dir ${fullPath}: ${String(err)}`);
@@ -31882,7 +32532,7 @@ async function pruneAttachmentCache(rootDir, maxBytes, log, preserveDirs) {
31882
32532
  return;
31883
32533
  let entries;
31884
32534
  try {
31885
- entries = await fs2.readdir(rootDir, { withFileTypes: true });
32535
+ entries = await fs3.readdir(rootDir, { withFileTypes: true });
31886
32536
  } catch {
31887
32537
  return;
31888
32538
  }
@@ -31891,9 +32541,9 @@ async function pruneAttachmentCache(rootDir, maxBytes, log, preserveDirs) {
31891
32541
  for (const entry of entries) {
31892
32542
  if (!entry.isDirectory())
31893
32543
  continue;
31894
- const fullPath = path2.join(rootDir, entry.name);
32544
+ const fullPath = path3.join(rootDir, entry.name);
31895
32545
  try {
31896
- const stat = await fs2.lstat(fullPath);
32546
+ const stat = await fs3.lstat(fullPath);
31897
32547
  if (!stat.isDirectory())
31898
32548
  continue;
31899
32549
  const size = await directorySize(fullPath);
@@ -31909,10 +32559,10 @@ async function pruneAttachmentCache(rootDir, maxBytes, log, preserveDirs) {
31909
32559
  for (const dir of dirs) {
31910
32560
  if (total <= maxBytes)
31911
32561
  break;
31912
- if (preserveDirs?.has(path2.resolve(dir.path)))
32562
+ if (preserveDirs?.has(path3.resolve(dir.path)))
31913
32563
  continue;
31914
32564
  try {
31915
- await fs2.rm(dir.path, { recursive: true, force: true });
32565
+ await fs3.rm(dir.path, { recursive: true, force: true });
31916
32566
  total -= dir.size;
31917
32567
  } catch (err) {
31918
32568
  log?.warn?.(`agent-core: failed to prune attachment cache dir ${dir.path}: ${String(err)}`);
@@ -31921,12 +32571,12 @@ async function pruneAttachmentCache(rootDir, maxBytes, log, preserveDirs) {
31921
32571
  }
31922
32572
  async function directorySize(dirPath) {
31923
32573
  let total = 0;
31924
- const entries = await fs2.readdir(dirPath, { withFileTypes: true });
32574
+ const entries = await fs3.readdir(dirPath, { withFileTypes: true });
31925
32575
  for (const entry of entries) {
31926
- const fullPath = path2.join(dirPath, entry.name);
32576
+ const fullPath = path3.join(dirPath, entry.name);
31927
32577
  let stat;
31928
32578
  try {
31929
- stat = await fs2.lstat(fullPath);
32579
+ stat = await fs3.lstat(fullPath);
31930
32580
  } catch {
31931
32581
  continue;
31932
32582
  }
@@ -31941,10 +32591,10 @@ async function directorySize(dirPath) {
31941
32591
  return total;
31942
32592
  }
31943
32593
  function activeDirsForRoot(rootDir) {
31944
- const root = path2.resolve(rootDir);
32594
+ const root = path3.resolve(rootDir);
31945
32595
  const dirs = /* @__PURE__ */ new Set();
31946
32596
  for (const dir of activeAttachmentDirs) {
31947
- if (dir === root || dir.startsWith(`${root}${path2.sep}`)) {
32597
+ if (dir === root || dir.startsWith(`${root}${path3.sep}`)) {
31948
32598
  dirs.add(dir);
31949
32599
  }
31950
32600
  }
@@ -32041,9 +32691,9 @@ async function writeResponseToFileWithLimit(res, filePath, maxBytes, rootDir) {
32041
32691
  }
32042
32692
  writtenStat = await file.stat();
32043
32693
  await closeFile();
32044
- await localDirectoryStatInsideRoot(path2.dirname(filePath), rootDir);
32694
+ await localDirectoryStatInsideRoot(path3.dirname(filePath), rootDir);
32045
32695
  await assertLocalFileIdentity(tmpPath, rootDir, writtenStat);
32046
- await fs2.rename(tmpPath, filePath);
32696
+ await fs3.rename(tmpPath, filePath);
32047
32697
  completed = true;
32048
32698
  return written;
32049
32699
  } finally {
@@ -32059,9 +32709,9 @@ async function writeResponseToFileWithLimit(res, filePath, maxBytes, rootDir) {
32059
32709
  }
32060
32710
  }
32061
32711
  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;
32712
+ const safeName = sanitizePathSegment(path3.basename(fileName || attachmentId));
32713
+ const ext = path3.extname(safeName) || extensionForMime(mimeType);
32714
+ const stem = path3.basename(safeName, path3.extname(safeName)) || attachmentId;
32065
32715
  return `${sanitizePathSegment(attachmentId)}-${stem}${ext}`;
32066
32716
  }
32067
32717
  function extensionForMime(mimeType) {
@@ -32127,7 +32777,7 @@ function parseContentLength(value) {
32127
32777
  // dist/gateway.js
32128
32778
  import * as crypto2 from "node:crypto";
32129
32779
  import * as os2 from "node:os";
32130
- import * as path6 from "node:path";
32780
+ import * as path7 from "node:path";
32131
32781
 
32132
32782
  // dist/runtime.js
32133
32783
  var runtime = null;
@@ -32185,15 +32835,15 @@ function buildOrchestratorSessionKey(accountId) {
32185
32835
  }
32186
32836
 
32187
32837
  // dist/config-manager.js
32188
- import * as fs3 from "node:fs";
32189
- import * as path3 from "node:path";
32838
+ import * as fs4 from "node:fs";
32839
+ import * as path4 from "node:path";
32190
32840
  var CACHE_FILENAME = "parall-platform-config.json";
32191
32841
  function cachePath(stateDir) {
32192
- return path3.join(stateDir, CACHE_FILENAME);
32842
+ return path4.join(stateDir, CACHE_FILENAME);
32193
32843
  }
32194
32844
  function loadCachedConfig(stateDir) {
32195
32845
  try {
32196
- const raw = fs3.readFileSync(cachePath(stateDir), "utf-8");
32846
+ const raw = fs4.readFileSync(cachePath(stateDir), "utf-8");
32197
32847
  return JSON.parse(raw);
32198
32848
  } catch {
32199
32849
  return null;
@@ -32207,14 +32857,14 @@ function saveCachedConfig(stateDir, config) {
32207
32857
  };
32208
32858
  const filePath = cachePath(stateDir);
32209
32859
  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);
32860
+ fs4.mkdirSync(path4.dirname(filePath), { recursive: true });
32861
+ fs4.writeFileSync(tmpPath, JSON.stringify(cached, null, 2), "utf-8");
32862
+ fs4.renameSync(tmpPath, filePath);
32213
32863
  }
32214
32864
  function applyToOpenClawConfig(configPath, platformConfig, credentials) {
32215
32865
  let existing = {};
32216
32866
  try {
32217
- const raw = fs3.readFileSync(configPath, "utf-8");
32867
+ const raw = fs4.readFileSync(configPath, "utf-8");
32218
32868
  existing = JSON.parse(raw);
32219
32869
  } catch {
32220
32870
  }
@@ -32286,9 +32936,9 @@ function applyToOpenClawConfig(configPath, platformConfig, credentials) {
32286
32936
  agents.defaults = cleanedExisting;
32287
32937
  existing.agents = agents;
32288
32938
  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);
32939
+ fs4.mkdirSync(path4.dirname(configPath), { recursive: true });
32940
+ fs4.writeFileSync(tmpPath, JSON.stringify(existing, null, 2), "utf-8");
32941
+ fs4.renameSync(tmpPath, configPath);
32292
32942
  }
32293
32943
  async function fetchAndApplyPlatformConfig(opts) {
32294
32944
  const { client, stateDir, configPath, credentials, log } = opts;
@@ -32327,7 +32977,7 @@ async function fetchAndApplyPlatformConfig(opts) {
32327
32977
 
32328
32978
  // dist/wiki-helper.js
32329
32979
  import { spawn, spawnSync } from "node:child_process";
32330
- import path4 from "node:path";
32980
+ import path5 from "node:path";
32331
32981
  var DEFAULT_SYNC_TIMEOUT_MS = 9e4;
32332
32982
  var DEFAULT_WATCH_INTERVAL_SEC = 30;
32333
32983
  function isCommandMissing(error) {
@@ -32342,7 +32992,7 @@ function resolveParallCli() {
32342
32992
  return _cli;
32343
32993
  }
32344
32994
  function resolveMountRoot(stateDir) {
32345
- return process.env.PRLL_WIKI_MOUNT_ROOT?.trim() || path4.join(stateDir, "workspace");
32995
+ return process.env.PRLL_WIKI_MOUNT_ROOT?.trim() || path5.join(stateDir, "workspace");
32346
32996
  }
32347
32997
  function resolveWatchIntervalSec() {
32348
32998
  const raw = process.env.PRLL_WIKI_REFRESH_INTERVAL_SEC?.trim();
@@ -32458,7 +33108,7 @@ async function startWikiHelper(params) {
32458
33108
  // dist/oc-session.js
32459
33109
  import { randomUUID } from "node:crypto";
32460
33110
  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";
33111
+ import { join as join4, resolve as resolve2 } from "node:path";
32462
33112
  var CURRENT_SESSION_VERSION = 3;
32463
33113
  function generateId(existing) {
32464
33114
  for (let i = 0; i < 100; i++) {
@@ -32604,7 +33254,7 @@ var SessionManager = class _SessionManager {
32604
33254
  this.leafId = null;
32605
33255
  this.flushed = false;
32606
33256
  const ts = timestamp.replace(/[:.]/g, "-");
32607
- this.sessionFile = join3(this.sessionDir, `${ts}_${this.sessionId}.jsonl`);
33257
+ this.sessionFile = join4(this.sessionDir, `${ts}_${this.sessionId}.jsonl`);
32608
33258
  }
32609
33259
  buildIndex() {
32610
33260
  this.byId.clear();
@@ -32651,14 +33301,14 @@ var SessionManager = class _SessionManager {
32651
33301
  }
32652
33302
  // -- Branching -------------------------------------------------------------
32653
33303
  getBranch(fromId) {
32654
- const path7 = [];
33304
+ const path8 = [];
32655
33305
  const startId = fromId ?? this.leafId;
32656
33306
  let current = startId ? this.byId.get(startId) : void 0;
32657
33307
  while (current) {
32658
- path7.unshift(current);
33308
+ path8.unshift(current);
32659
33309
  current = current.parentId ? this.byId.get(current.parentId) : void 0;
32660
33310
  }
32661
- return path7;
33311
+ return path8;
32662
33312
  }
32663
33313
  createBranchedSession(leafId) {
32664
33314
  const branch = this.getBranch(leafId);
@@ -32668,7 +33318,7 @@ var SessionManager = class _SessionManager {
32668
33318
  const newId = randomUUID();
32669
33319
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
32670
33320
  const ts = timestamp.replace(/[:.]/g, "-");
32671
- const newFile = join3(this.sessionDir, `${ts}_${newId}.jsonl`);
33321
+ const newFile = join4(this.sessionDir, `${ts}_${newId}.jsonl`);
32672
33322
  const header = {
32673
33323
  type: "session",
32674
33324
  version: CURRENT_SESSION_VERSION,
@@ -32714,50 +33364,50 @@ var SessionManager = class _SessionManager {
32714
33364
  return newFile;
32715
33365
  }
32716
33366
  // -- Factory ---------------------------------------------------------------
32717
- static open(path7) {
32718
- const entries = loadEntries(path7);
33367
+ static open(path8) {
33368
+ const entries = loadEntries(path8);
32719
33369
  const header = entries.find((e) => e.type === "session");
32720
33370
  const cwd = header?.cwd ?? process.cwd();
32721
- const dir = resolve2(path7, "..");
32722
- return new _SessionManager(cwd, dir, path7);
33371
+ const dir = resolve2(path8, "..");
33372
+ return new _SessionManager(cwd, dir, path8);
32723
33373
  }
32724
33374
  };
32725
33375
 
32726
33376
  // dist/fork.js
32727
- import * as fs4 from "node:fs";
32728
- import * as path5 from "node:path";
33377
+ import * as fs5 from "node:fs";
33378
+ import * as path6 from "node:path";
32729
33379
  import * as crypto from "node:crypto";
32730
33380
  function readStoreEntry(sessionsDir, sessionKey) {
32731
- const storeFile = path5.join(sessionsDir, "sessions.json");
33381
+ const storeFile = path6.join(sessionsDir, "sessions.json");
32732
33382
  try {
32733
- const store = JSON.parse(fs4.readFileSync(storeFile, "utf-8"));
33383
+ const store = JSON.parse(fs5.readFileSync(storeFile, "utf-8"));
32734
33384
  return store[sessionKey] ?? store[sessionKey.toLowerCase()] ?? null;
32735
33385
  } catch {
32736
33386
  return null;
32737
33387
  }
32738
33388
  }
32739
33389
  function writeStoreEntry(sessionsDir, sessionKey, entry) {
32740
- const storeFile = path5.join(sessionsDir, "sessions.json");
33390
+ const storeFile = path6.join(sessionsDir, "sessions.json");
32741
33391
  try {
32742
33392
  let store = {};
32743
33393
  try {
32744
- store = JSON.parse(fs4.readFileSync(storeFile, "utf-8"));
33394
+ store = JSON.parse(fs5.readFileSync(storeFile, "utf-8"));
32745
33395
  } catch {
32746
33396
  }
32747
33397
  store[sessionKey.toLowerCase()] = entry;
32748
- fs4.writeFileSync(storeFile, JSON.stringify(store, null, 2), { encoding: "utf-8" });
33398
+ fs5.writeFileSync(storeFile, JSON.stringify(store, null, 2), { encoding: "utf-8" });
32749
33399
  return true;
32750
33400
  } catch {
32751
33401
  return false;
32752
33402
  }
32753
33403
  }
32754
33404
  function deleteStoreEntry(sessionsDir, sessionKey) {
32755
- const storeFile = path5.join(sessionsDir, "sessions.json");
33405
+ const storeFile = path6.join(sessionsDir, "sessions.json");
32756
33406
  try {
32757
- const store = JSON.parse(fs4.readFileSync(storeFile, "utf-8"));
33407
+ const store = JSON.parse(fs5.readFileSync(storeFile, "utf-8"));
32758
33408
  delete store[sessionKey];
32759
33409
  delete store[sessionKey.toLowerCase()];
32760
- fs4.writeFileSync(storeFile, JSON.stringify(store, null, 2), { encoding: "utf-8" });
33410
+ fs5.writeFileSync(storeFile, JSON.stringify(store, null, 2), { encoding: "utf-8" });
32761
33411
  } catch {
32762
33412
  }
32763
33413
  }
@@ -32769,17 +33419,17 @@ function resolveTranscriptFile(sessionsDir, sessionKey) {
32769
33419
  if (!entry?.sessionId)
32770
33420
  return null;
32771
33421
  if (entry.sessionFile) {
32772
- const resolved = path5.isAbsolute(entry.sessionFile) ? entry.sessionFile : path5.join(sessionsDir, entry.sessionFile);
32773
- if (fs4.existsSync(resolved))
33422
+ const resolved = path6.isAbsolute(entry.sessionFile) ? entry.sessionFile : path6.join(sessionsDir, entry.sessionFile);
33423
+ if (fs5.existsSync(resolved))
32774
33424
  return resolved;
32775
33425
  }
32776
- const conventional = path5.join(sessionsDir, `${entry.sessionId}.jsonl`);
32777
- if (fs4.existsSync(conventional))
33426
+ const conventional = path6.join(sessionsDir, `${entry.sessionId}.jsonl`);
33427
+ if (fs5.existsSync(conventional))
32778
33428
  return conventional;
32779
33429
  try {
32780
- const files = fs4.readdirSync(sessionsDir);
33430
+ const files = fs5.readdirSync(sessionsDir);
32781
33431
  const match = files.find((file) => file.includes(entry.sessionId) && file.endsWith(".jsonl"));
32782
- return match ? path5.join(sessionsDir, match) : null;
33432
+ return match ? path6.join(sessionsDir, match) : null;
32783
33433
  } catch {
32784
33434
  return null;
32785
33435
  }
@@ -32790,7 +33440,7 @@ function resolveSessionId(sessionsDir, sessionKey) {
32790
33440
  }
32791
33441
  function forkOrchestratorSession(opts) {
32792
33442
  const { orchestratorSessionKey, accountId, transcriptFile, sessionsDir } = opts;
32793
- if (!fs4.existsSync(transcriptFile))
33443
+ if (!fs5.existsSync(transcriptFile))
32794
33444
  return null;
32795
33445
  try {
32796
33446
  const manager = SessionManager.open(transcriptFile);
@@ -32810,7 +33460,7 @@ function forkOrchestratorSession(opts) {
32810
33460
  sessionId = crypto.randomUUID();
32811
33461
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
32812
33462
  const fileTimestamp = timestamp.replace(/[:.]/g, "-");
32813
- sessionFile = path5.join(manager.getSessionDir(), `${fileTimestamp}_${sessionId}.jsonl`);
33463
+ sessionFile = path6.join(manager.getSessionDir(), `${fileTimestamp}_${sessionId}.jsonl`);
32814
33464
  const header = {
32815
33465
  type: "session",
32816
33466
  version: CURRENT_SESSION_VERSION,
@@ -32819,7 +33469,7 @@ function forkOrchestratorSession(opts) {
32819
33469
  cwd: manager.getCwd(),
32820
33470
  parentSession: transcriptFile
32821
33471
  };
32822
- fs4.writeFileSync(sessionFile, `${JSON.stringify(header)}
33472
+ fs5.writeFileSync(sessionFile, `${JSON.stringify(header)}
32823
33473
  `, {
32824
33474
  encoding: "utf-8",
32825
33475
  mode: 384,
@@ -32829,7 +33479,7 @@ function forkOrchestratorSession(opts) {
32829
33479
  const forkSessionKey = `${orchestratorSessionKey}:fork:${sessionId}`;
32830
33480
  const wrote = writeStoreEntry(sessionsDir, forkSessionKey, {
32831
33481
  sessionId,
32832
- sessionFile: path5.relative(sessionsDir, sessionFile),
33482
+ sessionFile: path6.relative(sessionsDir, sessionFile),
32833
33483
  updatedAt: Date.now(),
32834
33484
  spawnedBy: orchestratorSessionKey,
32835
33485
  parentSessionKey: orchestratorSessionKey,
@@ -32844,8 +33494,8 @@ function forkOrchestratorSession(opts) {
32844
33494
  }
32845
33495
  function cleanupForkSession(opts) {
32846
33496
  try {
32847
- if (fs4.existsSync(opts.sessionFile)) {
32848
- fs4.unlinkSync(opts.sessionFile);
33497
+ if (fs5.existsSync(opts.sessionFile)) {
33498
+ fs5.unlinkSync(opts.sessionFile);
32849
33499
  }
32850
33500
  } catch {
32851
33501
  }
@@ -33155,8 +33805,8 @@ var parallGateway = {
33155
33805
  const telemetry = await initAgentTelemetry("parall-openclaw-agent", "openclaw");
33156
33806
  const otelLog = createOtelLogger("agent", "openclaw-agent");
33157
33807
  try {
33158
- const stateDir = process.env.OPENCLAW_STATE_DIR || path6.join(process.env.HOME || "/data", ".openclaw");
33159
- const openclawConfigPath = path6.join(stateDir, "openclaw.json");
33808
+ const stateDir = process.env.OPENCLAW_STATE_DIR || path7.join(process.env.HOME || "/data", ".openclaw");
33809
+ const openclawConfigPath = path7.join(stateDir, "openclaw.json");
33160
33810
  const configManagerOpts = {
33161
33811
  client,
33162
33812
  stateDir,
@@ -33192,7 +33842,7 @@ var parallGateway = {
33192
33842
  wsUrl
33193
33843
  });
33194
33844
  const orchestratorKey = buildOrchestratorSessionKey(ctx.accountId);
33195
- const sessionsDir = path6.join(stateDir, "agents", "main", "sessions");
33845
+ const sessionsDir = path7.join(stateDir, "agents", "main", "sessions");
33196
33846
  const workspaceDir = process.cwd();
33197
33847
  ensureLocalAttachmentGitExclude(workspaceDir);
33198
33848
  const dispatchAdapter = createOpenClawDispatchAdapter({