@selesai/code 0.5.28 → 0.5.29

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 (307) hide show
  1. package/dist/cli/args.d.ts.map +1 -1
  2. package/dist/cli/args.js +9 -1
  3. package/dist/cli/args.js.map +1 -1
  4. package/dist/cli/config-selector.d.ts.map +1 -1
  5. package/dist/cli/config-selector.js +1 -1
  6. package/dist/cli/config-selector.js.map +1 -1
  7. package/dist/cli/credential-print.d.ts +23 -0
  8. package/dist/cli/credential-print.d.ts.map +1 -0
  9. package/dist/cli/credential-print.js +117 -0
  10. package/dist/cli/credential-print.js.map +1 -0
  11. package/dist/cli/startup-ui.d.ts.map +1 -1
  12. package/dist/cli/startup-ui.js +1 -1
  13. package/dist/cli/startup-ui.js.map +1 -1
  14. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  15. package/dist/core/agent-session-runtime.js +3 -0
  16. package/dist/core/agent-session-runtime.js.map +1 -1
  17. package/dist/core/agent-session.d.ts +12 -1
  18. package/dist/core/agent-session.d.ts.map +1 -1
  19. package/dist/core/agent-session.js +20 -14
  20. package/dist/core/agent-session.js.map +1 -1
  21. package/dist/core/compaction/compaction.d.ts.map +1 -1
  22. package/dist/core/compaction/compaction.js +11 -3
  23. package/dist/core/compaction/compaction.js.map +1 -1
  24. package/dist/core/extensions/runner.d.ts +1 -0
  25. package/dist/core/extensions/runner.d.ts.map +1 -1
  26. package/dist/core/extensions/runner.js +11 -0
  27. package/dist/core/extensions/runner.js.map +1 -1
  28. package/dist/core/extensions/types.d.ts +14 -1
  29. package/dist/core/extensions/types.d.ts.map +1 -1
  30. package/dist/core/extensions/types.js.map +1 -1
  31. package/dist/core/footer-data-provider.d.ts +10 -0
  32. package/dist/core/footer-data-provider.d.ts.map +1 -1
  33. package/dist/core/footer-data-provider.js +1 -1
  34. package/dist/core/footer-data-provider.js.map +1 -1
  35. package/dist/core/llama/provider.d.ts.map +1 -1
  36. package/dist/core/llama/provider.js +8 -3
  37. package/dist/core/llama/provider.js.map +1 -1
  38. package/dist/core/model-config.d.ts +30 -0
  39. package/dist/core/model-config.d.ts.map +1 -1
  40. package/dist/core/model-config.js +6 -0
  41. package/dist/core/model-config.js.map +1 -1
  42. package/dist/core/model-registry.d.ts.map +1 -1
  43. package/dist/core/model-registry.js +2 -2
  44. package/dist/core/model-registry.js.map +1 -1
  45. package/dist/core/model-resolver.d.ts +1 -0
  46. package/dist/core/model-resolver.d.ts.map +1 -1
  47. package/dist/core/model-resolver.js +20 -3
  48. package/dist/core/model-resolver.js.map +1 -1
  49. package/dist/core/model-runtime.d.ts +2 -0
  50. package/dist/core/model-runtime.d.ts.map +1 -1
  51. package/dist/core/model-runtime.js +5 -4
  52. package/dist/core/model-runtime.js.map +1 -1
  53. package/dist/core/package-manager.d.ts.map +1 -1
  54. package/dist/core/package-manager.js +13 -6
  55. package/dist/core/package-manager.js.map +1 -1
  56. package/dist/core/remote-catalog-provider.d.ts +1 -1
  57. package/dist/core/remote-catalog-provider.d.ts.map +1 -1
  58. package/dist/core/remote-catalog-provider.js +24 -11
  59. package/dist/core/remote-catalog-provider.js.map +1 -1
  60. package/dist/core/resource-loader.d.ts +15 -0
  61. package/dist/core/resource-loader.d.ts.map +1 -1
  62. package/dist/core/resource-loader.js +66 -9
  63. package/dist/core/resource-loader.js.map +1 -1
  64. package/dist/core/settings-manager.d.ts +1 -1
  65. package/dist/core/settings-manager.d.ts.map +1 -1
  66. package/dist/core/settings-manager.js.map +1 -1
  67. package/dist/core/system-prompt.d.ts.map +1 -1
  68. package/dist/core/system-prompt.js +1 -1
  69. package/dist/core/system-prompt.js.map +1 -1
  70. package/dist/core/tools/bash.d.ts +2 -0
  71. package/dist/core/tools/bash.d.ts.map +1 -1
  72. package/dist/core/tools/bash.js +34 -5
  73. package/dist/core/tools/bash.js.map +1 -1
  74. package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -1
  75. package/dist/core/tools/tool-definition-wrapper.js +3 -1
  76. package/dist/core/tools/tool-definition-wrapper.js.map +1 -1
  77. package/dist/extensions/pi-intercom/CHANGELOG.md +249 -0
  78. package/dist/extensions/pi-intercom/README.md +102 -39
  79. package/dist/extensions/pi-intercom/broker/broker.ts +1233 -36
  80. package/dist/extensions/pi-intercom/broker/client.test.ts +83 -0
  81. package/dist/extensions/pi-intercom/broker/client.ts +315 -12
  82. package/dist/extensions/pi-intercom/broker/extension-state.ts +186 -0
  83. package/dist/extensions/pi-intercom/broker/extension.test.ts +387 -0
  84. package/dist/extensions/pi-intercom/broker/framing.test.ts +114 -0
  85. package/dist/extensions/pi-intercom/broker/framing.ts +82 -24
  86. package/dist/extensions/pi-intercom/broker/paths.test.ts +153 -0
  87. package/dist/extensions/pi-intercom/broker/paths.ts +117 -8
  88. package/dist/extensions/pi-intercom/broker/runtime-claim.test.ts +34 -0
  89. package/dist/extensions/pi-intercom/broker/runtime-claim.ts +21 -0
  90. package/dist/extensions/pi-intercom/broker/spawn.test.ts +160 -23
  91. package/dist/extensions/pi-intercom/broker/spawn.ts +113 -27
  92. package/dist/extensions/pi-intercom/config.test.ts +93 -0
  93. package/dist/extensions/pi-intercom/config.ts +55 -6
  94. package/dist/extensions/pi-intercom/cwd.test.ts +40 -0
  95. package/dist/extensions/pi-intercom/cwd.ts +31 -0
  96. package/dist/extensions/pi-intercom/extension-api.ts +44 -0
  97. package/dist/extensions/pi-intercom/format-context.test.ts +31 -0
  98. package/dist/extensions/pi-intercom/format-context.ts +32 -0
  99. package/dist/extensions/pi-intercom/index.ts +742 -145
  100. package/dist/extensions/pi-intercom/intercom.integration.test.ts +2646 -0
  101. package/dist/extensions/pi-intercom/package.json +15 -5
  102. package/dist/extensions/pi-intercom/reply-tracker.test.ts +134 -0
  103. package/dist/extensions/pi-intercom/reply-tracker.ts +31 -13
  104. package/dist/extensions/pi-intercom/skills/pi-intercom/SKILL.md +13 -11
  105. package/dist/extensions/pi-intercom/test/inline-message.test.ts +184 -0
  106. package/dist/extensions/pi-intercom/test/overlay-width.test.ts +66 -0
  107. package/dist/extensions/pi-intercom/types.ts +94 -4
  108. package/dist/extensions/pi-intercom/ui/compose.ts +8 -4
  109. package/dist/extensions/pi-intercom/ui/inline-message.ts +61 -25
  110. package/dist/extensions/pi-intercom/ui/session-list.ts +7 -3
  111. package/dist/extensions/pi-subagents/CHANGELOG.md +85 -0
  112. package/dist/extensions/pi-subagents/LICENSE +21 -0
  113. package/dist/extensions/pi-subagents/README.md +146 -52
  114. package/dist/extensions/pi-subagents/agents/architect.md +1 -0
  115. package/dist/extensions/pi-subagents/agents/builder.md +1 -0
  116. package/dist/extensions/pi-subagents/package-lock.json +2 -2
  117. package/dist/extensions/pi-subagents/package.json +3 -3
  118. package/dist/extensions/pi-subagents/prompts/review-loop.md +1 -1
  119. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +21 -989
  120. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +256 -0
  121. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +430 -0
  122. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +144 -0
  123. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +282 -0
  124. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +71 -30
  125. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +4 -0
  126. package/dist/extensions/pi-subagents/src/agents/agents.ts +118 -9
  127. package/dist/extensions/pi-subagents/src/agents/skills.ts +14 -12
  128. package/dist/extensions/pi-subagents/src/api/delegation.ts +3 -0
  129. package/dist/extensions/pi-subagents/src/api/preflight.ts +16 -12
  130. package/dist/extensions/pi-subagents/src/extension/chain-validation.ts +17 -1
  131. package/dist/extensions/pi-subagents/src/extension/index.ts +17 -7
  132. package/dist/extensions/pi-subagents/src/extension/rpc.ts +248 -6
  133. package/dist/extensions/pi-subagents/src/extension/schemas.ts +31 -6
  134. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +8 -7
  135. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +9 -4
  136. package/dist/extensions/pi-subagents/src/intercom/result-intercom.ts +33 -4
  137. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +63 -16
  138. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +24 -13
  139. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +7 -5
  140. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +18 -2
  141. package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +48 -5
  142. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +68 -1
  143. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +29 -4
  144. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +29 -6
  145. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +29 -3
  146. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +372 -28
  147. package/dist/extensions/pi-subagents/src/runs/foreground/async-stop-action.ts +65 -0
  148. package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +3 -3
  149. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +143 -43
  150. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +527 -247
  151. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +42 -0
  152. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +426 -123
  153. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +15 -7
  154. package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +51 -19
  155. package/dist/extensions/pi-subagents/src/runs/shared/chain-outputs.ts +3 -1
  156. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +1 -1
  157. package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +44 -11
  158. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +8 -0
  159. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +97 -20
  160. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +43 -12
  161. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +52 -4
  162. package/dist/extensions/pi-subagents/src/runs/shared/process-signal.ts +19 -0
  163. package/dist/extensions/pi-subagents/src/runs/shared/run-history.ts +45 -9
  164. package/dist/extensions/pi-subagents/src/runs/shared/runtime-acknowledged-extensions.ts +71 -0
  165. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +31 -1
  166. package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +101 -0
  167. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +1 -1
  168. package/dist/extensions/pi-subagents/src/runs/shared/usage-budget.ts +65 -0
  169. package/dist/extensions/pi-subagents/src/runs/shared/workflow-graph.ts +26 -1
  170. package/dist/extensions/pi-subagents/src/shared/settings.ts +17 -1
  171. package/dist/extensions/pi-subagents/src/shared/types.ts +155 -10
  172. package/dist/extensions/pi-subagents/src/shared/utils.ts +73 -4
  173. package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +5 -0
  174. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +46 -4
  175. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +5 -3
  176. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +87 -24
  177. package/dist/extensions/pi-subagents/src/tui/fleet.ts +182 -9
  178. package/dist/extensions/pi-subagents/src/tui/render.ts +27 -28
  179. package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +14 -7
  180. package/dist/extensions/pi-subagents/src/watchdog/review.ts +5 -4
  181. package/dist/extensions/pi-subagents/src/watchdog/runtime.ts +170 -17
  182. package/dist/extensions/pi-subagents/src/watchdog/scope.ts +62 -0
  183. package/dist/extensions/pi-subagents/src/watchdog/settings.ts +41 -1
  184. package/dist/extensions/pi-subagents/src/watchdog/types.ts +10 -0
  185. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +2 -2
  186. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +348 -7
  187. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +84 -0
  188. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +4 -2
  189. package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +51 -2
  190. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +26 -1
  191. package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +48 -0
  192. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +3 -3
  193. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +213 -13
  194. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +34 -0
  195. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +44 -0
  196. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +118 -12
  197. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +850 -7
  198. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +112 -23
  199. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +17 -5
  200. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +2 -0
  201. package/dist/extensions/pi-subagents/test/support/register-loader.mjs +3 -3
  202. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +3 -1
  203. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +128 -0
  204. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +47 -2
  205. package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +60 -0
  206. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +8 -0
  207. package/dist/extensions/pi-subagents/test/unit/capability-ceiling-agent-allowlist.test.ts +102 -0
  208. package/dist/extensions/pi-subagents/test/unit/capability-ceiling.test.ts +1 -1
  209. package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +6 -0
  210. package/dist/extensions/pi-subagents/test/unit/chain-validation.test.ts +1 -1
  211. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +48 -48
  212. package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +23 -0
  213. package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +27 -0
  214. package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +4 -2
  215. package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +1 -0
  216. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +176 -8
  217. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +164 -5
  218. package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +41 -1
  219. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +11 -11
  220. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +18 -2
  221. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +20 -1
  222. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +30 -2
  223. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +10 -2
  224. package/dist/extensions/pi-subagents/test/unit/parallel-utils.test.ts +22 -0
  225. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +103 -0
  226. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +18 -0
  227. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +46 -0
  228. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +13 -1
  229. package/dist/extensions/pi-subagents/test/unit/result-intercom.test.ts +29 -4
  230. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +223 -4
  231. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +30 -0
  232. package/dist/extensions/pi-subagents/test/unit/runtime-acknowledged-extensions.test.ts +52 -0
  233. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +34 -2
  234. package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +1 -1
  235. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +1 -1
  236. package/dist/extensions/pi-subagents/test/unit/streamed-progress-bounds.test.ts +78 -0
  237. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +41 -0
  238. package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +77 -0
  239. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +3 -0
  240. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +1 -1
  241. package/dist/extensions/pi-subagents/test/unit/total-cost.test.ts +1 -0
  242. package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +210 -1
  243. package/dist/extensions/pi-subagents/test/unit/watchdog-scope.test.ts +33 -0
  244. package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +32 -0
  245. package/dist/extensions/pi-subagents/test/unit/writer-budget-guidance.test.ts +1 -1
  246. package/dist/main.d.ts.map +1 -1
  247. package/dist/main.js +43 -0
  248. package/dist/main.js.map +1 -1
  249. package/dist/modes/interactive/components/custom-message.d.ts +3 -1
  250. package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
  251. package/dist/modes/interactive/components/custom-message.js +10 -2
  252. package/dist/modes/interactive/components/custom-message.js.map +1 -1
  253. package/dist/modes/interactive/components/extension-editor.d.ts +1 -2
  254. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  255. package/dist/modes/interactive/components/extension-editor.js +16 -46
  256. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  257. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  258. package/dist/modes/interactive/components/model-selector.js +4 -1
  259. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  260. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  261. package/dist/modes/interactive/components/scoped-models-selector.js +22 -13
  262. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  263. package/dist/modes/interactive/external-editor.d.ts +12 -0
  264. package/dist/modes/interactive/external-editor.d.ts.map +1 -0
  265. package/dist/modes/interactive/external-editor.js +37 -0
  266. package/dist/modes/interactive/external-editor.js.map +1 -0
  267. package/dist/modes/interactive/interactive-mode.d.ts +1 -1
  268. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  269. package/dist/modes/interactive/interactive-mode.js +72 -64
  270. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  271. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  272. package/dist/modes/rpc/rpc-mode.js +14 -0
  273. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  274. package/dist/utils/clipboard.d.ts.map +1 -1
  275. package/dist/utils/clipboard.js +19 -8
  276. package/dist/utils/clipboard.js.map +1 -1
  277. package/dist/utils/version-check.d.ts.map +1 -1
  278. package/dist/utils/version-check.js +1 -1
  279. package/dist/utils/version-check.js.map +1 -1
  280. package/docs/compaction.md +1 -1
  281. package/docs/custom-provider.md +14 -5
  282. package/docs/environment-variables.md +86 -0
  283. package/docs/extensions.md +15 -5
  284. package/docs/index.md +1 -0
  285. package/docs/models.md +9 -2
  286. package/docs/providers.md +21 -2
  287. package/docs/rpc.md +23 -6
  288. package/docs/session-format.md +2 -0
  289. package/docs/settings.md +1 -1
  290. package/docs/usage.md +0 -13
  291. package/examples/extensions/custom-compaction.ts +5 -2
  292. package/examples/extensions/custom-provider-anthropic/index.ts +9 -3
  293. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  294. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  295. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  296. package/examples/extensions/gondolin/package-lock.json +2 -2
  297. package/examples/extensions/gondolin/package.json +1 -1
  298. package/examples/extensions/handoff.ts +11 -3
  299. package/examples/extensions/message-renderer.ts +3 -3
  300. package/examples/extensions/sandbox/package-lock.json +2 -2
  301. package/examples/extensions/sandbox/package.json +1 -1
  302. package/examples/extensions/summarize.ts +5 -2
  303. package/examples/extensions/with-deps/package-lock.json +2 -2
  304. package/examples/extensions/with-deps/package.json +1 -1
  305. package/examples/sdk/12-full-control.ts +3 -1
  306. package/package.json +15 -6
  307. package/dist/extensions/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@selesai/code";
