@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
@@ -2,34 +2,29 @@ import fs from "node:fs/promises";
2
2
  import { resolveHumanDelayConfig } from "../../agents/identity.js";
3
3
  import { resolveTextChunkLimit } from "../../auto-reply/chunk.js";
4
4
  import { hasControlCommand } from "../../auto-reply/command-detection.js";
5
- import { formatInboundEnvelope, formatInboundFromLabel, resolveEnvelopeFormatOptions, } from "../../auto-reply/envelope.js";
6
- import { createInboundDebouncer, resolveInboundDebounceMs, } from "../../auto-reply/inbound-debounce.js";
7
5
  import { dispatchInboundMessage } from "../../auto-reply/dispatch.js";
8
- import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js";
9
- import { buildPendingHistoryContextFromMap, clearHistoryEntriesIfEnabled, DEFAULT_GROUP_HISTORY_LIMIT, recordPendingHistoryEntryIfEnabled, } from "../../auto-reply/reply/history.js";
10
- import { buildMentionRegexes, matchesMentionPatterns } from "../../auto-reply/reply/mentions.js";
6
+ import { createInboundDebouncer, resolveInboundDebounceMs, } from "../../auto-reply/inbound-debounce.js";
7
+ import { clearHistoryEntriesIfEnabled, DEFAULT_GROUP_HISTORY_LIMIT, } from "../../auto-reply/reply/history.js";
11
8
  import { createReplyDispatcher } from "../../auto-reply/reply/reply-dispatcher.js";
12
- import { logInboundDrop } from "../../channels/logging.js";
13
- import { createReplyPrefixContext } from "../../channels/reply-prefix.js";
9
+ import { createReplyPrefixOptions } from "../../channels/reply-prefix.js";
14
10
  import { recordInboundSession } from "../../channels/session.js";
15
11
  import { loadConfig } from "../../config/config.js";
16
- import { resolveChannelGroupPolicy, resolveChannelGroupRequireMention, } from "../../config/group-policy.js";
17
12
  import { readSessionUpdatedAt, resolveStorePath } from "../../config/sessions.js";
18
13
  import { danger, logVerbose, shouldLogVerbose } from "../../globals.js";
19
14
  import { waitForTransportReady } from "../../infra/transport-ready.js";
20
15
  import { mediaKindFromMime } from "../../media/constants.js";
21
16
  import { buildPairingReply } from "../../pairing/pairing-messages.js";
22
17
  import { readChannelAllowFromStore, upsertChannelPairingRequest, } from "../../pairing/pairing-store.js";
23
- import { resolveAgentRoute } from "../../routing/resolve-route.js";
24
18
  import { truncateUtf16Safe } from "../../utils.js";
25
- import { resolveControlCommandGate } from "../../channels/command-gating.js";
26
19
  import { resolveIMessageAccount } from "../accounts.js";
27
20
  import { createIMessageRpcClient } from "../client.js";
28
21
  import { DEFAULT_IMESSAGE_PROBE_TIMEOUT_MS } from "../constants.js";
29
22
  import { probeIMessage } from "../probe.js";
30
23
  import { sendMessageIMessage } from "../send.js";
31
- import { formatIMessageChatTarget, isAllowedIMessageSender, normalizeIMessageHandle, } from "../targets.js";
24
+ import { attachIMessageMonitorAbortHandler } from "./abort-handler.js";
32
25
  import { deliverReplies } from "./deliver.js";
26
+ import { buildIMessageInboundContext, resolveIMessageInboundDecision, } from "./inbound-processing.js";
27
+ import { parseIMessageNotification } from "./parse-notification.js";
33
28
  import { normalizeAllowList, resolveRuntime } from "./runtime.js";
