@parall/agent-core 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.
- package/dist/bridge-workspace.d.ts +1 -1
- package/dist/bridge-workspace.d.ts.map +1 -1
- package/dist/bridge-workspace.js +13 -3
- package/dist/event-format.d.ts.map +1 -1
- package/dist/event-format.js +36 -1
- package/dist/gateway-base.d.ts +2 -0
- package/dist/gateway-base.d.ts.map +1 -1
- package/dist/gateway-base.js +132 -6
- package/dist/platform-config.d.ts +19 -0
- package/dist/platform-config.d.ts.map +1 -1
- package/dist/platform-config.js +72 -9
- package/dist/prompt-fragments.d.ts +1 -1
- package/dist/prompt-fragments.d.ts.map +1 -1
- package/dist/prompt-fragments.js +2 -0
- package/dist/skills/parall-platform.d.ts +1 -1
- package/dist/skills/parall-platform.d.ts.map +1 -1
- package/dist/skills/parall-platform.js +27 -6
- package/dist/types.d.ts +9 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/bridge-workspace.ts +13 -3
- package/src/event-format.ts +38 -1
- package/src/gateway-base.ts +149 -6
- package/src/platform-config.ts +85 -9
- package/src/prompt-fragments.ts +2 -0
- package/src/skills/parall-platform.ts +27 -6
- package/src/types.ts +15 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const PARALL_PLATFORM_SKILL = "# Parall Platform\n\nQuery organization data via the Parall CLI. Auth is pre-configured.\n\n## Identity\n\n```bash\nparall whoami\n```\n\n## Members & Agents\n\n```bash\nparall members list # All org members (humans + agents)\nparall agents list # Agents only\nparall users get prll://usr_xxx # Get user details by ID\n```\n\nCreate a hosted agent when the user asks for a Parall-managed runtime. Hosted\nprovisioning is asynchronous: creation means the agent identity, API key, and\nmachine record were accepted, not that the runtime is online yet. Use `--wait`\nto wait until the machine reaches `running`, and use `--wait-online` when the\ntask requires the child agent to be connected before you report completion.\nFor hosted agents, use `--discard-api-key`; the server injects the one-time key\ninto the hosted runtime, so the parent agent must not print or persist it.\n\nCreate a self-hosted agent only when the runtime will be connected outside\nParall-managed compute. In that case, write the one-time `api_key` to\n`--api-key-file` so it is not captured in tool-result logs. Treat `api_key` as a\nsecret: do not print, read aloud, post it in shared chats, or echo the file\ncontents. Include the `user.id` in normal responses, and pass the key file only\nthrough an explicit secure runtime handoff when connection is required. Never\nuse `--show-api-key` from an agent runtime. Agent callers cannot set provider\noverrides until the dedicated fine-grained permission flow lands.\n\n```bash\n# Hosted runtime (Parall-managed compute)\nparall agents create \\\n --name \"Research Agent\" \\\n --runtime-type codex \\\n --machine-type cloud \\\n --machine-label standard \\\n --discard-api-key \\\n --wait \\\n --wait-online\n\n# Self-hosted runtime\nparall agents create --name \"Research Agent\" --runtime-type codex --api-key-file /tmp/research-agent.api-key\n```\n\nInspect hosted provisioning directly when a create command returns before the\nruntime is online, or when you need logs for a failed machine. If `agents create`\nexits non-zero after creating a hosted agent, read the printed `user.id` and\n`machine.id`, then use these commands to decide whether to wait, inspect logs,\nor report the failed machine for retry.\n\n```bash\nparall machines status prll://mch_xxx\nparall machines logs prll://mch_xxx --lines 100\n```\n\n## Chats & Messages\n\n```bash\nparall chats list # List all chats\nparall messages list prll://cht_xxx # Read chat message history\nparall messages list prll://cht_xxx --since 2026-01-01 # Only messages at/after a date (RFC3339 or YYYY-MM-DD)\n```\n\n## Org-Context Search\n\nBefore deciding or starting non-trivial work, search the org's real history \u2014\npast discussions, decisions, tasks, and wiki notes \u2014 so you don't re-litigate\nsettled questions or repeat known mistakes. This searches live org data\n(semantic + keyword), not a local copy, and is permission-filtered to what you\ncan see.\n\n```bash\n# Semantic + keyword search across messages, tasks, and wiki\nparall search \"auth v5 upgrade\"\n\n# Restrict entity types (m=message, t=task, w=wiki). --channel narrows the\n# MESSAGE hits to one chat (tasks/wiki are unaffected by it).\nparall search \"auth v5 upgrade\" --types m,w --channel prll://cht_eng\n\n# Time-box to recent activity (RFC3339 or YYYY-MM-DD). Narrows messages + tasks;\n# wiki is always matched by relevance (the index has no authored timestamp).\nparall search \"auth v5 upgrade\" --since 2026-01-01\n\n# Narrow wiki hits to a frontmatter document type\nparall search \"deploy steps\" --types w --wiki-type Runbook\n```\n\nEven with zero curated notes, the raw message + task history is searchable \u2014 the\noriginal discussion and its approval/rejection IS the precedent.\n\n## Sending Messages\n\nEach `[Event: message.new]` includes `[Chat: ... (prll://cht_xxx)]` \u2014 use that chat URI to reply.\n\n```bash\n# Reply to a chat (use the chat URI from the event)\nparall messages send prll://cht_xxx --text \"Your reply\"\n\n# Direct message by user URI or display name\nparall dm prll://usr_xxx --text \"Hello\"\nparall dm \"Alice\" --text \"Hello\"\n\n# Thread reply\nparall messages send prll://cht_xxx --text \"Reply\" --thread-root-id 01JWC...\n\n# FYI message (no response expected \u2014 the recipient sees `[Hint: no_reply]`)\nparall messages send prll://cht_xxx --text \"FYI: done\" --no-reply\n\n# Silence this turn entirely \u2014 no chat message produced. Use when you receive\n# `[Hint: no_reply]` or otherwise decide the turn needs no visible reply.\n# Run BEFORE any `messages send` / `dm`; those still deliver real messages.\nparall no-reply --reason \"ack only, nothing to add\"\n```\n\n## Files & Attachments\n\nAttachments appear in events as `[Attachment: prll://att_xxx | mime | size | name]`.\n\n```bash\n# Download an attachment\nparall files download att_xxx --output /tmp/file.png\n\n# Upload a file (returns attachment_id)\nparall files upload /tmp/report.pdf\n\n# Send a message with a file\nparall messages send prll://cht_xxx --file /tmp/output.png --text \"Done\"\n\n# Send an existing attachment to another chat\nparall messages send prll://cht_xxx --attachment att_xxx --text \"See attached\"\n\n# DM with a file\nparall dm \"Alice\" --file /tmp/report.pdf --text \"Report attached\"\n```\n\n`--file` and `--attachment` are mutually exclusive. `--text` can be combined with either.\n\n## Approvals\n\nWhen a CLI command returns a `PERMISSION_DENIED` error, the output includes the denied `action` and `resource_uri`. Whether that action can be approved is decided by the server (there is no fixed allowlist):\n- If it IS approvable, a `Request approval:` line with a `parall approvals request` command follows \u2014 fill in the placeholders it shows (`--chat`, `--title`, `--reason`) and run it.\n- If it is NOT approvable, the output says so \u2014 ask a human with permission instead of requesting approval.\n\nA different `INVALID_TARGET` error means you addressed the wrong kind of thing (e.g. a `usr_` id where a chat is expected). Follow the message (e.g. use `parall dm` to message a user) \u2014 do not request approval for it.\n\n```bash\n# Request approval (use action and resource_uri from the error)\nparall approvals request --action chat.archive --resource prll://cht_xxx --chat prll://cht_yyy --title \"Archive old channel\" --reason \"No activity in 6 months\"\n\n# Check a specific approval's status\nparall approvals get prll://apr_xxx\n\n# Wait for a decision (blocks until approved/rejected/timeout)\nparall approvals wait prll://apr_xxx --timeout 300\n\n# List all your pending approvals\nparall approvals list\n\n# List available approvable actions\nparall approvals actions\n\n# Cancel a pending request you made\nparall approvals cancel prll://apr_xxx\n```\n\nOnly request approval after receiving an actual `PERMISSION_DENIED` error \u2014 never preemptively. The `--chat` flag specifies where the approval card appears; use the chat where the conversation is happening.\n\n## Reference URIs\n\nEvery entity is addressable with a `prll://` URI. Common prefixes you'll see in events, messages, and schedule descriptions:\n\n| Prefix | Entity | Skill |\n|--------|--------|-------|\n| `prll://usr_` | User (human or agent) | parall-platform |\n| `prll://cht_` | Chat | parall-platform |\n| `prll://msg_` | Message | parall-platform |\n| `prll://tsk_` | Task | parall-tasks |\n| `prll://prj_` | Project | parall-tasks |\n| `prll://sch_` | Schedule (time trigger) | parall-schedules |\n| `prll://srn_` | Schedule run (single fire audit record; carries fire-time snapshot) | parall-schedules |\n| `prll://xcn_` | External Trigger Connection (incoming endpoint) | parall-external-triggers |\n| `prll://xin_` | External Trigger Event (single incoming event audit record) | parall-external-triggers |\n| `prll://xtr_` | External Trigger (incoming trigger configuration) | parall-external-triggers |\n| `prll://xrn_` | External Trigger run (single matched dispatch audit record) | parall-external-triggers |\n| `prll://wik_` | Wiki | parall-wiki |\n| `prll://att_` | Attachment | parall-platform (files) |\n\nWhen a message or event references `prll://sch_xxx` or `prll://srn_xxx`, or when you receive `[Event: schedule.fired]`, switch to the **parall-schedules** skill for the CLI commands (create / list / pause / resume / cancel / runs).\n\nWhen a message or event references `prll://xcn_xxx`, `prll://xin_xxx`, `prll://xtr_xxx`, or `prll://xrn_xxx`, or when you receive `[Event: external.trigger]`, switch to the **parall-external-triggers** skill for the CLI commands (connections / triggers / events / runs).\n\n## References (relationship graph)\n\n`prll://` references between entities form a graph \u2014 a message cites a task, a\ntask cites a wiki page, and so on. Walk it to answer \"what is this decision /\nentity connected to\". All results are permission-filtered to what you can see.\n\n```bash\n# Resolve URIs to entity metadata (titles, status, previews)\nparall refs resolve prll://tsk_xxx prll://wik_xxx\n\n# Single hop \u2014 who references X\nparall refs backlinks prll://tsk_xxx\n\n# Multi-hop \u2014 the connected sub-graph around X (entity-level URI only \u2014 no\n# path/anchor; depth 1\u20134, default 2)\nparall refs graph prll://tsk_xxx --depth 2\n```\n\n`refs graph` traverses both directions (inbound + outbound) and returns `nodes`\nand `edges` with each node's hop `depth`. `truncated: true` means a size cap clipped\nthe result \u2014 narrow it with a smaller `--depth`.\n\nCLI success output is JSON. Errors print a JSON line (`{\"error\",\"status\",\"code\",...}`) and, on a `PERMISSION_DENIED`, may add a plain-text `Request approval:` line \u2014 read both.\n";
|
|
1
|
+
export declare const PARALL_PLATFORM_SKILL = "# Parall Platform\n\nQuery organization data via the Parall CLI. Auth is pre-configured.\n\n## Identity\n\n```bash\nparall whoami\n```\n\n## Members & Agents\n\n```bash\nparall members list # All org members (humans + agents)\nparall agents list # Agents only\nparall users get prll://usr_xxx # Get user details by ID\n```\n\nCreate a hosted agent when the user asks for a Parall-managed runtime. Hosted\nprovisioning is asynchronous: creation means the agent identity, API key, and\nmachine record were accepted, not that the runtime is online yet. Use `--wait`\nto wait until the machine reaches `running`, and use `--wait-online` when the\ntask requires the child agent to be connected before you report completion.\nFor hosted agents, use `--discard-api-key`; the server injects the one-time key\ninto the hosted runtime, so the parent agent must not print or persist it.\n\nCreate a self-hosted agent only when the runtime will be connected outside\nParall-managed compute. In that case, write the one-time `api_key` to\n`--api-key-file` so it is not captured in tool-result logs. Treat `api_key` as a\nsecret: do not print, read aloud, post it in shared chats, or echo the file\ncontents. Include the `user.id` in normal responses, and pass the key file only\nthrough an explicit secure runtime handoff when connection is required. Never\nuse `--show-api-key` from an agent runtime. Agent callers cannot set provider\noverrides until the dedicated fine-grained permission flow lands.\n\n```bash\n# Hosted runtime (Parall-managed compute)\nparall agents create \\\n --name \"Research Agent\" \\\n --runtime-type codex \\\n --machine-type cloud \\\n --machine-label standard \\\n --discard-api-key \\\n --wait \\\n --wait-online\n\n# Self-hosted runtime\nparall agents create --name \"Research Agent\" --runtime-type codex --api-key-file /tmp/research-agent.api-key\n```\n\nInspect hosted provisioning directly when a create command returns before the\nruntime is online, or when you need logs for a failed machine. If `agents create`\nexits non-zero after creating a hosted agent, read the printed `user.id` and\n`machine.id`, then use these commands to decide whether to wait, inspect logs,\nor report the failed machine for retry.\n\n```bash\nparall machines status prll://mch_xxx\nparall machines logs prll://mch_xxx --lines 100\n```\n\n## Chats & Messages\n\n```bash\nparall chats list # List all chats\nparall messages list prll://cht_xxx # Read chat message history\nparall messages list prll://cht_xxx --since 2026-01-01 # Only messages at/after a date (RFC3339 or YYYY-MM-DD)\n```\n\n## Org-Context Search\n\nBefore deciding or starting non-trivial work, search the org's real history \u2014\npast discussions, decisions, tasks, and wiki notes \u2014 so you don't re-litigate\nsettled questions or repeat known mistakes. This searches live org data\n(semantic + keyword), not a local copy, and is permission-filtered to what you\ncan see.\n\n```bash\n# Semantic + keyword search across messages, tasks, and wiki\nparall search \"auth v5 upgrade\"\n\n# Restrict entity types (m=message, t=task, w=wiki). --channel narrows the\n# MESSAGE hits to one chat (tasks/wiki are unaffected by it).\nparall search \"auth v5 upgrade\" --types m,w --channel prll://cht_eng\n\n# Time-box to recent activity (RFC3339 or YYYY-MM-DD). Narrows messages + tasks;\n# wiki is always matched by relevance (the index has no authored timestamp).\nparall search \"auth v5 upgrade\" --since 2026-01-01\n\n# Narrow wiki hits to a frontmatter document type\nparall search \"deploy steps\" --types w --wiki-type Runbook\n```\n\nEven with zero curated notes, the raw message + task history is searchable \u2014 the\noriginal discussion and its approval/rejection IS the precedent.\n\n## Sending Messages\n\nEach `[Event: message.new]` includes `[Chat: ... (prll://cht_xxx)]` \u2014 use that chat URI to reply.\n\n> **How you pass the message body matters \u2014 your command runs through a shell.**\n> Inside double quotes the shell expands `$`, backticks, and `$(...)` *before*\n> the CLI sees them: `--text \"That costs $1,000\"` sends `That costs ,000`, and\n> `--text \"$(cmd)\"` runs `cmd`. Single quotes instead break on apostrophes\n> (`I'm`, `don't`). So do **not** wrap real message content in quotes \u2014 pass it\n> through `--text-file` (a written file, or a quoted heredoc `<<'EOF'` that\n> disables all expansion). Reserve `--text \"...\"` for short literals with no\n> `$`, backtick, or apostrophe.\n\n```bash\n# One-off reply \u2192 quoted heredoc into stdin. The quoted delimiter <<'EOF'\n# disables ALL shell expansion, so $, backticks and apostrophes pass verbatim.\nparall messages send prll://cht_xxx --text-file - <<'PARALL_EOF'\nSure \u2014 that's $1,000, and $(whoami) stays literal. I'm on it.\nPARALL_EOF\n\n# Longer / multi-line reply \u2192 write it with your file tool (no shell touches\n# the body), then point --text-file at the file.\nparall messages send prll://cht_xxx --text-file /tmp/reply.md\n\n# Short literal with no $, backtick, or apostrophe \u2192 --text is fine.\nparall messages send prll://cht_xxx --text \"On it\"\n\n# Direct message by user URI or display name (same --text-file / heredoc rules)\nparall dm prll://usr_xxx --text-file /tmp/reply.md\nparall dm \"Alice\" --text \"Hello\"\n\n# Thread reply\nparall messages send prll://cht_xxx --text-file /tmp/reply.md --thread-root-id 01JWC...\n\n# FYI message (no response expected \u2014 the recipient sees `[Hint: no_reply]`)\nparall messages send prll://cht_xxx --text \"FYI: done\" --no-reply\n\n# Silence this turn entirely \u2014 no chat message produced. Use when you receive\n# `[Hint: no_reply]` or otherwise decide the turn needs no visible reply.\n# Run BEFORE any `messages send` / `dm`; those still deliver real messages.\nparall no-reply --reason \"ack only, nothing to add\"\n```\n\n## Files & Attachments\n\nAttachments appear in events as `[Attachment: prll://att_xxx | mime | size | name]`.\n\n```bash\n# Download an attachment\nparall files download att_xxx --output /tmp/file.png\n\n# Upload a file (returns attachment_id)\nparall files upload /tmp/report.pdf\n\n# Send a message with a file\nparall messages send prll://cht_xxx --file /tmp/output.png --text \"Done\"\n\n# Send an existing attachment to another chat\nparall messages send prll://cht_xxx --attachment att_xxx --text \"See attached\"\n\n# DM with a file\nparall dm \"Alice\" --file /tmp/report.pdf --text \"Report attached\"\n```\n\n`--file` and `--attachment` are mutually exclusive. A caption (`--text` for\nshort literals, or `--text-file` for anything with `$`, backticks, or quotes)\ncan be combined with either.\n\n## Approvals\n\nWhen a CLI command returns a `PERMISSION_DENIED` error, the output includes the denied `action` and `resource_uri`. Whether that action can be approved is decided by the server (there is no fixed allowlist):\n- If it IS approvable, a `Request approval:` line with a `parall approvals request` command follows \u2014 fill in the placeholders it shows (`--chat`, `--title`, `--reason`) and run it.\n- If it is NOT approvable, the output says so \u2014 ask a human with permission instead of requesting approval.\n\nA different `INVALID_TARGET` error means you addressed the wrong kind of thing (e.g. a `usr_` id where a chat is expected). Follow the message (e.g. use `parall dm` to message a user) \u2014 do not request approval for it.\n\n```bash\n# Request approval (use action and resource_uri from the error)\nparall approvals request --action chat.archive --resource prll://cht_xxx --chat prll://cht_yyy --title \"Archive old channel\" --reason \"No activity in 6 months\"\n\n# Check a specific approval's status\nparall approvals get prll://apr_xxx\n\n# Wait for a decision (blocks until approved/rejected/timeout)\nparall approvals wait prll://apr_xxx --timeout 300\n\n# List all your pending approvals\nparall approvals list\n\n# List available approvable actions\nparall approvals actions\n\n# Cancel a pending request you made\nparall approvals cancel prll://apr_xxx\n```\n\nOnly request approval after receiving an actual `PERMISSION_DENIED` error \u2014 never preemptively. The `--chat` flag specifies where the approval card appears; use the chat where the conversation is happening.\n\n## Reference URIs\n\nEvery entity is addressable with a `prll://` URI. Common prefixes you'll see in events, messages, and schedule descriptions:\n\n| Prefix | Entity | Skill |\n|--------|--------|-------|\n| `prll://usr_` | User (human or agent) | parall-platform |\n| `prll://cht_` | Chat | parall-platform |\n| `prll://msg_` | Message | parall-platform |\n| `prll://tsk_` | Task | parall-tasks |\n| `prll://prj_` | Project | parall-tasks |\n| `prll://sch_` | Schedule (time trigger) | parall-schedules |\n| `prll://srn_` | Schedule run (single fire audit record; carries fire-time snapshot) | parall-schedules |\n| `prll://xcn_` | External Trigger Connection (incoming endpoint) | parall-external-triggers |\n| `prll://xin_` | External Trigger Event (single incoming event audit record) | parall-external-triggers |\n| `prll://xtr_` | External Trigger (incoming trigger configuration) | parall-external-triggers |\n| `prll://xrn_` | External Trigger run (single matched dispatch audit record) | parall-external-triggers |\n| `prll://wik_` | Wiki | parall-wiki |\n| `prll://att_` | Attachment | parall-platform (files) |\n\nWhen a message or event references `prll://sch_xxx` or `prll://srn_xxx`, or when you receive `[Event: schedule.fired]`, switch to the **parall-schedules** skill for the CLI commands (create / list / pause / resume / cancel / runs).\n\nWhen a message or event references `prll://xcn_xxx`, `prll://xin_xxx`, `prll://xtr_xxx`, or `prll://xrn_xxx`, or when you receive `[Event: external.trigger]`, switch to the **parall-external-triggers** skill for the CLI commands (connections / triggers / events / runs).\n\n## References (relationship graph)\n\n`prll://` references between entities form a graph \u2014 a message cites a task, a\ntask cites a wiki page, and so on. Walk it to answer \"what is this decision /\nentity connected to\". All results are permission-filtered to what you can see.\n\n```bash\n# Resolve URIs to entity metadata (titles, status, previews)\nparall refs resolve prll://tsk_xxx prll://wik_xxx\n\n# Single hop \u2014 who references X\nparall refs backlinks prll://tsk_xxx\n\n# Multi-hop \u2014 the connected sub-graph around X (entity-level URI only \u2014 no\n# path/anchor; depth 1\u20134, default 2)\nparall refs graph prll://tsk_xxx --depth 2\n```\n\n`refs graph` traverses both directions (inbound + outbound) and returns `nodes`\nand `edges` with each node's hop `depth`. `truncated: true` means a size cap clipped\nthe result \u2014 narrow it with a smaller `--depth`.\n\nCLI success output is JSON. Errors print a JSON line (`{\"error\",\"status\",\"code\",...}`) and, on a `PERMISSION_DENIED`, may add a plain-text `Request approval:` line \u2014 read both.\n";
|
|
2
2
|
//# sourceMappingURL=parall-platform.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parall-platform.d.ts","sourceRoot":"","sources":["../../src/skills/parall-platform.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,qBAAqB,
|
|
1
|
+
{"version":3,"file":"parall-platform.d.ts","sourceRoot":"","sources":["../../src/skills/parall-platform.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,qBAAqB,wzVAiPjC,CAAC"}
|
|
@@ -98,16 +98,35 @@ original discussion and its approval/rejection IS the precedent.
|
|
|
98
98
|
|
|
99
99
|
Each \`[Event: message.new]\` includes \`[Chat: ... (prll://cht_xxx)]\` — use that chat URI to reply.
|
|
100
100
|
|
|
101
|
+
> **How you pass the message body matters — your command runs through a shell.**
|
|
102
|
+
> Inside double quotes the shell expands \`$\`, backticks, and \`$(...)\` *before*
|
|
103
|
+
> the CLI sees them: \`--text "That costs $1,000"\` sends \`That costs ,000\`, and
|
|
104
|
+
> \`--text "$(cmd)"\` runs \`cmd\`. Single quotes instead break on apostrophes
|
|
105
|
+
> (\`I'm\`, \`don't\`). So do **not** wrap real message content in quotes — pass it
|
|
106
|
+
> through \`--text-file\` (a written file, or a quoted heredoc \`<<'EOF'\` that
|
|
107
|
+
> disables all expansion). Reserve \`--text "..."\` for short literals with no
|
|
108
|
+
> \`$\`, backtick, or apostrophe.
|
|
109
|
+
|
|
101
110
|
\`\`\`bash
|
|
102
|
-
#
|
|
103
|
-
|
|
111
|
+
# One-off reply → quoted heredoc into stdin. The quoted delimiter <<'EOF'
|
|
112
|
+
# disables ALL shell expansion, so $, backticks and apostrophes pass verbatim.
|
|
113
|
+
parall messages send prll://cht_xxx --text-file - <<'PARALL_EOF'
|
|
114
|
+
Sure — that's $1,000, and $(whoami) stays literal. I'm on it.
|
|
115
|
+
PARALL_EOF
|
|
116
|
+
|
|
117
|
+
# Longer / multi-line reply → write it with your file tool (no shell touches
|
|
118
|
+
# the body), then point --text-file at the file.
|
|
119
|
+
parall messages send prll://cht_xxx --text-file /tmp/reply.md
|
|
120
|
+
|
|
121
|
+
# Short literal with no $, backtick, or apostrophe → --text is fine.
|
|
122
|
+
parall messages send prll://cht_xxx --text "On it"
|
|
104
123
|
|
|
105
|
-
# Direct message by user URI or display name
|
|
106
|
-
parall dm prll://usr_xxx --text
|
|
124
|
+
# Direct message by user URI or display name (same --text-file / heredoc rules)
|
|
125
|
+
parall dm prll://usr_xxx --text-file /tmp/reply.md
|
|
107
126
|
parall dm "Alice" --text "Hello"
|
|
108
127
|
|
|
109
128
|
# Thread reply
|
|
110
|
-
parall messages send prll://cht_xxx --text
|
|
129
|
+
parall messages send prll://cht_xxx --text-file /tmp/reply.md --thread-root-id 01JWC...
|
|
111
130
|
|
|
112
131
|
# FYI message (no response expected — the recipient sees \`[Hint: no_reply]\`)
|
|
113
132
|
parall messages send prll://cht_xxx --text "FYI: done" --no-reply
|
|
@@ -139,7 +158,9 @@ parall messages send prll://cht_xxx --attachment att_xxx --text "See attached"
|
|
|
139
158
|
parall dm "Alice" --file /tmp/report.pdf --text "Report attached"
|
|
140
159
|
\`\`\`
|
|
141
160
|
|
|
142
|
-
\`--file\` and \`--attachment\` are mutually exclusive. \`--text\`
|
|
161
|
+
\`--file\` and \`--attachment\` are mutually exclusive. A caption (\`--text\` for
|
|
162
|
+
short literals, or \`--text-file\` for anything with \`$\`, backticks, or quotes)
|
|
163
|
+
can be combined with either.
|
|
143
164
|
|
|
144
165
|
## Approvals
|
|
145
166
|
|
package/dist/types.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export type DispatchState = {
|
|
|
26
26
|
};
|
|
27
27
|
/** Normalized inbound event from Parall. */
|
|
28
28
|
export type ParallEvent = {
|
|
29
|
-
type: 'message' | 'task' | 'task_comment' | 'wiki_comment' | 'schedule' | 'external_trigger' | 'approval';
|
|
29
|
+
type: 'message' | 'task' | 'task_comment' | 'wiki_comment' | 'schedule' | 'external_trigger' | 'channel_message' | 'approval';
|
|
30
30
|
targetId: string;
|
|
31
31
|
targetName?: string;
|
|
32
32
|
targetType?: string;
|
|
@@ -60,10 +60,17 @@ export type ParallEvent = {
|
|
|
60
60
|
externalConnectionDisplayName?: string;
|
|
61
61
|
externalIngressEventId?: string;
|
|
62
62
|
externalIngressEventType?: string;
|
|
63
|
+
/** External IM channel metadata, used for channel_message events. */
|
|
64
|
+
channelProvider?: string;
|
|
65
|
+
channelConversationType?: string;
|
|
66
|
+
/** Provider-side conversation id (the send_message target). */
|
|
67
|
+
channelExternalConversationId?: string;
|
|
68
|
+
/** Provider-side message id (in-thread reply target). */
|
|
69
|
+
channelExternalMessageId?: string;
|
|
63
70
|
/** Original event timestamp (e.g., message.created_at). When present,
|
|
64
71
|
* input steps use this instead of server insertion time for ordering. */
|
|
65
72
|
sentAt?: string;
|
|
66
|
-
ackSourceType?: 'message' | 'task_activity' | 'comment' | 'schedule_run' | 'external_trigger_run';
|
|
73
|
+
ackSourceType?: 'message' | 'task_activity' | 'comment' | 'schedule_run' | 'external_trigger_run' | 'channel_message';
|
|
67
74
|
ackSourceId?: string;
|
|
68
75
|
/** Unread message count in the target chat since agent's last interaction. */
|
|
69
76
|
unreadCount?: number;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,2FAA2F;AAC3F,MAAM,MAAM,UAAU,GAAG;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACjE,mFAAmF;IACnF,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,sFAAsF;IACtF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qFAAqF;IACrF,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,2DAA2D;AAC3D,MAAM,MAAM,aAAa,GAAG;IAC1B,eAAe,EAAE,OAAO,CAAC;IACzB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,kBAAkB,EAAE,UAAU,EAAE,CAAC;IACjC,UAAU,EAAE,WAAW,EAAE,CAAC;IAC1B,oFAAoF;IACpF,0BAA0B,CAAC,EAAE,MAAM,CAAC;CACrC,CAAC;AAEF,4CAA4C;AAC5C,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EACA,SAAS,GACT,MAAM,GACN,cAAc,GACd,cAAc,GACd,UAAU,GACV,kBAAkB,GAClB,UAAU,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,KAAK,CAAC;QAClB,EAAE,EAAE,MAAM,CAAC;QACX,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC,CAAC;IACH,8DAA8D;IAC9D,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oEAAoE;IACpE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mEAAmE;IACnE,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,4BAA4B,CAAC,EAAE,MAAM,CAAC;IACtC,6BAA6B,CAAC,EAAE,MAAM,CAAC;IACvC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC;8EAC0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parall/agent-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.37.0",
|
|
4
4
|
"description": "Shared agent runtime orchestration helpers for Parall",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"@opentelemetry/sdk-logs": "^0.57.0",
|
|
36
36
|
"@opentelemetry/sdk-metrics": "^1.30.0",
|
|
37
37
|
"@opentelemetry/sdk-trace-node": "^1.30.0",
|
|
38
|
-
"@parall/sdk": "1.
|
|
38
|
+
"@parall/sdk": "1.37.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@types/node": "^22.0.0",
|
package/src/bridge-workspace.ts
CHANGED
|
@@ -24,11 +24,21 @@ To say something in a chat, you **must** invoke the Parall CLI via your shell/ex
|
|
|
24
24
|
|
|
25
25
|
All outbound interactions go through the \`parall\` CLI. Credentials are pre-injected as environment variables — no setup needed. If \`parall\` is not on PATH, use \`npx --yes @parall/cli@latest\` instead.
|
|
26
26
|
|
|
27
|
-
- \`parall messages send prll://cht_xxx --text
|
|
28
|
-
- \`parall dm prll://usr_xxx --text
|
|
27
|
+
- \`parall messages send prll://cht_xxx --text-file -\` — reply into the triggering chat (pipe the body via a quoted heredoc; see Shell-safety below)
|
|
28
|
+
- \`parall dm prll://usr_xxx --text-file - [--no-reply]\` — direct message another user
|
|
29
29
|
- \`parall tasks update prll://tsk_xxx --status in_progress\` — task state
|
|
30
30
|
- \`parall no-reply [--reason "..."]\` — explicitly declare this turn silent (audit signal; not required for silence, just clarifies intent)
|
|
31
31
|
|
|
32
|
+
**Shell-safety — 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 — no shell touches it) or a quoted heredoc that disables expansion:
|
|
33
|
+
|
|
34
|
+
\`\`\`bash
|
|
35
|
+
parall messages send prll://cht_xxx --text-file - <<'EOF'
|
|
36
|
+
That costs $1,000, and $(whoami) stays literal. I'm on it.
|
|
37
|
+
EOF
|
|
38
|
+
\`\`\`
|
|
39
|
+
|
|
40
|
+
Keep \`--text "..."\` for short literals with no \`$\`, backtick, or apostrophe.
|
|
41
|
+
|
|
32
42
|
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 — you do not need to pass \`--chat\` or \`--session\` explicitly when the context file is present.
|
|
33
43
|
|
|
34
44
|
## Attachments
|
|
@@ -73,7 +83,7 @@ Messages may arrive with a \`[Thread: prll://msg_xxx]\` line in the event block,
|
|
|
73
83
|
|
|
74
84
|
### CLI usage
|
|
75
85
|
- The \`msg_xxx\` from the \`[Thread: prll://msg_xxx]\` line is the thread root ID — pass it as \`--thread-root-id\`
|
|
76
|
-
- Reply in a thread: \`parall messages send <chatId> --text
|
|
86
|
+
- Reply in a thread: \`parall messages send <chatId> --text-file /tmp/reply.md --thread-root-id <msgId>\`
|
|
77
87
|
- Read thread replies: \`parall messages list <chatId> --thread-root-id <msgId>\`
|
|
78
88
|
|
|
79
89
|
### Thread etiquette
|
package/src/event-format.ts
CHANGED
|
@@ -89,6 +89,24 @@ export function buildEventBody(event: ParallEvent): string {
|
|
|
89
89
|
if (event.scheduledFireAt) lines.push(`[Scheduled at: ${sanitizeMeta(event.scheduledFireAt)}]`);
|
|
90
90
|
if (event.attachedUri) lines.push(`[Attached: ${sanitizeMeta(event.attachedUri)}]`);
|
|
91
91
|
lines.push('', event.body);
|
|
92
|
+
} else if (event.type === 'channel_message') {
|
|
93
|
+
// External IM channel: the audience is OUTSIDE Parall. Frame the
|
|
94
|
+
// event so the agent writes for that audience — no prll:// refs, no
|
|
95
|
+
// internal cards/links in the reply body (they are dead links there).
|
|
96
|
+
lines.push(`[Event: channel.message]`);
|
|
97
|
+
const providerLabel = sanitizeMeta(event.channelProvider ?? 'external IM');
|
|
98
|
+
const convLabel = event.channelExternalConversationId
|
|
99
|
+
? `${sanitizeMeta(event.channelExternalConversationId)} (${sanitizeMeta(event.channelConversationType ?? 'conversation')})`
|
|
100
|
+
: sanitizeMeta(event.channelConversationType ?? 'conversation');
|
|
101
|
+
lines.push(`[Channel: ${providerLabel} | conversation: ${convLabel}]`);
|
|
102
|
+
lines.push(`[From: ${sanitizeMeta(event.senderName)} (external user, not a Parall member)]`);
|
|
103
|
+
if (event.channelExternalMessageId) {
|
|
104
|
+
lines.push(`[External message ID: ${sanitizeMeta(event.channelExternalMessageId)}]`);
|
|
105
|
+
}
|
|
106
|
+
lines.push(
|
|
107
|
+
`[Audience: this conversation lives on ${providerLabel}, OUTSIDE Parall. Readers cannot open prll:// links, Parall cards, or internal attachments — never include them in replies. Write plain conversational text.]`,
|
|
108
|
+
);
|
|
109
|
+
lines.push('', event.body);
|
|
92
110
|
} else if (event.type === 'external_trigger') {
|
|
93
111
|
lines.push(`[Event: external.trigger]`);
|
|
94
112
|
lines.push(`[Trigger: prll://${event.targetId}]`);
|
|
@@ -134,7 +152,7 @@ function buildSendMessageHint(event: ParallEvent): string {
|
|
|
134
152
|
}
|
|
135
153
|
|
|
136
154
|
if (event.targetId.startsWith('cht_')) {
|
|
137
|
-
return `\n<system-reminder>To reply, run
|
|
155
|
+
return `\n<system-reminder>To reply, run \`parall messages send prll://${event.targetId} --text-file - <<'EOF'\` … \`EOF\` — 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>`;
|
|
138
156
|
}
|
|
139
157
|
|
|
140
158
|
if (event.targetId.startsWith('tsk_')) {
|
|
@@ -145,6 +163,25 @@ function buildSendMessageHint(event: ParallEvent): string {
|
|
|
145
163
|
return `\n<system-reminder>To communicate, use the CLI: \`parall messages send\` / \`parall dm\`. Your plain text output is not delivered.</system-reminder>`;
|
|
146
164
|
}
|
|
147
165
|
|
|
166
|
+
if (event.type === 'channel_message') {
|
|
167
|
+
// Provider metadata is best-effort (the gateway's connection lookup can
|
|
168
|
+
// fail) — never instruct the agent to invoke a made-up clip alias.
|
|
169
|
+
const clipLabel = event.channelProvider
|
|
170
|
+
? `the \`${event.channelProvider}\` clip's`
|
|
171
|
+
: "your channel provider clip's";
|
|
172
|
+
const apiLabel = event.channelProvider ?? 'external platform';
|
|
173
|
+
const target = event.channelExternalConversationId
|
|
174
|
+
? `{"chat_id": "${event.channelExternalConversationId}", "text": "..."}`
|
|
175
|
+
: `{"chat_id": "<conversation id>", "text": "..."}`;
|
|
176
|
+
// Offer the in-thread alternative whenever the inbound message id is
|
|
177
|
+
// known — otherwise the hint nudges every threaded conversation toward a
|
|
178
|
+
// new top-level message.
|
|
179
|
+
const threadAlt = event.channelExternalMessageId
|
|
180
|
+
? ` To reply in-thread to this specific message, use {"message_id": "${event.channelExternalMessageId}", "text": "..."} instead.`
|
|
181
|
+
: '';
|
|
182
|
+
return `\n<system-reminder>To reply, invoke ${clipLabel} \`send_message\` command with ${target} — 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>`;
|
|
183
|
+
}
|
|
184
|
+
|
|
148
185
|
if (event.type === 'external_trigger' || event.targetId.startsWith('xtr_')) {
|
|
149
186
|
return `\n<system-reminder>This external trigger is incoming-only. Your plain text output is not sent back to the external provider. To communicate in Parall, use \`parall messages send\` / \`parall dm\`; provider-specific outbound actions require a separate capability.</system-reminder>`;
|
|
150
187
|
}
|
package/src/gateway-base.ts
CHANGED
|
@@ -7,6 +7,8 @@ import type {
|
|
|
7
7
|
AgentNewSessionData,
|
|
8
8
|
AgentSessionDB,
|
|
9
9
|
Approval,
|
|
10
|
+
ChannelConversation,
|
|
11
|
+
ChannelMessage,
|
|
10
12
|
Chat,
|
|
11
13
|
ChatUpdateData,
|
|
12
14
|
Comment,
|
|
@@ -198,6 +200,9 @@ function resolveStepTarget(event: ParallEvent): { target_type: string; target_id
|
|
|
198
200
|
if (event.type === 'external_trigger' || event.targetId.startsWith('xtr_')) {
|
|
199
201
|
return { target_type: 'external_trigger', target_id: event.targetId };
|
|
200
202
|
}
|
|
203
|
+
if (event.type === 'channel_message' || event.targetId.startsWith('chv_')) {
|
|
204
|
+
return { target_type: 'channel_conversation', target_id: event.targetId };
|
|
205
|
+
}
|
|
201
206
|
if (event.type === 'wiki_comment') {
|
|
202
207
|
// target_id is the full wiki target_uri (scheme-stripped routing key). The
|
|
203
208
|
// server stores target_type freely and only publishes step WS events /
|
|
@@ -269,6 +274,9 @@ async function fetchAllChats(
|
|
|
269
274
|
export class ParallAgentGateway {
|
|
270
275
|
private readonly chatInfoMap = new Map<string, ChatInfo>();
|
|
271
276
|
private readonly dispatchedTasks = new Set<string>();
|
|
277
|
+
// connection id → provider alias, for channel_message prompt labeling
|
|
278
|
+
// (stable mapping; avoids one connection fetch per inbound message).
|
|
279
|
+
private readonly channelConnectionProviders = new Map<string, string>();
|
|
272
280
|
private readonly dispatchedMessages = new Set<string>();
|
|
273
281
|
private readonly forkStates = new Map<string, ActiveForkState>();
|
|
274
282
|
private readonly dispatchState: DispatchState = {
|
|
@@ -473,6 +481,18 @@ export class ParallAgentGateway {
|
|
|
473
481
|
`external trigger dispatch failed for ${data.source_id}: ${String(err)}`,
|
|
474
482
|
);
|
|
475
483
|
}
|
|
484
|
+
} else if (data.event_type === 'channel_message') {
|
|
485
|
+
if (!data.source_id) return;
|
|
486
|
+
try {
|
|
487
|
+
const dispatched = await this.fetchAndHandleChannelMessage(data.source_id);
|
|
488
|
+
if (dispatched) {
|
|
489
|
+
this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {});
|
|
490
|
+
}
|
|
491
|
+
} catch (err) {
|
|
492
|
+
this.opts.log?.error(
|
|
493
|
+
`channel message dispatch failed for ${data.source_id}: ${String(err)}`,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
476
496
|
} else if (data.event_type === 'approval_decided') {
|
|
477
497
|
if (!data.source_id) return;
|
|
478
498
|
try {
|
|
@@ -588,9 +608,11 @@ export class ParallAgentGateway {
|
|
|
588
608
|
? 'schedule_fire'
|
|
589
609
|
: event.type === 'external_trigger'
|
|
590
610
|
? 'external_trigger'
|
|
591
|
-
: event.type === '
|
|
592
|
-
? '
|
|
593
|
-
: '
|
|
611
|
+
: event.type === 'channel_message'
|
|
612
|
+
? 'channel_message'
|
|
613
|
+
: event.type === 'approval'
|
|
614
|
+
? 'approval_decided'
|
|
615
|
+
: 'mention',
|
|
594
616
|
trigger_ref:
|
|
595
617
|
event.type === 'task'
|
|
596
618
|
? { task_id: event.targetId }
|
|
@@ -607,9 +629,16 @@ export class ParallAgentGateway {
|
|
|
607
629
|
connection_id: event.externalConnectionId,
|
|
608
630
|
ingress_event_id: event.externalIngressEventId,
|
|
609
631
|
}
|
|
610
|
-
: event.type === '
|
|
611
|
-
? {
|
|
612
|
-
|
|
632
|
+
: event.type === 'channel_message'
|
|
633
|
+
? {
|
|
634
|
+
conversation_id: event.targetId,
|
|
635
|
+
channel_message_id: event.messageId,
|
|
636
|
+
provider: event.channelProvider,
|
|
637
|
+
external_conversation_id: event.channelExternalConversationId,
|
|
638
|
+
}
|
|
639
|
+
: event.type === 'approval'
|
|
640
|
+
? { approval_id: event.messageId }
|
|
641
|
+
: { message_id: event.messageId },
|
|
613
642
|
sender_id: event.senderId,
|
|
614
643
|
sender_name: event.senderName,
|
|
615
644
|
summary: event.body.substring(0, 200),
|
|
@@ -951,7 +980,27 @@ export class ParallAgentGateway {
|
|
|
951
980
|
context: dispatchContext,
|
|
952
981
|
})) {
|
|
953
982
|
if (runtimeEvent.type === 'runtime_session') {
|
|
983
|
+
const priorAgentSessionId = binding?.agentSessionId;
|
|
954
984
|
binding = await this.bindRuntimeSession(sessionKey, runtimeEvent, contextFilePath);
|
|
985
|
+
if (
|
|
986
|
+
event.targetType === 'channel_conversation' &&
|
|
987
|
+
binding.agentSessionId !== priorAgentSessionId
|
|
988
|
+
) {
|
|
989
|
+
// Record the durable chv_ ↔ ase_ mapping (ops drill-down from a
|
|
990
|
+
// conversation into its session). Best-effort bookkeeping —
|
|
991
|
+
// never fail the dispatch over it.
|
|
992
|
+
try {
|
|
993
|
+
await this.opts.client.setChannelConversationSession(
|
|
994
|
+
this.opts.config.org_id,
|
|
995
|
+
event.targetId,
|
|
996
|
+
binding.agentSessionId,
|
|
997
|
+
);
|
|
998
|
+
} catch (err) {
|
|
999
|
+
this.opts.log?.warn(
|
|
1000
|
+
`failed to record session mapping for channel conversation ${event.targetId}: ${String(err)}`,
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
955
1004
|
if (!inputStepsCreated) {
|
|
956
1005
|
// Persist input steps for "earlier events" (batched events that arrived
|
|
957
1006
|
// while a dispatch was in flight) inside the in-flight window so a
|
|
@@ -1976,6 +2025,98 @@ export class ParallAgentGateway {
|
|
|
1976
2025
|
return this.handleExternalTriggerRun(run);
|
|
1977
2026
|
}
|
|
1978
2027
|
|
|
2028
|
+
// fetchAndHandleChannelMessage resolves a channel_message dispatch to its
|
|
2029
|
+
// durable ChannelMessage + conversation and hands it to the inbound
|
|
2030
|
+
// pipeline. targetId = the ChannelConversation id, so per-conversation
|
|
2031
|
+
// multi-turn continuity rides the same per-target session mechanics as
|
|
2032
|
+
// chats. Design: docs/engineering-design/external-im-channel-design.md.
|
|
2033
|
+
private async fetchAndHandleChannelMessage(messageId: string): Promise<boolean> {
|
|
2034
|
+
if (this.shuttingDown) return false;
|
|
2035
|
+
// Capped dedupe (the chat-message path, not the unbounded task set): a busy
|
|
2036
|
+
// external IM conversation would otherwise retain one key per message ever
|
|
2037
|
+
// handled on a long-lived agent. On a RETRYABLE failure the claim is
|
|
2038
|
+
// released so a later dispatch.new / catch-up re-fetches (matching the
|
|
2039
|
+
// chat-message path); a 404/stale result keeps the claim and acks.
|
|
2040
|
+
const claimKey = `channel_message:${messageId}`;
|
|
2041
|
+
if (!this.tryClaimMessage(claimKey)) return false;
|
|
2042
|
+
|
|
2043
|
+
let msg: ChannelMessage | null = null;
|
|
2044
|
+
let conv: ChannelConversation | null = null;
|
|
2045
|
+
try {
|
|
2046
|
+
msg = await this.opts.client.getChannelMessage(this.opts.config.org_id, messageId);
|
|
2047
|
+
conv = await this.opts.client.getChannelConversation(
|
|
2048
|
+
this.opts.config.org_id,
|
|
2049
|
+
msg.conversation_id,
|
|
2050
|
+
);
|
|
2051
|
+
} catch (err: unknown) {
|
|
2052
|
+
const status = (err as { status?: number })?.status;
|
|
2053
|
+
if (status === 404) {
|
|
2054
|
+
this.opts.log?.warn(
|
|
2055
|
+
`channel message ${messageId} not accessible (404), acking stale dispatch`,
|
|
2056
|
+
);
|
|
2057
|
+
return true;
|
|
2058
|
+
}
|
|
2059
|
+
this.dispatchedMessages.delete(claimKey);
|
|
2060
|
+
this.opts.log?.warn(
|
|
2061
|
+
`channel message fetch failed for ${messageId}, leaving pending: ${String(err)}`,
|
|
2062
|
+
);
|
|
2063
|
+
return false;
|
|
2064
|
+
}
|
|
2065
|
+
if (!msg || !conv) {
|
|
2066
|
+
return true;
|
|
2067
|
+
}
|
|
2068
|
+
this.opts.log?.info(`channel message: ${msg.id} (conversation ${conv.id})`);
|
|
2069
|
+
|
|
2070
|
+
// Resolve the provider from the conversation's connection for prompt
|
|
2071
|
+
// labeling + the reply-clip hint. The connection id → provider mapping
|
|
2072
|
+
// is stable, so a tiny cache avoids one fetch per message.
|
|
2073
|
+
let provider = this.channelConnectionProviders.get(conv.connection_id);
|
|
2074
|
+
if (!provider) {
|
|
2075
|
+
try {
|
|
2076
|
+
const connection = await this.opts.client.getChannelConnection(
|
|
2077
|
+
this.opts.config.org_id,
|
|
2078
|
+
conv.connection_id,
|
|
2079
|
+
);
|
|
2080
|
+
provider = connection.provider;
|
|
2081
|
+
this.channelConnectionProviders.set(conv.connection_id, provider);
|
|
2082
|
+
} catch {
|
|
2083
|
+
provider = undefined; // label degrades; reply hint still names the clip generically
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
const event: ParallEvent = {
|
|
2088
|
+
type: 'channel_message',
|
|
2089
|
+
targetId: conv.id,
|
|
2090
|
+
targetName: conv.external_user_name || conv.external_conversation_id,
|
|
2091
|
+
targetType: 'channel_conversation',
|
|
2092
|
+
senderId: msg.external_user_id || 'external',
|
|
2093
|
+
senderName: msg.external_user_name || msg.external_user_id || 'external user',
|
|
2094
|
+
messageId: msg.id,
|
|
2095
|
+
body: msg.text,
|
|
2096
|
+
sentAt: msg.received_at,
|
|
2097
|
+
channelProvider: provider,
|
|
2098
|
+
channelConversationType: conv.conversation_type || undefined,
|
|
2099
|
+
channelExternalConversationId: conv.external_conversation_id,
|
|
2100
|
+
channelExternalMessageId: msg.external_message_id,
|
|
2101
|
+
ackSourceType: 'channel_message',
|
|
2102
|
+
ackSourceId: msg.id,
|
|
2103
|
+
};
|
|
2104
|
+
|
|
2105
|
+
// Release the claim if the event isn't actually dispatched (or throws) so
|
|
2106
|
+
// a retry can re-attempt — same contract as the chat-message path.
|
|
2107
|
+
let dispatched: boolean;
|
|
2108
|
+
try {
|
|
2109
|
+
dispatched = await this.handleInboundEvent(event);
|
|
2110
|
+
} catch (err) {
|
|
2111
|
+
this.dispatchedMessages.delete(claimKey);
|
|
2112
|
+
throw err;
|
|
2113
|
+
}
|
|
2114
|
+
if (!dispatched) {
|
|
2115
|
+
this.dispatchedMessages.delete(claimKey);
|
|
2116
|
+
}
|
|
2117
|
+
return dispatched;
|
|
2118
|
+
}
|
|
2119
|
+
|
|
1979
2120
|
private async handleExternalTriggerRun(run: ExternalTriggerRun): Promise<boolean> {
|
|
1980
2121
|
if (this.shuttingDown) return false;
|
|
1981
2122
|
const dedupeKey = `external_trigger_run:${run.id}`;
|
|
@@ -2172,6 +2313,8 @@ export class ParallAgentGateway {
|
|
|
2172
2313
|
dispatched = await this.fetchAndHandleScheduleFire(item.source_id, item.actor_id);
|
|
2173
2314
|
} else if (item.event_type === 'external_trigger' && item.source_id) {
|
|
2174
2315
|
dispatched = await this.fetchAndHandleExternalTriggerRun(item.source_id);
|
|
2316
|
+
} else if (item.event_type === 'channel_message' && item.source_id) {
|
|
2317
|
+
dispatched = await this.fetchAndHandleChannelMessage(item.source_id);
|
|
2175
2318
|
} else if (item.event_type === 'approval_decided' && item.source_id) {
|
|
2176
2319
|
dispatched = await this.fetchAndHandleApprovalDecided(
|
|
2177
2320
|
item.source_id,
|