@poolzin/pool-bot 2026.2.21 → 2026.2.22

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 (369) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/agents/api-key-rotation.js +47 -0
  3. package/dist/agents/apply-patch-update.js +19 -9
  4. package/dist/agents/apply-patch.js +72 -47
  5. package/dist/agents/bash-tools.exec.js +141 -559
  6. package/dist/agents/cli-backends.js +49 -6
  7. package/dist/agents/cli-runner/helpers.js +69 -152
  8. package/dist/agents/cli-runner.js +70 -19
  9. package/dist/agents/identity.js +20 -1
  10. package/dist/agents/image-sanitization.js +9 -0
  11. package/dist/agents/live-auth-keys.js +123 -26
  12. package/dist/agents/live-model-filter.js +13 -4
  13. package/dist/agents/model-catalog.js +40 -9
  14. package/dist/agents/model-forward-compat.js +60 -23
  15. package/dist/agents/model-selection.js +134 -41
  16. package/dist/agents/pi-auth-json.js +2 -2
  17. package/dist/agents/pi-embedded-helpers/bootstrap.js +65 -15
  18. package/dist/agents/pi-embedded-helpers/errors.js +140 -15
  19. package/dist/agents/pi-embedded-helpers/images.js +22 -12
  20. package/dist/agents/pi-embedded-helpers.js +2 -2
  21. package/dist/agents/pi-embedded-runner/abort.js +10 -3
  22. package/dist/agents/pi-embedded-runner/compact.js +230 -32
  23. package/dist/agents/pi-embedded-runner/extra-params.js +203 -12
  24. package/dist/agents/pi-embedded-runner/google.js +109 -19
  25. package/dist/agents/pi-embedded-runner/history.js +35 -17
  26. package/dist/agents/pi-embedded-runner/run/attempt.js +386 -95
  27. package/dist/agents/pi-embedded-runner/run/images.js +81 -55
  28. package/dist/agents/pi-embedded-runner/run/payloads.js +89 -39
  29. package/dist/agents/pi-embedded-runner/run.js +193 -25
  30. package/dist/agents/pi-embedded-runner/run.overflow-compaction.mocks.shared.js +2 -2
  31. package/dist/agents/pi-embedded-runner/runs.js +17 -8
  32. package/dist/agents/pi-embedded-runner/tool-result-context-guard.js +262 -0
  33. package/dist/agents/pi-embedded-runner.js +1 -1
  34. package/dist/agents/pi-embedded-subscribe.handlers.tools.js +180 -10
  35. package/dist/agents/pi-embedded-subscribe.js +37 -0
  36. package/dist/agents/pi-embedded-subscribe.tools.js +127 -30
  37. package/dist/agents/pi-model-discovery.js +9 -2
  38. package/dist/agents/pi-tool-definition-adapter.js +60 -8
  39. package/dist/agents/pi-tools.before-tool-call.js +1 -1
  40. package/dist/agents/pi-tools.js +113 -94
  41. package/dist/agents/pi-tools.read.js +337 -38
  42. package/dist/agents/poolbot-tools.js +14 -5
  43. package/dist/agents/sandbox/docker.js +10 -5
  44. package/dist/agents/sandbox/registry.js +96 -46
  45. package/dist/agents/sandbox/sanitize-env-vars.js +82 -0
  46. package/dist/agents/sandbox-paths.js +43 -10
  47. package/dist/agents/session-tool-result-guard-wrapper.js +23 -11
  48. package/dist/agents/session-tool-result-guard.js +39 -39
  49. package/dist/agents/session-transcript-repair.js +36 -33
  50. package/dist/agents/session-write-lock.js +62 -44
  51. package/dist/agents/skills/frontmatter.js +49 -88
  52. package/dist/agents/skills/workspace.js +335 -28
  53. package/dist/agents/subagent-announce.js +508 -174
  54. package/dist/agents/subagent-registry.js +45 -4
  55. package/dist/agents/subagent-spawn.js +16 -33
  56. package/dist/agents/system-prompt-report.js +27 -10
  57. package/dist/agents/system-prompt.js +26 -32
  58. package/dist/agents/tool-call-id.js +69 -17
  59. package/dist/agents/tool-display-common.js +1 -1
  60. package/dist/agents/tool-images.js +64 -31
  61. package/dist/agents/tools/canvas-tool.js +17 -11
  62. package/dist/agents/tools/common.js +37 -19
  63. package/dist/agents/tools/cron-tool.js +40 -38
  64. package/dist/agents/tools/gateway.js +70 -2
  65. package/dist/agents/tools/message-tool.js +181 -40
  66. package/dist/agents/tools/nodes-tool.js +128 -36
  67. package/dist/agents/tools/nodes-utils.js +12 -38
  68. package/dist/agents/tools/session-status-tool.js +24 -71
  69. package/dist/agents/tools/sessions-helpers.js +38 -210
  70. package/dist/agents/tools/sessions-spawn-tool.js +28 -198
  71. package/dist/agents/tools/telegram-actions.js +58 -7
  72. package/dist/agents/tools/web-fetch-utils.js +112 -7
  73. package/dist/agents/tools/web-fetch.js +279 -175
  74. package/dist/agents/tools/web-shared.js +71 -8
  75. package/dist/agents/usage.js +25 -16
  76. package/dist/auto-reply/commands-registry.data.js +85 -11
  77. package/dist/auto-reply/dispatch.js +40 -21
  78. package/dist/auto-reply/reply/abort.js +102 -33
  79. package/dist/auto-reply/reply/commands-core.js +82 -33
  80. package/dist/auto-reply/reply/commands-export-session.js +1 -1
  81. package/dist/auto-reply/reply/commands-info.js +41 -12
  82. package/dist/auto-reply/reply/commands-subagents.js +352 -100
  83. package/dist/auto-reply/reply/commands-system-prompt.js +2 -2
  84. package/dist/auto-reply/reply/dispatch-from-config.js +100 -29
  85. package/dist/auto-reply/reply/elevated-unavailable.js +1 -1
  86. package/dist/auto-reply/reply/inbound-meta.js +12 -1
  87. package/dist/auto-reply/reply/mentions.js +18 -11
  88. package/dist/auto-reply/reply/normalize-reply.js +17 -8
  89. package/dist/auto-reply/reply/reply-dispatcher.js +62 -10
  90. package/dist/auto-reply/reply/session.js +102 -21
  91. package/dist/auto-reply/reply/streaming-directives.js +16 -5
  92. package/dist/auto-reply/status.js +73 -50
  93. package/dist/browser/extension-relay.js +3 -3
  94. package/dist/browser/http-auth.js +1 -1
  95. package/dist/browser/paths.js +2 -2
  96. package/dist/build-info.json +3 -3
  97. package/dist/channels/allowlist-match.js +20 -0
  98. package/dist/channels/allowlists/resolve-utils.js +65 -2
  99. package/dist/channels/chat-type.js +8 -4
  100. package/dist/channels/dock.js +127 -35
  101. package/dist/channels/draft-stream-loop.js +6 -2
  102. package/dist/channels/plugins/actions/telegram.js +42 -18
  103. package/dist/channels/plugins/allowlist-match.js +1 -1
  104. package/dist/channels/plugins/group-mentions.js +51 -41
  105. package/dist/channels/plugins/message-action-names.js +2 -0
  106. package/dist/channels/plugins/message-actions.js +24 -5
  107. package/dist/channels/plugins/normalize/discord.js +26 -4
  108. package/dist/channels/plugins/normalize/signal.js +35 -22
  109. package/dist/channels/plugins/onboarding/helpers.js +8 -26
  110. package/dist/channels/plugins/outbound/imessage.js +15 -14
  111. package/dist/channels/registry.js +20 -7
  112. package/dist/cli/acp-cli.js +7 -5
  113. package/dist/cli/browser-cli-extension.js +25 -12
  114. package/dist/cli/browser-cli-state.cookies-storage.js +25 -6
  115. package/dist/cli/browser-cli-state.js +101 -145
  116. package/dist/cli/command-options.js +28 -0
  117. package/dist/cli/completion-cli.js +6 -6
  118. package/dist/cli/cron-cli/register.cron-add.js +25 -1
  119. package/dist/cli/cron-cli/register.cron-edit.js +44 -0
  120. package/dist/cli/cron-cli/shared.js +7 -1
  121. package/dist/cli/daemon-cli/lifecycle-core.js +23 -21
  122. package/dist/cli/daemon-cli/lifecycle.js +23 -247
  123. package/dist/cli/daemon-cli/register-service-commands.js +25 -4
  124. package/dist/cli/daemon-cli.js +1 -0
  125. package/dist/cli/devices-cli.js +33 -20
  126. package/dist/cli/gateway-cli/register.js +37 -105
  127. package/dist/cli/gateway-cli/run.js +49 -11
  128. package/dist/cli/nodes-camera.js +59 -4
  129. package/dist/cli/nodes-cli/register.camera.js +27 -24
  130. package/dist/cli/nodes-cli/rpc.js +21 -38
  131. package/dist/cli/qr-cli.js +2 -2
  132. package/dist/cli/skills-cli.format.js +2 -2
  133. package/dist/cli/update-cli/progress.js +2 -2
  134. package/dist/cli/update-cli/restart-helper.js +28 -7
  135. package/dist/cli/update-cli/shared.js +7 -7
  136. package/dist/cli/update-cli/status.js +1 -1
  137. package/dist/cli/update-cli/update-command.js +14 -8
  138. package/dist/cli/update-cli/wizard.js +2 -2
  139. package/dist/cli/update-cli.js +21 -1027
  140. package/dist/commands/auth-choice.apply.anthropic.js +10 -2
  141. package/dist/commands/channels/add-mutators.js +3 -35
  142. package/dist/commands/channels/add.js +39 -51
  143. package/dist/commands/config-validation.js +1 -1
  144. package/dist/commands/configure.gateway-auth.js +52 -15
  145. package/dist/commands/configure.gateway.js +84 -40
  146. package/dist/commands/doctor-completion.js +3 -3
  147. package/dist/commands/doctor-config-flow.js +536 -16
  148. package/dist/commands/doctor-gateway-services.js +103 -79
  149. package/dist/commands/doctor-memory-search.js +9 -9
  150. package/dist/commands/doctor-platform-notes.js +57 -30
  151. package/dist/commands/doctor-prompter.js +26 -15
  152. package/dist/commands/doctor-session-locks.js +1 -1
  153. package/dist/commands/doctor.js +21 -9
  154. package/dist/commands/model-picker.js +120 -95
  155. package/dist/commands/models/set.js +2 -21
  156. package/dist/commands/models/shared.js +65 -37
  157. package/dist/commands/onboard-helpers.js +81 -39
  158. package/dist/commands/openai-codex-oauth.js +1 -1
  159. package/dist/commands/sessions.js +52 -53
  160. package/dist/commands/status.summary.js +52 -34
  161. package/dist/commands/test-wizard-helpers.js +2 -2
  162. package/dist/config/defaults.js +79 -42
  163. package/dist/config/group-policy.js +50 -18
  164. package/dist/config/includes.js +37 -10
  165. package/dist/config/schema.help.js +5 -4
  166. package/dist/config/schema.hints.js +2 -2
  167. package/dist/config/schema.labels.js +1 -0
  168. package/dist/config/sessions/group.js +12 -11
  169. package/dist/config/sessions/paths.js +137 -11
  170. package/dist/config/sessions/store.js +185 -65
  171. package/dist/config/sessions/types.js +15 -1
  172. package/dist/config/sessions.js +1 -0
  173. package/dist/config/telegram-custom-commands.js +3 -2
  174. package/dist/config/types.js +2 -0
  175. package/dist/config/zod-schema.agent-defaults.js +6 -27
  176. package/dist/config/zod-schema.agent-runtime.js +171 -79
  177. package/dist/config/zod-schema.providers-core.js +138 -65
  178. package/dist/config/zod-schema.session.js +49 -22
  179. package/dist/control-ui/assets/index-HRr1grwl.js.map +1 -1
  180. package/dist/cron/isolated-agent/run.js +224 -57
  181. package/dist/cron/normalize.js +48 -45
  182. package/dist/cron/run-log.js +14 -0
  183. package/dist/cron/service/jobs.js +190 -28
  184. package/dist/cron/service/normalize.js +29 -11
  185. package/dist/cron/service/store.js +30 -44
  186. package/dist/cron/service/timer.js +182 -96
  187. package/dist/cron/service.js +3 -0
  188. package/dist/cron/stagger.js +37 -0
  189. package/dist/daemon/inspect.js +132 -92
  190. package/dist/daemon/runtime-paths.js +25 -4
  191. package/dist/daemon/service-audit.js +47 -16
  192. package/dist/discord/accounts.js +23 -20
  193. package/dist/discord/monitor/agent-components.js +1115 -219
  194. package/dist/discord/monitor/allow-list.js +114 -34
  195. package/dist/discord/monitor/listeners.js +204 -97
  196. package/dist/discord/monitor/message-handler.js +21 -10
  197. package/dist/discord/monitor/message-handler.preflight.js +195 -101
  198. package/dist/discord/monitor/message-handler.process.js +384 -123
  199. package/dist/discord/monitor/message-utils.js +86 -23
  200. package/dist/discord/monitor/native-command.js +77 -57
  201. package/dist/discord/monitor/provider.js +122 -117
  202. package/dist/discord/monitor/reply-context.js +20 -16
  203. package/dist/discord/monitor/reply-delivery.js +40 -8
  204. package/dist/discord/monitor/rest-fetch.js +22 -0
  205. package/dist/discord/monitor/threading.js +117 -24
  206. package/dist/discord/send.js +2 -1
  207. package/dist/discord/send.outbound.js +124 -11
  208. package/dist/discord/send.shared.js +112 -72
  209. package/dist/discord/voice-message.js +3 -3
  210. package/dist/gateway/auth.js +119 -44
  211. package/dist/gateway/call.js +76 -34
  212. package/dist/gateway/channel-health-monitor.js +57 -50
  213. package/dist/gateway/client.js +63 -29
  214. package/dist/gateway/control-ui-contract.js +1 -1
  215. package/dist/gateway/gateway-config-prompts.shared.js +2 -2
  216. package/dist/gateway/net.js +109 -1
  217. package/dist/gateway/protocol/index.js +5 -8
  218. package/dist/gateway/protocol/schema/agent.js +19 -1
  219. package/dist/gateway/protocol/schema/channels.js +21 -0
  220. package/dist/gateway/protocol/schema/cron.js +43 -30
  221. package/dist/gateway/protocol/schema/protocol-schemas.js +6 -11
  222. package/dist/gateway/protocol/schema/sessions.js +5 -1
  223. package/dist/gateway/protocol/schema.js +0 -1
  224. package/dist/gateway/server/presence-events.js +12 -0
  225. package/dist/gateway/server/ws-connection/message-handler.js +203 -212
  226. package/dist/gateway/server/ws-connection.js +58 -21
  227. package/dist/gateway/server-broadcast.js +18 -13
  228. package/dist/gateway/server-cron.js +177 -10
  229. package/dist/gateway/server-methods/agent-job.js +131 -38
  230. package/dist/gateway/server-methods/send.js +60 -14
  231. package/dist/gateway/server-methods/sessions.js +160 -96
  232. package/dist/gateway/server-methods/system.js +5 -7
  233. package/dist/gateway/server-methods-list.js +8 -0
  234. package/dist/gateway/server-methods.js +24 -8
  235. package/dist/gateway/server-node-events.js +278 -68
  236. package/dist/gateway/session-utils.fs.js +316 -75
  237. package/dist/gateway/session-utils.js +224 -70
  238. package/dist/gateway/sessions-patch.js +63 -20
  239. package/dist/gateway/test-temp-config.js +1 -1
  240. package/dist/gateway/tools-invoke-http.js +118 -70
  241. package/dist/gateway/ws-log.js +135 -107
  242. package/dist/hooks/frontmatter.js +36 -82
  243. package/dist/hooks/install.js +149 -139
  244. package/dist/hooks/internal-hooks.js +29 -4
  245. package/dist/hooks/plugin-hooks.js +2 -1
  246. package/dist/imessage/monitor/deliver.js +10 -4
  247. package/dist/imessage/monitor/monitor-provider.js +138 -375
  248. package/dist/imessage/monitor/runtime.js +4 -8
  249. package/dist/imessage/send.js +65 -19
  250. package/dist/infra/exec-approvals-allowlist.js +7 -0
  251. package/dist/infra/exec-approvals.js +35 -920
  252. package/dist/infra/exec-safe-bin-trust.js +64 -0
  253. package/dist/infra/heartbeat-runner.js +207 -134
  254. package/dist/infra/heartbeat-wake.js +183 -22
  255. package/dist/infra/install-source-utils.js +47 -0
  256. package/dist/infra/net/ssrf.js +170 -36
  257. package/dist/infra/outbound/deliver.js +224 -58
  258. package/dist/infra/outbound/message-action-spec.js +12 -5
  259. package/dist/infra/outbound/outbound-session.js +27 -25
  260. package/dist/infra/poolbot-root.js +32 -22
  261. package/dist/infra/ports.js +14 -11
  262. package/dist/infra/skills-remote.js +48 -37
  263. package/dist/infra/system-events.js +25 -11
  264. package/dist/infra/system-presence.js +26 -33
  265. package/dist/infra/tmp-poolbot-dir.js +81 -2
  266. package/dist/infra/wsl.js +37 -1
  267. package/dist/line/bot-message-context.js +163 -191
  268. package/dist/logging/subsystem.js +59 -22
  269. package/dist/markdown/ir.js +124 -50
  270. package/dist/media/store.js +1 -1
  271. package/dist/media-understanding/runner.entries.js +42 -25
  272. package/dist/media-understanding/runner.js +53 -488
  273. package/dist/memory/embeddings-gemini.js +53 -38
  274. package/dist/memory/manager-embedding-ops.js +48 -69
  275. package/dist/pairing/pairing-store.js +178 -119
  276. package/dist/plugin-sdk/index.js +34 -6
  277. package/dist/plugins/hooks.js +135 -14
  278. package/dist/plugins/install.js +190 -152
  279. package/dist/polls.js +11 -0
  280. package/dist/routing/resolve-route.js +190 -56
  281. package/dist/routing/session-key.js +38 -22
  282. package/dist/runtime.js +35 -9
  283. package/dist/security/audit-channel.js +1 -1
  284. package/dist/sessions/session-key-utils.js +29 -11
  285. package/dist/shared/frontmatter.js +5 -5
  286. package/dist/shared/node-list-types.js +1 -0
  287. package/dist/shared/string-normalization.js +15 -0
  288. package/dist/signal/monitor/event-handler.js +68 -36
  289. package/dist/signal/send.js +29 -37
  290. package/dist/slack/monitor/allow-list.js +10 -11
  291. package/dist/slack/monitor/commands.js +14 -3
  292. package/dist/slack/monitor/events/interactions.js +4 -4
  293. package/dist/slack/monitor/media.js +224 -16
  294. package/dist/slack/monitor/message-handler/dispatch.js +247 -13
  295. package/dist/slack/monitor/message-handler/prepare.js +128 -45
  296. package/dist/slack/monitor/slash.js +357 -144
  297. package/dist/slack/streaming.js +77 -0
  298. package/dist/telegram/accounts.js +40 -13
  299. package/dist/telegram/allowed-updates.js +3 -0
  300. package/dist/telegram/bot/delivery.js +129 -66
  301. package/dist/telegram/bot/helpers.js +136 -122
  302. package/dist/telegram/bot-handlers.js +600 -339
  303. package/dist/telegram/bot-message-context.js +115 -73
  304. package/dist/telegram/bot-message-dispatch.js +235 -104
  305. package/dist/telegram/bot-native-command-menu.js +3 -1
  306. package/dist/telegram/bot-native-commands.js +213 -193
  307. package/dist/telegram/bot.js +24 -132
  308. package/dist/telegram/draft-stream.js +84 -75
  309. package/dist/telegram/format.js +150 -6
  310. package/dist/telegram/send.js +415 -255
  311. package/dist/telegram/targets.js +21 -2
  312. package/dist/telegram/update-offset-store.js +19 -3
  313. package/dist/terminal/restore.js +5 -2
  314. package/dist/test-utils/fetch-mock.js +5 -0
  315. package/dist/version.js +18 -5
  316. package/dist/web/auto-reply/monitor/broadcast.js +7 -3
  317. package/dist/web/auto-reply/monitor/on-message.js +6 -3
  318. package/dist/web/inbound/media.js +34 -8
  319. package/dist/web/inbound/monitor.js +34 -17
  320. package/dist/web/inbound/send-api.js +18 -17
  321. package/dist/web/outbound.js +12 -5
  322. package/dist/wizard/clack-prompter.js +40 -7
  323. package/extensions/bluebubbles/package.json +1 -1
  324. package/extensions/copilot-proxy/package.json +1 -1
  325. package/extensions/diagnostics-otel/package.json +1 -1
  326. package/extensions/discord/package.json +1 -1
  327. package/extensions/feishu/package.json +1 -1
  328. package/extensions/google-antigravity-auth/package.json +1 -1
  329. package/extensions/google-gemini-cli-auth/package.json +1 -1
  330. package/extensions/googlechat/package.json +1 -1
  331. package/extensions/imessage/package.json +1 -1
  332. package/extensions/irc/package.json +1 -1
  333. package/extensions/line/package.json +1 -1
  334. package/extensions/llm-task/package.json +1 -1
  335. package/extensions/lobster/package.json +1 -1
  336. package/extensions/matrix/CHANGELOG.md +5 -0
  337. package/extensions/matrix/package.json +1 -1
  338. package/extensions/mattermost/package.json +1 -1
  339. package/extensions/memory-core/package.json +1 -1
  340. package/extensions/memory-lancedb/package.json +1 -1
  341. package/extensions/minimax-portal-auth/package.json +1 -1
  342. package/extensions/msteams/CHANGELOG.md +5 -0
  343. package/extensions/msteams/package.json +1 -1
  344. package/extensions/nextcloud-talk/package.json +1 -1
  345. package/extensions/nostr/CHANGELOG.md +5 -0
  346. package/extensions/nostr/package.json +1 -1
  347. package/extensions/open-prose/package.json +1 -1
  348. package/extensions/openai-codex-auth/package.json +1 -1
  349. package/extensions/signal/package.json +1 -1
  350. package/extensions/slack/package.json +1 -1
  351. package/extensions/telegram/package.json +1 -1
  352. package/extensions/tlon/package.json +1 -1
  353. package/extensions/twitch/CHANGELOG.md +5 -0
  354. package/extensions/twitch/package.json +1 -1
  355. package/extensions/voice-call/CHANGELOG.md +5 -0
  356. package/extensions/voice-call/package.json +1 -1
  357. package/extensions/whatsapp/package.json +1 -1
  358. package/extensions/zalo/CHANGELOG.md +5 -0
  359. package/extensions/zalo/package.json +1 -1
  360. package/extensions/zalouser/CHANGELOG.md +5 -0
  361. package/extensions/zalouser/package.json +1 -1
  362. package/package.json +1 -1
  363. package/skills/apple-reminders/SKILL.md +100 -49
  364. package/skills/coding-agent/SKILL.md +34 -28
  365. package/skills/github/SKILL.md +131 -16
  366. package/skills/imsg/SKILL.md +112 -15
  367. package/skills/openhue/SKILL.md +101 -19
  368. package/skills/tmux/SKILL.md +111 -79
  369. package/skills/weather/SKILL.md +88 -25