2
+ import { StringEnum } from "@earendil-works/pi-ai";
2
3
  import { randomUUID } from "crypto";
3
4
  import { Type } from "typebox";
4
5
  import { Text } from "@earendil-works/pi-tui";
@@ -7,24 +8,45 @@ import { spawnBrokerIfNeeded } from "./broker/spawn.ts";
7
8
  import { SessionListOverlay } from "./ui/session-list.ts";
8
9
  import { ComposeOverlay, type ComposeResult } from "./ui/compose.ts";
9
10
  import { InlineMessageComponent } from "./ui/inline-message.ts";
10
- import { loadConfig, type IntercomConfig } from "./config.ts";
11
- import type { SessionInfo, Message, Attachment } from "./types.ts";
11
+ import { getAskTimeoutMs, loadConfig, type IntercomConfig } from "./config.ts";
12
+ import { EXTENSION_BUS_FEATURE } from "./types.ts";
13
+ import type { Attachment, BrokerMessage, Message, MessageControl, MessageReceiptStatus, SessionInfo, SessionRegistration } from "./types.ts";
14
+ import {
15
+ INTERCOM_EXTENSION_REGISTER_EVENT,
16
+ INTERCOM_EXTENSION_REGISTRY_READY_EVENT,
17
+ type IntercomExtensionChannel,
18
+ type IntercomExtensionEvent,
19
+ type IntercomExtensionOwner,
20
+ type IntercomExtensionRegistration,
21
+ type IntercomExtensionState,
22
+ } from "./extension-api.ts";
12
23
  import { ReplyTracker } from "./reply-tracker.ts";
24
+ import { resolve as resolvePath } from "node:path";
25
+ import { sameCwd } from "./cwd.ts";
26
+ import { formatContextUsage } from "./format-context.ts";
13
27
 
14
28
  const SUBAGENT_CONTROL_INTERCOM_EVENT = "subagent:control-intercom";
15
29
  const SUBAGENT_RESULT_INTERCOM_EVENT = "subagent:result-intercom";
16
30
  const SUBAGENT_RESULT_INTERCOM_DELIVERY_EVENT = "subagent:result-intercom-delivery";
17
31
  const INBOUND_FLUSH_DELAY_MS = 200;
18
32
  const INBOUND_IDLE_RETRY_MS = 500;
33
+ const INBOUND_MESSAGE_DEDUPE_MAX = 1000;
34
+ const INBOUND_MESSAGE_DEDUPE_RETENTION_MS = 60 * 60 * 1000;
19
35
  const DEFAULT_UNNAMED_SESSION_ALIAS_PREFIX = "subagent-chat";
20
36
  const SUBAGENT_ORCHESTRATOR_TARGET_ENV = "SELESAI_SUBAGENT_ORCHESTRATOR_TARGET";
37
+ const SUBAGENT_ORCHESTRATOR_SESSION_ID_ENV = "SELESAI_SUBAGENT_ORCHESTRATOR_SESSION_ID";
38
+ const INTERCOM_SESSION_ID_ENV = "PI_INTERCOM_SESSION_ID";
39
+ const STABLE_INTERCOM_SESSION_ID_ENV = "PI_INTERCOM_STABLE_ID";
40
+ const NAME_POLL_MS_ENV = "PI_INTERCOM_NAME_POLL_MS";
21
41
  const SUBAGENT_RUN_ID_ENV = "SELESAI_SUBAGENT_RUN_ID";
22
42
  const SUBAGENT_CHILD_AGENT_ENV = "SELESAI_SUBAGENT_CHILD_AGENT";
23
43
  const SUBAGENT_CHILD_INDEX_ENV = "SELESAI_SUBAGENT_CHILD_INDEX";
24
44
  const SUBAGENT_INTERCOM_SESSION_NAME_ENV = "SELESAI_SUBAGENT_INTERCOM_SESSION_NAME";
45
+ const SUBAGENT_SUPERVISOR_CHANNEL_DIR_ENV = "SELESAI_SUBAGENT_SUPERVISOR_CHANNEL_DIR";
25
46
 
