@parall/parall 1.36.1 → 1.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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();
@@ -26662,6 +26672,17 @@ function buildEventBody(event) {
26662
26672
  if (event.attachedUri)
26663
26673
  lines.push(`[Attached: ${sanitizeMeta(event.attachedUri)}]`);
26664
26674
  lines.push("", event.body);
26675
+ } else if (event.type === "channel_message") {
26676
+ lines.push(`[Event: channel.message]`);
26677
+ const providerLabel = sanitizeMeta(event.channelProvider ?? "external IM");
26678
+ const convLabel = event.channelExternalConversationId ? `${sanitizeMeta(event.channelExternalConversationId)} (${sanitizeMeta(event.channelConversationType ?? "conversation")})` : sanitizeMeta(event.channelConversationType ?? "conversation");
26679
+ lines.push(`[Channel: ${providerLabel} | conversation: ${convLabel}]`);
26680
+ lines.push(`[From: ${sanitizeMeta(event.senderName)} (external user, not a Parall member)]`);
26681
+ if (event.channelExternalMessageId) {
26682
+ lines.push(`[External message ID: ${sanitizeMeta(event.channelExternalMessageId)}]`);
26683
+ }
26684
+ lines.push(`[Audience: this conversation lives on ${providerLabel}, OUTSIDE Parall. Readers cannot open prll:// links, Parall cards, or internal attachments \u2014 never include them in replies. Write plain conversational text.]`);
26685
+ lines.push("", event.body);
26665
26686
  } else if (event.type === "external_trigger") {
26666
26687
  lines.push(`[Event: external.trigger]`);
26667
26688
  lines.push(`[Trigger: prll://${event.targetId}]`);
@@ -26703,7 +26724,7 @@ function buildSendMessageHint(event) {
26703
26724
  }
26704
26725
  if (event.targetId.startsWith("cht_")) {
26705
26726
  return `
26706
- <system-reminder>To reply, run: \`parall messages send prll://${event.targetId} --text "..."\` \u2014 your plain text output is not delivered to the chat.</system-reminder>`;
26727
+ <system-reminder>To reply, run \`parall messages send prll://${event.targetId} --text-file - <<'EOF'\` \u2026 \`EOF\` \u2014 the quoted heredoc keeps \`$\`, backticks and apostrophes literal (plain \`--text "$1,000"\` sends \`,000\`). Your plain text output is not delivered to the chat.</system-reminder>`;
26707
26728
  }
26708
26729
  if (event.targetId.startsWith("tsk_")) {
26709
26730
  return `
@@ -26712,6 +26733,14 @@ function buildSendMessageHint(event) {
26712
26733
  if (event.targetId.startsWith("sch_")) {
26713
26734
  return `
26714
26735
  <system-reminder>To communicate, use the CLI: \`parall messages send\` / \`parall dm\`. Your plain text output is not delivered.</system-reminder>`;
26736
+ }
26737
+ if (event.type === "channel_message") {
26738
+ const clipLabel = event.channelProvider ? `the \`${event.channelProvider}\` clip's` : "your channel provider clip's";
26739
+ const apiLabel = event.channelProvider ?? "external platform";
26740
+ const target = event.channelExternalConversationId ? `{"chat_id": "${event.channelExternalConversationId}", "text": "..."}` : `{"chat_id": "<conversation id>", "text": "..."}`;
26741
+ const threadAlt = event.channelExternalMessageId ? ` To reply in-thread to this specific message, use {"message_id": "${event.channelExternalMessageId}", "text": "..."} instead.` : "";
26742
+ return `
26743
+ <system-reminder>To reply, invoke ${clipLabel} \`send_message\` command with ${target} \u2014 your plain text output is NOT delivered to the external conversation.${threadAlt} The same clip's \`call\` command reaches the wider ${apiLabel} API when needed.</system-reminder>`;
26715
26744
  }
26716
26745
  if (event.type === "external_trigger" || event.targetId.startsWith("xtr_")) {
26717
26746
  return `
@@ -26919,6 +26948,8 @@ Or upload first and reuse across chats:
26919
26948
  parall messages send prll://cht_aaa --attachment att_yyy --text "Report"
26920
26949
  parall messages send prll://cht_bbb --attachment att_yyy --text "FYI"
26921
26950
 
26951
+ The \`--text\` captions above are safe short literals. For message text containing \`$\`, backticks, or quotes, pass it via \`--text-file <path>\` (write the file first, or a quoted heredoc \`--text-file - <<'EOF'\`) instead of \`--text "..."\` \u2014 inside double quotes the shell turns \`$1,000\` into \`,000\` and executes \`$(...)\`.
26952
+
26922
26953
  ### When to reference
26923
26954
 
26924
26955
  - **Origin** \u2014 always link the message or task that triggered your work
@@ -27000,8 +27031,8 @@ function isParallNoReplyCommand(command) {
27000
27031
 
27001
27032
  // ../agent-core/dist/gateway-base.js
27002
27033
  import * as os from "node:os";
27003
- import * as fs from "node:fs";
27004
- import * as path from "node:path";
27034
+ import * as fs2 from "node:fs";
27035
+ import * as path2 from "node:path";
27005
27036
 
27006
27037
  // ../sdk/dist/types.js
27007
27038
  var MENTION_ALL_USER_ID = "all";
@@ -27033,6 +27064,7 @@ var ENDPOINTS = {
27033
27064
  // Org-scoped
27034
27065
  ORG: (orgId) => `${API_BASE}/orgs/${orgId}`,
27035
27066
  ORG_MEMBERS: (orgId) => `${API_BASE}/orgs/${orgId}/members`,
27067
+ ORG_MEMBERS_FORMER: (orgId) => `${API_BASE}/orgs/${orgId}/members/former`,
27036
27068
  TEAMS: (orgId) => `${API_BASE}/orgs/${orgId}/teams`,
27037
27069
  ORG_MEMBERS_ONLINE: (orgId) => `${API_BASE}/orgs/${orgId}/members/online`,
27038
27070
  ORG_MEMBER: (orgId, userId) => `${API_BASE}/orgs/${orgId}/members/${userId}`,
@@ -27182,6 +27214,19 @@ var ENDPOINTS = {
27182
27214
  EXTERNAL_TRIGGER: (orgId, triggerId) => `${API_BASE}/orgs/${orgId}/external-triggers/${triggerId}`,
27183
27215
  EXTERNAL_TRIGGER_RUNS: (orgId) => `${API_BASE}/orgs/${orgId}/external-trigger-runs`,
27184
27216
  EXTERNAL_TRIGGER_RUN: (orgId, runId) => `${API_BASE}/orgs/${orgId}/external-trigger-runs/${runId}`,
27217
+ // External IM channel (org-scoped, platform-mediated Feishu/Slack)
27218
+ CHANNEL_CONNECTIONS: (orgId) => `${API_BASE}/orgs/${orgId}/channel-connections`,
27219
+ CHANNEL_CONNECTION: (orgId, connectionId) => `${API_BASE}/orgs/${orgId}/channel-connections/${connectionId}`,
27220
+ CHANNEL_CONNECTION_CREDENTIALS: (orgId, connectionId) => `${API_BASE}/orgs/${orgId}/channel-connections/${connectionId}/credentials`,
27221
+ CHANNEL_CONNECTION_INGRESS_TOKEN_REGENERATE: (orgId, connectionId) => `${API_BASE}/orgs/${orgId}/channel-connections/${connectionId}/ingress-token/regenerate`,
27222
+ CHANNEL_CONNECTION_CONVERSATIONS: (orgId, connectionId) => `${API_BASE}/orgs/${orgId}/channel-connections/${connectionId}/conversations`,
27223
+ CHANNEL_CONVERSATION: (orgId, conversationId) => `${API_BASE}/orgs/${orgId}/channel-conversations/${conversationId}`,
27224
+ CHANNEL_CONVERSATION_MESSAGES: (orgId, conversationId) => `${API_BASE}/orgs/${orgId}/channel-conversations/${conversationId}/messages`,
27225
+ CHANNEL_CONVERSATION_SESSION: (orgId, conversationId) => `${API_BASE}/orgs/${orgId}/channel-conversations/${conversationId}/session`,
27226
+ CHANNEL_MESSAGE: (orgId, messageId) => `${API_BASE}/orgs/${orgId}/channel-messages/${messageId}`,
27227
+ CHANNEL_PROVISIONING: (orgId) => `${API_BASE}/orgs/${orgId}/channel-provisioning`,
27228
+ CHANNEL_PROVISIONING_SESSION: (orgId, sessionId) => `${API_BASE}/orgs/${orgId}/channel-provisioning/${sessionId}`,
27229
+ CHANNEL_PROVISIONING_CANCEL: (orgId, sessionId) => `${API_BASE}/orgs/${orgId}/channel-provisioning/${sessionId}/cancel`,
27185
27230
  // Invitations (org-scoped, admin)
27186
27231
  ORG_INVITATIONS: (orgId) => `${API_BASE}/orgs/${orgId}/invitations`,
27187
27232
  ORG_INVITATION: (orgId, invId) => `${API_BASE}/orgs/${orgId}/invitations/${invId}`,
@@ -27199,9 +27244,16 @@ var ENDPOINTS = {
27199
27244
  INVITE_LINK_JOIN: `${API_BASE}/invite-link/join`,
27200
27245
  // Wikis (org-scoped, served by wiki-service)
27201
27246
  WIKIS: (orgId) => `${WIKI_BASE}/orgs/${orgId}/wikis`,
27247
+ // Recycle bin — soft-deleted wikis (owner only). Must precede WIKI in the
27248
+ // backend router so `deleted` isn't captured as a {wikiId}; the SDK builder
27249
+ // is just a string.
27250
+ WIKIS_DELETED: (orgId) => `${WIKI_BASE}/orgs/${orgId}/wikis/deleted`,
27251
+ // Detail URL — also reused for DELETE (soft-delete) and `${WIKI}/restore`.
27202
27252
  WIKI: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}`,
27253
+ WIKI_RESTORE: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/restore`,
27203
27254
  WIKI_TREE: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/tree`,
27204
27255
  WIKI_BLOB: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/blob`,
27256
+ WIKI_COPY: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/copy`,
27205
27257
  WIKI_NODE_SECTIONS: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/node-sections`,
27206
27258
  WIKI_SEARCH: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/search`,
27207
27259
  WIKI_PAGE_INDEX: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/page-index`,
@@ -27255,6 +27307,11 @@ var ENDPOINTS = {
27255
27307
  DISPATCH_ACK_BY_ID: (orgId, id) => `${API_BASE}/orgs/${orgId}/dispatch/${id}/ack`,
27256
27308
  DISPATCH_EXPIRE: (orgId) => `${API_BASE}/orgs/${orgId}/dispatch/expire`,
27257
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`,
27258
27315
  // Unread
27259
27316
  UNREAD: `${API_BASE}/me/unread`,
27260
27317
  ORG_UNREAD: (orgId) => `${API_BASE}/orgs/${orgId}/unread`,
@@ -27284,6 +27341,14 @@ var ENDPOINTS = {
27284
27341
  BILLING_AUTO_RELOAD: (orgId) => `${API_BASE}/orgs/${orgId}/billing/auto-reload`,
27285
27342
  BILLING_SETUP_INTENT: (orgId) => `${API_BASE}/orgs/${orgId}/billing/setup-intent`,
27286
27343
  COMPUTE_PRICING: () => `${API_BASE}/billing/compute-pricing`,
27344
+ // Runtime capability table (public, no auth) — SSOT for the create/settings
27345
+ // interlock: per-runtime compute modes, native model family, cross-family
27346
+ // availability, and recommended model.
27347
+ RUNTIMES: () => `${API_BASE}/runtimes`,
27348
+ // Model catalog (public, no auth) — served from the server's DB-backed
27349
+ // snapshot. Optional include_model keeps an agent's pinned hidden legacy
27350
+ // model representable in settings pickers.
27351
+ MODELS: (includeModel) => includeModel ? `${API_BASE}/models?include_model=${encodeURIComponent(includeModel)}` : `${API_BASE}/models`,
27287
27352
  // ADMIN_GRANTS intentionally NOT exported here — it sits under the
27288
27353
  // unauthenticated `/internal/admin/*` surface and must not bleed into the
27289
27354
  // public SDK. Admin dashboard hits the URL directly from its own client.
@@ -27296,6 +27361,7 @@ var ENDPOINTS = {
27296
27361
  AGENT_CLIP: (orgId, agentId, clipId) => `${CLIP_BASE}/orgs/${orgId}/agents/${agentId}/clips/${clipId}`,
27297
27362
  CLIP_INVOKE: (orgId) => `${CLIP_BASE}/orgs/${orgId}/invoke`,
27298
27363
  CLIP_ONLINE: (orgId) => `${CLIP_BASE}/orgs/${orgId}/online`,
27364
+ BROWSER_RUNTIME_STATUS: (orgId) => `${CLIP_BASE}/orgs/${orgId}/browser-runtime/status`,
27299
27365
  BROWSER_PROFILES: (orgId) => `${CLIP_BASE}/orgs/${orgId}/browser-profiles`,
27300
27366
  BROWSER_PROFILE: (orgId, profileId) => `${CLIP_BASE}/orgs/${orgId}/browser-profiles/${profileId}`,
27301
27367
  BROWSER_PROFILE_OPEN: (orgId, profileId) => `${CLIP_BASE}/orgs/${orgId}/browser-profiles/${profileId}/open`,
@@ -27349,10 +27415,13 @@ var WS_EVENTS = {
27349
27415
  INVITATION_REVOKED: "invitation.revoked",
27350
27416
  ORG_JOIN_REQUEST_NEW: "org.join_request.new",
27351
27417
  ORG_INVITE_LINK_JOINED: "org.invite_link.joined",
27418
+ ORG_MEMBER_REMOVED: "org.member.removed",
27352
27419
  AGENT_CONFIG_UPDATE: "agent_config.update",
27353
27420
  PRESENCE_UPDATE: "presence.update",
27354
27421
  WIKI_CHANGESET_CREATED: "wiki.changeset.created",
27355
27422
  WIKI_CHANGESET_UPDATED: "wiki.changeset.updated",
27423
+ WIKI_DELETED: "wiki.deleted",
27424
+ WIKI_RESTORED: "wiki.restored",
27356
27425
  COMMENT_CREATED: "comment.created",
27357
27426
  COMMENT_UPDATED: "comment.updated",
27358
27427
  COMMENT_DELETED: "comment.deleted",
@@ -27363,6 +27432,7 @@ var WS_EVENTS = {
27363
27432
  READ_POSITION_UPDATED: "read_position.updated",
27364
27433
  DISPATCH_NEW: "dispatch.new",
27365
27434
  DISPATCH_RECEIVED: "dispatch.received",
27435
+ DISPATCH_RESOLVED: "dispatch.resolved",
27366
27436
  SCHEDULE_CREATED: "schedule.created",
27367
27437
  SCHEDULE_UPDATED: "schedule.updated",
27368
27438
  SCHEDULE_DELETED: "schedule.deleted",
@@ -27380,6 +27450,7 @@ var WS_EVENTS = {
27380
27450
  MACHINE_FILESYSTEM_BROWSE: "machine.filesystem.browse",
27381
27451
  MACHINE_UPDATE: "machine.update",
27382
27452
  MACHINE_CONFIG_UPDATED: "machine.config.updated",
27453
+ MACHINE_AGENT_CONFIG_UPDATED: "machine.agent_config.updated",
27383
27454
  MACHINE_CLIP_SYNC: "machine.clip.sync",
27384
27455
  MACHINE_BROWSER_PROFILE_LIFECYCLE: "machine.browser_profile.lifecycle",
27385
27456
  MACHINE_BROWSER_PROFILE_VIEWER: "machine.browser_profile.viewer",
@@ -27457,8 +27528,8 @@ var ParallClient = class _ParallClient {
27457
27528
  * is authoritative, so wiki vs api routing can't drift from how a caller
27458
27529
  * happens to invoke the client.
27459
27530
  */
27460
- baseUrlFor(path7) {
27461
- return path7.startsWith(WIKI_BASE) ? this.wikiBaseUrl : this.baseUrl;
27531
+ baseUrlFor(path8) {
27532
+ return path8.startsWith(WIKI_BASE) ? this.wikiBaseUrl : this.baseUrl;
27462
27533
  }
27463
27534
  setToken(token) {
27464
27535
  this.token = token;
@@ -27485,10 +27556,10 @@ var ParallClient = class _ParallClient {
27485
27556
  * REFRESH_THRESHOLD_S, refresh it **before** sending the request.
27486
27557
  * No-op when the token is still fresh, missing, or un-parseable.
27487
27558
  */
27488
- async ensureFreshToken(path7) {
27559
+ async ensureFreshToken(path8) {
27489
27560
  if (!this.token || !this.getRefreshToken)
27490
27561
  return;
27491
- const pathSuffix = path7.replace(/^\/api\/v1/, "");
27562
+ const pathSuffix = path8.replace(/^\/api\/v1/, "");
27492
27563
  if (_ParallClient.AUTH_PATHS.has(pathSuffix))
27493
27564
  return;
27494
27565
  const exp = _ParallClient.decodeJwtExp(this.token);
@@ -27520,11 +27591,11 @@ var ParallClient = class _ParallClient {
27520
27591
  this.refreshPromise = null;
27521
27592
  }
27522
27593
  }
27523
- async request(method, path7, body, query, retried = false, opts) {
27594
+ async request(method, path8, body, query, retried = false, opts) {
27524
27595
  if (!retried) {
27525
- await this.ensureFreshToken(path7);
27596
+ await this.ensureFreshToken(path8);
27526
27597
  }
27527
- let url = `${this.baseUrlFor(path7)}${path7}`;
27598
+ let url = `${this.baseUrlFor(path8)}${path8}`;
27528
27599
  if (query) {
27529
27600
  const params = new URLSearchParams();
27530
27601
  for (const [key, value] of Object.entries(query)) {
@@ -27554,12 +27625,12 @@ var ParallClient = class _ParallClient {
27554
27625
  throw _ParallClient.normalizeFetchError(err);
27555
27626
  }
27556
27627
  if (res.status === 401) {
27557
- const pathSuffix = path7.replace(/^\/api\/v1/, "");
27628
+ const pathSuffix = path8.replace(/^\/api\/v1/, "");
27558
27629
  const isAuthPath = _ParallClient.AUTH_PATHS.has(pathSuffix);
27559
27630
  if (!retried && !isAuthPath && this.getRefreshToken) {
27560
27631
  const refreshed = await this.tryRefresh();
27561
27632
  if (refreshed) {
27562
- return this.request(method, path7, body, query, true, opts);
27633
+ return this.request(method, path8, body, query, true, opts);
27563
27634
  }
27564
27635
  }
27565
27636
  if (this.onTokenExpired && !isAuthPath) {
@@ -27570,6 +27641,7 @@ var ParallClient = class _ParallClient {
27570
27641
  const rawErrorBody = await res.json().catch(() => ({}));
27571
27642
  throw buildApiError(res, rawErrorBody);
27572
27643
  }
27644
+ opts?.onStatus?.(res.status);
27573
27645
  if (res.status === 204)
27574
27646
  return void 0;
27575
27647
  if (res.status === 202) {
@@ -27588,15 +27660,15 @@ var ParallClient = class _ParallClient {
27588
27660
  * hit the 100 MiB cap, so a longer 5-minute timeout is used so a
27589
27661
  * 50 MiB blob on a slow connection doesn't get chopped at 15 s.
27590
27662
  */
27591
- async multipartRequest(method, path7, body, retried = false) {
27663
+ async multipartRequest(method, path8, body, retried = false) {
27592
27664
  if (!retried) {
27593
- await this.ensureFreshToken(path7);
27665
+ await this.ensureFreshToken(path8);
27594
27666
  }
27595
27667
  const { "Content-Type": _drop, ...headers } = this.buildHeaders();
27596
27668
  void _drop;
27597
27669
  let res;
27598
27670
  try {
27599
- res = await fetch(`${this.baseUrlFor(path7)}${path7}`, {
27671
+ res = await fetch(`${this.baseUrlFor(path8)}${path8}`, {
27600
27672
  method,
27601
27673
  headers,
27602
27674
  body,
@@ -27606,12 +27678,12 @@ var ParallClient = class _ParallClient {
27606
27678
  throw _ParallClient.normalizeFetchError(err);
27607
27679
  }
27608
27680
  if (res.status === 401) {
27609
- const pathSuffix = path7.replace(/^\/api\/v1/, "");
27681
+ const pathSuffix = path8.replace(/^\/api\/v1/, "");
27610
27682
  const isAuthPath = _ParallClient.AUTH_PATHS.has(pathSuffix);
27611
27683
  if (!retried && !isAuthPath && this.getRefreshToken) {
27612
27684
  const refreshed = await this.tryRefresh();
27613
27685
  if (refreshed) {
27614
- return this.multipartRequest(method, path7, body, true);
27686
+ return this.multipartRequest(method, path8, body, true);
27615
27687
  }
27616
27688
  }
27617
27689
  if (this.onTokenExpired && !isAuthPath) {
@@ -27719,6 +27791,13 @@ var ParallClient = class _ParallClient {
27719
27791
  const res = await this.request("GET", ENDPOINTS.ORG_MEMBERS(orgId));
27720
27792
  return res.data;
27721
27793
  }
27794
+ /** User IDs of soft-removed (former) org members — for marking their avatar /
27795
+ * name as "left" where dormant relations still surface them (DMs, task
27796
+ * assignees, message history). */
27797
+ async getFormerMemberIds(orgId) {
27798
+ const res = await this.request("GET", ENDPOINTS.ORG_MEMBERS_FORMER(orgId));
27799
+ return res.user_ids ?? [];
27800
+ }
27722
27801
  async getTeams(orgId) {
27723
27802
  const res = await this.request("GET", ENDPOINTS.TEAMS(orgId));
27724
27803
  return res.data;
@@ -27896,6 +27975,21 @@ var ParallClient = class _ParallClient {
27896
27975
  async sendMessage(orgId, chatId, req) {
27897
27976
  return this.request("POST", ENDPOINTS.CHAT_MESSAGES(orgId, chatId), req);
27898
27977
  }
27978
+ /**
27979
+ * sendMessage variant that also reports whether the server answered with an
27980
+ * idempotent replay (HTTP 200 — the message already existed for this
27981
+ * idempotency/effect key) instead of a fresh create (201). Used by the CLI
27982
+ * to surface "already sent by a previous run (deduplicated)".
27983
+ */
27984
+ async sendMessageDetailed(orgId, chatId, req) {
27985
+ let status = 0;
27986
+ const message = await this.request("POST", ENDPOINTS.CHAT_MESSAGES(orgId, chatId), req, void 0, false, {
27987
+ onStatus: (s) => {
27988
+ status = s;
27989
+ }
27990
+ });
27991
+ return { message, deduplicated: status === 200 };
27992
+ }
27899
27993
  async getMessages(orgId, chatId, params) {
27900
27994
  return this.request("GET", ENDPOINTS.CHAT_MESSAGES(orgId, chatId), void 0, params);
27901
27995
  }
@@ -28207,20 +28301,25 @@ var ParallClient = class _ParallClient {
28207
28301
  return res.data;
28208
28302
  }
28209
28303
  /** `POST /machines/me/browser-profiles/{profileId}/status` — report runtime status. */
28210
- async reportBrowserProfileStatus(profileId, status, errorMsg) {
28304
+ async reportBrowserProfileStatus(profileId, status, errorMsg, generation) {
28211
28305
  await this.request("POST", ENDPOINTS.MACHINES_ME_BROWSER_PROFILE_STATUS(profileId), {
28212
28306
  status,
28213
- ...errorMsg ? { error_msg: errorMsg } : {}
28307
+ ...errorMsg ? { error_msg: errorMsg } : {},
28308
+ ...generation !== void 0 ? { generation } : {}
28214
28309
  });
28215
28310
  }
28216
28311
  /**
28217
28312
  * `POST /machines/me/health` — bump the Machine's `updated_at` to now and
28218
- * report daemon state. The daemon should call this on a fixed cadence (e.g.
28219
- * every 30s) so an external observer can detect a wedged supervisor. Both
28313
+ * report daemon state. The daemon calls this on startup and again whenever
28314
+ * periodic runtime re-detection produces a CHANGED result (there is no
28315
+ * fixed-cadence keepalive loop today — steady state posts nothing). All
28220
28316
  * fields are optional and only persisted when changed:
28221
28317
  * - `daemonVersion` — the running bundle/launcher version.
28222
28318
  * - `selfUpdateCapable` — whether the daemon can act on a machine.update
28223
28319
  * signal (true under a service manager, false for bare `npx` foreground).
28320
+ * - `detectedRuntimes` — runtime CLIs found on the host. Omitted = no
28321
+ * report this beat (server keeps the stored value); [] = detection ran
28322
+ * and found nothing (server clears to empty).
28224
28323
  */
28225
28324
  async postMachineHeartbeat(opts) {
28226
28325
  const body = {};
@@ -28228,6 +28327,8 @@ var ParallClient = class _ParallClient {
28228
28327
  body.daemon_version = opts.daemonVersion;
28229
28328
  if (opts?.selfUpdateCapable !== void 0)
28230
28329
  body.self_update_capable = opts.selfUpdateCapable;
28330
+ if (opts?.detectedRuntimes !== void 0)
28331
+ body.detected_runtimes = opts.detectedRuntimes;
28231
28332
  return this.request("POST", ENDPOINTS.MACHINES_ME_HEALTH, Object.keys(body).length > 0 ? body : void 0);
28232
28333
  }
28233
28334
  async reportAgentWorkspaceState(agentId, state) {
@@ -28276,8 +28377,8 @@ var ParallClient = class _ParallClient {
28276
28377
  async requestMachineUpdate(orgId, machineId, mandatory = false) {
28277
28378
  await this.request("POST", ENDPOINTS.MACHINE_REQUEST_UPDATE(orgId, machineId), { mandatory });
28278
28379
  }
28279
- async browseMachineFilesystem(orgId, machineId, path7) {
28280
- return this.request("POST", ENDPOINTS.MACHINE_BROWSE(orgId, machineId), { path: path7 }, void 0, false, { timeoutMs: 15e3 });
28380
+ async browseMachineFilesystem(orgId, machineId, path8) {
28381
+ return this.request("POST", ENDPOINTS.MACHINE_BROWSE(orgId, machineId), { path: path8 }, void 0, false, { timeoutMs: 15e3 });
28281
28382
  }
28282
28383
  /** Create a new machine key. Returns the raw key string (shown once) + metadata. */
28283
28384
  async createMachineKey(orgId, machineId, name) {
@@ -28350,6 +28451,30 @@ var ParallClient = class _ParallClient {
28350
28451
  const params = maxAgeHours !== void 0 ? { max_age_hours: String(maxAgeHours) } : void 0;
28351
28452
  return this.request("POST", ENDPOINTS.DISPATCH_EXPIRE(orgId), void 0, params);
28352
28453
  }
28454
+ /**
28455
+ * Claim a dispatch lane (explicit consume endpoint). Occupies the
28456
+ * (agent, target, thread) lane and folds claimable WorkItems into it;
28457
+ * `claimed: false` means a healthy incumbent holds the lane.
28458
+ */
28459
+ async claimDispatch(orgId, req) {
28460
+ return this.request("POST", ENDPOINTS.DISPATCH_CLAIM(orgId), req);
28461
+ }
28462
+ /** Fold a pending same-target WorkItem into a live lane (409 STALE_LANE when dethroned). */
28463
+ async steerDispatch(orgId, req) {
28464
+ return this.request("POST", ENDPOINTS.DISPATCH_STEER(orgId), req);
28465
+ }
28466
+ /** End a turn: no_action sweep of the lane's members + lane release + re-drive check. */
28467
+ async completeDispatch(orgId, req) {
28468
+ return this.request("POST", ENDPOINTS.DISPATCH_COMPLETE(orgId), req);
28469
+ }
28470
+ /** Release a lane on graceful shutdown — members return to pending immediately. */
28471
+ async releaseDispatchLane(orgId, lane) {
28472
+ return this.request("POST", ENDPOINTS.DISPATCH_RELEASE(orgId), { lane });
28473
+ }
28474
+ /** Renew a live lane's lease (long-turn keepalive). 409 STALE_LANE when dethroned. */
28475
+ async heartbeatDispatchLane(orgId, req) {
28476
+ return this.request("POST", ENDPOINTS.DISPATCH_HEARTBEAT(orgId), req);
28477
+ }
28353
28478
  async getDispatchByMessages(orgId, chatId, messageIds) {
28354
28479
  const params = { chat_id: chatId, message_ids: messageIds.join(",") };
28355
28480
  return this.request("GET", ENDPOINTS.DISPATCH_BY_MESSAGES(orgId), void 0, params);
@@ -28546,6 +28671,59 @@ var ParallClient = class _ParallClient {
28546
28671
  async deleteExternalConnection(orgId, connectionId) {
28547
28672
  return this.request("DELETE", ENDPOINTS.EXTERNAL_CONNECTION(orgId, connectionId));
28548
28673
  }
28674
+ // ---- External IM channel (org-scoped) ----
28675
+ async createChannelConnection(orgId, input) {
28676
+ return this.request("POST", ENDPOINTS.CHANNEL_CONNECTIONS(orgId), input);
28677
+ }
28678
+ async listChannelConnections(orgId) {
28679
+ return this.request("GET", ENDPOINTS.CHANNEL_CONNECTIONS(orgId));
28680
+ }
28681
+ async getChannelConnection(orgId, connectionId) {
28682
+ return this.request("GET", ENDPOINTS.CHANNEL_CONNECTION(orgId, connectionId));
28683
+ }
28684
+ async updateChannelConnection(orgId, connectionId, patch) {
28685
+ return this.request("PATCH", ENDPOINTS.CHANNEL_CONNECTION(orgId, connectionId), patch);
28686
+ }
28687
+ async archiveChannelConnection(orgId, connectionId) {
28688
+ return this.request("DELETE", ENDPOINTS.CHANNEL_CONNECTION(orgId, connectionId));
28689
+ }
28690
+ async deliverChannelCredentials(orgId, connectionId, credentials) {
28691
+ return this.request("POST", ENDPOINTS.CHANNEL_CONNECTION_CREDENTIALS(orgId, connectionId), credentials);
28692
+ }
28693
+ async regenerateChannelIngressToken(orgId, connectionId) {
28694
+ return this.request("POST", ENDPOINTS.CHANNEL_CONNECTION_INGRESS_TOKEN_REGENERATE(orgId, connectionId));
28695
+ }
28696
+ /** Start a Feishu one-click provisioning session (device flow QR). */
28697
+ async initiateChannelProvisioning(orgId, input) {
28698
+ return this.request("POST", ENDPOINTS.CHANNEL_PROVISIONING(orgId), input);
28699
+ }
28700
+ /**
28701
+ * Lazy status poll — server-side this may forward one provider poll, so
28702
+ * call it at the session's `poll_interval_seconds` cadence, not faster.
28703
+ */
28704
+ async getChannelProvisioningSession(orgId, sessionId) {
28705
+ return this.request("GET", ENDPOINTS.CHANNEL_PROVISIONING_SESSION(orgId, sessionId));
28706
+ }
28707
+ async cancelChannelProvisioning(orgId, sessionId) {
28708
+ return this.request("POST", ENDPOINTS.CHANNEL_PROVISIONING_CANCEL(orgId, sessionId));
28709
+ }
28710
+ async listChannelConversations(orgId, connectionId) {
28711
+ return this.request("GET", ENDPOINTS.CHANNEL_CONNECTION_CONVERSATIONS(orgId, connectionId));
28712
+ }
28713
+ async getChannelConversation(orgId, conversationId) {
28714
+ return this.request("GET", ENDPOINTS.CHANNEL_CONVERSATION(orgId, conversationId));
28715
+ }
28716
+ async listChannelConversationMessages(orgId, conversationId, limit) {
28717
+ return this.request("GET", ENDPOINTS.CHANNEL_CONVERSATION_MESSAGES(orgId, conversationId), void 0, { limit });
28718
+ }
28719
+ async setChannelConversationSession(orgId, conversationId, agentSessionId) {
28720
+ return this.request("PATCH", ENDPOINTS.CHANNEL_CONVERSATION_SESSION(orgId, conversationId), {
28721
+ agent_session_id: agentSessionId
28722
+ });
28723
+ }
28724
+ async getChannelMessage(orgId, messageId) {
28725
+ return this.request("GET", ENDPOINTS.CHANNEL_MESSAGE(orgId, messageId));
28726
+ }
28549
28727
  async getExternalTriggerSchema(orgId, connectionId) {
28550
28728
  return this.request("GET", ENDPOINTS.EXTERNAL_TRIGGER_SCHEMA(orgId, connectionId));
28551
28729
  }
@@ -28587,6 +28765,23 @@ var ParallClient = class _ParallClient {
28587
28765
  async getWiki(orgId, wikiId) {
28588
28766
  return this.request("GET", ENDPOINTS.WIKI(orgId, wikiId));
28589
28767
  }
28768
+ /** Soft-delete an entire wiki (org owner only; the default wiki is
28769
+ * protected → 403 WIKI_PROTECTED). Returns the deleted wiki. Recoverable
28770
+ * via restoreWiki within the retention window. */
28771
+ async deleteWiki(orgId, wikiId) {
28772
+ return this.request("DELETE", ENDPOINTS.WIKI(orgId, wikiId));
28773
+ }
28774
+ /** Restore a soft-deleted wiki (org owner only). 409 WIKI_PURGE_STARTED once
28775
+ * purge has been claimed (no longer restorable), or 404 once the row is gone.
28776
+ * Returns the restored wiki. */
28777
+ async restoreWiki(orgId, wikiId) {
28778
+ return this.request("POST", ENDPOINTS.WIKI_RESTORE(orgId, wikiId));
28779
+ }
28780
+ /** Recycle bin — list soft-deleted wikis for the org (owner only). */
28781
+ async getDeletedWikis(orgId) {
28782
+ const res = await this.request("GET", ENDPOINTS.WIKIS_DELETED(orgId));
28783
+ return res.data;
28784
+ }
28590
28785
  async getWikiTree(orgId, wikiId, params) {
28591
28786
  return this.request("GET", ENDPOINTS.WIKI_TREE(orgId, wikiId), void 0, params);
28592
28787
  }
@@ -28620,6 +28815,15 @@ var ParallClient = class _ParallClient {
28620
28815
  blob.signed_url = this.absoluteMediaUrl(blob.signed_url);
28621
28816
  return blob;
28622
28817
  }
28818
+ /**
28819
+ * Copy one wiki file to another path as an independent snapshot (no sync).
28820
+ * Used to "share a private file out": copy a file from the caller's personal
28821
+ * namespace (`users/{userId}/…`) into the public wiki, leaving the original
28822
+ * untouched. 409 `FILE_EXISTS` if `dest_path` already exists.
28823
+ */
28824
+ async copyWikiFile(orgId, wikiId, req) {
28825
+ return this.request("POST", ENDPOINTS.WIKI_COPY(orgId, wikiId), req);
28826
+ }
28623
28827
  async getWikiNodeSections(orgId, wikiId, params) {
28624
28828
  return this.request("GET", ENDPOINTS.WIKI_NODE_SECTIONS(orgId, wikiId), void 0, params);
28625
28829
  }
@@ -28735,8 +28939,8 @@ var ParallClient = class _ParallClient {
28735
28939
  async deleteWikiRestriction(orgId, wikiId, restrictionId) {
28736
28940
  await this.request("DELETE", ENDPOINTS.WIKI_RESTRICTION(orgId, wikiId, restrictionId));
28737
28941
  }
28738
- async getWikiAccessStatus(orgId, wikiId, path7) {
28739
- return this.request("GET", ENDPOINTS.WIKI_ACCESS_STATUS(orgId, wikiId), void 0, path7 ? { path: path7 } : void 0);
28942
+ async getWikiAccessStatus(orgId, wikiId, path8) {
28943
+ return this.request("GET", ENDPOINTS.WIKI_ACCESS_STATUS(orgId, wikiId), void 0, path8 ? { path: path8 } : void 0);
28740
28944
  }
28741
28945
  async createWikiAccessRequest(orgId, wikiId, data) {
28742
28946
  await this.request("POST", ENDPOINTS.WIKI_ACCESS_REQUESTS(orgId, wikiId), data);
@@ -28745,14 +28949,14 @@ var ParallClient = class _ParallClient {
28745
28949
  async getWikiCommits(orgId, wikiId, params) {
28746
28950
  return this.request("GET", ENDPOINTS.WIKI_COMMITS(orgId, wikiId), void 0, params);
28747
28951
  }
28748
- async getWikiFileCommits(orgId, wikiId, path7, params) {
28952
+ async getWikiFileCommits(orgId, wikiId, path8, params) {
28749
28953
  return this.request("GET", ENDPOINTS.WIKI_FILE_COMMITS(orgId, wikiId), void 0, {
28750
- path: path7,
28954
+ path: path8,
28751
28955
  ...params
28752
28956
  });
28753
28957
  }
28754
- async getWikiBlame(orgId, wikiId, path7, ref) {
28755
- return this.request("GET", ENDPOINTS.WIKI_BLAME(orgId, wikiId), void 0, { path: path7, ref });
28958
+ async getWikiBlame(orgId, wikiId, path8, ref) {
28959
+ return this.request("GET", ENDPOINTS.WIKI_BLAME(orgId, wikiId), void 0, { path: path8, ref });
28756
28960
  }
28757
28961
  // ---- Wiki Operations (audit log) ----
28758
28962
  async getWikiOperations(orgId, wikiId, params) {
@@ -28893,6 +29097,19 @@ var ParallClient = class _ParallClient {
28893
29097
  const resp = await this.request("GET", ENDPOINTS.COMPUTE_PRICING());
28894
29098
  return resp.data;
28895
29099
  }
29100
+ /** Runtime capability table (public) — SSOT for the create/settings interlock. */
29101
+ async getRuntimes() {
29102
+ const resp = await this.request("GET", ENDPOINTS.RUNTIMES());
29103
+ return resp.data;
29104
+ }
29105
+ /**
29106
+ * Platform model catalog (public) — DB-backed, replaces the compile-time
29107
+ * PLATFORM_MODELS constant. Pass includeModel to keep an agent's pinned
29108
+ * hidden legacy model representable (settings picker).
29109
+ */
29110
+ async getModels(includeModel) {
29111
+ return this.request("GET", ENDPOINTS.MODELS(includeModel));
29112
+ }
28896
29113
  // ---- Clips ----
28897
29114
  async listClips(orgId) {
28898
29115
  const resp = await this.request("GET", ENDPOINTS.CLIPS(orgId));
@@ -28937,6 +29154,11 @@ var ParallClient = class _ParallClient {
28937
29154
  const resp = await this.request("GET", ENDPOINTS.CLIP_ONLINE(orgId));
28938
29155
  return resp.data;
28939
29156
  }
29157
+ /** Deployment-wide hosted browser runtime availability — drives whether the
29158
+ * create UI offers the platform-hosted placement. Fail-closed server-side. */
29159
+ async getBrowserRuntimeStatus(orgId) {
29160
+ return this.request("GET", ENDPOINTS.BROWSER_RUNTIME_STATUS(orgId));
29161
+ }
28940
29162
  /** Org-wide browser-profile discovery list. Returns the sanitized
28941
29163
  * {@link BrowserProfileListItem} shape (not the full domain model), each row
28942
29164
  * carrying a per-viewer `can_open` control hint. */
@@ -29366,6 +29588,404 @@ var ParallWs = class {
29366
29588
  }
29367
29589
  };
29368
29590
 
29591
+ // ../agent-core/dist/lane-ledger.js
29592
+ import * as fs from "node:fs";
29593
+ var LedgerUnsupportedError = class extends Error {
29594
+ };
29595
+ function isStaleLane(err) {
29596
+ return err instanceof ApiError && err.status === 409 && err.code === "STALE_LANE";
29597
+ }
29598
+ function isEndpointMissing(err) {
29599
+ return err instanceof ApiError && err.status === 404 && !err.code;
29600
+ }
29601
+ var LaneLedger = class {
29602
+ opts;
29603
+ lanes = /* @__PURE__ */ new Map();
29604
+ constructor(opts) {
29605
+ this.opts = opts;
29606
+ }
29607
+ get contextDir() {
29608
+ return this.opts.contextDir;
29609
+ }
29610
+ /** Only chat message events ride the lane ledger; typed events stay on the legacy ack path. */
29611
+ handles(event) {
29612
+ return event.type === "message" && event.targetId.startsWith("cht_");
29613
+ }
29614
+ laneKeyFor(event) {
29615
+ if (event.type !== "message" && event.dispatchEventId) {
29616
+ return laneKeyForTarget(`dsp:${event.dispatchEventId}`);
29617
+ }
29618
+ return laneKeyForTarget(`prll://${event.targetId}`, event.threadRootId);
29619
+ }
29620
+ getForEvent(event) {
29621
+ return this.lanes.get(this.laneKeyFor(event));
29622
+ }
29623
+ laneContextPath(lane) {
29624
+ return laneContextFilePath(this.opts.contextDir, lane.targetUri, lane.threadRootId);
29625
+ }
29626
+ /**
29627
+ * Claim (or reuse) the lane for a group of same-lane message events and
29628
+ * fold every group member into it. Returns 'foreign' when a healthy
29629
+ * incumbent (another pod) holds the resource — the caller must not
29630
+ * dispatch; the events stay pending server-side and re-drive after the
29631
+ * incumbent completes.
29632
+ */
29633
+ async ensureLane(events) {
29634
+ const trigger = events[events.length - 1];
29635
+ const laneKey = this.laneKeyFor(trigger);
29636
+ let lane = this.lanes.get(laneKey);
29637
+ if (!lane) {
29638
+ const targetUri = `prll://${trigger.targetId}`;
29639
+ let res;
29640
+ try {
29641
+ res = await this.opts.client.claimDispatch(this.opts.orgId, {
29642
+ target_uri: targetUri,
29643
+ thread_root_id: trigger.threadRootId,
29644
+ limit: 100
29645
+ });
29646
+ } catch (err) {
29647
+ if (isEndpointMissing(err))
29648
+ throw new LedgerUnsupportedError("claim endpoint unavailable");
29649
+ throw err;
29650
+ }
29651
+ if (!res.claimed || !res.lane) {
29652
+ this.opts.log?.info(`lane for ${targetUri} held by a healthy incumbent \u2014 leaving events pending for re-drive`);
29653
+ return null;
29654
+ }
29655
+ const leaseUntilMs = Date.parse(res.lease_until ?? "");
29656
+ lane = {
29657
+ laneKey,
29658
+ lane: res.lane,
29659
+ targetUri,
29660
+ threadRootId: trigger.threadRootId,
29661
+ folded: /* @__PURE__ */ new Map(),
29662
+ ...Number.isNaN(leaseUntilMs) ? {} : { leaseUntilMs, leaseTtlMs: Math.max(leaseUntilMs - Date.now(), 6e4) }
29663
+ };
29664
+ for (const ev of res.events ?? []) {
29665
+ lane.folded.set(ev.source_id, ev.id);
29666
+ }
29667
+ this.lanes.set(laneKey, lane);
29668
+ }
29669
+ for (const ev of events) {
29670
+ if (lane.folded.has(ev.messageId))
29671
+ continue;
29672
+ try {
29673
+ const res = await this.opts.client.steerDispatch(this.opts.orgId, {
29674
+ lane: lane.lane,
29675
+ target_uri: lane.targetUri,
29676
+ thread_root_id: lane.threadRootId,
29677
+ ...ev.dispatchEventId ? { dispatch_event_id: ev.dispatchEventId } : { source_type: "message", source_id: ev.messageId }
29678
+ });
29679
+ lane.folded.set(ev.messageId, res.dispatch_event_id);
29680
+ } catch (err) {
29681
+ if (isStaleLane(err)) {
29682
+ this.lanes.delete(laneKey);
29683
+ return null;
29684
+ }
29685
+ this.opts.log?.warn(`steer fold failed for ${ev.messageId} \u2014 failing closed, releasing lane: ${String(err)}`);
29686
+ await this.release(laneKey);
29687
+ return null;
29688
+ }
29689
+ }
29690
+ return lane;
29691
+ }
29692
+ /**
29693
+ * Fold a live mid-turn message into its active lane BEFORE injecting it
29694
+ * into the running turn. Injection without a successful fold is forbidden —
29695
+ * an un-folded injected message would be re-driven after complete and the
29696
+ * model would handle it twice.
29697
+ */
29698
+ async steerLive(event) {
29699
+ const laneKey = this.laneKeyFor(event);
29700
+ const lane = this.lanes.get(laneKey);
29701
+ if (!lane)
29702
+ return false;
29703
+ if (lane.folded.has(event.messageId))
29704
+ return true;
29705
+ try {
29706
+ const res = await this.opts.client.steerDispatch(this.opts.orgId, {
29707
+ lane: lane.lane,
29708
+ target_uri: lane.targetUri,
29709
+ thread_root_id: lane.threadRootId,
29710
+ ...event.dispatchEventId ? { dispatch_event_id: event.dispatchEventId } : { source_type: "message", source_id: event.messageId }
29711
+ });
29712
+ lane.folded.set(event.messageId, res.dispatch_event_id);
29713
+ return true;
29714
+ } catch (err) {
29715
+ if (isStaleLane(err)) {
29716
+ this.lanes.delete(laneKey);
29717
+ } else {
29718
+ this.opts.log?.warn(`live steer failed for ${event.messageId}: ${String(err)}`);
29719
+ }
29720
+ return false;
29721
+ }
29722
+ }
29723
+ /**
29724
+ * Complete the lane when no local work remains for it: the server sweeps
29725
+ * still-leased members as no_action, releases the occupancy row, and
29726
+ * re-drives any same-target pending work. A STALE_LANE answer means a
29727
+ * takeover already owns the resource — local state is dropped either way.
29728
+ */
29729
+ async completeIfIdle(laneKey, hasMoreLocal) {
29730
+ const lane = this.lanes.get(laneKey);
29731
+ if (!lane || hasMoreLocal)
29732
+ return;
29733
+ this.lanes.delete(laneKey);
29734
+ this.removeLaneContext(lane);
29735
+ try {
29736
+ const res = await this.opts.client.completeDispatch(this.opts.orgId, {
29737
+ lane: lane.lane,
29738
+ target_uri: lane.targetUri,
29739
+ thread_root_id: lane.threadRootId
29740
+ });
29741
+ if (res.swept_no_action > 0 || res.redriven) {
29742
+ this.opts.log?.info(`lane complete for ${lane.targetUri}: swept ${res.swept_no_action} no_action, redriven=${res.redriven}`);
29743
+ }
29744
+ } catch (err) {
29745
+ if (isStaleLane(err)) {
29746
+ this.opts.log?.info(`lane complete skipped for ${lane.targetUri} \u2014 taken over`);
29747
+ return;
29748
+ }
29749
+ this.opts.log?.warn(`lane complete failed for ${lane.targetUri}: ${String(err)}`);
29750
+ }
29751
+ }
29752
+ /**
29753
+ * Long-turn keepalive: renew the lane's lease on runtime activity, throttled
29754
+ * so a chatty turn doesn't spam the server. Without this, a legitimately
29755
+ * long turn (> lane TTL) would be dethroned mid-flight and every subsequent
29756
+ * write misfired with STALE_LANE — the design doc's "long turns renew via
29757
+ * step writes". Fire-and-forget: a failed renewal is surfaced by the next
29758
+ * write's incumbency check anyway.
29759
+ */
29760
+ maybeRenew(lane) {
29761
+ const now = Date.now();
29762
+ const ttl = lane.leaseTtlMs ?? 10 * 6e4;
29763
+ const until = lane.leaseUntilMs ?? now;
29764
+ if (until - now > ttl / 2)
29765
+ return;
29766
+ lane.leaseUntilMs = now + ttl;
29767
+ void this.opts.client.heartbeatDispatchLane(this.opts.orgId, {
29768
+ lane: lane.lane,
29769
+ target_uri: lane.targetUri,
29770
+ thread_root_id: lane.threadRootId
29771
+ }).then((res) => {
29772
+ const until2 = Date.parse(res?.lease_until ?? "");
29773
+ if (!Number.isNaN(until2))
29774
+ lane.leaseUntilMs = until2;
29775
+ }).catch((err) => {
29776
+ if (isStaleLane(err)) {
29777
+ this.lanes.delete(lane.laneKey);
29778
+ this.opts.log?.warn(`lane ${lane.targetUri} was taken over during the turn`);
29779
+ return;
29780
+ }
29781
+ this.opts.log?.warn(`lane heartbeat failed for ${lane.targetUri}: ${String(err)}`);
29782
+ });
29783
+ }
29784
+ /**
29785
+ * Release a lane's unresolved members back to pending (dispatch error /
29786
+ * shutdown) so the next pod re-claims immediately instead of waiting out
29787
+ * the lease.
29788
+ */
29789
+ async release(laneKey) {
29790
+ const lane = this.lanes.get(laneKey);
29791
+ if (!lane)
29792
+ return;
29793
+ this.lanes.delete(laneKey);
29794
+ this.removeLaneContext(lane);
29795
+ try {
29796
+ await this.opts.client.releaseDispatchLane(this.opts.orgId, lane.lane);
29797
+ } catch (err) {
29798
+ this.opts.log?.warn(`lane release failed for ${lane.targetUri}: ${String(err)}`);
29799
+ }
29800
+ }
29801
+ async releaseAll() {
29802
+ const keys = [...this.lanes.keys()];
29803
+ for (const key of keys) {
29804
+ await this.release(key);
29805
+ }
29806
+ }
29807
+ /** True when any lane is currently active (used by shutdown logging). */
29808
+ get activeCount() {
29809
+ return this.lanes.size;
29810
+ }
29811
+ /**
29812
+ * Claim the single-member lane of one typed WorkItem (resource = dsp:<id>),
29813
+ * by WorkItem id or by source identity (the live task.assigned event has no
29814
+ * WorkItem id). Returns null when a healthy incumbent (another pod) holds
29815
+ * it or the WorkItem is already resolved — the caller must skip processing.
29816
+ */
29817
+ async claimTyped(ref) {
29818
+ let res;
29819
+ try {
29820
+ res = await this.opts.client.claimDispatch(this.opts.orgId, {
29821
+ dispatch_event_id: ref.dispatchEventId,
29822
+ source_type: ref.dispatchEventId ? void 0 : ref.sourceType,
29823
+ source_id: ref.dispatchEventId ? void 0 : ref.sourceId
29824
+ });
29825
+ } catch (err) {
29826
+ if (isEndpointMissing(err))
29827
+ throw new LedgerUnsupportedError("claim endpoint unavailable");
29828
+ throw err;
29829
+ }
29830
+ if (!res.claimed || !res.lane || !res.events?.length)
29831
+ return null;
29832
+ const workItem = res.events[0];
29833
+ const targetUri = `dsp:${workItem.id}`;
29834
+ const leaseUntilMs = Date.parse(res.lease_until ?? "");
29835
+ const lane = {
29836
+ laneKey: laneKeyForTarget(targetUri),
29837
+ lane: res.lane,
29838
+ targetUri,
29839
+ folded: /* @__PURE__ */ new Map([[workItem.source_id, workItem.id]]),
29840
+ typedDispatchEventId: workItem.id,
29841
+ ...Number.isNaN(leaseUntilMs) ? {} : { leaseUntilMs, leaseTtlMs: Math.max(leaseUntilMs - Date.now(), 6e4) }
29842
+ };
29843
+ this.lanes.set(lane.laneKey, lane);
29844
+ return lane;
29845
+ }
29846
+ /**
29847
+ * Remove the per-lane context file (and its CLI sidecar) when the lane
29848
+ * ends. A leftover file would make a later cross-context send to the same
29849
+ * target bind a dead lane token and misfire with STALE_LANE instead of
29850
+ * taking the plain non-ledger path.
29851
+ */
29852
+ removeLaneContext(lane) {
29853
+ const contextPath = this.laneContextPath(lane);
29854
+ for (const p of [contextPath, contextPath.replace(/\.json$/, ".reply-state.json")]) {
29855
+ try {
29856
+ fs.rmSync(p, { force: true });
29857
+ } catch {
29858
+ }
29859
+ }
29860
+ }
29861
+ };
29862
+
29863
+ // ../agent-core/dist/gateway-lane-flow.js
29864
+ async function dispatchLaneGroup(host, opts) {
29865
+ const ledger = host.laneLedger;
29866
+ const event = opts.events[opts.events.length - 1];
29867
+ let lane;
29868
+ try {
29869
+ lane = await ledger.ensureLane(opts.events);
29870
+ } catch (err) {
29871
+ if (!(err instanceof LedgerUnsupportedError))
29872
+ throw err;
29873
+ host.disableLedger("claim endpoint missing");
29874
+ await host.emitDispatchReceived(event);
29875
+ const dispatched2 = await host.runDispatch(event, opts.sessionKey, opts.body, opts.earlier, opts.captureText);
29876
+ if (!dispatched2)
29877
+ return "shutdown";
29878
+ for (const ev of opts.events) {
29879
+ host.opts.client.ackDispatch(host.opts.config.org_id, {
29880
+ source_type: ev.ackSourceType ?? "message",
29881
+ source_id: ev.ackSourceId ?? ev.messageId
29882
+ }).catch(() => {
29883
+ });
29884
+ }
29885
+ return "dispatched";
29886
+ }
29887
+ if (!lane) {
29888
+ for (const ev of opts.events) {
29889
+ host.dispatchedMessages.delete(ev.messageId);
29890
+ }
29891
+ return "foreign";
29892
+ }
29893
+ let dispatched = false;
29894
+ try {
29895
+ dispatched = await host.runDispatch(event, opts.sessionKey, opts.body, opts.earlier, opts.captureText);
29896
+ } catch (err) {
29897
+ await ledger.release(lane.laneKey).catch(() => {
29898
+ });
29899
+ throw err;
29900
+ }
29901
+ if (!dispatched) {
29902
+ return "shutdown";
29903
+ }
29904
+ const pendingInjections = host.opts.dispatchAdapter.hasPendingInjections?.(opts.sessionKey) ?? false;
29905
+ await ledger.completeIfIdle(lane.laneKey, pendingInjections || opts.hasMoreLocal());
29906
+ return "dispatched";
29907
+ }
29908
+ async function consumeTypedDispatch(host, ref, run, ack) {
29909
+ if (!host.laneLedger || host.ledgerDisabled) {
29910
+ if (await run(ref.dispatchEventId))
29911
+ ack(ref.dispatchEventId);
29912
+ return;
29913
+ }
29914
+ let lane;
29915
+ try {
29916
+ lane = await host.laneLedger.claimTyped(ref);
29917
+ } catch (err) {
29918
+ if (err instanceof LedgerUnsupportedError) {
29919
+ host.disableLedger("claim endpoint missing");
29920
+ if (await run(ref.dispatchEventId))
29921
+ ack(ref.dispatchEventId);
29922
+ return;
29923
+ }
29924
+ throw err;
29925
+ }
29926
+ if (!lane) {
29927
+ host.opts.log?.info(`typed dispatch ${ref.dispatchEventId ?? `${ref.sourceType}:${ref.sourceId}`} not claimable (held elsewhere or already resolved) \u2014 skipping`);
29928
+ return;
29929
+ }
29930
+ try {
29931
+ if (await run(lane.typedDispatchEventId))
29932
+ ack(lane.typedDispatchEventId);
29933
+ } finally {
29934
+ await host.laneLedger.completeIfIdle(lane.laneKey, false).catch(() => {
29935
+ });
29936
+ }
29937
+ }
29938
+ async function consumeMessageWorkItem(host, item) {
29939
+ if (host.shuttingDown)
29940
+ return;
29941
+ if (!host.tryClaimMessage(item.source_id))
29942
+ return;
29943
+ const ackItem = () => {
29944
+ host.opts.client.ackDispatchByID(host.opts.config.org_id, item.id).catch(() => {
29945
+ });
29946
+ };
29947
+ let msg = null;
29948
+ try {
29949
+ msg = await host.opts.client.getMessage(item.source_id);
29950
+ } catch (err) {
29951
+ const status = err?.status;
29952
+ if (status !== 404) {
29953
+ host.opts.log?.warn(`message fetch failed for ${item.source_id}, leaving pending: ${String(err)}`);
29954
+ host.dispatchedMessages.delete(item.source_id);
29955
+ return;
29956
+ }
29957
+ }
29958
+ if (!msg || msg.sender_id === host.opts.agentUserId) {
29959
+ host.dispatchedMessages.delete(item.source_id);
29960
+ ackItem();
29961
+ return;
29962
+ }
29963
+ const decision = await host.buildMessageDispatchDecision(item.chat_id, msg);
29964
+ if (decision.action === "retry") {
29965
+ host.dispatchedMessages.delete(item.source_id);
29966
+ return;
29967
+ }
29968
+ if (decision.action === "skip") {
29969
+ host.dispatchedMessages.delete(item.source_id);
29970
+ ackItem();
29971
+ return;
29972
+ }
29973
+ decision.event.dispatchEventId = item.id;
29974
+ const laneResolved = host.usesLaneLedger(decision.event);
29975
+ try {
29976
+ const dispatched = await host.handleInboundEvent(decision.event);
29977
+ if (dispatched) {
29978
+ if (!laneResolved)
29979
+ ackItem();
29980
+ } else {
29981
+ host.dispatchedMessages.delete(item.source_id);
29982
+ }
29983
+ } catch (err) {
29984
+ host.dispatchedMessages.delete(item.source_id);
29985
+ throw err;
29986
+ }
29987
+ }
29988
+
29369
29989
  // ../agent-core/dist/telemetry.js
29370
29990
  init_esm();
29371
29991
  var import_api_logs = __toESM(require_src(), 1);
@@ -29596,6 +30216,9 @@ function resolveStepTarget(event) {
29596
30216
  if (event.type === "external_trigger" || event.targetId.startsWith("xtr_")) {
29597
30217
  return { target_type: "external_trigger", target_id: event.targetId };
29598
30218
  }
30219
+ if (event.type === "channel_message" || event.targetId.startsWith("chv_")) {
30220
+ return { target_type: "channel_conversation", target_id: event.targetId };
30221
+ }
29599
30222
  if (event.type === "wiki_comment") {
29600
30223
  return { target_type: "wiki", target_id: event.targetId || void 0 };
29601
30224
  }
@@ -29639,6 +30262,9 @@ var ParallAgentGateway = class {
29639
30262
  opts;
29640
30263
  chatInfoMap = /* @__PURE__ */ new Map();
29641
30264
  dispatchedTasks = /* @__PURE__ */ new Set();
30265
+ // connection id → provider alias, for channel_message prompt labeling
30266
+ // (stable mapping; avoids one connection fetch per inbound message).
30267
+ channelConnectionProviders = /* @__PURE__ */ new Map();
29642
30268
  dispatchedMessages = /* @__PURE__ */ new Set();
29643
30269
  forkStates = /* @__PURE__ */ new Map();
29644
30270
  dispatchState = {
@@ -29661,6 +30287,14 @@ var ParallAgentGateway = class {
29661
30287
  inFlightDispatches = 0;
29662
30288
  drainResolvers = [];
29663
30289
  pendingRestartNotification = null;
30290
+ laneLedger;
30291
+ // Sticky fallback: flipped when the server predates the ledger (claim
30292
+ // endpoint 404) so every subsequent dispatch uses the legacy flow.
30293
+ ledgerDisabled = false;
30294
+ // Group key of the group currently being dispatched on main — lane-aware
30295
+ // (targetId + thread), unlike mainCurrentTargetId which stays chat-level
30296
+ // for fork routing decisions.
30297
+ mainCurrentGroupKey;
29664
30298
  DISPATCHED_MESSAGES_CAP = 5e3;
29665
30299
  // SHUTDOWN_DEADLINE_MS is read by waitForDrain via the configured value
29666
30300
  // below — kept as instance state so per-runtime configs can override it
@@ -29670,6 +30304,14 @@ var ParallAgentGateway = class {
29670
30304
  DISPATCH_DEADLINE_MS;
29671
30305
  constructor(opts) {
29672
30306
  this.opts = opts;
30307
+ if (opts.dispatchContextDir) {
30308
+ this.laneLedger = new LaneLedger({
30309
+ client: opts.client,
30310
+ orgId: opts.config.org_id,
30311
+ contextDir: opts.dispatchContextDir,
30312
+ log: opts.log
30313
+ });
30314
+ }
29673
30315
  this.SHUTDOWN_DEADLINE_MS = opts.shutdownDeadlineMs ?? 6e4;
29674
30316
  this.FORK_DEADLINE_MS = opts.forkDeadlineMs ?? 2 * 60 * 6e4;
29675
30317
  this.DISPATCH_DEADLINE_MS = opts.dispatchDeadlineMs ?? 20 * 6e4;
@@ -29739,14 +30381,18 @@ var ParallAgentGateway = class {
29739
30381
  if (data.status !== "todo" && data.status !== "in_progress")
29740
30382
  return;
29741
30383
  try {
29742
- const dispatched = await this.handleTaskAssignment(data, data.id);
29743
- if (dispatched) {
30384
+ await this.consumeTypedDispatch(data.dispatch_event_id ? { dispatchEventId: data.dispatch_event_id } : { sourceType: "task_activity", sourceId: data.id }, (dispatchEventId) => this.handleTaskAssignment(data, data.id, dispatchEventId), (dispatchEventId) => {
30385
+ if (dispatchEventId) {
30386
+ this.opts.client.ackDispatchByID(this.opts.config.org_id, dispatchEventId).catch(() => {
30387
+ });
30388
+ return;
30389
+ }
29744
30390
  this.opts.client.ackDispatch(this.opts.config.org_id, {
29745
30391
  source_type: "task_activity",
29746
30392
  source_id: data.id
29747
30393
  }).catch(() => {
29748
30394
  });
29749
- }
30395
+ });
29750
30396
  } catch (err) {
29751
30397
  this.opts.log?.error(`task dispatch failed for ${data.id}: ${String(err)}`);
29752
30398
  }
@@ -29756,11 +30402,10 @@ var ParallAgentGateway = class {
29756
30402
  if (!data.source_id || !data.task_id)
29757
30403
  return;
29758
30404
  try {
29759
- const dispatched = await this.handleTaskComment(data.source_id, data.task_id, data.actor_id, data.delivery_reason);
29760
- if (dispatched) {
30405
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.handleTaskComment(data.source_id, data.task_id ?? "", data.actor_id, data.delivery_reason), () => {
29761
30406
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
29762
30407
  });
29763
- }
30408
+ });
29764
30409
  } catch (err) {
29765
30410
  this.opts.log?.error(`task comment dispatch failed for ${data.source_id}: ${String(err)}`);
29766
30411
  }
@@ -29768,11 +30413,10 @@ var ParallAgentGateway = class {
29768
30413
  if (!data.source_id)
29769
30414
  return;
29770
30415
  try {
29771
- const dispatched = await this.handleWikiComment(data.source_id, data.actor_id, data.delivery_reason);
29772
- if (dispatched) {
30416
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.handleWikiComment(data.source_id, data.actor_id, data.delivery_reason), () => {
29773
30417
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
29774
30418
  });
29775
- }
30419
+ });
29776
30420
  } catch (err) {
29777
30421
  this.opts.log?.error(`wiki comment dispatch failed for ${data.source_id}: ${String(err)}`);
29778
30422
  }
@@ -29780,11 +30424,13 @@ var ParallAgentGateway = class {
29780
30424
  if (!data.task_id)
29781
30425
  return;
29782
30426
  try {
29783
- const dispatched = await this.handleTaskDispatch(data.task_id, data.source_id ?? data.task_id, { allowCreator: true });
29784
- if (dispatched) {
30427
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, (dispatchEventId) => this.handleTaskDispatch(data.task_id ?? "", data.source_id ?? data.task_id ?? "", {
30428
+ allowCreator: true,
30429
+ dispatchEventId
30430
+ }), () => {
29785
30431
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
29786
30432
  });
29787
- }
30433
+ });
29788
30434
  } catch (err) {
29789
30435
  this.opts.log?.error(`task update dispatch failed for ${data.task_id}: ${String(err)}`);
29790
30436
  }
@@ -29792,11 +30438,10 @@ var ParallAgentGateway = class {
29792
30438
  if (!data.source_id)
29793
30439
  return;
29794
30440
  try {
29795
- const dispatched = await this.fetchAndHandleScheduleFire(data.source_id, data.actor_id);
29796
- if (dispatched) {
30441
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.fetchAndHandleScheduleFire(data.source_id, data.actor_id), () => {
29797
30442
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
29798
30443
  });
29799
- }
30444
+ });
29800
30445
  } catch (err) {
29801
30446
  this.opts.log?.error(`schedule fire dispatch failed for ${data.source_id}: ${String(err)}`);
29802
30447
  }
@@ -29804,26 +30449,41 @@ var ParallAgentGateway = class {
29804
30449
  if (!data.source_id)
29805
30450
  return;
29806
30451
  try {
29807
- const dispatched = await this.fetchAndHandleExternalTriggerRun(data.source_id);
29808
- if (dispatched) {
30452
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.fetchAndHandleExternalTriggerRun(data.source_id), () => {
29809
30453
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
29810
30454
  });
29811
- }
30455
+ });
29812
30456
  } catch (err) {
29813
30457
  this.opts.log?.error(`external trigger dispatch failed for ${data.source_id}: ${String(err)}`);
29814
30458
  }
30459
+ } else if (data.event_type === "channel_message") {
30460
+ if (!data.source_id)
30461
+ return;
30462
+ try {
30463
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.fetchAndHandleChannelMessage(data.source_id), () => {
30464
+ this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
30465
+ });
30466
+ });
30467
+ } catch (err) {
30468
+ this.opts.log?.error(`channel message dispatch failed for ${data.source_id}: ${String(err)}`);
30469
+ }
29815
30470
  } else if (data.event_type === "approval_decided") {
29816
30471
  if (!data.source_id)
29817
30472
  return;
29818
30473
  try {
29819
- const dispatched = await this.fetchAndHandleApprovalDecided(data.source_id, data.actor_id, data.chat_id ?? null);
29820
- if (dispatched) {
30474
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.fetchAndHandleApprovalDecided(data.source_id, data.actor_id, data.chat_id ?? null), () => {
29821
30475
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
29822
30476
  });
29823
- }
30477
+ });
29824
30478
  } catch (err) {
29825
30479
  this.opts.log?.error(`approval decided dispatch failed for ${data.source_id}: ${String(err)}`);
29826
30480
  }
30481
+ } else if (data.event_type === "message" && this.laneLedger && data.source_id && data.chat_id) {
30482
+ try {
30483
+ await this.handleMessageRedrive(data);
30484
+ } catch (err) {
30485
+ this.opts.log?.error(`message re-drive failed for ${data.source_id}: ${String(err)}`);
30486
+ }
29827
30487
  } else if (data.event_type !== "message" && data.event_type !== "task_assign") {
29828
30488
  this.opts.log?.info(`dispatch.new with unhandled event_type=${String(data.event_type)} (id=${data.id}) \u2014 no-op`);
29829
30489
  }
@@ -29859,6 +30519,39 @@ var ParallAgentGateway = class {
29859
30519
  source_id: sourceId
29860
30520
  });
29861
30521
  }
30522
+ /** True when this event's lifecycle is owned by the dispatch lane ledger. */
30523
+ usesLaneLedger(event) {
30524
+ return this.laneLedger != null && !this.ledgerDisabled && this.laneLedger.handles(event);
30525
+ }
30526
+ disableLedger(reason) {
30527
+ if (this.ledgerDisabled)
30528
+ return;
30529
+ this.ledgerDisabled = true;
30530
+ this.opts.log?.warn(`dispatch ledger unavailable (${reason}) \u2014 falling back to legacy received/ack flow`);
30531
+ }
30532
+ /**
30533
+ * Buffer grouping key. Lane-ledger message events group by full lane
30534
+ * identity (chat + thread) so a channel lane and a thread lane in the same
30535
+ * chat dispatch as separate turns with separate claims; everything else
30536
+ * keeps the historical chat-level grouping.
30537
+ */
30538
+ dispatchGroupKey(event) {
30539
+ if (this.usesLaneLedger(event)) {
30540
+ return this.laneLedger.laneKeyFor(event);
30541
+ }
30542
+ return event.targetId;
30543
+ }
30544
+ // Lane-flow protocols live in gateway-lane-flow.ts; these thin delegates
30545
+ // keep call sites and tests on the class surface.
30546
+ laneFlowHost() {
30547
+ return this;
30548
+ }
30549
+ dispatchLaneGroup(opts) {
30550
+ return dispatchLaneGroup(this.laneFlowHost(), opts);
30551
+ }
30552
+ consumeTypedDispatch(ref, run, ack) {
30553
+ return consumeTypedDispatch(this.laneFlowHost(), ref, run, ack);
30554
+ }
29862
30555
  buildDispatchContext(event, sessionKey) {
29863
30556
  const binding = this.sessionBindings.get(sessionKey);
29864
30557
  return {
@@ -29875,6 +30568,7 @@ var ParallAgentGateway = class {
29875
30568
  noReply: event.noReply ?? false,
29876
30569
  contextFilePath: this.opts.contextFilePathForSession?.(sessionKey),
29877
30570
  stepIdFilePath: this.opts.stepIdFilePathForSession?.(sessionKey),
30571
+ contextDirPath: this.opts.dispatchContextDir,
29878
30572
  client: this.opts.client,
29879
30573
  log: this.opts.log
29880
30574
  };
@@ -29890,12 +30584,17 @@ var ParallAgentGateway = class {
29890
30584
  target_type: target.target_type,
29891
30585
  target_id: target.target_id,
29892
30586
  content: {
29893
- trigger_type: event.type === "task" ? "task_assign" : event.type === "task_comment" ? "task_comment" : event.type === "wiki_comment" ? "wiki_comment" : event.type === "schedule" ? "schedule_fire" : event.type === "external_trigger" ? "external_trigger" : event.type === "approval" ? "approval_decided" : "mention",
30587
+ trigger_type: event.type === "task" ? "task_assign" : event.type === "task_comment" ? "task_comment" : event.type === "wiki_comment" ? "wiki_comment" : event.type === "schedule" ? "schedule_fire" : event.type === "external_trigger" ? "external_trigger" : event.type === "channel_message" ? "channel_message" : event.type === "approval" ? "approval_decided" : "mention",
29894
30588
  trigger_ref: event.type === "task" ? { task_id: event.targetId } : event.type === "task_comment" ? { comment_id: event.messageId, task_id: event.targetId } : event.type === "wiki_comment" ? { comment_id: event.messageId, target_uri: event.replyTargetUri } : event.type === "schedule" ? { schedule_id: event.targetId, run_id: event.messageId } : event.type === "external_trigger" ? {
29895
30589
  trigger_id: event.targetId,
29896
30590
  run_id: event.messageId,
29897
30591
  connection_id: event.externalConnectionId,
29898
30592
  ingress_event_id: event.externalIngressEventId
30593
+ } : event.type === "channel_message" ? {
30594
+ conversation_id: event.targetId,
30595
+ channel_message_id: event.messageId,
30596
+ provider: event.channelProvider,
30597
+ external_conversation_id: event.channelExternalConversationId
29899
30598
  } : event.type === "approval" ? { approval_id: event.messageId } : { message_id: event.messageId },
29900
30599
  sender_id: event.senderId,
29901
30600
  sender_name: event.senderName,
@@ -29909,7 +30608,7 @@ var ParallAgentGateway = class {
29909
30608
  this.opts.log?.warn(`failed to create input step: ${String(err)}`);
29910
30609
  }
29911
30610
  }
29912
- async createRuntimeStep(sessionId, event, runtimeEvent, stepIdFilePath, contextFilePath) {
30611
+ async createRuntimeStep(sessionId, event, runtimeEvent, stepIdFilePath, contextFilePath, laneContextFilePath2) {
29913
30612
  const target = resolveStepTarget(event);
29914
30613
  try {
29915
30614
  switch (runtimeEvent.type) {
@@ -29955,6 +30654,9 @@ var ParallAgentGateway = class {
29955
30654
  } else if (stepIdFilePath) {
29956
30655
  this.writeStepIdFile(stepIdFilePath, step.id);
29957
30656
  }
30657
+ if (laneContextFilePath2) {
30658
+ this.updateContextFileStepId(laneContextFilePath2, step.id);
30659
+ }
29958
30660
  break;
29959
30661
  }
29960
30662
  case "tool_result":
@@ -29977,6 +30679,9 @@ var ParallAgentGateway = class {
29977
30679
  } else if (stepIdFilePath) {
29978
30680
  this.clearStepIdFile(stepIdFilePath);
29979
30681
  }
30682
+ if (laneContextFilePath2) {
30683
+ this.updateContextFileStepId(laneContextFilePath2, null);
30684
+ }
29980
30685
  break;
29981
30686
  case "error":
29982
30687
  await this.opts.client.createAgentStep(this.opts.config.org_id, this.opts.agentUserId, sessionId, {
@@ -29996,28 +30701,28 @@ var ParallAgentGateway = class {
29996
30701
  }
29997
30702
  writeContextFile(filePath, ctx) {
29998
30703
  try {
29999
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
30000
- fs.writeFileSync(filePath, JSON.stringify(ctx), "utf8");
30704
+ fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
30705
+ fs2.writeFileSync(filePath, JSON.stringify(ctx), "utf8");
30001
30706
  } catch (err) {
30002
30707
  this.opts.log?.warn(`failed to write context file ${filePath}: ${String(err)}`);
30003
30708
  }
30004
30709
  }
30005
30710
  updateContextFileStepId(filePath, stepId) {
30006
30711
  try {
30007
- const raw = fs.readFileSync(filePath, "utf8");
30712
+ const raw = fs2.readFileSync(filePath, "utf8");
30008
30713
  const ctx = JSON.parse(raw);
30009
30714
  ctx.step_id = stepId;
30010
- fs.writeFileSync(filePath, JSON.stringify(ctx), "utf8");
30715
+ fs2.writeFileSync(filePath, JSON.stringify(ctx), "utf8");
30011
30716
  } catch (err) {
30012
30717
  this.opts.log?.warn(`failed to update context file step_id ${filePath}: ${String(err)}`);
30013
30718
  }
30014
30719
  }
30015
30720
  updateContextFileSessionId(filePath, sessionId) {
30016
30721
  try {
30017
- const raw = fs.readFileSync(filePath, "utf8");
30722
+ const raw = fs2.readFileSync(filePath, "utf8");
30018
30723
  const ctx = JSON.parse(raw);
30019
30724
  ctx.session_id = sessionId;
30020
- fs.writeFileSync(filePath, JSON.stringify(ctx), "utf8");
30725
+ fs2.writeFileSync(filePath, JSON.stringify(ctx), "utf8");
30021
30726
  } catch (err) {
30022
30727
  this.opts.log?.warn(`failed to update context file session_id ${filePath}: ${String(err)}`);
30023
30728
  }
@@ -30025,8 +30730,8 @@ var ParallAgentGateway = class {
30025
30730
  /** @deprecated Use writeContextFile / updateContextFileStepId. */
30026
30731
  writeStepIdFile(filePath, stepId) {
30027
30732
  try {
30028
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
30029
- fs.writeFileSync(filePath, stepId, "utf8");
30733
+ fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
30734
+ fs2.writeFileSync(filePath, stepId, "utf8");
30030
30735
  } catch (err) {
30031
30736
  this.opts.log?.warn(`failed to write step id file ${filePath}: ${String(err)}`);
30032
30737
  }
@@ -30034,7 +30739,7 @@ var ParallAgentGateway = class {
30034
30739
  /** @deprecated Use writeContextFile / updateContextFileStepId. */
30035
30740
  clearStepIdFile(filePath) {
30036
30741
  try {
30037
- fs.writeFileSync(filePath, "", "utf8");
30742
+ fs2.writeFileSync(filePath, "", "utf8");
30038
30743
  } catch {
30039
30744
  }
30040
30745
  }
@@ -30043,7 +30748,7 @@ var ParallAgentGateway = class {
30043
30748
  await this.createInputStep(sessionId, event);
30044
30749
  }
30045
30750
  }
30046
- async bindRuntimeSession(sessionKey, runtimeEvent, contextFilePath) {
30751
+ async bindRuntimeSession(sessionKey, runtimeEvent, contextFilePath, laneContextFilePath2) {
30047
30752
  const runtimeLaneKey = runtimeEvent.runtimeLaneKey || sessionKey;
30048
30753
  const existing = this.sessionBindings.get(sessionKey);
30049
30754
  if (existing && existing.runtimeLaneKey === runtimeLaneKey && existing.runtimeSessionId === runtimeEvent.runtimeSessionId) {
@@ -30087,6 +30792,9 @@ var ParallAgentGateway = class {
30087
30792
  if (contextFilePath) {
30088
30793
  this.updateContextFileSessionId(contextFilePath, session.id);
30089
30794
  }
30795
+ if (laneContextFilePath2) {
30796
+ this.updateContextFileSessionId(laneContextFilePath2, session.id);
30797
+ }
30090
30798
  await this.opts.onSessionBinding?.(binding);
30091
30799
  return binding;
30092
30800
  }
@@ -30113,14 +30821,27 @@ var ParallAgentGateway = class {
30113
30821
  const dispatchContext = this.buildDispatchContext(event, sessionKey);
30114
30822
  const contextFilePath = dispatchContext.contextFilePath;
30115
30823
  const stepIdFilePath = dispatchContext.stepIdFilePath;
30824
+ const activeLane = this.ledgerDisabled ? void 0 : this.laneLedger?.getForEvent(event);
30825
+ const laneContextFilePath2 = activeLane ? this.laneLedger?.laneContextPath(activeLane) : void 0;
30826
+ const contextBody = {
30827
+ session_id: dispatchContext.sessionId ?? null,
30828
+ chat_id: dispatchContext.chatId ?? null,
30829
+ trigger_message_id: dispatchContext.triggerMessageId ?? null,
30830
+ no_reply: dispatchContext.noReply,
30831
+ step_id: null,
30832
+ dispatch_event_id: activeLane?.typedDispatchEventId ?? activeLane?.folded.get(event.messageId) ?? null,
30833
+ lane: activeLane?.lane ?? null,
30834
+ target_uri: activeLane?.targetUri ?? null,
30835
+ thread_root_id: activeLane?.threadRootId ?? null,
30836
+ // Typed binding hint for the CLI: which task this dispatch is about
30837
+ // (parall task update attaches the typed effect only on a match).
30838
+ task_id: event.type === "task" ? event.targetId : null
30839
+ };
30116
30840
  if (contextFilePath) {
30117
- this.writeContextFile(contextFilePath, {
30118
- session_id: dispatchContext.sessionId ?? null,
30119
- chat_id: dispatchContext.chatId ?? null,
30120
- trigger_message_id: dispatchContext.triggerMessageId ?? null,
30121
- no_reply: dispatchContext.noReply,
30122
- step_id: null
30123
- });
30841
+ this.writeContextFile(contextFilePath, contextBody);
30842
+ }
30843
+ if (laneContextFilePath2) {
30844
+ this.writeContextFile(laneContextFilePath2, contextBody);
30124
30845
  }
30125
30846
  this.inFlightDispatches++;
30126
30847
  const deadlineTimer = this.DISPATCH_DEADLINE_MS > 0 ? setTimeout(() => {
@@ -30146,7 +30867,15 @@ var ParallAgentGateway = class {
30146
30867
  context: dispatchContext
30147
30868
  })) {
30148
30869
  if (runtimeEvent.type === "runtime_session") {
30149
- binding = await this.bindRuntimeSession(sessionKey, runtimeEvent, contextFilePath);
30870
+ const priorAgentSessionId = binding?.agentSessionId;
30871
+ binding = await this.bindRuntimeSession(sessionKey, runtimeEvent, contextFilePath, laneContextFilePath2);
30872
+ if (event.targetType === "channel_conversation" && binding.agentSessionId !== priorAgentSessionId) {
30873
+ try {
30874
+ await this.opts.client.setChannelConversationSession(this.opts.config.org_id, event.targetId, binding.agentSessionId);
30875
+ } catch (err) {
30876
+ this.opts.log?.warn(`failed to record session mapping for channel conversation ${event.targetId}: ${String(err)}`);
30877
+ }
30878
+ }
30150
30879
  if (!inputStepsCreated) {
30151
30880
  if (earlierEvents.length > 0) {
30152
30881
  await this.createInputStepsForEarlierEvents(binding.agentSessionId, earlierEvents);
@@ -30171,6 +30900,9 @@ var ParallAgentGateway = class {
30171
30900
  await this.createInputStep(binding.agentSessionId, event);
30172
30901
  inputStepsCreated = true;
30173
30902
  }
30903
+ if (activeLane && !this.ledgerDisabled) {
30904
+ this.laneLedger?.maybeRenew(activeLane);
30905
+ }
30174
30906
  if (captureText && runtimeEvent.type === "text" && runtimeEvent.text) {
30175
30907
  captureText.push(runtimeEvent.text);
30176
30908
  }
@@ -30187,7 +30919,7 @@ var ParallAgentGateway = class {
30187
30919
  } else if (runtimeEvent.type === "tool_result" && pendingSendCallIds.delete(runtimeEvent.callId)) {
30188
30920
  recordMessageSend(sessionKey, !runtimeEvent.error);
30189
30921
  }
30190
- await this.createRuntimeStep(binding.agentSessionId, event, runtimeEvent, stepIdFilePath, contextFilePath);
30922
+ await this.createRuntimeStep(binding.agentSessionId, event, runtimeEvent, stepIdFilePath, contextFilePath, laneContextFilePath2);
30191
30923
  }
30192
30924
  if (!binding) {
30193
30925
  binding = this.sessionBindings.get(sessionKey);
@@ -30209,7 +30941,7 @@ var ParallAgentGateway = class {
30209
30941
  await this.createRuntimeStep(binding.agentSessionId, event, {
30210
30942
  type: "error",
30211
30943
  message: `Dispatch failed: ${String(err)}`
30212
- }, stepIdFilePath, contextFilePath);
30944
+ }, stepIdFilePath, contextFilePath, laneContextFilePath2);
30213
30945
  } catch (stepErr) {
30214
30946
  if (this.isSessionNotLiveError(stepErr))
30215
30947
  staleDetected = true;
@@ -30252,6 +30984,9 @@ var ParallAgentGateway = class {
30252
30984
  } else if (stepIdFilePath) {
30253
30985
  this.clearStepIdFile(stepIdFilePath);
30254
30986
  }
30987
+ if (laneContextFilePath2) {
30988
+ this.updateContextFileStepId(laneContextFilePath2, null);
30989
+ }
30255
30990
  this.inFlightDispatches--;
30256
30991
  if (this.inFlightDispatches === 0 && this.drainResolvers.length > 0) {
30257
30992
  const resolvers = this.drainResolvers.splice(0);
@@ -30309,13 +31044,39 @@ var ParallAgentGateway = class {
30309
31044
  item.resolve(false);
30310
31045
  break;
30311
31046
  }
30312
- const items = fork.queue.splice(0);
31047
+ let items;
31048
+ const head = fork.queue[0];
31049
+ if (head && this.usesLaneLedger(head.event)) {
31050
+ const headKey = this.dispatchGroupKey(head.event);
31051
+ const splitAt = fork.queue.findIndex((it) => this.dispatchGroupKey(it.event) !== headKey);
31052
+ items = splitAt === -1 ? fork.queue.splice(0) : fork.queue.splice(0, splitAt);
31053
+ } else {
31054
+ items = fork.queue.splice(0);
31055
+ }
30313
31056
  const events = items.map((item) => item.event);
30314
31057
  const last = events[events.length - 1];
30315
31058
  const earlier = events.slice(0, -1);
30316
31059
  try {
30317
31060
  const batchText = [];
30318
- const dispatched = await this.runDispatch(last, fork.fork.sessionKey, buildForkScopePrefix(last) + buildEventBody(last), earlier, batchText);
31061
+ let dispatched;
31062
+ if (this.usesLaneLedger(last)) {
31063
+ const outcome = await this.dispatchLaneGroup({
31064
+ events,
31065
+ sessionKey: fork.fork.sessionKey,
31066
+ body: buildForkScopePrefix(last) + buildEventBody(last),
31067
+ earlier,
31068
+ captureText: batchText,
31069
+ hasMoreLocal: () => fork.queue.length > 0
31070
+ });
31071
+ if (outcome === "foreign") {
31072
+ for (const item of items)
31073
+ item.resolve(false);
31074
+ break;
31075
+ }
31076
+ dispatched = outcome === "dispatched";
31077
+ } else {
31078
+ dispatched = await this.runDispatch(last, fork.fork.sessionKey, buildForkScopePrefix(last) + buildEventBody(last), earlier, batchText);
31079
+ }
30319
31080
  if (!dispatched) {
30320
31081
  for (const item of items) {
30321
31082
  item.resolve(false);
@@ -30418,9 +31179,9 @@ var ParallAgentGateway = class {
30418
31179
  this.opts.log?.info(`drainMainBuffer halted (shutting down) \u2014 ${this.dispatchState.mainBuffer.length} buffered, ${this.dispatchState.pendingForkResults.length} pending fork results left for catch-up`);
30419
31180
  break;
30420
31181
  }
30421
- const targetId = this.dispatchState.mainBuffer[0].targetId;
31182
+ const groupKey = this.dispatchGroupKey(this.dispatchState.mainBuffer[0]);
30422
31183
  const events = [];
30423
- while (this.dispatchState.mainBuffer[0]?.targetId === targetId) {
31184
+ while (this.dispatchState.mainBuffer[0] && this.dispatchGroupKey(this.dispatchState.mainBuffer[0]) === groupKey) {
30424
31185
  events.push(this.dispatchState.mainBuffer.shift());
30425
31186
  }
30426
31187
  const event = events[events.length - 1];
@@ -30429,7 +31190,36 @@ var ParallAgentGateway = class {
30429
31190
  const pendingFork = hasPendingInjections ? [] : this.dispatchState.pendingForkResults.splice(0);
30430
31191
  const forkPrefix = buildForkResultPrefix(pendingFork);
30431
31192
  this.dispatchState.mainCurrentTargetId = event.targetId;
31193
+ this.mainCurrentGroupKey = groupKey;
30432
31194
  this.dispatchState.mainPreDispatchBranchPoint = this.opts.dispatchAdapter.getBranchPoint?.(this.opts.runtimeKey);
31195
+ if (this.usesLaneLedger(event)) {
31196
+ let outcome;
31197
+ try {
31198
+ outcome = await this.dispatchLaneGroup({
31199
+ events,
31200
+ sessionKey: this.opts.runtimeKey,
31201
+ body: forkPrefix + buildEventBody(event),
31202
+ earlier,
31203
+ hasMoreLocal: () => this.dispatchState.mainBuffer.some((e) => this.dispatchGroupKey(e) === groupKey)
31204
+ });
31205
+ } catch (err) {
31206
+ this.opts.log?.error(`lane dispatch failed for ${event.messageId}: ${String(err)}`);
31207
+ this.dispatchState.pendingForkResults.unshift(...pendingFork);
31208
+ for (const ev of events)
31209
+ this.dispatchedMessages.delete(ev.messageId);
31210
+ continue;
31211
+ }
31212
+ if (outcome === "shutdown") {
31213
+ this.dispatchState.mainBuffer.unshift(...events);
31214
+ this.dispatchState.pendingForkResults.unshift(...pendingFork);
31215
+ break;
31216
+ }
31217
+ if (outcome === "foreign") {
31218
+ this.dispatchState.pendingForkResults.unshift(...pendingFork);
31219
+ continue;
31220
+ }
31221
+ continue;
31222
+ }
30433
31223
  try {
30434
31224
  await this.emitDispatchReceived(event);
30435
31225
  } catch (err) {
@@ -30458,6 +31248,7 @@ var ParallAgentGateway = class {
30458
31248
  this.draining = false;
30459
31249
  this.dispatchState.mainDispatching = false;
30460
31250
  this.dispatchState.mainCurrentTargetId = void 0;
31251
+ this.mainCurrentGroupKey = void 0;
30461
31252
  this.dispatchState.mainPreDispatchBranchPoint = void 0;
30462
31253
  if (!this.shuttingDown && this.dispatchState.mainBuffer.length > 0) {
30463
31254
  setTimeout(() => {
@@ -30477,13 +31268,38 @@ var ParallAgentGateway = class {
30477
31268
  const forkPrefix = buildForkResultPrefix(pendingFork);
30478
31269
  this.dispatchState.mainDispatching = true;
30479
31270
  this.dispatchState.mainCurrentTargetId = event.targetId;
31271
+ this.mainCurrentGroupKey = this.dispatchGroupKey(event);
30480
31272
  this.dispatchState.mainPreDispatchBranchPoint = this.opts.dispatchAdapter.getBranchPoint?.(this.opts.runtimeKey);
31273
+ if (this.usesLaneLedger(event)) {
31274
+ let outcome = "shutdown";
31275
+ try {
31276
+ try {
31277
+ outcome = await this.dispatchLaneGroup({
31278
+ events: [event],
31279
+ sessionKey: this.opts.runtimeKey,
31280
+ body: forkPrefix + buildEventBody(event),
31281
+ earlier: [],
31282
+ hasMoreLocal: () => this.dispatchState.mainBuffer.some((e) => this.dispatchGroupKey(e) === this.dispatchGroupKey(event))
31283
+ });
31284
+ } catch (err) {
31285
+ this.dispatchState.pendingForkResults.unshift(...pendingFork);
31286
+ throw err;
31287
+ }
31288
+ if (outcome !== "dispatched") {
31289
+ this.dispatchState.pendingForkResults.unshift(...pendingFork);
31290
+ }
31291
+ } finally {
31292
+ await this.drainMainBuffer();
31293
+ }
31294
+ return outcome === "dispatched";
31295
+ }
30481
31296
  try {
30482
31297
  await this.emitDispatchReceived(event);
30483
31298
  } catch (err) {
30484
31299
  this.opts.log?.warn?.(`mark-received failed, leaving unacked for retry: ${String(err)}`);
30485
31300
  this.dispatchState.mainDispatching = false;
30486
31301
  this.dispatchState.mainCurrentTargetId = void 0;
31302
+ this.mainCurrentGroupKey = void 0;
30487
31303
  this.dispatchState.mainPreDispatchBranchPoint = void 0;
30488
31304
  this.dispatchState.pendingForkResults.unshift(...pendingFork);
30489
31305
  return false;
@@ -30504,7 +31320,11 @@ var ParallAgentGateway = class {
30504
31320
  return false;
30505
31321
  }
30506
31322
  this.dispatchState.mainBuffer.push(event);
30507
- if (this.dispatchState.mainCurrentTargetId === event.targetId && await this.opts.dispatchAdapter.enqueueDuringDispatch?.(this.opts.runtimeKey, buildEventBody(event))) {
31323
+ if (this.usesLaneLedger(event)) {
31324
+ if (this.mainCurrentGroupKey === this.dispatchGroupKey(event) && await this.laneLedger?.steerLive(event) && await this.opts.dispatchAdapter.enqueueDuringDispatch?.(this.opts.runtimeKey, buildEventBody(event))) {
31325
+ this.opts.log?.info(`steer folded+injected for ${event.messageId} (will drain for bookkeeping)`);
31326
+ }
31327
+ } else if (this.dispatchState.mainCurrentTargetId === event.targetId && await this.opts.dispatchAdapter.enqueueDuringDispatch?.(this.opts.runtimeKey, buildEventBody(event))) {
30508
31328
  this.opts.log?.info(`steer injected for ${event.messageId} (will drain for bookkeeping)`);
30509
31329
  }
30510
31330
  if (!this.dispatchState.mainDispatching && !this.draining && this.dispatchState.mainBuffer.length > 0) {
@@ -30675,8 +31495,10 @@ var ParallAgentGateway = class {
30675
31495
  try {
30676
31496
  const dispatched = await this.handleInboundEvent(event);
30677
31497
  if (dispatched) {
30678
- this.opts.client.ackDispatch(this.opts.config.org_id, { source_type: "message", source_id: data.id }).catch(() => {
30679
- });
31498
+ if (!this.usesLaneLedger(event)) {
31499
+ this.opts.client.ackDispatch(this.opts.config.org_id, { source_type: "message", source_id: data.id }).catch(() => {
31500
+ });
31501
+ }
30680
31502
  } else {
30681
31503
  this.dispatchedMessages.delete(data.id);
30682
31504
  }
@@ -30685,7 +31507,21 @@ var ParallAgentGateway = class {
30685
31507
  this.dispatchedMessages.delete(data.id);
30686
31508
  }
30687
31509
  }
30688
- async handleTaskAssignment(task, ackSourceId) {
31510
+ // Ledger re-drive consumption: dispatch.new message hints re-enter the
31511
+ // shared WorkItem consumption path (same protocol as catch-up).
31512
+ async handleMessageRedrive(item) {
31513
+ if (!item.chat_id || !item.source_id)
31514
+ return;
31515
+ await this.consumeMessageWorkItem({
31516
+ id: item.id,
31517
+ source_id: item.source_id,
31518
+ chat_id: item.chat_id
31519
+ });
31520
+ }
31521
+ consumeMessageWorkItem(item) {
31522
+ return consumeMessageWorkItem(this.laneFlowHost(), item);
31523
+ }
31524
+ async handleTaskAssignment(task, ackSourceId, dispatchEventId) {
30689
31525
  if (this.shuttingDown)
30690
31526
  return false;
30691
31527
  const dedupeKey = `${task.id}:${task.updated_at}`;
@@ -30714,7 +31550,8 @@ var ParallAgentGateway = class {
30714
31550
  body: parts.join("\n"),
30715
31551
  sentAt: task.updated_at ?? task.created_at,
30716
31552
  ackSourceType: "task_activity",
30717
- ackSourceId
31553
+ ackSourceId,
31554
+ dispatchEventId
30718
31555
  };
30719
31556
  const dispatched = await this.handleInboundEvent(event);
30720
31557
  if (!dispatched) {
@@ -30738,7 +31575,7 @@ var ParallAgentGateway = class {
30738
31575
  this.opts.log?.info(`skipping stale task dispatch ${ackSourceId ?? taskId} \u2014 assigned to ${task.assignee_id}, creator ${task.creator_id}`);
30739
31576
  return true;
30740
31577
  }
30741
- return this.handleTaskAssignment(task, ackSourceId);
31578
+ return this.handleTaskAssignment(task, ackSourceId, opts.dispatchEventId);
30742
31579
  }
30743
31580
  async handleTaskComment(commentId, taskId, actorId, deliveryReason) {
30744
31581
  if (this.shuttingDown)
@@ -30949,6 +31786,75 @@ var ParallAgentGateway = class {
30949
31786
  return true;
30950
31787
  return this.handleExternalTriggerRun(run);
30951
31788
  }
31789
+ // fetchAndHandleChannelMessage resolves a channel_message dispatch to its
31790
+ // durable ChannelMessage + conversation and hands it to the inbound
31791
+ // pipeline. targetId = the ChannelConversation id, so per-conversation
31792
+ // multi-turn continuity rides the same per-target session mechanics as
31793
+ // chats. Design: docs/engineering-design/external-im-channel-design.md.
31794
+ async fetchAndHandleChannelMessage(messageId) {
31795
+ if (this.shuttingDown)
31796
+ return false;
31797
+ const claimKey = `channel_message:${messageId}`;
31798
+ if (!this.tryClaimMessage(claimKey))
31799
+ return false;
31800
+ let msg = null;
31801
+ let conv = null;
31802
+ try {
31803
+ msg = await this.opts.client.getChannelMessage(this.opts.config.org_id, messageId);
31804
+ conv = await this.opts.client.getChannelConversation(this.opts.config.org_id, msg.conversation_id);
31805
+ } catch (err) {
31806
+ const status = err?.status;
31807
+ if (status === 404) {
31808
+ this.opts.log?.warn(`channel message ${messageId} not accessible (404), acking stale dispatch`);
31809
+ return true;
31810
+ }
31811
+ this.dispatchedMessages.delete(claimKey);
31812
+ this.opts.log?.warn(`channel message fetch failed for ${messageId}, leaving pending: ${String(err)}`);
31813
+ return false;
31814
+ }
31815
+ if (!msg || !conv) {
31816
+ return true;
31817
+ }
31818
+ this.opts.log?.info(`channel message: ${msg.id} (conversation ${conv.id})`);
31819
+ let provider = this.channelConnectionProviders.get(conv.connection_id);
31820
+ if (!provider) {
31821
+ try {
31822
+ const connection = await this.opts.client.getChannelConnection(this.opts.config.org_id, conv.connection_id);
31823
+ provider = connection.provider;
31824
+ this.channelConnectionProviders.set(conv.connection_id, provider);
31825
+ } catch {
31826
+ provider = void 0;
31827
+ }
31828
+ }
31829
+ const event = {
31830
+ type: "channel_message",
31831
+ targetId: conv.id,
31832
+ targetName: conv.external_user_name || conv.external_conversation_id,
31833
+ targetType: "channel_conversation",
31834
+ senderId: msg.external_user_id || "external",
31835
+ senderName: msg.external_user_name || msg.external_user_id || "external user",
31836
+ messageId: msg.id,
31837
+ body: msg.text,
31838
+ sentAt: msg.received_at,
31839
+ channelProvider: provider,
31840
+ channelConversationType: conv.conversation_type || void 0,
31841
+ channelExternalConversationId: conv.external_conversation_id,
31842
+ channelExternalMessageId: msg.external_message_id,
31843
+ ackSourceType: "channel_message",
31844
+ ackSourceId: msg.id
31845
+ };
31846
+ let dispatched;
31847
+ try {
31848
+ dispatched = await this.handleInboundEvent(event);
31849
+ } catch (err) {
31850
+ this.dispatchedMessages.delete(claimKey);
31851
+ throw err;
31852
+ }
31853
+ if (!dispatched) {
31854
+ this.dispatchedMessages.delete(claimKey);
31855
+ }
31856
+ return dispatched;
31857
+ }
30952
31858
  async handleExternalTriggerRun(run) {
30953
31859
  if (this.shuttingDown)
30954
31860
  return false;
@@ -31077,72 +31983,46 @@ var ParallAgentGateway = class {
31077
31983
  }
31078
31984
  processed++;
31079
31985
  try {
31080
- let dispatched = false;
31986
+ const ackItem = () => {
31987
+ this.opts.client.ackDispatchByID(this.opts.config.org_id, item.id).catch(() => {
31988
+ });
31989
+ };
31081
31990
  if (item.event_type === "task_assign" && item.task_id) {
31082
31991
  try {
31083
- dispatched = await this.handleTaskDispatch(item.task_id, item.source_id ?? item.task_id);
31992
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, (dispatchEventId) => this.handleTaskDispatch(item.task_id ?? "", item.source_id ?? item.task_id ?? "", {
31993
+ dispatchEventId
31994
+ }), ackItem);
31084
31995
  } catch (err) {
31085
31996
  this.opts.log?.warn(`catch-up task fetch failed for ${item.task_id}, leaving pending: ${String(err)}`);
31086
31997
  continue;
31087
31998
  }
31088
31999
  } else if (item.event_type === "task_update" && item.task_id) {
31089
32000
  try {
31090
- dispatched = await this.handleTaskDispatch(item.task_id, item.source_id ?? item.task_id, { allowCreator: true });
32001
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, (dispatchEventId) => this.handleTaskDispatch(item.task_id ?? "", item.source_id ?? item.task_id ?? "", {
32002
+ allowCreator: true,
32003
+ dispatchEventId
32004
+ }), ackItem);
31091
32005
  } catch (err) {
31092
32006
  this.opts.log?.warn(`catch-up task fetch failed for ${item.task_id}, leaving pending: ${String(err)}`);
31093
32007
  continue;
31094
32008
  }
31095
32009
  } else if (item.event_type === "task_comment" && item.source_id && item.task_id) {
31096
- dispatched = await this.handleTaskComment(item.source_id, item.task_id, item.actor_id, item.delivery_reason);
32010
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.handleTaskComment(item.source_id, item.task_id ?? "", item.actor_id, item.delivery_reason), ackItem);
31097
32011
  } else if (item.event_type === "wiki_comment" && item.source_id) {
31098
- dispatched = await this.handleWikiComment(item.source_id, item.actor_id, item.delivery_reason);
32012
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.handleWikiComment(item.source_id, item.actor_id, item.delivery_reason), ackItem);
31099
32013
  } else if (item.event_type === "schedule.fire" && item.source_id) {
31100
- dispatched = await this.fetchAndHandleScheduleFire(item.source_id, item.actor_id);
32014
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.fetchAndHandleScheduleFire(item.source_id, item.actor_id), ackItem);
31101
32015
  } else if (item.event_type === "external_trigger" && item.source_id) {
31102
- dispatched = await this.fetchAndHandleExternalTriggerRun(item.source_id);
32016
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.fetchAndHandleExternalTriggerRun(item.source_id), ackItem);
32017
+ } else if (item.event_type === "channel_message" && item.source_id) {
32018
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.fetchAndHandleChannelMessage(item.source_id), ackItem);
31103
32019
  } else if (item.event_type === "approval_decided" && item.source_id) {
31104
- dispatched = await this.fetchAndHandleApprovalDecided(item.source_id, item.actor_id, item.chat_id ?? null);
32020
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.fetchAndHandleApprovalDecided(item.source_id, item.actor_id, item.chat_id ?? null), ackItem);
31105
32021
  } else if (item.event_type === "message" && item.source_id && item.chat_id) {
31106
- if (!this.tryClaimMessage(item.source_id))
31107
- continue;
31108
- let msg = null;
31109
- let msgFetchFailed = false;
31110
- try {
31111
- msg = await this.opts.client.getMessage(item.source_id);
31112
- } catch (err) {
31113
- const status = err?.status;
31114
- if (status === 404) {
31115
- msg = null;
31116
- } else {
31117
- msgFetchFailed = true;
31118
- this.opts.log?.warn(`catch-up message fetch failed for ${item.source_id}, leaving pending: ${String(err)}`);
31119
- }
31120
- }
31121
- if (msgFetchFailed) {
31122
- this.dispatchedMessages.delete(item.source_id);
31123
- continue;
31124
- }
31125
- if (!msg || msg.sender_id === this.opts.agentUserId) {
31126
- this.dispatchedMessages.delete(item.source_id);
31127
- this.opts.client.ackDispatchByID(this.opts.config.org_id, item.id).catch(() => {
31128
- });
31129
- continue;
31130
- }
31131
- const decision = await this.buildMessageDispatchDecision(item.chat_id, msg);
31132
- if (decision.action === "retry") {
31133
- this.dispatchedMessages.delete(item.source_id);
31134
- continue;
31135
- }
31136
- if (decision.action === "skip") {
31137
- this.dispatchedMessages.delete(item.source_id);
31138
- this.opts.client.ackDispatchByID(this.opts.config.org_id, item.id).catch(() => {
31139
- });
31140
- continue;
31141
- }
31142
- dispatched = await this.handleInboundEvent(decision.event);
31143
- }
31144
- if (dispatched) {
31145
- this.opts.client.ackDispatchByID(this.opts.config.org_id, item.id).catch(() => {
32022
+ await this.consumeMessageWorkItem({
32023
+ id: item.id,
32024
+ source_id: item.source_id,
32025
+ chat_id: item.chat_id
31146
32026
  });
31147
32027
  }
31148
32028
  } catch (err) {
@@ -31208,6 +32088,14 @@ ${fullSummary}` : fullSummary;
31208
32088
  this.abortFork(targetId, "ws reconnect");
31209
32089
  }
31210
32090
  }
32091
+ if (this.laneLedger && this.inFlightDispatches === 0 && this.laneLedger.activeCount > 0) {
32092
+ log?.info(`releasing ${this.laneLedger.activeCount} stale lane(s) on reconnect`);
32093
+ await this.laneLedger.releaseAll();
32094
+ }
32095
+ if (this.laneLedger && this.ledgerDisabled) {
32096
+ log?.info("re-probing dispatch ledger after reconnect (was disabled)");
32097
+ this.ledgerDisabled = false;
32098
+ }
31211
32099
  const intervalSec = data.heartbeat_interval > 0 ? data.heartbeat_interval : 30;
31212
32100
  try {
31213
32101
  const count = await fetchAllChats(client, config.org_id, this.chatInfoMap);
@@ -31278,6 +32166,10 @@ ${fullSummary}` : fullSummary;
31278
32166
  }
31279
32167
  if (this.heartbeatTimer)
31280
32168
  clearInterval(this.heartbeatTimer);
32169
+ if (this.laneLedger && this.laneLedger.activeCount > 0) {
32170
+ this.opts.log?.info(`releasing ${this.laneLedger.activeCount} lane(s) on shutdown`);
32171
+ await this.laneLedger.releaseAll();
32172
+ }
31281
32173
  await this.opts.onBeforeDisconnect?.();
31282
32174
  this.opts.ws.disconnect();
31283
32175
  this.opts.log?.info(`disconnected`);
@@ -31288,8 +32180,8 @@ ${fullSummary}` : fullSummary;
31288
32180
  import { execSync } from "node:child_process";
31289
32181
  import { constants } from "node:fs";
31290
32182
  import * as fsSync from "node:fs";
31291
- import * as fs2 from "node:fs/promises";
31292
- import * as path2 from "node:path";
32183
+ import * as fs3 from "node:fs/promises";
32184
+ import * as path3 from "node:path";
31293
32185
  var DEFAULT_MAX_TOTAL_IMAGE_BYTES = 20 * 1024 * 1024;
31294
32186
  var DEFAULT_ATTACHMENT_CACHE_MAX_BYTES = 512 * 1024 * 1024;
31295
32187
  var DEFAULT_ATTACHMENT_DOWNLOAD_TIMEOUT_MS = 3e4;
@@ -31319,11 +32211,11 @@ async function prepareLocalImageAttachments(event, context2, opts) {
31319
32211
  };
31320
32212
  }
31321
32213
  const rootDir = await ensureAttachmentRootDir(opts.workspaceDir);
31322
- const messageDir = path2.join(rootDir, sanitizePathSegment(event.messageId));
32214
+ const messageDir = path3.join(rootDir, sanitizePathSegment(event.messageId));
31323
32215
  await ensurePathIsNotSymlink(messageDir);
31324
- await fs2.mkdir(messageDir, { recursive: true });
32216
+ await fs3.mkdir(messageDir, { recursive: true });
31325
32217
  await ensurePathIsNotSymlink(messageDir);
31326
- const activeMessageDir = path2.resolve(messageDir);
32218
+ const activeMessageDir = path3.resolve(messageDir);
31327
32219
  activeAttachmentDirs.add(activeMessageDir);
31328
32220
  const maintenanceCooldownMs = opts.maintenanceCooldownMs ?? DEFAULT_MAINTENANCE_COOLDOWN_MS;
31329
32221
  const maintenancePromise = scheduleAttachmentMaintenance(rootDir, {
@@ -31340,7 +32232,7 @@ async function prepareLocalImageAttachments(event, context2, opts) {
31340
32232
  const notes = [];
31341
32233
  let downloadedBytes = 0;
31342
32234
  for (const att of imageAttachments) {
31343
- const localPath = path2.join(messageDir, localFileName(att.id, att.fileName, att.mimeType));
32235
+ const localPath = path3.join(messageDir, localFileName(att.id, att.fileName, att.mimeType));
31344
32236
  const downloadTimeoutMs = opts.downloadTimeoutMs ?? DEFAULT_ATTACHMENT_DOWNLOAD_TIMEOUT_MS;
31345
32237
  const fetchFresh = async () => {
31346
32238
  const fileInfo = await withTimeout(context2.client.getFileUrl(att.id), downloadTimeoutMs, `file URL lookup timed out after ${downloadTimeoutMs}ms`);
@@ -31397,7 +32289,7 @@ async function appendPreparedLocalAttachmentRefs(body, event, context2, opts) {
31397
32289
  return { body: appendLocalAttachmentRefs(body, attachments), attachments };
31398
32290
  }
31399
32291
  function pinLocalAttachmentPaths(images) {
31400
- const dirs = new Set(images.map((image) => path2.resolve(path2.dirname(image.localPath))));
32292
+ const dirs = new Set(images.map((image) => path3.resolve(path3.dirname(image.localPath))));
31401
32293
  for (const dir of dirs) {
31402
32294
  activeAttachmentDirs.add(dir);
31403
32295
  }
@@ -31412,7 +32304,7 @@ function pinLocalAttachmentPaths(images) {
31412
32304
  };
31413
32305
  }
31414
32306
  function attachmentRootDir(workspaceDir) {
31415
- return path2.join(path2.resolve(workspaceDir), ".parall", "attachments");
32307
+ return path3.join(path3.resolve(workspaceDir), ".parall", "attachments");
31416
32308
  }
31417
32309
  function ensureLocalAttachmentGitExclude(workingDirectory) {
31418
32310
  try {
@@ -31421,8 +32313,8 @@ function ensureLocalAttachmentGitExclude(workingDirectory) {
31421
32313
  encoding: "utf8",
31422
32314
  stdio: ["ignore", "pipe", "ignore"]
31423
32315
  }).trim();
31424
- const excludePath = path2.isAbsolute(rel) ? rel : path2.join(workingDirectory, rel);
31425
- fsSync.mkdirSync(path2.dirname(excludePath), { recursive: true });
32316
+ const excludePath = path3.isAbsolute(rel) ? rel : path3.join(workingDirectory, rel);
32317
+ fsSync.mkdirSync(path3.dirname(excludePath), { recursive: true });
31426
32318
  const existing = fsSync.existsSync(excludePath) ? fsSync.readFileSync(excludePath, "utf8") : "";
31427
32319
  if (existing.split(/\r?\n/).some((line) => line.trim() === ".parall/"))
31428
32320
  return;
@@ -31458,18 +32350,18 @@ function scheduleAttachmentMaintenance(rootDir, opts) {
31458
32350
  return run;
31459
32351
  }
31460
32352
  async function ensureAttachmentRootDir(workspaceDir) {
31461
- const workspaceRoot = path2.resolve(workspaceDir);
31462
- const parallDir = path2.join(workspaceRoot, ".parall");
32353
+ const workspaceRoot = path3.resolve(workspaceDir);
32354
+ const parallDir = path3.join(workspaceRoot, ".parall");
31463
32355
  const rootDir = attachmentRootDir(workspaceRoot);
31464
- await fs2.mkdir(workspaceRoot, { recursive: true });
32356
+ await fs3.mkdir(workspaceRoot, { recursive: true });
31465
32357
  await ensurePathIsNotSymlink(parallDir);
31466
- await fs2.mkdir(parallDir, { recursive: true, mode: 448 });
32358
+ await fs3.mkdir(parallDir, { recursive: true, mode: 448 });
31467
32359
  await ensurePathIsNotSymlink(parallDir);
31468
32360
  await ensurePathIsNotSymlink(rootDir);
31469
- await fs2.mkdir(rootDir, { recursive: true, mode: 448 });
32361
+ await fs3.mkdir(rootDir, { recursive: true, mode: 448 });
31470
32362
  await ensurePathIsNotSymlink(rootDir);
31471
- const realWorkspace = await fs2.realpath(workspaceRoot);
31472
- const realRoot = await fs2.realpath(rootDir);
32363
+ const realWorkspace = await fs3.realpath(workspaceRoot);
32364
+ const realRoot = await fs3.realpath(rootDir);
31473
32365
  if (!isPathInside(realRoot, realWorkspace)) {
31474
32366
  throw new Error(`attachment root escapes workspace: ${rootDir}`);
31475
32367
  }
@@ -31477,7 +32369,7 @@ async function ensureAttachmentRootDir(workspaceDir) {
31477
32369
  }
31478
32370
  async function ensurePathIsNotSymlink(filePath) {
31479
32371
  try {
31480
- const stat = await fs2.lstat(filePath);
32372
+ const stat = await fs3.lstat(filePath);
31481
32373
  if (stat.isSymbolicLink()) {
31482
32374
  throw new Error(`refusing to use symlinked attachment path ${filePath}`);
31483
32375
  }
@@ -31488,8 +32380,8 @@ async function ensurePathIsNotSymlink(filePath) {
31488
32380
  }
31489
32381
  }
31490
32382
  function isPathInside(childPath, parentPath) {
31491
- const rel = path2.relative(parentPath, childPath);
31492
- return rel === "" || !!rel && !rel.startsWith("..") && !path2.isAbsolute(rel);
32383
+ const rel = path3.relative(parentPath, childPath);
32384
+ return rel === "" || !!rel && !rel.startsWith("..") && !path3.isAbsolute(rel);
31493
32385
  }
31494
32386
  async function existingUsableFile(filePath, expectedSize, rootDir) {
31495
32387
  try {
@@ -31500,30 +32392,30 @@ async function existingUsableFile(filePath, expectedSize, rootDir) {
31500
32392
  }
31501
32393
  }
31502
32394
  async function localFileStatInsideRoot(filePath, rootDir) {
31503
- const stat = await fs2.lstat(filePath);
32395
+ const stat = await fs3.lstat(filePath);
31504
32396
  if (stat.isSymbolicLink()) {
31505
32397
  throw new Error(`refusing to use symlinked attachment file ${filePath}`);
31506
32398
  }
31507
32399
  if (!stat.isFile()) {
31508
32400
  throw new Error(`attachment path is not a file ${filePath}`);
31509
32401
  }
31510
- const realRoot = await fs2.realpath(rootDir);
31511
- const realFile = await fs2.realpath(filePath);
32402
+ const realRoot = await fs3.realpath(rootDir);
32403
+ const realFile = await fs3.realpath(filePath);
31512
32404
  if (!isPathInside(realFile, realRoot)) {
31513
32405
  throw new Error(`attachment file escapes workspace: ${filePath}`);
31514
32406
  }
31515
32407
  return stat;
31516
32408
  }
31517
32409
  async function localDirectoryStatInsideRoot(dirPath, rootDir) {
31518
- const stat = await fs2.lstat(dirPath);
32410
+ const stat = await fs3.lstat(dirPath);
31519
32411
  if (stat.isSymbolicLink()) {
31520
32412
  throw new Error(`refusing to use symlinked attachment directory ${dirPath}`);
31521
32413
  }
31522
32414
  if (!stat.isDirectory()) {
31523
32415
  throw new Error(`attachment path is not a directory ${dirPath}`);
31524
32416
  }
31525
- const realRoot = await fs2.realpath(rootDir);
31526
- const realDir = await fs2.realpath(dirPath);
32417
+ const realRoot = await fs3.realpath(rootDir);
32418
+ const realDir = await fs3.realpath(dirPath);
31527
32419
  if (!isPathInside(realDir, realRoot)) {
31528
32420
  throw new Error(`attachment directory escapes workspace: ${dirPath}`);
31529
32421
  }
@@ -31531,7 +32423,7 @@ async function localDirectoryStatInsideRoot(dirPath, rootDir) {
31531
32423
  }
31532
32424
  async function openLocalFileInsideRoot(filePath, rootDir) {
31533
32425
  const checkedStat = await localFileStatInsideRoot(filePath, rootDir);
31534
- const file = await fs2.open(filePath, constants.O_RDONLY | constants.O_NOFOLLOW);
32426
+ const file = await fs3.open(filePath, constants.O_RDONLY | constants.O_NOFOLLOW);
31535
32427
  let keepOpen = false;
31536
32428
  try {
31537
32429
  const openedStat = await file.stat();
@@ -31547,8 +32439,8 @@ async function openLocalFileInsideRoot(filePath, rootDir) {
31547
32439
  }
31548
32440
  }
31549
32441
  async function openLocalTempFileInsideRoot(filePath, rootDir) {
31550
- await localDirectoryStatInsideRoot(path2.dirname(filePath), rootDir);
31551
- const file = await fs2.open(filePath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
32442
+ await localDirectoryStatInsideRoot(path3.dirname(filePath), rootDir);
32443
+ const file = await fs3.open(filePath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
31552
32444
  let keepOpen = false;
31553
32445
  try {
31554
32446
  const checkedStat = await localFileStatInsideRoot(filePath, rootDir);
@@ -31573,7 +32465,7 @@ async function assertLocalFileIdentity(filePath, rootDir, expected) {
31573
32465
  async function removeLocalFileIfInside(filePath, rootDir) {
31574
32466
  try {
31575
32467
  await localFileStatInsideRoot(filePath, rootDir);
31576
- await fs2.rm(filePath, { force: true });
32468
+ await fs3.rm(filePath, { force: true });
31577
32469
  } catch {
31578
32470
  }
31579
32471
  }
@@ -31586,7 +32478,7 @@ function sameFile(a, b) {
31586
32478
  async function cleanupOldAttachmentFiles(rootDir, ttlMs, log, preserveDirs) {
31587
32479
  let entries;
31588
32480
  try {
31589
- entries = await fs2.readdir(rootDir, { withFileTypes: true });
32481
+ entries = await fs3.readdir(rootDir, { withFileTypes: true });
31590
32482
  } catch {
31591
32483
  return;
31592
32484
  }
@@ -31594,15 +32486,15 @@ async function cleanupOldAttachmentFiles(rootDir, ttlMs, log, preserveDirs) {
31594
32486
  await Promise.all(entries.map(async (entry) => {
31595
32487
  if (!entry.isDirectory())
31596
32488
  return;
31597
- const fullPath = path2.join(rootDir, entry.name);
32489
+ const fullPath = path3.join(rootDir, entry.name);
31598
32490
  try {
31599
- if (preserveDirs?.has(path2.resolve(fullPath)))
32491
+ if (preserveDirs?.has(path3.resolve(fullPath)))
31600
32492
  return;
31601
- const stat = await fs2.lstat(fullPath);
32493
+ const stat = await fs3.lstat(fullPath);
31602
32494
  if (!stat.isDirectory())
31603
32495
  return;
31604
32496
  if (stat.mtimeMs < cutoff) {
31605
- await fs2.rm(fullPath, { recursive: true, force: true });
32497
+ await fs3.rm(fullPath, { recursive: true, force: true });
31606
32498
  }
31607
32499
  } catch (err) {
31608
32500
  log?.warn?.(`agent-core: failed to clean attachment temp dir ${fullPath}: ${String(err)}`);
@@ -31614,7 +32506,7 @@ async function pruneAttachmentCache(rootDir, maxBytes, log, preserveDirs) {
31614
32506
  return;
31615
32507
  let entries;
31616
32508
  try {
31617
- entries = await fs2.readdir(rootDir, { withFileTypes: true });
32509
+ entries = await fs3.readdir(rootDir, { withFileTypes: true });
31618
32510
  } catch {
31619
32511
  return;
31620
32512
  }
@@ -31623,9 +32515,9 @@ async function pruneAttachmentCache(rootDir, maxBytes, log, preserveDirs) {
31623
32515
  for (const entry of entries) {
31624
32516
  if (!entry.isDirectory())
31625
32517
  continue;
31626
- const fullPath = path2.join(rootDir, entry.name);
32518
+ const fullPath = path3.join(rootDir, entry.name);
31627
32519
  try {
31628
- const stat = await fs2.lstat(fullPath);
32520
+ const stat = await fs3.lstat(fullPath);
31629
32521
  if (!stat.isDirectory())
31630
32522
  continue;
31631
32523
  const size = await directorySize(fullPath);
@@ -31641,10 +32533,10 @@ async function pruneAttachmentCache(rootDir, maxBytes, log, preserveDirs) {
31641
32533
  for (const dir of dirs) {
31642
32534
  if (total <= maxBytes)
31643
32535
  break;
31644
- if (preserveDirs?.has(path2.resolve(dir.path)))
32536
+ if (preserveDirs?.has(path3.resolve(dir.path)))
31645
32537
  continue;
31646
32538
  try {
31647
- await fs2.rm(dir.path, { recursive: true, force: true });
32539
+ await fs3.rm(dir.path, { recursive: true, force: true });
31648
32540
  total -= dir.size;
31649
32541
  } catch (err) {
31650
32542
  log?.warn?.(`agent-core: failed to prune attachment cache dir ${dir.path}: ${String(err)}`);
@@ -31653,12 +32545,12 @@ async function pruneAttachmentCache(rootDir, maxBytes, log, preserveDirs) {
31653
32545
  }
31654
32546
  async function directorySize(dirPath) {
31655
32547
  let total = 0;
31656
- const entries = await fs2.readdir(dirPath, { withFileTypes: true });
32548
+ const entries = await fs3.readdir(dirPath, { withFileTypes: true });
31657
32549
  for (const entry of entries) {
31658
- const fullPath = path2.join(dirPath, entry.name);
32550
+ const fullPath = path3.join(dirPath, entry.name);
31659
32551
  let stat;
31660
32552
  try {
31661
- stat = await fs2.lstat(fullPath);
32553
+ stat = await fs3.lstat(fullPath);
31662
32554
  } catch {
31663
32555
  continue;
31664
32556
  }
@@ -31673,10 +32565,10 @@ async function directorySize(dirPath) {
31673
32565
  return total;
31674
32566
  }
31675
32567
  function activeDirsForRoot(rootDir) {
31676
- const root = path2.resolve(rootDir);
32568
+ const root = path3.resolve(rootDir);
31677
32569
  const dirs = /* @__PURE__ */ new Set();
31678
32570
  for (const dir of activeAttachmentDirs) {
31679
- if (dir === root || dir.startsWith(`${root}${path2.sep}`)) {
32571
+ if (dir === root || dir.startsWith(`${root}${path3.sep}`)) {
31680
32572
  dirs.add(dir);
31681
32573
  }
31682
32574
  }
@@ -31773,9 +32665,9 @@ async function writeResponseToFileWithLimit(res, filePath, maxBytes, rootDir) {
31773
32665
  }
31774
32666
  writtenStat = await file.stat();
31775
32667
  await closeFile();
31776
- await localDirectoryStatInsideRoot(path2.dirname(filePath), rootDir);
32668
+ await localDirectoryStatInsideRoot(path3.dirname(filePath), rootDir);
31777
32669
  await assertLocalFileIdentity(tmpPath, rootDir, writtenStat);
31778
- await fs2.rename(tmpPath, filePath);
32670
+ await fs3.rename(tmpPath, filePath);
31779
32671
  completed = true;
31780
32672
  return written;
31781
32673
  } finally {
@@ -31791,9 +32683,9 @@ async function writeResponseToFileWithLimit(res, filePath, maxBytes, rootDir) {
31791
32683
  }
31792
32684
  }
31793
32685
  function localFileName(attachmentId, fileName, mimeType) {
31794
- const safeName = sanitizePathSegment(path2.basename(fileName || attachmentId));
31795
- const ext = path2.extname(safeName) || extensionForMime(mimeType);
31796
- const stem = path2.basename(safeName, path2.extname(safeName)) || attachmentId;
32686
+ const safeName = sanitizePathSegment(path3.basename(fileName || attachmentId));
32687
+ const ext = path3.extname(safeName) || extensionForMime(mimeType);
32688
+ const stem = path3.basename(safeName, path3.extname(safeName)) || attachmentId;
31797
32689
  return `${sanitizePathSegment(attachmentId)}-${stem}${ext}`;
31798
32690
  }
31799
32691
  function extensionForMime(mimeType) {
@@ -31859,7 +32751,7 @@ function parseContentLength(value) {
31859
32751
  // dist/gateway.js
31860
32752
  import * as crypto2 from "node:crypto";
31861
32753
  import * as os2 from "node:os";
31862
- import * as path6 from "node:path";
32754
+ import * as path7 from "node:path";
31863
32755
 
31864
32756
  // dist/runtime.js
31865
32757
  var runtime = null;
@@ -31917,15 +32809,15 @@ function buildOrchestratorSessionKey(accountId) {
31917
32809
  }
31918
32810
 
31919
32811
  // dist/config-manager.js
31920
- import * as fs3 from "node:fs";
31921
- import * as path3 from "node:path";
32812
+ import * as fs4 from "node:fs";
32813
+ import * as path4 from "node:path";
31922
32814
  var CACHE_FILENAME = "parall-platform-config.json";
31923
32815
  function cachePath(stateDir) {
31924
- return path3.join(stateDir, CACHE_FILENAME);
32816
+ return path4.join(stateDir, CACHE_FILENAME);
31925
32817
  }
31926
32818
  function loadCachedConfig(stateDir) {
31927
32819
  try {
31928
- const raw = fs3.readFileSync(cachePath(stateDir), "utf-8");
32820
+ const raw = fs4.readFileSync(cachePath(stateDir), "utf-8");
31929
32821
  return JSON.parse(raw);
31930
32822
  } catch {
31931
32823
  return null;
@@ -31939,14 +32831,14 @@ function saveCachedConfig(stateDir, config) {
31939
32831
  };
31940
32832
  const filePath = cachePath(stateDir);
31941
32833
  const tmpPath = `${filePath}.tmp`;
31942
- fs3.mkdirSync(path3.dirname(filePath), { recursive: true });
31943
- fs3.writeFileSync(tmpPath, JSON.stringify(cached, null, 2), "utf-8");
31944
- fs3.renameSync(tmpPath, filePath);
32834
+ fs4.mkdirSync(path4.dirname(filePath), { recursive: true });
32835
+ fs4.writeFileSync(tmpPath, JSON.stringify(cached, null, 2), "utf-8");
32836
+ fs4.renameSync(tmpPath, filePath);
31945
32837
  }
31946
32838
  function applyToOpenClawConfig(configPath, platformConfig, credentials) {
31947
32839
  let existing = {};
31948
32840
  try {
31949
- const raw = fs3.readFileSync(configPath, "utf-8");
32841
+ const raw = fs4.readFileSync(configPath, "utf-8");
31950
32842
  existing = JSON.parse(raw);
31951
32843
  } catch {
31952
32844
  }
@@ -32018,9 +32910,9 @@ function applyToOpenClawConfig(configPath, platformConfig, credentials) {
32018
32910
  agents.defaults = cleanedExisting;
32019
32911
  existing.agents = agents;
32020
32912
  const tmpPath = `${configPath}.tmp`;
32021
- fs3.mkdirSync(path3.dirname(configPath), { recursive: true });
32022
- fs3.writeFileSync(tmpPath, JSON.stringify(existing, null, 2), "utf-8");
32023
- fs3.renameSync(tmpPath, configPath);
32913
+ fs4.mkdirSync(path4.dirname(configPath), { recursive: true });
32914
+ fs4.writeFileSync(tmpPath, JSON.stringify(existing, null, 2), "utf-8");
32915
+ fs4.renameSync(tmpPath, configPath);
32024
32916
  }
32025
32917
  async function fetchAndApplyPlatformConfig(opts) {
32026
32918
  const { client, stateDir, configPath, credentials, log } = opts;
@@ -32059,7 +32951,7 @@ async function fetchAndApplyPlatformConfig(opts) {
32059
32951
 
32060
32952
  // dist/wiki-helper.js
32061
32953
  import { spawn, spawnSync } from "node:child_process";
32062
- import path4 from "node:path";
32954
+ import path5 from "node:path";
32063
32955
  var DEFAULT_SYNC_TIMEOUT_MS = 9e4;
32064
32956
  var DEFAULT_WATCH_INTERVAL_SEC = 30;
32065
32957
  function isCommandMissing(error) {
@@ -32074,7 +32966,7 @@ function resolveParallCli() {
32074
32966
  return _cli;
32075
32967
  }
32076
32968
  function resolveMountRoot(stateDir) {
32077
- return process.env.PRLL_WIKI_MOUNT_ROOT?.trim() || path4.join(stateDir, "workspace");
32969
+ return process.env.PRLL_WIKI_MOUNT_ROOT?.trim() || path5.join(stateDir, "workspace");
32078
32970
  }
32079
32971
  function resolveWatchIntervalSec() {
32080
32972
  const raw = process.env.PRLL_WIKI_REFRESH_INTERVAL_SEC?.trim();
@@ -32190,7 +33082,7 @@ async function startWikiHelper(params) {
32190
33082
  // dist/oc-session.js
32191
33083
  import { randomUUID } from "node:crypto";
32192
33084
  import { existsSync as existsSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
32193
- import { join as join3, resolve as resolve2 } from "node:path";
33085
+ import { join as join4, resolve as resolve2 } from "node:path";
32194
33086
  var CURRENT_SESSION_VERSION = 3;
32195
33087
  function generateId(existing) {
32196
33088
  for (let i = 0; i < 100; i++) {
@@ -32336,7 +33228,7 @@ var SessionManager = class _SessionManager {
32336
33228
  this.leafId = null;
32337
33229
  this.flushed = false;
32338
33230
  const ts = timestamp.replace(/[:.]/g, "-");
32339
- this.sessionFile = join3(this.sessionDir, `${ts}_${this.sessionId}.jsonl`);
33231
+ this.sessionFile = join4(this.sessionDir, `${ts}_${this.sessionId}.jsonl`);
32340
33232
  }
32341
33233
  buildIndex() {
32342
33234
  this.byId.clear();
@@ -32383,14 +33275,14 @@ var SessionManager = class _SessionManager {
32383
33275
  }
32384
33276
  // -- Branching -------------------------------------------------------------
32385
33277
  getBranch(fromId) {
32386
- const path7 = [];
33278
+ const path8 = [];
32387
33279
  const startId = fromId ?? this.leafId;
32388
33280
  let current = startId ? this.byId.get(startId) : void 0;
32389
33281
  while (current) {
32390
- path7.unshift(current);
33282
+ path8.unshift(current);
32391
33283
  current = current.parentId ? this.byId.get(current.parentId) : void 0;
32392
33284
  }
32393
- return path7;
33285
+ return path8;
32394
33286
  }
32395
33287
  createBranchedSession(leafId) {
32396
33288
  const branch = this.getBranch(leafId);
@@ -32400,7 +33292,7 @@ var SessionManager = class _SessionManager {
32400
33292
  const newId = randomUUID();
32401
33293
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
32402
33294
  const ts = timestamp.replace(/[:.]/g, "-");
32403
- const newFile = join3(this.sessionDir, `${ts}_${newId}.jsonl`);
33295
+ const newFile = join4(this.sessionDir, `${ts}_${newId}.jsonl`);
32404
33296
  const header = {
32405
33297
  type: "session",
32406
33298
  version: CURRENT_SESSION_VERSION,
@@ -32446,50 +33338,50 @@ var SessionManager = class _SessionManager {
32446
33338
  return newFile;
32447
33339
  }
32448
33340
  // -- Factory ---------------------------------------------------------------
32449
- static open(path7) {
32450
- const entries = loadEntries(path7);
33341
+ static open(path8) {
33342
+ const entries = loadEntries(path8);
32451
33343
  const header = entries.find((e) => e.type === "session");
32452
33344
  const cwd = header?.cwd ?? process.cwd();
32453
- const dir = resolve2(path7, "..");
32454
- return new _SessionManager(cwd, dir, path7);
33345
+ const dir = resolve2(path8, "..");
33346
+ return new _SessionManager(cwd, dir, path8);
32455
33347
  }
32456
33348
  };
32457
33349
 
32458
33350
  // dist/fork.js
32459
- import * as fs4 from "node:fs";
32460
- import * as path5 from "node:path";
33351
+ import * as fs5 from "node:fs";
33352
+ import * as path6 from "node:path";
32461
33353
  import * as crypto from "node:crypto";
32462
33354
  function readStoreEntry(sessionsDir, sessionKey) {
32463
- const storeFile = path5.join(sessionsDir, "sessions.json");
33355
+ const storeFile = path6.join(sessionsDir, "sessions.json");
32464
33356
  try {
32465
- const store = JSON.parse(fs4.readFileSync(storeFile, "utf-8"));
33357
+ const store = JSON.parse(fs5.readFileSync(storeFile, "utf-8"));
32466
33358
  return store[sessionKey] ?? store[sessionKey.toLowerCase()] ?? null;
32467
33359
  } catch {
32468
33360
  return null;
32469
33361
  }
32470
33362
  }
32471
33363
  function writeStoreEntry(sessionsDir, sessionKey, entry) {
32472
- const storeFile = path5.join(sessionsDir, "sessions.json");
33364
+ const storeFile = path6.join(sessionsDir, "sessions.json");
32473
33365
  try {
32474
33366
  let store = {};
32475
33367
  try {
32476
- store = JSON.parse(fs4.readFileSync(storeFile, "utf-8"));
33368
+ store = JSON.parse(fs5.readFileSync(storeFile, "utf-8"));
32477
33369
  } catch {
32478
33370
  }
32479
33371
  store[sessionKey.toLowerCase()] = entry;
32480
- fs4.writeFileSync(storeFile, JSON.stringify(store, null, 2), { encoding: "utf-8" });
33372
+ fs5.writeFileSync(storeFile, JSON.stringify(store, null, 2), { encoding: "utf-8" });
32481
33373
  return true;
32482
33374
  } catch {
32483
33375
  return false;
32484
33376
  }
32485
33377
  }
32486
33378
  function deleteStoreEntry(sessionsDir, sessionKey) {
32487
- const storeFile = path5.join(sessionsDir, "sessions.json");
33379
+ const storeFile = path6.join(sessionsDir, "sessions.json");
32488
33380
  try {
32489
- const store = JSON.parse(fs4.readFileSync(storeFile, "utf-8"));
33381
+ const store = JSON.parse(fs5.readFileSync(storeFile, "utf-8"));
32490
33382
  delete store[sessionKey];
32491
33383
  delete store[sessionKey.toLowerCase()];
32492
- fs4.writeFileSync(storeFile, JSON.stringify(store, null, 2), { encoding: "utf-8" });
33384
+ fs5.writeFileSync(storeFile, JSON.stringify(store, null, 2), { encoding: "utf-8" });
32493
33385
  } catch {
32494
33386
  }
32495
33387
  }
@@ -32501,17 +33393,17 @@ function resolveTranscriptFile(sessionsDir, sessionKey) {
32501
33393
  if (!entry?.sessionId)
32502
33394
  return null;
32503
33395
  if (entry.sessionFile) {
32504
- const resolved = path5.isAbsolute(entry.sessionFile) ? entry.sessionFile : path5.join(sessionsDir, entry.sessionFile);
32505
- if (fs4.existsSync(resolved))
33396
+ const resolved = path6.isAbsolute(entry.sessionFile) ? entry.sessionFile : path6.join(sessionsDir, entry.sessionFile);
33397
+ if (fs5.existsSync(resolved))
32506
33398
  return resolved;
32507
33399
  }
32508
- const conventional = path5.join(sessionsDir, `${entry.sessionId}.jsonl`);
32509
- if (fs4.existsSync(conventional))
33400
+ const conventional = path6.join(sessionsDir, `${entry.sessionId}.jsonl`);
33401
+ if (fs5.existsSync(conventional))
32510
33402
  return conventional;
32511
33403
  try {
32512
- const files = fs4.readdirSync(sessionsDir);
33404
+ const files = fs5.readdirSync(sessionsDir);
32513
33405
  const match = files.find((file) => file.includes(entry.sessionId) && file.endsWith(".jsonl"));
32514
- return match ? path5.join(sessionsDir, match) : null;
33406
+ return match ? path6.join(sessionsDir, match) : null;
32515
33407
  } catch {
32516
33408
  return null;
32517
33409
  }
@@ -32522,7 +33414,7 @@ function resolveSessionId(sessionsDir, sessionKey) {
32522
33414
  }
32523
33415
  function forkOrchestratorSession(opts) {
32524
33416
  const { orchestratorSessionKey, accountId, transcriptFile, sessionsDir } = opts;
32525
- if (!fs4.existsSync(transcriptFile))
33417
+ if (!fs5.existsSync(transcriptFile))
32526
33418
  return null;
32527
33419
  try {
32528
33420
  const manager = SessionManager.open(transcriptFile);
@@ -32542,7 +33434,7 @@ function forkOrchestratorSession(opts) {
32542
33434
  sessionId = crypto.randomUUID();
32543
33435
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
32544
33436
  const fileTimestamp = timestamp.replace(/[:.]/g, "-");
32545
- sessionFile = path5.join(manager.getSessionDir(), `${fileTimestamp}_${sessionId}.jsonl`);
33437
+ sessionFile = path6.join(manager.getSessionDir(), `${fileTimestamp}_${sessionId}.jsonl`);
32546
33438
  const header = {
32547
33439
  type: "session",
32548
33440
  version: CURRENT_SESSION_VERSION,
@@ -32551,7 +33443,7 @@ function forkOrchestratorSession(opts) {
32551
33443
  cwd: manager.getCwd(),
32552
33444
  parentSession: transcriptFile
32553
33445
  };
32554
- fs4.writeFileSync(sessionFile, `${JSON.stringify(header)}
33446
+ fs5.writeFileSync(sessionFile, `${JSON.stringify(header)}
32555
33447
  `, {
32556
33448
  encoding: "utf-8",
32557
33449
  mode: 384,
@@ -32561,7 +33453,7 @@ function forkOrchestratorSession(opts) {
32561
33453
  const forkSessionKey = `${orchestratorSessionKey}:fork:${sessionId}`;
32562
33454
  const wrote = writeStoreEntry(sessionsDir, forkSessionKey, {
32563
33455
  sessionId,
32564
- sessionFile: path5.relative(sessionsDir, sessionFile),
33456
+ sessionFile: path6.relative(sessionsDir, sessionFile),
32565
33457
  updatedAt: Date.now(),
32566
33458
  spawnedBy: orchestratorSessionKey,
32567
33459
  parentSessionKey: orchestratorSessionKey,
@@ -32576,8 +33468,8 @@ function forkOrchestratorSession(opts) {
32576
33468
  }
32577
33469
  function cleanupForkSession(opts) {
32578
33470
  try {
32579
- if (fs4.existsSync(opts.sessionFile)) {
32580
- fs4.unlinkSync(opts.sessionFile);
33471
+ if (fs5.existsSync(opts.sessionFile)) {
33472
+ fs5.unlinkSync(opts.sessionFile);
32581
33473
  }
32582
33474
  } catch {
32583
33475
  }
@@ -32887,8 +33779,8 @@ var parallGateway = {
32887
33779
  const telemetry = await initAgentTelemetry("parall-openclaw-agent", "openclaw");
32888
33780
  const otelLog = createOtelLogger("agent", "openclaw-agent");
32889
33781
  try {
32890
- const stateDir = process.env.OPENCLAW_STATE_DIR || path6.join(process.env.HOME || "/data", ".openclaw");
32891
- const openclawConfigPath = path6.join(stateDir, "openclaw.json");
33782
+ const stateDir = process.env.OPENCLAW_STATE_DIR || path7.join(process.env.HOME || "/data", ".openclaw");
33783
+ const openclawConfigPath = path7.join(stateDir, "openclaw.json");
32892
33784
  const configManagerOpts = {
32893
33785
  client,
32894
33786
  stateDir,
@@ -32924,7 +33816,7 @@ var parallGateway = {
32924
33816
  wsUrl
32925
33817
  });
32926
33818
  const orchestratorKey = buildOrchestratorSessionKey(ctx.accountId);
32927
- const sessionsDir = path6.join(stateDir, "agents", "main", "sessions");
33819
+ const sessionsDir = path7.join(stateDir, "agents", "main", "sessions");
32928
33820
  const workspaceDir = process.cwd();
32929
33821
  ensureLocalAttachmentGitExclude(workspaceDir);
32930
33822
  const dispatchAdapter = createOpenClawDispatchAdapter({
@@ -33117,9 +34009,9 @@ To respond, you **must** use the Parall CLI via the exec (Bash) tool. If \`paral
33117
34009
 
33118
34010
  ### Event types and where to reply
33119
34011
 
33120
- - **\`[Event: message.new]\`** \u2014 includes \`[Chat: ... (prll://cht_xxx)]\`. Reply into that chat:
34012
+ - **\`[Event: message.new]\`** \u2014 includes \`[Chat: ... (prll://cht_xxx)]\`. Reply into that chat. Write the reply with your file tool first (shell-safe), then send it \u2014 don't wrap message content in \`--text "..."\`, since the shell expands \`$1,000\`\u2192\`,000\` and runs \`$(...)\` inside double quotes:
33121
34013
 
33122
- parall messages send prll://cht_xxx --text "Your reply here"
34014
+ parall messages send prll://cht_xxx --text-file /tmp/reply.md
33123
34015
 
33124
34016
  - **\`[Event: task.assigned]\` / \`[Event: task.comment.created]\`** \u2014 includes \`[Task: ... (prll://tsk_xxx)]\`. Act on the task; use task CLI subcommands (\`tasks update\`, \`tasks comment\`). See the \`parall-tasks\` skill.
33125
34017