@@ -1,97 +1,258 @@
1
- import crypto from "node:crypto";
2
- import path from "node:path";
3
1
  import { resolveQueueSettings } from "../auto-reply/reply/queue.js";
2
+ import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
4
3
  import { loadConfig } from "../config/config.js";
5
4
  import { loadSessionStore, resolveAgentIdFromSessionKey, resolveMainSessionKey, resolveStorePath, } from "../config/sessions.js";
6
5
  import { callGateway } from "../gateway/call.js";
7
- import { formatDurationCompact } from "../infra/format-time/format-duration.js";
8
6
  import { normalizeMainKey } from "../routing/session-key.js";
9
7
  import { defaultRuntime } from "../runtime.js";
8
+ import { extractTextFromChatContent } from "../shared/chat-content.js";
10
9
  import { deliveryContextFromSession, mergeDeliveryContext, normalizeDeliveryContext, } from "../utils/delivery-context.js";
10
+ import { isDeliverableMessageChannel } from "../utils/message-channel.js";
11
+ import { buildAnnounceIdFromChildRun, buildAnnounceIdempotencyKey, resolveQueueAnnounceId, } from "./announce-idempotency.js";
11
12
  import { isEmbeddedPiRunActive, queueEmbeddedPiMessage, waitForEmbeddedPiRunEnd, } from "./pi-embedded.js";