26
47
  interface ChildOrchestratorMetadata {
27
48
  orchestratorTarget: string;
49
+ orchestratorSessionId?: string;
28
50
  runId: string;
29
51
  agent: string;
30
52
  index: string;
@@ -78,6 +100,8 @@ function formatAttachments(attachments: Attachment[]): string {
78
100
  }
79
101
  function readChildOrchestratorMetadata(): ChildOrchestratorMetadata | null {
80
102
  const orchestratorTarget = process.env[SUBAGENT_ORCHESTRATOR_TARGET_ENV]?.trim();
103
+ const orchestratorSessionId = process.env[SUBAGENT_ORCHESTRATOR_SESSION_ID_ENV]?.trim()
104
+ || process.env[INTERCOM_SESSION_ID_ENV]?.trim();
81
105
  const runId = process.env[SUBAGENT_RUN_ID_ENV]?.trim();
82
106
  const agent = process.env[SUBAGENT_CHILD_AGENT_ENV]?.trim();
83
107
  const index = process.env[SUBAGENT_CHILD_INDEX_ENV]?.trim();
@@ -87,6 +111,7 @@ function readChildOrchestratorMetadata(): ChildOrchestratorMetadata | null {
87
111
  const sessionName = process.env[SUBAGENT_INTERCOM_SESSION_NAME_ENV]?.trim();
88
112
  return {
89
113
  orchestratorTarget,
114
+ ...(orchestratorSessionId ? { orchestratorSessionId } : {}),
90
115
  runId,
91
116
  agent,
92
117
  index,
@@ -381,6 +406,12 @@ function buildPresenceIdentity(pi: ExtensionAPI, sessionId: string): { name: str
381
406
  name: resolveIntercomPresenceName(pi.getSessionName(), sessionId),
382
407
  };
383
408
  }
409
+ function resolveConfiguredIntercomSessionId(piSessionId: string, config: IntercomConfig): string {
410
+ return process.env[STABLE_INTERCOM_SESSION_ID_ENV]?.trim() || config.stableId || piSessionId;
411
+ }
412
+ function formatIntercomContactSnippet(sessionId: string): string {
413
+ return `Use pi-intercom: intercom({ action: "send", to: "${sessionId}", message: "..." })`;
414
+ }
384
415
  function formatSessionLabel(session: SessionInfo, duplicates: Set<string>): string {
385
416
  if (!session.name) {
386
417
  return session.id;
@@ -394,7 +425,7 @@ function formatSessionListRow(session: SessionInfo, currentCwd: string, isSelf:
394
425
  const tags = [isSelf ? "self" : session.cwd === currentCwd ? "same cwd" : undefined, session.status]
395
426
  .filter((tag): tag is string => Boolean(tag));
396
427
  const suffix = tags.length ? ` [${tags.join(", ")}]` : "";
397
- return `• ${name} (${shortSessionId(session.id)}) — ${session.cwd} (${session.model})${suffix}`;
428
+ return `• ${name} (${shortSessionId(session.id)}) — ${session.cwd} (${session.model}${formatContextUsage(session)})${suffix}`;
398
429
  }
399
430
  function previewText(value: unknown, maxLength = 72): string | undefined {
400
431
  if (typeof value !== "string") {
@@ -409,14 +440,53 @@ function previewText(value: unknown, maxLength = 72): string | undefined {
409
440
  function firstTextContent(result: { content?: Array<{ type: string; text?: string }> }): string {
410
441
  return result.content?.find((item) => item.type === "text" && typeof item.text === "string")?.text?.replace(/\*\*/g, "") ?? "";
411
442
  }
443
+ function formatMessageTimestamp(timestamp: number | undefined): string | undefined {
444
+ return typeof timestamp === "number" && Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : undefined;
445
+ }
446
+ function formatInboundDeliveryMetadata(message: Message): string {
447
+ const parts = [`id ${message.id}`];
448
+ if (typeof message.senderSequence === "number") parts.push(`seq ${message.senderSequence}`);
449
+ if (message.supersedes) parts.push(`supersedes ${message.supersedes}`);
450
+ if (message.retryOf) parts.push(`retry of ${message.retryOf}`);
451
+ const sentAt = formatMessageTimestamp(message.timestamp);
452
+ if (sentAt) parts.push(`sent ${sentAt}`);
453
+ const brokerDeliveredAt = formatMessageTimestamp(message.brokerDeliveredAt);
454
+ if (brokerDeliveredAt) parts.push(`broker delivered ${brokerDeliveredAt}`);
455
+ const receiverReceivedAt = formatMessageTimestamp(message.receiverReceivedAt);
456
+ if (receiverReceivedAt) parts.push(`receiver received ${receiverReceivedAt}`);
457
+ const injectedAt = formatMessageTimestamp(message.injectedAt);
458
+ if (injectedAt) parts.push(`injected ${injectedAt}`);
459
+ return parts.join(" · ");
460
+ }
461
+ function getNamePollMs(): number {
462
+ const configured = process.env[NAME_POLL_MS_ENV];
463
+ if (configured !== undefined) {
464
+ const value = Number(configured);
465
+ if (Number.isFinite(value) && value > 0) {
466
+ return value;
467
+ }
468
+ }
469
+ return 1000;
470
+ }
412
471
  export default function piIntercomExtension(pi: ExtensionAPI) {
413
472
  let client: IntercomClient | null = null;
414
473
  const config: IntercomConfig = loadConfig();
474
+ const askTimeoutMs = getAskTimeoutMs();
475
+ const localExtensions = new Map<string, {
476
+ registration: IntercomExtensionRegistration;
477
+ channel: IntercomExtensionChannel;
478
+ owner?: IntercomExtensionOwner;
479
+ state?: IntercomExtensionState;
480
+ }>();
415
481
  let runtimeContext: ExtensionContext | null = null;
416
482
  let currentSessionId: string | null = null;
483
+ let currentIntercomSessionId: string | null = null;
417
484
  let currentModel = "unknown";
418
485
  let sessionStartedAt: number | null = null;
419
486
  let reconnectTimer: NodeJS.Timeout | null = null;
487
+ let namePollTimer: NodeJS.Timeout | null = null;
488
+ let lastPresenceName: string | null = null;
489
+ const previousIntercomSessionId = process.env[INTERCOM_SESSION_ID_ENV];
420
490
  let reconnectPromise: Promise<IntercomClient> | null = null;
421
491
  let reconnectPromiseGeneration: number | null = null;
422
492
  let startupConnectTimer: NodeJS.Timeout | null = null;
@@ -429,6 +499,75 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
429
499
  const activeTools = new Map<string, string>();
430
500
  const replyTracker = new ReplyTracker();
431
501
  const pendingIdleMessages: InboundMessageEntry[] = [];
502
+ const seenInboundMessages = new Map<string, number>();
503
+ const latestOutboundReceipts = new Map<string, { status: MessageReceiptStatus; timestamp: number; detail?: string }>();
504
+ function dismissIncomingAsk(messageId: string): void {
505
+ replyTracker.dismissPendingAsk(messageId);
506
+ const queuedIndex = pendingIdleMessages.findIndex((entry) => entry.message.id === messageId);
507
+ if (queuedIndex >= 0) pendingIdleMessages.splice(queuedIndex, 1);
508
+ }
509
+ function expirePendingIdleMessages(detail: string): void {
510
+ for (const entry of pendingIdleMessages) {
511
+ emitMessageReceipt(entry.message.id, "expired", detail);
512
+ }
513
+ pendingIdleMessages.length = 0;
514
+ }
515
+ function hasSeenInboundMessage(from: SessionInfo, message: Message, now = Date.now()): boolean {
516
+ for (const [key, seenAt] of seenInboundMessages) {
517
+ if (now - seenAt > INBOUND_MESSAGE_DEDUPE_RETENTION_MS) {
518
+ seenInboundMessages.delete(key);
519
+ }
520
+ }
521
+ const key = `${from.id}\0${message.id}`;
522
+ if (seenInboundMessages.has(key)) {
523
+ return true;
524
+ }
525
+ seenInboundMessages.set(key, now);
526
+ while (seenInboundMessages.size > INBOUND_MESSAGE_DEDUPE_MAX) {
527
+ const oldestKey = seenInboundMessages.keys().next().value;
528
+ if (typeof oldestKey !== "string") break;
529
+ seenInboundMessages.delete(oldestKey);
530
+ }
531
+ return false;
532
+ }
533
+ function emitMessageReceipt(messageId: string, status: MessageReceiptStatus, detail?: string): void {
534
+ try {
535
+ client?.sendMessageReceipt({
536
+ messageId,
537
+ status,
538
+ timestamp: Date.now(),
539
+ ...(detail ? { detail } : {}),
540
+ });
541
+ } catch {
542
+ // Receipts are diagnostics; message handling should not fail when the sender disconnects.
543
+ }
544
+ }
545
+ function handleMessageControl(control: MessageControl): void {
546
+ const queuedIndex = pendingIdleMessages.findIndex((entry) => entry.message.id === control.messageId);
547
+ if (control.action === "cancel") {
548
+ if (queuedIndex >= 0) {
549
+ pendingIdleMessages.splice(queuedIndex, 1);
550
+ replyTracker.dismissPendingAsk(control.messageId);
551
+ emitMessageReceipt(control.messageId, "cancelled", "cancelled before injection");
552
+ } else {
553
+ emitMessageReceipt(control.messageId, "cancellation_requested", "message was not queued; it may already be injected or processed");
554
+ }
555
+ return;
556
+ }
557
+
558
+ if (queuedIndex >= 0) {
559
+ pendingIdleMessages.splice(queuedIndex, 1);
560
+ }
561
+ replyTracker.dismissPendingAsk(control.messageId);
562
+ emitMessageReceipt(control.messageId, "superseded", control.supersededBy ? `superseded by ${control.supersededBy}` : undefined);
563
+ }
564
+ function latestDeliveryState(messageId: string | null, fallback: string): string {
565
+ if (!messageId) {
566
+ return fallback;
567
+ }
568
+ const receipt = latestOutboundReceipts.get(messageId);
569
+ return receipt ? receipt.status : fallback;
570
+ }
432
571
  let inboundFlushTimer: NodeJS.Timeout | null = null;
433
572
  let replyWaiter: {
434
573
  from: string;
@@ -436,7 +575,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
436
575
  resolve: (message: Message) => void;
437
576
  reject: (error: Error) => void;
438
577
  } | null = null;
439
- function waitForReply(from: string, replyTo: string, signal?: AbortSignal): Promise<Message> {
578
+ function waitForReply(from: string, replyTo: string, signal?: AbortSignal, cancelOnAbort?: () => void, getDeliveryState: () => string = () => "unknown"): Promise<Message> {
440
579
  if (replyWaiter) {
441
580
  return Promise.reject(new Error("Already waiting for a reply"));
442
581
  }
@@ -445,8 +584,9 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
445
584
  }
446
585
  return new Promise((resolve, reject) => {
447
586
  const timeout = setTimeout(() => {
448
- rejectReplyWaiter(new Error(`No reply from "${from}" within 10 minutes`));
449
- }, 10 * 60 * 1000);
587
+ const timeoutDescription = askTimeoutMs % 60000 === 0 ? `${askTimeoutMs / 60000} minutes` : `${askTimeoutMs}ms`;
588
+ rejectReplyWaiter(new Error(`No reply from "${from}" for message ${replyTo} within ${timeoutDescription}. Last known delivery state: ${getDeliveryState()}. This waiter timeout is not cancellation; the delivered message may still be queued or actionable in the recipient session.`));
589
+ }, askTimeoutMs);
450
590
  const cleanup = () => {
451
591
  clearTimeout(timeout);
452
592
  signal?.removeEventListener("abort", onAbort);
@@ -455,6 +595,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
455
595
  }
456
596
  };
457
597
  const onAbort = () => {
598
+ cancelOnAbort?.();
458
599
  cleanup();
459
600
  reject(new Error("Cancelled"));
460
601
  };
@@ -490,6 +631,13 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
490
631
  clearTimeout(startupConnectTimer);
491
632
  startupConnectTimer = null;
492
633
  }
634
+ function clearNamePollTimer(): void {
635
+ if (!namePollTimer) {
636
+ return;
637
+ }
638
+ clearInterval(namePollTimer);
639
+ namePollTimer = null;
640
+ }
493
641
  function clearInboundFlushTimer(): void {
494
642
  if (!inboundFlushTimer) {
495
643
  return;
@@ -532,34 +680,171 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
532
680
  const lifecycleStatus = activeToolName ? `tool:${activeToolName}` : agentRunning ? "thinking" : "idle";
533
681
  return config.status ? `${lifecycleStatus} · ${config.status}` : lifecycleStatus;
534
682
  }
535
- function buildRegistration(): Omit<SessionInfo, "id"> {
683
+ function emitLocalExtensionEvent(namespace: string, event: IntercomExtensionEvent): void {
684
+ try {
685
+ localExtensions.get(namespace)?.registration.onEvent(event);
686
+ } catch {
687
+ // One local extension must not break intercom or other extension channels.
688
+ }
689
+ }
690
+ function createExtensionChannel(namespace: string): IntercomExtensionChannel {
691
+ return {
692
+ namespace,
693
+ snapshot() {
694
+ const extension = localExtensions.get(namespace);
695
+ return {
696
+ connected: Boolean(client?.isConnected()),
697
+ supported: Boolean(client?.supportsFeature(EXTENSION_BUS_FEATURE)),
698
+ ...(extension?.owner ? { owner: extension.owner } : {}),
699
+ ...(extension?.state ? { state: extension.state } : {}),
700
+ };
701
+ },
702
+ publish(payload, options = {}) {
703
+ const activeClient = client;
704
+ if (!activeClient?.isConnected()) throw new Error("Intercom is not connected");
705
+ const extension = localExtensions.get(namespace);
706
+ const ownerOnly = options.ownerOnly ?? false;
707
+ const ownerEpoch = ownerOnly ? extension?.owner?.epoch : undefined;
708
+ if (ownerOnly && !ownerEpoch) throw new Error(`No owner is available for ${namespace}`);
709
+ activeClient.sendExtensionMessage({
710
+ type: "extension_publish",
711
+ namespace,
712
+ audience: options.audience ?? "owner",
713
+ ...(ownerOnly ? { ownerOnly: true, ownerEpoch } : {}),
714
+ payload,
715
+ });
716
+ },
717
+ commitState(payload, expectedRevision) {
718
+ const activeClient = client;
719
+ if (!activeClient?.isConnected()) throw new Error("Intercom is not connected");
720
+ const extension = localExtensions.get(namespace);
721
+ const ownerEpoch = extension?.owner?.epoch;
722
+ if (!ownerEpoch || extension.owner?.sessionId !== activeClient.sessionId) {
723
+ throw new Error(`Current session is not the owner of ${namespace}`);
724
+ }
725
+ activeClient.sendExtensionMessage({
726
+ type: "extension_state_commit",
727
+ namespace,
728
+ ownerEpoch,
729
+ expectedRevision: expectedRevision ?? extension.state?.revision ?? 0,
730
+ payload,
731
+ });
732
+ },
733
+ async listSessions() {
734
+ const activeClient = client;
735
+ if (!activeClient?.isConnected()) throw new Error("Intercom is not connected");
736
+ return activeClient.listSessions();
737
+ },
738
+ };
739
+ }
740
+ function currentExtensionCapabilities() {
741
+ return [...localExtensions.values()].map(({ registration }) => ({
742
+ namespace: registration.namespace,
743
+ ownerEligible: registration.ownerEligible,
744
+ }));
745
+ }
746
+ function registerLocalExtension(registration: IntercomExtensionRegistration): void {
747
+ if (!/^[a-z0-9][a-z0-9._/-]{0,63}$/.test(registration.namespace)) {
748
+ throw new Error(`Invalid intercom extension namespace: ${registration.namespace}`);
749
+ }
750
+ if (localExtensions.has(registration.namespace)) {
751
+ throw new Error(`Intercom extension namespace already registered: ${registration.namespace}`);
752
+ }
753
+ const channel = createExtensionChannel(registration.namespace);
754
+ localExtensions.set(registration.namespace, { registration, channel });
755
+ const activeClient = client;
756
+ const connected = Boolean(activeClient?.isConnected());
757
+ const supported = Boolean(activeClient?.supportsFeature(EXTENSION_BUS_FEATURE));
758
+ // Write the capability update before exposing a connected channel. Socket
759
+ // framing preserves this order if onReady publishes synchronously.
760
+ if (activeClient && connected && supported) {
761
+ activeClient.updateExtensionCapabilities(currentExtensionCapabilities());
762
+ }
763
+ registration.onReady(channel);
764
+ if (connected) {
765
+ emitLocalExtensionEvent(registration.namespace, { type: "connection", connected: true, supported });
766
+ }
767
+ }
768
+ function buildRegistration(): SessionRegistration {
536
769
  const liveContext = getLiveContext();
537
770
  if (!liveContext || !currentSessionId || sessionStartedAt === null) {
538
771
  throw new Error("Intercom runtime not initialized");
539
772
  }
540
773
 
541
- const identity = buildPresenceIdentity(pi, currentSessionId);
774
+ const identity = buildPresenceIdentity(pi, currentIntercomSessionId ?? currentSessionId);
542
775
  return {
543
776
  name: identity.name,
544
- cwd: liveContext.cwd ?? process.cwd(),
777
+ cwd: liveContext.cwd,
545
778
  model: currentModel,
546
779
  pid: process.pid,
547
780
  startedAt: sessionStartedAt,
548
781
  lastActivity: Date.now(),
549
782
  status: currentStatus(),
783
+ ...(localExtensions.size > 0
784
+ ? {
785
+ extensions: currentExtensionCapabilities(),
786
+ }
787
+ : {}),
788
+ };
789
+ }
790
+ // Snapshot the live session's context-window usage for presence. getContextUsage()
791
+ // (stock SDK) reports { tokens, contextWindow, percent }, with tokens/percent null
792
+ // right after a compaction (before the next assistant response). We emit null in
793
+ // that case to CLEAR a peer's stale value rather than freeze the old percentage.
794
+ // Feature-detected so an older runtime without getContextUsage() just omits it.
795
+ function currentContextUsage(): { contextPct?: number | null; contextTokens?: number | null; contextWindow?: number } {
796
+ const usage = getLiveContext()?.getContextUsage?.();
797
+ if (!usage) {
798
+ return {};
799
+ }
800
+ const result: { contextPct?: number | null; contextTokens?: number | null; contextWindow?: number } = {
801
+ contextPct: typeof usage.percent === "number" && Number.isFinite(usage.percent) ? Math.round(usage.percent) : null,
802
+ contextTokens: typeof usage.tokens === "number" && Number.isFinite(usage.tokens) ? usage.tokens : null,
550
803
  };
804
+ if (typeof usage.contextWindow === "number" && usage.contextWindow > 0) {
805
+ result.contextWindow = usage.contextWindow;
806
+ }
807
+ return result;
551
808
  }
809
+
552
810
  function syncPresenceIdentity(sessionId: string): void {
553
811
  if (!client || !getLiveContext()) {
554
812
  return;
555
813
  }
556
- client.updatePresence({ ...buildPresenceIdentity(pi, sessionId), status: currentStatus() });
814
+ const identity = buildPresenceIdentity(pi, currentIntercomSessionId ?? sessionId);
815
+ lastPresenceName = identity.name;
816
+ client.updatePresence({ ...identity, status: currentStatus(), ...currentContextUsage() });
817
+ }
818
+ function startNamePoll(): void {
819
+ clearNamePollTimer();
820
+ lastPresenceName = currentSessionId ? buildPresenceIdentity(pi, currentIntercomSessionId ?? currentSessionId).name : null;
821
+ namePollTimer = setInterval(() => {
822
+ if (!currentSessionId || !getLiveContext()) {
823
+ return;
824
+ }
825
+ const identity = buildPresenceIdentity(pi, currentIntercomSessionId ?? currentSessionId);
826
+ if (identity.name !== lastPresenceName) {
827
+ syncPresenceIdentity(currentSessionId);
828
+ }
829
+ }, getNamePollMs());
830
+ namePollTimer.unref?.();
831
+ }
832
+ function publishIntercomSessionId(sessionId: string): void {
833
+ process.env[INTERCOM_SESSION_ID_ENV] = sessionId;
834
+ }
835
+ function restoreIntercomSessionId(): void {
836
+ if (previousIntercomSessionId === undefined) {
837
+ delete process.env[INTERCOM_SESSION_ID_ENV];
838
+ return;
839
+ }
840
+ process.env[INTERCOM_SESSION_ID_ENV] = previousIntercomSessionId;
557
841
  }
558
842
  function syncPresenceStatus(): void {
559
843
  if (!client || !currentSessionId || !getLiveContext()) {
560
844
  return;
561
845
  }
562
- client.updatePresence({ status: currentStatus() });
846
+ // context% rides the status heartbeat so peers see live usage at turn boundaries.
847
+ client.updatePresence({ status: currentStatus(), ...currentContextUsage() });
563
848
  }
564
849
  function currentSessionTargetMatches(to: string, resolvedTo?: string | null, activeClient?: IntercomClient): boolean {
565
850
  const targets = new Set<string>();
@@ -568,29 +853,46 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
568
853
  if (trimmed) targets.add(trimmed.toLowerCase());
569
854
  };
570
855
  addTarget(currentSessionId);
856
+ addTarget(currentIntercomSessionId);
571
857
  addTarget(activeClient?.sessionId);
572
858
  addTarget(pi.getSessionName());
573
- if (currentSessionId) addTarget(buildPresenceIdentity(pi, currentSessionId).name);
859
+ if (currentSessionId) addTarget(buildPresenceIdentity(pi, currentIntercomSessionId ?? currentSessionId).name);
574
860
  return Boolean(resolvedTo && activeClient?.sessionId && resolvedTo === activeClient.sessionId)
575
861
  || targets.has(to.trim().toLowerCase());
576
862
  }
577
- function sendIncomingMessage(entry: InboundMessageEntry, delivery: "trigger" | "followUp", generation = runtimeGeneration): void {
863
+ function shouldTriggerInboundMessage(entry: InboundMessageEntry, forceTrigger = false): boolean {
864
+ if (forceTrigger) {
865
+ return true;
866
+ }
867
+ if (config.inboundTrigger === "always") {
868
+ return true;
869
+ }
870
+ if (config.inboundTrigger === "replies") {
871
+ return Boolean(entry.message.replyTo);
872
+ }
873
+ return false;
874
+ }
875
+ function sendIncomingMessage(entry: InboundMessageEntry, delivery: "trigger" | "followUp", generation = runtimeGeneration, forceTrigger = false): void {
578
876
  if (runtimeStarted && !getLiveContext(runtimeContext, generation)) {
579
877
  return;
580
878
  }
879
+ const injectedMessage = { ...entry.message, injectedAt: Date.now() };
880
+ emitMessageReceipt(injectedMessage.id, "injected");
881
+ const deliveredEntry = { ...entry, message: injectedMessage };
581
882
  if (delivery !== "followUp") {
582
- replyTracker.queueTurnContext({ from: entry.from, message: entry.message, receivedAt: Date.now() });
883
+ replyTracker.queueTurnContext({ from: entry.from, message: injectedMessage, receivedAt: Date.now() });
583
884
  }
584
885
  const senderDisplay = entry.from.name || entry.from.id.slice(0, 8);
585
886
  const replyInstruction = entry.replyCommand ? `\n\nTo reply, use the intercom tool: ${entry.replyCommand}` : "";
887
+ const deliveryMetadata = formatInboundDeliveryMetadata(injectedMessage);
586
888
  pi.sendMessage(
587
889
  {
588
890
  customType: "intercom_message",
589
- content: `**📨 From ${senderDisplay}** (${entry.from.cwd})${replyInstruction}\n\n${entry.bodyText}`,
891
+ content: `**📨 From ${senderDisplay}** (${entry.from.cwd})${replyInstruction}\n\n_${deliveryMetadata}_\n\n${entry.bodyText}`,
590
892
  display: true,
591
- details: entry,
893
+ details: deliveredEntry,
592
894
  },
593
- delivery === "trigger"
895
+ delivery === "trigger" && shouldTriggerInboundMessage(entry, forceTrigger)
594
896
  ? { triggerTurn: true }
595
897
  : { deliverAs: "followUp" }
596
898
  );
@@ -634,6 +936,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
634
936
  }
635
937
  function queueIdleMessage(entry: InboundMessageEntry): void {
636
938
  pendingIdleMessages.push(entry);
939
+ emitMessageReceipt(entry.message.id, "queued");
637
940
  scheduleInboundFlush();
638
941
  }
639
942
  function handleIncomingMessage(ctx: ExtensionContext, from: SessionInfo, message: Message): void {
@@ -642,25 +945,34 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
642
945
  if (!liveContext) {
643
946
  return;
644
947
  }
948
+ const receiverReceivedAt = Date.now();
949
+ if (hasSeenInboundMessage(from, message, receiverReceivedAt)) {
950
+ emitMessageReceipt(message.id, "acknowledged", "duplicate message id suppressed");
951
+ return;
952
+ }
953
+ const receivedMessage = { ...message, receiverReceivedAt };
954
+ emitMessageReceipt(receivedMessage.id, "receiver_received");
645
955
  if (replyWaiter) {
646
956
  const senderTarget = from.name || from.id;
647
957
  const fromMatches = senderTarget.toLowerCase() === replyWaiter.from.toLowerCase()
648
958
  || from.id === replyWaiter.from;
649
- const replyMatches = message.replyTo === replyWaiter.replyTo;
959
+ const replyMatches = receivedMessage.replyTo === replyWaiter.replyTo;
650
960
  if (fromMatches && replyMatches) {
651
- replyWaiter.resolve(message);
961
+ emitMessageReceipt(receivedMessage.id, "acknowledged", "matched reply waiter");
962
+ replyWaiter.resolve(receivedMessage);
652
963
  return;
653
964
  }
654
965
  }
655
- const attachmentText = message.content.attachments?.length
656
- ? formatAttachments(message.content.attachments)
966
+ const attachmentText = receivedMessage.content.attachments?.length
967
+ ? formatAttachments(receivedMessage.content.attachments)
657
968
  : "";
658
- const bodyText = `${message.content.text}${attachmentText}`;
659
- const replyCommand = config.replyHint && message.expectsReply
969
+ const bodyText = `${receivedMessage.content.text}${attachmentText}`;
970
+ const replyCommand = config.replyHint && receivedMessage.expectsReply
660
971
  ? `intercom({ action: "reply", message: "..." })`
661
972
  : undefined;
662
- replyTracker.recordIncomingMessage(from, message);
663
- const entry = { from, message, replyCommand, bodyText };
973
+ replyTracker.recordIncomingMessage(from, receivedMessage, receiverReceivedAt);
974
+ emitMessageReceipt(receivedMessage.id, "acknowledged", "accepted by receiver");
975
+ const entry = { from, message: receivedMessage, replyCommand, bodyText };
664
976
  void (async () => {
665
977
  const activeContext = getLiveContext(liveContext, messageGeneration);
666
978
  if (!activeContext) {
@@ -676,7 +988,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
676
988
  replyTo: message.id,
677
989
  });
678
990
  if (result.delivered && getLiveContext(liveContext, messageGeneration)) {
679
- replyTracker.markReplied(message.id);
991
+ dismissIncomingAsk(message.id);
680
992
  }
681
993
  } catch {
682
994
  // Best-effort reply; keep the busy non-interactive session running either way.
@@ -693,6 +1005,83 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
693
1005
  })();
694
1006
  }
695
1007
  function attachClientHandlers(nextClient: IntercomClient): void {
1008
+ nextClient.onBrokerMessage((message: BrokerMessage) => {
1009
+ if (client !== nextClient) return;
1010
+ switch (message.type) {
1011
+ case "registered": {
1012
+ const supported = message.features?.includes(EXTENSION_BUS_FEATURE) ?? false;
1013
+ if (supported && localExtensions.size > 0) {
1014
+ nextClient.updateExtensionCapabilities(currentExtensionCapabilities());
1015
+ }
1016
+ for (const namespace of localExtensions.keys()) {
1017
+ emitLocalExtensionEvent(namespace, { type: "connection", connected: true, supported });
1018
+ }
1019
+ break;
1020
+ }
1021
+ case "extension_owner": {
1022
+ const extension = localExtensions.get(message.namespace);
1023
+ if (!extension) break;
1024
+ extension.owner = message.ownerId && message.ownerEpoch
1025
+ ? { sessionId: message.ownerId, epoch: message.ownerEpoch }
1026
+ : undefined;
1027
+ emitLocalExtensionEvent(message.namespace, { type: "owner", ...(extension.owner ? { owner: extension.owner } : {}) });
1028
+ break;
1029
+ }
1030
+ case "extension_message": {
1031
+ const extension = localExtensions.get(message.namespace);
1032
+ if (!extension) break;
1033
+ emitLocalExtensionEvent(message.namespace, {
1034
+ type: "message",
1035
+ fromSessionId: message.fromSessionId,
1036
+ ...(message.ownerId && message.ownerEpoch
1037
+ ? { owner: { sessionId: message.ownerId, epoch: message.ownerEpoch } }
1038
+ : {}),
1039
+ payload: message.payload,
1040
+ });
1041
+ break;
1042
+ }
1043
+ case "extension_state": {
1044
+ const extension = localExtensions.get(message.namespace);
1045
+ if (!extension) break;
1046
+ extension.state = { revision: message.revision, payload: message.payload };
1047
+ emitLocalExtensionEvent(message.namespace, { type: "state", state: extension.state });
1048
+ break;
1049
+ }
1050
+ case "extension_state_result":
1051
+ emitLocalExtensionEvent(message.namespace, {
1052
+ type: "state_result",
1053
+ committed: message.committed,
1054
+ revision: message.revision,
1055
+ ...(message.reason ? { reason: message.reason } : {}),
1056
+ });
1057
+ break;
1058
+ case "message_receipt":
1059
+ latestOutboundReceipts.set(message.receipt.messageId, {
1060
+ status: message.receipt.status,
1061
+ timestamp: message.receipt.timestamp,
1062
+ ...(message.receipt.detail ? { detail: message.receipt.detail } : {}),
1063
+ });
1064
+ break;
1065
+ case "message_control":
1066
+ handleMessageControl(message.control);
1067
+ break;
1068
+ case "session_joined":
1069
+ for (const namespace of localExtensions.keys()) {
1070
+ emitLocalExtensionEvent(namespace, { type: "session_joined", session: message.session });
1071
+ }
1072
+ break;
1073
+ case "session_left":
1074
+ for (const namespace of localExtensions.keys()) {
1075
+ emitLocalExtensionEvent(namespace, { type: "session_left", sessionId: message.sessionId });
1076
+ }
1077
+ break;
1078
+ case "presence_update":
1079
+ for (const namespace of localExtensions.keys()) {
1080
+ emitLocalExtensionEvent(namespace, { type: "presence_update", session: message.session });
1081
+ }
1082
+ break;
1083
+ }
1084
+ });
696
1085
  nextClient.on("message", (from, message) => {
697
1086
  const liveContext = getLiveContext();
698
1087
  if (client !== nextClient || !liveContext) {
@@ -705,6 +1094,11 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
705
1094
  return;
706
1095
  }
707
1096
  rejectReplyWaiter(new Error(`Disconnected while waiting for reply: ${error.message}`, { cause: error }));
1097
+ for (const [namespace, extension] of localExtensions) {
1098
+ extension.owner = undefined;
1099
+ emitLocalExtensionEvent(namespace, { type: "connection", connected: false, supported: false });
1100
+ emitLocalExtensionEvent(namespace, { type: "owner" });
1101
+ }
708
1102
  client = null;
709
1103
  if (!shuttingDown && !disposed) {
710
1104
  clearReconnectTimer();
@@ -756,7 +1150,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
756
1150
  attachClientHandlers(nextClient);
757
1151
  try {
758
1152
  await spawnBrokerIfNeeded(config.brokerCommand, config.brokerArgs);
759
- await nextClient.connect(buildRegistration());
1153
+ await nextClient.connect(buildRegistration(), currentIntercomSessionId ?? currentSessionId);
760
1154
  if (!getLiveContext(contextAtStart, generationAtStart)) {
761
1155
  await nextClient.disconnect();
762
1156
  throw new Error("Intercom runtime no longer active");
@@ -792,17 +1186,39 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
792
1186
  const lowerName = nameOrId.toLowerCase();
793
1187
  const byName = sessions.filter(s => s.name?.toLowerCase() === lowerName);
794
1188
  if (byName.length > 1) {
795
- throw new Error(`Multiple sessions named "${nameOrId}" are connected. Use the session ID instead.`);
1189
+ const ids = byName.map(s => shortSessionId(s.id)).join(", ");
1190
+ throw new Error(`Multiple sessions named "${nameOrId}" are connected. Address one by the id shown in parentheses by "list" (${ids}).`);
796
1191
  }
797
- return byName[0]?.id ?? null;
1192
+ if (byName.length === 1) {
1193
+ return byName[0]!.id;
1194
+ }
1195
+
1196
+ const byIdPrefix = sessions.filter(s => s.id.startsWith(nameOrId));
1197
+ if (byIdPrefix.length === 1) {
1198
+ return byIdPrefix[0]!.id;
1199
+ }
1200
+ if (byIdPrefix.length > 1) {
1201
+ throw new Error(`Multiple sessions match ID prefix "${nameOrId}". Use a longer session ID prefix.`);
1202
+ }
1203
+ return null;
1204
+ }
1205
+ async function resolveSupervisorTarget(activeClient: IntercomClient, metadata: ChildOrchestratorMetadata): Promise<string> {
1206
+ if (metadata.orchestratorSessionId) {
1207
+ const bySessionId = await resolveSessionTarget(activeClient, metadata.orchestratorSessionId);
1208
+ if (bySessionId) {
1209
+ return bySessionId;
1210
+ }
1211
+ }
1212
+ return await resolveSessionTarget(activeClient, metadata.orchestratorTarget) ?? metadata.orchestratorTarget;
798
1213
  }
799
1214
  function deliverLocalSubagentRelayMessage(sender: "subagent-control" | "subagent-result", status: string, messageText: string): void {
1215
+ const liveContext = getLiveContext();
800
1216
  const now = Date.now();
801
1217
  sendIncomingMessage({
802
1218
  from: {
803
1219
  id: sender,
804
1220
  name: sender,
805
- cwd: runtimeContext?.cwd ?? process.cwd(),
1221
+ cwd: liveContext?.cwd ?? "",
806
1222
  model: sender,
807
1223
  pid: process.pid,
808
1224
  startedAt: now,
@@ -815,7 +1231,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
815
1231
  content: { text: messageText },
816
1232
  },
817
1233
  bodyText: messageText,
818
- }, "trigger");
1234
+ }, "trigger", runtimeGeneration, true);
819
1235
  }
820
1236
  function recordSubagentDeliveryError(entryType: string, to: string, message: string, error: unknown): void {
821
1237
  pi.appendEntry(entryType, {
@@ -825,6 +1241,49 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
825
1241
  timestamp: Date.now(),
826
1242
  });
827
1243
  }
1244
+ function startSessionRuntime(ctx: ExtensionContext): void {
1245
+ const previousClient = client;
1246
+ shuttingDown = false;
1247
+ disposed = false;
1248
+ runtimeStarted = true;
1249
+ runtimeGeneration += 1;
1250
+ reconnectAttempt = 0;
1251
+ clearReconnectTimer();
1252
+ clearStartupConnectTimer();
1253
+ clearNamePollTimer();
1254
+ clearInboundFlushTimer();
1255
+ rejectReplyWaiter(new Error("Session replaced"));
1256
+ replyTracker.reset();
1257
+ expirePendingIdleMessages("receiver session replaced before injection");
1258
+ if (previousClient) {
1259
+ client = null;
1260
+ void previousClient.disconnect().catch(() => undefined);
1261
+ }
1262
+ runtimeContext = ctx;
1263
+ currentSessionId = ctx.sessionManager.getSessionId();
1264
+ currentIntercomSessionId = resolveConfiguredIntercomSessionId(currentSessionId, config);
1265
+ publishIntercomSessionId(currentIntercomSessionId);
1266
+ currentModel = ctx.model?.id ?? "unknown";
1267
+ sessionStartedAt = Date.now();
1268
+ lastPresenceName = buildPresenceIdentity(pi, currentIntercomSessionId).name;
1269
+ agentRunning = false;
1270
+ activeTools.clear();
1271
+ startNamePoll();
1272
+ const startupGeneration = runtimeGeneration;
1273
+ startupConnectTimer = setTimeout(() => {
1274
+ startupConnectTimer = null;
1275
+ if (!getLiveContext(ctx, startupGeneration)) {
1276
+ return;
1277
+ }
1278
+ void ensureConnected("startup").catch(() => {
1279
+ if (!getLiveContext(ctx, startupGeneration)) {
1280
+ return;
1281
+ }
1282
+ client = null;
1283
+ scheduleReconnect();
1284
+ });
1285
+ }, 0);
1286
+ }
828
1287
  function emitResultDelivery(requestId: string | undefined, delivered: boolean, error?: unknown): void {
829
1288
  if (!requestId) return;
830
1289
  pi.events.emit(SUBAGENT_RESULT_INTERCOM_DELIVERY_EVENT, {
@@ -892,14 +1351,28 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
892
1351
  }
893
1352
  })();
894
1353
  }
895
- pi.events.on(SUBAGENT_CONTROL_INTERCOM_EVENT, (payload) => {
1354
+ const unsubscribeExtensionRegister = pi.events.on(INTERCOM_EXTENSION_REGISTER_EVENT, (payload) => {
1355
+ if (!payload || typeof payload !== "object") return;
1356
+ const registration = payload as Partial<IntercomExtensionRegistration>;
1357
+ if (
1358
+ typeof registration.namespace !== "string"
1359
+ || typeof registration.ownerEligible !== "boolean"
1360
+ || typeof registration.onEvent !== "function"
1361
+ || typeof registration.onReady !== "function"
1362
+ ) {
1363
+ return;
1364
+ }
1365
+ registerLocalExtension(registration as IntercomExtensionRegistration);
1366
+ });
1367
+ pi.events.emit(INTERCOM_EXTENSION_REGISTRY_READY_EVENT, { version: 1 });
1368
+ const unsubscribeSubagentControlIntercom = pi.events.on(SUBAGENT_CONTROL_INTERCOM_EVENT, (payload) => {
896
1369
  relaySubagentIntercomPayload(payload, {
897
1370
  sender: "subagent-control",
898
1371
  status: "needs_attention",
899
1372
  errorEntryType: "intercom_control_error",
900
1373
  });
901
1374
  });
902
- pi.events.on(SUBAGENT_RESULT_INTERCOM_EVENT, (payload) => {
1375
+ const unsubscribeSubagentResultIntercom = pi.events.on(SUBAGENT_RESULT_INTERCOM_EVENT, (payload) => {
903
1376
  relaySubagentIntercomPayload(payload, {
904
1377
  sender: "subagent-result",
905
1378
  status: "result",
@@ -911,44 +1384,23 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
911
1384
  if (!config.enabled) {
912
1385
  return;
913
1386
  }
914
- shuttingDown = false;
915
- disposed = false;
916
- runtimeStarted = true;
917
- runtimeGeneration += 1;
918
- reconnectAttempt = 0;
919
- clearReconnectTimer();
920
- clearStartupConnectTimer();
921
- runtimeContext = ctx;
922
- currentSessionId = ctx.sessionManager.getSessionId();
923
- currentModel = ctx.model?.id ?? "unknown";
924
- sessionStartedAt = Date.now();
925
- agentRunning = false;
926
- activeTools.clear();
927
- const startupGeneration = runtimeGeneration;
928
- startupConnectTimer = setTimeout(() => {
929
- startupConnectTimer = null;
930
- if (!getLiveContext(ctx, startupGeneration)) {
931
- return;
932
- }
933
- void ensureConnected("startup").catch(() => {
934
- if (!getLiveContext(ctx, startupGeneration)) {
935
- return;
936
- }
937
- client = null;
938
- scheduleReconnect();
939
- });
940
- }, 0);
1387
+ startSessionRuntime(ctx);
941
1388
  });
942
1389
 
943
1390
  pi.on("session_shutdown", async () => {
1391
+ unsubscribeExtensionRegister();
1392
+ unsubscribeSubagentControlIntercom();
1393
+ unsubscribeSubagentResultIntercom();
944
1394
  shuttingDown = true;
945
1395
  disposed = true;
946
1396
  runtimeGeneration += 1;
947
1397
  clearStartupConnectTimer();
948
1398
  clearReconnectTimer();
1399
+ clearNamePollTimer();
1400
+ restoreIntercomSessionId();
949
1401
  rejectReplyWaiter(new Error("Session shutting down"));
950
1402
  replyTracker.reset();
951
- pendingIdleMessages.length = 0;
1403
+ expirePendingIdleMessages("receiver session shut down before injection");
952
1404
  clearInboundFlushTimer();
953
1405
  agentRunning = false;
954
1406
  activeTools.clear();
@@ -958,6 +1410,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
958
1410
  }
959
1411
  runtimeContext = null;
960
1412
  currentSessionId = null;
1413
+ currentIntercomSessionId = null;
961
1414
  sessionStartedAt = null;
962
1415
  });
963
1416
  pi.on("turn_end", () => {
@@ -999,19 +1452,28 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
999
1452
  scheduleInboundFlush(0);
1000
1453
  });
1001
1454
  pi.on("turn_start", (_event, ctx) => {
1455
+ const sessionId = ctx.sessionManager.getSessionId();
1456
+ if (!currentSessionId || sessionId !== currentSessionId) {
1457
+ if (!config.enabled) {
1458
+ return;
1459
+ }
1460
+ startSessionRuntime(ctx);
1461
+ replyTracker.beginTurn();
1462
+ return;
1463
+ }
1002
1464
  if (!getLiveContext(ctx)) {
1003
1465
  return;
1004
1466
  }
1005
- currentSessionId = ctx.sessionManager.getSessionId();
1006
- syncPresenceIdentity(ctx.sessionManager.getSessionId());
1467
+ syncPresenceIdentity(sessionId);
1007
1468
  replyTracker.beginTurn();
1008
1469
  });
1009
1470
  pi.on("session_info_changed", (_event, ctx) => {
1010
1471
  if (!getLiveContext(ctx)) {
1011
1472
  return;
1012
1473
  }
1013
- currentSessionId = ctx.sessionManager.getSessionId();
1014
- syncPresenceIdentity(ctx.sessionManager.getSessionId());
1474
+ const sessionId = ctx.sessionManager.getSessionId();
1475
+ currentSessionId = sessionId;
1476
+ syncPresenceIdentity(sessionId);
1015
1477
  });
1016
1478
  pi.on("model_select", (event, ctx) => {
1017
1479
  if (!getLiveContext(ctx)) {
@@ -1020,21 +1482,36 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
1020
1482
  currentModel = event.model.id;
1021
1483
  if (client) {
1022
1484
  client.updatePresence({
1023
- ...buildPresenceIdentity(pi, ctx.sessionManager.getSessionId()),
1485
+ ...buildPresenceIdentity(pi, currentIntercomSessionId ?? ctx.sessionManager.getSessionId()),
1024
1486
  model: event.model.id,
1025
1487
  status: currentStatus(),
1026
1488
  });
1027
1489
  }
1028
1490
  });
1029
1491
 
1030
- pi.registerMessageRenderer("intercom_message", (message, _options, theme) => {
1492
+ pi.registerMessageRenderer("intercom_message", (message, options, theme) => {
1031
1493
  const details = message.details as { from: SessionInfo; message: Message; replyCommand?: string; bodyText?: string } | undefined;
1032
1494
  if (!details) return undefined;
1033
- return new InlineMessageComponent(details.from, details.message, theme, details.replyCommand, details.bodyText);
1495
+ return new InlineMessageComponent(details.from, details.message, theme, details.replyCommand, details.bodyText, !options.expanded);
1496
+ });
1497
+
1498
+ pi.on("tool_result", (event) => {
1499
+ if (event.toolName !== "intercom" && event.toolName !== "contact_supervisor") {
1500
+ return;
1501
+ }
1502
+ if (!event.details || typeof event.details !== "object") {
1503
+ return;
1504
+ }
1505
+
1506
+ const details = event.details as { error?: unknown; delivered?: unknown };
1507
+ if (details.error === true || details.delivered === false) {
1508
+ return { isError: true };
1509
+ }
1034
1510
  });
1035
1511
 
1036
1512
  const childOrchestratorMetadata = readChildOrchestratorMetadata();
1037
- if (childOrchestratorMetadata) {
1513
+ const nativeSupervisorChannelAvailable = Boolean(process.env[SUBAGENT_SUPERVISOR_CHANNEL_DIR_ENV]?.trim());
1514
+ if (childOrchestratorMetadata && !nativeSupervisorChannelAvailable) {
1038
1515
  pi.registerTool({
1039
1516
  name: "contact_supervisor",
1040
1517
  label: "Contact Supervisor",
@@ -1047,8 +1524,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
1047
1524
  "Do not use contact_supervisor for routine completion handoffs; return the final subagent result normally.",
1048
1525
  ],
1049
1526
  parameters: Type.Object({
1050
- reason: Type.String({
1051
- enum: ["need_decision", "progress_update", "interview_request"],
1527
+ reason: StringEnum(["need_decision", "progress_update", "interview_request"] as const, {
1052
1528
  description: "Contact reason: 'need_decision' waits for a reply; 'interview_request' sends structured questions and waits for a reply; 'progress_update' sends a non-blocking update",
1053
1529
  }),
1054
1530
  message: Type.Optional(Type.String({
@@ -1059,7 +1535,9 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
1059
1535
  description: Type.Optional(Type.String()),
1060
1536
  questions: Type.Array(Type.Object({
1061
1537
  id: Type.String(),
1062
- type: Type.String({ description: "Question type: single, multi, text, image, or info" }),
1538
+ type: StringEnum(["single", "multi", "text", "image", "info"] as const, {
1539
+ description: "Question type: single, multi, text, image, or info",
1540
+ }),
1063
1541
  question: Type.String(),
1064
1542
  options: Type.Optional(Type.Array(Type.Any())),
1065
1543
  context: Type.Optional(Type.String()),
@@ -1071,14 +1549,12 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
1071
1549
  if (reason !== "need_decision" && reason !== "progress_update" && reason !== "interview_request") {
1072
1550
  return {
1073
1551
  content: [{ type: "text", text: "Invalid reason. Use 'need_decision', 'interview_request', or 'progress_update'." }],
1074
- isError: true,
1075
1552
  details: { error: true },
1076
1553
  };
1077
1554
  }
1078
1555
  if ((reason === "need_decision" || reason === "progress_update") && typeof params.message !== "string") {
1079
1556
  return {
1080
1557
  content: [{ type: "text", text: `Missing 'message' parameter for reason '${reason}'.` }],
1081
- isError: true,
1082
1558
  details: { error: true },
1083
1559
  };
1084
1560
  }
@@ -1088,7 +1564,6 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
1088
1564
  if (interviewValidation?.ok === false) {
1089
1565
  return {
1090
1566
  content: [{ type: "text", text: `Invalid interview request: ${interviewValidation.error}` }],
1091
- isError: true,
1092
1567
  details: { error: true },
1093
1568
  };
1094
1569
  }
@@ -1100,7 +1575,6 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
1100
1575
  } catch (error) {
1101
1576
  return {
1102
1577
  content: [{ type: "text", text: `Intercom not connected: ${getErrorMessage(error)}` }],
1103
- isError: true,
1104
1578
  details: { error: true },
1105
1579
  };
1106
1580
  }
@@ -1110,7 +1584,6 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
1110
1584
  if (signal?.aborted) {
1111
1585
  return {
1112
1586
  content: [{ type: "text", text: "Cancelled" }],
1113
- isError: true,
1114
1587
  details: { error: true },
1115
1588
  };
1116
1589
  }
@@ -1118,25 +1591,22 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
1118
1591
  const metadata = childOrchestratorMetadata;
1119
1592
  let sendTo: string;
1120
1593
  try {
1121
- sendTo = await resolveSessionTarget(connectedClient, metadata.orchestratorTarget) ?? metadata.orchestratorTarget;
1594
+ sendTo = await resolveSupervisorTarget(connectedClient, metadata);
1122
1595
  } catch (error) {
1123
1596
  return {
1124
1597
  content: [{ type: "text", text: `Failed to resolve supervisor target: ${getErrorMessage(error)}` }],
1125
- isError: true,
1126
1598
  details: { error: true },
1127
1599
  };
1128
1600
  }
1129
1601
  if (signal?.aborted) {
1130
1602
  return {
1131
1603
  content: [{ type: "text", text: "Cancelled" }],
1132
- isError: true,
1133
1604
  details: { error: true },
1134
1605
  };
1135
1606
  }
1136
1607
  if (sendTo === connectedClient.sessionId) {
1137
1608
  return {
1138
1609
  content: [{ type: "text", text: "Cannot message the current session" }],
1139
- isError: true,
1140
1610
  details: { error: true },
1141
1611
  };
1142
1612
  }
@@ -1151,7 +1621,6 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
1151
1621
  const errorText = result.reason ?? "Session may not exist or has disconnected.";
1152
1622
  return {
1153
1623
  content: [{ type: "text", text: `Message to "${metadata.orchestratorTarget}" was not delivered: ${errorText}` }],
1154
- isError: true,
1155
1624
  details: { messageId: result.id, delivered: false, reason: result.reason },
1156
1625
  };
1157
1626
  }
@@ -1164,13 +1633,11 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
1164
1633
  });
1165
1634
  return {
1166
1635
  content: [{ type: "text", text: `Progress update sent to supervisor ${metadata.orchestratorTarget}` }],
1167
- isError: false,
1168
1636
  details: { messageId: result.id, delivered: true },
1169
1637
  };
1170
1638
  } catch (error) {
1171
1639
  return {
1172
1640
  content: [{ type: "text", text: `Failed to send progress update: ${getErrorMessage(error)}` }],
1173
- isError: true,
1174
1641
  details: { error: true },
1175
1642
  };
1176
1643
  }
@@ -1179,15 +1646,16 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
1179
1646
  if (replyWaiter) {
1180
1647
  return {
1181
1648
  content: [{ type: "text", text: "Already waiting for a reply" }],
1182
- isError: true,
1183
1649
  details: { error: true },
1184
1650
  };
1185
1651
  }
1186
1652
 
1187
1653
  let replyPromise: Promise<Message> | null = null;
1654
+ let deliveryState = "created";
1655
+ let questionId: string | null = null;
1188
1656
  try {
1189
- const questionId = randomUUID();
1190
- replyPromise = waitForReply(sendTo, questionId, signal);
1657
+ questionId = randomUUID();
1658
+ replyPromise = waitForReply(sendTo, questionId, signal, () => connectedClient.cancelAsk(questionId!), () => latestDeliveryState(questionId, deliveryState));
1191
1659
  replyPromise.catch(() => undefined);
1192
1660
  if (signal?.aborted) {
1193
1661
  rejectReplyWaiter(new Error("Cancelled"));
@@ -1198,7 +1666,6 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
1198
1666
  }
1199
1667
  return {
1200
1668
  content: [{ type: "text", text: "Cancelled" }],
1201
- isError: true,
1202
1669
  details: { error: true },
1203
1670
  };
1204
1671
  }
@@ -1210,6 +1677,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
1210
1677
  text: requestText,
1211
1678
  expectsReply: true,
1212
1679
  });
1680
+ deliveryState = sendResult.delivered ? "socket_delivered" : "delivery_failed";
1213
1681
  if (!sendResult.delivered) {
1214
1682
  const errorText = sendResult.reason ?? "Session may not exist or has disconnected.";
1215
1683
  rejectReplyWaiter(new Error(`Message to "${metadata.orchestratorTarget}" was not delivered: ${errorText}`));
@@ -1222,7 +1690,6 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
1222
1690
  }
1223
1691
  return {
1224
1692
  content: [{ type: "text", text: `Message to "${metadata.orchestratorTarget}" was not delivered: ${errorText}` }],
1225
- isError: true,
1226
1693
  details: { error: true },
1227
1694
  };
1228
1695
  }
@@ -1252,10 +1719,11 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
1252
1719
  });
1253
1720
  return {
1254
1721
  content: [{ type: "text", text: `**Reply from supervisor:**\n${replyText}${replyAttachments}` }],
1255
- isError: false,
1256
- ...(structuredReply
1257
- ? { details: structuredReply.value !== undefined ? { structuredReply: structuredReply.value } : { structuredReplyParseError: structuredReply.error } }
1258
- : {}),
1722
+ details: structuredReply
1723
+ ? structuredReply.value !== undefined
1724
+ ? { structuredReply: structuredReply.value }
1725
+ : { structuredReplyParseError: structuredReply.error }
1726
+ : {},
1259
1727
  };
1260
1728
  } catch (error) {
1261
1729
  rejectReplyWaiter(toError(error));
@@ -1268,8 +1736,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
1268
1736
  }
1269
1737
  return {
1270
1738
  content: [{ type: "text", text: `Failed: ${getErrorMessage(error)}` }],
1271
- isError: true,
1272
- details: { error: true },
1739
+ details: { error: true, ...(questionId ? { messageId: questionId, deliveryState: latestDeliveryState(questionId, deliveryState) } : {}) },
1273
1740
  };
1274
1741
  }
1275
1742
  },
@@ -1306,37 +1773,44 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
1306
1773
  }
1307
1774
  return new Text(text, 0, 0);
1308
1775
  },
1309
- });
1776
+ } as any);
1310
1777
  }
1311
1778
 
1312
1779
  pi.registerTool({
1313
1780
  name: "intercom",
1314
1781
  label: "Intercom",
1315
- description: `Send a message to another pi session running on this machine.
1782
+ description: `Send a message to another Selesai session running on this machine.
1316
1783
  Use this to communicate findings, request help, or coordinate work with other sessions.
1317
1784
 
1785
+ Target a session by name, full session ID, or the short id shown in parentheses
1786
+ by "list" (a leading prefix of the ID is enough). Prefer the short id when two
1787
+ sessions share a name.
1788
+
1318
1789
  Usage:
1319
1790
  intercom({ action: "list" }) → List active sessions
1320
- intercom({ action: "send", to: "session-name", message: "..." }) → Send message
1321
- intercom({ action: "ask", to: "session-name", message: "..." }) → Ask and wait for reply
1791
+ intercom({ action: "list-cwd" }) → List sessions in the current working directory
1792
+ intercom({ action: "list-cwd", cwd: "/path" }) → List sessions in a specific directory
1793
+ intercom({ action: "send", to: "name-or-id", message: "..." }) → Send message
1794
+ intercom({ action: "ask", to: "name-or-id", message: "..." }) → Ask and wait for reply
1795
+ intercom({ action: "cancel", messageId: "..." }) → Request cancellation of a sent message
1322
1796
  intercom({ action: "reply", message: "..." }) → Reply to the active/single pending ask
1323
1797
  intercom({ action: "pending" }) → List unresolved inbound asks
1324
1798
  intercom({ action: "status" }) → Show connection status`,
1325
1799
  promptSnippet:
1326
- "Use to coordinate with other local pi sessions: list peers, send updates, ask for help, or check intercom connectivity.",
1800
+ "Use to coordinate with other local Selesai sessions: list peers, send updates, ask for help, or check intercom connectivity.",
1327
1801
 
1328
1802
  parameters: Type.Object({
1329
- action: Type.String({
1330
- description: "Action: 'list', 'send', 'ask', 'reply', 'pending', or 'status'",
1803
+ action: StringEnum(["list", "list-cwd", "send", "ask", "reply", "pending", "status", "cancel"] as const, {
1804
+ description: "Action: 'list', 'list-cwd', 'send', 'ask', 'reply', 'pending', 'status', or 'cancel'",
1331
1805
  }),
1332
1806
  to: Type.Optional(Type.String({
1333
- description: "Target session name or ID (for 'send', 'ask', or disambiguating 'reply')",
1807
+ description: "Target session: name, full session ID, or the short id shown in parentheses by 'list' (a leading ID prefix resolves). For 'send', 'ask', or disambiguating 'reply'.",
1334
1808
  })),
1335
1809
  message: Type.Optional(Type.String({
1336
1810
  description: "Message to send (for 'send', 'ask', or 'reply' action)",
1337
1811
  })),
1338
1812
  attachments: Type.Optional(Type.Array(Type.Object({
1339
- type: Type.Union([Type.Literal("file"), Type.Literal("snippet"), Type.Literal("context")]),
1813
+ type: StringEnum(["file", "snippet", "context"] as const),
1340
1814
  name: Type.String(),
1341
1815
  content: Type.String(),
1342
1816
  language: Type.Optional(Type.String()),
@@ -1344,6 +1818,18 @@ Usage:
1344
1818
  replyTo: Type.Optional(Type.String({
1345
1819
  description: "Message ID to reply to (for threading or responding to an 'ask')",
1346
1820
  })),
1821
+ messageId: Type.Optional(Type.String({
1822
+ description: "Message ID for actions that operate on an existing message, such as 'cancel'.",
1823
+ })),
1824
+ supersedes: Type.Optional(Type.String({
1825
+ description: "Previous message ID this send/ask explicitly supersedes. Only works for the same sender and receiver.",
1826
+ })),
1827
+ retryOf: Type.Optional(Type.String({
1828
+ description: "Previous message ID this send/ask is a user-authored retry of. Retries always send a new message ID.",
1829
+ })),
1830
+ cwd: Type.Optional(Type.String({
1831
+ description: "Working directory filter for 'list-cwd' (absolute, or relative to the current session's cwd; '.' means the current cwd). Defaults to the current session's cwd.",
1832
+ })),
1347
1833
  }),
1348
1834
 
1349
1835
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
@@ -1353,14 +1839,13 @@ Usage:
1353
1839
  } catch (error) {
1354
1840
  return {
1355
1841
  content: [{ type: "text", text: `Intercom not connected: ${getErrorMessage(error)}` }],
1356
- isError: true,
1357
1842
  details: { error: true },
1358
1843
  };
1359
1844
  }
1360
1845
 
1361
1846
  syncPresenceIdentity(ctx.sessionManager.getSessionId());
1362
1847
 
1363
- const { action, to, message, attachments, replyTo } = params;
1848
+ const { action, to, message, attachments, replyTo, messageId, supersedes, retryOf, cwd } = params;
1364
1849
 
1365
1850
  switch (action) {
1366
1851
  case "list": {
@@ -1373,7 +1858,6 @@ Usage:
1373
1858
  if (!currentSession) {
1374
1859
  return {
1375
1860
  content: [{ type: "text", text: "Current session is missing from intercom session list." }],
1376
- isError: true,
1377
1861
  details: { error: true },
1378
1862
  };
1379
1863
  }
@@ -1385,22 +1869,101 @@ Usage:
1385
1869
 
1386
1870
  return {
1387
1871
  content: [{ type: "text", text: `${currentSection}\n\n${otherSection}` }],
1388
- isError: false,
1872
+ details: {},
1389
1873
  };
1390
1874
  } catch (error) {
1391
1875
  return {
1392
1876
  content: [{ type: "text", text: `Failed to list sessions: ${getErrorMessage(error)}` }],
1393
- isError: true,
1394
1877
  details: { error: true },
1395
1878
  };
1396
1879
  }
1397
1880
  }
1398
1881
 
1882
+ case "list-cwd": {
1883
+ try {
1884
+ const mySessionId = connectedClient.sessionId;
1885
+ const sessions = await connectedClient.listSessions();
1886
+ const currentSession = sessions.find(s => s.id === mySessionId);
1887
+
1888
+ if (!currentSession) {
1889
+ return {
1890
+ content: [{ type: "text", text: "Current session is missing from intercom session list." }],
1891
+ details: { error: true },
1892
+ };
1893
+ }
1894
+
1895
+ // Default to the current session's cwd; an explicit `cwd` overrides
1896
+ // (relative paths resolved against it, "." meaning the current cwd).
1897
+ const filterCwd = cwd && cwd !== "."
1898
+ ? resolvePath(currentSession.cwd, cwd)
1899
+ : currentSession.cwd;
1900
+
1901
+ const otherSessions = sessions.filter(
1902
+ s => s.id !== mySessionId && sameCwd(s.cwd, filterCwd),
1903
+ );
1904
+
1905
+ // Fail loud: filtering by a directory with no peers while the
1906
+ // session's OWN cwd has some otherwise reads as a misleading empty
1907
+ // result (common when a caller passes a guessed parent cwd).
1908
+ let emptyNote = "No other sessions in this directory.";
1909
+ if (otherSessions.length === 0 && !sameCwd(filterCwd, currentSession.cwd)) {
1910
+ const here = sessions.filter(
1911
+ s => s.id !== mySessionId && sameCwd(s.cwd, currentSession.cwd),
1912
+ ).length;
1913
+ if (here > 0) {
1914
+ emptyNote += ` Your session's cwd is ${currentSession.cwd} (${here} peer${here === 1 ? "" : "s"} there) — call list-cwd without a cwd argument to list them.`;
1915
+ }
1916
+ }
1917
+
1918
+ const currentSection = `**Current session:**\n${formatSessionListRow(currentSession, currentSession.cwd, true)}`;
1919
+ const otherSection = otherSessions.length === 0
1920
+ ? `**Other sessions (cwd: ${filterCwd}):**\n${emptyNote}`
1921
+ : `**Other sessions (cwd: ${filterCwd}):**\n${otherSessions.map(s => formatSessionListRow(s, currentSession.cwd, false)).join("\n")}`;
1922
+
1923
+ return {
1924
+ content: [{ type: "text", text: `${currentSection}\n\n${otherSection}` }],
1925
+ details: {},
1926
+ };
1927
+ } catch (error) {
1928
+ return {
1929
+ content: [{ type: "text", text: `Failed to list sessions: ${getErrorMessage(error)}` }],
1930
+ details: { error: true },
1931
+ };
1932
+ }
1933
+ }
1934
+
1935
+ case "cancel": {
1936
+ if (!messageId) {
1937
+ return {
1938
+ content: [{ type: "text", text: "Missing 'messageId' parameter" }],
1939
+ details: { error: true },
1940
+ };
1941
+ }
1942
+ try {
1943
+ const result = await connectedClient.cancelMessage(messageId);
1944
+ if (!result.delivered) {
1945
+ const errorText = result.reason ?? "Message may not exist or may belong to another sender.";
1946
+ return {
1947
+ content: [{ type: "text", text: `Cancellation for ${messageId} was not delivered: ${errorText}` }],
1948
+ details: { messageId, delivered: false, reason: result.reason },
1949
+ };
1950
+ }
1951
+ return {
1952
+ content: [{ type: "text", text: `Cancellation requested for ${messageId}` }],
1953
+ details: { messageId, delivered: true },
1954
+ };
1955
+ } catch (error) {
1956
+ return {
1957
+ content: [{ type: "text", text: `Failed to cancel message: ${getErrorMessage(error)}` }],
1958
+ details: { error: true, messageId },
1959
+ };
1960
+ }
1961
+ }
1962
+
1399
1963
  case "send": {
1400
1964
  if (!to || !message) {
1401
1965
  return {
1402
1966
  content: [{ type: "text", text: "Missing 'to' or 'message' parameter" }],
1403
- isError: true,
1404
1967
  details: { error: true },
1405
1968
  };
1406
1969
  }
@@ -1409,7 +1972,6 @@ Usage:
1409
1972
  if (sendTo === connectedClient.sessionId) {
1410
1973
  return {
1411
1974
  content: [{ type: "text", text: "Cannot message the current session" }],
1412
- isError: true,
1413
1975
  details: { error: true },
1414
1976
  };
1415
1977
  }
@@ -1422,7 +1984,7 @@ Usage:
1422
1984
  if (!confirmed) {
1423
1985
  return {
1424
1986
  content: [{ type: "text", text: "Message cancelled by user" }],
1425
- isError: false,
1987
+ details: {},
1426
1988
  };
1427
1989
  }
1428
1990
  }
@@ -1430,33 +1992,32 @@ Usage:
1430
1992
  text: message,
1431
1993
  attachments,
1432
1994
  replyTo,
1995
+ supersedes,
1996
+ retryOf,
1433
1997
  });
1434
1998
  if (!result.delivered) {
1435
1999
  const errorText = result.reason ?? "Session may not exist or has disconnected.";
1436
2000
  return {
1437
2001
  content: [{ type: "text", text: `Message to "${to}" was not delivered: ${errorText}` }],
1438
- isError: true,
1439
2002
  details: { messageId: result.id, delivered: false, reason: result.reason },
1440
2003
  };
1441
2004
  }
1442
2005
  pi.appendEntry("intercom_sent", {
1443
2006
  to,
1444
- message: { text: message, attachments, replyTo },
2007
+ message: { text: message, attachments, replyTo, supersedes, retryOf },
1445
2008
  messageId: result.id,
1446
2009
  timestamp: Date.now(),
1447
2010
  });
1448
2011
  if (replyTo) {
1449
- replyTracker.markReplied(replyTo);
2012
+ dismissIncomingAsk(replyTo);
1450
2013
  }
1451
2014
  return {
1452
2015
  content: [{ type: "text", text: `Message sent to ${to}` }],
1453
- isError: false,
1454
2016
  details: { messageId: result.id, delivered: true },
1455
2017
  };
1456
2018
  } catch (error) {
1457
2019
  return {
1458
2020
  content: [{ type: "text", text: `Failed to send: ${getErrorMessage(error)}` }],
1459
- isError: true,
1460
2021
  details: { error: true },
1461
2022
  };
1462
2023
  }
@@ -1466,7 +2027,6 @@ Usage:
1466
2027
  if (!to || !message) {
1467
2028
  return {
1468
2029
  content: [{ type: "text", text: "Missing 'to' or 'message' parameter" }],
1469
- isError: true,
1470
2030
  details: { error: true },
1471
2031
  };
1472
2032
  }
@@ -1474,7 +2034,6 @@ Usage:
1474
2034
  if (replyWaiter) {
1475
2035
  return {
1476
2036
  content: [{ type: "text", text: "Already waiting for a reply" }],
1477
- isError: true,
1478
2037
  details: { error: true },
1479
2038
  };
1480
2039
  }
@@ -1482,38 +2041,47 @@ Usage:
1482
2041
  if (_signal?.aborted) {
1483
2042
  return {
1484
2043
  content: [{ type: "text", text: "Cancelled" }],
1485
- isError: true,
1486
2044
  details: { error: true },
1487
2045
  };
1488
2046
  }
1489
2047
  let replyPromise: Promise<Message> | null = null;
2048
+ let deliveryState = "created";
2049
+ let questionId: string | null = null;
1490
2050
 
1491
2051
  try {
1492
2052
  const sendTo = await resolveSessionTarget(connectedClient, to) ?? to;
1493
2053
  if (_signal?.aborted) {
1494
2054
  return {
1495
2055
  content: [{ type: "text", text: "Cancelled" }],
1496
- isError: true,
1497
2056
  details: { error: true },
1498
2057
  };
1499
2058
  }
1500
2059
  if (sendTo === connectedClient.sessionId) {
1501
2060
  return {
1502
2061
  content: [{ type: "text", text: "Cannot message the current session" }],
1503
- isError: true,
1504
2062
  details: { error: true },
1505
2063
  };
1506
2064
  }
1507
- const questionId = randomUUID();
1508
- replyPromise = waitForReply(sendTo, questionId, _signal);
2065
+ if (replyWaiter) {
2066
+ return {
2067
+ content: [{ type: "text", text: "Already waiting for a reply" }],
2068
+ details: { error: true },
2069
+ };
2070
+ }
2071
+ questionId = randomUUID();
2072
+ replyPromise = waitForReply(sendTo, questionId, _signal, () => connectedClient.cancelAsk(questionId!), () => latestDeliveryState(questionId, deliveryState));
2073
+ replyPromise.catch(() => undefined);
1509
2074
  const sendResult = await connectedClient.send(sendTo, {
1510
2075
  messageId: questionId,
1511
2076
  text: message,
1512
2077
  attachments,
1513
2078
  replyTo,
1514
2079
  expectsReply: true,
2080
+ supersedes,
2081
+ retryOf,
1515
2082
  });
1516
2083
 
2084
+ deliveryState = sendResult.delivered ? "socket_delivered" : "delivery_failed";
1517
2085
  if (!sendResult.delivered) {
1518
2086
  const errorText = sendResult.reason ?? "Session may not exist or has disconnected.";
1519
2087
  rejectReplyWaiter(new Error(`Message to "${to}" was not delivered: ${errorText}`));
@@ -1526,13 +2094,12 @@ Usage:
1526
2094
  }
1527
2095
  return {
1528
2096
  content: [{ type: "text", text: `Message to "${to}" was not delivered: ${errorText}` }],
1529
- isError: true,
1530
2097
  details: { error: true },
1531
2098
  };
1532
2099
  }
1533
2100
  pi.appendEntry("intercom_sent", {
1534
2101
  to,
1535
- message: { text: message, attachments, replyTo },
2102
+ message: { text: message, attachments, replyTo, supersedes, retryOf },
1536
2103
  messageId: sendResult.id,
1537
2104
  timestamp: Date.now(),
1538
2105
  });
@@ -1549,7 +2116,7 @@ Usage:
1549
2116
  });
1550
2117
  return {
1551
2118
  content: [{ type: "text", text: `**Reply from ${to}:**\n${replyText}${replyAttachments}` }],
1552
- isError: false,
2119
+ details: {},
1553
2120
  };
1554
2121
  } catch (error) {
1555
2122
  rejectReplyWaiter(toError(error));
@@ -1562,8 +2129,7 @@ Usage:
1562
2129
  }
1563
2130
  return {
1564
2131
  content: [{ type: "text", text: `Failed: ${getErrorMessage(error)}` }],
1565
- isError: true,
1566
- details: { error: true },
2132
+ details: { error: true, ...(questionId ? { messageId: questionId, deliveryState: latestDeliveryState(questionId, deliveryState) } : {}) },
1567
2133
  };
1568
2134
  }
1569
2135
  }
@@ -1572,17 +2138,15 @@ Usage:
1572
2138
  if (!message) {
1573
2139
  return {
1574
2140
  content: [{ type: "text", text: "Missing 'message' parameter" }],
1575
- isError: true,
1576
2141
  details: { error: true },
1577
2142
  };
1578
2143
  }
1579
2144
 
1580
2145
  try {
1581
- const target = replyTracker.resolveReplyTarget({ to });
2146
+ const target = replyTracker.resolveReplyTarget({ to, replyTo });
1582
2147
  if (target.from.id === connectedClient.sessionId) {
1583
2148
  return {
1584
2149
  content: [{ type: "text", text: "Cannot message the current session" }],
1585
- isError: true,
1586
2150
  details: { error: true },
1587
2151
  };
1588
2152
  }
@@ -1592,13 +2156,15 @@ Usage:
1592
2156
  });
1593
2157
  if (!result.delivered) {
1594
2158
  const errorText = result.reason ?? "Session may not exist or has disconnected.";
2159
+ if (result.reason === "Session not found") {
2160
+ dismissIncomingAsk(target.message.id);
2161
+ }
1595
2162
  return {
1596
2163
  content: [{ type: "text", text: `Reply to "${target.from.name || target.from.id}" was not delivered: ${errorText}` }],
1597
- isError: true,
1598
2164
  details: { messageId: result.id, delivered: false, reason: result.reason },
1599
2165
  };
1600
2166
  }
1601
- replyTracker.markReplied(target.message.id);
2167
+ dismissIncomingAsk(target.message.id);
1602
2168
  pi.appendEntry("intercom_sent", {
1603
2169
  to: target.from.name || target.from.id,
1604
2170
  message: { text: message, replyTo: target.message.id },
@@ -1607,13 +2173,11 @@ Usage:
1607
2173
  });
1608
2174
  return {
1609
2175
  content: [{ type: "text", text: `Reply sent to ${target.from.name || target.from.id}` }],
1610
- isError: false,
1611
2176
  details: { messageId: result.id, delivered: true, replyTo: target.message.id },
1612
2177
  };
1613
2178
  } catch (error) {
1614
2179
  return {
1615
2180
  content: [{ type: "text", text: `Failed to reply: ${getErrorMessage(error)}` }],
1616
- isError: true,
1617
2181
  details: { error: true },
1618
2182
  };
1619
2183
  }
