@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 path8 of paths) {
17721
+ for (const path9 of paths) {
17722
17722
  try {
17723
- const result = await fs_1.promises.readFile(path8, { encoding: "utf8" });
17723
+ const result = await fs_1.promises.readFile(path9, { 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, path8) {
21126
+ function appendResourcePathToUrl(url, path9) {
21127
21127
  try {
21128
21128
  new URL(url);
21129
21129
  } catch (_a) {
@@ -21133,11 +21133,11 @@ function appendResourcePathToUrl(url, path8) {
21133
21133
  if (!url.endsWith("/")) {
21134
21134
  url = url + "/";
21135
21135
  }
21136
- url += path8;
21136
+ url += path9;
21137
21137
  try {
21138
21138
  new URL(url);
21139
21139
  } catch (_b) {
21140
- diag2.warn("Configuration: Provided URL appended with '" + path8 + "' is not a valid URL, using 'undefined' instead of '" + url + "'");
21140
+ diag2.warn("Configuration: Provided URL appended with '" + path9 + "' 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(path8) {
27532
- return path8.startsWith(WIKI_BASE) ? this.wikiBaseUrl : this.baseUrl;
27615
+ baseUrlFor(path9) {
27616
+ return path9.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(path8) {
27643
+ async ensureFreshToken(path9) {
27560
27644
  if (!this.token || !this.getRefreshToken)
27561
27645
  return;
27562
- const pathSuffix = path8.replace(/^\/api\/v1/, "");
27646
+ const pathSuffix = path9.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, path8, body, query, retried = false, opts) {
27678
+ async request(method, path9, body, query, retried = false, opts) {
27595
27679
  if (!retried) {
27596
- await this.ensureFreshToken(path8);
27680
+ await this.ensureFreshToken(path9);
27597
27681
  }
27598
- let url = `${this.baseUrlFor(path8)}${path8}`;
27682
+ let url = `${this.baseUrlFor(path9)}${path9}`;
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 = path8.replace(/^\/api\/v1/, "");
27712
+ const pathSuffix = path9.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, path8, body, query, true, opts);
27717
+ return this.request(method, path9, 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, path8, body, retried = false) {
27747
+ async multipartRequest(method, path9, body, retried = false) {
27663
27748
  if (!retried) {
27664
- await this.ensureFreshToken(path8);
27749
+ await this.ensureFreshToken(path9);
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(path8)}${path8}`, {
27755
+ res = await fetch(`${this.baseUrlFor(path9)}${path9}`, {
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 = path8.replace(/^\/api\/v1/, "");
27765
+ const pathSuffix = path9.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, path8, body, true);
27770
+ return this.multipartRequest(method, path9, 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, path8) {
28351
- return this.request("POST", ENDPOINTS.MACHINE_BROWSE(orgId, machineId), { path: path8 }, void 0, false, { timeoutMs: 15e3 });
28464
+ async browseMachineFilesystem(orgId, machineId, path9) {
28465
+ return this.request("POST", ENDPOINTS.MACHINE_BROWSE(orgId, machineId), { path: path9 }, 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, path8) {
28810
- return this.request("GET", ENDPOINTS.WIKI_ACCESS_STATUS(orgId, wikiId), void 0, path8 ? { path: path8 } : void 0);
29026
+ async getWikiAccessStatus(orgId, wikiId, path9) {
29027
+ return this.request("GET", ENDPOINTS.WIKI_ACCESS_STATUS(orgId, wikiId), void 0, path9 ? { path: path9 } : 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, path8, params) {
29036
+ async getWikiFileCommits(orgId, wikiId, path9, params) {
28820
29037
  return this.request("GET", ENDPOINTS.WIKI_FILE_COMMITS(orgId, wikiId), void 0, {
28821
- path: path8,
29038
+ path: path9,
28822
29039
  ...params
28823
29040
  });
28824
29041
  }
28825
- async getWikiBlame(orgId, wikiId, path8, ref) {
28826
- return this.request("GET", ENDPOINTS.WIKI_BLAME(orgId, wikiId), void 0, { path: path8, ref });
29042
+ async getWikiBlame(orgId, wikiId, path9, ref) {
29043
+ return this.request("GET", ENDPOINTS.WIKI_BLAME(orgId, wikiId), void 0, { path: path9, 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
32513
+
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
31578
32517
 
31579
- # Direct message by user URI or display name
31580
- parall dm prll://usr_xxx --text "Hello"
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/codex-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) {
@@ -32246,15 +33190,15 @@ function requireEnv(env, name) {
32246
33190
  return value;
32247
33191
  }
32248
33192
  function resolvePath(value) {
32249
- return path4.isAbsolute(value) ? value : path4.resolve(process.cwd(), value);
33193
+ return path5.isAbsolute(value) ? value : path5.resolve(process.cwd(), value);
32250
33194
  }
32251
33195
  function resolveCodexAgentConfig(env = process.env) {
32252
33196
  const apiUrl = requireEnv(env, "PRLL_API_URL");
32253
33197
  const apiKey = requireEnv(env, "PRLL_API_KEY");
32254
33198
  const orgId = requireEnv(env, "PRLL_ORG_ID");
32255
- const codexHome = resolvePath(env.PRLL_CODEX_HOME?.trim() || env.CODEX_HOME?.trim() || path4.join(env.HOME || os2.homedir(), ".codex"));
32256
- const stateDir = resolvePath(env.PRLL_STATE_DIR?.trim() || path4.join(env.HOME || os2.homedir(), ".parall-agent"));
32257
- const workspaceDir = resolvePath(env.PRLL_WORKSPACE_DIR?.trim() || path4.join(stateDir, "workspace"));
33199
+ const codexHome = resolvePath(env.PRLL_CODEX_HOME?.trim() || env.CODEX_HOME?.trim() || path5.join(env.HOME || os2.homedir(), ".codex"));
33200
+ const stateDir = resolvePath(env.PRLL_STATE_DIR?.trim() || path5.join(env.HOME || os2.homedir(), ".parall-agent"));
33201
+ const workspaceDir = resolvePath(env.PRLL_WORKSPACE_DIR?.trim() || path5.join(stateDir, "workspace"));
32258
33202
  return {
32259
33203
  apiUrl,
32260
33204
  apiKey,
@@ -32308,28 +33252,31 @@ function buildCodexRuntimeKey(agentUserId) {
32308
33252
  }
32309
33253
  function sessionStateFilePathForRuntime(stateDir, runtimeKey) {
32310
33254
  const fileName = Buffer.from(runtimeKey).toString("base64url");
32311
- return path4.join(stateDir, "threads", `${fileName}.json`);
33255
+ return path5.join(stateDir, "threads", `${fileName}.json`);
32312
33256
  }
32313
33257
  function contextFilePathForSession(stateDir, sessionKey) {
32314
33258
  const fileName = Buffer.from(sessionKey).toString("base64url");
32315
- return path4.join(stateDir, "dispatch-context", `${fileName}.json`);
33259
+ return path5.join(stateDir, "dispatch-context", `${fileName}.json`);
33260
+ }
33261
+ function dispatchContextDirPath(stateDir) {
33262
+ return dispatchLaneContextDir(stateDir);
32316
33263
  }
32317
33264
  function stepIdFilePathForSession(stateDir, sessionKey) {
32318
33265
  const fileName = Buffer.from(sessionKey).toString("base64url");
32319
- return path4.join(stateDir, "step-ids", `${fileName}.txt`);
33266
+ return path5.join(stateDir, "step-ids", `${fileName}.txt`);
32320
33267
  }
32321
33268
 
32322
33269
  // ts/codex-agent/dist/dispatch.js
32323
33270
  import { execSync as execSync2, spawn } from "node:child_process";
32324
33271
  import { randomUUID } from "node:crypto";
32325
- import * as fs5 from "node:fs";
33272
+ import * as fs6 from "node:fs";
32326
33273
 
32327
33274
  // ts/agent-core/dist/internal/attachment-input.js
32328
33275
  import { execSync } from "node:child_process";
32329
33276
  import { constants } from "node:fs";
32330
33277
  import * as fsSync from "node:fs";
32331
- import * as fs4 from "node:fs/promises";
32332
- import * as path5 from "node:path";
33278
+ import * as fs5 from "node:fs/promises";
33279
+ import * as path6 from "node:path";
32333
33280
  var DEFAULT_MAX_TOTAL_IMAGE_BYTES = 20 * 1024 * 1024;
32334
33281
  var DEFAULT_ATTACHMENT_CACHE_MAX_BYTES = 512 * 1024 * 1024;
32335
33282
  var DEFAULT_ATTACHMENT_DOWNLOAD_TIMEOUT_MS = 3e4;
@@ -32359,11 +33306,11 @@ async function prepareLocalImageAttachments(event, context2, opts) {
32359
33306
  };
32360
33307
  }
32361
33308
  const rootDir = await ensureAttachmentRootDir(opts.workspaceDir);
32362
- const messageDir = path5.join(rootDir, sanitizePathSegment(event.messageId));
33309
+ const messageDir = path6.join(rootDir, sanitizePathSegment(event.messageId));
32363
33310
  await ensurePathIsNotSymlink(messageDir);
32364
- await fs4.mkdir(messageDir, { recursive: true });
33311
+ await fs5.mkdir(messageDir, { recursive: true });
32365
33312
  await ensurePathIsNotSymlink(messageDir);
32366
- const activeMessageDir = path5.resolve(messageDir);
33313
+ const activeMessageDir = path6.resolve(messageDir);
32367
33314
  activeAttachmentDirs.add(activeMessageDir);
32368
33315
  const maintenanceCooldownMs = opts.maintenanceCooldownMs ?? DEFAULT_MAINTENANCE_COOLDOWN_MS;
32369
33316
  const maintenancePromise = scheduleAttachmentMaintenance(rootDir, {
@@ -32380,7 +33327,7 @@ async function prepareLocalImageAttachments(event, context2, opts) {
32380
33327
  const notes = [];
32381
33328
  let downloadedBytes = 0;
32382
33329
  for (const att of imageAttachments) {
32383
- const localPath = path5.join(messageDir, localFileName(att.id, att.fileName, att.mimeType));
33330
+ const localPath = path6.join(messageDir, localFileName(att.id, att.fileName, att.mimeType));
32384
33331
  const downloadTimeoutMs = opts.downloadTimeoutMs ?? DEFAULT_ATTACHMENT_DOWNLOAD_TIMEOUT_MS;
32385
33332
  const fetchFresh = async () => {
32386
33333
  const fileInfo = await withTimeout(context2.client.getFileUrl(att.id), downloadTimeoutMs, `file URL lookup timed out after ${downloadTimeoutMs}ms`);
@@ -32437,7 +33384,7 @@ async function appendPreparedLocalAttachmentRefs(body, event, context2, opts) {
32437
33384
  return { body: appendLocalAttachmentRefs(body, attachments), attachments };
32438
33385
  }
32439
33386
  function pinLocalAttachmentPaths(images) {
32440
- const dirs = new Set(images.map((image) => path5.resolve(path5.dirname(image.localPath))));
33387
+ const dirs = new Set(images.map((image) => path6.resolve(path6.dirname(image.localPath))));
32441
33388
  for (const dir of dirs) {
32442
33389
  activeAttachmentDirs.add(dir);
32443
33390
  }
@@ -32452,7 +33399,7 @@ function pinLocalAttachmentPaths(images) {
32452
33399
  };
32453
33400
  }
32454
33401
  function attachmentRootDir(workspaceDir) {
32455
- return path5.join(path5.resolve(workspaceDir), ".parall", "attachments");
33402
+ return path6.join(path6.resolve(workspaceDir), ".parall", "attachments");
32456
33403
  }
32457
33404
  function ensureLocalAttachmentGitExclude(workingDirectory) {
32458
33405
  try {
@@ -32461,8 +33408,8 @@ function ensureLocalAttachmentGitExclude(workingDirectory) {
32461
33408
  encoding: "utf8",
32462
33409
  stdio: ["ignore", "pipe", "ignore"]
32463
33410
  }).trim();
32464
- const excludePath = path5.isAbsolute(rel) ? rel : path5.join(workingDirectory, rel);
32465
- fsSync.mkdirSync(path5.dirname(excludePath), { recursive: true });
33411
+ const excludePath = path6.isAbsolute(rel) ? rel : path6.join(workingDirectory, rel);
33412
+ fsSync.mkdirSync(path6.dirname(excludePath), { recursive: true });
32466
33413
  const existing = fsSync.existsSync(excludePath) ? fsSync.readFileSync(excludePath, "utf8") : "";
32467
33414
  if (existing.split(/\r?\n/).some((line) => line.trim() === ".parall/"))
32468
33415
  return;
@@ -32498,18 +33445,18 @@ function scheduleAttachmentMaintenance(rootDir, opts) {
32498
33445
  return run;
32499
33446
  }
32500
33447
  async function ensureAttachmentRootDir(workspaceDir) {
32501
- const workspaceRoot = path5.resolve(workspaceDir);
32502
- const parallDir = path5.join(workspaceRoot, ".parall");
33448
+ const workspaceRoot = path6.resolve(workspaceDir);
33449
+ const parallDir = path6.join(workspaceRoot, ".parall");
32503
33450
  const rootDir = attachmentRootDir(workspaceRoot);
32504
- await fs4.mkdir(workspaceRoot, { recursive: true });
33451
+ await fs5.mkdir(workspaceRoot, { recursive: true });
32505
33452
  await ensurePathIsNotSymlink(parallDir);
32506
- await fs4.mkdir(parallDir, { recursive: true, mode: 448 });
33453
+ await fs5.mkdir(parallDir, { recursive: true, mode: 448 });
32507
33454
  await ensurePathIsNotSymlink(parallDir);
32508
33455
  await ensurePathIsNotSymlink(rootDir);
32509
- await fs4.mkdir(rootDir, { recursive: true, mode: 448 });
33456
+ await fs5.mkdir(rootDir, { recursive: true, mode: 448 });
32510
33457
  await ensurePathIsNotSymlink(rootDir);
32511
- const realWorkspace = await fs4.realpath(workspaceRoot);
32512
- const realRoot = await fs4.realpath(rootDir);
33458
+ const realWorkspace = await fs5.realpath(workspaceRoot);
33459
+ const realRoot = await fs5.realpath(rootDir);
32513
33460
  if (!isPathInside(realRoot, realWorkspace)) {
32514
33461
  throw new Error(`attachment root escapes workspace: ${rootDir}`);
32515
33462
  }
@@ -32517,7 +33464,7 @@ async function ensureAttachmentRootDir(workspaceDir) {
32517
33464
  }
32518
33465
  async function ensurePathIsNotSymlink(filePath) {
32519
33466
  try {
32520
- const stat = await fs4.lstat(filePath);
33467
+ const stat = await fs5.lstat(filePath);
32521
33468
  if (stat.isSymbolicLink()) {
32522
33469
  throw new Error(`refusing to use symlinked attachment path ${filePath}`);
32523
33470
  }
@@ -32528,8 +33475,8 @@ async function ensurePathIsNotSymlink(filePath) {
32528
33475
  }
32529
33476
  }
32530
33477
  function isPathInside(childPath, parentPath) {
32531
- const rel = path5.relative(parentPath, childPath);
32532
- return rel === "" || !!rel && !rel.startsWith("..") && !path5.isAbsolute(rel);
33478
+ const rel = path6.relative(parentPath, childPath);
33479
+ return rel === "" || !!rel && !rel.startsWith("..") && !path6.isAbsolute(rel);
32533
33480
  }
32534
33481
  async function existingUsableFile(filePath, expectedSize, rootDir) {
32535
33482
  try {
@@ -32540,30 +33487,30 @@ async function existingUsableFile(filePath, expectedSize, rootDir) {
32540
33487
  }
32541
33488
  }
32542
33489
  async function localFileStatInsideRoot(filePath, rootDir) {
32543
- const stat = await fs4.lstat(filePath);
33490
+ const stat = await fs5.lstat(filePath);
32544
33491
  if (stat.isSymbolicLink()) {
32545
33492
  throw new Error(`refusing to use symlinked attachment file ${filePath}`);
32546
33493
  }
32547
33494
  if (!stat.isFile()) {
32548
33495
  throw new Error(`attachment path is not a file ${filePath}`);
32549
33496
  }
32550
- const realRoot = await fs4.realpath(rootDir);
32551
- const realFile = await fs4.realpath(filePath);
33497
+ const realRoot = await fs5.realpath(rootDir);
33498
+ const realFile = await fs5.realpath(filePath);
32552
33499
  if (!isPathInside(realFile, realRoot)) {
32553
33500
  throw new Error(`attachment file escapes workspace: ${filePath}`);
32554
33501
  }
32555
33502
  return stat;
32556
33503
  }
32557
33504
  async function localDirectoryStatInsideRoot(dirPath, rootDir) {
32558
- const stat = await fs4.lstat(dirPath);
33505
+ const stat = await fs5.lstat(dirPath);
32559
33506
  if (stat.isSymbolicLink()) {
32560
33507
  throw new Error(`refusing to use symlinked attachment directory ${dirPath}`);
32561
33508
  }
32562
33509
  if (!stat.isDirectory()) {
32563
33510
  throw new Error(`attachment path is not a directory ${dirPath}`);
32564
33511
  }
32565
- const realRoot = await fs4.realpath(rootDir);
32566
- const realDir = await fs4.realpath(dirPath);
33512
+ const realRoot = await fs5.realpath(rootDir);
33513
+ const realDir = await fs5.realpath(dirPath);
32567
33514
  if (!isPathInside(realDir, realRoot)) {
32568
33515
  throw new Error(`attachment directory escapes workspace: ${dirPath}`);
32569
33516
  }
@@ -32571,7 +33518,7 @@ async function localDirectoryStatInsideRoot(dirPath, rootDir) {
32571
33518
  }
32572
33519
  async function openLocalFileInsideRoot(filePath, rootDir) {
32573
33520
  const checkedStat = await localFileStatInsideRoot(filePath, rootDir);
32574
- const file = await fs4.open(filePath, constants.O_RDONLY | constants.O_NOFOLLOW);
33521
+ const file = await fs5.open(filePath, constants.O_RDONLY | constants.O_NOFOLLOW);
32575
33522
  let keepOpen = false;
32576
33523
  try {
32577
33524
  const openedStat = await file.stat();
@@ -32587,8 +33534,8 @@ async function openLocalFileInsideRoot(filePath, rootDir) {
32587
33534
  }
32588
33535
  }
32589
33536
  async function openLocalTempFileInsideRoot(filePath, rootDir) {
32590
- await localDirectoryStatInsideRoot(path5.dirname(filePath), rootDir);
32591
- const file = await fs4.open(filePath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
33537
+ await localDirectoryStatInsideRoot(path6.dirname(filePath), rootDir);
33538
+ const file = await fs5.open(filePath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
32592
33539
  let keepOpen = false;
32593
33540
  try {
32594
33541
  const checkedStat = await localFileStatInsideRoot(filePath, rootDir);
@@ -32613,7 +33560,7 @@ async function assertLocalFileIdentity(filePath, rootDir, expected) {
32613
33560
  async function removeLocalFileIfInside(filePath, rootDir) {
32614
33561
  try {
32615
33562
  await localFileStatInsideRoot(filePath, rootDir);
32616
- await fs4.rm(filePath, { force: true });
33563
+ await fs5.rm(filePath, { force: true });
32617
33564
  } catch {
32618
33565
  }
32619
33566
  }
@@ -32626,7 +33573,7 @@ function sameFile(a, b) {
32626
33573
  async function cleanupOldAttachmentFiles(rootDir, ttlMs, log2, preserveDirs) {
32627
33574
  let entries;
32628
33575
  try {
32629
- entries = await fs4.readdir(rootDir, { withFileTypes: true });
33576
+ entries = await fs5.readdir(rootDir, { withFileTypes: true });
32630
33577
  } catch {
32631
33578
  return;
32632
33579
  }
@@ -32634,15 +33581,15 @@ async function cleanupOldAttachmentFiles(rootDir, ttlMs, log2, preserveDirs) {
32634
33581
  await Promise.all(entries.map(async (entry) => {
32635
33582
  if (!entry.isDirectory())
32636
33583
  return;
32637
- const fullPath = path5.join(rootDir, entry.name);
33584
+ const fullPath = path6.join(rootDir, entry.name);
32638
33585
  try {
32639
- if (preserveDirs?.has(path5.resolve(fullPath)))
33586
+ if (preserveDirs?.has(path6.resolve(fullPath)))
32640
33587
  return;
32641
- const stat = await fs4.lstat(fullPath);
33588
+ const stat = await fs5.lstat(fullPath);
32642
33589
  if (!stat.isDirectory())
32643
33590
  return;
32644
33591
  if (stat.mtimeMs < cutoff) {
32645
- await fs4.rm(fullPath, { recursive: true, force: true });
33592
+ await fs5.rm(fullPath, { recursive: true, force: true });
32646
33593
  }
32647
33594
  } catch (err) {
32648
33595
  log2?.warn?.(`agent-core: failed to clean attachment temp dir ${fullPath}: ${String(err)}`);
@@ -32654,7 +33601,7 @@ async function pruneAttachmentCache(rootDir, maxBytes, log2, preserveDirs) {
32654
33601
  return;
32655
33602
  let entries;
32656
33603
  try {
32657
- entries = await fs4.readdir(rootDir, { withFileTypes: true });
33604
+ entries = await fs5.readdir(rootDir, { withFileTypes: true });
32658
33605
  } catch {
32659
33606
  return;
32660
33607
  }
@@ -32663,9 +33610,9 @@ async function pruneAttachmentCache(rootDir, maxBytes, log2, preserveDirs) {
32663
33610
  for (const entry of entries) {
32664
33611
  if (!entry.isDirectory())
32665
33612
  continue;
32666
- const fullPath = path5.join(rootDir, entry.name);
33613
+ const fullPath = path6.join(rootDir, entry.name);
32667
33614
  try {
32668
- const stat = await fs4.lstat(fullPath);
33615
+ const stat = await fs5.lstat(fullPath);
32669
33616
  if (!stat.isDirectory())
32670
33617
  continue;
32671
33618
  const size = await directorySize(fullPath);
@@ -32681,10 +33628,10 @@ async function pruneAttachmentCache(rootDir, maxBytes, log2, preserveDirs) {
32681
33628
  for (const dir of dirs) {
32682
33629
  if (total <= maxBytes)
32683
33630
  break;
32684
- if (preserveDirs?.has(path5.resolve(dir.path)))
33631
+ if (preserveDirs?.has(path6.resolve(dir.path)))
32685
33632
  continue;
32686
33633
  try {
32687
- await fs4.rm(dir.path, { recursive: true, force: true });
33634
+ await fs5.rm(dir.path, { recursive: true, force: true });
32688
33635
  total -= dir.size;
32689
33636
  } catch (err) {
32690
33637
  log2?.warn?.(`agent-core: failed to prune attachment cache dir ${dir.path}: ${String(err)}`);
@@ -32693,12 +33640,12 @@ async function pruneAttachmentCache(rootDir, maxBytes, log2, preserveDirs) {
32693
33640
  }
32694
33641
  async function directorySize(dirPath) {
32695
33642
  let total = 0;
32696
- const entries = await fs4.readdir(dirPath, { withFileTypes: true });
33643
+ const entries = await fs5.readdir(dirPath, { withFileTypes: true });
32697
33644
  for (const entry of entries) {
32698
- const fullPath = path5.join(dirPath, entry.name);
33645
+ const fullPath = path6.join(dirPath, entry.name);
32699
33646
  let stat;
32700
33647
  try {
32701
- stat = await fs4.lstat(fullPath);
33648
+ stat = await fs5.lstat(fullPath);
32702
33649
  } catch {
32703
33650
  continue;
32704
33651
  }
@@ -32713,10 +33660,10 @@ async function directorySize(dirPath) {
32713
33660
  return total;
32714
33661
  }
32715
33662
  function activeDirsForRoot(rootDir) {
32716
- const root = path5.resolve(rootDir);
33663
+ const root = path6.resolve(rootDir);
32717
33664
  const dirs = /* @__PURE__ */ new Set();
32718
33665
  for (const dir of activeAttachmentDirs) {
32719
- if (dir === root || dir.startsWith(`${root}${path5.sep}`)) {
33666
+ if (dir === root || dir.startsWith(`${root}${path6.sep}`)) {
32720
33667
  dirs.add(dir);
32721
33668
  }
32722
33669
  }
@@ -32813,9 +33760,9 @@ async function writeResponseToFileWithLimit(res, filePath, maxBytes, rootDir) {
32813
33760
  }
32814
33761
  writtenStat = await file.stat();
32815
33762
  await closeFile();
32816
- await localDirectoryStatInsideRoot(path5.dirname(filePath), rootDir);
33763
+ await localDirectoryStatInsideRoot(path6.dirname(filePath), rootDir);
32817
33764
  await assertLocalFileIdentity(tmpPath, rootDir, writtenStat);
32818
- await fs4.rename(tmpPath, filePath);
33765
+ await fs5.rename(tmpPath, filePath);
32819
33766
  completed = true;
32820
33767
  return written;
32821
33768
  } finally {
@@ -32831,9 +33778,9 @@ async function writeResponseToFileWithLimit(res, filePath, maxBytes, rootDir) {
32831
33778
  }
32832
33779
  }
32833
33780
  function localFileName(attachmentId, fileName, mimeType) {
32834
- const safeName = sanitizePathSegment(path5.basename(fileName || attachmentId));
32835
- const ext = path5.extname(safeName) || extensionForMime(mimeType);
32836
- const stem = path5.basename(safeName, path5.extname(safeName)) || attachmentId;
33781
+ const safeName = sanitizePathSegment(path6.basename(fileName || attachmentId));
33782
+ const ext = path6.extname(safeName) || extensionForMime(mimeType);
33783
+ const stem = path6.basename(safeName, path6.extname(safeName)) || attachmentId;
32837
33784
  return `${sanitizePathSegment(attachmentId)}-${stem}${ext}`;
32838
33785
  }
32839
33786
  function extensionForMime(mimeType) {
@@ -33616,6 +34563,9 @@ var CodexAppServerAdapter = class {
33616
34563
  if (this.opts.contextFilePath) {
33617
34564
  env.PRLL_CONTEXT_FILE = this.opts.contextFilePath;
33618
34565
  }
34566
+ if (this.opts.contextDirPath) {
34567
+ env.PRLL_CONTEXT_DIR = this.opts.contextDirPath;
34568
+ }
33619
34569
  if (!env.PRLL_WIKI_MOUNT_ROOT?.trim()) {
33620
34570
  env.PRLL_WIKI_MOUNT_ROOT = this.opts.workspaceDir;
33621
34571
  }
@@ -33761,7 +34711,7 @@ var TurnSink = class {
33761
34711
  }
33762
34712
  };
33763
34713
  function ensureGitRepo(workingDirectory) {
33764
- fs5.mkdirSync(workingDirectory, { recursive: true });
34714
+ fs6.mkdirSync(workingDirectory, { recursive: true });
33765
34715
  try {
33766
34716
  execSync2("git rev-parse --is-inside-work-tree", { cwd: workingDirectory, stdio: "pipe" });
33767
34717
  ensureLocalAttachmentGitExclude(workingDirectory);
@@ -33831,8 +34781,8 @@ function errToString(err) {
33831
34781
  }
33832
34782
 
33833
34783
  // ts/codex-agent/dist/session-manager.js
33834
- import * as fs6 from "node:fs";
33835
- import * as path6 from "node:path";
34784
+ import * as fs7 from "node:fs";
34785
+ import * as path7 from "node:path";
33836
34786
  var CodexSessionManager = class {
33837
34787
  mainSessionKey;
33838
34788
  stateFilePath;
@@ -33869,14 +34819,14 @@ var CodexSessionManager = class {
33869
34819
  clearMainThread() {
33870
34820
  this.threadIds.delete(this.mainSessionKey);
33871
34821
  try {
33872
- fs6.rmSync(this.stateFilePath, { force: true });
34822
+ fs7.rmSync(this.stateFilePath, { force: true });
33873
34823
  } catch (error) {
33874
34824
  this.logger?.warn(`codex-agent: failed to clear main thread state at ${this.stateFilePath}: ${String(error)}`);
33875
34825
  }
33876
34826
  }
33877
34827
  restore() {
33878
34828
  try {
33879
- const raw = fs6.readFileSync(this.stateFilePath, "utf8");
34829
+ const raw = fs7.readFileSync(this.stateFilePath, "utf8");
33880
34830
  const parsed = JSON.parse(raw);
33881
34831
  if (parsed.runtimeKey === this.mainSessionKey && typeof parsed.threadId === "string" && parsed.threadId.trim()) {
33882
34832
  this.threadIds.set(this.mainSessionKey, parsed.threadId.trim());
@@ -33889,10 +34839,10 @@ var CodexSessionManager = class {
33889
34839
  }
33890
34840
  persist(threadId) {
33891
34841
  try {
33892
- fs6.mkdirSync(path6.dirname(this.stateFilePath), { recursive: true });
34842
+ fs7.mkdirSync(path7.dirname(this.stateFilePath), { recursive: true });
33893
34843
  const tmpPath = `${this.stateFilePath}.tmp`;
33894
- fs6.writeFileSync(tmpPath, JSON.stringify({ runtimeKey: this.mainSessionKey, threadId }, null, 2));
33895
- fs6.renameSync(tmpPath, this.stateFilePath);
34844
+ fs7.writeFileSync(tmpPath, JSON.stringify({ runtimeKey: this.mainSessionKey, threadId }, null, 2));
34845
+ fs7.renameSync(tmpPath, this.stateFilePath);
33896
34846
  } catch (error) {
33897
34847
  this.logger?.warn(`codex-agent: failed to persist thread state at ${this.stateFilePath}: ${String(error)}`);
33898
34848
  }
@@ -33900,8 +34850,8 @@ var CodexSessionManager = class {
33900
34850
  };
33901
34851
 
33902
34852
  // ts/codex-agent/dist/workspace.js
33903
- import * as fs7 from "node:fs";
33904
- import * as path7 from "node:path";
34853
+ import * as fs8 from "node:fs";
34854
+ import * as path8 from "node:path";
33905
34855
 
33906
34856
  // ts/node_modules/.pnpm/smol-toml@1.6.1/node_modules/smol-toml/dist/error.js
33907
34857
  function getLineColFromPtr(string, ptr) {
@@ -34595,12 +35545,12 @@ function parse(toml, { maxDepth = 1e3, integersAsBigInt } = {}) {
34595
35545
 
34596
35546
  // ts/codex-agent/dist/workspace.js
34597
35547
  function ensureWorkspaceTrusted(codexHome, workspaceDir, log2) {
34598
- const configPath = path7.join(codexHome, "config.toml");
34599
- const normalizedPath = path7.resolve(workspaceDir);
35548
+ const configPath = path8.join(codexHome, "config.toml");
35549
+ const normalizedPath = path8.resolve(workspaceDir);
34600
35550
  try {
34601
35551
  let content = "";
34602
35552
  try {
34603
- content = fs7.readFileSync(configPath, "utf8");
35553
+ content = fs8.readFileSync(configPath, "utf8");
34604
35554
  } catch (err) {
34605
35555
  if (err.code !== "ENOENT")
34606
35556
  throw err;
@@ -34640,14 +35590,14 @@ ${trustLine}
34640
35590
  content = content.substring(0, afterHeader) + trustLine + "\n" + content.substring(afterHeader);
34641
35591
  }
34642
35592
  }
34643
- fs7.writeFileSync(configPath, content, "utf8");
35593
+ fs8.writeFileSync(configPath, content, "utf8");
34644
35594
  } else if (projects?.[normalizedPath] !== void 0) {
34645
35595
  log2?.warn(`Codex config.toml has non-canonical header for ${normalizedPath}; skipping trust write`);
34646
35596
  } else if (projects && !content.includes("[projects.")) {
34647
35597
  log2?.warn(`Codex config.toml uses inline table for projects; skipping trust write for ${normalizedPath}`);
34648
35598
  } else {
34649
- fs7.mkdirSync(codexHome, { recursive: true });
34650
- fs7.appendFileSync(configPath, `
35599
+ fs8.mkdirSync(codexHome, { recursive: true });
35600
+ fs8.appendFileSync(configPath, `
34651
35601
  ${sectionHeader}
34652
35602
  ${trustLine}
34653
35603
  `, "utf8");
@@ -34664,12 +35614,12 @@ function isParallProxyMode(env = process.env) {
34664
35614
  return baseUrl === apiUrl || baseUrl.startsWith(`${apiUrl}/`);
34665
35615
  }
34666
35616
  function ensureParallProvider(codexHome, apiUrl, log2) {
34667
- const configPath = path7.join(codexHome, "config.toml");
35617
+ const configPath = path8.join(codexHome, "config.toml");
34668
35618
  const baseUrl = apiUrl.replace(/\/$/, "") + "/api/llm/v1";
34669
35619
  try {
34670
35620
  let content = "";
34671
35621
  try {
34672
- content = fs7.readFileSync(configPath, "utf8");
35622
+ content = fs8.readFileSync(configPath, "utf8");
34673
35623
  } catch (err) {
34674
35624
  if (err.code !== "ENOENT")
34675
35625
  throw err;
@@ -34700,8 +35650,8 @@ function ensureParallProvider(codexHome, apiUrl, log2) {
34700
35650
  } else {
34701
35651
  content = content.trimEnd() + "\n\n" + providerBlock + "\n";
34702
35652
  }
34703
- fs7.mkdirSync(codexHome, { recursive: true });
34704
- fs7.writeFileSync(configPath, content, "utf8");
35653
+ fs8.mkdirSync(codexHome, { recursive: true });
35654
+ fs8.writeFileSync(configPath, content, "utf8");
34705
35655
  } catch (err) {
34706
35656
  throw new Error(`failed to write Parall provider config: ${String(err)}`);
34707
35657
  }
@@ -34718,18 +35668,18 @@ function ensureCodexWorkspace(workspaceDir, log2, agentIdentity) {
34718
35668
  PRLL_REFERENCE_GUIDE,
34719
35669
  buildSkillReferences(workspaceDir)
34720
35670
  ].join("\n\n");
34721
- fs7.mkdirSync(workspaceDir, { recursive: true });
34722
- const parallDir = path7.join(workspaceDir, ".parall");
34723
- fs7.mkdirSync(parallDir, { recursive: true });
34724
- fs7.writeFileSync(path7.join(parallDir, "system-prompt.md"), systemPrompt, "utf8");
34725
- writeSkillFiles(path7.join(parallDir, "skills"));
34726
- const codexConfigDir = path7.join(workspaceDir, ".codex");
34727
- fs7.mkdirSync(codexConfigDir, { recursive: true });
35671
+ fs8.mkdirSync(workspaceDir, { recursive: true });
35672
+ const parallDir = path8.join(workspaceDir, ".parall");
35673
+ fs8.mkdirSync(parallDir, { recursive: true });
35674
+ fs8.writeFileSync(path8.join(parallDir, "system-prompt.md"), systemPrompt, "utf8");
35675
+ writeSkillFiles(path8.join(parallDir, "skills"));
35676
+ const codexConfigDir = path8.join(workspaceDir, ".codex");
35677
+ fs8.mkdirSync(codexConfigDir, { recursive: true });
34728
35678
  const toml = `developer_instructions = """
34729
35679
  ${systemPrompt.replace(/\\/g, "\\\\").replace(/"""/g, '\\"""')}
34730
35680
  """
34731
35681
  `;
34732
- fs7.writeFileSync(path7.join(codexConfigDir, "config.toml"), toml, "utf8");
35682
+ fs8.writeFileSync(path8.join(codexConfigDir, "config.toml"), toml, "utf8");
34733
35683
  }
34734
35684
 
34735
35685
  // ts/codex-agent/dist/index.js
@@ -34804,14 +35754,13 @@ async function main() {
34804
35754
  log: agentLog
34805
35755
  });
34806
35756
  const platformDefaults = await configMgr.fetch();
34807
- let platformManaged = isPlatformManagedProfile(me.agent_profile);
34808
- let platformModelOverride = isPlatformModelOverride(me.agent_profile);
35757
+ const platformModelOverride = deriveModelIsPin(platformDefaults, me.agent_profile);
34809
35758
  const resolvedModel = resolveRuntimeModel(platformModelOverride, platformDefaults.model, config.model);
34810
- const resolvedEffort = platformManaged ? platformDefaults.thinkingEffort ?? config.reasoningEffort : config.reasoningEffort;
35759
+ const resolvedEffort = platformDefaults.thinkingEffort ?? config.reasoningEffort;
34811
35760
  if (platformDefaults.model)
34812
- agentLog.info(`platform config: model=${platformDefaults.model} \u2192 resolved=${resolvedModel ?? "cli-default"} (${platformModelOverride ? "platform-managed" : "floor/self"})`);
35761
+ agentLog.info(`platform config: model=${platformDefaults.model} \u2192 resolved=${resolvedModel ?? "cli-default"} (${platformModelOverride ? "pin/override" : "floor"})`);
34813
35762
  if (platformDefaults.thinkingEffort)
34814
- agentLog.info(`platform config: reasoning_effort=${platformDefaults.thinkingEffort} (${platformModelOverride ? "applied" : "ignored, model_management=self"})`);
35763
+ agentLog.info(`platform config: reasoning_effort=${platformDefaults.thinkingEffort}`);
34815
35764
  const adapter = new CodexAppServerAdapter({
34816
35765
  codexBin: config.codexBin,
34817
35766
  codexHome: config.codexHome,
@@ -34823,8 +35772,25 @@ async function main() {
34823
35772
  sessionManager,
34824
35773
  log: agentLog,
34825
35774
  contextFilePath: mainContextFilePath,
35775
+ contextDirPath: dispatchContextDirPath(config.stateDir),
34826
35776
  useParallProvider
34827
35777
  });
35778
+ const refreshPlatformConfig = async () => {
35779
+ const updated = await configMgr.fetch();
35780
+ let refreshedProfile;
35781
+ if (updated.modelIsPin === void 0) {
35782
+ try {
35783
+ refreshedProfile = (await getAgentMeWithLegacyFallback(client, config.orgId)).agent_profile;
35784
+ } catch (err) {
35785
+ agentLog.warn(`platform config refresh: legacy /agents/me fallback failed: ${String(err)}`);
35786
+ }
35787
+ }
35788
+ const isPin = deriveModelIsPin(updated, refreshedProfile);
35789
+ adapter.updateConfig({
35790
+ model: resolveRuntimeModel(isPin, updated.model, config.model) ?? null,
35791
+ reasoningEffort: updated.thinkingEffort ?? config.reasoningEffort ?? null
35792
+ });
35793
+ };
34828
35794
  const gateway = new ParallAgentGateway({
34829
35795
  accountId: agentUserId,
34830
35796
  client,
@@ -34852,26 +35818,9 @@ async function main() {
34852
35818
  dispatchDeadlineMs: parseDispatchDeadlineMs(process.env.PRLL_DISPATCH_DEADLINE_MS),
34853
35819
  contextFilePathForSession: (sessionKey) => contextFilePathForSession(config.stateDir, sessionKey),
34854
35820
  stepIdFilePathForSession: (sessionKey) => stepIdFilePathForSession(config.stateDir, sessionKey),
34855
- onConfigUpdate: async () => {
34856
- const refreshed = await getAgentMeWithLegacyFallback(client, config.orgId);
34857
- platformManaged = isPlatformManagedProfile(refreshed.agent_profile);
34858
- platformModelOverride = isPlatformModelOverride(refreshed.agent_profile);
34859
- const updated = await configMgr.fetch();
34860
- adapter.updateConfig({
34861
- model: resolveRuntimeModel(platformModelOverride, updated.model, config.model) ?? null,
34862
- reasoningEffort: platformManaged ? updated.thinkingEffort ?? config.reasoningEffort : config.reasoningEffort ?? null
34863
- });
34864
- },
34865
- onSessionReady: async () => {
34866
- const refreshed = await getAgentMeWithLegacyFallback(client, config.orgId);
34867
- platformManaged = isPlatformManagedProfile(refreshed.agent_profile);
34868
- platformModelOverride = isPlatformModelOverride(refreshed.agent_profile);
34869
- const updated = await configMgr.fetch();
34870
- adapter.updateConfig({
34871
- model: resolveRuntimeModel(platformModelOverride, updated.model, config.model) ?? null,
34872
- reasoningEffort: platformManaged ? updated.thinkingEffort ?? config.reasoningEffort : config.reasoningEffort ?? null
34873
- });
34874
- },
35821
+ dispatchContextDir: dispatchContextDirPath(config.stateDir),
35822
+ onConfigUpdate: refreshPlatformConfig,
35823
+ onSessionReady: refreshPlatformConfig,
34875
35824
  onNewSession: () => {
34876
35825
  sessionManager.clearMainThread();
34877
35826
  },