34
29
  /**
35
30
  * Try to detect remote host from an SSH wrapper script like:
@@ -46,8 +41,9 @@ async function detectRemoteHostFromCliPath(cliPath) {
46
41
  const content = await fs.readFile(expanded, "utf8");
47
42
  // Match user@host pattern first (e.g., poolbot@192.168.64.3)
48
43
  const userHostMatch = content.match(/\bssh\b[^\n]*?\s+([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+)/);
49
- if (userHostMatch)
44
+ if (userHostMatch) {
50
45
  return userHostMatch[1];
46
+ }
51
47
  // Fallback: match host-only before imsg command (e.g., ssh -T mac-mini imsg)
52
48
  const hostOnlyMatch = content.match(/\bssh\b[^\n]*?\s+([a-zA-Z][a-zA-Z0-9._-]*)\s+\S*\bimsg\b/);
53
49
  return hostOnlyMatch?.[1];
@@ -56,40 +52,31 @@ async function detectRemoteHostFromCliPath(cliPath) {
56
52
  return undefined;
57
53
  }
58
54
  }
59
- function normalizeReplyField(value) {
60
- if (typeof value === "string") {
61
- const trimmed = value.trim();
62
- return trimmed ? trimmed : undefined;
63
- }
64
- if (typeof value === "number")
65
- return String(value);
66
- return undefined;
67
- }
68
- function describeReplyContext(message) {
69
- const body = normalizeReplyField(message.reply_to_text);
70
- if (!body)
71
- return null;
72
- const id = normalizeReplyField(message.reply_to_id);
73
- const sender = normalizeReplyField(message.reply_to_sender);
74
- return { body, id, sender };
75
- }
55
+ /**
56
+ * Cache for recently sent messages, used for echo detection.
57
+ * Keys are scoped by conversation (accountId:target) so the same text in different chats is not conflated.
58
+ * Entries expire after 5 seconds; we do not forget on match so multiple echo deliveries are all filtered.
59
+ */
76
60
  class SentMessageCache {
77
61
  cache = new Map();
78
- ttlMs = 5000;
62
+ ttlMs = 5000; // 5 seconds
79
63
  remember(scope, text) {
80
- if (!text?.trim())
64
+ if (!text?.trim()) {
81
65
  return;
66
+ }
82
67
  const key = `${scope}:${text.trim()}`;
83
68
  this.cache.set(key, Date.now());
84
69
  this.cleanup();
85
70
  }
86
71
  has(scope, text) {
87
- if (!text?.trim())
72
+ if (!text?.trim()) {
88
73
  return false;
74
+ }
89
75
  const key = `${scope}:${text.trim()}`;
90
76
  const timestamp = this.cache.get(key);
91
- if (!timestamp)
77
+ if (!timestamp) {
92
78
  return false;
79
+ }
93
80
  const age = Date.now() - timestamp;
94
81
  if (age > this.ttlMs) {
95
82
  this.cache.delete(key);
@@ -100,8 +87,9 @@ class SentMessageCache {
100
87
  cleanup() {
101
88
  const now = Date.now();
102
89
  for (const [text, timestamp] of this.cache.entries()) {
103
- if (now - timestamp > this.ttlMs)
90
+ if (now - timestamp > this.ttlMs) {
104
91
  this.cache.delete(text);
92
+ }
105
93
  }
106
94
  }
107
95
  }
@@ -143,18 +131,10 @@ export async function monitorIMessageProvider(opts = {}) {
143
131
  const inboundDebouncer = createInboundDebouncer({
144
132
  debounceMs: inboundDebounceMs,
145
133
  buildKey: (entry) => {
146
- // Use message-specific key to preserve attachments
147
- // This prevents different messages from being merged incorrectly
148
134
  const sender = entry.message.sender?.trim();
149
- if (!sender)
135
+ if (!sender) {
150
136
  return null;
151
- // Prefer message id for unique identification (prevents cross-message contamination)
152
- // If id is not available, fall back to conversation-based key
153
- const messageId = entry.message.id;
154
- if (messageId != null) {
155
- return `imessage:${accountInfo.accountId}:msg:${messageId}`;
156
137
  }
157
- // Fallback to conversation-based key only when id is unavailable
158
138
  const conversationId = entry.message.chat_id != null
159
139
  ? `chat:${entry.message.chat_id}`
160
140
  : (entry.message.chat_guid ?? entry.message.chat_identifier ?? "unknown");
@@ -162,18 +142,19 @@ export async function monitorIMessageProvider(opts = {}) {
162
142
  },
163
143
  shouldDebounce: (entry) => {
164
144
  const text = entry.message.text?.trim() ?? "";
165
- if (!text)
145
+ if (!text) {
166
146
  return false;
167
- // Messages with attachments should not be debounced across message boundaries
168
- // to preserve attachment context
169
- if (entry.message.attachments && entry.message.attachments.length > 0)
147
+ }
148
+ if (entry.message.attachments && entry.message.attachments.length > 0) {
170
149
  return false;
150
+ }
171
151
  return !hasControlCommand(text, cfg);
172
152
  },
173
153
  onFlush: async (entries) => {
174
154
  const last = entries.at(-1);
175
- if (!last)
155
+ if (!last) {
176
156
  return;
157
+ }
177
158
  if (entries.length === 1) {
178
159
  await handleMessageNow(last.message);
179
160
  return;
@@ -185,9 +166,7 @@ export async function monitorIMessageProvider(opts = {}) {
185
166
  const syntheticMessage = {
186
167
  ...last.message,
187
168
  text: combinedText,
188
- // Preserve attachments from the last message in the batch
189
- // (should always be null here due to shouldDebounce guard, but keep for safety)
190
- attachments: last.message.attachments ?? null,
169
+ attachments: null,
191
170
  };
192
171
  await handleMessageNow(syntheticMessage);
193
172
  },
@@ -196,140 +175,7 @@ export async function monitorIMessageProvider(opts = {}) {
196
175
  },
197
176
  });
198
177
  async function handleMessageNow(message) {
199
- const senderRaw = message.sender ?? "";
200
- const sender = senderRaw.trim();
201
- if (!sender)
202
- return;
203
- const senderNormalized = normalizeIMessageHandle(sender);
204
- if (message.is_from_me)
205
- return;
206
- const chatId = message.chat_id ?? undefined;
207
- const chatGuid = message.chat_guid ?? undefined;
208
- const chatIdentifier = message.chat_identifier ?? undefined;
209
- const groupIdCandidate = chatId !== undefined ? String(chatId) : undefined;
210
- const groupListPolicy = groupIdCandidate
211
- ? resolveChannelGroupPolicy({
212
- cfg,
213
- channel: "imessage",
214
- accountId: accountInfo.accountId,
215
- groupId: groupIdCandidate,
216
- })
217
- : {
218
- allowlistEnabled: false,
219
- allowed: true,
220
- groupConfig: undefined,
221
- defaultConfig: undefined,
222
- };
223
- // Some iMessage threads can have multiple participants but still report
224
- // is_group=false depending on how Messages stores the identifier.
225
- // If the owner explicitly configures a chat_id under imessage.groups, treat
226
- // that thread as a "group" for permission gating and session isolation.
227
- const treatAsGroupByConfig = Boolean(groupIdCandidate && groupListPolicy.allowlistEnabled && groupListPolicy.groupConfig);
228
- const isGroup = Boolean(message.is_group) || treatAsGroupByConfig;
229
- if (isGroup && !chatId)
230
- return;
231
- const groupId = isGroup ? groupIdCandidate : undefined;
232
- const storeAllowFrom = await readChannelAllowFromStore("imessage").catch(() => []);
233
- const effectiveDmAllowFrom = Array.from(new Set([...allowFrom, ...storeAllowFrom]))
234
- .map((v) => String(v).trim())
235
- .filter(Boolean);
236
- const effectiveGroupAllowFrom = Array.from(new Set([...groupAllowFrom, ...storeAllowFrom]))
237
- .map((v) => String(v).trim())
238
- .filter(Boolean);
239
- if (isGroup) {
240
- if (groupPolicy === "disabled") {
241
- logVerbose("Blocked iMessage group message (groupPolicy: disabled)");
242
- return;
243
- }
244
- if (groupPolicy === "allowlist") {
245
- if (effectiveGroupAllowFrom.length === 0) {
246
- logVerbose("Blocked iMessage group message (groupPolicy: allowlist, no groupAllowFrom)");
247
- return;
248
- }
249
- const allowed = isAllowedIMessageSender({
250
- allowFrom: effectiveGroupAllowFrom,
251
- sender,
252
- chatId: chatId ?? undefined,
253
- chatGuid,
254
- chatIdentifier,
255
- });
256
- if (!allowed) {
257
- logVerbose(`Blocked iMessage sender ${sender} (not in groupAllowFrom)`);
258
- return;
259
- }
260
- }
261
- if (groupListPolicy.allowlistEnabled && !groupListPolicy.allowed) {
262
- logVerbose(`imessage: skipping group message (${groupId ?? "unknown"}) not in allowlist`);
263
- return;
264
- }
265
- }
266
- const dmHasWildcard = effectiveDmAllowFrom.includes("*");
267
- const dmAuthorized = dmPolicy === "open"
268
- ? true
269
- : dmHasWildcard ||
270
- (effectiveDmAllowFrom.length > 0 &&
271
- isAllowedIMessageSender({
272
- allowFrom: effectiveDmAllowFrom,
273
- sender,
274
- chatId: chatId ?? undefined,
275
- chatGuid,
276
- chatIdentifier,
277
- }));
278
- if (!isGroup) {
279
- if (dmPolicy === "disabled")
280
- return;
281
- if (!dmAuthorized) {
282
- if (dmPolicy === "pairing") {
283
- const senderId = normalizeIMessageHandle(sender);
284
- const { code, created } = await upsertChannelPairingRequest({
285
- channel: "imessage",
286
- id: senderId,
287
- meta: {
288
- sender: senderId,
289
- chatId: chatId ? String(chatId) : undefined,
290
- },
291
- });
292
- if (created) {
293
- logVerbose(`imessage pairing request sender=${senderId}`);
294
- try {
295
- await sendMessageIMessage(sender, buildPairingReply({
296
- channel: "imessage",
297
- idLine: `Your iMessage sender id: ${senderId}`,
298
- code,
299
- }), {
300
- client,
301
- maxBytes: mediaMaxBytes,
302
- accountId: accountInfo.accountId,
303
- ...(chatId ? { chatId } : {}),
304
- });
305
- }
306
- catch (err) {
307
- logVerbose(`imessage pairing reply failed for ${senderId}: ${String(err)}`);
308
- }
309
- }
310
- }
311
- else {
312
- logVerbose(`Blocked iMessage sender ${sender} (dmPolicy=${dmPolicy})`);
313
- }
314
- return;
315
- }
316
- }
317
- const route = resolveAgentRoute({
318
- cfg,
319
- channel: "imessage",
320
- accountId: accountInfo.accountId,
321
- peer: {
322
- kind: isGroup ? "group" : "dm",
323
- id: isGroup ? String(chatId ?? "unknown") : normalizeIMessageHandle(sender),
324
- },
325
- });
326
- const mentionRegexes = buildMentionRegexes(cfg, route.agentId);
327
178
  const messageText = (message.text ?? "").trim();
328
- const echoScope = `${accountInfo.accountId}:${isGroup ? formatIMessageChatTarget(chatId) : `imessage:${sender}`}`;
329
- if (messageText && sentMessageCache.has(echoScope, messageText)) {
330
- logVerbose(`imessage: skipping echo message (matches recently sent text within 5s): "${truncateUtf16Safe(messageText, 50)}"`);
331
- return;
332
- }
333
179
  const attachments = includeAttachments ? (message.attachments ?? []) : [];
334
180
  // Filter to valid attachments with paths
335
181
  const validAttachments = attachments.filter((entry) => entry?.original_path && !entry?.missing);
@@ -342,184 +188,94 @@ export async function monitorIMessageProvider(opts = {}) {
342
188
  const kind = mediaKindFromMime(mediaType ?? undefined);
343
189
  const placeholder = kind ? `<media:${kind}>` : attachments?.length ? "<media:attachment>" : "";
344
190
  const bodyText = messageText || placeholder;
345
- if (!bodyText)
346
- return;
347
- const replyContext = describeReplyContext(message);
348
- const createdAt = message.created_at ? Date.parse(message.created_at) : undefined;
349
- const historyKey = isGroup
350
- ? String(chatId ?? chatGuid ?? chatIdentifier ?? "unknown")
351
- : undefined;
352
- const mentioned = isGroup ? matchesMentionPatterns(messageText, mentionRegexes) : true;
353
- const requireMention = resolveChannelGroupRequireMention({
191
+ const storeAllowFrom = await readChannelAllowFromStore("imessage").catch(() => []);
192
+ const decision = resolveIMessageInboundDecision({
354
193
  cfg,
355
- channel: "imessage",
356
194
  accountId: accountInfo.accountId,
357
- groupId,
358
- requireMentionOverride: opts.requireMention,
359
- overrideOrder: "before-config",
195
+ message,
196
+ opts,
197
+ messageText,
198
+ bodyText,
199
+ allowFrom,
200
+ groupAllowFrom,
201
+ groupPolicy,
202
+ dmPolicy,
203
+ storeAllowFrom,
204
+ historyLimit,
205
+ groupHistories,
206
+ echoCache: sentMessageCache,
207
+ logVerbose,
360
208
  });
361
- const canDetectMention = mentionRegexes.length > 0;
362
- const useAccessGroups = cfg.commands?.useAccessGroups !== false;
363
- const ownerAllowedForCommands = effectiveDmAllowFrom.length > 0
364
- ? isAllowedIMessageSender({
365
- allowFrom: effectiveDmAllowFrom,
366
- sender,
367
- chatId: chatId ?? undefined,
368
- chatGuid,
369
- chatIdentifier,
370
- })
371
- : false;
372
- const groupAllowedForCommands = effectiveGroupAllowFrom.length > 0
373
- ? isAllowedIMessageSender({
374
- allowFrom: effectiveGroupAllowFrom,
375
- sender,
376
- chatId: chatId ?? undefined,
377
- chatGuid,
378
- chatIdentifier,
379
- })
380
- : false;
381
- const hasControlCommandInMessage = hasControlCommand(messageText, cfg);
382
- const commandGate = resolveControlCommandGate({
383
- useAccessGroups,
384
- authorizers: [
385
- { configured: effectiveDmAllowFrom.length > 0, allowed: ownerAllowedForCommands },
386
- { configured: effectiveGroupAllowFrom.length > 0, allowed: groupAllowedForCommands },
387
- ],
388
- allowTextCommands: true,
389
- hasControlCommand: hasControlCommandInMessage,
390
- });
391
- const commandAuthorized = isGroup ? commandGate.commandAuthorized : dmAuthorized;
392
- if (isGroup && commandGate.shouldBlock) {
393
- logInboundDrop({
394
- log: logVerbose,
395
- channel: "imessage",
396
- reason: "control command (unauthorized)",
397
- target: sender,
398
- });
209
+ if (decision.kind === "drop") {
399
210
  return;
400
211
  }
401
- const shouldBypassMention = isGroup && requireMention && !mentioned && commandAuthorized && hasControlCommandInMessage;
402
- const effectiveWasMentioned = mentioned || shouldBypassMention;
403
- if (isGroup && requireMention && canDetectMention && !mentioned && !shouldBypassMention) {
404
- logVerbose(`imessage: skipping group message (no mention)`);
405
- recordPendingHistoryEntryIfEnabled({
406
- historyMap: groupHistories,
407
- historyKey: historyKey ?? "",
408
- limit: historyLimit,
409
- entry: historyKey
410
- ? {
411
- sender: senderNormalized,
412
- body: bodyText,
413
- timestamp: createdAt,
414
- messageId: message.id ? String(message.id) : undefined,
415
- }
416
- : null,
212
+ const chatId = message.chat_id ?? undefined;
213
+ if (decision.kind === "pairing") {
214
+ const sender = (message.sender ?? "").trim();
215
+ if (!sender) {
216
+ return;
217
+ }
218
+ const { code, created } = await upsertChannelPairingRequest({
219
+ channel: "imessage",
220
+ id: decision.senderId,
221
+ meta: {
222
+ sender: decision.senderId,
223
+ chatId: chatId ? String(chatId) : undefined,
224
+ },
417
225
  });
226
+ if (created) {
227
+ logVerbose(`imessage pairing request sender=${decision.senderId}`);
228
+ try {
229
+ await sendMessageIMessage(sender, buildPairingReply({
230
+ channel: "imessage",
231
+ idLine: `Your iMessage sender id: ${decision.senderId}`,
232
+ code,
233
+ }), {
234
+ client,
235
+ maxBytes: mediaMaxBytes,
236
+ accountId: accountInfo.accountId,
237
+ ...(chatId ? { chatId } : {}),
238
+ });
239
+ }
240
+ catch (err) {
241
+ logVerbose(`imessage pairing reply failed for ${decision.senderId}: ${String(err)}`);
242
+ }
243
+ }
418
244
  return;
419
245
  }
420
- const chatTarget = formatIMessageChatTarget(chatId);
421
- const fromLabel = formatInboundFromLabel({
422
- isGroup,
423
- groupLabel: message.chat_name ?? undefined,
424
- groupId: chatId !== undefined ? String(chatId) : "unknown",
425
- groupFallback: "Group",
426
- directLabel: senderNormalized,
427
- directId: sender,
428
- });
429
246
  const storePath = resolveStorePath(cfg.session?.store, {
430
- agentId: route.agentId,
247
+ agentId: decision.route.agentId,
431
248
  });
432
- const envelopeOptions = resolveEnvelopeFormatOptions(cfg);
433
249
  const previousTimestamp = readSessionUpdatedAt({
434
250
  storePath,
435
- sessionKey: route.sessionKey,
251
+ sessionKey: decision.route.sessionKey,
436
252
  });
437
- const replySuffix = replyContext
438
- ? `\n\n[Replying to ${replyContext.sender ?? "unknown sender"}${replyContext.id ? ` id:${replyContext.id}` : ""}]\n${replyContext.body}\n[/Replying]`
439
- : "";
440
- const body = formatInboundEnvelope({
441
- channel: "iMessage",
442
- from: fromLabel,
443
- timestamp: createdAt,
444
- body: `${bodyText}${replySuffix}`,
445
- chatType: isGroup ? "group" : "direct",
446
- sender: { name: senderNormalized, id: sender },
253
+ const { ctxPayload, chatTarget } = buildIMessageInboundContext({
254
+ cfg,
255
+ decision,
256
+ message,
447
257
  previousTimestamp,
448
- envelope: envelopeOptions,
449
- });
450
- let combinedBody = body;
451
- if (isGroup && historyKey) {
452
- combinedBody = buildPendingHistoryContextFromMap({
453
- historyMap: groupHistories,
454
- historyKey,
455
- limit: historyLimit,
456
- currentMessage: combinedBody,
457
- formatEntry: (entry) => formatInboundEnvelope({
458
- channel: "iMessage",
459
- from: fromLabel,
460
- timestamp: entry.timestamp,
461
- body: `${entry.body}${entry.messageId ? ` [id:${entry.messageId}]` : ""}`,
462
- chatType: "group",
463
- senderLabel: entry.sender,
464
- envelope: envelopeOptions,
465
- }),
466
- });
467
- }
468
- const imessageTo = (isGroup ? chatTarget : undefined) || `imessage:${sender}`;
469
- const inboundHistory = isGroup && historyKey && historyLimit > 0
470
- ? (groupHistories.get(historyKey) ?? []).map((entry) => ({
471
- sender: entry.sender,
472
- body: entry.body,
473
- timestamp: entry.timestamp,
474
- }))
475
- : undefined;
476
- const ctxPayload = finalizeInboundContext({
477
- Body: combinedBody,
478
- BodyForAgent: bodyText,
479
- InboundHistory: inboundHistory,
480
- RawBody: bodyText,
481
- CommandBody: bodyText,
482
- From: isGroup ? `imessage:group:${chatId ?? "unknown"}` : `imessage:${sender}`,
483
- To: imessageTo,
484
- SessionKey: route.sessionKey,
485
- AccountId: route.accountId,
486
- ChatType: isGroup ? "group" : "direct",
487
- ConversationLabel: fromLabel,
488
- GroupSubject: isGroup ? (message.chat_name ?? undefined) : undefined,
489
- GroupMembers: isGroup ? (message.participants ?? []).filter(Boolean).join(", ") : undefined,
490
- SenderName: senderNormalized,
491
- SenderId: sender,
492
- Provider: "imessage",
493
- Surface: "imessage",
494
- MessageSid: message.id ? String(message.id) : undefined,
495
- ReplyToId: replyContext?.id,
496
- ReplyToBody: replyContext?.body,
497
- ReplyToSender: replyContext?.sender,
498
- Timestamp: createdAt,
499
- MediaPath: mediaPath,
500
- MediaType: mediaType,
501
- MediaUrl: mediaPath,
502
- MediaPaths: mediaPaths.length > 0 ? mediaPaths : undefined,
503
- MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined,
504
- MediaUrls: mediaPaths.length > 0 ? mediaPaths : undefined,
505
- MediaRemoteHost: remoteHost,
506
- WasMentioned: effectiveWasMentioned,
507
- CommandAuthorized: commandAuthorized,
508
- // Originating channel for reply routing.
509
- OriginatingChannel: "imessage",
510
- OriginatingTo: imessageTo,
258
+ remoteHost,
259
+ historyLimit,
260
+ groupHistories,
261
+ media: {
262
+ path: mediaPath,
263
+ type: mediaType,
264
+ paths: mediaPaths,
265
+ types: mediaTypes,
266
+ },
511
267
  });
512
- const updateTarget = (isGroup ? chatTarget : undefined) || sender;
268
+ const updateTarget = chatTarget || decision.sender;
513
269
  await recordInboundSession({
514
270
  storePath,
515
- sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
271
+ sessionKey: ctxPayload.SessionKey ?? decision.route.sessionKey,
516
272
  ctx: ctxPayload,
517
- updateLastRoute: !isGroup && updateTarget
273
+ updateLastRoute: !decision.isGroup && updateTarget
518
274
  ? {
519
- sessionKey: route.mainSessionKey,
275
+ sessionKey: decision.route.mainSessionKey,
520
276
  channel: "imessage",
521
277
  to: updateTarget,
522
- accountId: route.accountId,
278
+ accountId: decision.route.accountId,
523
279
  }
524
280
  : undefined,
525
281
  onRecordError: (err) => {
@@ -527,18 +283,27 @@ export async function monitorIMessageProvider(opts = {}) {
527
283
  },
528
284
  });
529
285
  if (shouldLogVerbose()) {
530
- const preview = truncateUtf16Safe(body, 200).replace(/\n/g, "\\n");
531
- logVerbose(`imessage inbound: chatId=${chatId ?? "unknown"} from=${ctxPayload.From} len=${body.length} preview="${preview}"`);
286
+ const preview = truncateUtf16Safe(String(ctxPayload.Body ?? ""), 200).replace(/\n/g, "\\n");
287
+ logVerbose(`imessage inbound: chatId=${chatId ?? "unknown"} from=${ctxPayload.From} len=${String(ctxPayload.Body ?? "").length} preview="${preview}"`);
532
288
  }
533
- const prefixContext = createReplyPrefixContext({ cfg, agentId: route.agentId });
289
+ const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
290
+ cfg,
291
+ agentId: decision.route.agentId,
292
+ channel: "imessage",
293
+ accountId: decision.route.accountId,
294
+ });
534
295
  const dispatcher = createReplyDispatcher({
535
- responsePrefix: prefixContext.responsePrefix,
536
- responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
537
- humanDelay: resolveHumanDelayConfig(cfg, route.agentId),
296
+ ...prefixOptions,
297
+ humanDelay: resolveHumanDelayConfig(cfg, decision.route.agentId),
538
298
  deliver: async (payload) => {
299
+ const target = ctxPayload.To;
300
+ if (!target) {
301
+ runtime.error?.(danger("imessage: missing delivery target"));
302
+ return;
303
+ }
539
304
  await deliverReplies({
540
305
  replies: [payload],
541
- target: ctxPayload.To,
306
+ target,
542
307
  client,
543
308
  accountId: accountInfo.accountId,
544
309
  runtime,
@@ -559,28 +324,33 @@ export async function monitorIMessageProvider(opts = {}) {
559
324
  disableBlockStreaming: typeof accountInfo.config.blockStreaming === "boolean"
560
325
  ? !accountInfo.config.blockStreaming
561
326
  : undefined,
562
- onModelSelected: prefixContext.onModelSelected,
327
+ onModelSelected,
563
328
  },
564
329
  });
565
330
  if (!queuedFinal) {
566
- if (isGroup && historyKey) {
331
+ if (decision.isGroup && decision.historyKey) {
567
332
  clearHistoryEntriesIfEnabled({
568
333
  historyMap: groupHistories,
569
- historyKey,
334
+ historyKey: decision.historyKey,
570
335
  limit: historyLimit,
571
336
  });
572
337
  }
573
338
  return;
574
339
  }
575
- if (isGroup && historyKey) {
576
- clearHistoryEntriesIfEnabled({ historyMap: groupHistories, historyKey, limit: historyLimit });
340
+ if (decision.isGroup && decision.historyKey) {
341
+ clearHistoryEntriesIfEnabled({
342
+ historyMap: groupHistories,
343
+ historyKey: decision.historyKey,
344
+ limit: historyLimit,
345
+ });
577
346
  }
578
347
  }
579
348
  const handleMessage = async (raw) => {
580
- const params = raw;
581
- const message = params?.message ?? null;
582
- if (!message)
349
+ const message = parseIMessageNotification(raw);
350
+ if (!message) {
351
+ logVerbose("imessage: dropping malformed RPC message payload");
583
352
  return;
353
+ }
584
354
  await inboundDebouncer.enqueue({ message });
585
355
  };
586
356
  await waitForTransportReady({
@@ -593,16 +363,18 @@ export async function monitorIMessageProvider(opts = {}) {
593
363
  runtime,
594
364
  check: async () => {
595
365
  const probe = await probeIMessage(probeTimeoutMs, { cliPath, dbPath, runtime });
596
- if (probe.ok)
366
+ if (probe.ok) {
597
367
  return { ok: true };
368
+ }
598
369
  if (probe.fatal) {
599
370
  throw new Error(probe.error ?? "imsg rpc unavailable");
600
371
  }
601
372
  return { ok: false, error: probe.error ?? "unreachable" };
602
373
  },
603
374
  });
604
- if (opts.abortSignal?.aborted)
375
+ if (opts.abortSignal?.aborted) {
605
376
  return;
377
+ }
606
378
  const client = await createIMessageRpcClient({
607
379
  cliPath,
608
380
  dbPath,
@@ -620,21 +392,11 @@ export async function monitorIMessageProvider(opts = {}) {
620
392
  });
621
393
  let subscriptionId = null;
622
394
  const abort = opts.abortSignal;
623
- const onAbort = () => {
624
- if (subscriptionId) {
625
- void client
626
- .request("watch.unsubscribe", {
627
- subscription: subscriptionId,
628
- })
629
- .catch(() => {
630
- // Ignore disconnect errors during shutdown.
631
- });
632
- }
633
- void client.stop().catch(() => {
634
- // Ignore disconnect errors during shutdown.
635
- });
636
- };
637
- abort?.addEventListener("abort", onAbort, { once: true });
395
+ const detachAbortHandler = attachIMessageMonitorAbortHandler({
396
+ abortSignal: abort,
397
+ client,
398
+ getSubscriptionId: () => subscriptionId,
399
+ });
638
400
  try {
639
401
  const result = await client.request("watch.subscribe", {
640
402
  attachments: includeAttachments,
@@ -643,13 +405,14 @@ export async function monitorIMessageProvider(opts = {}) {
643
405
  await client.waitForClose();
644
406
  }
645
407
  catch (err) {
646
- if (abort?.aborted)
408
+ if (abort?.aborted) {
647
409
  return;
410
+ }
648
411
  runtime.error?.(danger(`imessage: monitor failed: ${String(err)}`));
649
412
  throw err;
650
413
  }
651
414
  finally {
652
- abort?.removeEventListener("abort", onAbort);
415
+ detachAbortHandler();
653
416
  await client.stop();
654
417
  }
655
418
  }