@@ -1624,7 +2188,7 @@ Usage:
1624
2188
  if (pendingAsks.length === 0) {
1625
2189
  return {
1626
2190
  content: [{ type: "text", text: "No unresolved inbound asks." }],
1627
- isError: false,
2191
+ details: {},
1628
2192
  };
1629
2193
  }
1630
2194
 
@@ -1636,7 +2200,7 @@ Usage:
1636
2200
  });
1637
2201
  return {
1638
2202
  content: [{ type: "text", text: `**Pending asks:**\n${lines.join("\n")}` }],
1639
- isError: false,
2203
+ details: {},
1640
2204
  };
1641
2205
  }
1642
2206
 
@@ -1649,12 +2213,11 @@ Usage:
1649
2213
  type: "text",
1650
2214
  text: `**Intercom Status:**\nConnected: Yes\nSession ID: ${mySessionId}\nActive sessions: ${sessions.length}`,
1651
2215
  }],
1652
- isError: false,
2216
+ details: {},
1653
2217
  };
1654
2218
  } catch (error) {
1655
2219
  return {
1656
2220
  content: [{ type: "text", text: `Failed to get status: ${getErrorMessage(error)}` }],
1657
- isError: true,
1658
2221
  details: { error: true },
1659
2222
  };
1660
2223
  }
