@poolzin/pool-bot 2026.2.20 → 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 (388) hide show
  1. package/CHANGELOG.md +25 -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-auth.js +12 -0
  14. package/dist/agents/model-catalog.js +40 -9
  15. package/dist/agents/model-fallback.js +24 -0
  16. package/dist/agents/model-forward-compat.js +60 -23
  17. package/dist/agents/model-selection.js +134 -41
  18. package/dist/agents/pi-auth-json.js +2 -2
  19. package/dist/agents/pi-embedded-helpers/bootstrap.js +65 -15
  20. package/dist/agents/pi-embedded-helpers/errors.js +140 -15
  21. package/dist/agents/pi-embedded-helpers/images.js +22 -12
  22. package/dist/agents/pi-embedded-helpers.js +2 -2
  23. package/dist/agents/pi-embedded-runner/abort.js +10 -3
  24. package/dist/agents/pi-embedded-runner/compact.js +230 -32
  25. package/dist/agents/pi-embedded-runner/extra-params.js +203 -12
  26. package/dist/agents/pi-embedded-runner/google.js +109 -19
  27. package/dist/agents/pi-embedded-runner/history.js +35 -17
  28. package/dist/agents/pi-embedded-runner/run/attempt.js +386 -80
  29. package/dist/agents/pi-embedded-runner/run/images.js +81 -55
  30. package/dist/agents/pi-embedded-runner/run/payloads.js +89 -39
  31. package/dist/agents/pi-embedded-runner/run.js +193 -25
  32. package/dist/agents/pi-embedded-runner/run.overflow-compaction.mocks.shared.js +2 -2
  33. package/dist/agents/pi-embedded-runner/runs.js +17 -8
  34. package/dist/agents/pi-embedded-runner/tool-result-context-guard.js +262 -0
  35. package/dist/agents/pi-embedded-runner.js +1 -1
  36. package/dist/agents/pi-embedded-subscribe.handlers.tools.js +180 -10
  37. package/dist/agents/pi-embedded-subscribe.js +37 -0
  38. package/dist/agents/pi-embedded-subscribe.tools.js +127 -30
  39. package/dist/agents/pi-model-discovery.js +9 -2
  40. package/dist/agents/pi-tool-definition-adapter.js +60 -8
  41. package/dist/agents/pi-tools.before-tool-call.js +1 -1
  42. package/dist/agents/pi-tools.js +113 -94
  43. package/dist/agents/pi-tools.read.js +337 -38
  44. package/dist/agents/poolbot-tools.js +14 -5
  45. package/dist/agents/provider/config-loader.js +76 -0
  46. package/dist/agents/provider/index.js +15 -0
  47. package/dist/agents/provider/integration.js +136 -0
  48. package/dist/agents/provider/models-dev.js +129 -0
  49. package/dist/agents/provider/rate-limits.js +458 -0
  50. package/dist/agents/provider/request-monitor.js +449 -0
  51. package/dist/agents/provider/session-binding.js +376 -0
  52. package/dist/agents/provider/token-pool.js +541 -0
  53. package/dist/agents/sandbox/docker.js +10 -5
  54. package/dist/agents/sandbox/registry.js +96 -46
  55. package/dist/agents/sandbox/sanitize-env-vars.js +82 -0
  56. package/dist/agents/sandbox-paths.js +43 -10
  57. package/dist/agents/session-tool-result-guard-wrapper.js +23 -11
  58. package/dist/agents/session-tool-result-guard.js +39 -39
  59. package/dist/agents/session-transcript-repair.js +36 -33
  60. package/dist/agents/session-write-lock.js +62 -44
  61. package/dist/agents/skills/frontmatter.js +49 -88
  62. package/dist/agents/skills/workspace.js +335 -28
  63. package/dist/agents/subagent-announce.js +508 -174
  64. package/dist/agents/subagent-registry.js +45 -4
  65. package/dist/agents/subagent-spawn.js +16 -33
  66. package/dist/agents/system-prompt-report.js +27 -10
  67. package/dist/agents/system-prompt.js +26 -32
  68. package/dist/agents/tool-call-id.js +69 -17
  69. package/dist/agents/tool-display-common.js +1 -1
  70. package/dist/agents/tool-images.js +64 -31
  71. package/dist/agents/tools/canvas-tool.js +17 -11
  72. package/dist/agents/tools/common.js +37 -19
  73. package/dist/agents/tools/cron-tool.js +40 -38
  74. package/dist/agents/tools/gateway.js +70 -2
  75. package/dist/agents/tools/message-tool.js +181 -40
  76. package/dist/agents/tools/nodes-tool.js +128 -36
  77. package/dist/agents/tools/nodes-utils.js +12 -38
  78. package/dist/agents/tools/session-status-tool.js +24 -71
  79. package/dist/agents/tools/sessions-helpers.js +38 -210
  80. package/dist/agents/tools/sessions-spawn-tool.js +28 -198
  81. package/dist/agents/tools/telegram-actions.js +58 -7
  82. package/dist/agents/tools/web-fetch-utils.js +112 -7
  83. package/dist/agents/tools/web-fetch.js +279 -175
  84. package/dist/agents/tools/web-shared.js +71 -8
  85. package/dist/agents/usage.js +25 -16
  86. package/dist/auto-reply/commands-registry.data.js +85 -11
  87. package/dist/auto-reply/dispatch.js +40 -21
  88. package/dist/auto-reply/reply/abort.js +102 -33
  89. package/dist/auto-reply/reply/commands-core.js +82 -33
  90. package/dist/auto-reply/reply/commands-export-session.js +1 -1
  91. package/dist/auto-reply/reply/commands-info.js +41 -12
  92. package/dist/auto-reply/reply/commands-subagents.js +352 -100
  93. package/dist/auto-reply/reply/commands-system-prompt.js +2 -2
  94. package/dist/auto-reply/reply/dispatch-from-config.js +100 -29
  95. package/dist/auto-reply/reply/elevated-unavailable.js +1 -1
  96. package/dist/auto-reply/reply/inbound-meta.js +12 -1
  97. package/dist/auto-reply/reply/mentions.js +18 -11
  98. package/dist/auto-reply/reply/normalize-reply.js +17 -8
  99. package/dist/auto-reply/reply/reply-dispatcher.js +62 -10
  100. package/dist/auto-reply/reply/session.js +102 -21
  101. package/dist/auto-reply/reply/streaming-directives.js +16 -5
  102. package/dist/auto-reply/status.js +73 -50
  103. package/dist/browser/extension-relay.js +3 -3
  104. package/dist/browser/http-auth.js +1 -1
  105. package/dist/browser/paths.js +2 -2
  106. package/dist/build-info.json +3 -3
  107. package/dist/channels/allowlist-match.js +20 -0
  108. package/dist/channels/allowlists/resolve-utils.js +65 -2
  109. package/dist/channels/chat-type.js +8 -4
  110. package/dist/channels/dock.js +127 -35
  111. package/dist/channels/draft-stream-loop.js +6 -2
  112. package/dist/channels/plugins/actions/telegram.js +42 -18
  113. package/dist/channels/plugins/allowlist-match.js +1 -1
  114. package/dist/channels/plugins/group-mentions.js +51 -41
  115. package/dist/channels/plugins/message-action-names.js +2 -0
  116. package/dist/channels/plugins/message-actions.js +24 -5
  117. package/dist/channels/plugins/normalize/discord.js +26 -4
  118. package/dist/channels/plugins/normalize/signal.js +35 -22
  119. package/dist/channels/plugins/onboarding/helpers.js +8 -26
  120. package/dist/channels/plugins/outbound/imessage.js +15 -14
  121. package/dist/channels/registry.js +20 -7
  122. package/dist/cli/acp-cli.js +7 -5
  123. package/dist/cli/browser-cli-extension.js +25 -12
  124. package/dist/cli/browser-cli-state.cookies-storage.js +25 -6
  125. package/dist/cli/browser-cli-state.js +101 -145
  126. package/dist/cli/command-options.js +28 -0
  127. package/dist/cli/completion-cli.js +6 -6
  128. package/dist/cli/cron-cli/register.cron-add.js +25 -1
  129. package/dist/cli/cron-cli/register.cron-edit.js +44 -0
  130. package/dist/cli/cron-cli/shared.js +7 -1
  131. package/dist/cli/daemon-cli/lifecycle-core.js +23 -21
  132. package/dist/cli/daemon-cli/lifecycle.js +23 -247
  133. package/dist/cli/daemon-cli/register-service-commands.js +25 -4
  134. package/dist/cli/daemon-cli.js +1 -0
  135. package/dist/cli/devices-cli.js +33 -20
  136. package/dist/cli/gateway-cli/register.js +37 -105
  137. package/dist/cli/gateway-cli/run.js +49 -11
  138. package/dist/cli/nodes-camera.js +59 -4
  139. package/dist/cli/nodes-cli/register.camera.js +27 -24
  140. package/dist/cli/nodes-cli/rpc.js +21 -38
  141. package/dist/cli/qr-cli.js +2 -2
  142. package/dist/cli/skills-cli.format.js +2 -2
  143. package/dist/cli/update-cli/progress.js +2 -2
  144. package/dist/cli/update-cli/restart-helper.js +28 -7
  145. package/dist/cli/update-cli/shared.js +7 -7
  146. package/dist/cli/update-cli/status.js +1 -1
  147. package/dist/cli/update-cli/update-command.js +14 -8
  148. package/dist/cli/update-cli/wizard.js +2 -2
  149. package/dist/cli/update-cli.js +21 -1027
  150. package/dist/commands/auth-choice.apply.anthropic.js +10 -2
  151. package/dist/commands/channels/add-mutators.js +3 -35
  152. package/dist/commands/channels/add.js +39 -51
  153. package/dist/commands/config-validation.js +1 -1
  154. package/dist/commands/configure.gateway-auth.js +52 -15
  155. package/dist/commands/configure.gateway.js +84 -40
  156. package/dist/commands/doctor-completion.js +3 -3
  157. package/dist/commands/doctor-config-flow.js +536 -16
  158. package/dist/commands/doctor-gateway-services.js +103 -79
  159. package/dist/commands/doctor-memory-search.js +9 -9
  160. package/dist/commands/doctor-platform-notes.js +57 -30
  161. package/dist/commands/doctor-prompter.js +26 -15
  162. package/dist/commands/doctor-session-locks.js +1 -1
  163. package/dist/commands/doctor.js +21 -9
  164. package/dist/commands/model-picker.js +120 -95
  165. package/dist/commands/models/set.js +2 -21
  166. package/dist/commands/models/shared.js +65 -37
  167. package/dist/commands/onboard-helpers.js +81 -39
  168. package/dist/commands/openai-codex-oauth.js +1 -1
  169. package/dist/commands/sessions.js +52 -53
  170. package/dist/commands/status.summary.js +52 -34
  171. package/dist/commands/test-wizard-helpers.js +2 -2
  172. package/dist/config/defaults.js +79 -42
  173. package/dist/config/group-policy.js +50 -18
  174. package/dist/config/includes.js +37 -10
  175. package/dist/config/schema.help.js +5 -4
  176. package/dist/config/schema.hints.js +2 -2
  177. package/dist/config/schema.labels.js +1 -0
  178. package/dist/config/sessions/group.js +12 -11
  179. package/dist/config/sessions/paths.js +137 -11
  180. package/dist/config/sessions/store.js +185 -65
  181. package/dist/config/sessions/types.js +15 -1
  182. package/dist/config/sessions.js +1 -0
  183. package/dist/config/telegram-custom-commands.js +3 -2
  184. package/dist/config/types.js +2 -0
  185. package/dist/config/zod-schema.agent-defaults.js +6 -27
  186. package/dist/config/zod-schema.agent-runtime.js +171 -79
  187. package/dist/config/zod-schema.providers-core.js +138 -65
  188. package/dist/config/zod-schema.session.js +49 -22
  189. package/dist/control-ui/assets/index-HRr1grwl.js.map +1 -1
  190. package/dist/cron/isolated-agent/run.js +224 -57
  191. package/dist/cron/normalize.js +48 -45
  192. package/dist/cron/run-log.js +14 -0
  193. package/dist/cron/service/jobs.js +190 -28
  194. package/dist/cron/service/normalize.js +29 -11
  195. package/dist/cron/service/store.js +30 -44
  196. package/dist/cron/service/timer.js +182 -96
  197. package/dist/cron/service.js +3 -0
  198. package/dist/cron/stagger.js +37 -0
  199. package/dist/daemon/inspect.js +132 -92
  200. package/dist/daemon/runtime-paths.js +25 -4
  201. package/dist/daemon/service-audit.js +47 -16
  202. package/dist/discord/accounts.js +23 -20
  203. package/dist/discord/monitor/agent-components.js +1115 -219
  204. package/dist/discord/monitor/allow-list.js +114 -34
  205. package/dist/discord/monitor/listeners.js +204 -97
  206. package/dist/discord/monitor/message-handler.js +21 -10
  207. package/dist/discord/monitor/message-handler.preflight.js +195 -101
  208. package/dist/discord/monitor/message-handler.process.js +384 -123
  209. package/dist/discord/monitor/message-utils.js +86 -23
  210. package/dist/discord/monitor/native-command.js +77 -57
  211. package/dist/discord/monitor/provider.js +122 -117
  212. package/dist/discord/monitor/reply-context.js +20 -16
  213. package/dist/discord/monitor/reply-delivery.js +40 -8
  214. package/dist/discord/monitor/rest-fetch.js +22 -0
  215. package/dist/discord/monitor/threading.js +117 -24
  216. package/dist/discord/send.js +2 -1
  217. package/dist/discord/send.outbound.js +124 -11
  218. package/dist/discord/send.shared.js +112 -72
  219. package/dist/discord/voice-message.js +3 -3
  220. package/dist/gateway/auth.js +119 -44
  221. package/dist/gateway/call.js +76 -34
  222. package/dist/gateway/channel-health-monitor.js +57 -50
  223. package/dist/gateway/client.js +63 -29
  224. package/dist/gateway/control-ui-contract.js +1 -1
  225. package/dist/gateway/gateway-config-prompts.shared.js +2 -2
  226. package/dist/gateway/net.js +109 -1
  227. package/dist/gateway/protocol/index.js +5 -8
  228. package/dist/gateway/protocol/schema/agent.js +19 -1
  229. package/dist/gateway/protocol/schema/channels.js +21 -0
  230. package/dist/gateway/protocol/schema/cron.js +43 -30
  231. package/dist/gateway/protocol/schema/protocol-schemas.js +6 -11
  232. package/dist/gateway/protocol/schema/sessions.js +5 -1
  233. package/dist/gateway/protocol/schema.js +0 -1
  234. package/dist/gateway/server/presence-events.js +12 -0
  235. package/dist/gateway/server/ws-connection/message-handler.js +203 -212
  236. package/dist/gateway/server/ws-connection.js +58 -21
  237. package/dist/gateway/server-broadcast.js +18 -13
  238. package/dist/gateway/server-cron.js +177 -10
  239. package/dist/gateway/server-methods/agent-job.js +131 -38
  240. package/dist/gateway/server-methods/send.js +60 -14
  241. package/dist/gateway/server-methods/sessions.js +160 -96
  242. package/dist/gateway/server-methods/system.js +5 -7
  243. package/dist/gateway/server-methods-list.js +8 -0
  244. package/dist/gateway/server-methods.js +24 -8
  245. package/dist/gateway/server-node-events.js +278 -68
  246. package/dist/gateway/session-utils.fs.js +316 -75
  247. package/dist/gateway/session-utils.js +224 -70
  248. package/dist/gateway/sessions-patch.js +63 -20
  249. package/dist/gateway/test-temp-config.js +1 -1
  250. package/dist/gateway/tools-invoke-http.js +118 -70
  251. package/dist/gateway/ws-log.js +135 -107
  252. package/dist/hooks/frontmatter.js +36 -82
  253. package/dist/hooks/install.js +149 -139
  254. package/dist/hooks/internal-hooks.js +29 -4
  255. package/dist/hooks/plugin-hooks.js +2 -1
  256. package/dist/imessage/monitor/deliver.js +10 -4
  257. package/dist/imessage/monitor/monitor-provider.js +138 -375
  258. package/dist/imessage/monitor/runtime.js +4 -8
  259. package/dist/imessage/send.js +65 -19
  260. package/dist/infra/exec-approvals-allowlist.js +7 -0
  261. package/dist/infra/exec-approvals.js +35 -920
  262. package/dist/infra/exec-safe-bin-trust.js +64 -0
  263. package/dist/infra/heartbeat-runner.js +207 -134
  264. package/dist/infra/heartbeat-wake.js +183 -22
  265. package/dist/infra/install-source-utils.js +47 -0
  266. package/dist/infra/net/ssrf.js +170 -36
  267. package/dist/infra/outbound/deliver.js +224 -58
  268. package/dist/infra/outbound/message-action-spec.js +12 -5
  269. package/dist/infra/outbound/outbound-session.js +27 -25
  270. package/dist/infra/poolbot-root.js +32 -22
  271. package/dist/infra/ports.js +14 -11
  272. package/dist/infra/skills-remote.js +48 -37
  273. package/dist/infra/system-events.js +25 -11
  274. package/dist/infra/system-presence.js +26 -33
  275. package/dist/infra/tmp-poolbot-dir.js +81 -2
  276. package/dist/infra/wsl.js +37 -1
  277. package/dist/line/bot-message-context.js +163 -191
  278. package/dist/logging/subsystem.js +59 -22
  279. package/dist/markdown/ir.js +124 -50
  280. package/dist/media/store.js +1 -1
  281. package/dist/media-understanding/runner.entries.js +42 -25
  282. package/dist/media-understanding/runner.js +53 -488
  283. package/dist/memory/embeddings-gemini.js +53 -38
  284. package/dist/memory/manager-embedding-ops.js +48 -69
  285. package/dist/pairing/pairing-store.js +178 -119
  286. package/dist/plugin-sdk/index.js +34 -6
  287. package/dist/plugins/hooks.js +135 -14
  288. package/dist/plugins/install.js +190 -152
  289. package/dist/polls.js +11 -0
  290. package/dist/routing/resolve-route.js +190 -56
  291. package/dist/routing/session-key.js +38 -22
  292. package/dist/runtime.js +35 -9
  293. package/dist/security/audit-channel.js +1 -1
  294. package/dist/sessions/session-key-utils.js +29 -11
  295. package/dist/shared/frontmatter.js +5 -5
  296. package/dist/shared/node-list-types.js +1 -0
  297. package/dist/shared/string-normalization.js +15 -0
  298. package/dist/signal/monitor/event-handler.js +68 -36
  299. package/dist/signal/send.js +29 -37
  300. package/dist/slack/monitor/allow-list.js +10 -11
  301. package/dist/slack/monitor/commands.js +14 -3
  302. package/dist/slack/monitor/events/interactions.js +4 -4
  303. package/dist/slack/monitor/media.js +224 -16
  304. package/dist/slack/monitor/message-handler/dispatch.js +247 -13
  305. package/dist/slack/monitor/message-handler/prepare.js +128 -45
  306. package/dist/slack/monitor/slash.js +357 -144
  307. package/dist/slack/streaming.js +77 -0
  308. package/dist/telegram/accounts.js +40 -13
  309. package/dist/telegram/allowed-updates.js +3 -0
  310. package/dist/telegram/bot/delivery.js +129 -66
  311. package/dist/telegram/bot/helpers.js +136 -122
  312. package/dist/telegram/bot-handlers.js +600 -339
  313. package/dist/telegram/bot-message-context.js +115 -73
  314. package/dist/telegram/bot-message-dispatch.js +235 -104
  315. package/dist/telegram/bot-native-command-menu.js +3 -1
  316. package/dist/telegram/bot-native-commands.js +213 -193
  317. package/dist/telegram/bot.js +24 -132
  318. package/dist/telegram/draft-stream.js +84 -75
  319. package/dist/telegram/format.js +150 -6
  320. package/dist/telegram/send.js +415 -255
  321. package/dist/telegram/targets.js +21 -2
  322. package/dist/telegram/update-offset-store.js +19 -3
  323. package/dist/terminal/restore.js +5 -2
  324. package/dist/test-utils/fetch-mock.js +5 -0
  325. package/dist/version.js +18 -5
  326. package/dist/web/auto-reply/monitor/broadcast.js +7 -3
  327. package/dist/web/auto-reply/monitor/on-message.js +6 -3
  328. package/dist/web/inbound/media.js +34 -8
  329. package/dist/web/inbound/monitor.js +34 -17
  330. package/dist/web/inbound/send-api.js +18 -17
  331. package/dist/web/outbound.js +12 -5
  332. package/dist/wizard/clack-prompter.js +40 -7
  333. package/extensions/bluebubbles/package.json +1 -1
  334. package/extensions/copilot-proxy/package.json +1 -1
  335. package/extensions/diagnostics-otel/package.json +1 -1
  336. package/extensions/discord/package.json +1 -1
  337. package/extensions/feishu/package.json +1 -1
  338. package/extensions/google-antigravity-auth/package.json +1 -1
  339. package/extensions/google-gemini-cli-auth/package.json +1 -1
  340. package/extensions/googlechat/package.json +1 -1
  341. package/extensions/imessage/package.json +1 -1
  342. package/extensions/irc/package.json +1 -1
  343. package/extensions/line/package.json +1 -1
  344. package/extensions/llm-task/package.json +1 -1
  345. package/extensions/lobster/package.json +1 -1
  346. package/extensions/matrix/CHANGELOG.md +5 -0
  347. package/extensions/matrix/package.json +1 -1
  348. package/extensions/mattermost/package.json +1 -1
  349. package/extensions/memory-core/package.json +1 -1
  350. package/extensions/memory-lancedb/package.json +1 -1
  351. package/extensions/minimax-portal-auth/package.json +1 -1
  352. package/extensions/msteams/CHANGELOG.md +5 -0
  353. package/extensions/msteams/package.json +1 -1
  354. package/extensions/nextcloud-talk/package.json +1 -1
  355. package/extensions/nostr/CHANGELOG.md +5 -0
  356. package/extensions/nostr/package.json +1 -1
  357. package/extensions/open-prose/package.json +1 -1
  358. package/extensions/openai-codex-auth/package.json +1 -1
  359. package/extensions/signal/package.json +1 -1
  360. package/extensions/slack/package.json +1 -1
  361. package/extensions/telegram/package.json +1 -1
  362. package/extensions/tlon/package.json +1 -1
  363. package/extensions/twitch/CHANGELOG.md +5 -0
  364. package/extensions/twitch/package.json +1 -1
  365. package/extensions/voice-call/CHANGELOG.md +5 -0
  366. package/extensions/voice-call/package.json +1 -1
  367. package/extensions/whatsapp/package.json +1 -1
  368. package/extensions/zalo/CHANGELOG.md +5 -0
  369. package/extensions/zalo/package.json +1 -1
  370. package/extensions/zalouser/CHANGELOG.md +5 -0
  371. package/extensions/zalouser/package.json +1 -1
  372. package/package.json +1 -1
  373. package/skills/apple-reminders/SKILL.md +100 -49
  374. package/skills/coding-agent/SKILL.md +34 -28
  375. package/skills/github/SKILL.md +131 -16
  376. package/skills/imsg/SKILL.md +112 -15
  377. package/skills/openhue/SKILL.md +101 -19
  378. package/skills/plcode-controller/SKILL.md +156 -0
  379. package/skills/plcode-controller/assets/operator-prompts.md +65 -0
  380. package/skills/plcode-controller/references/command-cheatsheet.md +53 -0
  381. package/skills/plcode-controller/references/failure-handling.md +60 -0
  382. package/skills/plcode-controller/references/model-selection.md +57 -0
  383. package/skills/plcode-controller/references/plan-vs-build.md +52 -0
  384. package/skills/plcode-controller/references/question-handling.md +40 -0
  385. package/skills/plcode-controller/references/session-management.md +63 -0
  386. package/skills/plcode-controller/references/workflow.md +35 -0
  387. package/skills/tmux/SKILL.md +111 -79
  388. package/skills/weather/SKILL.md +88 -25
