@parall/daemon 1.36.1 → 1.37.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 +307 -27
  3. package/bundle/parall-claude-agent.js +363 -50
  4. package/bundle/parall-codex-agent.js +362 -48
  5. package/bundle/parall-daemon.js +33473 -31706
  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
@@ -26647,6 +26647,17 @@ function buildEventBody(event) {
26647
26647
  if (event.attachedUri)
26648
26648
  lines.push(`[Attached: ${sanitizeMeta(event.attachedUri)}]`);
26649
26649
  lines.push("", event.body);
26650
+ } else if (event.type === "channel_message") {
26651
+ lines.push(`[Event: channel.message]`);
26652
+ const providerLabel = sanitizeMeta(event.channelProvider ?? "external IM");
26653
+ const convLabel = event.channelExternalConversationId ? `${sanitizeMeta(event.channelExternalConversationId)} (${sanitizeMeta(event.channelConversationType ?? "conversation")})` : sanitizeMeta(event.channelConversationType ?? "conversation");
26654
+ lines.push(`[Channel: ${providerLabel} | conversation: ${convLabel}]`);
26655
+ lines.push(`[From: ${sanitizeMeta(event.senderName)} (external user, not a Parall member)]`);
26656
+ if (event.channelExternalMessageId) {
26657
+ lines.push(`[External message ID: ${sanitizeMeta(event.channelExternalMessageId)}]`);
26658
+ }
26659
+ 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.]`);
26660
+ lines.push("", event.body);
26650
26661
  } else if (event.type === "external_trigger") {
26651
26662
  lines.push(`[Event: external.trigger]`);
26652
26663
  lines.push(`[Trigger: prll://${event.targetId}]`);
@@ -26688,7 +26699,7 @@ function buildSendMessageHint(event) {
26688
26699
  }
26689
26700
  if (event.targetId.startsWith("cht_")) {
26690
26701
  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>`;
26702
+ <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
26703
  }
26693
26704
  if (event.targetId.startsWith("tsk_")) {
26694
26705
  return `
@@ -26697,6 +26708,14 @@ function buildSendMessageHint(event) {
26697
26708
  if (event.targetId.startsWith("sch_")) {
26698
26709
  return `
26699
26710
  <system-reminder>To communicate, use the CLI: \`parall messages send\` / \`parall dm\`. Your plain text output is not delivered.</system-reminder>`;
26711
+ }
26712
+ if (event.type === "channel_message") {
26713
+ const clipLabel = event.channelProvider ? `the \`${event.channelProvider}\` clip's` : "your channel provider clip's";
26714
+ const apiLabel = event.channelProvider ?? "external platform";
26715
+ const target = event.channelExternalConversationId ? `{"chat_id": "${event.channelExternalConversationId}", "text": "..."}` : `{"chat_id": "<conversation id>", "text": "..."}`;
26716
+ const threadAlt = event.channelExternalMessageId ? ` To reply in-thread to this specific message, use {"message_id": "${event.channelExternalMessageId}", "text": "..."} instead.` : "";
26717
+ return `
26718
+ <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
26719
  }
26701
26720
  if (event.type === "external_trigger" || event.targetId.startsWith("xtr_")) {
26702
26721
  return `
@@ -26904,6 +26923,8 @@ Or upload first and reuse across chats:
26904
26923
  parall messages send prll://cht_aaa --attachment att_yyy --text "Report"
26905
26924
  parall messages send prll://cht_bbb --attachment att_yyy --text "FYI"
26906
26925
 
26926
+ 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 \`$(...)\`.
26927
+
26907
26928
  ### When to reference
26908
26929
 
26909
26930
  - **Origin** \u2014 always link the message or task that triggered your work
@@ -26954,11 +26975,21 @@ To say something in a chat, you **must** invoke the Parall CLI via your shell/ex
26954
26975
 
26955
26976
  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
26977
 
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
26978
+ - \`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)
26979
+ - \`parall dm prll://usr_xxx --text-file - [--no-reply]\` \u2014 direct message another user
26959
26980
  - \`parall tasks update prll://tsk_xxx --status in_progress\` \u2014 task state
26960
26981
  - \`parall no-reply [--reason "..."]\` \u2014 explicitly declare this turn silent (audit signal; not required for silence, just clarifies intent)
26961
26982
 
26983
+ **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:
26984
+
26985
+ \`\`\`bash
26986
+ parall messages send prll://cht_xxx --text-file - <<'EOF'
26987
+ That costs $1,000, and $(whoami) stays literal. I'm on it.
26988
+ EOF
26989
+ \`\`\`
26990
+
26991
+ Keep \`--text "..."\` for short literals with no \`$\`, backtick, or apostrophe.
26992
+
26962
26993
  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
26994
 
26964
26995
  ## Attachments
@@ -27003,7 +27034,7 @@ Messages may arrive with a \`[Thread: prll://msg_xxx]\` line in the event block,
27003
27034
 
27004
27035
  ### CLI usage
27005
27036
  - 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>\`
27037
+ - Reply in a thread: \`parall messages send <chatId> --text-file /tmp/reply.md --thread-root-id <msgId>\`
27007
27038
  - Read thread replies: \`parall messages list <chatId> --thread-root-id <msgId>\`
27008
27039
 
27009
27040
  ### Thread etiquette
@@ -27104,6 +27135,7 @@ var ENDPOINTS = {
27104
27135
  // Org-scoped
27105
27136
  ORG: (orgId) => `${API_BASE}/orgs/${orgId}`,
27106
27137
  ORG_MEMBERS: (orgId) => `${API_BASE}/orgs/${orgId}/members`,
27138
+ ORG_MEMBERS_FORMER: (orgId) => `${API_BASE}/orgs/${orgId}/members/former`,
27107
27139
  TEAMS: (orgId) => `${API_BASE}/orgs/${orgId}/teams`,
27108
27140
  ORG_MEMBERS_ONLINE: (orgId) => `${API_BASE}/orgs/${orgId}/members/online`,
27109
27141
  ORG_MEMBER: (orgId, userId) => `${API_BASE}/orgs/${orgId}/members/${userId}`,
@@ -27253,6 +27285,19 @@ var ENDPOINTS = {
27253
27285
  EXTERNAL_TRIGGER: (orgId, triggerId) => `${API_BASE}/orgs/${orgId}/external-triggers/${triggerId}`,
27254
27286
  EXTERNAL_TRIGGER_RUNS: (orgId) => `${API_BASE}/orgs/${orgId}/external-trigger-runs`,
27255
27287
  EXTERNAL_TRIGGER_RUN: (orgId, runId) => `${API_BASE}/orgs/${orgId}/external-trigger-runs/${runId}`,
27288
+ // External IM channel (org-scoped, platform-mediated Feishu/Slack)
27289
+ CHANNEL_CONNECTIONS: (orgId) => `${API_BASE}/orgs/${orgId}/channel-connections`,
27290
+ CHANNEL_CONNECTION: (orgId, connectionId) => `${API_BASE}/orgs/${orgId}/channel-connections/${connectionId}`,
27291
+ CHANNEL_CONNECTION_CREDENTIALS: (orgId, connectionId) => `${API_BASE}/orgs/${orgId}/channel-connections/${connectionId}/credentials`,
27292
+ CHANNEL_CONNECTION_INGRESS_TOKEN_REGENERATE: (orgId, connectionId) => `${API_BASE}/orgs/${orgId}/channel-connections/${connectionId}/ingress-token/regenerate`,
27293
+ CHANNEL_CONNECTION_CONVERSATIONS: (orgId, connectionId) => `${API_BASE}/orgs/${orgId}/channel-connections/${connectionId}/conversations`,
27294
+ CHANNEL_CONVERSATION: (orgId, conversationId) => `${API_BASE}/orgs/${orgId}/channel-conversations/${conversationId}`,
27295
+ CHANNEL_CONVERSATION_MESSAGES: (orgId, conversationId) => `${API_BASE}/orgs/${orgId}/channel-conversations/${conversationId}/messages`,
27296
+ CHANNEL_CONVERSATION_SESSION: (orgId, conversationId) => `${API_BASE}/orgs/${orgId}/channel-conversations/${conversationId}/session`,
27297
+ CHANNEL_MESSAGE: (orgId, messageId) => `${API_BASE}/orgs/${orgId}/channel-messages/${messageId}`,
27298
+ CHANNEL_PROVISIONING: (orgId) => `${API_BASE}/orgs/${orgId}/channel-provisioning`,
27299
+ CHANNEL_PROVISIONING_SESSION: (orgId, sessionId) => `${API_BASE}/orgs/${orgId}/channel-provisioning/${sessionId}`,
27300
+ CHANNEL_PROVISIONING_CANCEL: (orgId, sessionId) => `${API_BASE}/orgs/${orgId}/channel-provisioning/${sessionId}/cancel`,
27256
27301
  // Invitations (org-scoped, admin)
27257
27302
  ORG_INVITATIONS: (orgId) => `${API_BASE}/orgs/${orgId}/invitations`,
27258
27303
  ORG_INVITATION: (orgId, invId) => `${API_BASE}/orgs/${orgId}/invitations/${invId}`,
@@ -27270,9 +27315,16 @@ var ENDPOINTS = {
27270
27315
  INVITE_LINK_JOIN: `${API_BASE}/invite-link/join`,
27271
27316
  // Wikis (org-scoped, served by wiki-service)
27272
27317
  WIKIS: (orgId) => `${WIKI_BASE}/orgs/${orgId}/wikis`,
27318
+ // Recycle bin — soft-deleted wikis (owner only). Must precede WIKI in the
27319
+ // backend router so `deleted` isn't captured as a {wikiId}; the SDK builder
27320
+ // is just a string.
27321
+ WIKIS_DELETED: (orgId) => `${WIKI_BASE}/orgs/${orgId}/wikis/deleted`,
27322
+ // Detail URL — also reused for DELETE (soft-delete) and `${WIKI}/restore`.
27273
27323
  WIKI: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}`,
27324
+ WIKI_RESTORE: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/restore`,
27274
27325
  WIKI_TREE: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/tree`,
27275
27326
  WIKI_BLOB: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/blob`,
27327
+ WIKI_COPY: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/copy`,
27276
27328
  WIKI_NODE_SECTIONS: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/node-sections`,
27277
27329
  WIKI_SEARCH: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/search`,
27278
27330
  WIKI_PAGE_INDEX: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/page-index`,
@@ -27355,6 +27407,14 @@ var ENDPOINTS = {
27355
27407
  BILLING_AUTO_RELOAD: (orgId) => `${API_BASE}/orgs/${orgId}/billing/auto-reload`,
27356
27408
  BILLING_SETUP_INTENT: (orgId) => `${API_BASE}/orgs/${orgId}/billing/setup-intent`,
27357
27409
  COMPUTE_PRICING: () => `${API_BASE}/billing/compute-pricing`,
27410
+ // Runtime capability table (public, no auth) — SSOT for the create/settings
27411
+ // interlock: per-runtime compute modes, native model family, cross-family
27412
+ // availability, and recommended model.
27413
+ RUNTIMES: () => `${API_BASE}/runtimes`,
27414
+ // Model catalog (public, no auth) — served from the server's DB-backed
27415
+ // snapshot. Optional include_model keeps an agent's pinned hidden legacy
27416
+ // model representable in settings pickers.
27417
+ MODELS: (includeModel) => includeModel ? `${API_BASE}/models?include_model=${encodeURIComponent(includeModel)}` : `${API_BASE}/models`,
27358
27418
  // ADMIN_GRANTS intentionally NOT exported here — it sits under the
27359
27419
  // unauthenticated `/internal/admin/*` surface and must not bleed into the
27360
27420
  // public SDK. Admin dashboard hits the URL directly from its own client.
@@ -27367,6 +27427,7 @@ var ENDPOINTS = {
27367
27427
  AGENT_CLIP: (orgId, agentId, clipId) => `${CLIP_BASE}/orgs/${orgId}/agents/${agentId}/clips/${clipId}`,
27368
27428
  CLIP_INVOKE: (orgId) => `${CLIP_BASE}/orgs/${orgId}/invoke`,
27369
27429
  CLIP_ONLINE: (orgId) => `${CLIP_BASE}/orgs/${orgId}/online`,
27430
+ BROWSER_RUNTIME_STATUS: (orgId) => `${CLIP_BASE}/orgs/${orgId}/browser-runtime/status`,
27370
27431
  BROWSER_PROFILES: (orgId) => `${CLIP_BASE}/orgs/${orgId}/browser-profiles`,
27371
27432
  BROWSER_PROFILE: (orgId, profileId) => `${CLIP_BASE}/orgs/${orgId}/browser-profiles/${profileId}`,
27372
27433
  BROWSER_PROFILE_OPEN: (orgId, profileId) => `${CLIP_BASE}/orgs/${orgId}/browser-profiles/${profileId}/open`,
@@ -27420,10 +27481,13 @@ var WS_EVENTS = {
27420
27481
  INVITATION_REVOKED: "invitation.revoked",
27421
27482
  ORG_JOIN_REQUEST_NEW: "org.join_request.new",
27422
27483
  ORG_INVITE_LINK_JOINED: "org.invite_link.joined",
27484
+ ORG_MEMBER_REMOVED: "org.member.removed",
27423
27485
  AGENT_CONFIG_UPDATE: "agent_config.update",
27424
27486
  PRESENCE_UPDATE: "presence.update",
27425
27487
  WIKI_CHANGESET_CREATED: "wiki.changeset.created",
27426
27488
  WIKI_CHANGESET_UPDATED: "wiki.changeset.updated",
27489
+ WIKI_DELETED: "wiki.deleted",
27490
+ WIKI_RESTORED: "wiki.restored",
27427
27491
  COMMENT_CREATED: "comment.created",
27428
27492
  COMMENT_UPDATED: "comment.updated",
27429
27493
  COMMENT_DELETED: "comment.deleted",
@@ -27451,6 +27515,7 @@ var WS_EVENTS = {
27451
27515
  MACHINE_FILESYSTEM_BROWSE: "machine.filesystem.browse",
27452
27516
  MACHINE_UPDATE: "machine.update",
27453
27517
  MACHINE_CONFIG_UPDATED: "machine.config.updated",
27518
+ MACHINE_AGENT_CONFIG_UPDATED: "machine.agent_config.updated",
27454
27519
  MACHINE_CLIP_SYNC: "machine.clip.sync",
27455
27520
  MACHINE_BROWSER_PROFILE_LIFECYCLE: "machine.browser_profile.lifecycle",
27456
27521
  MACHINE_BROWSER_PROFILE_VIEWER: "machine.browser_profile.viewer",
@@ -27790,6 +27855,13 @@ var ParallClient = class _ParallClient {
27790
27855
  const res = await this.request("GET", ENDPOINTS.ORG_MEMBERS(orgId));
27791
27856
  return res.data;
27792
27857
  }
27858
+ /** User IDs of soft-removed (former) org members — for marking their avatar /
27859
+ * name as "left" where dormant relations still surface them (DMs, task
27860
+ * assignees, message history). */
27861
+ async getFormerMemberIds(orgId) {
27862
+ const res = await this.request("GET", ENDPOINTS.ORG_MEMBERS_FORMER(orgId));
27863
+ return res.user_ids ?? [];
27864
+ }
27793
27865
  async getTeams(orgId) {
27794
27866
  const res = await this.request("GET", ENDPOINTS.TEAMS(orgId));
27795
27867
  return res.data;
@@ -28278,20 +28350,25 @@ var ParallClient = class _ParallClient {
28278
28350
  return res.data;
28279
28351
  }
28280
28352
  /** `POST /machines/me/browser-profiles/{profileId}/status` — report runtime status. */
28281
- async reportBrowserProfileStatus(profileId, status, errorMsg) {
28353
+ async reportBrowserProfileStatus(profileId, status, errorMsg, generation) {
28282
28354
  await this.request("POST", ENDPOINTS.MACHINES_ME_BROWSER_PROFILE_STATUS(profileId), {
28283
28355
  status,
28284
- ...errorMsg ? { error_msg: errorMsg } : {}
28356
+ ...errorMsg ? { error_msg: errorMsg } : {},
28357
+ ...generation !== void 0 ? { generation } : {}
28285
28358
  });
28286
28359
  }
28287
28360
  /**
28288
28361
  * `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
28362
+ * report daemon state. The daemon calls this on startup and again whenever
28363
+ * periodic runtime re-detection produces a CHANGED result (there is no
28364
+ * fixed-cadence keepalive loop today — steady state posts nothing). All
28291
28365
  * fields are optional and only persisted when changed:
28292
28366
  * - `daemonVersion` — the running bundle/launcher version.
28293
28367
  * - `selfUpdateCapable` — whether the daemon can act on a machine.update
28294
28368
  * signal (true under a service manager, false for bare `npx` foreground).
28369
+ * - `detectedRuntimes` — runtime CLIs found on the host. Omitted = no
28370
+ * report this beat (server keeps the stored value); [] = detection ran
28371
+ * and found nothing (server clears to empty).
28295
28372
  */
28296
28373
  async postMachineHeartbeat(opts) {
28297
28374
  const body = {};
@@ -28299,6 +28376,8 @@ var ParallClient = class _ParallClient {
28299
28376
  body.daemon_version = opts.daemonVersion;
28300
28377
  if (opts?.selfUpdateCapable !== void 0)
28301
28378
  body.self_update_capable = opts.selfUpdateCapable;
28379
+ if (opts?.detectedRuntimes !== void 0)
28380
+ body.detected_runtimes = opts.detectedRuntimes;
28302
28381
  return this.request("POST", ENDPOINTS.MACHINES_ME_HEALTH, Object.keys(body).length > 0 ? body : void 0);
28303
28382
  }
28304
28383
  async reportAgentWorkspaceState(agentId, state) {
@@ -28617,6 +28696,59 @@ var ParallClient = class _ParallClient {
28617
28696
  async deleteExternalConnection(orgId, connectionId) {
28618
28697
  return this.request("DELETE", ENDPOINTS.EXTERNAL_CONNECTION(orgId, connectionId));
28619
28698
  }
28699
+ // ---- External IM channel (org-scoped) ----
28700
+ async createChannelConnection(orgId, input) {
28701
+ return this.request("POST", ENDPOINTS.CHANNEL_CONNECTIONS(orgId), input);
28702
+ }
28703
+ async listChannelConnections(orgId) {
28704
+ return this.request("GET", ENDPOINTS.CHANNEL_CONNECTIONS(orgId));
28705
+ }
28706
+ async getChannelConnection(orgId, connectionId) {
28707
+ return this.request("GET", ENDPOINTS.CHANNEL_CONNECTION(orgId, connectionId));
28708
+ }
28709
+ async updateChannelConnection(orgId, connectionId, patch) {
28710
+ return this.request("PATCH", ENDPOINTS.CHANNEL_CONNECTION(orgId, connectionId), patch);
28711
+ }
28712
+ async archiveChannelConnection(orgId, connectionId) {
28713
+ return this.request("DELETE", ENDPOINTS.CHANNEL_CONNECTION(orgId, connectionId));
28714
+ }
28715
+ async deliverChannelCredentials(orgId, connectionId, credentials) {
28716
+ return this.request("POST", ENDPOINTS.CHANNEL_CONNECTION_CREDENTIALS(orgId, connectionId), credentials);
28717
+ }
28718
+ async regenerateChannelIngressToken(orgId, connectionId) {
28719
+ return this.request("POST", ENDPOINTS.CHANNEL_CONNECTION_INGRESS_TOKEN_REGENERATE(orgId, connectionId));
28720
+ }
28721
+ /** Start a Feishu one-click provisioning session (device flow QR). */
28722
+ async initiateChannelProvisioning(orgId, input) {
28723
+ return this.request("POST", ENDPOINTS.CHANNEL_PROVISIONING(orgId), input);
28724
+ }
28725
+ /**
28726
+ * Lazy status poll — server-side this may forward one provider poll, so
28727
+ * call it at the session's `poll_interval_seconds` cadence, not faster.
28728
+ */
28729
+ async getChannelProvisioningSession(orgId, sessionId) {
28730
+ return this.request("GET", ENDPOINTS.CHANNEL_PROVISIONING_SESSION(orgId, sessionId));
28731
+ }
28732
+ async cancelChannelProvisioning(orgId, sessionId) {
28733
+ return this.request("POST", ENDPOINTS.CHANNEL_PROVISIONING_CANCEL(orgId, sessionId));
28734
+ }
28735
+ async listChannelConversations(orgId, connectionId) {
28736
+ return this.request("GET", ENDPOINTS.CHANNEL_CONNECTION_CONVERSATIONS(orgId, connectionId));
28737
+ }
28738
+ async getChannelConversation(orgId, conversationId) {
28739
+ return this.request("GET", ENDPOINTS.CHANNEL_CONVERSATION(orgId, conversationId));
28740
+ }
28741
+ async listChannelConversationMessages(orgId, conversationId, limit) {
28742
+ return this.request("GET", ENDPOINTS.CHANNEL_CONVERSATION_MESSAGES(orgId, conversationId), void 0, { limit });
28743
+ }
28744
+ async setChannelConversationSession(orgId, conversationId, agentSessionId) {
28745
+ return this.request("PATCH", ENDPOINTS.CHANNEL_CONVERSATION_SESSION(orgId, conversationId), {
28746
+ agent_session_id: agentSessionId
28747
+ });
28748
+ }
28749
+ async getChannelMessage(orgId, messageId) {
28750
+ return this.request("GET", ENDPOINTS.CHANNEL_MESSAGE(orgId, messageId));
28751
+ }
28620
28752
  async getExternalTriggerSchema(orgId, connectionId) {
28621
28753
  return this.request("GET", ENDPOINTS.EXTERNAL_TRIGGER_SCHEMA(orgId, connectionId));
28622
28754
  }
@@ -28658,6 +28790,23 @@ var ParallClient = class _ParallClient {
28658
28790
  async getWiki(orgId, wikiId) {
28659
28791
  return this.request("GET", ENDPOINTS.WIKI(orgId, wikiId));
28660
28792
  }
28793
+ /** Soft-delete an entire wiki (org owner only; the default wiki is
28794
+ * protected → 403 WIKI_PROTECTED). Returns the deleted wiki. Recoverable
28795
+ * via restoreWiki within the retention window. */
28796
+ async deleteWiki(orgId, wikiId) {
28797
+ return this.request("DELETE", ENDPOINTS.WIKI(orgId, wikiId));
28798
+ }
28799
+ /** Restore a soft-deleted wiki (org owner only). 409 WIKI_PURGE_STARTED once
28800
+ * purge has been claimed (no longer restorable), or 404 once the row is gone.
28801
+ * Returns the restored wiki. */
28802
+ async restoreWiki(orgId, wikiId) {
28803
+ return this.request("POST", ENDPOINTS.WIKI_RESTORE(orgId, wikiId));
28804
+ }
28805
+ /** Recycle bin — list soft-deleted wikis for the org (owner only). */
28806
+ async getDeletedWikis(orgId) {
28807
+ const res = await this.request("GET", ENDPOINTS.WIKIS_DELETED(orgId));
28808
+ return res.data;
28809
+ }
28661
28810
  async getWikiTree(orgId, wikiId, params) {
28662
28811
  return this.request("GET", ENDPOINTS.WIKI_TREE(orgId, wikiId), void 0, params);
28663
28812
  }
@@ -28691,6 +28840,15 @@ var ParallClient = class _ParallClient {
28691
28840
  blob.signed_url = this.absoluteMediaUrl(blob.signed_url);
28692
28841
  return blob;
28693
28842
  }
28843
+ /**
28844
+ * Copy one wiki file to another path as an independent snapshot (no sync).
28845
+ * Used to "share a private file out": copy a file from the caller's personal
28846
+ * namespace (`users/{userId}/…`) into the public wiki, leaving the original
28847
+ * untouched. 409 `FILE_EXISTS` if `dest_path` already exists.
28848
+ */
28849
+ async copyWikiFile(orgId, wikiId, req) {
28850
+ return this.request("POST", ENDPOINTS.WIKI_COPY(orgId, wikiId), req);
28851
+ }
28694
28852
  async getWikiNodeSections(orgId, wikiId, params) {
28695
28853
  return this.request("GET", ENDPOINTS.WIKI_NODE_SECTIONS(orgId, wikiId), void 0, params);
28696
28854
  }
@@ -28964,6 +29122,19 @@ var ParallClient = class _ParallClient {
28964
29122
  const resp = await this.request("GET", ENDPOINTS.COMPUTE_PRICING());
28965
29123
  return resp.data;
28966
29124
  }
29125
+ /** Runtime capability table (public) — SSOT for the create/settings interlock. */
29126
+ async getRuntimes() {
29127
+ const resp = await this.request("GET", ENDPOINTS.RUNTIMES());
29128
+ return resp.data;
29129
+ }
29130
+ /**
29131
+ * Platform model catalog (public) — DB-backed, replaces the compile-time
29132
+ * PLATFORM_MODELS constant. Pass includeModel to keep an agent's pinned
29133
+ * hidden legacy model representable (settings picker).
29134
+ */
29135
+ async getModels(includeModel) {
29136
+ return this.request("GET", ENDPOINTS.MODELS(includeModel));
29137
+ }
28967
29138
  // ---- Clips ----
28968
29139
  async listClips(orgId) {
28969
29140
  const resp = await this.request("GET", ENDPOINTS.CLIPS(orgId));
@@ -29008,6 +29179,11 @@ var ParallClient = class _ParallClient {
29008
29179
  const resp = await this.request("GET", ENDPOINTS.CLIP_ONLINE(orgId));
29009
29180
  return resp.data;
29010
29181
  }
29182
+ /** Deployment-wide hosted browser runtime availability — drives whether the
29183
+ * create UI offers the platform-hosted placement. Fail-closed server-side. */
29184
+ async getBrowserRuntimeStatus(orgId) {
29185
+ return this.request("GET", ENDPOINTS.BROWSER_RUNTIME_STATUS(orgId));
29186
+ }
29011
29187
  /** Org-wide browser-profile discovery list. Returns the sanitized
29012
29188
  * {@link BrowserProfileListItem} shape (not the full domain model), each row
29013
29189
  * carrying a per-viewer `can_open` control hint. */
@@ -29667,6 +29843,9 @@ function resolveStepTarget(event) {
29667
29843
  if (event.type === "external_trigger" || event.targetId.startsWith("xtr_")) {
29668
29844
  return { target_type: "external_trigger", target_id: event.targetId };
29669
29845
  }
29846
+ if (event.type === "channel_message" || event.targetId.startsWith("chv_")) {
29847
+ return { target_type: "channel_conversation", target_id: event.targetId };
29848
+ }
29670
29849
  if (event.type === "wiki_comment") {
29671
29850
  return { target_type: "wiki", target_id: event.targetId || void 0 };
29672
29851
  }
@@ -29710,6 +29889,9 @@ var ParallAgentGateway = class {
29710
29889
  opts;
29711
29890
  chatInfoMap = /* @__PURE__ */ new Map();
29712
29891
  dispatchedTasks = /* @__PURE__ */ new Set();
29892
+ // connection id → provider alias, for channel_message prompt labeling
29893
+ // (stable mapping; avoids one connection fetch per inbound message).
29894
+ channelConnectionProviders = /* @__PURE__ */ new Map();
29713
29895
  dispatchedMessages = /* @__PURE__ */ new Set();
29714
29896
  forkStates = /* @__PURE__ */ new Map();
29715
29897
  dispatchState = {
@@ -29883,6 +30065,18 @@ var ParallAgentGateway = class {
29883
30065
  } catch (err) {
29884
30066
  this.opts.log?.error(`external trigger dispatch failed for ${data.source_id}: ${String(err)}`);
29885
30067
  }
30068
+ } else if (data.event_type === "channel_message") {
30069
+ if (!data.source_id)
30070
+ return;
30071
+ try {
30072
+ const dispatched = await this.fetchAndHandleChannelMessage(data.source_id);
30073
+ if (dispatched) {
30074
+ this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
30075
+ });
30076
+ }
30077
+ } catch (err) {
30078
+ this.opts.log?.error(`channel message dispatch failed for ${data.source_id}: ${String(err)}`);
30079
+ }
29886
30080
  } else if (data.event_type === "approval_decided") {
29887
30081
  if (!data.source_id)
29888
30082
  return;
@@ -29961,12 +30155,17 @@ var ParallAgentGateway = class {
29961
30155
  target_type: target.target_type,
29962
30156
  target_id: target.target_id,
29963
30157
  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",
30158
+ 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
30159
  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
30160
  trigger_id: event.targetId,
29967
30161
  run_id: event.messageId,
29968
30162
  connection_id: event.externalConnectionId,
29969
30163
  ingress_event_id: event.externalIngressEventId
30164
+ } : event.type === "channel_message" ? {
30165
+ conversation_id: event.targetId,
30166
+ channel_message_id: event.messageId,
30167
+ provider: event.channelProvider,
30168
+ external_conversation_id: event.channelExternalConversationId
29970
30169
  } : event.type === "approval" ? { approval_id: event.messageId } : { message_id: event.messageId },
29971
30170
  sender_id: event.senderId,
29972
30171
  sender_name: event.senderName,
@@ -30217,7 +30416,15 @@ var ParallAgentGateway = class {
30217
30416
  context: dispatchContext
30218
30417
  })) {
30219
30418
  if (runtimeEvent.type === "runtime_session") {
30419
+ const priorAgentSessionId = binding?.agentSessionId;
30220
30420
  binding = await this.bindRuntimeSession(sessionKey, runtimeEvent, contextFilePath);
30421
+ if (event.targetType === "channel_conversation" && binding.agentSessionId !== priorAgentSessionId) {
30422
+ try {
30423
+ await this.opts.client.setChannelConversationSession(this.opts.config.org_id, event.targetId, binding.agentSessionId);
30424
+ } catch (err) {
30425
+ this.opts.log?.warn(`failed to record session mapping for channel conversation ${event.targetId}: ${String(err)}`);
30426
+ }
30427
+ }
30221
30428
  if (!inputStepsCreated) {
30222
30429
  if (earlierEvents.length > 0) {
30223
30430
  await this.createInputStepsForEarlierEvents(binding.agentSessionId, earlierEvents);
@@ -31020,6 +31227,75 @@ var ParallAgentGateway = class {
31020
31227
  return true;
31021
31228
  return this.handleExternalTriggerRun(run);
31022
31229
  }
31230
+ // fetchAndHandleChannelMessage resolves a channel_message dispatch to its
31231
+ // durable ChannelMessage + conversation and hands it to the inbound
31232
+ // pipeline. targetId = the ChannelConversation id, so per-conversation
31233
+ // multi-turn continuity rides the same per-target session mechanics as
31234
+ // chats. Design: docs/engineering-design/external-im-channel-design.md.
31235
+ async fetchAndHandleChannelMessage(messageId) {
31236
+ if (this.shuttingDown)
31237
+ return false;
31238
+ const claimKey = `channel_message:${messageId}`;
31239
+ if (!this.tryClaimMessage(claimKey))
31240
+ return false;
31241
+ let msg = null;
31242
+ let conv = null;
31243
+ try {
31244
+ msg = await this.opts.client.getChannelMessage(this.opts.config.org_id, messageId);
31245
+ conv = await this.opts.client.getChannelConversation(this.opts.config.org_id, msg.conversation_id);
31246
+ } catch (err) {
31247
+ const status = err?.status;
31248
+ if (status === 404) {
31249
+ this.opts.log?.warn(`channel message ${messageId} not accessible (404), acking stale dispatch`);
31250
+ return true;
31251
+ }
31252
+ this.dispatchedMessages.delete(claimKey);
31253
+ this.opts.log?.warn(`channel message fetch failed for ${messageId}, leaving pending: ${String(err)}`);
31254
+ return false;
31255
+ }
31256
+ if (!msg || !conv) {
31257
+ return true;
31258
+ }
31259
+ this.opts.log?.info(`channel message: ${msg.id} (conversation ${conv.id})`);
31260
+ let provider = this.channelConnectionProviders.get(conv.connection_id);
31261
+ if (!provider) {
31262
+ try {
31263
+ const connection = await this.opts.client.getChannelConnection(this.opts.config.org_id, conv.connection_id);
31264
+ provider = connection.provider;
31265
+ this.channelConnectionProviders.set(conv.connection_id, provider);
31266
+ } catch {
31267
+ provider = void 0;
31268
+ }
31269
+ }
31270
+ const event = {
31271
+ type: "channel_message",
31272
+ targetId: conv.id,
31273
+ targetName: conv.external_user_name || conv.external_conversation_id,
31274
+ targetType: "channel_conversation",
31275
+ senderId: msg.external_user_id || "external",
31276
+ senderName: msg.external_user_name || msg.external_user_id || "external user",
31277
+ messageId: msg.id,
31278
+ body: msg.text,
31279
+ sentAt: msg.received_at,
31280
+ channelProvider: provider,
31281
+ channelConversationType: conv.conversation_type || void 0,
31282
+ channelExternalConversationId: conv.external_conversation_id,
31283
+ channelExternalMessageId: msg.external_message_id,
31284
+ ackSourceType: "channel_message",
31285
+ ackSourceId: msg.id
31286
+ };
31287
+ let dispatched;
31288
+ try {
31289
+ dispatched = await this.handleInboundEvent(event);
31290
+ } catch (err) {
31291
+ this.dispatchedMessages.delete(claimKey);
31292
+ throw err;
31293
+ }
31294
+ if (!dispatched) {
31295
+ this.dispatchedMessages.delete(claimKey);
31296
+ }
31297
+ return dispatched;
31298
+ }
31023
31299
  async handleExternalTriggerRun(run) {
31024
31300
  if (this.shuttingDown)
31025
31301
  return false;
@@ -31171,6 +31447,8 @@ var ParallAgentGateway = class {
31171
31447
  dispatched = await this.fetchAndHandleScheduleFire(item.source_id, item.actor_id);
31172
31448
  } else if (item.event_type === "external_trigger" && item.source_id) {
31173
31449
  dispatched = await this.fetchAndHandleExternalTriggerRun(item.source_id);
31450
+ } else if (item.event_type === "channel_message" && item.source_id) {
31451
+ dispatched = await this.fetchAndHandleChannelMessage(item.source_id);
31174
31452
  } else if (item.event_type === "approval_decided" && item.source_id) {
31175
31453
  dispatched = await this.fetchAndHandleApprovalDecided(item.source_id, item.actor_id, item.chat_id ?? null);
31176
31454
  } else if (item.event_type === "message" && item.source_id && item.chat_id) {
@@ -31368,6 +31646,9 @@ function resolveRuntimeModel(isOperatorOverride, platformModel, configModel) {
31368
31646
  const operatorOverride = isOperatorOverride ? platformModel : null;
31369
31647
  return operatorOverride ?? configModel ?? platformModel ?? void 0;
31370
31648
  }
31649
+ function deriveModelIsPin(defaults, profile) {
31650
+ return defaults.modelIsPin !== void 0 ? defaults.modelIsPin : isPlatformModelOverride(profile);
31651
+ }
31371
31652
  var CACHE_FILENAME = "parall-platform-config.json";
31372
31653
  var SUPPORTED_SCHEMA_VERSION = 1;
31373
31654
  function cachePath(stateDir) {
@@ -31385,6 +31666,7 @@ function saveCache(stateDir, response) {
31385
31666
  const cached = {
31386
31667
  version: response.version,
31387
31668
  config: response.config,
31669
+ modelIsPin: response.model_is_pin,
31388
31670
  fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
31389
31671
  };
31390
31672
  const filePath = cachePath(stateDir);
@@ -31395,23 +31677,27 @@ function saveCache(stateDir, response) {
31395
31677
  }
31396
31678
  function runtimeModelName(canonicalModel, runtimeType, config) {
31397
31679
  const runtime = runtimeType?.trim();
31398
- if (!runtime || runtime === "openclaw")
31680
+ if (!runtime || runtime === "openclaw" || runtime === "hermes")
31399
31681
  return canonicalModel;
31400
31682
  const models = config.models ?? {};
31401
31683
  const providers = models.providers ?? {};
31402
31684
  const parall = providers.parall ?? {};
31403
31685
  const catalog = Array.isArray(parall.models) ? parall.models : [];
31404
31686
  const match = catalog.find((model) => model.id === canonicalModel);
31405
- const runtimeNames = match?.runtime_names ?? {};
31687
+ if (!match)
31688
+ return null;
31689
+ const runtimeNames = match.runtime_names ?? {};
31406
31690
  const runtimeName = runtimeNames[runtime];
31407
- return typeof runtimeName === "string" && runtimeName ? runtimeName : null;
31691
+ if (typeof runtimeName === "string" && runtimeName)
31692
+ return runtimeName;
31693
+ return canonicalModel;
31408
31694
  }
31409
31695
  function extractDefaults(config, runtimeType) {
31410
31696
  const agents = config.agents ?? {};
31411
31697
  const defaults = agents.defaults ?? {};
31412
31698
  let model = null;
31413
31699
  if (typeof defaults.model === "string" && defaults.model) {
31414
- const canonicalModel = defaults.model.replace(/^parall\//, "");
31700
+ const canonicalModel = defaults.model.replace(/^parall-anthropic\//, "").replace(/^parall\//, "");
31415
31701
  model = runtimeModelName(canonicalModel, runtimeType, config);
31416
31702
  }
31417
31703
  let thinkingEffort = null;
@@ -31423,13 +31709,20 @@ function extractDefaults(config, runtimeType) {
31423
31709
  function createPlatformConfigManager(opts) {
31424
31710
  const { client, stateDir, runtimeType, log: log2 } = opts;
31425
31711
  let cachedVersion;
31426
- let currentDefaults = { model: null, thinkingEffort: null };
31712
+ let currentDefaults = {
31713
+ model: null,
31714
+ thinkingEffort: null,
31715
+ modelIsPin: void 0
31716
+ };
31427
31717
  let currentRawConfig = null;
31428
31718
  const cached = loadCache(stateDir);
31429
31719
  if (cached) {
31430
- cachedVersion = cached.version;
31720
+ cachedVersion = cached.modelIsPin === void 0 ? void 0 : cached.version;
31431
31721
  currentRawConfig = cached.config;
31432
- currentDefaults = extractDefaults(cached.config, runtimeType);
31722
+ currentDefaults = {
31723
+ ...extractDefaults(cached.config, runtimeType),
31724
+ modelIsPin: cached.modelIsPin
31725
+ };
31433
31726
  }
31434
31727
  return {
31435
31728
  async fetch() {
@@ -31455,7 +31748,10 @@ function createPlatformConfigManager(opts) {
31455
31748
  saveCache(stateDir, fresh);
31456
31749
  cachedVersion = fresh.version;
31457
31750
  currentRawConfig = fresh.config;
31458
- currentDefaults = extractDefaults(fresh.config, runtimeType);
31751
+ currentDefaults = {
31752
+ ...extractDefaults(fresh.config, runtimeType),
31753
+ modelIsPin: fresh.model_is_pin
31754
+ };
31459
31755
  return currentDefaults;
31460
31756
  },
31461
31757
  current() {
@@ -31572,16 +31868,35 @@ original discussion and its approval/rejection IS the precedent.
31572
31868
 
31573
31869
  Each \`[Event: message.new]\` includes \`[Chat: ... (prll://cht_xxx)]\` \u2014 use that chat URI to reply.
31574
31870
 
31871
+ > **How you pass the message body matters \u2014 your command runs through a shell.**
31872
+ > Inside double quotes the shell expands \`$\`, backticks, and \`$(...)\` *before*
31873
+ > the CLI sees them: \`--text "That costs $1,000"\` sends \`That costs ,000\`, and
31874
+ > \`--text "$(cmd)"\` runs \`cmd\`. Single quotes instead break on apostrophes
31875
+ > (\`I'm\`, \`don't\`). So do **not** wrap real message content in quotes \u2014 pass it
31876
+ > through \`--text-file\` (a written file, or a quoted heredoc \`<<'EOF'\` that
31877
+ > disables all expansion). Reserve \`--text "..."\` for short literals with no
31878
+ > \`$\`, backtick, or apostrophe.
31879
+
31575
31880
  \`\`\`bash
31576
- # Reply to a chat (use the chat URI from the event)
31577
- parall messages send prll://cht_xxx --text "Your reply"
31881
+ # One-off reply \u2192 quoted heredoc into stdin. The quoted delimiter <<'EOF'
31882
+ # disables ALL shell expansion, so $, backticks and apostrophes pass verbatim.
31883
+ parall messages send prll://cht_xxx --text-file - <<'PARALL_EOF'
31884
+ Sure \u2014 that's $1,000, and $(whoami) stays literal. I'm on it.
31885
+ PARALL_EOF
31578
31886
 
31579
- # Direct message by user URI or display name
31580
- parall dm prll://usr_xxx --text "Hello"
31887
+ # Longer / multi-line reply \u2192 write it with your file tool (no shell touches
31888
+ # the body), then point --text-file at the file.
31889
+ parall messages send prll://cht_xxx --text-file /tmp/reply.md
31890
+
31891
+ # Short literal with no $, backtick, or apostrophe \u2192 --text is fine.
31892
+ parall messages send prll://cht_xxx --text "On it"
31893
+
31894
+ # Direct message by user URI or display name (same --text-file / heredoc rules)
31895
+ parall dm prll://usr_xxx --text-file /tmp/reply.md
31581
31896
  parall dm "Alice" --text "Hello"
31582
31897
 
31583
31898
  # Thread reply
31584
- parall messages send prll://cht_xxx --text "Reply" --thread-root-id 01JWC...
31899
+ parall messages send prll://cht_xxx --text-file /tmp/reply.md --thread-root-id 01JWC...
31585
31900
 
31586
31901
  # FYI message (no response expected \u2014 the recipient sees \`[Hint: no_reply]\`)
31587
31902
  parall messages send prll://cht_xxx --text "FYI: done" --no-reply
@@ -31613,7 +31928,9 @@ parall messages send prll://cht_xxx --attachment att_xxx --text "See attached"
31613
31928
  parall dm "Alice" --file /tmp/report.pdf --text "Report attached"
31614
31929
  \`\`\`
31615
31930
 
31616
- \`--file\` and \`--attachment\` are mutually exclusive. \`--text\` can be combined with either.
31931
+ \`--file\` and \`--attachment\` are mutually exclusive. A caption (\`--text\` for
31932
+ short literals, or \`--text-file\` for anything with \`$\`, backticks, or quotes)
31933
+ can be combined with either.
31617
31934
 
31618
31935
  ## Approvals
31619
31936
 
@@ -33792,14 +34109,13 @@ async function main() {
33792
34109
  log: agentLog
33793
34110
  });
33794
34111
  const platformDefaults = await configMgr.fetch();
33795
- let platformManaged = isPlatformManagedProfile(me.agent_profile);
33796
- let platformModelOverride = isPlatformModelOverride(me.agent_profile);
34112
+ const platformModelOverride = deriveModelIsPin(platformDefaults, me.agent_profile);
33797
34113
  const resolvedModel = resolveRuntimeModel(platformModelOverride, platformDefaults.model, config.model);
33798
- const resolvedEffort = platformManaged ? platformDefaults.thinkingEffort ?? (process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || void 0) : process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || void 0;
34114
+ const resolvedEffort = platformDefaults.thinkingEffort ?? (process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || void 0);
33799
34115
  if (platformDefaults.model)
33800
- agentLog.info(`platform config: model=${platformDefaults.model} \u2192 resolved=${resolvedModel ?? "cli-default"} (${platformModelOverride ? "platform-managed" : "floor/self"})`);
34116
+ agentLog.info(`platform config: model=${platformDefaults.model} \u2192 resolved=${resolvedModel ?? "cli-default"} (${platformModelOverride ? "pin/override" : "floor"})`);
33801
34117
  if (platformDefaults.thinkingEffort)
33802
- agentLog.info(`platform config: thinking_effort=${platformDefaults.thinkingEffort} (${platformModelOverride ? "applied" : "ignored, model_management=self"})`);
34118
+ agentLog.info(`platform config: thinking_effort=${platformDefaults.thinkingEffort}`);
33803
34119
  const adapter = new ClaudeCodeAdapter({
33804
34120
  claudeBin: config.claudeBin,
33805
34121
  claudeHome: config.claudeHome,
@@ -33824,6 +34140,23 @@ async function main() {
33824
34140
  if (resolvedEffort) {
33825
34141
  adapter.updateConfig({ effort: resolvedEffort });
33826
34142
  }
34143
+ const refreshPlatformConfig = async () => {
34144
+ const updated = await configMgr.fetch();
34145
+ let refreshedProfile;
34146
+ if (updated.modelIsPin === void 0) {
34147
+ try {
34148
+ refreshedProfile = (await getAgentMeWithLegacyFallback(client, config.orgId)).agent_profile;
34149
+ } catch (err) {
34150
+ agentLog.warn(`platform config refresh: legacy /agents/me fallback failed: ${String(err)}`);
34151
+ }
34152
+ }
34153
+ const isPin = deriveModelIsPin(updated, refreshedProfile);
34154
+ const localEffort = process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || null;
34155
+ adapter.updateConfig({
34156
+ model: resolveRuntimeModel(isPin, updated.model, config.model) ?? null,
34157
+ effort: updated.thinkingEffort ?? localEffort
34158
+ });
34159
+ };
33827
34160
  const gateway = new ParallAgentGateway({
33828
34161
  accountId: agentUserId,
33829
34162
  client,
@@ -33850,28 +34183,8 @@ async function main() {
33850
34183
  dispatchDeadlineMs: parseDispatchDeadlineMs(process.env.PRLL_DISPATCH_DEADLINE_MS),
33851
34184
  contextFilePathForSession: (sessionKey) => contextFilePathForSession(config.stateDir, sessionKey),
33852
34185
  stepIdFilePathForSession: (sessionKey) => stepIdFilePathForSession(config.stateDir, sessionKey),
33853
- onConfigUpdate: async () => {
33854
- const refreshed = await getAgentMeWithLegacyFallback(client, config.orgId);
33855
- platformManaged = isPlatformManagedProfile(refreshed.agent_profile);
33856
- platformModelOverride = isPlatformModelOverride(refreshed.agent_profile);
33857
- const updated = await configMgr.fetch();
33858
- const localEffort = process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || null;
33859
- adapter.updateConfig({
33860
- model: resolveRuntimeModel(platformModelOverride, updated.model, config.model) ?? null,
33861
- effort: platformManaged ? updated.thinkingEffort ?? localEffort : localEffort
33862
- });
33863
- },
33864
- onSessionReady: async () => {
33865
- const refreshed = await getAgentMeWithLegacyFallback(client, config.orgId);
33866
- platformManaged = isPlatformManagedProfile(refreshed.agent_profile);
33867
- platformModelOverride = isPlatformModelOverride(refreshed.agent_profile);
33868
- const updated = await configMgr.fetch();
33869
- const localEffort = process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || null;
33870
- adapter.updateConfig({
33871
- model: resolveRuntimeModel(platformModelOverride, updated.model, config.model) ?? null,
33872
- effort: platformManaged ? updated.thinkingEffort ?? localEffort : localEffort
33873
- });
33874
- },
34186
+ onConfigUpdate: refreshPlatformConfig,
34187
+ onSessionReady: refreshPlatformConfig,
33875
34188
  // Long-lived `claude` subprocesses outlive a single dispatch, so the
33876
34189
  // gateway's shuttingDown flag alone does not tear them down. Piggyback on
33877
34190
  // onBeforeDisconnect to close stdin and SIGTERM any survivors after