12
13
  import { enqueueAnnounce } from "./subagent-announce-queue.js";
13
- import { readLatestAssistantReply } from "./tools/agent-step.js";
14
+ import { getSubagentDepthFromSessionStore } from "./subagent-depth.js";
15
+ import { sanitizeTextContent, extractAssistantText } from "./tools/sessions-helpers.js";
16
+ function buildCompletionDeliveryMessage(params) {
17
+ const findingsText = params.findings.trim();
18
+ const hasFindings = findingsText.length > 0 && findingsText !== "(no output)";
19
+ const header = `✅ Subagent ${params.subagentName} finished`;
20
+ if (!hasFindings) {
21
+ return header;
22
+ }
23
+ return `${header}\n\n${findingsText}`;
24
+ }
25
+ function summarizeDeliveryError(error) {
26
+ if (error instanceof Error) {
27
+ return error.message || "error";
28
+ }
29
+ if (typeof error === "string") {
30
+ return error;
31
+ }
32
+ if (error === undefined || error === null) {
33
+ return "unknown error";
34
+ }
35
+ try {
36
+ return JSON.stringify(error);
37
+ }
38
+ catch {
39
+ return "error";
40
+ }
41
+ }
42
+ function extractToolResultText(content) {
43
+ if (typeof content === "string") {
44
+ return sanitizeTextContent(content);
45
+ }
46
+ if (content && typeof content === "object" && !Array.isArray(content)) {
47
+ const obj = content;
48
+ if (typeof obj.text === "string") {
49
+ return sanitizeTextContent(obj.text);
50
+ }
51
+ if (typeof obj.output === "string") {
52
+ return sanitizeTextContent(obj.output);
53
+ }
54
+ if (typeof obj.content === "string") {
55
+ return sanitizeTextContent(obj.content);
56
+ }
57
+ if (typeof obj.result === "string") {
58
+ return sanitizeTextContent(obj.result);
59
+ }
60
+ if (typeof obj.error === "string") {
61
+ return sanitizeTextContent(obj.error);
62
+ }
63
+ if (typeof obj.summary === "string") {
64
+ return sanitizeTextContent(obj.summary);
65
+ }
66
+ }
67
+ if (!Array.isArray(content)) {
68
+ return "";
69
+ }
70
+ const joined = extractTextFromChatContent(content, {
71
+ sanitizeText: sanitizeTextContent,
72
+ normalizeText: (text) => text,
73
+ joinWith: "\n",
74
+ });
75
+ return joined?.trim() ?? "";
76
+ }
77
+ function extractSubagentOutputText(message) {
78
+ if (!message || typeof message !== "object") {
79
+ return "";
80
+ }
81
+ const role = message.role;
82
+ const content = message.content;
83
+ if (role === "assistant") {
84
+ const assistantText = extractAssistantText(message);
85
+ if (assistantText) {
86
+ return assistantText;
87
+ }
88
+ if (typeof content === "string") {
89
+ return sanitizeTextContent(content);
90
+ }
91
+ if (Array.isArray(content)) {
92
+ return (extractTextFromChatContent(content, {
93
+ sanitizeText: sanitizeTextContent,
94
+ normalizeText: (text) => text.trim(),
95
+ joinWith: "",
96
+ }) ?? "");
97
+ }
98
+ return "";
99
+ }
100
+ if (role === "toolResult" || role === "tool") {
101
+ return extractToolResultText(message.content);
102
+ }
103
+ if (typeof content === "string") {
104
+ return sanitizeTextContent(content);
105
+ }
106
+ if (Array.isArray(content)) {
107
+ return (extractTextFromChatContent(content, {
108
+ sanitizeText: sanitizeTextContent,
109
+ normalizeText: (text) => text.trim(),
110
+ joinWith: "",
111
+ }) ?? "");
112
+ }
113
+ return "";
114
+ }
115
+ async function readLatestSubagentOutput(sessionKey) {
116
+ const history = await callGateway({
117
+ method: "chat.history",
118
+ params: { sessionKey, limit: 50 },
119
+ });
120
+ const messages = Array.isArray(history?.messages) ? history.messages : [];
121
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
122
+ const msg = messages[i];
123
+ const text = extractSubagentOutputText(msg);
124
+ if (text) {
125
+ return text;
126
+ }
127
+ }
128
+ return undefined;
129
+ }
130
+ async function readLatestSubagentOutputWithRetry(params) {
131
+ const RETRY_INTERVAL_MS = 100;
132
+ const deadline = Date.now() + Math.max(0, Math.min(params.maxWaitMs, 15_000));
133
+ let result;
134
+ while (Date.now() < deadline) {
135
+ result = await readLatestSubagentOutput(params.sessionKey);
136
+ if (result?.trim()) {
137
+ return result;
138
+ }
139
+ await new Promise((resolve) => setTimeout(resolve, RETRY_INTERVAL_MS));
140
+ }
141
+ return result;
142
+ }
143
+ function formatDurationShort(valueMs) {
144
+ if (!valueMs || !Number.isFinite(valueMs) || valueMs <= 0) {
145
+ return "n/a";
146
+ }
147
+ const totalSeconds = Math.round(valueMs / 1000);
148
+ const hours = Math.floor(totalSeconds / 3600);
149
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
150
+ const seconds = totalSeconds % 60;
151
+ if (hours > 0) {
152
+ return `${hours}h${minutes}m`;
153
+ }
154
+ if (minutes > 0) {
155
+ return `${minutes}m${seconds}s`;
156
+ }
157
+ return `${seconds}s`;
158
+ }
14
159
  function formatTokenCount(value) {
15
- if (!value || !Number.isFinite(value))
160
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
16
161
  return "0";
17
- if (value >= 1_000_000)
162
+ }
163
+ if (value >= 1_000_000) {
18
164
  return `${(value / 1_000_000).toFixed(1)}m`;
19
- if (value >= 1_000)
165
+ }
166
+ if (value >= 1_000) {
20
167
  return `${(value / 1_000).toFixed(1)}k`;
168
+ }
21
169
  return String(Math.round(value));