@@ -1663,7 +2226,6 @@ Usage:
1663
2226
  default:
1664
2227
  return {
1665
2228
  content: [{ type: "text", text: `Unknown action: ${action}` }],
1666
- isError: true,
1667
2229
  details: { error: true },
1668
2230
  };
1669
2231
  }
@@ -1702,12 +2264,42 @@ Usage:
1702
2264
  }
1703
2265
  return new Text(text, 0, 0);
1704
2266
  },
1705
- });
2267
+ } as any);
2268
+
2269
+ function insertIntoEditor(ctx: ExtensionContext, text: string): boolean {
2270
+ if (!ctx.hasUI) return false;
2271
+ const ui = ctx.ui as { getEditorText?: () => string; setEditorText?: (text: string) => void };
2272
+ if (typeof ui.setEditorText !== "function") return false;
2273
+ const existing = typeof ui.getEditorText === "function" ? ui.getEditorText() : "";
2274
+ ui.setEditorText(existing.trim() ? `${existing.trimEnd()}\n\n${text}` : text);
2275
+ return true;
2276
+ }
2277
+
2278
+ async function insertIntercomId(ctx: ExtensionContext): Promise<void> {
2279
+ const commandGeneration = runtimeGeneration;
2280
+ const liveContext = getLiveContext(ctx, commandGeneration);
2281
+ if (!liveContext) return;
2282
+ let contactClient: IntercomClient;
2283
+ try {
2284
+ contactClient = await ensureConnected("tool");
2285
+ } catch (error) {
2286
+ notifyIfLive(ctx, `Intercom unavailable: ${getErrorMessage(error)}`, "error", commandGeneration);
2287
+ return;
2288
+ }
2289
+ const sessionId = contactClient.sessionId;
2290
+ if (!sessionId || !getLiveContext(liveContext, commandGeneration)) return;
2291
+ const snippet = formatIntercomContactSnippet(sessionId);
2292
+ if (insertIntoEditor(liveContext, snippet)) {
2293
+ notifyIfLive(liveContext, `Inserted intercom contact target: ${sessionId}`, "info", commandGeneration);
2294
+ return;
2295
+ }
2296
+ notifyIfLive(liveContext, `Intercom contact target: ${sessionId}`, "info", commandGeneration);
2297
+ }
1706
2298
 
