@parall/daemon 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.
Files changed (56) hide show
  1. package/bundle/manifest.json +11 -11
  2. package/bundle/parall-browser-pod.js +352 -27
  3. package/bundle/parall-claude-agent.js +1219 -269
  4. package/bundle/parall-codex-agent.js +1225 -276
  5. package/bundle/parall-daemon.js +33552 -31712
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +2 -2
  8. package/dist/clip-runtime/browser-daemon-env.d.ts +29 -0
  9. package/dist/clip-runtime/browser-daemon-env.d.ts.map +1 -0
  10. package/dist/clip-runtime/browser-daemon-env.js +70 -0
  11. package/dist/clip-runtime/browser-profile-manager.d.ts +18 -5
  12. package/dist/clip-runtime/browser-profile-manager.d.ts.map +1 -1
  13. package/dist/clip-runtime/browser-profile-manager.js +149 -24
  14. package/dist/clip-runtime/browser-profile-pool.d.ts +250 -0
  15. package/dist/clip-runtime/browser-profile-pool.d.ts.map +1 -0
  16. package/dist/clip-runtime/browser-profile-pool.js +581 -0
  17. package/dist/clip-runtime/index.d.ts +2 -2
  18. package/dist/clip-runtime/index.d.ts.map +1 -1
  19. package/dist/clip-runtime/index.js +2 -1
  20. package/dist/clip-runtime/process-manager.d.ts +5 -3
  21. package/dist/clip-runtime/process-manager.d.ts.map +1 -1
  22. package/dist/clip-runtime/subprocess.d.ts +14 -0
  23. package/dist/clip-runtime/subprocess.d.ts.map +1 -1
  24. package/dist/clip-runtime/subprocess.js +49 -0
  25. package/dist/config.d.ts +14 -15
  26. package/dist/config.d.ts.map +1 -1
  27. package/dist/config.js +8 -26
  28. package/dist/daemon-main.d.ts +8 -0
  29. package/dist/daemon-main.d.ts.map +1 -0
  30. package/dist/daemon-main.js +165 -0
  31. package/dist/daemon-paths.d.ts +14 -0
  32. package/dist/daemon-paths.d.ts.map +1 -0
  33. package/dist/daemon-paths.js +26 -0
  34. package/dist/daemon-update-mode.d.ts +8 -0
  35. package/dist/daemon-update-mode.d.ts.map +1 -0
  36. package/dist/daemon-update-mode.js +18 -0
  37. package/dist/index.js +41 -167
  38. package/dist/runtime-bin-resolver.d.ts +4 -0
  39. package/dist/runtime-bin-resolver.d.ts.map +1 -1
  40. package/dist/runtime-bin-resolver.js +40 -7
  41. package/dist/runtime-detector.d.ts +30 -0
  42. package/dist/runtime-detector.d.ts.map +1 -0
  43. package/dist/runtime-detector.js +100 -0
  44. package/dist/supervisor.d.ts +53 -2
  45. package/dist/supervisor.d.ts.map +1 -1
  46. package/dist/supervisor.js +604 -67
  47. package/dist/update-health-gate.d.ts +66 -0
  48. package/dist/update-health-gate.d.ts.map +1 -0
  49. package/dist/update-health-gate.js +93 -0
  50. package/dist/updater-manifest.d.ts +2 -1
  51. package/dist/updater-manifest.d.ts.map +1 -1
  52. package/dist/updater-manifest.js +38 -7
  53. package/dist/updater.d.ts +13 -2
  54. package/dist/updater.d.ts.map +1 -1
  55. package/dist/updater.js +126 -17
  56. package/package.json +6 -6