@@ -1,58 +1,134 @@
1
1
  import crypto from "node:crypto";
2
- import { abortEmbeddedPiRun } from "../../agents/pi-embedded.js";
3
2
  import { AGENT_LANE_SUBAGENT } from "../../agents/lanes.js";
4
- import { listSubagentRunsForRequester } from "../../agents/subagent-registry.js";
3
+ import { abortEmbeddedPiRun } from "../../agents/pi-embedded.js";
4
+ import { clearSubagentRunSteerRestart, listSubagentRunsForRequester, markSubagentRunTerminated, markSubagentRunForSteerRestart, replaceSubagentRunAfterSteer, } from "../../agents/subagent-registry.js";
5
+ import { spawnSubagentDirect } from "../../agents/subagent-spawn.js";
5
6
  import { extractAssistantText, resolveInternalSessionKey, resolveMainSessionAlias, sanitizeTextContent, stripToolMessages, } from "../../agents/tools/sessions-helpers.js";
6
- import { loadSessionStore, resolveStorePath, updateSessionStore } from "../../config/sessions.js";
7
+ import { loadSessionStore, resolveStorePath, updateSessionStore, } from "../../config/sessions.js";
7
8
  import { callGateway } from "../../gateway/call.js";
8
9
  import { logVerbose } from "../../globals.js";