22
170
  }
23
- function formatUsd(value) {
24
- if (value === undefined || !Number.isFinite(value))
25
- return undefined;
26
- if (value >= 1)
27
- return `$${value.toFixed(2)}`;
28
- if (value >= 0.01)
29
- return `$${value.toFixed(2)}`;
30
- return `$${value.toFixed(4)}`;
31
- }
32
- function resolveModelCost(params) {
33
- const provider = params.provider?.trim();
34
- const model = params.model?.trim();
35
- if (!provider || !model)
36
- return undefined;
37
- const models = params.config.models?.providers?.[provider]?.models ?? [];
38
- const entry = models.find((candidate) => candidate.id === model);
39
- return entry?.cost;
40
- }
41
- async function waitForSessionUsage(params) {
171
+ async function buildCompactAnnounceStatsLine(params) {
42
172
  const cfg = loadConfig();
43
173
  const agentId = resolveAgentIdFromSessionKey(params.sessionKey);
44
174
  const storePath = resolveStorePath(cfg.session?.store, { agentId });
45
175
  let entry = loadSessionStore(storePath)[params.sessionKey];
46
- if (!entry)
47
- return { entry, storePath };
48
- const hasTokens = () => entry &&
49
- (typeof entry.totalTokens === "number" ||
50
- typeof entry.inputTokens === "number" ||
51
- typeof entry.outputTokens === "number");
52
- if (hasTokens())
53
- return { entry, storePath };
54
- for (let attempt = 0; attempt < 4; attempt += 1) {
55
- await new Promise((resolve) => setTimeout(resolve, 200));
56
- entry = loadSessionStore(storePath)[params.sessionKey];
57
- if (hasTokens())
176
+ for (let attempt = 0; attempt < 3; attempt += 1) {
177
+ const hasTokenData = typeof entry?.inputTokens === "number" ||
178
+ typeof entry?.outputTokens === "number" ||
179
+ typeof entry?.totalTokens === "number";
180
+ if (hasTokenData) {
58
181
  break;
182
+ }
183
+ await new Promise((resolve) => setTimeout(resolve, 150));
184
+ entry = loadSessionStore(storePath)[params.sessionKey];
185
+ }
186
+ const input = typeof entry?.inputTokens === "number" ? entry.inputTokens : 0;
187
+ const output = typeof entry?.outputTokens === "number" ? entry.outputTokens : 0;
188
+ const ioTotal = input + output;
189
+ const promptCache = typeof entry?.totalTokens === "number" ? entry.totalTokens : undefined;
190
+ const runtimeMs = typeof params.startedAt === "number" && typeof params.endedAt === "number"
191
+ ? Math.max(0, params.endedAt - params.startedAt)
192
+ : undefined;
193
+ const parts = [
194
+ `runtime ${formatDurationShort(runtimeMs)}`,
195
+ `tokens ${formatTokenCount(ioTotal)} (in ${formatTokenCount(input)} / out ${formatTokenCount(output)})`,
196
+ ];
197
+ if (typeof promptCache === "number" && promptCache > ioTotal) {
198
+ parts.push(`prompt/cache ${formatTokenCount(promptCache)}`);
59
199
  }
60
- return { entry, storePath };
200
+ return `Stats: ${parts.join(" ")}`;
61
201
  }
62
202
  function resolveAnnounceOrigin(entry, requesterOrigin) {
203
+ const normalizedRequester = normalizeDeliveryContext(requesterOrigin);
204
+ const normalizedEntry = deliveryContextFromSession(entry);
205
+ if (normalizedRequester?.channel && !isDeliverableMessageChannel(normalizedRequester.channel)) {
206
+ // Ignore internal/non-deliverable channel hints (for example webchat)
207
+ // so a valid persisted route can still be used for outbound delivery.
208
+ return mergeDeliveryContext({
209
+ accountId: normalizedRequester.accountId,
210
+ threadId: normalizedRequester.threadId,
211
+ }, normalizedEntry);
212
+ }
63
213
  // requesterOrigin (captured at spawn time) reflects the channel the user is
64
214
  // actually on and must take priority over the session entry, which may carry
65
215
  // stale lastChannel / lastTo values from a previous channel interaction.
66
- return mergeDeliveryContext(requesterOrigin, deliveryContextFromSession(entry));
216
+ return mergeDeliveryContext(normalizedRequester, normalizedEntry);
67
217
  }
68
218
  async function sendAnnounce(item) {
219
+ const requesterDepth = getSubagentDepthFromSessionStore(item.sessionKey);
220
+ const requesterIsSubagent = requesterDepth >= 1;
69
221
  const origin = item.origin;
70
222
  const threadId = origin?.threadId != null && origin.threadId !== "" ? String(origin.threadId) : undefined;
223
+ // Share one announce identity across direct and queued delivery paths so
224
+ // gateway dedupe suppresses true retries without collapsing distinct events.
225
+ const idempotencyKey = buildAnnounceIdempotencyKey(resolveQueueAnnounceId({
226
+ announceId: item.announceId,
227
+ sessionKey: item.sessionKey,
228
+ enqueuedAt: item.enqueuedAt,
229
+ }));
71
230
  await callGateway({
72
231
  method: "agent",
73
232
  params: {
74
233
  sessionKey: item.sessionKey,
75
234
  message: item.prompt,
76
- channel: origin?.channel,
77
- accountId: origin?.accountId,
78
- to: origin?.to,
79
- threadId,
80
- deliver: true,
81
- idempotencyKey: crypto.randomUUID(),
235
+ channel: requesterIsSubagent ? undefined : origin?.channel,
236
+ accountId: requesterIsSubagent ? undefined : origin?.accountId,
237
+ to: requesterIsSubagent ? undefined : origin?.to,
238
+ threadId: requesterIsSubagent ? undefined : threadId,
239
+ deliver: !requesterIsSubagent,
240
+ idempotencyKey,
82
241
  },
83
- expectFinal: true,
84
- timeoutMs: 60_000,
242
+ timeoutMs: 15_000,
85
243
  });
86
244
  }
87
245
  function resolveRequesterStoreKey(cfg, requesterSessionKey) {
88
246
  const raw = requesterSessionKey.trim();
89
- if (!raw)
247
+ if (!raw) {
90
248
  return raw;
91
- if (raw === "global" || raw === "unknown")
249
+ }
250
+ if (raw === "global" || raw === "unknown") {
92
251
  return raw;
93
- if (raw.startsWith("agent:"))
252
+ }
253
+ if (raw.startsWith("agent:")) {
94
254
  return raw;
255
+ }
95
256
  const mainKey = normalizeMainKey(cfg.session?.mainKey);
96
257
  if (raw === "main" || raw === mainKey) {
97
258
  return resolveMainSessionKey(cfg);
@@ -112,8 +273,9 @@ async function maybeQueueSubagentAnnounce(params) {
112
273
  const { cfg, entry } = loadRequesterSessionEntry(params.requesterSessionKey);
113
274
  const canonicalKey = resolveRequesterStoreKey(cfg, params.requesterSessionKey);
114
275
  const sessionId = entry?.sessionId;
115
- if (!sessionId)
276
+ if (!sessionId) {
116
277
  return "none";
278
+ }
117
279
  const queueSettings = resolveQueueSettings({
118
280
  cfg,
119
281
  channel: entry?.channel ?? entry?.lastChannel,
@@ -123,8 +285,9 @@ async function maybeQueueSubagentAnnounce(params) {
123
285
  const shouldSteer = queueSettings.mode === "steer" || queueSettings.mode === "steer-backlog";
124
286
  if (shouldSteer) {
125
287
  const steered = queueEmbeddedPiMessage(sessionId, params.triggerMessage);
126
- if (steered)
288
+ if (steered) {
127
289
  return "steered";
290
+ }
128
291
  }
129
292
  const shouldFollowup = queueSettings.mode === "followup" ||
130
293
  queueSettings.mode === "collect" ||
@@ -135,6 +298,7 @@ async function maybeQueueSubagentAnnounce(params) {
135
298
  enqueueAnnounce({
136
299
  key: canonicalKey,
137
300
  item: {
301
+ announceId: params.announceId,
138
302
  prompt: params.triggerMessage,
139
303
  summaryLine: params.summaryLine,
140
304
  enqueuedAt: Date.now(),
@@ -148,47 +312,137 @@ async function maybeQueueSubagentAnnounce(params) {
148
312
  }
149
313
  return "none";
150
314
  }
151
- async function buildSubagentStatsLine(params) {
315
+ function queueOutcomeToDeliveryResult(outcome) {
316
+ if (outcome === "steered") {
317
+ return {
318
+ delivered: true,
319
+ path: "steered",
320
+ };
321
+ }
322
+ if (outcome === "queued") {
323
+ return {
324
+ delivered: true,
325
+ path: "queued",
326
+ };
327
+ }
328
+ return {
329
+ delivered: false,
330
+ path: "none",
331
+ };
332
+ }
333
+ async function sendSubagentAnnounceDirectly(params) {
152
334
  const cfg = loadConfig();
153
- const { entry, storePath } = await waitForSessionUsage({
154
- sessionKey: params.sessionKey,
335
+ const canonicalRequesterSessionKey = resolveRequesterStoreKey(cfg, params.targetRequesterSessionKey);
336
+ try {
337
+ const completionDirectOrigin = normalizeDeliveryContext(params.completionDirectOrigin);
338
+ const completionChannelRaw = typeof completionDirectOrigin?.channel === "string"
339
+ ? completionDirectOrigin.channel.trim()
340
+ : "";
341
+ const completionChannel = completionChannelRaw && isDeliverableMessageChannel(completionChannelRaw)
342
+ ? completionChannelRaw
343
+ : "";
344
+ const completionTo = typeof completionDirectOrigin?.to === "string" ? completionDirectOrigin.to.trim() : "";
345
+ const hasCompletionDirectTarget = !params.requesterIsSubagent && Boolean(completionChannel) && Boolean(completionTo);
346
+ if (params.expectsCompletionMessage &&
347
+ hasCompletionDirectTarget &&
348
+ params.completionMessage?.trim()) {
349
+ const completionThreadId = completionDirectOrigin?.threadId != null && completionDirectOrigin.threadId !== ""
350
+ ? String(completionDirectOrigin.threadId)
351
+ : undefined;
352
+ await callGateway({
353
+ method: "send",
354
+ params: {
355
+ channel: completionChannel,
356
+ to: completionTo,
357
+ accountId: completionDirectOrigin?.accountId,
358
+ threadId: completionThreadId,
359
+ sessionKey: canonicalRequesterSessionKey,
360
+ message: params.completionMessage,
361
+ idempotencyKey: params.directIdempotencyKey,
362
+ },
363
+ timeoutMs: 15_000,
364
+ });
365
+ return {
366
+ delivered: true,
367
+ path: "direct",
368
+ };
369
+ }
370
+ const directOrigin = normalizeDeliveryContext(params.directOrigin);
371
+ const threadId = directOrigin?.threadId != null && directOrigin.threadId !== ""
372
+ ? String(directOrigin.threadId)
373
+ : undefined;
374
+ await callGateway({
375
+ method: "agent",
376
+ params: {
377
+ sessionKey: canonicalRequesterSessionKey,
378
+ message: params.triggerMessage,
379
+ deliver: !params.requesterIsSubagent,
380
+ channel: params.requesterIsSubagent ? undefined : directOrigin?.channel,
381
+ accountId: params.requesterIsSubagent ? undefined : directOrigin?.accountId,
382
+ to: params.requesterIsSubagent ? undefined : directOrigin?.to,
383
+ threadId: params.requesterIsSubagent ? undefined : threadId,
384
+ idempotencyKey: params.directIdempotencyKey,
385
+ },
386
+ expectFinal: true,
387
+ timeoutMs: 15_000,
388
+ });
389
+ return {
390
+ delivered: true,
391
+ path: "direct",
392
+ };
393
+ }
394
+ catch (err) {
395
+ return {
396
+ delivered: false,
397
+ path: "direct",
398
+ error: summarizeDeliveryError(err),
399
+ };
400
+ }
401
+ }
402
+ async function deliverSubagentAnnouncement(params) {
403
+ // Non-completion mode mirrors historical behavior: try queued/steered delivery first,
404
+ // then (only if not queued) attempt direct delivery.
405
+ if (!params.expectsCompletionMessage) {
406
+ const queueOutcome = await maybeQueueSubagentAnnounce({
407
+ requesterSessionKey: params.requesterSessionKey,
408
+ announceId: params.announceId,
409
+ triggerMessage: params.triggerMessage,
410
+ summaryLine: params.summaryLine,
411
+ requesterOrigin: params.requesterOrigin,
412
+ });
413
+ const queued = queueOutcomeToDeliveryResult(queueOutcome);
414
+ if (queued.delivered) {
415
+ return queued;
416
+ }
417
+ }
418
+ // Completion-mode uses direct send first so manual spawns can return immediately
419
+ // in the common ready-to-deliver case.
420
+ const direct = await sendSubagentAnnounceDirectly({
421
+ targetRequesterSessionKey: params.targetRequesterSessionKey,
422
+ triggerMessage: params.triggerMessage,
423
+ completionMessage: params.completionMessage,
424
+ directIdempotencyKey: params.directIdempotencyKey,
425
+ completionDirectOrigin: params.completionDirectOrigin,
426
+ directOrigin: params.directOrigin,
427
+ requesterIsSubagent: params.requesterIsSubagent,
428
+ expectsCompletionMessage: params.expectsCompletionMessage,
155
429
  });
156
- const sessionId = entry?.sessionId;
157
- const transcriptPath = sessionId && storePath ? path.join(path.dirname(storePath), `${sessionId}.jsonl`) : undefined;
158
- const input = entry?.inputTokens;
159
- const output = entry?.outputTokens;
160
- const total = entry?.totalTokens ??
161
- (typeof input === "number" && typeof output === "number" ? input + output : undefined);
162
- const runtimeMs = typeof params.startedAt === "number" && typeof params.endedAt === "number"
163
- ? Math.max(0, params.endedAt - params.startedAt)
164
- : undefined;
165
- const provider = entry?.modelProvider;
166
- const model = entry?.model;
167
- const costConfig = resolveModelCost({ provider, model, config: cfg });
168
- const cost = costConfig && typeof input === "number" && typeof output === "number"
169
- ? (input * costConfig.input + output * costConfig.output) / 1_000_000
170
- : undefined;
171
- const parts = [];
172
- const runtime = formatDurationCompact(runtimeMs);
173
- parts.push(`runtime ${runtime ?? "n/a"}`);
174
- if (typeof total === "number") {
175
- const inputText = typeof input === "number" ? formatTokenCount(input) : "n/a";
176
- const outputText = typeof output === "number" ? formatTokenCount(output) : "n/a";
177
- const totalText = formatTokenCount(total);
178
- parts.push(`tokens ${totalText} (in ${inputText} / out ${outputText})`);
179
- }
180
- else {
181
- parts.push("tokens n/a");
182
- }
183
- const costText = formatUsd(cost);
184
- if (costText)
185
- parts.push(`est ${costText}`);
186
- parts.push(`sessionKey ${params.sessionKey}`);
187
- if (sessionId)
188
- parts.push(`sessionId ${sessionId}`);
189
- if (transcriptPath)
190
- parts.push(`transcript ${transcriptPath}`);
191
- return `Stats: ${parts.join(" \u2022 ")}`;
430
+ if (direct.delivered || !params.expectsCompletionMessage) {
431
+ return direct;
432
+ }
433
+ // If completion path failed direct delivery, try queueing as a fallback so the
434
+ // report can still be delivered once the requester session is idle.
435
+ const queueOutcome = await maybeQueueSubagentAnnounce({
436
+ requesterSessionKey: params.requesterSessionKey,
437
+ announceId: params.announceId,
438
+ triggerMessage: params.triggerMessage,
439
+ summaryLine: params.summaryLine,
440
+ requesterOrigin: params.requesterOrigin,
441
+ });
442
+ if (queueOutcome === "steered" || queueOutcome === "queued") {
443
+ return queueOutcomeToDeliveryResult(queueOutcome);
444
+ }
445
+ return direct;
192
446
  }
193
447
  function loadSessionEntryByKey(sessionKey) {
194
448
  const cfg = loadConfig();
@@ -197,70 +451,84 @@ function loadSessionEntryByKey(sessionKey) {
197
451
  const store = loadSessionStore(storePath);
198
452
  return store[sessionKey];
199
453
  }
200
- async function readLatestAssistantReplyWithRetry(params) {
201
- let reply = params.initialReply?.trim() ? params.initialReply : undefined;
202
- if (reply) {
203
- return reply;
204
- }
205
- const deadline = Date.now() + Math.max(0, Math.min(params.maxWaitMs, 15_000));
206
- while (Date.now() < deadline) {
207
- await new Promise((resolve) => setTimeout(resolve, 300));
208
- const latest = await readLatestAssistantReply({ sessionKey: params.sessionKey });
209
- if (latest?.trim()) {
210
- return latest;
211
- }
212
- }
213
- return reply;
214
- }
215
454
  export function buildSubagentSystemPrompt(params) {
216
455
  const taskText = typeof params.task === "string" && params.task.trim()
217
456
  ? params.task.replace(/\s+/g, " ").trim()
218
457
  : "{{TASK_DESCRIPTION}}";
458
+ const childDepth = typeof params.childDepth === "number" ? params.childDepth : 1;
459
+ const maxSpawnDepth = typeof params.maxSpawnDepth === "number" ? params.maxSpawnDepth : 1;
460
+ const canSpawn = childDepth < maxSpawnDepth;
461
+ const parentLabel = childDepth >= 2 ? "parent orchestrator" : "main agent";
219
462
  const lines = [
220
463
  "# Subagent Context",
221
464
  "",
222
- "You are a **subagent** spawned by the main agent for a specific task.",
465
+ `You are a **subagent** spawned by the ${parentLabel} for a specific task.`,
223
466
  "",
224
467
  "## Your Role",
225
468
  `- You were created to handle: ${taskText}`,
226
469
  "- Complete this task. That's your entire purpose.",
227
- "- You are NOT the main agent. Don't try to be.",
470
+ `- You are NOT the ${parentLabel}. Don't try to be.`,
228
471
  "",
229
472
  "## Rules",
230
473
  "1. **Stay focused** - Do your assigned task, nothing else",
231
- "2. **Complete the task** - Your final message will be automatically reported to the main agent",
474
+ `2. **Complete the task** - Your final message will be automatically reported to the ${parentLabel}`,
232
475
  "3. **Don't initiate** - No heartbeats, no proactive actions, no side quests",
233
476
  "4. **Be ephemeral** - You may be terminated after task completion. That's fine.",
477
+ "5. **Trust push-based completion** - Descendant results are auto-announced back to you; do not busy-poll for status.",
478
+ "6. **Recover from compacted/truncated tool output** - If you see `[compacted: tool output removed to free context]` or `[truncated: output exceeded context limit]`, assume prior output was reduced. Re-read only what you need using smaller chunks (`read` with offset/limit, or targeted `rg`/`head`/`tail`) instead of full-file `cat`.",
234
479
  "",
235
480
  "## Output Format",
236
481
  "When complete, your final response should include:",
237
- "- What you accomplished or found",
238
- "- Any relevant details the main agent should know",
482
+ `- What you accomplished or found`,
483
+ `- Any relevant details the ${parentLabel} should know`,
239
484
  "- Keep it concise but informative",
240
485
  "",
241
486
  "## What You DON'T Do",
242
- "- NO user conversations (that's main agent's job)",
487
+ `- NO user conversations (that's ${parentLabel}'s job)`,
243
488
  "- NO external messages (email, tweets, etc.) unless explicitly tasked with a specific recipient/channel",
244
489
  "- NO cron jobs or persistent state",
245
- "- NO pretending to be the main agent",
246
- "- Only use the `message` tool when explicitly instructed to contact a specific external recipient; otherwise return plain text and let the main agent deliver it",
490
+ `- NO pretending to be the ${parentLabel}`,
491
+ `- Only use the \`message\` tool when explicitly instructed to contact a specific external recipient; otherwise return plain text and let the ${parentLabel} deliver it`,
247
492
  "",
248
- "## Session Context",
493
+ ];
494
+ if (canSpawn) {
495
+ lines.push("## Sub-Agent Spawning", "You CAN spawn your own sub-agents for parallel or complex work using `sessions_spawn`.", "Use the `subagents` tool to steer, kill, or do an on-demand status check for your spawned sub-agents.", "Your sub-agents will announce their results back to you automatically (not to the main agent).", "Default workflow: spawn work, continue orchestrating, and wait for auto-announced completions.", "Do NOT repeatedly poll `subagents list` in a loop unless you are actively debugging or intervening.", "Coordinate their work and synthesize results before reporting back.", "");
496
+ }
497
+ else if (childDepth >= 2) {
498
+ lines.push("## Sub-Agent Spawning", "You are a leaf worker and CANNOT spawn further sub-agents. Focus on your assigned task.", "");
499
+ }
500
+ lines.push("## Session Context", ...[
249
501
  params.label ? `- Label: ${params.label}` : undefined,
250
- params.requesterSessionKey ? `- Requester session: ${params.requesterSessionKey}.` : undefined,
502
+ params.requesterSessionKey
503
+ ? `- Requester session: ${params.requesterSessionKey}.`
504
+ : undefined,
251
505
  params.requesterOrigin?.channel
252
506
  ? `- Requester channel: ${params.requesterOrigin.channel}.`
253
507
  : undefined,
254
508
  `- Your session: ${params.childSessionKey}.`,
255
- "",
256
- ].filter((line) => line !== undefined);
509
+ ].filter((line) => line !== undefined), "");
257
510
  return lines.join("\n");
258
511
  }
512
+ function buildAnnounceReplyInstruction(params) {
513
+ if (params.expectsCompletionMessage) {
514
+ return `A completed ${params.announceType} is ready for user delivery. Convert the result above into your normal assistant voice and send that user-facing update now. Keep this internal context private (don't mention system/log/stats/session details or announce type).`;
515
+ }
516
+ if (params.remainingActiveSubagentRuns > 0) {
517
+ const activeRunsLabel = params.remainingActiveSubagentRuns === 1 ? "run" : "runs";
518
+ return `There are still ${params.remainingActiveSubagentRuns} active subagent ${activeRunsLabel} for this session. If they are part of the same workflow, wait for the remaining results before sending a user update. If they are unrelated, respond normally using only the result above.`;
519
+ }
520
+ if (params.requesterIsSubagent) {
521
+ return `Convert this completion into a concise internal orchestration update for your parent agent in your own words. Keep this internal context private (don't mention system/log/stats/session details or announce type). If this result is duplicate or no update is needed, reply ONLY: ${SILENT_REPLY_TOKEN}.`;
522
+ }
523
+ return `A completed ${params.announceType} is ready for user delivery. Convert the result above into your normal assistant voice and send that user-facing update now. Keep this internal context private (don't mention system/log/stats/session details or announce type), and do not copy the system message verbatim. Reply ONLY: ${SILENT_REPLY_TOKEN} if this exact result was already delivered to the user in this same turn.`;
524
+ }
259
525
  export async function runSubagentAnnounceFlow(params) {
260
526
  let didAnnounce = false;
527
+ const expectsCompletionMessage = params.expectsCompletionMessage === true;
261
528
  let shouldDeleteChildSession = params.cleanup === "delete";
262
529
  try {
263
- const requesterOrigin = normalizeDeliveryContext(params.requesterOrigin);
530
+ let targetRequesterSessionKey = params.requesterSessionKey;
531
+ let targetRequesterOrigin = normalizeDeliveryContext(params.requesterOrigin);
264
532
  const childSessionId = (() => {
265
533
  const entry = loadSessionEntryByKey(params.childSessionKey);
266
534
  return typeof entry?.sessionId === "string" && entry.sessionId.trim()
@@ -272,7 +540,7 @@ export async function runSubagentAnnounceFlow(params) {
272
540
  let outcome = params.outcome;
273
541
  // Lifecycle "end" can arrive before auto-compaction retries finish. If the
274
542
  // subagent is still active, wait for the embedded run to fully settle.
275
- if (childSessionId && isEmbeddedPiRunActive(childSessionId)) {
543
+ if (!expectsCompletionMessage && childSessionId && isEmbeddedPiRunActive(childSessionId)) {
276
544
  const settled = await waitForEmbeddedPiRunEnd(childSessionId, settleTimeoutMs);
277
545
  if (!settled && isEmbeddedPiRunActive(childSessionId)) {
278
546
  // The child run is still active (e.g., compaction retry still in progress).
@@ -313,19 +581,21 @@ export async function runSubagentAnnounceFlow(params) {
313
581
  outcome = { status: "timeout" };
314
582
  }
315
583
  }
316
- reply = await readLatestAssistantReply({ sessionKey: params.childSessionKey });
584
+ reply = await readLatestSubagentOutput(params.childSessionKey);
317
585
  }
318
586
  if (!reply) {
319
- reply = await readLatestAssistantReply({ sessionKey: params.childSessionKey });
587
+ reply = await readLatestSubagentOutput(params.childSessionKey);
320
588
  }
321
589
  if (!reply?.trim()) {
322
- reply = await readLatestAssistantReplyWithRetry({
590
+ reply = await readLatestSubagentOutputWithRetry({
323
591
  sessionKey: params.childSessionKey,
324
- initialReply: reply,
325
592
  maxWaitMs: params.timeoutMs,
326
593
  });
327
594
  }
328
- if (!reply?.trim() && childSessionId && isEmbeddedPiRunActive(childSessionId)) {
595
+ if (!expectsCompletionMessage &&
596
+ !reply?.trim() &&
597
+ childSessionId &&
598
+ isEmbeddedPiRunActive(childSessionId)) {
329
599
  // Avoid announcing "(no output)" while the child run is still producing output.
330
600
  shouldDeleteChildSession = false;
331
601
  return false;
@@ -333,12 +603,20 @@ export async function runSubagentAnnounceFlow(params) {
333
603
  if (!outcome) {
334
604
  outcome = { status: "unknown" };
335
605
  }
336
- // Build stats
337
- const statsLine = await buildSubagentStatsLine({
338
- sessionKey: params.childSessionKey,
339
- startedAt: params.startedAt,
340
- endedAt: params.endedAt,
341
- });
606
+ let activeChildDescendantRuns = 0;
607
+ try {
608
+ const { countActiveDescendantRuns } = await import("./subagent-registry.js");
609
+ activeChildDescendantRuns = Math.max(0, countActiveDescendantRuns(params.childSessionKey));
610
+ }
611
+ catch {
612
+ // Best-effort only; fall back to direct announce behavior when unavailable.
613
+ }
614
+ if (!expectsCompletionMessage && activeChildDescendantRuns > 0) {
615
+ // The finished run still has active descendant subagents. Defer announcing
616
+ // this run until descendants settle so we avoid posting in-progress updates.
617
+ shouldDeleteChildSession = false;
618
+ return false;
619
+ }
342
620
  // Build status label
343
621
  const statusLabel = outcome.status === "ok"
344
622
  ? "completed successfully"
@@ -350,56 +628,112 @@ export async function runSubagentAnnounceFlow(params) {
350
628
  // Build instructional message for main agent
351
629
  const announceType = params.announceType ?? "subagent task";
352
630
  const taskLabel = params.label || params.task || "task";
353
- const triggerMessage = [
354
- `A ${announceType} "${taskLabel}" just ${statusLabel}.`,
631
+ const subagentName = resolveAgentIdFromSessionKey(params.childSessionKey);
632
+ const announceSessionId = childSessionId || "unknown";
633
+ const findings = reply || "(no output)";
634
+ let completionMessage = "";
635
+ let triggerMessage = "";
636
+ let requesterDepth = getSubagentDepthFromSessionStore(targetRequesterSessionKey);
637
+ let requesterIsSubagent = !expectsCompletionMessage && requesterDepth >= 1;
638
+ // If the requester subagent has already finished, bubble the announce to its
639
+ // requester (typically main) so descendant completion is not silently lost.
640
+ // BUT: only fallback if the parent SESSION is deleted, not just if the current
641
+ // run ended. A parent waiting for child results has no active run but should
642
+ // still receive the announce — injecting will start a new agent turn.
643
+ if (requesterIsSubagent) {
644
+ const { isSubagentSessionRunActive, resolveRequesterForChildSession } = await import("./subagent-registry.js");
645
+ if (!isSubagentSessionRunActive(targetRequesterSessionKey)) {
646
+ // Parent run has ended. Check if parent SESSION still exists.
647
+ // If it does, the parent may be waiting for child results — inject there.
648
+ const parentSessionEntry = loadSessionEntryByKey(targetRequesterSessionKey);
649
+ const parentSessionAlive = parentSessionEntry &&
650
+ typeof parentSessionEntry.sessionId === "string" &&
651
+ parentSessionEntry.sessionId.trim();
652
+ if (!parentSessionAlive) {
653
+ // Parent session is truly gone — fallback to grandparent
654
+ const fallback = resolveRequesterForChildSession(targetRequesterSessionKey);
655
+ if (!fallback?.requesterSessionKey) {
656
+ // Without a requester fallback we cannot safely deliver this nested
657
+ // completion. Keep cleanup retryable so a later registry restore can
658
+ // recover and re-announce instead of silently dropping the result.
659
+ shouldDeleteChildSession = false;
660
+ return false;
661
+ }
662
+ targetRequesterSessionKey = fallback.requesterSessionKey;
663
+ targetRequesterOrigin =
664
+ normalizeDeliveryContext(fallback.requesterOrigin) ?? targetRequesterOrigin;
665
+ requesterDepth = getSubagentDepthFromSessionStore(targetRequesterSessionKey);
666
+ requesterIsSubagent = requesterDepth >= 1;
667
+ }
668
+ // If parent session is alive (just has no active run), continue with parent
669
+ // as target. Injecting the announce will start a new agent turn for processing.
670
+ }
671
+ }
672
+ let remainingActiveSubagentRuns = 0;
673
+ try {
674
+ const { countActiveDescendantRuns } = await import("./subagent-registry.js");
675
+ remainingActiveSubagentRuns = Math.max(0, countActiveDescendantRuns(targetRequesterSessionKey));
676
+ }
677
+ catch {
678
+ // Best-effort only; fall back to default announce instructions when unavailable.
679
+ }
680
+ const replyInstruction = buildAnnounceReplyInstruction({
681
+ remainingActiveSubagentRuns,
682
+ requesterIsSubagent,
683
+ announceType,
684
+ expectsCompletionMessage,
685
+ });
686
+ const statsLine = await buildCompactAnnounceStatsLine({
687
+ sessionKey: params.childSessionKey,
688
+ startedAt: params.startedAt,
689
+ endedAt: params.endedAt,
690
+ });
691
+ completionMessage = buildCompletionDeliveryMessage({
692
+ findings,
693
+ subagentName,
694
+ });
695
+ const internalSummaryMessage = [
696
+ `[System Message] [sessionId: ${announceSessionId}] A ${announceType} "${taskLabel}" just ${statusLabel}.`,
355
697
  "",
356
- "Findings:",
357
- reply || "(no output)",
698
+ "Result:",
699
+ findings,
358
700
  "",
359
701
  statsLine,
360
- "",
361
- "Summarize this naturally for the user. Keep it brief (1-2 sentences). Flow it into the conversation naturally.",
362
- `Do not mention technical details like tokens, stats, or that this was a ${announceType}.`,
363
- "You can respond with NO_REPLY if no announcement is needed (e.g., internal task with no user-facing result).",
364
702
  ].join("\n");
365
- const queued = await maybeQueueSubagentAnnounce({
366
- requesterSessionKey: params.requesterSessionKey,
703
+ triggerMessage = [internalSummaryMessage, "", replyInstruction].join("\n");
704
+ const announceId = buildAnnounceIdFromChildRun({
705
+ childSessionKey: params.childSessionKey,
706
+ childRunId: params.childRunId,
707
+ });
708
+ // Send to the requester session. For nested subagents this is an internal
709
+ // follow-up injection (deliver=false) so the orchestrator receives it.
710
+ let directOrigin = targetRequesterOrigin;
711
+ if (!requesterIsSubagent) {
712
+ const { entry } = loadRequesterSessionEntry(targetRequesterSessionKey);
713
+ directOrigin = resolveAnnounceOrigin(entry, targetRequesterOrigin);
714
+ }
715
+ // Use a deterministic idempotency key so the gateway dedup cache
716
+ // catches duplicates if this announce is also queued by the gateway-
717
+ // level message queue while the main session is busy (#17122).
718
+ const directIdempotencyKey = buildAnnounceIdempotencyKey(announceId);
719
+ const delivery = await deliverSubagentAnnouncement({
720
+ requesterSessionKey: targetRequesterSessionKey,
721
+ announceId,
367
722
  triggerMessage,
723
+ completionMessage,
368
724
  summaryLine: taskLabel,
369
- requesterOrigin,
725
+ requesterOrigin: targetRequesterOrigin,
726
+ completionDirectOrigin: targetRequesterOrigin,
727
+ directOrigin,
728
+ targetRequesterSessionKey,
729
+ requesterIsSubagent,
730
+ expectsCompletionMessage: expectsCompletionMessage,
731
+ directIdempotencyKey,
370
732
  });
371
- if (queued === "steered") {
372
- didAnnounce = true;
373
- return true;
733
+ didAnnounce = delivery.delivered;
734
+ if (!delivery.delivered && delivery.path === "direct" && delivery.error) {
735
+ defaultRuntime.error?.(`Subagent completion direct announce failed for run ${params.childRunId}: ${delivery.error}`);
374
736
  }
375
- if (queued === "queued") {
376
- didAnnounce = true;
377
- return true;
378
- }
379
- // Send to main agent - it will respond in its own voice
380
- let directOrigin = requesterOrigin;
381
- if (!directOrigin) {
382
- const { entry } = loadRequesterSessionEntry(params.requesterSessionKey);
383
- directOrigin = deliveryContextFromSession(entry);
384
- }
385
- await callGateway({
386
- method: "agent",
387
- params: {
388
- sessionKey: params.requesterSessionKey,
389
- message: triggerMessage,
390
- deliver: true,
391
- channel: directOrigin?.channel,
392
- accountId: directOrigin?.accountId,
393
- to: directOrigin?.to,
394
- threadId: directOrigin?.threadId != null && directOrigin.threadId !== ""
395
- ? String(directOrigin.threadId)
396
- : undefined,
397
- idempotencyKey: crypto.randomUUID(),
398
- },
399
- expectFinal: true,
400
- timeoutMs: 60_000,
401
- });
402
- didAnnounce = true;
403
737
  }
404
738
  catch (err) {
405
739
  defaultRuntime.error?.(`Subagent announce failed: ${String(err)}`);