@@ -17718,9 +17718,9 @@ var require_getMachineId_linux = __commonJS({
17718
17718
  var api_1 = (init_esm(), __toCommonJS(esm_exports));
17719
17719
  async function getMachineId() {
17720
17720
  const paths = ["/etc/machine-id", "/var/lib/dbus/machine-id"];
17721
- for (const path9 of paths) {
17721
+ for (const path10 of paths) {
17722
17722
  try {
17723
- const result = await fs_1.promises.readFile(path9, { encoding: "utf8" });
17723
+ const result = await fs_1.promises.readFile(path10, { encoding: "utf8" });
17724
17724
  return result.trim();
17725
17725
  } catch (e) {
17726
17726
  api_1.diag.debug(`error reading machine id: ${e}`);
@@ -21123,7 +21123,7 @@ function appendRootPathToUrlIfNeeded(url) {
21123
21123
  return void 0;
21124
21124
  }
21125
21125
  }
21126
- function appendResourcePathToUrl(url, path9) {
21126
+ function appendResourcePathToUrl(url, path10) {
21127
21127
  try {
21128
21128
  new URL(url);
21129
21129
  } catch (_a) {
@@ -21133,11 +21133,11 @@ function appendResourcePathToUrl(url, path9) {
21133
21133
  if (!url.endsWith("/")) {
21134
21134
  url = url + "/";
21135
21135
  }
21136
- url += path9;
21136
+ url += path10;
21137
21137
  try {
21138
21138
  new URL(url);
21139
21139
  } catch (_b) {
21140
- diag2.warn("Configuration: Provided URL appended with '" + path9 + "' is not a valid URL, using 'undefined' instead of '" + url + "'");
21140
+ diag2.warn("Configuration: Provided URL appended with '" + path10 + "' is not a valid URL, using 'undefined' instead of '" + url + "'");
21141
21141
  return void 0;
21142
21142
  }
21143
21143
  return url;
@@ -26474,6 +26474,19 @@ function parseProviderConfig(env) {
26474
26474
  }
26475
26475
  }
26476
26476
 
26477
+ // ts/agent-core/dist/lane-key.js
26478
+ import * as path from "node:path";
26479
+ function laneKeyForTarget(targetUri, threadRootId) {
26480
+ return Buffer.from(`${targetUri}
26481
+ ${threadRootId ?? ""}`, "utf8").toString("base64url");
26482
+ }
26483
+ function dispatchLaneContextDir(stateDir) {
26484
+ return path.join(stateDir, "dispatch-lane-context");
26485
+ }
26486
+ function laneContextFilePath(contextDir, targetUri, threadRootId) {
26487
+ return path.join(contextDir, `${laneKeyForTarget(targetUri, threadRootId)}.json`);
26488
+ }
26489
+
26477
26490
  // ts/agent-core/dist/session-state.js
26478
26491
  function normalizeSessionKey(sessionKey) {
26479
26492
  return sessionKey.toLowerCase();
@@ -26647,6 +26660,17 @@ function buildEventBody(event) {
26647
26660
  if (event.attachedUri)
26648
26661
  lines.push(`[Attached: ${sanitizeMeta(event.attachedUri)}]`);
26649
26662
  lines.push("", event.body);
26663
+ } else if (event.type === "channel_message") {
26664
+ lines.push(`[Event: channel.message]`);
26665
+ const providerLabel = sanitizeMeta(event.channelProvider ?? "external IM");
26666
+ const convLabel = event.channelExternalConversationId ? `${sanitizeMeta(event.channelExternalConversationId)} (${sanitizeMeta(event.channelConversationType ?? "conversation")})` : sanitizeMeta(event.channelConversationType ?? "conversation");
26667
+ lines.push(`[Channel: ${providerLabel} | conversation: ${convLabel}]`);
26668
+ lines.push(`[From: ${sanitizeMeta(event.senderName)} (external user, not a Parall member)]`);
26669
+ if (event.channelExternalMessageId) {
26670
+ lines.push(`[External message ID: ${sanitizeMeta(event.channelExternalMessageId)}]`);
26671
+ }
26672
+ 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.]`);
26673
+ lines.push("", event.body);
26650
26674
  } else if (event.type === "external_trigger") {
26651
26675
  lines.push(`[Event: external.trigger]`);
26652
26676
  lines.push(`[Trigger: prll://${event.targetId}]`);
@@ -26688,7 +26712,7 @@ function buildSendMessageHint(event) {
26688
26712
  }
26689
26713
  if (event.targetId.startsWith("cht_")) {
26690
26714
  return `
26691
- <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>`;
26715
+ <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>`;
26692
26716
  }
26693
26717
  if (event.targetId.startsWith("tsk_")) {
26694
26718
  return `
@@ -26697,6 +26721,14 @@ function buildSendMessageHint(event) {
26697
26721
  if (event.targetId.startsWith("sch_")) {
26698
26722
  return `
26699
26723
  <system-reminder>To communicate, use the CLI: \`parall messages send\` / \`parall dm\`. Your plain text output is not delivered.</system-reminder>`;
26724
+ }
26725
+ if (event.type === "channel_message") {
26726
+ const clipLabel = event.channelProvider ? `the \`${event.channelProvider}\` clip's` : "your channel provider clip's";
26727
+ const apiLabel = event.channelProvider ?? "external platform";
26728
+ const target = event.channelExternalConversationId ? `{"chat_id": "${event.channelExternalConversationId}", "text": "..."}` : `{"chat_id": "<conversation id>", "text": "..."}`;
26729
+ const threadAlt = event.channelExternalMessageId ? ` To reply in-thread to this specific message, use {"message_id": "${event.channelExternalMessageId}", "text": "..."} instead.` : "";
26730
+ return `
26731
+ <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>`;
26700
26732
  }
26701
26733
  if (event.type === "external_trigger" || event.targetId.startsWith("xtr_")) {
26702
26734
  return `
@@ -26904,6 +26936,8 @@ Or upload first and reuse across chats:
26904
26936
  parall messages send prll://cht_aaa --attachment att_yyy --text "Report"
26905
26937
  parall messages send prll://cht_bbb --attachment att_yyy --text "FYI"
26906
26938
 
26939
+ 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 \`$(...)\`.
26940
+
26907
26941
  ### When to reference
26908
26942
 
26909
26943
  - **Origin** \u2014 always link the message or task that triggered your work
@@ -26954,11 +26988,21 @@ To say something in a chat, you **must** invoke the Parall CLI via your shell/ex
26954
26988
 
26955
26989
  All outbound interactions go through the \`parall\` CLI. Credentials are pre-injected as environment variables \u2014 no setup needed. If \`parall\` is not on PATH, use \`npx --yes @parall/cli@latest\` instead.
26956
26990
 
26957
- - \`parall messages send prll://cht_xxx --text "..."\` \u2014 reply into the triggering chat
26958
- - \`parall dm prll://usr_xxx --text "..." [--no-reply]\` \u2014 direct message another user
26991
+ - \`parall messages send prll://cht_xxx --text-file -\` \u2014 reply into the triggering chat (pipe the body via a quoted heredoc; see Shell-safety below)
26992
+ - \`parall dm prll://usr_xxx --text-file - [--no-reply]\` \u2014 direct message another user
26959
26993
  - \`parall tasks update prll://tsk_xxx --status in_progress\` \u2014 task state
26960
26994
  - \`parall no-reply [--reason "..."]\` \u2014 explicitly declare this turn silent (audit signal; not required for silence, just clarifies intent)
26961
26995
 
26996
+ **Shell-safety \u2014 never wrap real message content in double quotes.** Your command runs in a shell, which expands \`$\`, backticks, and \`$(...)\` inside \`"..."\` before the CLI sees them: \`--text "That costs $1,000"\` sends \`That costs ,000\`, and \`--text "$(cmd)"\` executes \`cmd\`. Pass message bodies via \`--text-file <path>\` (write the file first \u2014 no shell touches it) or a quoted heredoc that disables expansion:
26997
+
26998
+ \`\`\`bash
26999
+ parall messages send prll://cht_xxx --text-file - <<'EOF'
27000
+ That costs $1,000, and $(whoami) stays literal. I'm on it.
27001
+ EOF
27002
+ \`\`\`
27003
+
27004
+ Keep \`--text "..."\` for short literals with no \`$\`, backtick, or apostrophe.
27005
+
26962
27006
  The bridge injects Parall context via environment variables. The static credentials \`PRLL_API_URL\`, \`PRLL_API_KEY\`, and \`PRLL_ORG_ID\` are always set. \`PRLL_CONTEXT_FILE\` points to a per-session JSON file that the gateway updates each dispatch with \`session_id\`, \`chat_id\`, \`trigger_message_id\`, \`no_reply\`, and \`step_id\` (updated per tool call). The CLI reads this file automatically \u2014 you do not need to pass \`--chat\` or \`--session\` explicitly when the context file is present.
26963
27007
 
26964
27008
  ## Attachments
@@ -27003,7 +27047,7 @@ Messages may arrive with a \`[Thread: prll://msg_xxx]\` line in the event block,
27003
27047
 
27004
27048
  ### CLI usage
27005
27049
  - The \`msg_xxx\` from the \`[Thread: prll://msg_xxx]\` line is the thread root ID \u2014 pass it as \`--thread-root-id\`
27006
- - Reply in a thread: \`parall messages send <chatId> --text "..." --thread-root-id <msgId>\`
27050
+ - Reply in a thread: \`parall messages send <chatId> --text-file /tmp/reply.md --thread-root-id <msgId>\`
27007
27051
  - Read thread replies: \`parall messages list <chatId> --thread-root-id <msgId>\`
27008
27052
 
27009
27053
  ### Thread etiquette
@@ -27071,8 +27115,8 @@ function childLogger(logger, sub) {
27071
27115
 
27072
27116
  // ts/agent-core/dist/gateway-base.js
27073
27117
  import * as os from "node:os";
27074
- import * as fs from "node:fs";
27075
- import * as path from "node:path";
27118
+ import * as fs2 from "node:fs";
27119
+ import * as path2 from "node:path";
27076
27120
 
27077
27121
  // ts/sdk/dist/types.js
27078
27122
  var MENTION_ALL_USER_ID = "all";
@@ -27104,6 +27148,7 @@ var ENDPOINTS = {
27104
27148
  // Org-scoped
27105
27149
  ORG: (orgId) => `${API_BASE}/orgs/${orgId}`,
27106
27150
  ORG_MEMBERS: (orgId) => `${API_BASE}/orgs/${orgId}/members`,
27151
+ ORG_MEMBERS_FORMER: (orgId) => `${API_BASE}/orgs/${orgId}/members/former`,
27107
27152
  TEAMS: (orgId) => `${API_BASE}/orgs/${orgId}/teams`,
27108
27153
  ORG_MEMBERS_ONLINE: (orgId) => `${API_BASE}/orgs/${orgId}/members/online`,
27109
27154
  ORG_MEMBER: (orgId, userId) => `${API_BASE}/orgs/${orgId}/members/${userId}`,
@@ -27253,6 +27298,19 @@ var ENDPOINTS = {
27253
27298
  EXTERNAL_TRIGGER: (orgId, triggerId) => `${API_BASE}/orgs/${orgId}/external-triggers/${triggerId}`,
27254
27299
  EXTERNAL_TRIGGER_RUNS: (orgId) => `${API_BASE}/orgs/${orgId}/external-trigger-runs`,
27255
27300
  EXTERNAL_TRIGGER_RUN: (orgId, runId) => `${API_BASE}/orgs/${orgId}/external-trigger-runs/${runId}`,
27301
+ // External IM channel (org-scoped, platform-mediated Feishu/Slack)
27302
+ CHANNEL_CONNECTIONS: (orgId) => `${API_BASE}/orgs/${orgId}/channel-connections`,
27303
+ CHANNEL_CONNECTION: (orgId, connectionId) => `${API_BASE}/orgs/${orgId}/channel-connections/${connectionId}`,
27304
+ CHANNEL_CONNECTION_CREDENTIALS: (orgId, connectionId) => `${API_BASE}/orgs/${orgId}/channel-connections/${connectionId}/credentials`,
27305
+ CHANNEL_CONNECTION_INGRESS_TOKEN_REGENERATE: (orgId, connectionId) => `${API_BASE}/orgs/${orgId}/channel-connections/${connectionId}/ingress-token/regenerate`,
27306
+ CHANNEL_CONNECTION_CONVERSATIONS: (orgId, connectionId) => `${API_BASE}/orgs/${orgId}/channel-connections/${connectionId}/conversations`,
27307
+ CHANNEL_CONVERSATION: (orgId, conversationId) => `${API_BASE}/orgs/${orgId}/channel-conversations/${conversationId}`,
27308
+ CHANNEL_CONVERSATION_MESSAGES: (orgId, conversationId) => `${API_BASE}/orgs/${orgId}/channel-conversations/${conversationId}/messages`,
27309
+ CHANNEL_CONVERSATION_SESSION: (orgId, conversationId) => `${API_BASE}/orgs/${orgId}/channel-conversations/${conversationId}/session`,
27310
+ CHANNEL_MESSAGE: (orgId, messageId) => `${API_BASE}/orgs/${orgId}/channel-messages/${messageId}`,
27311
+ CHANNEL_PROVISIONING: (orgId) => `${API_BASE}/orgs/${orgId}/channel-provisioning`,
27312
+ CHANNEL_PROVISIONING_SESSION: (orgId, sessionId) => `${API_BASE}/orgs/${orgId}/channel-provisioning/${sessionId}`,
27313
+ CHANNEL_PROVISIONING_CANCEL: (orgId, sessionId) => `${API_BASE}/orgs/${orgId}/channel-provisioning/${sessionId}/cancel`,
27256
27314
  // Invitations (org-scoped, admin)
27257
27315
  ORG_INVITATIONS: (orgId) => `${API_BASE}/orgs/${orgId}/invitations`,
27258
27316
  ORG_INVITATION: (orgId, invId) => `${API_BASE}/orgs/${orgId}/invitations/${invId}`,
@@ -27270,9 +27328,16 @@ var ENDPOINTS = {
27270
27328
  INVITE_LINK_JOIN: `${API_BASE}/invite-link/join`,
27271
27329
  // Wikis (org-scoped, served by wiki-service)
27272
27330
  WIKIS: (orgId) => `${WIKI_BASE}/orgs/${orgId}/wikis`,
27331
+ // Recycle bin — soft-deleted wikis (owner only). Must precede WIKI in the
27332
+ // backend router so `deleted` isn't captured as a {wikiId}; the SDK builder
27333
+ // is just a string.
27334
+ WIKIS_DELETED: (orgId) => `${WIKI_BASE}/orgs/${orgId}/wikis/deleted`,
27335
+ // Detail URL — also reused for DELETE (soft-delete) and `${WIKI}/restore`.
27273
27336
  WIKI: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}`,
27337
+ WIKI_RESTORE: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/restore`,
27274
27338
  WIKI_TREE: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/tree`,
27275
27339
  WIKI_BLOB: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/blob`,
27340
+ WIKI_COPY: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/copy`,
27276
27341
  WIKI_NODE_SECTIONS: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/node-sections`,
27277
27342
  WIKI_SEARCH: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/search`,
27278
27343
  WIKI_PAGE_INDEX: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/page-index`,
@@ -27326,6 +27391,11 @@ var ENDPOINTS = {
27326
27391
  DISPATCH_ACK_BY_ID: (orgId, id) => `${API_BASE}/orgs/${orgId}/dispatch/${id}/ack`,
27327
27392
  DISPATCH_EXPIRE: (orgId) => `${API_BASE}/orgs/${orgId}/dispatch/expire`,
27328
27393
  DISPATCH_BY_MESSAGES: (orgId) => `${API_BASE}/orgs/${orgId}/dispatch/by-messages`,
27394
+ DISPATCH_CLAIM: (orgId) => `${API_BASE}/orgs/${orgId}/dispatch/claim`,
27395
+ DISPATCH_STEER: (orgId) => `${API_BASE}/orgs/${orgId}/dispatch/steer`,
27396
+ DISPATCH_COMPLETE: (orgId) => `${API_BASE}/orgs/${orgId}/dispatch/complete`,
27397
+ DISPATCH_RELEASE: (orgId) => `${API_BASE}/orgs/${orgId}/dispatch/release`,
27398
+ DISPATCH_HEARTBEAT: (orgId) => `${API_BASE}/orgs/${orgId}/dispatch/heartbeat`,
27329
27399
  // Unread
27330
27400
  UNREAD: `${API_BASE}/me/unread`,
27331
27401
  ORG_UNREAD: (orgId) => `${API_BASE}/orgs/${orgId}/unread`,
@@ -27355,6 +27425,14 @@ var ENDPOINTS = {
27355
27425
  BILLING_AUTO_RELOAD: (orgId) => `${API_BASE}/orgs/${orgId}/billing/auto-reload`,
27356
27426
  BILLING_SETUP_INTENT: (orgId) => `${API_BASE}/orgs/${orgId}/billing/setup-intent`,
27357
27427
  COMPUTE_PRICING: () => `${API_BASE}/billing/compute-pricing`,
27428
+ // Runtime capability table (public, no auth) — SSOT for the create/settings
27429
+ // interlock: per-runtime compute modes, native model family, cross-family
27430
+ // availability, and recommended model.
27431
+ RUNTIMES: () => `${API_BASE}/runtimes`,
27432
+ // Model catalog (public, no auth) — served from the server's DB-backed
27433
+ // snapshot. Optional include_model keeps an agent's pinned hidden legacy
27434
+ // model representable in settings pickers.
27435
+ MODELS: (includeModel) => includeModel ? `${API_BASE}/models?include_model=${encodeURIComponent(includeModel)}` : `${API_BASE}/models`,
27358
27436
  // ADMIN_GRANTS intentionally NOT exported here — it sits under the
27359
27437
  // unauthenticated `/internal/admin/*` surface and must not bleed into the
27360
27438
  // public SDK. Admin dashboard hits the URL directly from its own client.
@@ -27367,6 +27445,7 @@ var ENDPOINTS = {
27367
27445
  AGENT_CLIP: (orgId, agentId, clipId) => `${CLIP_BASE}/orgs/${orgId}/agents/${agentId}/clips/${clipId}`,
27368
27446
  CLIP_INVOKE: (orgId) => `${CLIP_BASE}/orgs/${orgId}/invoke`,
27369
27447
  CLIP_ONLINE: (orgId) => `${CLIP_BASE}/orgs/${orgId}/online`,
27448
+ BROWSER_RUNTIME_STATUS: (orgId) => `${CLIP_BASE}/orgs/${orgId}/browser-runtime/status`,
27370
27449
  BROWSER_PROFILES: (orgId) => `${CLIP_BASE}/orgs/${orgId}/browser-profiles`,
27371
27450
  BROWSER_PROFILE: (orgId, profileId) => `${CLIP_BASE}/orgs/${orgId}/browser-profiles/${profileId}`,
27372
27451
  BROWSER_PROFILE_OPEN: (orgId, profileId) => `${CLIP_BASE}/orgs/${orgId}/browser-profiles/${profileId}/open`,
@@ -27420,10 +27499,13 @@ var WS_EVENTS = {
27420
27499
  INVITATION_REVOKED: "invitation.revoked",
27421
27500
  ORG_JOIN_REQUEST_NEW: "org.join_request.new",
27422
27501
  ORG_INVITE_LINK_JOINED: "org.invite_link.joined",
27502
+ ORG_MEMBER_REMOVED: "org.member.removed",
27423
27503
  AGENT_CONFIG_UPDATE: "agent_config.update",
27424
27504
  PRESENCE_UPDATE: "presence.update",
27425
27505
  WIKI_CHANGESET_CREATED: "wiki.changeset.created",
27426
27506
  WIKI_CHANGESET_UPDATED: "wiki.changeset.updated",
27507
+ WIKI_DELETED: "wiki.deleted",
27508
+ WIKI_RESTORED: "wiki.restored",
27427
27509
  COMMENT_CREATED: "comment.created",
27428
27510
  COMMENT_UPDATED: "comment.updated",
27429
27511
  COMMENT_DELETED: "comment.deleted",
@@ -27434,6 +27516,7 @@ var WS_EVENTS = {
27434
27516
  READ_POSITION_UPDATED: "read_position.updated",
27435
27517
  DISPATCH_NEW: "dispatch.new",
27436
27518
  DISPATCH_RECEIVED: "dispatch.received",
27519
+ DISPATCH_RESOLVED: "dispatch.resolved",
27437
27520
  SCHEDULE_CREATED: "schedule.created",
27438
27521
  SCHEDULE_UPDATED: "schedule.updated",
27439
27522
  SCHEDULE_DELETED: "schedule.deleted",
@@ -27451,6 +27534,7 @@ var WS_EVENTS = {
27451
27534
  MACHINE_FILESYSTEM_BROWSE: "machine.filesystem.browse",
27452
27535
  MACHINE_UPDATE: "machine.update",
27453
27536
  MACHINE_CONFIG_UPDATED: "machine.config.updated",
27537
+ MACHINE_AGENT_CONFIG_UPDATED: "machine.agent_config.updated",
27454
27538
  MACHINE_CLIP_SYNC: "machine.clip.sync",
27455
27539
  MACHINE_BROWSER_PROFILE_LIFECYCLE: "machine.browser_profile.lifecycle",
27456
27540
  MACHINE_BROWSER_PROFILE_VIEWER: "machine.browser_profile.viewer",
@@ -27528,8 +27612,8 @@ var ParallClient = class _ParallClient {
27528
27612
  * is authoritative, so wiki vs api routing can't drift from how a caller
27529
27613
  * happens to invoke the client.
27530
27614
  */
27531
- baseUrlFor(path9) {
27532
- return path9.startsWith(WIKI_BASE) ? this.wikiBaseUrl : this.baseUrl;
27615
+ baseUrlFor(path10) {
27616
+ return path10.startsWith(WIKI_BASE) ? this.wikiBaseUrl : this.baseUrl;
27533
27617
  }
27534
27618
  setToken(token) {
27535
27619
  this.token = token;
@@ -27556,10 +27640,10 @@ var ParallClient = class _ParallClient {
27556
27640
  * REFRESH_THRESHOLD_S, refresh it **before** sending the request.
27557
27641
  * No-op when the token is still fresh, missing, or un-parseable.
27558
27642
  */
27559
- async ensureFreshToken(path9) {
27643
+ async ensureFreshToken(path10) {
27560
27644
  if (!this.token || !this.getRefreshToken)
27561
27645
  return;
27562
- const pathSuffix = path9.replace(/^\/api\/v1/, "");
27646
+ const pathSuffix = path10.replace(/^\/api\/v1/, "");
27563
27647
  if (_ParallClient.AUTH_PATHS.has(pathSuffix))
27564
27648
  return;
27565
27649
  const exp = _ParallClient.decodeJwtExp(this.token);
@@ -27591,11 +27675,11 @@ var ParallClient = class _ParallClient {
27591
27675
  this.refreshPromise = null;
27592
27676
  }
27593
27677
  }
27594
- async request(method, path9, body, query, retried = false, opts) {
27678
+ async request(method, path10, body, query, retried = false, opts) {
27595
27679
  if (!retried) {
27596
- await this.ensureFreshToken(path9);
27680
+ await this.ensureFreshToken(path10);
27597
27681
  }
27598
- let url = `${this.baseUrlFor(path9)}${path9}`;
27682
+ let url = `${this.baseUrlFor(path10)}${path10}`;
27599
27683
  if (query) {
27600
27684
  const params = new URLSearchParams();
27601
27685
  for (const [key, value] of Object.entries(query)) {
@@ -27625,12 +27709,12 @@ var ParallClient = class _ParallClient {
27625
27709
  throw _ParallClient.normalizeFetchError(err);
27626
27710
  }
27627
27711
  if (res.status === 401) {
27628
- const pathSuffix = path9.replace(/^\/api\/v1/, "");
27712
+ const pathSuffix = path10.replace(/^\/api\/v1/, "");
27629
27713
  const isAuthPath = _ParallClient.AUTH_PATHS.has(pathSuffix);
27630
27714
  if (!retried && !isAuthPath && this.getRefreshToken) {
27631
27715
  const refreshed = await this.tryRefresh();
27632
27716
  if (refreshed) {
27633
- return this.request(method, path9, body, query, true, opts);
27717
+ return this.request(method, path10, body, query, true, opts);
27634
27718
  }
27635
27719
  }
27636
27720
  if (this.onTokenExpired && !isAuthPath) {
@@ -27641,6 +27725,7 @@ var ParallClient = class _ParallClient {
27641
27725
  const rawErrorBody = await res.json().catch(() => ({}));
27642
27726
  throw buildApiError(res, rawErrorBody);
27643
27727
  }
27728
+ opts?.onStatus?.(res.status);
27644
27729
  if (res.status === 204)
27645
27730
  return void 0;
27646
27731
  if (res.status === 202) {
@@ -27659,15 +27744,15 @@ var ParallClient = class _ParallClient {
27659
27744
  * hit the 100 MiB cap, so a longer 5-minute timeout is used so a
27660
27745
  * 50 MiB blob on a slow connection doesn't get chopped at 15 s.
27661
27746
  */
27662
- async multipartRequest(method, path9, body, retried = false) {
27747
+ async multipartRequest(method, path10, body, retried = false) {
27663
27748
  if (!retried) {
27664
- await this.ensureFreshToken(path9);
27749
+ await this.ensureFreshToken(path10);
27665
27750
  }
27666
27751
  const { "Content-Type": _drop, ...headers } = this.buildHeaders();
27667
27752
  void _drop;
27668
27753
  let res;
27669
27754
  try {
27670
- res = await fetch(`${this.baseUrlFor(path9)}${path9}`, {
27755
+ res = await fetch(`${this.baseUrlFor(path10)}${path10}`, {
27671
27756
  method,
27672
27757
  headers,
27673
27758
  body,
@@ -27677,12 +27762,12 @@ var ParallClient = class _ParallClient {
27677
27762
  throw _ParallClient.normalizeFetchError(err);
27678
27763
  }
27679
27764
  if (res.status === 401) {
27680
- const pathSuffix = path9.replace(/^\/api\/v1/, "");
27765
+ const pathSuffix = path10.replace(/^\/api\/v1/, "");
27681
27766
  const isAuthPath = _ParallClient.AUTH_PATHS.has(pathSuffix);
27682
27767
  if (!retried && !isAuthPath && this.getRefreshToken) {
27683
27768
  const refreshed = await this.tryRefresh();
27684
27769
  if (refreshed) {
27685
- return this.multipartRequest(method, path9, body, true);
27770
+ return this.multipartRequest(method, path10, body, true);
27686
27771
  }
27687
27772
  }
27688
27773
  if (this.onTokenExpired && !isAuthPath) {
@@ -27790,6 +27875,13 @@ var ParallClient = class _ParallClient {
27790
27875
  const res = await this.request("GET", ENDPOINTS.ORG_MEMBERS(orgId));
27791
27876
  return res.data;
27792
27877
  }
27878
+ /** User IDs of soft-removed (former) org members — for marking their avatar /
27879
+ * name as "left" where dormant relations still surface them (DMs, task
27880
+ * assignees, message history). */
27881
+ async getFormerMemberIds(orgId) {
27882
+ const res = await this.request("GET", ENDPOINTS.ORG_MEMBERS_FORMER(orgId));
27883
+ return res.user_ids ?? [];
27884
+ }
27793
27885
  async getTeams(orgId) {
27794
27886
  const res = await this.request("GET", ENDPOINTS.TEAMS(orgId));
27795
27887
  return res.data;
@@ -27967,6 +28059,21 @@ var ParallClient = class _ParallClient {
27967
28059
  async sendMessage(orgId, chatId, req) {
27968
28060
  return this.request("POST", ENDPOINTS.CHAT_MESSAGES(orgId, chatId), req);
27969
28061
  }
28062
+ /**
28063
+ * sendMessage variant that also reports whether the server answered with an
28064
+ * idempotent replay (HTTP 200 — the message already existed for this
28065
+ * idempotency/effect key) instead of a fresh create (201). Used by the CLI
28066
+ * to surface "already sent by a previous run (deduplicated)".
28067
+ */
28068
+ async sendMessageDetailed(orgId, chatId, req) {
28069
+ let status = 0;
28070
+ const message = await this.request("POST", ENDPOINTS.CHAT_MESSAGES(orgId, chatId), req, void 0, false, {
28071
+ onStatus: (s) => {
28072
+ status = s;
28073
+ }
28074
+ });
28075
+ return { message, deduplicated: status === 200 };
28076
+ }
27970
28077
  async getMessages(orgId, chatId, params) {
27971
28078
  return this.request("GET", ENDPOINTS.CHAT_MESSAGES(orgId, chatId), void 0, params);
27972
28079
  }
@@ -28278,20 +28385,25 @@ var ParallClient = class _ParallClient {
28278
28385
  return res.data;
28279
28386
  }
28280
28387
  /** `POST /machines/me/browser-profiles/{profileId}/status` — report runtime status. */
28281
- async reportBrowserProfileStatus(profileId, status, errorMsg) {
28388
+ async reportBrowserProfileStatus(profileId, status, errorMsg, generation) {
28282
28389
  await this.request("POST", ENDPOINTS.MACHINES_ME_BROWSER_PROFILE_STATUS(profileId), {
28283
28390
  status,
28284
- ...errorMsg ? { error_msg: errorMsg } : {}
28391
+ ...errorMsg ? { error_msg: errorMsg } : {},
28392
+ ...generation !== void 0 ? { generation } : {}
28285
28393
  });
28286
28394
  }
28287
28395
  /**
28288
28396
  * `POST /machines/me/health` — bump the Machine's `updated_at` to now and
28289
- * report daemon state. The daemon should call this on a fixed cadence (e.g.
28290
- * every 30s) so an external observer can detect a wedged supervisor. Both
28397
+ * report daemon state. The daemon calls this on startup and again whenever
28398
+ * periodic runtime re-detection produces a CHANGED result (there is no
28399
+ * fixed-cadence keepalive loop today — steady state posts nothing). All
28291
28400
  * fields are optional and only persisted when changed:
28292
28401
  * - `daemonVersion` — the running bundle/launcher version.
28293
28402
  * - `selfUpdateCapable` — whether the daemon can act on a machine.update
28294
28403
  * signal (true under a service manager, false for bare `npx` foreground).
28404
+ * - `detectedRuntimes` — runtime CLIs found on the host. Omitted = no
28405
+ * report this beat (server keeps the stored value); [] = detection ran
28406
+ * and found nothing (server clears to empty).
28295
28407
  */
28296
28408
  async postMachineHeartbeat(opts) {
28297
28409
  const body = {};
@@ -28299,6 +28411,8 @@ var ParallClient = class _ParallClient {
28299
28411
  body.daemon_version = opts.daemonVersion;
28300
28412
  if (opts?.selfUpdateCapable !== void 0)
28301
28413
  body.self_update_capable = opts.selfUpdateCapable;
28414
+ if (opts?.detectedRuntimes !== void 0)
28415
+ body.detected_runtimes = opts.detectedRuntimes;
28302
28416
  return this.request("POST", ENDPOINTS.MACHINES_ME_HEALTH, Object.keys(body).length > 0 ? body : void 0);
28303
28417
  }
28304
28418
  async reportAgentWorkspaceState(agentId, state) {
@@ -28347,8 +28461,8 @@ var ParallClient = class _ParallClient {
28347
28461
  async requestMachineUpdate(orgId, machineId, mandatory = false) {
28348
28462
  await this.request("POST", ENDPOINTS.MACHINE_REQUEST_UPDATE(orgId, machineId), { mandatory });
28349
28463
  }
28350
- async browseMachineFilesystem(orgId, machineId, path9) {
28351
- return this.request("POST", ENDPOINTS.MACHINE_BROWSE(orgId, machineId), { path: path9 }, void 0, false, { timeoutMs: 15e3 });
28464
+ async browseMachineFilesystem(orgId, machineId, path10) {
28465
+ return this.request("POST", ENDPOINTS.MACHINE_BROWSE(orgId, machineId), { path: path10 }, void 0, false, { timeoutMs: 15e3 });
28352
28466
  }
28353
28467
  /** Create a new machine key. Returns the raw key string (shown once) + metadata. */
28354
28468
  async createMachineKey(orgId, machineId, name) {
@@ -28421,6 +28535,30 @@ var ParallClient = class _ParallClient {
28421
28535
  const params = maxAgeHours !== void 0 ? { max_age_hours: String(maxAgeHours) } : void 0;
28422
28536
  return this.request("POST", ENDPOINTS.DISPATCH_EXPIRE(orgId), void 0, params);
28423
28537
  }
28538
+ /**
28539
+ * Claim a dispatch lane (explicit consume endpoint). Occupies the
28540
+ * (agent, target, thread) lane and folds claimable WorkItems into it;
28541
+ * `claimed: false` means a healthy incumbent holds the lane.
28542
+ */
28543
+ async claimDispatch(orgId, req) {
28544
+ return this.request("POST", ENDPOINTS.DISPATCH_CLAIM(orgId), req);
28545
+ }
28546
+ /** Fold a pending same-target WorkItem into a live lane (409 STALE_LANE when dethroned). */
28547
+ async steerDispatch(orgId, req) {
28548
+ return this.request("POST", ENDPOINTS.DISPATCH_STEER(orgId), req);
28549
+ }
28550
+ /** End a turn: no_action sweep of the lane's members + lane release + re-drive check. */
28551
+ async completeDispatch(orgId, req) {
28552
+ return this.request("POST", ENDPOINTS.DISPATCH_COMPLETE(orgId), req);
28553
+ }
28554
+ /** Release a lane on graceful shutdown — members return to pending immediately. */
28555
+ async releaseDispatchLane(orgId, lane) {
28556
+ return this.request("POST", ENDPOINTS.DISPATCH_RELEASE(orgId), { lane });
28557
+ }
28558
+ /** Renew a live lane's lease (long-turn keepalive). 409 STALE_LANE when dethroned. */
28559
+ async heartbeatDispatchLane(orgId, req) {
28560
+ return this.request("POST", ENDPOINTS.DISPATCH_HEARTBEAT(orgId), req);
28561
+ }
28424
28562
  async getDispatchByMessages(orgId, chatId, messageIds) {
28425
28563
  const params = { chat_id: chatId, message_ids: messageIds.join(",") };
28426
28564
  return this.request("GET", ENDPOINTS.DISPATCH_BY_MESSAGES(orgId), void 0, params);
@@ -28617,6 +28755,59 @@ var ParallClient = class _ParallClient {
28617
28755
  async deleteExternalConnection(orgId, connectionId) {
28618
28756
  return this.request("DELETE", ENDPOINTS.EXTERNAL_CONNECTION(orgId, connectionId));
28619
28757
  }
28758
+ // ---- External IM channel (org-scoped) ----
28759
+ async createChannelConnection(orgId, input) {
28760
+ return this.request("POST", ENDPOINTS.CHANNEL_CONNECTIONS(orgId), input);
28761
+ }
28762
+ async listChannelConnections(orgId) {
28763
+ return this.request("GET", ENDPOINTS.CHANNEL_CONNECTIONS(orgId));
28764
+ }
28765
+ async getChannelConnection(orgId, connectionId) {
28766
+ return this.request("GET", ENDPOINTS.CHANNEL_CONNECTION(orgId, connectionId));
28767
+ }
28768
+ async updateChannelConnection(orgId, connectionId, patch) {
28769
+ return this.request("PATCH", ENDPOINTS.CHANNEL_CONNECTION(orgId, connectionId), patch);
28770
+ }
28771
+ async archiveChannelConnection(orgId, connectionId) {
28772
+ return this.request("DELETE", ENDPOINTS.CHANNEL_CONNECTION(orgId, connectionId));
28773
+ }
28774
+ async deliverChannelCredentials(orgId, connectionId, credentials) {
28775
+ return this.request("POST", ENDPOINTS.CHANNEL_CONNECTION_CREDENTIALS(orgId, connectionId), credentials);
28776
+ }
28777
+ async regenerateChannelIngressToken(orgId, connectionId) {
28778
+ return this.request("POST", ENDPOINTS.CHANNEL_CONNECTION_INGRESS_TOKEN_REGENERATE(orgId, connectionId));
28779
+ }
28780
+ /** Start a Feishu one-click provisioning session (device flow QR). */
28781
+ async initiateChannelProvisioning(orgId, input) {
28782
+ return this.request("POST", ENDPOINTS.CHANNEL_PROVISIONING(orgId), input);
28783
+ }
28784
+ /**
28785
+ * Lazy status poll — server-side this may forward one provider poll, so
28786
+ * call it at the session's `poll_interval_seconds` cadence, not faster.
28787
+ */
28788
+ async getChannelProvisioningSession(orgId, sessionId) {
28789
+ return this.request("GET", ENDPOINTS.CHANNEL_PROVISIONING_SESSION(orgId, sessionId));
28790
+ }
28791
+ async cancelChannelProvisioning(orgId, sessionId) {
28792
+ return this.request("POST", ENDPOINTS.CHANNEL_PROVISIONING_CANCEL(orgId, sessionId));
28793
+ }
28794
+ async listChannelConversations(orgId, connectionId) {
28795
+ return this.request("GET", ENDPOINTS.CHANNEL_CONNECTION_CONVERSATIONS(orgId, connectionId));
28796
+ }
28797
+ async getChannelConversation(orgId, conversationId) {
28798
+ return this.request("GET", ENDPOINTS.CHANNEL_CONVERSATION(orgId, conversationId));
28799
+ }
28800
+ async listChannelConversationMessages(orgId, conversationId, limit) {
28801
+ return this.request("GET", ENDPOINTS.CHANNEL_CONVERSATION_MESSAGES(orgId, conversationId), void 0, { limit });
28802
+ }
28803
+ async setChannelConversationSession(orgId, conversationId, agentSessionId) {
28804
+ return this.request("PATCH", ENDPOINTS.CHANNEL_CONVERSATION_SESSION(orgId, conversationId), {
28805
+ agent_session_id: agentSessionId
28806
+ });
28807
+ }
28808
+ async getChannelMessage(orgId, messageId) {
28809
+ return this.request("GET", ENDPOINTS.CHANNEL_MESSAGE(orgId, messageId));
28810
+ }
28620
28811
  async getExternalTriggerSchema(orgId, connectionId) {
28621
28812
  return this.request("GET", ENDPOINTS.EXTERNAL_TRIGGER_SCHEMA(orgId, connectionId));
28622
28813
  }
@@ -28658,6 +28849,23 @@ var ParallClient = class _ParallClient {
28658
28849
  async getWiki(orgId, wikiId) {
28659
28850
  return this.request("GET", ENDPOINTS.WIKI(orgId, wikiId));
28660
28851
  }
28852
+ /** Soft-delete an entire wiki (org owner only; the default wiki is
28853
+ * protected → 403 WIKI_PROTECTED). Returns the deleted wiki. Recoverable
28854
+ * via restoreWiki within the retention window. */
28855
+ async deleteWiki(orgId, wikiId) {
28856
+ return this.request("DELETE", ENDPOINTS.WIKI(orgId, wikiId));
28857
+ }
28858
+ /** Restore a soft-deleted wiki (org owner only). 409 WIKI_PURGE_STARTED once
28859
+ * purge has been claimed (no longer restorable), or 404 once the row is gone.
28860
+ * Returns the restored wiki. */
28861
+ async restoreWiki(orgId, wikiId) {
28862
+ return this.request("POST", ENDPOINTS.WIKI_RESTORE(orgId, wikiId));
28863
+ }
28864
+ /** Recycle bin — list soft-deleted wikis for the org (owner only). */
28865
+ async getDeletedWikis(orgId) {
28866
+ const res = await this.request("GET", ENDPOINTS.WIKIS_DELETED(orgId));
28867
+ return res.data;
28868
+ }
28661
28869
  async getWikiTree(orgId, wikiId, params) {
28662
28870
  return this.request("GET", ENDPOINTS.WIKI_TREE(orgId, wikiId), void 0, params);
28663
28871
  }
@@ -28691,6 +28899,15 @@ var ParallClient = class _ParallClient {
28691
28899
  blob.signed_url = this.absoluteMediaUrl(blob.signed_url);
28692
28900
  return blob;
28693
28901
  }
28902
+ /**
28903
+ * Copy one wiki file to another path as an independent snapshot (no sync).
28904
+ * Used to "share a private file out": copy a file from the caller's personal
28905
+ * namespace (`users/{userId}/…`) into the public wiki, leaving the original
28906
+ * untouched. 409 `FILE_EXISTS` if `dest_path` already exists.
28907
+ */
28908
+ async copyWikiFile(orgId, wikiId, req) {
28909
+ return this.request("POST", ENDPOINTS.WIKI_COPY(orgId, wikiId), req);
28910
+ }
28694
28911
  async getWikiNodeSections(orgId, wikiId, params) {
28695
28912
  return this.request("GET", ENDPOINTS.WIKI_NODE_SECTIONS(orgId, wikiId), void 0, params);
28696
28913
  }
@@ -28806,8 +29023,8 @@ var ParallClient = class _ParallClient {
28806
29023
  async deleteWikiRestriction(orgId, wikiId, restrictionId) {
28807
29024
  await this.request("DELETE", ENDPOINTS.WIKI_RESTRICTION(orgId, wikiId, restrictionId));
28808
29025
  }
28809
- async getWikiAccessStatus(orgId, wikiId, path9) {
28810
- return this.request("GET", ENDPOINTS.WIKI_ACCESS_STATUS(orgId, wikiId), void 0, path9 ? { path: path9 } : void 0);
29026
+ async getWikiAccessStatus(orgId, wikiId, path10) {
29027
+ return this.request("GET", ENDPOINTS.WIKI_ACCESS_STATUS(orgId, wikiId), void 0, path10 ? { path: path10 } : void 0);
28811
29028
  }
28812
29029
  async createWikiAccessRequest(orgId, wikiId, data) {
28813
29030
  await this.request("POST", ENDPOINTS.WIKI_ACCESS_REQUESTS(orgId, wikiId), data);
@@ -28816,14 +29033,14 @@ var ParallClient = class _ParallClient {
28816
29033
  async getWikiCommits(orgId, wikiId, params) {
28817
29034
  return this.request("GET", ENDPOINTS.WIKI_COMMITS(orgId, wikiId), void 0, params);
28818
29035
  }
28819
- async getWikiFileCommits(orgId, wikiId, path9, params) {
29036
+ async getWikiFileCommits(orgId, wikiId, path10, params) {
28820
29037
  return this.request("GET", ENDPOINTS.WIKI_FILE_COMMITS(orgId, wikiId), void 0, {
28821
- path: path9,
29038
+ path: path10,
28822
29039
  ...params
28823
29040
  });
28824
29041
  }
28825
- async getWikiBlame(orgId, wikiId, path9, ref) {
28826
- return this.request("GET", ENDPOINTS.WIKI_BLAME(orgId, wikiId), void 0, { path: path9, ref });
29042
+ async getWikiBlame(orgId, wikiId, path10, ref) {
29043
+ return this.request("GET", ENDPOINTS.WIKI_BLAME(orgId, wikiId), void 0, { path: path10, ref });
28827
29044
  }
28828
29045
  // ---- Wiki Operations (audit log) ----
28829
29046
  async getWikiOperations(orgId, wikiId, params) {
@@ -28964,6 +29181,19 @@ var ParallClient = class _ParallClient {
28964
29181
  const resp = await this.request("GET", ENDPOINTS.COMPUTE_PRICING());
28965
29182
  return resp.data;
28966
29183
  }
29184
+ /** Runtime capability table (public) — SSOT for the create/settings interlock. */
29185
+ async getRuntimes() {
29186
+ const resp = await this.request("GET", ENDPOINTS.RUNTIMES());
29187
+ return resp.data;
29188
+ }
29189
+ /**
29190
+ * Platform model catalog (public) — DB-backed, replaces the compile-time
29191
+ * PLATFORM_MODELS constant. Pass includeModel to keep an agent's pinned
29192
+ * hidden legacy model representable (settings picker).
29193
+ */
29194
+ async getModels(includeModel) {
29195
+ return this.request("GET", ENDPOINTS.MODELS(includeModel));
29196
+ }
28967
29197
  // ---- Clips ----
28968
29198
  async listClips(orgId) {
28969
29199
  const resp = await this.request("GET", ENDPOINTS.CLIPS(orgId));
@@ -29008,6 +29238,11 @@ var ParallClient = class _ParallClient {
29008
29238
  const resp = await this.request("GET", ENDPOINTS.CLIP_ONLINE(orgId));
29009
29239
  return resp.data;
29010
29240
  }
29241
+ /** Deployment-wide hosted browser runtime availability — drives whether the
29242
+ * create UI offers the platform-hosted placement. Fail-closed server-side. */
29243
+ async getBrowserRuntimeStatus(orgId) {
29244
+ return this.request("GET", ENDPOINTS.BROWSER_RUNTIME_STATUS(orgId));
29245
+ }
29011
29246
  /** Org-wide browser-profile discovery list. Returns the sanitized
29012
29247
  * {@link BrowserProfileListItem} shape (not the full domain model), each row
29013
29248
  * carrying a per-viewer `can_open` control hint. */
@@ -29437,6 +29672,404 @@ var ParallWs = class {
29437
29672
  }
29438
29673
  };
29439
29674
 
29675
+ // ts/agent-core/dist/lane-ledger.js
29676
+ import * as fs from "node:fs";
29677
+ var LedgerUnsupportedError = class extends Error {
29678
+ };
29679
+ function isStaleLane(err) {
29680
+ return err instanceof ApiError && err.status === 409 && err.code === "STALE_LANE";
29681
+ }
29682
+ function isEndpointMissing(err) {
29683
+ return err instanceof ApiError && err.status === 404 && !err.code;
29684
+ }
29685
+ var LaneLedger = class {
29686
+ opts;
29687
+ lanes = /* @__PURE__ */ new Map();
29688
+ constructor(opts) {
29689
+ this.opts = opts;
29690
+ }
29691
+ get contextDir() {
29692
+ return this.opts.contextDir;
29693
+ }
29694
+ /** Only chat message events ride the lane ledger; typed events stay on the legacy ack path. */
29695
+ handles(event) {
29696
+ return event.type === "message" && event.targetId.startsWith("cht_");
29697
+ }
29698
+ laneKeyFor(event) {
29699
+ if (event.type !== "message" && event.dispatchEventId) {
29700
+ return laneKeyForTarget(`dsp:${event.dispatchEventId}`);
29701
+ }
29702
+ return laneKeyForTarget(`prll://${event.targetId}`, event.threadRootId);
29703
+ }
29704
+ getForEvent(event) {
29705
+ return this.lanes.get(this.laneKeyFor(event));
29706
+ }
29707
+ laneContextPath(lane) {
29708
+ return laneContextFilePath(this.opts.contextDir, lane.targetUri, lane.threadRootId);
29709
+ }
29710
+ /**
29711
+ * Claim (or reuse) the lane for a group of same-lane message events and
29712
+ * fold every group member into it. Returns 'foreign' when a healthy
29713
+ * incumbent (another pod) holds the resource — the caller must not
29714
+ * dispatch; the events stay pending server-side and re-drive after the
29715
+ * incumbent completes.
29716
+ */
29717
+ async ensureLane(events) {
29718
+ const trigger = events[events.length - 1];
29719
+ const laneKey = this.laneKeyFor(trigger);
29720
+ let lane = this.lanes.get(laneKey);
29721
+ if (!lane) {
29722
+ const targetUri = `prll://${trigger.targetId}`;
29723
+ let res;
29724
+ try {
29725
+ res = await this.opts.client.claimDispatch(this.opts.orgId, {
29726
+ target_uri: targetUri,
29727
+ thread_root_id: trigger.threadRootId,
29728
+ limit: 100
29729
+ });
29730
+ } catch (err) {
29731
+ if (isEndpointMissing(err))
29732
+ throw new LedgerUnsupportedError("claim endpoint unavailable");
29733
+ throw err;
29734
+ }
29735
+ if (!res.claimed || !res.lane) {
29736
+ this.opts.log?.info(`lane for ${targetUri} held by a healthy incumbent \u2014 leaving events pending for re-drive`);
29737
+ return null;
29738
+ }
29739
+ const leaseUntilMs = Date.parse(res.lease_until ?? "");
29740
+ lane = {
29741
+ laneKey,
29742
+ lane: res.lane,
29743
+ targetUri,
29744
+ threadRootId: trigger.threadRootId,
29745
+ folded: /* @__PURE__ */ new Map(),
29746
+ ...Number.isNaN(leaseUntilMs) ? {} : { leaseUntilMs, leaseTtlMs: Math.max(leaseUntilMs - Date.now(), 6e4) }
29747
+ };
29748
+ for (const ev of res.events ?? []) {
29749
+ lane.folded.set(ev.source_id, ev.id);
29750
+ }
29751
+ this.lanes.set(laneKey, lane);
29752
+ }
29753
+ for (const ev of events) {
29754
+ if (lane.folded.has(ev.messageId))
29755
+ continue;
29756
+ try {
29757
+ const res = await this.opts.client.steerDispatch(this.opts.orgId, {
29758
+ lane: lane.lane,
29759
+ target_uri: lane.targetUri,
29760
+ thread_root_id: lane.threadRootId,
29761
+ ...ev.dispatchEventId ? { dispatch_event_id: ev.dispatchEventId } : { source_type: "message", source_id: ev.messageId }
29762
+ });
29763
+ lane.folded.set(ev.messageId, res.dispatch_event_id);
29764
+ } catch (err) {
29765
+ if (isStaleLane(err)) {
29766
+ this.lanes.delete(laneKey);
29767
+ return null;
29768
+ }
29769
+ this.opts.log?.warn(`steer fold failed for ${ev.messageId} \u2014 failing closed, releasing lane: ${String(err)}`);
29770
+ await this.release(laneKey);
29771
+ return null;
29772
+ }
29773
+ }
29774
+ return lane;
29775
+ }
29776
+ /**
29777
+ * Fold a live mid-turn message into its active lane BEFORE injecting it
29778
+ * into the running turn. Injection without a successful fold is forbidden —
29779
+ * an un-folded injected message would be re-driven after complete and the
29780
+ * model would handle it twice.
29781
+ */
29782
+ async steerLive(event) {
29783
+ const laneKey = this.laneKeyFor(event);
29784
+ const lane = this.lanes.get(laneKey);
29785
+ if (!lane)
29786
+ return false;
29787
+ if (lane.folded.has(event.messageId))
29788
+ return true;
29789
+ try {
29790
+ const res = await this.opts.client.steerDispatch(this.opts.orgId, {
29791
+ lane: lane.lane,
29792
+ target_uri: lane.targetUri,
29793
+ thread_root_id: lane.threadRootId,
29794
+ ...event.dispatchEventId ? { dispatch_event_id: event.dispatchEventId } : { source_type: "message", source_id: event.messageId }
29795
+ });
29796
+ lane.folded.set(event.messageId, res.dispatch_event_id);
29797
+ return true;
29798
+ } catch (err) {
29799
+ if (isStaleLane(err)) {
29800
+ this.lanes.delete(laneKey);
29801
+ } else {
29802
+ this.opts.log?.warn(`live steer failed for ${event.messageId}: ${String(err)}`);
29803
+ }
29804
+ return false;
29805
+ }
29806
+ }
29807
+ /**
29808
+ * Complete the lane when no local work remains for it: the server sweeps
29809
+ * still-leased members as no_action, releases the occupancy row, and
29810
+ * re-drives any same-target pending work. A STALE_LANE answer means a
29811
+ * takeover already owns the resource — local state is dropped either way.
29812
+ */
29813
+ async completeIfIdle(laneKey, hasMoreLocal) {
29814
+ const lane = this.lanes.get(laneKey);
29815
+ if (!lane || hasMoreLocal)
29816
+ return;
29817
+ this.lanes.delete(laneKey);
29818
+ this.removeLaneContext(lane);
29819
+ try {
29820
+ const res = await this.opts.client.completeDispatch(this.opts.orgId, {
29821
+ lane: lane.lane,
29822
+ target_uri: lane.targetUri,
29823
+ thread_root_id: lane.threadRootId
29824
+ });
29825
+ if (res.swept_no_action > 0 || res.redriven) {
29826
+ this.opts.log?.info(`lane complete for ${lane.targetUri}: swept ${res.swept_no_action} no_action, redriven=${res.redriven}`);
29827
+ }
29828
+ } catch (err) {
29829
+ if (isStaleLane(err)) {
29830
+ this.opts.log?.info(`lane complete skipped for ${lane.targetUri} \u2014 taken over`);
29831
+ return;
29832
+ }
29833
+ this.opts.log?.warn(`lane complete failed for ${lane.targetUri}: ${String(err)}`);
29834
+ }
29835
+ }
29836
+ /**
29837
+ * Long-turn keepalive: renew the lane's lease on runtime activity, throttled
29838
+ * so a chatty turn doesn't spam the server. Without this, a legitimately
29839
+ * long turn (> lane TTL) would be dethroned mid-flight and every subsequent
29840
+ * write misfired with STALE_LANE — the design doc's "long turns renew via
29841
+ * step writes". Fire-and-forget: a failed renewal is surfaced by the next
29842
+ * write's incumbency check anyway.
29843
+ */
29844
+ maybeRenew(lane) {
29845
+ const now = Date.now();
29846
+ const ttl = lane.leaseTtlMs ?? 10 * 6e4;
29847
+ const until = lane.leaseUntilMs ?? now;
29848
+ if (until - now > ttl / 2)
29849
+ return;
29850
+ lane.leaseUntilMs = now + ttl;
29851
+ void this.opts.client.heartbeatDispatchLane(this.opts.orgId, {
29852
+ lane: lane.lane,
29853
+ target_uri: lane.targetUri,
29854
+ thread_root_id: lane.threadRootId
29855
+ }).then((res) => {
29856
+ const until2 = Date.parse(res?.lease_until ?? "");
29857
+ if (!Number.isNaN(until2))
29858
+ lane.leaseUntilMs = until2;
29859
+ }).catch((err) => {
29860
+ if (isStaleLane(err)) {
29861
+ this.lanes.delete(lane.laneKey);
29862
+ this.opts.log?.warn(`lane ${lane.targetUri} was taken over during the turn`);
29863
+ return;
29864
+ }
29865
+ this.opts.log?.warn(`lane heartbeat failed for ${lane.targetUri}: ${String(err)}`);
29866
+ });
29867
+ }
29868
+ /**
29869
+ * Release a lane's unresolved members back to pending (dispatch error /
29870
+ * shutdown) so the next pod re-claims immediately instead of waiting out
29871
+ * the lease.
29872
+ */
29873
+ async release(laneKey) {
29874
+ const lane = this.lanes.get(laneKey);
29875
+ if (!lane)
29876
+ return;
29877
+ this.lanes.delete(laneKey);
29878
+ this.removeLaneContext(lane);
29879
+ try {
29880
+ await this.opts.client.releaseDispatchLane(this.opts.orgId, lane.lane);
29881
+ } catch (err) {
29882
+ this.opts.log?.warn(`lane release failed for ${lane.targetUri}: ${String(err)}`);
29883
+ }
29884
+ }
29885
+ async releaseAll() {
29886
+ const keys = [...this.lanes.keys()];
29887
+ for (const key of keys) {
29888
+ await this.release(key);
29889
+ }
29890
+ }
29891
+ /** True when any lane is currently active (used by shutdown logging). */
29892
+ get activeCount() {
29893
+ return this.lanes.size;
29894
+ }
29895
+ /**
29896
+ * Claim the single-member lane of one typed WorkItem (resource = dsp:<id>),
29897
+ * by WorkItem id or by source identity (the live task.assigned event has no
29898
+ * WorkItem id). Returns null when a healthy incumbent (another pod) holds
29899
+ * it or the WorkItem is already resolved — the caller must skip processing.
29900
+ */
29901
+ async claimTyped(ref) {
29902
+ let res;
29903
+ try {
29904
+ res = await this.opts.client.claimDispatch(this.opts.orgId, {
29905
+ dispatch_event_id: ref.dispatchEventId,
29906
+ source_type: ref.dispatchEventId ? void 0 : ref.sourceType,
29907
+ source_id: ref.dispatchEventId ? void 0 : ref.sourceId
29908
+ });
29909
+ } catch (err) {
29910
+ if (isEndpointMissing(err))
29911
+ throw new LedgerUnsupportedError("claim endpoint unavailable");
29912
+ throw err;
29913
+ }
29914
+ if (!res.claimed || !res.lane || !res.events?.length)
29915
+ return null;
29916
+ const workItem = res.events[0];
29917
+ const targetUri = `dsp:${workItem.id}`;
29918
+ const leaseUntilMs = Date.parse(res.lease_until ?? "");
29919
+ const lane = {
29920
+ laneKey: laneKeyForTarget(targetUri),
29921
+ lane: res.lane,
29922
+ targetUri,
29923
+ folded: /* @__PURE__ */ new Map([[workItem.source_id, workItem.id]]),
29924
+ typedDispatchEventId: workItem.id,
29925
+ ...Number.isNaN(leaseUntilMs) ? {} : { leaseUntilMs, leaseTtlMs: Math.max(leaseUntilMs - Date.now(), 6e4) }
29926
+ };
29927
+ this.lanes.set(lane.laneKey, lane);
29928
+ return lane;
29929
+ }
29930
+ /**
29931
+ * Remove the per-lane context file (and its CLI sidecar) when the lane
29932
+ * ends. A leftover file would make a later cross-context send to the same
29933
+ * target bind a dead lane token and misfire with STALE_LANE instead of
29934
+ * taking the plain non-ledger path.
29935
+ */
29936
+ removeLaneContext(lane) {
29937
+ const contextPath = this.laneContextPath(lane);
29938
+ for (const p of [contextPath, contextPath.replace(/\.json$/, ".reply-state.json")]) {
29939
+ try {
29940
+ fs.rmSync(p, { force: true });
29941
+ } catch {
29942
+ }
29943
+ }
29944
+ }
29945
+ };
29946
+
29947
+ // ts/agent-core/dist/gateway-lane-flow.js
29948
+ async function dispatchLaneGroup(host, opts) {
29949
+ const ledger = host.laneLedger;
29950
+ const event = opts.events[opts.events.length - 1];
29951
+ let lane;
29952
+ try {
29953
+ lane = await ledger.ensureLane(opts.events);
29954
+ } catch (err) {
29955
+ if (!(err instanceof LedgerUnsupportedError))
29956
+ throw err;
29957
+ host.disableLedger("claim endpoint missing");
29958
+ await host.emitDispatchReceived(event);
29959
+ const dispatched2 = await host.runDispatch(event, opts.sessionKey, opts.body, opts.earlier, opts.captureText);
29960
+ if (!dispatched2)
29961
+ return "shutdown";
29962
+ for (const ev of opts.events) {
29963
+ host.opts.client.ackDispatch(host.opts.config.org_id, {
29964
+ source_type: ev.ackSourceType ?? "message",
29965
+ source_id: ev.ackSourceId ?? ev.messageId
29966
+ }).catch(() => {
29967
+ });
29968
+ }
29969
+ return "dispatched";
29970
+ }
29971
+ if (!lane) {
29972
+ for (const ev of opts.events) {
29973
+ host.dispatchedMessages.delete(ev.messageId);
29974
+ }
29975
+ return "foreign";
29976
+ }
29977
+ let dispatched = false;
29978
+ try {
29979
+ dispatched = await host.runDispatch(event, opts.sessionKey, opts.body, opts.earlier, opts.captureText);
29980
+ } catch (err) {
29981
+ await ledger.release(lane.laneKey).catch(() => {
29982
+ });
29983
+ throw err;
29984
+ }
29985
+ if (!dispatched) {
29986
+ return "shutdown";
29987
+ }
29988
+ const pendingInjections = host.opts.dispatchAdapter.hasPendingInjections?.(opts.sessionKey) ?? false;
29989
+ await ledger.completeIfIdle(lane.laneKey, pendingInjections || opts.hasMoreLocal());
29990
+ return "dispatched";
29991
+ }
29992
+ async function consumeTypedDispatch(host, ref, run, ack) {
29993
+ if (!host.laneLedger || host.ledgerDisabled) {
29994
+ if (await run(ref.dispatchEventId))
29995
+ ack(ref.dispatchEventId);
29996
+ return;
29997
+ }
29998
+ let lane;
29999
+ try {
30000
+ lane = await host.laneLedger.claimTyped(ref);
30001
+ } catch (err) {
30002
+ if (err instanceof LedgerUnsupportedError) {
30003
+ host.disableLedger("claim endpoint missing");
30004
+ if (await run(ref.dispatchEventId))
30005
+ ack(ref.dispatchEventId);
30006
+ return;
30007
+ }
30008
+ throw err;
30009
+ }
30010
+ if (!lane) {
30011
+ host.opts.log?.info(`typed dispatch ${ref.dispatchEventId ?? `${ref.sourceType}:${ref.sourceId}`} not claimable (held elsewhere or already resolved) \u2014 skipping`);
30012
+ return;
30013
+ }
30014
+ try {
30015
+ if (await run(lane.typedDispatchEventId))
30016
+ ack(lane.typedDispatchEventId);
30017
+ } finally {
30018
+ await host.laneLedger.completeIfIdle(lane.laneKey, false).catch(() => {
30019
+ });
30020
+ }
30021
+ }
30022
+ async function consumeMessageWorkItem(host, item) {
30023
+ if (host.shuttingDown)
30024
+ return;
30025
+ if (!host.tryClaimMessage(item.source_id))
30026
+ return;
30027
+ const ackItem = () => {
30028
+ host.opts.client.ackDispatchByID(host.opts.config.org_id, item.id).catch(() => {
30029
+ });
30030
+ };
30031
+ let msg = null;
30032
+ try {
30033
+ msg = await host.opts.client.getMessage(item.source_id);
30034
+ } catch (err) {
30035
+ const status = err?.status;
30036
+ if (status !== 404) {
30037
+ host.opts.log?.warn(`message fetch failed for ${item.source_id}, leaving pending: ${String(err)}`);
30038
+ host.dispatchedMessages.delete(item.source_id);
30039
+ return;
30040
+ }
30041
+ }
30042
+ if (!msg || msg.sender_id === host.opts.agentUserId) {
30043
+ host.dispatchedMessages.delete(item.source_id);
30044
+ ackItem();
30045
+ return;
30046
+ }
30047
+ const decision = await host.buildMessageDispatchDecision(item.chat_id, msg);
30048
+ if (decision.action === "retry") {
30049
+ host.dispatchedMessages.delete(item.source_id);
30050
+ return;
30051
+ }
30052
+ if (decision.action === "skip") {
30053
+ host.dispatchedMessages.delete(item.source_id);
30054
+ ackItem();
30055
+ return;
30056
+ }
30057
+ decision.event.dispatchEventId = item.id;
30058
+ const laneResolved = host.usesLaneLedger(decision.event);
30059
+ try {
30060
+ const dispatched = await host.handleInboundEvent(decision.event);
30061
+ if (dispatched) {
30062
+ if (!laneResolved)
30063
+ ackItem();
30064
+ } else {
30065
+ host.dispatchedMessages.delete(item.source_id);
30066
+ }
30067
+ } catch (err) {
30068
+ host.dispatchedMessages.delete(item.source_id);
30069
+ throw err;
30070
+ }
30071
+ }
30072
+
29440
30073
  // ts/agent-core/dist/telemetry.js
29441
30074
  init_esm();
29442
30075
  var import_api_logs = __toESM(require_src(), 1);
@@ -29667,6 +30300,9 @@ function resolveStepTarget(event) {
29667
30300
  if (event.type === "external_trigger" || event.targetId.startsWith("xtr_")) {
29668
30301
  return { target_type: "external_trigger", target_id: event.targetId };
29669
30302
  }
30303
+ if (event.type === "channel_message" || event.targetId.startsWith("chv_")) {
30304
+ return { target_type: "channel_conversation", target_id: event.targetId };
30305
+ }
29670
30306
  if (event.type === "wiki_comment") {
29671
30307
  return { target_type: "wiki", target_id: event.targetId || void 0 };
29672
30308
  }
@@ -29710,6 +30346,9 @@ var ParallAgentGateway = class {
29710
30346
  opts;
29711
30347
  chatInfoMap = /* @__PURE__ */ new Map();
29712
30348
  dispatchedTasks = /* @__PURE__ */ new Set();
30349
+ // connection id → provider alias, for channel_message prompt labeling
30350
+ // (stable mapping; avoids one connection fetch per inbound message).
30351
+ channelConnectionProviders = /* @__PURE__ */ new Map();
29713
30352
  dispatchedMessages = /* @__PURE__ */ new Set();
29714
30353
  forkStates = /* @__PURE__ */ new Map();
29715
30354
  dispatchState = {
@@ -29732,6 +30371,14 @@ var ParallAgentGateway = class {
29732
30371
  inFlightDispatches = 0;
29733
30372
  drainResolvers = [];
29734
30373
  pendingRestartNotification = null;
30374
+ laneLedger;
30375
+ // Sticky fallback: flipped when the server predates the ledger (claim
30376
+ // endpoint 404) so every subsequent dispatch uses the legacy flow.
30377
+ ledgerDisabled = false;
30378
+ // Group key of the group currently being dispatched on main — lane-aware
30379
+ // (targetId + thread), unlike mainCurrentTargetId which stays chat-level
30380
+ // for fork routing decisions.
30381
+ mainCurrentGroupKey;
29735
30382
  DISPATCHED_MESSAGES_CAP = 5e3;
29736
30383
  // SHUTDOWN_DEADLINE_MS is read by waitForDrain via the configured value
29737
30384
  // below — kept as instance state so per-runtime configs can override it
@@ -29741,6 +30388,14 @@ var ParallAgentGateway = class {
29741
30388
  DISPATCH_DEADLINE_MS;
29742
30389
  constructor(opts) {
29743
30390
  this.opts = opts;
30391
+ if (opts.dispatchContextDir) {
30392
+ this.laneLedger = new LaneLedger({
30393
+ client: opts.client,
30394
+ orgId: opts.config.org_id,
30395
+ contextDir: opts.dispatchContextDir,
30396
+ log: opts.log
30397
+ });
30398
+ }
29744
30399
  this.SHUTDOWN_DEADLINE_MS = opts.shutdownDeadlineMs ?? 6e4;
29745
30400
  this.FORK_DEADLINE_MS = opts.forkDeadlineMs ?? 2 * 60 * 6e4;
29746
30401
  this.DISPATCH_DEADLINE_MS = opts.dispatchDeadlineMs ?? 20 * 6e4;
@@ -29810,14 +30465,18 @@ var ParallAgentGateway = class {
29810
30465
  if (data.status !== "todo" && data.status !== "in_progress")
29811
30466
  return;
29812
30467
  try {
29813
- const dispatched = await this.handleTaskAssignment(data, data.id);
29814
- if (dispatched) {
30468
+ 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) => {
30469
+ if (dispatchEventId) {
30470
+ this.opts.client.ackDispatchByID(this.opts.config.org_id, dispatchEventId).catch(() => {
30471
+ });
30472
+ return;
30473
+ }
29815
30474
  this.opts.client.ackDispatch(this.opts.config.org_id, {
29816
30475
  source_type: "task_activity",
29817
30476
  source_id: data.id
29818
30477
  }).catch(() => {
29819
30478
  });
29820
- }
30479
+ });
29821
30480
  } catch (err) {
29822
30481
  this.opts.log?.error(`task dispatch failed for ${data.id}: ${String(err)}`);
29823
30482
  }
@@ -29827,11 +30486,10 @@ var ParallAgentGateway = class {
29827
30486
  if (!data.source_id || !data.task_id)
29828
30487
  return;
29829
30488
  try {
29830
- const dispatched = await this.handleTaskComment(data.source_id, data.task_id, data.actor_id, data.delivery_reason);
29831
- if (dispatched) {
30489
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.handleTaskComment(data.source_id, data.task_id ?? "", data.actor_id, data.delivery_reason), () => {
29832
30490
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
29833
30491
  });
29834
- }
30492
+ });
29835
30493
  } catch (err) {
29836
30494
  this.opts.log?.error(`task comment dispatch failed for ${data.source_id}: ${String(err)}`);
29837
30495
  }
@@ -29839,11 +30497,10 @@ var ParallAgentGateway = class {
29839
30497
  if (!data.source_id)
29840
30498
  return;
29841
30499
  try {
29842
- const dispatched = await this.handleWikiComment(data.source_id, data.actor_id, data.delivery_reason);
29843
- if (dispatched) {
30500
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.handleWikiComment(data.source_id, data.actor_id, data.delivery_reason), () => {
29844
30501
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
29845
30502
  });
29846
- }
30503
+ });
29847
30504
  } catch (err) {
29848
30505
  this.opts.log?.error(`wiki comment dispatch failed for ${data.source_id}: ${String(err)}`);
29849
30506
  }
@@ -29851,11 +30508,13 @@ var ParallAgentGateway = class {
29851
30508
  if (!data.task_id)
29852
30509
  return;
29853
30510
  try {
29854
- const dispatched = await this.handleTaskDispatch(data.task_id, data.source_id ?? data.task_id, { allowCreator: true });
29855
- if (dispatched) {
30511
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, (dispatchEventId) => this.handleTaskDispatch(data.task_id ?? "", data.source_id ?? data.task_id ?? "", {
30512
+ allowCreator: true,
30513
+ dispatchEventId
30514
+ }), () => {
29856
30515
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
29857
30516
  });
29858
- }
30517
+ });
29859
30518
  } catch (err) {
29860
30519
  this.opts.log?.error(`task update dispatch failed for ${data.task_id}: ${String(err)}`);
29861
30520
  }
@@ -29863,11 +30522,10 @@ var ParallAgentGateway = class {
29863
30522
  if (!data.source_id)
29864
30523
  return;
29865
30524
  try {
29866
- const dispatched = await this.fetchAndHandleScheduleFire(data.source_id, data.actor_id);
29867
- if (dispatched) {
30525
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.fetchAndHandleScheduleFire(data.source_id, data.actor_id), () => {
29868
30526
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
29869
30527
  });
29870
- }
30528
+ });
29871
30529
  } catch (err) {
29872
30530
  this.opts.log?.error(`schedule fire dispatch failed for ${data.source_id}: ${String(err)}`);
29873
30531
  }
@@ -29875,26 +30533,41 @@ var ParallAgentGateway = class {
29875
30533
  if (!data.source_id)
29876
30534
  return;
29877
30535
  try {
29878
- const dispatched = await this.fetchAndHandleExternalTriggerRun(data.source_id);
29879
- if (dispatched) {
30536
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.fetchAndHandleExternalTriggerRun(data.source_id), () => {
29880
30537
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
29881
30538
  });
29882
- }
30539
+ });
29883
30540
  } catch (err) {
29884
30541
  this.opts.log?.error(`external trigger dispatch failed for ${data.source_id}: ${String(err)}`);
29885
30542
  }
30543
+ } else if (data.event_type === "channel_message") {
30544
+ if (!data.source_id)
30545
+ return;
30546
+ try {
30547
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.fetchAndHandleChannelMessage(data.source_id), () => {
30548
+ this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
30549
+ });
30550
+ });
30551
+ } catch (err) {
30552
+ this.opts.log?.error(`channel message dispatch failed for ${data.source_id}: ${String(err)}`);
30553
+ }
29886
30554
  } else if (data.event_type === "approval_decided") {
29887
30555
  if (!data.source_id)
29888
30556
  return;
29889
30557
  try {
29890
- const dispatched = await this.fetchAndHandleApprovalDecided(data.source_id, data.actor_id, data.chat_id ?? null);
29891
- if (dispatched) {
30558
+ await this.consumeTypedDispatch({ dispatchEventId: data.id }, () => this.fetchAndHandleApprovalDecided(data.source_id, data.actor_id, data.chat_id ?? null), () => {
29892
30559
  this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
29893
30560
  });
29894
- }
30561
+ });
29895
30562
  } catch (err) {
29896
30563
  this.opts.log?.error(`approval decided dispatch failed for ${data.source_id}: ${String(err)}`);
29897
30564
  }
30565
+ } else if (data.event_type === "message" && this.laneLedger && data.source_id && data.chat_id) {
30566
+ try {
30567
+ await this.handleMessageRedrive(data);
30568
+ } catch (err) {
30569
+ this.opts.log?.error(`message re-drive failed for ${data.source_id}: ${String(err)}`);
30570
+ }
29898
30571
  } else if (data.event_type !== "message" && data.event_type !== "task_assign") {
29899
30572
  this.opts.log?.info(`dispatch.new with unhandled event_type=${String(data.event_type)} (id=${data.id}) \u2014 no-op`);
29900
30573
  }
@@ -29930,6 +30603,39 @@ var ParallAgentGateway = class {
29930
30603
  source_id: sourceId
29931
30604
  });
29932
30605
  }
30606
+ /** True when this event's lifecycle is owned by the dispatch lane ledger. */
30607
+ usesLaneLedger(event) {
30608
+ return this.laneLedger != null && !this.ledgerDisabled && this.laneLedger.handles(event);
30609
+ }
30610
+ disableLedger(reason) {
30611
+ if (this.ledgerDisabled)
30612
+ return;
30613
+ this.ledgerDisabled = true;
30614
+ this.opts.log?.warn(`dispatch ledger unavailable (${reason}) \u2014 falling back to legacy received/ack flow`);
30615
+ }
30616
+ /**
30617
+ * Buffer grouping key. Lane-ledger message events group by full lane
30618
+ * identity (chat + thread) so a channel lane and a thread lane in the same
30619
+ * chat dispatch as separate turns with separate claims; everything else
30620
+ * keeps the historical chat-level grouping.
30621
+ */
30622
+ dispatchGroupKey(event) {
30623
+ if (this.usesLaneLedger(event)) {
30624
+ return this.laneLedger.laneKeyFor(event);
30625
+ }
30626
+ return event.targetId;
30627
+ }
30628
+ // Lane-flow protocols live in gateway-lane-flow.ts; these thin delegates
30629
+ // keep call sites and tests on the class surface.
30630
+ laneFlowHost() {
30631
+ return this;
30632
+ }
30633
+ dispatchLaneGroup(opts) {
30634
+ return dispatchLaneGroup(this.laneFlowHost(), opts);
30635
+ }
30636
+ consumeTypedDispatch(ref, run, ack) {
30637
+ return consumeTypedDispatch(this.laneFlowHost(), ref, run, ack);
30638
+ }
29933
30639
  buildDispatchContext(event, sessionKey) {
29934
30640
  const binding = this.sessionBindings.get(sessionKey);
29935
30641
  return {
@@ -29946,6 +30652,7 @@ var ParallAgentGateway = class {
29946
30652
  noReply: event.noReply ?? false,
29947
30653
  contextFilePath: this.opts.contextFilePathForSession?.(sessionKey),
29948
30654
  stepIdFilePath: this.opts.stepIdFilePathForSession?.(sessionKey),
30655
+ contextDirPath: this.opts.dispatchContextDir,
29949
30656
  client: this.opts.client,
29950
30657
  log: this.opts.log
29951
30658
  };
@@ -29961,12 +30668,17 @@ var ParallAgentGateway = class {
29961
30668
  target_type: target.target_type,
29962
30669
  target_id: target.target_id,
29963
30670
  content: {
29964
- 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",
30671
+ 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",
29965
30672
  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" ? {
29966
30673
  trigger_id: event.targetId,
29967
30674
  run_id: event.messageId,
29968
30675
  connection_id: event.externalConnectionId,
29969
30676
  ingress_event_id: event.externalIngressEventId
30677
+ } : event.type === "channel_message" ? {
30678
+ conversation_id: event.targetId,
30679
+ channel_message_id: event.messageId,
30680
+ provider: event.channelProvider,
30681
+ external_conversation_id: event.channelExternalConversationId
29970
30682
  } : event.type === "approval" ? { approval_id: event.messageId } : { message_id: event.messageId },
29971
30683
  sender_id: event.senderId,
29972
30684
  sender_name: event.senderName,
@@ -29980,7 +30692,7 @@ var ParallAgentGateway = class {
29980
30692
  this.opts.log?.warn(`failed to create input step: ${String(err)}`);
29981
30693
  }
29982
30694
  }
29983
- async createRuntimeStep(sessionId, event, runtimeEvent, stepIdFilePath, contextFilePath) {
30695
+ async createRuntimeStep(sessionId, event, runtimeEvent, stepIdFilePath, contextFilePath, laneContextFilePath2) {
29984
30696
  const target = resolveStepTarget(event);
29985
30697
  try {
29986
30698
  switch (runtimeEvent.type) {
@@ -30026,6 +30738,9 @@ var ParallAgentGateway = class {
30026
30738
  } else if (stepIdFilePath) {
30027
30739
  this.writeStepIdFile(stepIdFilePath, step.id);
30028
30740
  }
30741
+ if (laneContextFilePath2) {
30742
+ this.updateContextFileStepId(laneContextFilePath2, step.id);
30743
+ }
30029
30744
  break;
30030
30745
  }
30031
30746
  case "tool_result":
@@ -30048,6 +30763,9 @@ var ParallAgentGateway = class {
30048
30763
  } else if (stepIdFilePath) {
30049
30764
  this.clearStepIdFile(stepIdFilePath);
30050
30765
  }
30766
+ if (laneContextFilePath2) {
30767
+ this.updateContextFileStepId(laneContextFilePath2, null);
30768
+ }
30051
30769
  break;
30052
30770
  case "error":
30053
30771
  await this.opts.client.createAgentStep(this.opts.config.org_id, this.opts.agentUserId, sessionId, {
@@ -30067,28 +30785,28 @@ var ParallAgentGateway = class {
30067
30785
  }
30068
30786
  writeContextFile(filePath, ctx) {
30069
30787
  try {
30070
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
30071
- fs.writeFileSync(filePath, JSON.stringify(ctx), "utf8");
30788
+ fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
30789
+ fs2.writeFileSync(filePath, JSON.stringify(ctx), "utf8");
30072
30790
  } catch (err) {
30073
30791
  this.opts.log?.warn(`failed to write context file ${filePath}: ${String(err)}`);
30074
30792
  }
30075
30793
  }
30076
30794
  updateContextFileStepId(filePath, stepId) {
30077
30795
  try {
30078
- const raw = fs.readFileSync(filePath, "utf8");
30796
+ const raw = fs2.readFileSync(filePath, "utf8");
30079
30797
  const ctx = JSON.parse(raw);
30080
30798
  ctx.step_id = stepId;
30081
- fs.writeFileSync(filePath, JSON.stringify(ctx), "utf8");
30799
+ fs2.writeFileSync(filePath, JSON.stringify(ctx), "utf8");
30082
30800
  } catch (err) {
30083
30801
  this.opts.log?.warn(`failed to update context file step_id ${filePath}: ${String(err)}`);
30084
30802
  }
30085
30803
  }
30086
30804
  updateContextFileSessionId(filePath, sessionId) {
30087
30805
  try {
30088
- const raw = fs.readFileSync(filePath, "utf8");
30806
+ const raw = fs2.readFileSync(filePath, "utf8");
30089
30807
  const ctx = JSON.parse(raw);
30090
30808
  ctx.session_id = sessionId;
30091
- fs.writeFileSync(filePath, JSON.stringify(ctx), "utf8");
30809
+ fs2.writeFileSync(filePath, JSON.stringify(ctx), "utf8");
30092
30810
  } catch (err) {
30093
30811
  this.opts.log?.warn(`failed to update context file session_id ${filePath}: ${String(err)}`);
30094
30812
  }
@@ -30096,8 +30814,8 @@ var ParallAgentGateway = class {
30096
30814
  /** @deprecated Use writeContextFile / updateContextFileStepId. */
30097
30815
  writeStepIdFile(filePath, stepId) {
30098
30816
  try {
30099
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
30100
- fs.writeFileSync(filePath, stepId, "utf8");
30817
+ fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
30818
+ fs2.writeFileSync(filePath, stepId, "utf8");
30101
30819
  } catch (err) {
30102
30820
  this.opts.log?.warn(`failed to write step id file ${filePath}: ${String(err)}`);
30103
30821
  }
@@ -30105,7 +30823,7 @@ var ParallAgentGateway = class {
30105
30823
  /** @deprecated Use writeContextFile / updateContextFileStepId. */
30106
30824
  clearStepIdFile(filePath) {
30107
30825
  try {
30108
- fs.writeFileSync(filePath, "", "utf8");
30826
+ fs2.writeFileSync(filePath, "", "utf8");
30109
30827
  } catch {
30110
30828
  }
30111
30829
  }
@@ -30114,7 +30832,7 @@ var ParallAgentGateway = class {
30114
30832
  await this.createInputStep(sessionId, event);
30115
30833
  }
30116
30834
  }
30117
- async bindRuntimeSession(sessionKey, runtimeEvent, contextFilePath) {
30835
+ async bindRuntimeSession(sessionKey, runtimeEvent, contextFilePath, laneContextFilePath2) {
30118
30836
  const runtimeLaneKey = runtimeEvent.runtimeLaneKey || sessionKey;
30119
30837
  const existing = this.sessionBindings.get(sessionKey);
30120
30838
  if (existing && existing.runtimeLaneKey === runtimeLaneKey && existing.runtimeSessionId === runtimeEvent.runtimeSessionId) {
@@ -30158,6 +30876,9 @@ var ParallAgentGateway = class {
30158
30876
  if (contextFilePath) {
30159
30877
  this.updateContextFileSessionId(contextFilePath, session.id);
30160
30878
  }
30879
+ if (laneContextFilePath2) {
30880
+ this.updateContextFileSessionId(laneContextFilePath2, session.id);
30881
+ }
30161
30882
  await this.opts.onSessionBinding?.(binding);
30162
30883
  return binding;
30163
30884
  }
@@ -30184,14 +30905,27 @@ var ParallAgentGateway = class {
30184
30905
  const dispatchContext = this.buildDispatchContext(event, sessionKey);
30185
30906
  const contextFilePath = dispatchContext.contextFilePath;
30186
30907
  const stepIdFilePath = dispatchContext.stepIdFilePath;
30908
+ const activeLane = this.ledgerDisabled ? void 0 : this.laneLedger?.getForEvent(event);
30909
+ const laneContextFilePath2 = activeLane ? this.laneLedger?.laneContextPath(activeLane) : void 0;
30910
+ const contextBody = {
30911
+ session_id: dispatchContext.sessionId ?? null,
30912
+ chat_id: dispatchContext.chatId ?? null,
30913
+ trigger_message_id: dispatchContext.triggerMessageId ?? null,
30914
+ no_reply: dispatchContext.noReply,
30915
+ step_id: null,
30916
+ dispatch_event_id: activeLane?.typedDispatchEventId ?? activeLane?.folded.get(event.messageId) ?? null,
30917
+ lane: activeLane?.lane ?? null,
30918
+ target_uri: activeLane?.targetUri ?? null,
30919
+ thread_root_id: activeLane?.threadRootId ?? null,
30920
+ // Typed binding hint for the CLI: which task this dispatch is about
30921
+ // (parall task update attaches the typed effect only on a match).
30922
+ task_id: event.type === "task" ? event.targetId : null
30923
+ };
30187
30924
  if (contextFilePath) {
30188
- this.writeContextFile(contextFilePath, {
30189
- session_id: dispatchContext.sessionId ?? null,
30190
- chat_id: dispatchContext.chatId ?? null,
30191
- trigger_message_id: dispatchContext.triggerMessageId ?? null,
30192
- no_reply: dispatchContext.noReply,
30193
- step_id: null
30194
- });
30925
+ this.writeContextFile(contextFilePath, contextBody);
30926
+ }
30927
+ if (laneContextFilePath2) {
30928
+ this.writeContextFile(laneContextFilePath2, contextBody);
30195
30929
  }
30196
30930
  this.inFlightDispatches++;
30197
30931
  const deadlineTimer = this.DISPATCH_DEADLINE_MS > 0 ? setTimeout(() => {
@@ -30217,7 +30951,15 @@ var ParallAgentGateway = class {
30217
30951
  context: dispatchContext
30218
30952
  })) {
30219
30953
  if (runtimeEvent.type === "runtime_session") {
30220
- binding = await this.bindRuntimeSession(sessionKey, runtimeEvent, contextFilePath);
30954
+ const priorAgentSessionId = binding?.agentSessionId;
30955
+ binding = await this.bindRuntimeSession(sessionKey, runtimeEvent, contextFilePath, laneContextFilePath2);
30956
+ if (event.targetType === "channel_conversation" && binding.agentSessionId !== priorAgentSessionId) {
30957
+ try {
30958
+ await this.opts.client.setChannelConversationSession(this.opts.config.org_id, event.targetId, binding.agentSessionId);
30959
+ } catch (err) {
30960
+ this.opts.log?.warn(`failed to record session mapping for channel conversation ${event.targetId}: ${String(err)}`);
30961
+ }
30962
+ }
30221
30963
  if (!inputStepsCreated) {
30222
30964
  if (earlierEvents.length > 0) {
30223
30965
  await this.createInputStepsForEarlierEvents(binding.agentSessionId, earlierEvents);
@@ -30242,6 +30984,9 @@ var ParallAgentGateway = class {
30242
30984
  await this.createInputStep(binding.agentSessionId, event);
30243
30985
  inputStepsCreated = true;
30244
30986
  }
30987
+ if (activeLane && !this.ledgerDisabled) {
30988
+ this.laneLedger?.maybeRenew(activeLane);
30989
+ }
30245
30990
  if (captureText && runtimeEvent.type === "text" && runtimeEvent.text) {
30246
30991
  captureText.push(runtimeEvent.text);
30247
30992
  }
@@ -30258,7 +31003,7 @@ var ParallAgentGateway = class {
30258
31003
  } else if (runtimeEvent.type === "tool_result" && pendingSendCallIds.delete(runtimeEvent.callId)) {
30259
31004
  recordMessageSend(sessionKey, !runtimeEvent.error);
30260
31005
  }
30261
- await this.createRuntimeStep(binding.agentSessionId, event, runtimeEvent, stepIdFilePath, contextFilePath);
31006
+ await this.createRuntimeStep(binding.agentSessionId, event, runtimeEvent, stepIdFilePath, contextFilePath, laneContextFilePath2);
30262
31007
  }
30263
31008
  if (!binding) {
30264
31009
  binding = this.sessionBindings.get(sessionKey);
@@ -30280,7 +31025,7 @@ var ParallAgentGateway = class {
30280
31025
  await this.createRuntimeStep(binding.agentSessionId, event, {
30281
31026
  type: "error",
30282
31027
  message: `Dispatch failed: ${String(err)}`
30283
- }, stepIdFilePath, contextFilePath);
31028
+ }, stepIdFilePath, contextFilePath, laneContextFilePath2);
30284
31029
  } catch (stepErr) {
30285
31030
  if (this.isSessionNotLiveError(stepErr))
30286
31031
  staleDetected = true;
@@ -30323,6 +31068,9 @@ var ParallAgentGateway = class {
30323
31068
  } else if (stepIdFilePath) {
30324
31069
  this.clearStepIdFile(stepIdFilePath);
30325
31070
  }
31071
+ if (laneContextFilePath2) {
31072
+ this.updateContextFileStepId(laneContextFilePath2, null);
31073
+ }
30326
31074
  this.inFlightDispatches--;
30327
31075
  if (this.inFlightDispatches === 0 && this.drainResolvers.length > 0) {
30328
31076
  const resolvers = this.drainResolvers.splice(0);
@@ -30380,13 +31128,39 @@ var ParallAgentGateway = class {
30380
31128
  item.resolve(false);
30381
31129
  break;
30382
31130
  }
30383
- const items = fork.queue.splice(0);
31131
+ let items;
31132
+ const head = fork.queue[0];
31133
+ if (head && this.usesLaneLedger(head.event)) {
31134
+ const headKey = this.dispatchGroupKey(head.event);
31135
+ const splitAt = fork.queue.findIndex((it) => this.dispatchGroupKey(it.event) !== headKey);
31136
+ items = splitAt === -1 ? fork.queue.splice(0) : fork.queue.splice(0, splitAt);
31137
+ } else {
31138
+ items = fork.queue.splice(0);
31139
+ }
30384
31140
  const events = items.map((item) => item.event);
30385
31141
  const last = events[events.length - 1];
30386
31142
  const earlier = events.slice(0, -1);
30387
31143
  try {
30388
31144
  const batchText = [];
30389
- const dispatched = await this.runDispatch(last, fork.fork.sessionKey, buildForkScopePrefix(last) + buildEventBody(last), earlier, batchText);
31145
+ let dispatched;
31146
+ if (this.usesLaneLedger(last)) {
31147
+ const outcome = await this.dispatchLaneGroup({
31148
+ events,
31149
+ sessionKey: fork.fork.sessionKey,
31150
+ body: buildForkScopePrefix(last) + buildEventBody(last),
31151
+ earlier,
31152
+ captureText: batchText,
31153
+ hasMoreLocal: () => fork.queue.length > 0
31154
+ });
31155
+ if (outcome === "foreign") {
31156
+ for (const item of items)
31157
+ item.resolve(false);
31158
+ break;
31159
+ }
31160
+ dispatched = outcome === "dispatched";
31161
+ } else {
31162
+ dispatched = await this.runDispatch(last, fork.fork.sessionKey, buildForkScopePrefix(last) + buildEventBody(last), earlier, batchText);
31163
+ }
30390
31164
  if (!dispatched) {
30391
31165
  for (const item of items) {
30392
31166
  item.resolve(false);
@@ -30489,9 +31263,9 @@ var ParallAgentGateway = class {
30489
31263
  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`);
30490
31264
  break;
30491
31265
  }
30492
- const targetId = this.dispatchState.mainBuffer[0].targetId;
31266
+ const groupKey = this.dispatchGroupKey(this.dispatchState.mainBuffer[0]);
30493
31267
  const events = [];
30494
- while (this.dispatchState.mainBuffer[0]?.targetId === targetId) {
31268
+ while (this.dispatchState.mainBuffer[0] && this.dispatchGroupKey(this.dispatchState.mainBuffer[0]) === groupKey) {
30495
31269
  events.push(this.dispatchState.mainBuffer.shift());
30496
31270
  }
30497
31271
  const event = events[events.length - 1];
@@ -30500,7 +31274,36 @@ var ParallAgentGateway = class {
30500
31274
  const pendingFork = hasPendingInjections ? [] : this.dispatchState.pendingForkResults.splice(0);
30501
31275
  const forkPrefix = buildForkResultPrefix(pendingFork);
30502
31276
  this.dispatchState.mainCurrentTargetId = event.targetId;
31277
+ this.mainCurrentGroupKey = groupKey;
30503
31278
  this.dispatchState.mainPreDispatchBranchPoint = this.opts.dispatchAdapter.getBranchPoint?.(this.opts.runtimeKey);
31279
+ if (this.usesLaneLedger(event)) {
31280
+ let outcome;
31281
+ try {
31282
+ outcome = await this.dispatchLaneGroup({
31283
+ events,
31284
+ sessionKey: this.opts.runtimeKey,
31285
+ body: forkPrefix + buildEventBody(event),
31286
+ earlier,
31287
+ hasMoreLocal: () => this.dispatchState.mainBuffer.some((e) => this.dispatchGroupKey(e) === groupKey)
31288
+ });
31289
+ } catch (err) {
31290
+ this.opts.log?.error(`lane dispatch failed for ${event.messageId}: ${String(err)}`);
31291
+ this.dispatchState.pendingForkResults.unshift(...pendingFork);
31292
+ for (const ev of events)
31293
+ this.dispatchedMessages.delete(ev.messageId);
31294
+ continue;
31295
+ }
31296
+ if (outcome === "shutdown") {
31297
+ this.dispatchState.mainBuffer.unshift(...events);
31298
+ this.dispatchState.pendingForkResults.unshift(...pendingFork);
31299
+ break;
31300
+ }
31301
+ if (outcome === "foreign") {
31302
+ this.dispatchState.pendingForkResults.unshift(...pendingFork);
31303
+ continue;
31304
+ }
31305
+ continue;
31306
+ }
30504
31307
  try {
30505
31308
  await this.emitDispatchReceived(event);
30506
31309
  } catch (err) {
@@ -30529,6 +31332,7 @@ var ParallAgentGateway = class {
30529
31332
  this.draining = false;
30530
31333
  this.dispatchState.mainDispatching = false;
30531
31334
  this.dispatchState.mainCurrentTargetId = void 0;
31335
+ this.mainCurrentGroupKey = void 0;
30532
31336
  this.dispatchState.mainPreDispatchBranchPoint = void 0;
30533
31337
  if (!this.shuttingDown && this.dispatchState.mainBuffer.length > 0) {
30534
31338
  setTimeout(() => {
@@ -30548,13 +31352,38 @@ var ParallAgentGateway = class {
30548
31352
  const forkPrefix = buildForkResultPrefix(pendingFork);
30549
31353
  this.dispatchState.mainDispatching = true;
30550
31354
  this.dispatchState.mainCurrentTargetId = event.targetId;
31355
+ this.mainCurrentGroupKey = this.dispatchGroupKey(event);
30551
31356
  this.dispatchState.mainPreDispatchBranchPoint = this.opts.dispatchAdapter.getBranchPoint?.(this.opts.runtimeKey);
31357
+ if (this.usesLaneLedger(event)) {
31358
+ let outcome = "shutdown";
31359
+ try {
31360
+ try {
31361
+ outcome = await this.dispatchLaneGroup({
31362
+ events: [event],
31363
+ sessionKey: this.opts.runtimeKey,
31364
+ body: forkPrefix + buildEventBody(event),
31365
+ earlier: [],
31366
+ hasMoreLocal: () => this.dispatchState.mainBuffer.some((e) => this.dispatchGroupKey(e) === this.dispatchGroupKey(event))
31367
+ });
31368
+ } catch (err) {
31369
+ this.dispatchState.pendingForkResults.unshift(...pendingFork);
31370
+ throw err;
31371
+ }
31372
+ if (outcome !== "dispatched") {
31373
+ this.dispatchState.pendingForkResults.unshift(...pendingFork);
31374
+ }
31375
+ } finally {
31376
+ await this.drainMainBuffer();
31377
+ }
31378
+ return outcome === "dispatched";
31379
+ }
30552
31380
  try {
30553
31381
  await this.emitDispatchReceived(event);
30554
31382
  } catch (err) {
30555
31383
  this.opts.log?.warn?.(`mark-received failed, leaving unacked for retry: ${String(err)}`);
30556
31384
  this.dispatchState.mainDispatching = false;
30557
31385
  this.dispatchState.mainCurrentTargetId = void 0;
31386
+ this.mainCurrentGroupKey = void 0;
30558
31387
  this.dispatchState.mainPreDispatchBranchPoint = void 0;
30559
31388
  this.dispatchState.pendingForkResults.unshift(...pendingFork);
30560
31389
  return false;
@@ -30575,7 +31404,11 @@ var ParallAgentGateway = class {
30575
31404
  return false;
30576
31405
  }
30577
31406
  this.dispatchState.mainBuffer.push(event);
30578
- if (this.dispatchState.mainCurrentTargetId === event.targetId && await this.opts.dispatchAdapter.enqueueDuringDispatch?.(this.opts.runtimeKey, buildEventBody(event))) {
31407
+ if (this.usesLaneLedger(event)) {
31408
+ if (this.mainCurrentGroupKey === this.dispatchGroupKey(event) && await this.laneLedger?.steerLive(event) && await this.opts.dispatchAdapter.enqueueDuringDispatch?.(this.opts.runtimeKey, buildEventBody(event))) {
31409
+ this.opts.log?.info(`steer folded+injected for ${event.messageId} (will drain for bookkeeping)`);
31410
+ }
31411
+ } else if (this.dispatchState.mainCurrentTargetId === event.targetId && await this.opts.dispatchAdapter.enqueueDuringDispatch?.(this.opts.runtimeKey, buildEventBody(event))) {
30579
31412
  this.opts.log?.info(`steer injected for ${event.messageId} (will drain for bookkeeping)`);
30580
31413
  }
30581
31414
  if (!this.dispatchState.mainDispatching && !this.draining && this.dispatchState.mainBuffer.length > 0) {
@@ -30746,8 +31579,10 @@ var ParallAgentGateway = class {
30746
31579
  try {
30747
31580
  const dispatched = await this.handleInboundEvent(event);
30748
31581
  if (dispatched) {
30749
- this.opts.client.ackDispatch(this.opts.config.org_id, { source_type: "message", source_id: data.id }).catch(() => {
30750
- });
31582
+ if (!this.usesLaneLedger(event)) {
31583
+ this.opts.client.ackDispatch(this.opts.config.org_id, { source_type: "message", source_id: data.id }).catch(() => {
31584
+ });
31585
+ }
30751
31586
  } else {
30752
31587
  this.dispatchedMessages.delete(data.id);
30753
31588
  }
@@ -30756,7 +31591,21 @@ var ParallAgentGateway = class {
30756
31591
  this.dispatchedMessages.delete(data.id);
30757
31592
  }
30758
31593
  }
30759
- async handleTaskAssignment(task, ackSourceId) {
31594
+ // Ledger re-drive consumption: dispatch.new message hints re-enter the
31595
+ // shared WorkItem consumption path (same protocol as catch-up).
31596
+ async handleMessageRedrive(item) {
31597
+ if (!item.chat_id || !item.source_id)
31598
+ return;
31599
+ await this.consumeMessageWorkItem({
31600
+ id: item.id,
31601
+ source_id: item.source_id,
31602
+ chat_id: item.chat_id
31603
+ });
31604
+ }
31605
+ consumeMessageWorkItem(item) {
31606
+ return consumeMessageWorkItem(this.laneFlowHost(), item);
31607
+ }
31608
+ async handleTaskAssignment(task, ackSourceId, dispatchEventId) {
30760
31609
  if (this.shuttingDown)
30761
31610
  return false;
30762
31611
  const dedupeKey = `${task.id}:${task.updated_at}`;
@@ -30785,7 +31634,8 @@ var ParallAgentGateway = class {
30785
31634
  body: parts.join("\n"),
30786
31635
  sentAt: task.updated_at ?? task.created_at,
30787
31636
  ackSourceType: "task_activity",
30788
- ackSourceId
31637
+ ackSourceId,
31638
+ dispatchEventId
30789
31639
  };
30790
31640
  const dispatched = await this.handleInboundEvent(event);
30791
31641
  if (!dispatched) {
@@ -30809,7 +31659,7 @@ var ParallAgentGateway = class {
30809
31659
  this.opts.log?.info(`skipping stale task dispatch ${ackSourceId ?? taskId} \u2014 assigned to ${task.assignee_id}, creator ${task.creator_id}`);
30810
31660
  return true;
30811
31661
  }
30812
- return this.handleTaskAssignment(task, ackSourceId);
31662
+ return this.handleTaskAssignment(task, ackSourceId, opts.dispatchEventId);
30813
31663
  }
30814
31664
  async handleTaskComment(commentId, taskId, actorId, deliveryReason) {
30815
31665
  if (this.shuttingDown)
@@ -31020,6 +31870,75 @@ var ParallAgentGateway = class {
31020
31870
  return true;
31021
31871
  return this.handleExternalTriggerRun(run);
31022
31872
  }
31873
+ // fetchAndHandleChannelMessage resolves a channel_message dispatch to its
31874
+ // durable ChannelMessage + conversation and hands it to the inbound
31875
+ // pipeline. targetId = the ChannelConversation id, so per-conversation
31876
+ // multi-turn continuity rides the same per-target session mechanics as
31877
+ // chats. Design: docs/engineering-design/external-im-channel-design.md.
31878
+ async fetchAndHandleChannelMessage(messageId) {
31879
+ if (this.shuttingDown)
31880
+ return false;
31881
+ const claimKey = `channel_message:${messageId}`;
31882
+ if (!this.tryClaimMessage(claimKey))
31883
+ return false;
31884
+ let msg = null;
31885
+ let conv = null;
31886
+ try {
31887
+ msg = await this.opts.client.getChannelMessage(this.opts.config.org_id, messageId);
31888
+ conv = await this.opts.client.getChannelConversation(this.opts.config.org_id, msg.conversation_id);
31889
+ } catch (err) {
31890
+ const status = err?.status;
31891
+ if (status === 404) {
31892
+ this.opts.log?.warn(`channel message ${messageId} not accessible (404), acking stale dispatch`);
31893
+ return true;
31894
+ }
31895
+ this.dispatchedMessages.delete(claimKey);
31896
+ this.opts.log?.warn(`channel message fetch failed for ${messageId}, leaving pending: ${String(err)}`);
31897
+ return false;
31898
+ }
31899
+ if (!msg || !conv) {
31900
+ return true;
31901
+ }
31902
+ this.opts.log?.info(`channel message: ${msg.id} (conversation ${conv.id})`);
31903
+ let provider = this.channelConnectionProviders.get(conv.connection_id);
31904
+ if (!provider) {
31905
+ try {
31906
+ const connection = await this.opts.client.getChannelConnection(this.opts.config.org_id, conv.connection_id);
31907
+ provider = connection.provider;
31908
+ this.channelConnectionProviders.set(conv.connection_id, provider);
31909
+ } catch {
31910
+ provider = void 0;
31911
+ }
31912
+ }
31913
+ const event = {
31914
+ type: "channel_message",
31915
+ targetId: conv.id,
31916
+ targetName: conv.external_user_name || conv.external_conversation_id,
31917
+ targetType: "channel_conversation",
31918
+ senderId: msg.external_user_id || "external",
31919
+ senderName: msg.external_user_name || msg.external_user_id || "external user",
31920
+ messageId: msg.id,
31921
+ body: msg.text,
31922
+ sentAt: msg.received_at,
31923
+ channelProvider: provider,
31924
+ channelConversationType: conv.conversation_type || void 0,
31925
+ channelExternalConversationId: conv.external_conversation_id,
31926
+ channelExternalMessageId: msg.external_message_id,
31927
+ ackSourceType: "channel_message",
31928
+ ackSourceId: msg.id
31929
+ };
31930
+ let dispatched;
31931
+ try {
31932
+ dispatched = await this.handleInboundEvent(event);
31933
+ } catch (err) {
31934
+ this.dispatchedMessages.delete(claimKey);
31935
+ throw err;
31936
+ }
31937
+ if (!dispatched) {
31938
+ this.dispatchedMessages.delete(claimKey);
31939
+ }
31940
+ return dispatched;
31941
+ }
31023
31942
  async handleExternalTriggerRun(run) {
31024
31943
  if (this.shuttingDown)
31025
31944
  return false;
@@ -31148,72 +32067,46 @@ var ParallAgentGateway = class {
31148
32067
  }
31149
32068
  processed++;
31150
32069
  try {
31151
- let dispatched = false;
32070
+ const ackItem = () => {
32071
+ this.opts.client.ackDispatchByID(this.opts.config.org_id, item.id).catch(() => {
32072
+ });
32073
+ };
31152
32074
  if (item.event_type === "task_assign" && item.task_id) {
31153
32075
  try {
31154
- dispatched = await this.handleTaskDispatch(item.task_id, item.source_id ?? item.task_id);
32076
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, (dispatchEventId) => this.handleTaskDispatch(item.task_id ?? "", item.source_id ?? item.task_id ?? "", {
32077
+ dispatchEventId
32078
+ }), ackItem);
31155
32079
  } catch (err) {
31156
32080
  this.opts.log?.warn(`catch-up task fetch failed for ${item.task_id}, leaving pending: ${String(err)}`);
31157
32081
  continue;
31158
32082
  }
31159
32083
  } else if (item.event_type === "task_update" && item.task_id) {
31160
32084
  try {
31161
- dispatched = await this.handleTaskDispatch(item.task_id, item.source_id ?? item.task_id, { allowCreator: true });
32085
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, (dispatchEventId) => this.handleTaskDispatch(item.task_id ?? "", item.source_id ?? item.task_id ?? "", {
32086
+ allowCreator: true,
32087
+ dispatchEventId
32088
+ }), ackItem);
31162
32089
  } catch (err) {
31163
32090
  this.opts.log?.warn(`catch-up task fetch failed for ${item.task_id}, leaving pending: ${String(err)}`);
31164
32091
  continue;
31165
32092
  }
31166
32093
  } else if (item.event_type === "task_comment" && item.source_id && item.task_id) {
31167
- dispatched = await this.handleTaskComment(item.source_id, item.task_id, item.actor_id, item.delivery_reason);
32094
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.handleTaskComment(item.source_id, item.task_id ?? "", item.actor_id, item.delivery_reason), ackItem);
31168
32095
  } else if (item.event_type === "wiki_comment" && item.source_id) {
31169
- dispatched = await this.handleWikiComment(item.source_id, item.actor_id, item.delivery_reason);
32096
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.handleWikiComment(item.source_id, item.actor_id, item.delivery_reason), ackItem);
31170
32097
  } else if (item.event_type === "schedule.fire" && item.source_id) {
31171
- dispatched = await this.fetchAndHandleScheduleFire(item.source_id, item.actor_id);
32098
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.fetchAndHandleScheduleFire(item.source_id, item.actor_id), ackItem);
31172
32099
  } else if (item.event_type === "external_trigger" && item.source_id) {
31173
- dispatched = await this.fetchAndHandleExternalTriggerRun(item.source_id);
32100
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.fetchAndHandleExternalTriggerRun(item.source_id), ackItem);
32101
+ } else if (item.event_type === "channel_message" && item.source_id) {
32102
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.fetchAndHandleChannelMessage(item.source_id), ackItem);
31174
32103
  } else if (item.event_type === "approval_decided" && item.source_id) {
31175
- dispatched = await this.fetchAndHandleApprovalDecided(item.source_id, item.actor_id, item.chat_id ?? null);
32104
+ await this.consumeTypedDispatch({ dispatchEventId: item.id }, () => this.fetchAndHandleApprovalDecided(item.source_id, item.actor_id, item.chat_id ?? null), ackItem);
31176
32105
  } else if (item.event_type === "message" && item.source_id && item.chat_id) {
31177
- if (!this.tryClaimMessage(item.source_id))
31178
- continue;
31179
- let msg = null;
31180
- let msgFetchFailed = false;
31181
- try {
31182
- msg = await this.opts.client.getMessage(item.source_id);
31183
- } catch (err) {
31184
- const status = err?.status;
31185
- if (status === 404) {
31186
- msg = null;
31187
- } else {
31188
- msgFetchFailed = true;
31189
- this.opts.log?.warn(`catch-up message fetch failed for ${item.source_id}, leaving pending: ${String(err)}`);
31190
- }
31191
- }
31192
- if (msgFetchFailed) {
31193
- this.dispatchedMessages.delete(item.source_id);
31194
- continue;
31195
- }
31196
- if (!msg || msg.sender_id === this.opts.agentUserId) {
31197
- this.dispatchedMessages.delete(item.source_id);
31198
- this.opts.client.ackDispatchByID(this.opts.config.org_id, item.id).catch(() => {
31199
- });
31200
- continue;
31201
- }
31202
- const decision = await this.buildMessageDispatchDecision(item.chat_id, msg);
31203
- if (decision.action === "retry") {
31204
- this.dispatchedMessages.delete(item.source_id);
31205
- continue;
31206
- }
31207
- if (decision.action === "skip") {
31208
- this.dispatchedMessages.delete(item.source_id);
31209
- this.opts.client.ackDispatchByID(this.opts.config.org_id, item.id).catch(() => {
31210
- });
31211
- continue;
31212
- }
31213
- dispatched = await this.handleInboundEvent(decision.event);
31214
- }
31215
- if (dispatched) {
31216
- this.opts.client.ackDispatchByID(this.opts.config.org_id, item.id).catch(() => {
32106
+ await this.consumeMessageWorkItem({
32107
+ id: item.id,
32108
+ source_id: item.source_id,
32109
+ chat_id: item.chat_id
31217
32110
  });
31218
32111
  }
31219
32112
  } catch (err) {
@@ -31279,6 +32172,14 @@ ${fullSummary}` : fullSummary;
31279
32172
  this.abortFork(targetId, "ws reconnect");
31280
32173
  }
31281
32174
  }
32175
+ if (this.laneLedger && this.inFlightDispatches === 0 && this.laneLedger.activeCount > 0) {
32176
+ log2?.info(`releasing ${this.laneLedger.activeCount} stale lane(s) on reconnect`);
32177
+ await this.laneLedger.releaseAll();
32178
+ }
32179
+ if (this.laneLedger && this.ledgerDisabled) {
32180
+ log2?.info("re-probing dispatch ledger after reconnect (was disabled)");
32181
+ this.ledgerDisabled = false;
32182
+ }
31282
32183
  const intervalSec = data.heartbeat_interval > 0 ? data.heartbeat_interval : 30;
31283
32184
  try {
31284
32185
  const count = await fetchAllChats(client, config.org_id, this.chatInfoMap);
@@ -31349,6 +32250,10 @@ ${fullSummary}` : fullSummary;
31349
32250
  }
31350
32251
  if (this.heartbeatTimer)
31351
32252
  clearInterval(this.heartbeatTimer);
32253
+ if (this.laneLedger && this.laneLedger.activeCount > 0) {
32254
+ this.opts.log?.info(`releasing ${this.laneLedger.activeCount} lane(s) on shutdown`);
32255
+ await this.laneLedger.releaseAll();
32256
+ }
31352
32257
  await this.opts.onBeforeDisconnect?.();
31353
32258
  this.opts.ws.disconnect();
31354
32259
  this.opts.log?.info(`disconnected`);
@@ -31356,8 +32261,8 @@ ${fullSummary}` : fullSummary;
31356
32261
  };
31357
32262
 
31358
32263
  // ts/agent-core/dist/platform-config.js
31359
- import * as fs2 from "node:fs";
31360
- import * as path2 from "node:path";
32264
+ import * as fs3 from "node:fs";
32265
+ import * as path3 from "node:path";
31361
32266
  function isPlatformManagedProfile(profile) {
31362
32267
  return profile?.machine_id != null || profile?.model_management === "platform";
31363
32268
  }
@@ -31368,14 +32273,17 @@ function resolveRuntimeModel(isOperatorOverride, platformModel, configModel) {
31368
32273
  const operatorOverride = isOperatorOverride ? platformModel : null;
31369
32274
  return operatorOverride ?? configModel ?? platformModel ?? void 0;
31370
32275
  }
32276
+ function deriveModelIsPin(defaults, profile) {
32277
+ return defaults.modelIsPin !== void 0 ? defaults.modelIsPin : isPlatformModelOverride(profile);
32278
+ }
31371
32279
  var CACHE_FILENAME = "parall-platform-config.json";
31372
32280
  var SUPPORTED_SCHEMA_VERSION = 1;
31373
32281
  function cachePath(stateDir) {
31374
- return path2.join(stateDir, CACHE_FILENAME);
32282
+ return path3.join(stateDir, CACHE_FILENAME);
31375
32283
  }
31376
32284
  function loadCache(stateDir) {
31377
32285
  try {
31378
- const raw = fs2.readFileSync(cachePath(stateDir), "utf-8");
32286
+ const raw = fs3.readFileSync(cachePath(stateDir), "utf-8");
31379
32287
  return JSON.parse(raw);
31380
32288
  } catch {
31381
32289
  return null;
@@ -31385,33 +32293,38 @@ function saveCache(stateDir, response) {
31385
32293
  const cached = {
31386
32294
  version: response.version,
31387
32295
  config: response.config,
32296
+ modelIsPin: response.model_is_pin,
31388
32297
  fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
31389
32298
  };
31390
32299
  const filePath = cachePath(stateDir);
31391
32300
  const tmpPath = `${filePath}.tmp`;
31392
- fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
31393
- fs2.writeFileSync(tmpPath, JSON.stringify(cached, null, 2), "utf-8");
31394
- fs2.renameSync(tmpPath, filePath);
32301
+ fs3.mkdirSync(path3.dirname(filePath), { recursive: true });
32302
+ fs3.writeFileSync(tmpPath, JSON.stringify(cached, null, 2), "utf-8");
32303
+ fs3.renameSync(tmpPath, filePath);
31395
32304
  }
31396
32305
  function runtimeModelName(canonicalModel, runtimeType, config) {
31397
32306
  const runtime = runtimeType?.trim();
31398
- if (!runtime || runtime === "openclaw")
32307
+ if (!runtime || runtime === "openclaw" || runtime === "hermes")
31399
32308
  return canonicalModel;
31400
32309
  const models = config.models ?? {};
31401
32310
  const providers = models.providers ?? {};
31402
32311
  const parall = providers.parall ?? {};
31403
32312
  const catalog = Array.isArray(parall.models) ? parall.models : [];
31404
32313
  const match = catalog.find((model) => model.id === canonicalModel);
31405
- const runtimeNames = match?.runtime_names ?? {};
32314
+ if (!match)
32315
+ return null;
32316
+ const runtimeNames = match.runtime_names ?? {};
31406
32317
  const runtimeName = runtimeNames[runtime];
31407
- return typeof runtimeName === "string" && runtimeName ? runtimeName : null;
32318
+ if (typeof runtimeName === "string" && runtimeName)
32319
+ return runtimeName;
32320
+ return canonicalModel;
31408
32321
  }
31409
32322
  function extractDefaults(config, runtimeType) {
31410
32323
  const agents = config.agents ?? {};
31411
32324
  const defaults = agents.defaults ?? {};
31412
32325
  let model = null;
31413
32326
  if (typeof defaults.model === "string" && defaults.model) {
31414
- const canonicalModel = defaults.model.replace(/^parall\//, "");
32327
+ const canonicalModel = defaults.model.replace(/^parall-anthropic\//, "").replace(/^parall\//, "");
31415
32328
  model = runtimeModelName(canonicalModel, runtimeType, config);
31416
32329
  }
31417
32330
  let thinkingEffort = null;
@@ -31423,13 +32336,20 @@ function extractDefaults(config, runtimeType) {
31423
32336
  function createPlatformConfigManager(opts) {
31424
32337
  const { client, stateDir, runtimeType, log: log2 } = opts;
31425
32338
  let cachedVersion;
31426
- let currentDefaults = { model: null, thinkingEffort: null };
32339
+ let currentDefaults = {
32340
+ model: null,
32341
+ thinkingEffort: null,
32342
+ modelIsPin: void 0
32343
+ };
31427
32344
  let currentRawConfig = null;
31428
32345
  const cached = loadCache(stateDir);
31429
32346
  if (cached) {
31430
- cachedVersion = cached.version;
32347
+ cachedVersion = cached.modelIsPin === void 0 ? void 0 : cached.version;
31431
32348
  currentRawConfig = cached.config;
31432
- currentDefaults = extractDefaults(cached.config, runtimeType);
32349
+ currentDefaults = {
32350
+ ...extractDefaults(cached.config, runtimeType),
32351
+ modelIsPin: cached.modelIsPin
32352
+ };
31433
32353
  }
31434
32354
  return {
31435
32355
  async fetch() {
@@ -31455,7 +32375,10 @@ function createPlatformConfigManager(opts) {
31455
32375
  saveCache(stateDir, fresh);
31456
32376
  cachedVersion = fresh.version;
31457
32377
  currentRawConfig = fresh.config;
31458
- currentDefaults = extractDefaults(fresh.config, runtimeType);
32378
+ currentDefaults = {
32379
+ ...extractDefaults(fresh.config, runtimeType),
32380
+ modelIsPin: fresh.model_is_pin
32381
+ };
31459
32382
  return currentDefaults;
31460
32383
  },
31461
32384
  current() {
@@ -31468,8 +32391,8 @@ function createPlatformConfigManager(opts) {
31468
32391
  }
31469
32392
 
31470
32393
  // ts/agent-core/dist/skills/index.js
31471
- import * as fs3 from "node:fs";
31472
- import * as path3 from "node:path";
32394
+ import * as fs4 from "node:fs";
32395
+ import * as path4 from "node:path";
31473
32396
 
31474
32397
  // ts/agent-core/dist/skills/parall-platform.js
31475
32398
  var PARALL_PLATFORM_SKILL = `# Parall Platform
@@ -31572,16 +32495,35 @@ original discussion and its approval/rejection IS the precedent.
31572
32495
 
31573
32496
  Each \`[Event: message.new]\` includes \`[Chat: ... (prll://cht_xxx)]\` \u2014 use that chat URI to reply.
31574
32497
 
32498
+ > **How you pass the message body matters \u2014 your command runs through a shell.**
32499
+ > Inside double quotes the shell expands \`$\`, backticks, and \`$(...)\` *before*
32500
+ > the CLI sees them: \`--text "That costs $1,000"\` sends \`That costs ,000\`, and
32501
+ > \`--text "$(cmd)"\` runs \`cmd\`. Single quotes instead break on apostrophes
32502
+ > (\`I'm\`, \`don't\`). So do **not** wrap real message content in quotes \u2014 pass it
32503
+ > through \`--text-file\` (a written file, or a quoted heredoc \`<<'EOF'\` that
32504
+ > disables all expansion). Reserve \`--text "..."\` for short literals with no
32505
+ > \`$\`, backtick, or apostrophe.
32506
+
31575
32507
  \`\`\`bash
31576
- # Reply to a chat (use the chat URI from the event)
31577
- parall messages send prll://cht_xxx --text "Your reply"
32508
+ # One-off reply \u2192 quoted heredoc into stdin. The quoted delimiter <<'EOF'
32509
+ # disables ALL shell expansion, so $, backticks and apostrophes pass verbatim.
32510
+ parall messages send prll://cht_xxx --text-file - <<'PARALL_EOF'
32511
+ Sure \u2014 that's $1,000, and $(whoami) stays literal. I'm on it.
32512
+ PARALL_EOF
31578
32513
 
31579
- # Direct message by user URI or display name
31580
- parall dm prll://usr_xxx --text "Hello"
32514
+ # Longer / multi-line reply \u2192 write it with your file tool (no shell touches
32515
+ # the body), then point --text-file at the file.
32516
+ parall messages send prll://cht_xxx --text-file /tmp/reply.md
32517
+
32518
+ # Short literal with no $, backtick, or apostrophe \u2192 --text is fine.
32519
+ parall messages send prll://cht_xxx --text "On it"
32520
+
32521
+ # Direct message by user URI or display name (same --text-file / heredoc rules)
32522
+ parall dm prll://usr_xxx --text-file /tmp/reply.md
31581
32523
  parall dm "Alice" --text "Hello"
31582
32524
 
31583
32525
  # Thread reply
31584
- parall messages send prll://cht_xxx --text "Reply" --thread-root-id 01JWC...
32526
+ parall messages send prll://cht_xxx --text-file /tmp/reply.md --thread-root-id 01JWC...
31585
32527
 
31586
32528
  # FYI message (no response expected \u2014 the recipient sees \`[Hint: no_reply]\`)
31587
32529
  parall messages send prll://cht_xxx --text "FYI: done" --no-reply
@@ -31613,7 +32555,9 @@ parall messages send prll://cht_xxx --attachment att_xxx --text "See attached"
31613
32555
  parall dm "Alice" --file /tmp/report.pdf --text "Report attached"
31614
32556
  \`\`\`
31615
32557
 
31616
- \`--file\` and \`--attachment\` are mutually exclusive. \`--text\` can be combined with either.
32558
+ \`--file\` and \`--attachment\` are mutually exclusive. A caption (\`--text\` for
32559
+ short literals, or \`--text-file\` for anything with \`$\`, backticks, or quotes)
32560
+ can be combined with either.
31617
32561
 
31618
32562
  ## Approvals
31619
32563
 
@@ -32221,13 +33165,13 @@ var SKILLS = [
32221
33165
  }
32222
33166
  ];
32223
33167
  function writeSkillFiles(targetDir) {
32224
- fs3.mkdirSync(targetDir, { recursive: true });
33168
+ fs4.mkdirSync(targetDir, { recursive: true });
32225
33169
  for (const skill of SKILLS) {
32226
- fs3.writeFileSync(path3.join(targetDir, `${skill.name}.md`), skill.content, "utf8");
33170
+ fs4.writeFileSync(path4.join(targetDir, `${skill.name}.md`), skill.content, "utf8");
32227
33171
  }
32228
33172
  }
32229
33173
  function buildSkillReferences(workspaceDir) {
32230
- const dir = path3.join(workspaceDir, ".parall", "skills");
33174
+ const dir = path4.join(workspaceDir, ".parall", "skills");
32231
33175
  const lines = SKILLS.map((s) => `- ${s.description.split(":")[0]}: \`${dir}/${s.name}.md\``);
32232
33176
  return `## Platform Skills (read on demand)
32233
33177
 
@@ -32237,7 +33181,7 @@ ${lines.join("\n")}
32237
33181
 
32238
33182
  // ts/claude-agent/dist/config.js
32239
33183
  import * as os2 from "node:os";
32240
- import * as path4 from "node:path";
33184
+ import * as path5 from "node:path";
32241
33185
  function requireEnv(env, name) {
32242
33186
  const value = env[name]?.trim();
32243
33187
  if (!value) {
@@ -32251,15 +33195,15 @@ function parseList(value) {
32251
33195
  return value.split(/[,\n]/).map((item) => item.trim()).filter(Boolean);
32252
33196
  }
32253
33197
  function resolvePath(value) {
32254
- return path4.isAbsolute(value) ? value : path4.resolve(process.cwd(), value);
33198
+ return path5.isAbsolute(value) ? value : path5.resolve(process.cwd(), value);
32255
33199
  }
32256
33200
  function resolveClaudeAgentConfig(env = process.env) {
32257
33201
  const apiUrl = requireEnv(env, "PRLL_API_URL");
32258
33202
  const apiKey = requireEnv(env, "PRLL_API_KEY");
32259
33203
  const orgId = requireEnv(env, "PRLL_ORG_ID");
32260
33204
  const claudeHome = resolvePath(env.PRLL_CLAUDE_HOME?.trim() || env.HOME || os2.homedir());
32261
- const stateDir = resolvePath(env.PRLL_STATE_DIR?.trim() || path4.join(claudeHome, ".parall-agent"));
32262
- const workspaceDir = resolvePath(env.PRLL_WORKSPACE_DIR?.trim() || path4.join(stateDir, "workspace"));
33205
+ const stateDir = resolvePath(env.PRLL_STATE_DIR?.trim() || path5.join(claudeHome, ".parall-agent"));
33206
+ const workspaceDir = resolvePath(env.PRLL_WORKSPACE_DIR?.trim() || path5.join(stateDir, "workspace"));
32263
33207
  const additionalDirs = parseList(env.PRLL_CLAUDE_ADD_DIRS).map(resolvePath);
32264
33208
  return {
32265
33209
  apiUrl,
@@ -32294,20 +33238,23 @@ function buildClaudeRuntimeKey(agentUserId) {
32294
33238
  }
32295
33239
  function sessionStateFilePathForRuntime(stateDir, runtimeKey) {
32296
33240
  const fileName = Buffer.from(runtimeKey).toString("base64url");
32297
- return path4.join(stateDir, "sessions", `${fileName}.json`);
33241
+ return path5.join(stateDir, "sessions", `${fileName}.json`);
32298
33242
  }
32299
33243
  function contextFilePathForSession(stateDir, sessionKey) {
32300
33244
  const fileName = Buffer.from(sessionKey).toString("base64url");
32301
- return path4.join(stateDir, "dispatch-context", `${fileName}.json`);
33245
+ return path5.join(stateDir, "dispatch-context", `${fileName}.json`);
33246
+ }
33247
+ function dispatchContextDirPath(stateDir) {
33248
+ return dispatchLaneContextDir(stateDir);
32302
33249
  }
32303
33250
  function stepIdFilePathForSession(stateDir, sessionKey) {
32304
33251
  const fileName = Buffer.from(sessionKey).toString("base64url");
32305
- return path4.join(stateDir, "step-ids", `${fileName}.txt`);
33252
+ return path5.join(stateDir, "step-ids", `${fileName}.txt`);
32306
33253
  }
32307
33254
 
32308
33255
  // ts/claude-agent/dist/dispatch.js
32309
- import * as fs5 from "node:fs";
32310
- import * as path6 from "node:path";
33256
+ import * as fs6 from "node:fs";
33257
+ import * as path7 from "node:path";
32311
33258
  import { randomUUID } from "node:crypto";
32312
33259
  import { execSync as execSync2, spawn } from "node:child_process";
32313
33260
 
@@ -32315,8 +33262,8 @@ import { execSync as execSync2, spawn } from "node:child_process";
32315
33262
  import { execSync } from "node:child_process";
32316
33263
  import { constants } from "node:fs";
32317
33264
  import * as fsSync from "node:fs";
32318
- import * as fs4 from "node:fs/promises";
32319
- import * as path5 from "node:path";
33265
+ import * as fs5 from "node:fs/promises";
33266
+ import * as path6 from "node:path";
32320
33267
  var DEFAULT_MAX_TOTAL_IMAGE_BYTES = 20 * 1024 * 1024;
32321
33268
  var DEFAULT_ATTACHMENT_CACHE_MAX_BYTES = 512 * 1024 * 1024;
32322
33269
  var DEFAULT_ATTACHMENT_DOWNLOAD_TIMEOUT_MS = 3e4;
@@ -32346,11 +33293,11 @@ async function prepareLocalImageAttachments(event, context2, opts) {
32346
33293
  };
32347
33294
  }
32348
33295
  const rootDir = await ensureAttachmentRootDir(opts.workspaceDir);
32349
- const messageDir = path5.join(rootDir, sanitizePathSegment(event.messageId));
33296
+ const messageDir = path6.join(rootDir, sanitizePathSegment(event.messageId));
32350
33297
  await ensurePathIsNotSymlink(messageDir);
32351
- await fs4.mkdir(messageDir, { recursive: true });
33298
+ await fs5.mkdir(messageDir, { recursive: true });
32352
33299
  await ensurePathIsNotSymlink(messageDir);
32353
- const activeMessageDir = path5.resolve(messageDir);
33300
+ const activeMessageDir = path6.resolve(messageDir);
32354
33301
  activeAttachmentDirs.add(activeMessageDir);
32355
33302
  const maintenanceCooldownMs = opts.maintenanceCooldownMs ?? DEFAULT_MAINTENANCE_COOLDOWN_MS;
32356
33303
  const maintenancePromise = scheduleAttachmentMaintenance(rootDir, {
@@ -32367,7 +33314,7 @@ async function prepareLocalImageAttachments(event, context2, opts) {
32367
33314
  const notes = [];
32368
33315
  let downloadedBytes = 0;
32369
33316
  for (const att of imageAttachments) {
32370
- const localPath = path5.join(messageDir, localFileName(att.id, att.fileName, att.mimeType));
33317
+ const localPath = path6.join(messageDir, localFileName(att.id, att.fileName, att.mimeType));
32371
33318
  const downloadTimeoutMs = opts.downloadTimeoutMs ?? DEFAULT_ATTACHMENT_DOWNLOAD_TIMEOUT_MS;
32372
33319
  const fetchFresh = async () => {
32373
33320
  const fileInfo = await withTimeout(context2.client.getFileUrl(att.id), downloadTimeoutMs, `file URL lookup timed out after ${downloadTimeoutMs}ms`);
@@ -32424,7 +33371,7 @@ async function appendPreparedLocalAttachmentRefs(body, event, context2, opts) {
32424
33371
  return { body: appendLocalAttachmentRefs(body, attachments), attachments };
32425
33372
  }
32426
33373
  function pinLocalAttachmentPaths(images) {
32427
- const dirs = new Set(images.map((image) => path5.resolve(path5.dirname(image.localPath))));
33374
+ const dirs = new Set(images.map((image) => path6.resolve(path6.dirname(image.localPath))));
32428
33375
  for (const dir of dirs) {
32429
33376
  activeAttachmentDirs.add(dir);
32430
33377
  }
@@ -32439,7 +33386,7 @@ function pinLocalAttachmentPaths(images) {
32439
33386
  };
32440
33387
  }
32441
33388
  function attachmentRootDir(workspaceDir) {
32442
- return path5.join(path5.resolve(workspaceDir), ".parall", "attachments");
33389
+ return path6.join(path6.resolve(workspaceDir), ".parall", "attachments");
32443
33390
  }
32444
33391
  function ensureLocalAttachmentGitExclude(workingDirectory) {
32445
33392
  try {
@@ -32448,8 +33395,8 @@ function ensureLocalAttachmentGitExclude(workingDirectory) {
32448
33395
  encoding: "utf8",
32449
33396
  stdio: ["ignore", "pipe", "ignore"]
32450
33397
  }).trim();
32451
- const excludePath = path5.isAbsolute(rel) ? rel : path5.join(workingDirectory, rel);
32452
- fsSync.mkdirSync(path5.dirname(excludePath), { recursive: true });
33398
+ const excludePath = path6.isAbsolute(rel) ? rel : path6.join(workingDirectory, rel);
33399
+ fsSync.mkdirSync(path6.dirname(excludePath), { recursive: true });
32453
33400
  const existing = fsSync.existsSync(excludePath) ? fsSync.readFileSync(excludePath, "utf8") : "";
32454
33401
  if (existing.split(/\r?\n/).some((line) => line.trim() === ".parall/"))
32455
33402
  return;
@@ -32485,18 +33432,18 @@ function scheduleAttachmentMaintenance(rootDir, opts) {
32485
33432
  return run;
32486
33433
  }
32487
33434
  async function ensureAttachmentRootDir(workspaceDir) {
32488
- const workspaceRoot = path5.resolve(workspaceDir);
32489
- const parallDir = path5.join(workspaceRoot, ".parall");
33435
+ const workspaceRoot = path6.resolve(workspaceDir);
33436
+ const parallDir = path6.join(workspaceRoot, ".parall");
32490
33437
  const rootDir = attachmentRootDir(workspaceRoot);
32491
- await fs4.mkdir(workspaceRoot, { recursive: true });
33438
+ await fs5.mkdir(workspaceRoot, { recursive: true });
32492
33439
  await ensurePathIsNotSymlink(parallDir);
32493
- await fs4.mkdir(parallDir, { recursive: true, mode: 448 });
33440
+ await fs5.mkdir(parallDir, { recursive: true, mode: 448 });
32494
33441
  await ensurePathIsNotSymlink(parallDir);
32495
33442
  await ensurePathIsNotSymlink(rootDir);
32496
- await fs4.mkdir(rootDir, { recursive: true, mode: 448 });
33443
+ await fs5.mkdir(rootDir, { recursive: true, mode: 448 });
32497
33444
  await ensurePathIsNotSymlink(rootDir);
32498
- const realWorkspace = await fs4.realpath(workspaceRoot);
32499
- const realRoot = await fs4.realpath(rootDir);
33445
+ const realWorkspace = await fs5.realpath(workspaceRoot);
33446
+ const realRoot = await fs5.realpath(rootDir);
32500
33447
  if (!isPathInside(realRoot, realWorkspace)) {
32501
33448
  throw new Error(`attachment root escapes workspace: ${rootDir}`);
32502
33449
  }
@@ -32504,7 +33451,7 @@ async function ensureAttachmentRootDir(workspaceDir) {
32504
33451
  }
32505
33452
  async function ensurePathIsNotSymlink(filePath) {
32506
33453
  try {
32507
- const stat = await fs4.lstat(filePath);
33454
+ const stat = await fs5.lstat(filePath);
32508
33455
  if (stat.isSymbolicLink()) {
32509
33456
  throw new Error(`refusing to use symlinked attachment path ${filePath}`);
32510
33457
  }
@@ -32515,8 +33462,8 @@ async function ensurePathIsNotSymlink(filePath) {
32515
33462
  }
32516
33463
  }
32517
33464
  function isPathInside(childPath, parentPath) {
32518
- const rel = path5.relative(parentPath, childPath);
32519
- return rel === "" || !!rel && !rel.startsWith("..") && !path5.isAbsolute(rel);
33465
+ const rel = path6.relative(parentPath, childPath);
33466
+ return rel === "" || !!rel && !rel.startsWith("..") && !path6.isAbsolute(rel);
32520
33467
  }
32521
33468
  async function existingUsableFile(filePath, expectedSize, rootDir) {
32522
33469
  try {
@@ -32527,30 +33474,30 @@ async function existingUsableFile(filePath, expectedSize, rootDir) {
32527
33474
  }
32528
33475
  }
32529
33476
  async function localFileStatInsideRoot(filePath, rootDir) {
32530
- const stat = await fs4.lstat(filePath);
33477
+ const stat = await fs5.lstat(filePath);
32531
33478
  if (stat.isSymbolicLink()) {
32532
33479
  throw new Error(`refusing to use symlinked attachment file ${filePath}`);
32533
33480
  }
32534
33481
  if (!stat.isFile()) {
32535
33482
  throw new Error(`attachment path is not a file ${filePath}`);
32536
33483
  }
32537
- const realRoot = await fs4.realpath(rootDir);
32538
- const realFile = await fs4.realpath(filePath);
33484
+ const realRoot = await fs5.realpath(rootDir);
33485
+ const realFile = await fs5.realpath(filePath);
32539
33486
  if (!isPathInside(realFile, realRoot)) {
32540
33487
  throw new Error(`attachment file escapes workspace: ${filePath}`);
32541
33488
  }
32542
33489
  return stat;
32543
33490
  }
32544
33491
  async function localDirectoryStatInsideRoot(dirPath, rootDir) {
32545
- const stat = await fs4.lstat(dirPath);
33492
+ const stat = await fs5.lstat(dirPath);
32546
33493
  if (stat.isSymbolicLink()) {
32547
33494
  throw new Error(`refusing to use symlinked attachment directory ${dirPath}`);
32548
33495
  }
32549
33496
  if (!stat.isDirectory()) {
32550
33497
  throw new Error(`attachment path is not a directory ${dirPath}`);
32551
33498
  }
32552
- const realRoot = await fs4.realpath(rootDir);
32553
- const realDir = await fs4.realpath(dirPath);
33499
+ const realRoot = await fs5.realpath(rootDir);
33500
+ const realDir = await fs5.realpath(dirPath);
32554
33501
  if (!isPathInside(realDir, realRoot)) {
32555
33502
  throw new Error(`attachment directory escapes workspace: ${dirPath}`);
32556
33503
  }
@@ -32558,7 +33505,7 @@ async function localDirectoryStatInsideRoot(dirPath, rootDir) {
32558
33505
  }
32559
33506
  async function openLocalFileInsideRoot(filePath, rootDir) {
32560
33507
  const checkedStat = await localFileStatInsideRoot(filePath, rootDir);
32561
- const file = await fs4.open(filePath, constants.O_RDONLY | constants.O_NOFOLLOW);
33508
+ const file = await fs5.open(filePath, constants.O_RDONLY | constants.O_NOFOLLOW);
32562
33509
  let keepOpen = false;
32563
33510
  try {
32564
33511
  const openedStat = await file.stat();
@@ -32574,8 +33521,8 @@ async function openLocalFileInsideRoot(filePath, rootDir) {
32574
33521
  }
32575
33522
  }
32576
33523
  async function openLocalTempFileInsideRoot(filePath, rootDir) {
32577
- await localDirectoryStatInsideRoot(path5.dirname(filePath), rootDir);
32578
- const file = await fs4.open(filePath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
33524
+ await localDirectoryStatInsideRoot(path6.dirname(filePath), rootDir);
33525
+ const file = await fs5.open(filePath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
32579
33526
  let keepOpen = false;
32580
33527
  try {
32581
33528
  const checkedStat = await localFileStatInsideRoot(filePath, rootDir);
@@ -32600,7 +33547,7 @@ async function assertLocalFileIdentity(filePath, rootDir, expected) {
32600
33547
  async function removeLocalFileIfInside(filePath, rootDir) {
32601
33548
  try {
32602
33549
  await localFileStatInsideRoot(filePath, rootDir);
32603
- await fs4.rm(filePath, { force: true });
33550
+ await fs5.rm(filePath, { force: true });
32604
33551
  } catch {
32605
33552
  }
32606
33553
  }
@@ -32613,7 +33560,7 @@ function sameFile(a, b) {
32613
33560
  async function cleanupOldAttachmentFiles(rootDir, ttlMs, log2, preserveDirs) {
32614
33561
  let entries;
32615
33562
  try {
32616
- entries = await fs4.readdir(rootDir, { withFileTypes: true });
33563
+ entries = await fs5.readdir(rootDir, { withFileTypes: true });
32617
33564
  } catch {
32618
33565
  return;
32619
33566
  }
@@ -32621,15 +33568,15 @@ async function cleanupOldAttachmentFiles(rootDir, ttlMs, log2, preserveDirs) {
32621
33568
  await Promise.all(entries.map(async (entry) => {
32622
33569
  if (!entry.isDirectory())
32623
33570
  return;
32624
- const fullPath = path5.join(rootDir, entry.name);
33571
+ const fullPath = path6.join(rootDir, entry.name);
32625
33572
  try {
32626
- if (preserveDirs?.has(path5.resolve(fullPath)))
33573
+ if (preserveDirs?.has(path6.resolve(fullPath)))
32627
33574
  return;
32628
- const stat = await fs4.lstat(fullPath);
33575
+ const stat = await fs5.lstat(fullPath);
32629
33576
  if (!stat.isDirectory())
32630
33577
  return;
32631
33578
  if (stat.mtimeMs < cutoff) {
32632
- await fs4.rm(fullPath, { recursive: true, force: true });
33579
+ await fs5.rm(fullPath, { recursive: true, force: true });
32633
33580
  }
32634
33581
  } catch (err) {
32635
33582
  log2?.warn?.(`agent-core: failed to clean attachment temp dir ${fullPath}: ${String(err)}`);
@@ -32641,7 +33588,7 @@ async function pruneAttachmentCache(rootDir, maxBytes, log2, preserveDirs) {
32641
33588
  return;
32642
33589
  let entries;
32643
33590
  try {
32644
- entries = await fs4.readdir(rootDir, { withFileTypes: true });
33591
+ entries = await fs5.readdir(rootDir, { withFileTypes: true });
32645
33592
  } catch {
32646
33593
  return;
32647
33594
  }
@@ -32650,9 +33597,9 @@ async function pruneAttachmentCache(rootDir, maxBytes, log2, preserveDirs) {
32650
33597
  for (const entry of entries) {
32651
33598
  if (!entry.isDirectory())
32652
33599
  continue;
32653
- const fullPath = path5.join(rootDir, entry.name);
33600
+ const fullPath = path6.join(rootDir, entry.name);
32654
33601
  try {
32655
- const stat = await fs4.lstat(fullPath);
33602
+ const stat = await fs5.lstat(fullPath);
32656
33603
  if (!stat.isDirectory())
32657
33604
  continue;
32658
33605
  const size = await directorySize(fullPath);
@@ -32668,10 +33615,10 @@ async function pruneAttachmentCache(rootDir, maxBytes, log2, preserveDirs) {
32668
33615
  for (const dir of dirs) {
32669
33616
  if (total <= maxBytes)
32670
33617
  break;
32671
- if (preserveDirs?.has(path5.resolve(dir.path)))
33618
+ if (preserveDirs?.has(path6.resolve(dir.path)))
32672
33619
  continue;
32673
33620
  try {
32674
- await fs4.rm(dir.path, { recursive: true, force: true });
33621
+ await fs5.rm(dir.path, { recursive: true, force: true });
32675
33622
  total -= dir.size;
32676
33623
  } catch (err) {
32677
33624
  log2?.warn?.(`agent-core: failed to prune attachment cache dir ${dir.path}: ${String(err)}`);
@@ -32680,12 +33627,12 @@ async function pruneAttachmentCache(rootDir, maxBytes, log2, preserveDirs) {
32680
33627
  }
32681
33628
  async function directorySize(dirPath) {
32682
33629
  let total = 0;
32683
- const entries = await fs4.readdir(dirPath, { withFileTypes: true });
33630
+ const entries = await fs5.readdir(dirPath, { withFileTypes: true });
32684
33631
  for (const entry of entries) {
32685
- const fullPath = path5.join(dirPath, entry.name);
33632
+ const fullPath = path6.join(dirPath, entry.name);
32686
33633
  let stat;
32687
33634
  try {
32688
- stat = await fs4.lstat(fullPath);
33635
+ stat = await fs5.lstat(fullPath);
32689
33636
  } catch {
32690
33637
  continue;
32691
33638
  }
@@ -32700,10 +33647,10 @@ async function directorySize(dirPath) {
32700
33647
  return total;
32701
33648
  }
32702
33649
  function activeDirsForRoot(rootDir) {
32703
- const root = path5.resolve(rootDir);
33650
+ const root = path6.resolve(rootDir);
32704
33651
  const dirs = /* @__PURE__ */ new Set();
32705
33652
  for (const dir of activeAttachmentDirs) {
32706
- if (dir === root || dir.startsWith(`${root}${path5.sep}`)) {
33653
+ if (dir === root || dir.startsWith(`${root}${path6.sep}`)) {
32707
33654
  dirs.add(dir);
32708
33655
  }
32709
33656
  }
@@ -32800,9 +33747,9 @@ async function writeResponseToFileWithLimit(res, filePath, maxBytes, rootDir) {
32800
33747
  }
32801
33748
  writtenStat = await file.stat();
32802
33749
  await closeFile();
32803
- await localDirectoryStatInsideRoot(path5.dirname(filePath), rootDir);
33750
+ await localDirectoryStatInsideRoot(path6.dirname(filePath), rootDir);
32804
33751
  await assertLocalFileIdentity(tmpPath, rootDir, writtenStat);
32805
- await fs4.rename(tmpPath, filePath);
33752
+ await fs5.rename(tmpPath, filePath);
32806
33753
  completed = true;
32807
33754
  return written;
32808
33755
  } finally {
@@ -32818,9 +33765,9 @@ async function writeResponseToFileWithLimit(res, filePath, maxBytes, rootDir) {
32818
33765
  }
32819
33766
  }
32820
33767
  function localFileName(attachmentId, fileName, mimeType) {
32821
- const safeName = sanitizePathSegment(path5.basename(fileName || attachmentId));
32822
- const ext = path5.extname(safeName) || extensionForMime(mimeType);
32823
- const stem = path5.basename(safeName, path5.extname(safeName)) || attachmentId;
33768
+ const safeName = sanitizePathSegment(path6.basename(fileName || attachmentId));
33769
+ const ext = path6.extname(safeName) || extensionForMime(mimeType);
33770
+ const stem = path6.basename(safeName, path6.extname(safeName)) || attachmentId;
32824
33771
  return `${sanitizePathSegment(attachmentId)}-${stem}${ext}`;
32825
33772
  }
32826
33773
  function extensionForMime(mimeType) {
@@ -33100,7 +34047,11 @@ function buildSpawnEnv(parentEnv, claudeHome, context2, opts) {
33100
34047
  PRLL_TRIGGER_MESSAGE_ID: context2.triggerMessageId ?? "",
33101
34048
  PRLL_NO_REPLY: context2.noReply ? "1" : "",
33102
34049
  PRLL_CONTEXT_FILE: context2.contextFilePath ?? "",
33103
- PRLL_STEP_ID_FILE: context2.stepIdFilePath ?? ""
34050
+ PRLL_STEP_ID_FILE: context2.stepIdFilePath ?? "",
34051
+ // Per-lane dispatch context directory (stable for the bridge's lifetime,
34052
+ // so pinning it at spawn is safe even for long-lived subprocesses). The
34053
+ // CLI keys into it by send target to derive dispatch effect keys.
34054
+ PRLL_CONTEXT_DIR: context2.contextDirPath ?? ""
33104
34055
  };
33105
34056
  if (!result.PRLL_WIKI_MOUNT_ROOT?.trim() && opts.wikiMountRoot) {
33106
34057
  result.PRLL_WIKI_MOUNT_ROOT = opts.wikiMountRoot;
@@ -33206,8 +34157,8 @@ var ClaudeCodeAdapter = class {
33206
34157
  if (!sessionId)
33207
34158
  return void 0;
33208
34159
  const projectSlug = this.opts.workspaceDir.replace(/[/.]/g, "-");
33209
- const filePath = path6.join(this.opts.claudeHome, ".claude", "projects", projectSlug, `${sessionId}.jsonl`);
33210
- return fs5.existsSync(filePath) ? filePath : void 0;
34160
+ const filePath = path7.join(this.opts.claudeHome, ".claude", "projects", projectSlug, `${sessionId}.jsonl`);
34161
+ return fs6.existsSync(filePath) ? filePath : void 0;
33211
34162
  }
33212
34163
  forkSession({ sessionKey }) {
33213
34164
  return this.opts.sessionManager.createForkSession(sessionKey);
@@ -33493,7 +34444,7 @@ var ClaudeCodeAdapter = class {
33493
34444
  if (this.opts.disallowedTools.length > 0) {
33494
34445
  args.push("--disallowedTools", this.opts.disallowedTools.join(","));
33495
34446
  }
33496
- args.push("--append-system-prompt-file", path6.join(this.opts.workspaceDir, ".parall", "system-prompt.md"));
34447
+ args.push("--append-system-prompt-file", path7.join(this.opts.workspaceDir, ".parall", "system-prompt.md"));
33497
34448
  if (this.opts.appendSystemPrompt) {
33498
34449
  args.push("--append-system-prompt", this.opts.appendSystemPrompt);
33499
34450
  }
@@ -33518,14 +34469,15 @@ var ClaudeCodeAdapter = class {
33518
34469
  noReply: false,
33519
34470
  contextFilePath: this.opts.contextFilePathForSession?.(sessionKey) ?? "",
33520
34471
  stepIdFilePath: this.opts.stepIdFilePathForSession?.(sessionKey) ?? "",
34472
+ contextDirPath: this.opts.contextDirPath ?? "",
33521
34473
  client: void 0
33522
34474
  };
33523
34475
  }
33524
34476
  };
33525
34477
 
33526
34478
  // ts/claude-agent/dist/session-manager.js
33527
- import * as fs6 from "node:fs";
33528
- import * as path7 from "node:path";
34479
+ import * as fs7 from "node:fs";
34480
+ import * as path8 from "node:path";
33529
34481
  import { randomUUID as randomUUID2 } from "node:crypto";
33530
34482
  var ClaudeSessionManager = class _ClaudeSessionManager {
33531
34483
  mainSessionKey;
@@ -33674,13 +34626,13 @@ var ClaudeSessionManager = class _ClaudeSessionManager {
33674
34626
  clearMainSession() {
33675
34627
  this.sessionIds.delete(this.mainSessionKey);
33676
34628
  try {
33677
- fs6.unlinkSync(this.stateFilePath);
34629
+ fs7.unlinkSync(this.stateFilePath);
33678
34630
  } catch {
33679
34631
  }
33680
34632
  }
33681
34633
  restore() {
33682
34634
  try {
33683
- const raw = fs6.readFileSync(this.stateFilePath, "utf8");
34635
+ const raw = fs7.readFileSync(this.stateFilePath, "utf8");
33684
34636
  const parsed = JSON.parse(raw);
33685
34637
  if (parsed.runtimeKey === this.mainSessionKey && typeof parsed.sessionId === "string" && parsed.sessionId.trim()) {
33686
34638
  this.sessionIds.set(this.mainSessionKey, parsed.sessionId.trim());
@@ -33690,8 +34642,8 @@ var ClaudeSessionManager = class _ClaudeSessionManager {
33690
34642
  }
33691
34643
  persist(sessionId) {
33692
34644
  try {
33693
- fs6.mkdirSync(path7.dirname(this.stateFilePath), { recursive: true });
33694
- fs6.writeFileSync(this.stateFilePath, JSON.stringify({
34645
+ fs7.mkdirSync(path8.dirname(this.stateFilePath), { recursive: true });
34646
+ fs7.writeFileSync(this.stateFilePath, JSON.stringify({
33695
34647
  runtimeKey: this.mainSessionKey,
33696
34648
  sessionId
33697
34649
  }, null, 2));
@@ -33702,8 +34654,8 @@ var ClaudeSessionManager = class _ClaudeSessionManager {
33702
34654
  };
33703
34655
 
33704
34656
  // ts/claude-agent/dist/workspace.js
33705
- import * as fs7 from "node:fs";
33706
- import * as path8 from "node:path";
34657
+ import * as fs8 from "node:fs";
34658
+ import * as path9 from "node:path";
33707
34659
  function ensureClaudeWorkspace(workspaceDir, log2, agentIdentity) {
33708
34660
  const systemPrompt = [
33709
34661
  buildIdentity(agentIdentity),
@@ -33712,12 +34664,12 @@ function ensureClaudeWorkspace(workspaceDir, log2, agentIdentity) {
33712
34664
  PRLL_REFERENCE_GUIDE,
33713
34665
  buildSkillReferences(workspaceDir)
33714
34666
  ].join("\n\n");
33715
- fs7.mkdirSync(workspaceDir, { recursive: true });
33716
- fs7.mkdirSync(path8.join(workspaceDir, ".claude"), { recursive: true });
33717
- const parallDir = path8.join(workspaceDir, ".parall");
33718
- fs7.mkdirSync(parallDir, { recursive: true });
33719
- fs7.writeFileSync(path8.join(parallDir, "system-prompt.md"), systemPrompt, "utf8");
33720
- writeSkillFiles(path8.join(parallDir, "skills"));
34667
+ fs8.mkdirSync(workspaceDir, { recursive: true });
34668
+ fs8.mkdirSync(path9.join(workspaceDir, ".claude"), { recursive: true });
34669
+ const parallDir = path9.join(workspaceDir, ".parall");
34670
+ fs8.mkdirSync(parallDir, { recursive: true });
34671
+ fs8.writeFileSync(path9.join(parallDir, "system-prompt.md"), systemPrompt, "utf8");
34672
+ writeSkillFiles(path9.join(parallDir, "skills"));
33721
34673
  ensureLocalAttachmentGitExclude(workspaceDir);
33722
34674
  }
33723
34675
 
@@ -33792,14 +34744,13 @@ async function main() {
33792
34744
  log: agentLog
33793
34745
  });
33794
34746
  const platformDefaults = await configMgr.fetch();
33795
- let platformManaged = isPlatformManagedProfile(me.agent_profile);
33796
- let platformModelOverride = isPlatformModelOverride(me.agent_profile);
34747
+ const platformModelOverride = deriveModelIsPin(platformDefaults, me.agent_profile);
33797
34748
  const resolvedModel = resolveRuntimeModel(platformModelOverride, platformDefaults.model, config.model);
33798
- const resolvedEffort = platformManaged ? platformDefaults.thinkingEffort ?? (process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || void 0) : process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || void 0;
34749
+ const resolvedEffort = platformDefaults.thinkingEffort ?? (process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || void 0);
33799
34750
  if (platformDefaults.model)
33800
- agentLog.info(`platform config: model=${platformDefaults.model} \u2192 resolved=${resolvedModel ?? "cli-default"} (${platformModelOverride ? "platform-managed" : "floor/self"})`);
34751
+ agentLog.info(`platform config: model=${platformDefaults.model} \u2192 resolved=${resolvedModel ?? "cli-default"} (${platformModelOverride ? "pin/override" : "floor"})`);
33801
34752
  if (platformDefaults.thinkingEffort)
33802
- agentLog.info(`platform config: thinking_effort=${platformDefaults.thinkingEffort} (${platformModelOverride ? "applied" : "ignored, model_management=self"})`);
34753
+ agentLog.info(`platform config: thinking_effort=${platformDefaults.thinkingEffort}`);
33803
34754
  const adapter = new ClaudeCodeAdapter({
33804
34755
  claudeBin: config.claudeBin,
33805
34756
  claudeHome: config.claudeHome,
@@ -33819,11 +34770,29 @@ async function main() {
33819
34770
  apiKey: config.apiKey,
33820
34771
  orgId: config.orgId,
33821
34772
  contextFilePathForSession: (sessionKey) => contextFilePathForSession(config.stateDir, sessionKey),
33822
- stepIdFilePathForSession: (sessionKey) => stepIdFilePathForSession(config.stateDir, sessionKey)
34773
+ stepIdFilePathForSession: (sessionKey) => stepIdFilePathForSession(config.stateDir, sessionKey),
34774
+ contextDirPath: dispatchContextDirPath(config.stateDir)
33823
34775
  });
33824
34776
  if (resolvedEffort) {
33825
34777
  adapter.updateConfig({ effort: resolvedEffort });
33826
34778
  }
34779
+ const refreshPlatformConfig = async () => {
34780
+ const updated = await configMgr.fetch();
34781
+ let refreshedProfile;
34782
+ if (updated.modelIsPin === void 0) {
34783
+ try {
34784
+ refreshedProfile = (await getAgentMeWithLegacyFallback(client, config.orgId)).agent_profile;
34785
+ } catch (err) {
34786
+ agentLog.warn(`platform config refresh: legacy /agents/me fallback failed: ${String(err)}`);
34787
+ }
34788
+ }
34789
+ const isPin = deriveModelIsPin(updated, refreshedProfile);
34790
+ const localEffort = process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || null;
34791
+ adapter.updateConfig({
34792
+ model: resolveRuntimeModel(isPin, updated.model, config.model) ?? null,
34793
+ effort: updated.thinkingEffort ?? localEffort
34794
+ });
34795
+ };
33827
34796
  const gateway = new ParallAgentGateway({
33828
34797
  accountId: agentUserId,
33829
34798
  client,
@@ -33850,28 +34819,9 @@ async function main() {
33850
34819
  dispatchDeadlineMs: parseDispatchDeadlineMs(process.env.PRLL_DISPATCH_DEADLINE_MS),
33851
34820
  contextFilePathForSession: (sessionKey) => contextFilePathForSession(config.stateDir, sessionKey),
33852
34821
  stepIdFilePathForSession: (sessionKey) => stepIdFilePathForSession(config.stateDir, sessionKey),
33853
- onConfigUpdate: async () => {
33854
- const refreshed = await getAgentMeWithLegacyFallback(client, config.orgId);
33855
- platformManaged = isPlatformManagedProfile(refreshed.agent_profile);
33856
- platformModelOverride = isPlatformModelOverride(refreshed.agent_profile);
33857
- const updated = await configMgr.fetch();
33858
- const localEffort = process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || null;
33859
- adapter.updateConfig({
33860
- model: resolveRuntimeModel(platformModelOverride, updated.model, config.model) ?? null,
33861
- effort: platformManaged ? updated.thinkingEffort ?? localEffort : localEffort
33862
- });
33863
- },
33864
- onSessionReady: async () => {
33865
- const refreshed = await getAgentMeWithLegacyFallback(client, config.orgId);
33866
- platformManaged = isPlatformManagedProfile(refreshed.agent_profile);
33867
- platformModelOverride = isPlatformModelOverride(refreshed.agent_profile);
33868
- const updated = await configMgr.fetch();
33869
- const localEffort = process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || null;
33870
- adapter.updateConfig({
33871
- model: resolveRuntimeModel(platformModelOverride, updated.model, config.model) ?? null,
33872
- effort: platformManaged ? updated.thinkingEffort ?? localEffort : localEffort
33873
- });
33874
- },
34822
+ dispatchContextDir: dispatchContextDirPath(config.stateDir),
34823
+ onConfigUpdate: refreshPlatformConfig,
34824
+ onSessionReady: refreshPlatformConfig,
33875
34825
  // Long-lived `claude` subprocesses outlive a single dispatch, so the
33876
34826
  // gateway's shuttingDown flag alone does not tear them down. Piggyback on
33877
34827
  // onBeforeDisconnect to close stdin and SIGTERM any survivors after