10
+ import { formatTimeAgo } from "../../infra/format-time/format-relative.js";
9
11
  import { parseAgentSessionKey } from "../../routing/session-key.js";
12
+ import { extractTextFromChatContent } from "../../shared/chat-content.js";
13
+ import { formatDurationCompact, formatTokenUsageDisplay, truncateLine, } from "../../shared/subagents-format.js";
10
14
  import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js";
11
- import { formatAgeShort, formatDurationShort, formatRunLabel, formatRunStatus, sortSubagentRuns, } from "./subagents-utils.js";
12
15
  import { stopSubagentsForRequester } from "./abort.js";
13
16
  import { clearSessionQueues } from "./queue.js";
17
+ import { formatRunLabel, formatRunStatus, sortSubagentRuns } from "./subagents-utils.js";
14
18
  const COMMAND = "/subagents";
15
- const ACTIONS = new Set(["list", "stop", "log", "send", "info", "help"]);
19
+ const COMMAND_KILL = "/kill";
20
+ const COMMAND_STEER = "/steer";
21
+ const COMMAND_TELL = "/tell";
22
+ const ACTIONS = new Set(["list", "kill", "log", "send", "steer", "info", "spawn", "help"]);
23
+ const RECENT_WINDOW_MINUTES = 30;
24
+ const SUBAGENT_TASK_PREVIEW_MAX = 110;
25
+ const STEER_ABORT_SETTLE_TIMEOUT_MS = 5_000;
26
+ function compactLine(value) {
27
+ return value.replace(/\s+/g, " ").trim();
28
+ }
29
+ function formatTaskPreview(value) {
30
+ return truncateLine(compactLine(value), SUBAGENT_TASK_PREVIEW_MAX);
31
+ }
32
+ function resolveModelDisplay(entry, fallbackModel) {
33
+ const model = typeof entry?.model === "string" ? entry.model.trim() : "";
34
+ const provider = typeof entry?.modelProvider === "string" ? entry.modelProvider.trim() : "";
35
+ let combined = model.includes("/") ? model : model && provider ? `${provider}/${model}` : model;
36
+ if (!combined) {
37
+ // Fall back to override fields which are populated at spawn time,
38
+ // before the first run completes and writes model/modelProvider.
39
+ const overrideModel = typeof entry?.modelOverride === "string" ? entry.modelOverride.trim() : "";
40
+ const overrideProvider = typeof entry?.providerOverride === "string" ? entry.providerOverride.trim() : "";
41
+ combined = overrideModel.includes("/")
42
+ ? overrideModel
43
+ : overrideModel && overrideProvider
44
+ ? `${overrideProvider}/${overrideModel}`
45
+ : overrideModel;
46
+ }
47
+ if (!combined) {
48
+ combined = fallbackModel?.trim() || "";
49
+ }
50
+ if (!combined) {
51
+ return "model n/a";
52
+ }
53
+ const slash = combined.lastIndexOf("/");
54
+ if (slash >= 0 && slash < combined.length - 1) {
55
+ return combined.slice(slash + 1);
56
+ }
57
+ return combined;
58
+ }
59
+ function resolveDisplayStatus(entry) {
60
+ const status = formatRunStatus(entry);
61
+ return status === "error" ? "failed" : status;
62
+ }
16
63
  function formatTimestamp(valueMs) {
17
- if (!valueMs || !Number.isFinite(valueMs) || valueMs <= 0)
64
+ if (!valueMs || !Number.isFinite(valueMs) || valueMs <= 0) {
18
65
  return "n/a";
66
+ }
19
67
  return new Date(valueMs).toISOString();
20
68
  }