1707
2299
  async function openIntercomOverlay(ctx: ExtensionContext): Promise<void> {
1708
2300
  const overlayGeneration = runtimeGeneration;
1709
2301
  const liveContext = getLiveContext(ctx, overlayGeneration);
1710
- if (!liveContext?.hasUI) return;
2302
+ if (!liveContext?.hasUI || (liveContext as ExtensionContext & { mode?: string }).mode !== "tui") return;
1711
2303
 
1712
2304
  let overlayClient: IntercomClient;
1713
2305
  try {
@@ -1742,7 +2334,7 @@ Usage:
1742
2334
 
1743
2335
  const selectedSession = await ctx.ui.custom<SessionInfo | undefined>(
1744
2336
  (_tui, theme, keybindings, done) => new SessionListOverlay(theme, keybindings, currentSession, sessions, done),
1745
- { overlay: true }
2337
+ { overlay: true, overlayOptions: { width: 88 } }
1746
2338
  ).catch(() => undefined);
1747
2339
 
1748
2340
  if (!selectedSession || !getLiveContext(ctx, overlayGeneration)) return;
@@ -1759,7 +2351,7 @@ Usage:
1759
2351
 
1760
2352
  const result = await ctx.ui.custom<ComposeResult>(
1761
2353
  (tui, theme, keybindings, done) => new ComposeOverlay(tui, theme, keybindings, selectedSession, targetLabel, overlayClient, done),
1762
- { overlay: true }
2354
+ { overlay: true, overlayOptions: { width: 72 } }
1763
2355
  ).catch(() => undefined);
1764
2356
 
1765
2357
  if (result?.sent && result.messageId && result.text && getLiveContext(ctx, overlayGeneration)) {
@@ -1778,6 +2370,11 @@ Usage:
1778
2370
  handler: async (_args, ctx) => openIntercomOverlay(ctx),
1779
2371
  });
1780
2372
 
2373
+ pi.registerCommand("intercom-id", {
2374
+ description: "Insert a stable pi-intercom handoff snippet for this session into the editor",
2375
+ handler: async (_args, ctx) => insertIntercomId(ctx),
2376
+ });
2377
+
1781
2378
  pi.registerShortcut("alt+m", {
1782
2379
  description: "Open session intercom",
1783
2380
  handler: async (ctx) => openIntercomOverlay(ctx),