21
69
  function formatTimestampWithAge(valueMs) {
22
- if (!valueMs || !Number.isFinite(valueMs) || valueMs <= 0)
70
+ if (!valueMs || !Number.isFinite(valueMs) || valueMs <= 0) {
23
71
  return "n/a";
24
- return `${formatTimestamp(valueMs)} (${formatAgeShort(Date.now() - valueMs)})`;
72
+ }
73
+ return `${formatTimestamp(valueMs)} (${formatTimeAgo(Date.now() - valueMs, { fallback: "n/a" })})`;
25
74
  }
26
- function resolveRequesterSessionKey(params) {
27
- const raw = params.sessionKey?.trim() || params.ctx.CommandTargetSessionKey?.trim();
28
- if (!raw)
75
+ function resolveRequesterSessionKey(params, opts) {
76
+ const commandTarget = params.ctx.CommandTargetSessionKey?.trim();
77
+ const commandSession = params.sessionKey?.trim();
78
+ const raw = opts?.preferCommandTarget
79
+ ? commandTarget || commandSession
80
+ : commandSession || commandTarget;
81
+ if (!raw) {
29
82
  return undefined;
83
+ }
30
84
  const { mainKey, alias } = resolveMainSessionAlias(params.cfg);
31
85
  return resolveInternalSessionKey({ key: raw, alias, mainKey });
32
86
  }
33
87
  function resolveSubagentTarget(runs, token) {
34
88
  const trimmed = token?.trim();
35
- if (!trimmed)
89
+ if (!trimmed) {
36
90
  return { error: "Missing subagent id." };
91
+ }
37
92
  if (trimmed === "last") {
38
93
  const sorted = sortSubagentRuns(runs);
39
94
  return { entry: sorted[0] };
40
95
  }
41
96
  const sorted = sortSubagentRuns(runs);
97
+ const recentCutoff = Date.now() - RECENT_WINDOW_MINUTES * 60_000;
98
+ const numericOrder = [
99
+ ...sorted.filter((entry) => !entry.endedAt),
100
+ ...sorted.filter((entry) => !!entry.endedAt && (entry.endedAt ?? 0) >= recentCutoff),
101
+ ];
42
102
  if (/^\d+$/.test(trimmed)) {
43
103
  const idx = Number.parseInt(trimmed, 10);
44
- if (!Number.isFinite(idx) || idx <= 0 || idx > sorted.length) {
104
+ if (!Number.isFinite(idx) || idx <= 0 || idx > numericOrder.length) {
45
105
  return { error: `Invalid subagent index: ${trimmed}` };
46
106
  }
47
- return { entry: sorted[idx - 1] };
107
+ return { entry: numericOrder[idx - 1] };
48
108
  }
49
109
  if (trimmed.includes(":")) {
50
110
  const match = runs.find((entry) => entry.childSessionKey === trimmed);
51
111
  return match ? { entry: match } : { error: `Unknown subagent session: ${trimmed}` };
52
112
  }
113
+ const lowered = trimmed.toLowerCase();
114
+ const byLabel = runs.filter((entry) => formatRunLabel(entry).toLowerCase() === lowered);
115
+ if (byLabel.length === 1) {
116
+ return { entry: byLabel[0] };
117
+ }
118
+ if (byLabel.length > 1) {
119
+ return { error: `Ambiguous subagent label: ${trimmed}` };
120
+ }
121
+ const byLabelPrefix = runs.filter((entry) => formatRunLabel(entry).toLowerCase().startsWith(lowered));
122
+ if (byLabelPrefix.length === 1) {
123
+ return { entry: byLabelPrefix[0] };
124
+ }
125
+ if (byLabelPrefix.length > 1) {
126
+ return { error: `Ambiguous subagent label prefix: ${trimmed}` };
127
+ }
53
128
  const byRunId = runs.filter((entry) => entry.runId.startsWith(trimmed));
54
- if (byRunId.length === 1)
129
+ if (byRunId.length === 1) {
55
130
  return { entry: byRunId[0] };
131
+ }
56
132
  if (byRunId.length > 1) {
57
133
  return { error: `Ambiguous run id prefix: ${trimmed}` };
58
134
  }
@@ -60,81 +136,93 @@ function resolveSubagentTarget(runs, token) {
60
136
  }
61
137
  function buildSubagentsHelp() {
62
138
  return [
63
- "🧭 Subagents",
139
+ "Subagents",
64
140
  "Usage:",
65
141
  "- /subagents list",
66
- "- /subagents stop <id|#|all>",
142
+ "- /subagents kill <id|#|all>",
67
143
  "- /subagents log <id|#> [limit] [tools]",
68
144
  "- /subagents info <id|#>",
69
145
  "- /subagents send <id|#> <message>",
146
+ "- /subagents steer <id|#> <message>",
147
+ "- /subagents spawn <agentId> <task> [--model <model>] [--thinking <level>]",
148
+ "- /kill <id|#|all>",
149
+ "- /steer <id|#> <message>",
150
+ "- /tell <id|#> <message>",
70
151
  "",
71
- "Ids: use the list index (#), runId prefix, or full session key.",
152
+ "Ids: use the list index (#), runId/session prefix, label, or full session key.",
72
153
  ].join("\n");
73
154
  }
74
- function normalizeMessageText(text) {
75
- return text.replace(/\s+/g, " ").trim();
76
- }
77
155
  export function extractMessageText(message) {
78
156
  const role = typeof message.role === "string" ? message.role : "";
79
157
  const shouldSanitize = role === "assistant";
80
- const content = message.content;
81
- if (typeof content === "string") {
82
- const normalized = normalizeMessageText(shouldSanitize ? sanitizeTextContent(content) : content);
83
- return normalized ? { role, text: normalized } : null;
84
- }
85
- if (!Array.isArray(content))
86
- return null;
87
- const chunks = [];
88
- for (const block of content) {
89
- if (!block || typeof block !== "object")
90
- continue;
91
- if (block.type !== "text")
92
- continue;
93
- const text = block.text;
94
- if (typeof text === "string") {
95
- const value = shouldSanitize ? sanitizeTextContent(text) : text;
96
- if (value.trim()) {
97
- chunks.push(value);
98
- }
99
- }
100
- }
101
- const joined = normalizeMessageText(chunks.join(" "));
102
- return joined ? { role, text: joined } : null;
158
+ const text = extractTextFromChatContent(message.content, {
159
+ sanitizeText: shouldSanitize ? sanitizeTextContent : undefined,
160
+ });
161
+ return text ? { role, text } : null;
103
162
  }
104
163
  function formatLogLines(messages) {
105
164
  const lines = [];
106
165
  for (const msg of messages) {
107
166
  const extracted = extractMessageText(msg);
108
- if (!extracted)
167
+ if (!extracted) {
109
168
  continue;
169
+ }
110
170
  const label = extracted.role === "assistant" ? "Assistant" : "User";
111
171
  lines.push(`${label}: ${extracted.text}`);
112
172
  }
113
173
  return lines;
114
174
  }
115
- function loadSubagentSessionEntry(params, childKey) {
175
+ function loadSubagentSessionEntry(params, childKey, storeCache) {
116
176
  const parsed = parseAgentSessionKey(childKey);
117
177
  const storePath = resolveStorePath(params.cfg.session?.store, { agentId: parsed?.agentId });
118
- const store = loadSessionStore(storePath);
178
+ let store = storeCache?.get(storePath);
179
+ if (!store) {
180
+ store = loadSessionStore(storePath);
181
+ storeCache?.set(storePath, store);
182
+ }
119
183
  return { storePath, store, entry: store[childKey] };
120
184
  }
121
185
  export const handleSubagentsCommand = async (params, allowTextCommands) => {
122
- if (!allowTextCommands)
186
+ if (!allowTextCommands) {
123
187
  return null;
188
+ }
124
189
  const normalized = params.command.commandBodyNormalized;
125
- if (!normalized.startsWith(COMMAND))
190
+ const handledPrefix = normalized.startsWith(COMMAND)
191
+ ? COMMAND
192
+ : normalized.startsWith(COMMAND_KILL)
193
+ ? COMMAND_KILL
194
+ : normalized.startsWith(COMMAND_STEER)
195
+ ? COMMAND_STEER
196
+ : normalized.startsWith(COMMAND_TELL)
197
+ ? COMMAND_TELL
198
+ : null;
199
+ if (!handledPrefix) {
126
200
  return null;
201
+ }
127
202
  if (!params.command.isAuthorizedSender) {
128
- logVerbose(`Ignoring /subagents from unauthorized sender: ${params.command.senderId || "<unknown>"}`);
203
+ logVerbose(`Ignoring ${handledPrefix} from unauthorized sender: ${params.command.senderId || "<unknown>"}`);
129
204
  return { shouldContinue: false };
130
205
  }
131
- const rest = normalized.slice(COMMAND.length).trim();
132
- const [actionRaw, ...restTokens] = rest.split(/\s+/).filter(Boolean);
133
- const action = actionRaw?.toLowerCase() || "list";
134
- if (!ACTIONS.has(action)) {
135
- return { shouldContinue: false, reply: { text: buildSubagentsHelp() } };
206
+ const rest = normalized.slice(handledPrefix.length).trim();
207
+ const restTokens = rest.split(/\s+/).filter(Boolean);
208
+ let action = "list";
209
+ if (handledPrefix === COMMAND) {
210
+ const [actionRaw] = restTokens;
211
+ action = actionRaw?.toLowerCase() || "list";
212
+ if (!ACTIONS.has(action)) {
213
+ return { shouldContinue: false, reply: { text: buildSubagentsHelp() } };
214
+ }
215
+ restTokens.splice(0, 1);
216
+ }
217
+ else if (handledPrefix === COMMAND_KILL) {
218
+ action = "kill";
219
+ }
220
+ else {
221
+ action = "steer";
136
222
  }
137
- const requesterKey = resolveRequesterSessionKey(params);
223
+ const requesterKey = resolveRequesterSessionKey(params, {
224
+ preferCommandTarget: action === "spawn",
225
+ });
138
226
  if (!requesterKey) {
139
227
  return { shouldContinue: false, reply: { text: "⚠️ Missing session key." } };
140
228
  }
@@ -143,39 +231,71 @@ export const handleSubagentsCommand = async (params, allowTextCommands) => {
143
231
  return { shouldContinue: false, reply: { text: buildSubagentsHelp() } };
144
232
  }
145
233
  if (action === "list") {
146
- if (runs.length === 0) {
147
- return { shouldContinue: false, reply: { text: "🧭 Subagents: none for this session." } };
148
- }
149
234
  const sorted = sortSubagentRuns(runs);
150
- const active = sorted.filter((entry) => !entry.endedAt);
151
- const done = sorted.length - active.length;
152
- const lines = ["🧭 Subagents (current session)", `Active: ${active.length} · Done: ${done}`];
153
- sorted.forEach((entry, index) => {
154
- const status = formatRunStatus(entry);
155
- const label = formatRunLabel(entry);
156
- const runtime = entry.endedAt && entry.startedAt
157
- ? formatDurationShort(entry.endedAt - entry.startedAt)
158
- : formatAgeShort(Date.now() - (entry.startedAt ?? entry.createdAt));
159
- const runId = entry.runId.slice(0, 8);
160
- lines.push(`${index + 1}) ${status} · ${label} · ${runtime} · run ${runId} · ${entry.childSessionKey}`);
235
+ const now = Date.now();
236
+ const recentCutoff = now - RECENT_WINDOW_MINUTES * 60_000;
237
+ const storeCache = new Map();
238
+ let index = 1;
239
+ const activeLines = sorted
240
+ .filter((entry) => !entry.endedAt)
241
+ .map((entry) => {
242
+ const { entry: sessionEntry } = loadSubagentSessionEntry(params, entry.childSessionKey, storeCache);
243
+ const usageText = formatTokenUsageDisplay(sessionEntry);
244
+ const label = truncateLine(formatRunLabel(entry, { maxLength: 48 }), 48);
245
+ const task = formatTaskPreview(entry.task);
246
+ const runtime = formatDurationCompact(now - (entry.startedAt ?? entry.createdAt));
247
+ const status = resolveDisplayStatus(entry);
248
+ const line = `${index}. ${label} (${resolveModelDisplay(sessionEntry, entry.model)}, ${runtime}${usageText ? `, ${usageText}` : ""}) ${status}${task.toLowerCase() !== label.toLowerCase() ? ` - ${task}` : ""}`;
249
+ index += 1;
250
+ return line;
251
+ });
252
+ const recentLines = sorted
253
+ .filter((entry) => !!entry.endedAt && (entry.endedAt ?? 0) >= recentCutoff)
254
+ .map((entry) => {
255
+ const { entry: sessionEntry } = loadSubagentSessionEntry(params, entry.childSessionKey, storeCache);
256
+ const usageText = formatTokenUsageDisplay(sessionEntry);
257
+ const label = truncateLine(formatRunLabel(entry, { maxLength: 48 }), 48);
258
+ const task = formatTaskPreview(entry.task);
259
+ const runtime = formatDurationCompact((entry.endedAt ?? now) - (entry.startedAt ?? entry.createdAt));
260
+ const status = resolveDisplayStatus(entry);
261
+ const line = `${index}. ${label} (${resolveModelDisplay(sessionEntry, entry.model)}, ${runtime}${usageText ? `, ${usageText}` : ""}) ${status}${task.toLowerCase() !== label.toLowerCase() ? ` - ${task}` : ""}`;
262
+ index += 1;
263
+ return line;
161
264
  });
265
+ const lines = ["active subagents:", "-----"];
266
+ if (activeLines.length === 0) {
267
+ lines.push("(none)");
268
+ }
269
+ else {
270
+ lines.push(activeLines.join("\n"));
271
+ }
272
+ lines.push("", `recent subagents (last ${RECENT_WINDOW_MINUTES}m):`, "-----");
273
+ if (recentLines.length === 0) {
274
+ lines.push("(none)");
275
+ }
276
+ else {
277
+ lines.push(recentLines.join("\n"));
278
+ }
162
279
  return { shouldContinue: false, reply: { text: lines.join("\n") } };
163
280
  }
164
- if (action === "stop") {
281
+ if (action === "kill") {
165
282
  const target = restTokens[0];
166
283
  if (!target) {
167
- return { shouldContinue: false, reply: { text: "⚙️ Usage: /subagents stop <id|#|all>" } };
284
+ return {
285
+ shouldContinue: false,
286
+ reply: {
287
+ text: handledPrefix === COMMAND
288
+ ? "Usage: /subagents kill <id|#|all>"
289
+ : "Usage: /kill <id|#|all>",
290
+ },
291
+ };
168
292
  }
169
293
  if (target === "all" || target === "*") {
170
- const { stopped } = stopSubagentsForRequester({
294
+ stopSubagentsForRequester({
171
295
  cfg: params.cfg,
172
296
  requesterSessionKey: requesterKey,
173
297
  });
174
- const label = stopped === 1 ? "subagent" : "subagents";
175
- return {
176
- shouldContinue: false,
177
- reply: { text: `⚙️ Stopped ${stopped} ${label}.` },
178
- };
298
+ return { shouldContinue: false };
179
299
  }
180
300
  const resolved = resolveSubagentTarget(runs, target);
181
301
  if (!resolved.entry) {
@@ -187,7 +307,7 @@ export const handleSubagentsCommand = async (params, allowTextCommands) => {
187
307
  if (resolved.entry.endedAt) {
188
308
  return {
189
309
  shouldContinue: false,
190
- reply: { text: "⚙️ Subagent already finished." },
310
+ reply: { text: `${formatRunLabel(resolved.entry)} is already finished.` },
191
311
  };
192
312
  }
193
313
  const childKey = resolved.entry.childSessionKey;
@@ -198,7 +318,7 @@ export const handleSubagentsCommand = async (params, allowTextCommands) => {
198
318
  }
199
319
  const cleared = clearSessionQueues([childKey, sessionId]);
200
320
  if (cleared.followupCleared > 0 || cleared.laneCleared > 0) {
201
- logVerbose(`subagents stop: cleared followups=${cleared.followupCleared} lane=${cleared.laneCleared} keys=${cleared.keys.join(",")}`);
321
+ logVerbose(`subagents kill: cleared followups=${cleared.followupCleared} lane=${cleared.laneCleared} keys=${cleared.keys.join(",")}`);
202
322
  }
203
323
  if (entry) {
204
324
  entry.abortedLastRun = true;
@@ -208,10 +328,17 @@ export const handleSubagentsCommand = async (params, allowTextCommands) => {
208
328
  nextStore[childKey] = entry;
209
329
  });
210
330
  }
211
- return {
212
- shouldContinue: false,
213
- reply: { text: `⚙️ Stop requested for ${formatRunLabel(resolved.entry)}.` },
214
- };
331
+ markSubagentRunTerminated({
332
+ runId: resolved.entry.runId,
333
+ childSessionKey: childKey,
334
+ reason: "killed",
335
+ });
336
+ // Cascade: also stop any sub-sub-agents spawned by this child.
337
+ stopSubagentsForRequester({
338
+ cfg: params.cfg,
339
+ requesterSessionKey: childKey,
340
+ });
341
+ return { shouldContinue: false };
215
342
  }
216
343
  if (action === "info") {
217
344
  const target = restTokens[0];
@@ -228,14 +355,14 @@ export const handleSubagentsCommand = async (params, allowTextCommands) => {
228
355
  const run = resolved.entry;
229
356
  const { entry: sessionEntry } = loadSubagentSessionEntry(params, run.childSessionKey);
230
357
  const runtime = run.startedAt && Number.isFinite(run.startedAt)
231
- ? formatDurationShort((run.endedAt ?? Date.now()) - run.startedAt)
358
+ ? (formatDurationCompact((run.endedAt ?? Date.now()) - run.startedAt) ?? "n/a")
232
359
  : "n/a";
233
360
  const outcome = run.outcome
234
361
  ? `${run.outcome.status}${run.outcome.error ? ` (${run.outcome.error})` : ""}`
235
362
  : "n/a";
236
363
  const lines = [
237
364
  "ℹ️ Subagent info",
238
- `Status: ${formatRunStatus(run)}`,
365
+ `Status: ${resolveDisplayStatus(run)}`,
239
366
  `Label: ${formatRunLabel(run)}`,
240
367
  `Task: ${run.task}`,
241
368
  `Run: ${run.runId}`,
@@ -268,10 +395,10 @@ export const handleSubagentsCommand = async (params, allowTextCommands) => {
268
395
  reply: { text: `⚠️ ${resolved.error ?? "Unknown subagent."}` },
269
396
  };
270
397
  }
271
- const history = (await callGateway({
398
+ const history = await callGateway({
272
399
  method: "chat.history",
273
400
  params: { sessionKey: resolved.entry.childSessionKey, limit },
274
- }));
401
+ });
275
402
  const rawMessages = Array.isArray(history?.messages) ? history.messages : [];
276
403
  const filtered = includeTools ? rawMessages : stripToolMessages(rawMessages);
277
404
  const lines = formatLogLines(filtered);
@@ -281,13 +408,20 @@ export const handleSubagentsCommand = async (params, allowTextCommands) => {
281
408
  }
282
409
  return { shouldContinue: false, reply: { text: [header, ...lines].join("\n") } };
283
410
  }
284
- if (action === "send") {
411
+ if (action === "send" || action === "steer") {
412
+ const steerRequested = action === "steer";
285
413
  const target = restTokens[0];
286
414
  const message = restTokens.slice(1).join(" ").trim();
287
415
  if (!target || !message) {
288
416
  return {
289
417
  shouldContinue: false,
290
- reply: { text: "✉️ Usage: /subagents send <id|#> <message>" },
418
+ reply: {
419
+ text: steerRequested
420
+ ? handledPrefix === COMMAND
421
+ ? "Usage: /subagents steer <id|#> <message>"
422
+ : `Usage: ${handledPrefix} <id|#> <message>`
423
+ : "Usage: /subagents send <id|#> <message>",
424
+ },
291
425
  };
292
426
  }
293
427
  const resolved = resolveSubagentTarget(runs, target);
@@ -297,34 +431,94 @@ export const handleSubagentsCommand = async (params, allowTextCommands) => {
297
431
  reply: { text: `⚠️ ${resolved.error ?? "Unknown subagent."}` },
298
432
  };
299
433
  }
434
+ if (steerRequested && resolved.entry.endedAt) {
435
+ return {
436
+ shouldContinue: false,
437
+ reply: { text: `${formatRunLabel(resolved.entry)} is already finished.` },
438
+ };
439
+ }
440
+ const { entry: targetSessionEntry } = loadSubagentSessionEntry(params, resolved.entry.childSessionKey);
441
+ const targetSessionId = typeof targetSessionEntry?.sessionId === "string" && targetSessionEntry.sessionId.trim()
442
+ ? targetSessionEntry.sessionId.trim()
443
+ : undefined;
444
+ if (steerRequested) {
445
+ // Suppress stale announce before interrupting the in-flight run.
446
+ markSubagentRunForSteerRestart(resolved.entry.runId);
447
+ // Force an immediate interruption and make steer the next run.
448
+ if (targetSessionId) {
449
+ abortEmbeddedPiRun(targetSessionId);
450
+ }
451
+ const cleared = clearSessionQueues([resolved.entry.childSessionKey, targetSessionId]);
452
+ if (cleared.followupCleared > 0 || cleared.laneCleared > 0) {
453
+ logVerbose(`subagents steer: cleared followups=${cleared.followupCleared} lane=${cleared.laneCleared} keys=${cleared.keys.join(",")}`);
454
+ }
455
+ // Best effort: wait for the interrupted run to settle so the steer
456
+ // message is appended on the existing conversation state.
457
+ try {
458
+ await callGateway({
459
+ method: "agent.wait",
460
+ params: {
461
+ runId: resolved.entry.runId,
462
+ timeoutMs: STEER_ABORT_SETTLE_TIMEOUT_MS,
463
+ },
464
+ timeoutMs: STEER_ABORT_SETTLE_TIMEOUT_MS + 2_000,
465
+ });
466
+ }
467
+ catch {
468
+ // Continue even if wait fails; steer should still be attempted.
469
+ }
470
+ }
300
471
  const idempotencyKey = crypto.randomUUID();
301
472
  let runId = idempotencyKey;
302
473
  try {
303
- const response = (await callGateway({
474
+ const response = await callGateway({
304
475
  method: "agent",
305
476
  params: {
306
477
  message,
307
478
  sessionKey: resolved.entry.childSessionKey,
479
+ sessionId: targetSessionId,
308
480
  idempotencyKey,
309
481
  deliver: false,
310
482
  channel: INTERNAL_MESSAGE_CHANNEL,
311
483
  lane: AGENT_LANE_SUBAGENT,
484
+ timeout: 0,
312
485
  },
313
486
  timeoutMs: 10_000,
314
- }));
315
- if (response?.runId)
316
- runId = response.runId;
487
+ });
488
+ const responseRunId = typeof response?.runId === "string" ? response.runId : undefined;
489
+ if (responseRunId) {
490
+ runId = responseRunId;
491
+ }
317
492
  }
318
493
  catch (err) {
494
+ if (steerRequested) {
495
+ // Replacement launch failed; restore announce behavior for the
496
+ // original run so completion is not silently suppressed.
497
+ clearSubagentRunSteerRestart(resolved.entry.runId);
498
+ }
319
499
  const messageText = err instanceof Error ? err.message : typeof err === "string" ? err : "error";
320
- return { shouldContinue: false, reply: { text: `⚠️ Send failed: ${messageText}` } };
500
+ return { shouldContinue: false, reply: { text: `send failed: ${messageText}` } };
501
+ }
502
+ if (steerRequested) {
503
+ replaceSubagentRunAfterSteer({
504
+ previousRunId: resolved.entry.runId,
505
+ nextRunId: runId,
506
+ fallback: resolved.entry,
507
+ runTimeoutSeconds: resolved.entry.runTimeoutSeconds ?? 0,
508
+ });
509
+ return {
510
+ shouldContinue: false,
511
+ reply: {
512
+ text: `steered ${formatRunLabel(resolved.entry)} (run ${runId.slice(0, 8)}).`,
513
+ },
514
+ };
321
515
  }
322
516
  const waitMs = 30_000;
323
- const wait = (await callGateway({
517
+ const wait = await callGateway({
324
518
  method: "agent.wait",
325
519
  params: { runId, timeoutMs: waitMs },
326
520
  timeoutMs: waitMs + 2000,
327
- }));
521
+ });
328
522
  if (wait?.status === "timeout") {
329
523
  return {
330
524
  shouldContinue: false,
@@ -332,17 +526,18 @@ export const handleSubagentsCommand = async (params, allowTextCommands) => {
332
526
  };
333
527
  }
334
528
  if (wait?.status === "error") {
529
+ const waitError = typeof wait.error === "string" ? wait.error : "unknown error";
335
530
  return {
336
531
  shouldContinue: false,
337
532
  reply: {
338
- text: `⚠️ Subagent error: ${wait.error ?? "unknown error"} (run ${runId.slice(0, 8)}).`,
533
+ text: `⚠️ Subagent error: ${waitError} (run ${runId.slice(0, 8)}).`,
339
534
  },
340
535
  };
341
536
  }
342
- const history = (await callGateway({
537
+ const history = await callGateway({
343
538
  method: "chat.history",
344
539
  params: { sessionKey: resolved.entry.childSessionKey, limit: 50 },
345
- }));
540
+ });
346
541
  const filtered = stripToolMessages(Array.isArray(history?.messages) ? history.messages : []);
347
542
  const last = filtered.length > 0 ? filtered[filtered.length - 1] : undefined;
348
543
  const replyText = last ? extractAssistantText(last) : undefined;
@@ -353,5 +548,62 @@ export const handleSubagentsCommand = async (params, allowTextCommands) => {
353
548
  },
354
549
  };
355
550
  }
551
+ if (action === "spawn") {
552
+ const agentId = restTokens[0];
553
+ // Parse remaining tokens: task text with optional --model and --thinking flags.
554
+ const taskParts = [];
555
+ let model;
556
+ let thinking;
557
+ for (let i = 1; i < restTokens.length; i++) {
558
+ if (restTokens[i] === "--model" && i + 1 < restTokens.length) {
559
+ i += 1;
560
+ model = restTokens[i];
561
+ }
562
+ else if (restTokens[i] === "--thinking" && i + 1 < restTokens.length) {
563
+ i += 1;
564
+ thinking = restTokens[i];
565
+ }
566
+ else {
567
+ taskParts.push(restTokens[i]);
568
+ }
569
+ }
570
+ const task = taskParts.join(" ").trim();
571
+ if (!agentId || !task) {
572
+ return {
573
+ shouldContinue: false,
574
+ reply: {
575
+ text: "Usage: /subagents spawn <agentId> <task> [--model <model>] [--thinking <level>]",
576
+ },
577
+ };
578
+ }
579
+ const commandTo = typeof params.command.to === "string" ? params.command.to.trim() : "";
580
+ const originatingTo = typeof params.ctx.OriginatingTo === "string" ? params.ctx.OriginatingTo.trim() : "";
581
+ const fallbackTo = typeof params.ctx.To === "string" ? params.ctx.To.trim() : "";
582
+ // OriginatingTo reflects the active conversation target and is safer than
583
+ // command.to for cross-surface command dispatch.
584
+ const normalizedTo = originatingTo || commandTo || fallbackTo || undefined;
585
+ const result = await spawnSubagentDirect({ task, agentId, model, thinking, cleanup: "keep", expectsCompletionMessage: true }, {
586
+ agentSessionKey: requesterKey,
587
+ agentChannel: params.ctx.OriginatingChannel ?? params.command.channel,
588
+ agentAccountId: params.ctx.AccountId,
589
+ agentTo: normalizedTo,
590
+ agentThreadId: params.ctx.MessageThreadId,
591
+ agentGroupId: params.sessionEntry?.groupId ?? null,
592
+ agentGroupChannel: params.sessionEntry?.groupChannel ?? null,
593
+ agentGroupSpace: params.sessionEntry?.space ?? null,
594
+ });
595
+ if (result.status === "accepted") {
596
+ return {
597
+ shouldContinue: false,
598
+ reply: {
599
+ text: `Spawned subagent ${agentId} (session ${result.childSessionKey}, run ${result.runId?.slice(0, 8)}).`,
600
+ },
601
+ };
602
+ }
603
+ return {
604
+ shouldContinue: false,
605
+ reply: { text: `Spawn failed: ${result.error ?? result.status}` },
606
+ };
607
+ }
356
608
  return { shouldContinue: false, reply: { text: buildSubagentsHelp() } };
357
609
  };