@fitchmultz/pi-subagents 0.44.6 → 0.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (947) hide show
  1. package/CHANGELOG.md +265 -7
  2. package/LICENSE +1 -0
  3. package/README.md +58 -1483
  4. package/agents/context-builder.md +19 -1
  5. package/agents/debugger.md +10 -3
  6. package/agents/delegate.md +1 -0
  7. package/agents/fixer.md +4 -1
  8. package/agents/oracle.md +13 -2
  9. package/agents/planner.md +14 -3
  10. package/agents/researcher.md +11 -2
  11. package/agents/reviewer-claude.md +11 -2
  12. package/agents/reviewer-gpt.md +12 -2
  13. package/agents/reviewer-ponytail.md +11 -1
  14. package/agents/reviewer-security.md +13 -2
  15. package/agents/reviewer.md +12 -3
  16. package/agents/scout.md +12 -1
  17. package/agents/ui-designer.md +10 -3
  18. package/agents/watcher.md +1 -0
  19. package/agents/worker.md +8 -2
  20. package/agents/writer.md +6 -3
  21. package/dist/agents/agent-defaults.js +9 -0
  22. package/dist/agents/agent-definition.js +196 -0
  23. package/dist/agents/agent-management.js +64 -905
  24. package/dist/agents/agent-scope.js +4 -2
  25. package/dist/agents/agent-selection.js +10 -5
  26. package/dist/agents/agent-serializer.js +55 -67
  27. package/dist/agents/agents.js +75 -716
  28. package/dist/agents/builtin-overrides.js +218 -0
  29. package/dist/agents/chain-serializer.js +3 -367
  30. package/dist/agents/config-values.js +52 -0
  31. package/dist/agents/discovery-paths.js +91 -0
  32. package/dist/agents/frontmatter.js +15 -5
  33. package/dist/agents/identity.js +25 -8
  34. package/dist/agents/json-chain.js +254 -0
  35. package/dist/agents/management-config.js +332 -0
  36. package/dist/agents/management-format.js +231 -0
  37. package/dist/agents/management-persistence.js +287 -0
  38. package/dist/agents/management-targets.js +136 -0
  39. package/dist/agents/management-warnings.js +81 -0
  40. package/dist/agents/markdown-chain.js +210 -0
  41. package/dist/agents/package-agents.js +154 -0
  42. package/dist/agents/skill-files.js +157 -0
  43. package/dist/agents/skill-search-paths.js +246 -0
  44. package/dist/agents/skill-types.js +1 -0
  45. package/dist/agents/skills.js +54 -499
  46. package/dist/extension/acceptance-schema.js +55 -0
  47. package/dist/extension/build-info.js +1 -1
  48. package/dist/extension/chain-schema.js +196 -0
  49. package/dist/extension/child-runtime.js +237 -0
  50. package/dist/extension/compact-tools.js +122 -51
  51. package/dist/extension/config-schema.js +29 -0
  52. package/dist/extension/config.js +27 -11
  53. package/dist/extension/control-notice-schema.js +37 -0
  54. package/dist/extension/control-notices.js +13 -6
  55. package/dist/extension/delegation-bridges.js +43 -0
  56. package/dist/extension/doctor.js +48 -25
  57. package/dist/extension/everyday-schemas.js +207 -0
  58. package/dist/extension/fanout-child.js +18 -319
  59. package/dist/extension/index.js +8 -563
  60. package/dist/extension/message-renderers.js +225 -0
  61. package/dist/extension/nested-control-inbox.js +154 -0
  62. package/dist/extension/parent-runtime.js +339 -0
  63. package/dist/extension/primary-tool.js +69 -0
  64. package/dist/extension/result-snapshot.js +22 -0
  65. package/dist/extension/runtime-reload.js +62 -0
  66. package/dist/extension/runtime-storage.js +43 -0
  67. package/dist/extension/schema-overrides.js +75 -0
  68. package/dist/extension/schemas.js +265 -336
  69. package/dist/extension/tool-input.js +27 -12
  70. package/dist/extension/tool-result.js +19 -4
  71. package/dist/history/answer-texts.js +57 -0
  72. package/dist/history/canonical-result.js +76 -43
  73. package/dist/history/canonical-selection.js +40 -0
  74. package/dist/history/database-generation.js +82 -0
  75. package/dist/history/database.js +57 -33
  76. package/dist/history/entry-ingestion.js +61 -0
  77. package/dist/history/index.js +242 -97
  78. package/dist/history/ingest.js +168 -331
  79. package/dist/history/output-digests.js +40 -0
  80. package/dist/history/preview.js +210 -0
  81. package/dist/history/prune.js +33 -0
  82. package/dist/history/queries.js +60 -349
  83. package/dist/history/query-scope.js +190 -0
  84. package/dist/history/rows.js +77 -0
  85. package/dist/history/run-projection.js +137 -0
  86. package/dist/history/run-queries.js +153 -0
  87. package/dist/history/schema.js +31 -0
  88. package/dist/history/search-queries.js +163 -0
  89. package/dist/history/selected-record.js +108 -0
  90. package/dist/history/source-file.js +45 -0
  91. package/dist/history/source-watches.js +141 -0
  92. package/dist/history/store.js +164 -153
  93. package/dist/history/text-chunks.js +169 -0
  94. package/dist/history/text.js +35 -0
  95. package/dist/history/transcript-queries.js +177 -0
  96. package/dist/history/types.js +5 -1
  97. package/dist/history/values.js +36 -0
  98. package/dist/history/wire-inputs.js +122 -0
  99. package/dist/history/wire-models.js +38 -0
  100. package/dist/history/worker-runtime.js +441 -0
  101. package/dist/history/worker.js +66 -462
  102. package/dist/intercom/intercom-bridge.js +48 -20
  103. package/dist/intercom/live-intercom.js +80 -23
  104. package/dist/intercom/nested-result-tree.js +163 -0
  105. package/dist/intercom/result-delivery.js +40 -0
  106. package/dist/intercom/result-intercom.js +178 -261
  107. package/dist/pi-intercom/agent-dir.js +7 -3
  108. package/dist/pi-intercom/async.js +27 -0
  109. package/dist/pi-intercom/bridge-ask.js +57 -0
  110. package/dist/pi-intercom/bridge-config.js +79 -0
  111. package/dist/pi-intercom/bridge-http.js +86 -0
  112. package/dist/pi-intercom/bridge-protocol.js +106 -0
  113. package/dist/pi-intercom/bridge-routes.js +64 -0
  114. package/dist/pi-intercom/bridge-session.js +416 -0
  115. package/dist/pi-intercom/bridge.js +182 -603
  116. package/dist/pi-intercom/broker/broker.js +236 -244
  117. package/dist/pi-intercom/broker/client.js +189 -304
  118. package/dist/pi-intercom/broker/connection.js +165 -0
  119. package/dist/pi-intercom/broker/framing.js +5 -3
  120. package/dist/pi-intercom/broker/paths.js +4 -1
  121. package/dist/pi-intercom/broker/pid.js +53 -25
  122. package/dist/pi-intercom/broker/snapshot-reader.js +78 -0
  123. package/dist/pi-intercom/broker/snapshots.js +188 -0
  124. package/dist/pi-intercom/broker/spawn.js +36 -25
  125. package/dist/pi-intercom/config.js +85 -71
  126. package/dist/pi-intercom/connection.js +297 -0
  127. package/dist/pi-intercom/identity.js +58 -0
  128. package/dist/pi-intercom/inbound-delivery.js +356 -0
  129. package/dist/pi-intercom/inbound-journal.js +303 -0
  130. package/dist/pi-intercom/inbound-record.js +113 -0
  131. package/dist/pi-intercom/inbound.js +179 -0
  132. package/dist/pi-intercom/index.js +79 -2933
  133. package/dist/pi-intercom/inspection.js +62 -0
  134. package/dist/pi-intercom/interview.js +236 -0
  135. package/dist/pi-intercom/lifecycle.js +168 -0
  136. package/dist/pi-intercom/message-format.js +69 -0
  137. package/dist/pi-intercom/outbound.js +304 -0
  138. package/dist/pi-intercom/peer-awareness.js +66 -0
  139. package/dist/pi-intercom/reply-tracker.js +62 -43
  140. package/dist/pi-intercom/reply-wait.js +201 -0
  141. package/dist/pi-intercom/runtime-types.js +3 -0
  142. package/dist/pi-intercom/session-events.js +180 -0
  143. package/dist/pi-intercom/session-targets.js +42 -34
  144. package/dist/pi-intercom/subagent-events.js +238 -0
  145. package/dist/pi-intercom/subagent-live-events.js +129 -86
  146. package/dist/pi-intercom/supervisor.js +282 -0
  147. package/dist/pi-intercom/tool-arguments.js +74 -0
  148. package/dist/pi-intercom/tool-registration.js +95 -0
  149. package/dist/pi-intercom/tool-results.js +38 -0
  150. package/dist/pi-intercom/tool-schemas.js +82 -0
  151. package/dist/pi-intercom/tools.js +51 -0
  152. package/dist/pi-intercom/topic-actions.js +177 -0
  153. package/dist/pi-intercom/topics.js +177 -92
  154. package/dist/pi-intercom/transport.js +1 -0
  155. package/dist/pi-intercom/types.js +151 -114
  156. package/dist/pi-intercom/ui/compose-paste.js +93 -0
  157. package/dist/pi-intercom/ui/compose.js +121 -143
  158. package/dist/pi-intercom/ui/inline-message.js +59 -59
  159. package/dist/pi-intercom/ui/message-renderers.js +147 -0
  160. package/dist/pi-intercom/ui/overlay.js +181 -0
  161. package/dist/pi-intercom/ui/session-format.js +124 -0
  162. package/dist/pi-intercom/ui/session-list.js +76 -43
  163. package/dist/pi-intercom/ui/tool-renderers.js +110 -0
  164. package/dist/pi-intercom/ui/topic-view.js +88 -0
  165. package/dist/pi-intercom/validation.js +59 -0
  166. package/dist/runs/background/async-chain-plan.js +265 -0
  167. package/dist/runs/background/async-control.js +134 -58
  168. package/dist/runs/background/async-execution.js +136 -633
  169. package/dist/runs/background/async-job-admission.js +40 -0
  170. package/dist/runs/background/async-job-projection.js +169 -0
  171. package/dist/runs/background/async-job-tracker.js +297 -327
  172. package/dist/runs/background/async-launch.js +216 -0
  173. package/dist/runs/background/async-plan.js +116 -0
  174. package/dist/runs/background/async-result-file.js +47 -23
  175. package/dist/runs/background/async-resume.js +195 -253
  176. package/dist/runs/background/async-run-discovery.js +213 -0
  177. package/dist/runs/background/async-run-location.js +135 -0
  178. package/dist/runs/background/async-run-record.js +1 -0
  179. package/dist/runs/background/async-run-repair.js +223 -0
  180. package/dist/runs/background/async-run-summary.js +1 -0
  181. package/dist/runs/background/async-single-plan.js +105 -0
  182. package/dist/runs/background/async-status-projection.js +155 -0
  183. package/dist/runs/background/async-status-render.js +117 -0
  184. package/dist/runs/background/async-status-validation.js +181 -0
  185. package/dist/runs/background/async-status.js +3 -442
  186. package/dist/runs/background/async-value.js +21 -0
  187. package/dist/runs/background/completion-dedupe.js +36 -12
  188. package/dist/runs/background/completion-delivery.js +211 -205
  189. package/dist/runs/background/completion-receipts.js +237 -0
  190. package/dist/runs/background/legacy-status-acceptance.js +34 -0
  191. package/dist/runs/background/notify.js +96 -48
  192. package/dist/runs/background/parallel-groups.js +36 -19
  193. package/dist/runs/background/result-payload.js +178 -0
  194. package/dist/runs/background/result-watcher.js +348 -339
  195. package/dist/runs/background/run-id-resolver.js +53 -33
  196. package/dist/runs/background/run-schemas.js +189 -0
  197. package/dist/runs/background/run-status-contracts.js +14 -0
  198. package/dist/runs/background/run-status-render.js +270 -0
  199. package/dist/runs/background/run-status.js +231 -339
  200. package/dist/runs/background/runner-attempt-output.js +155 -0
  201. package/dist/runs/background/runner-attempt.js +263 -0
  202. package/dist/runs/background/runner-child-executor.js +166 -0
  203. package/dist/runs/background/runner-child-observer.js +192 -0
  204. package/dist/runs/background/runner-completion-summary.js +71 -0
  205. package/dist/runs/background/runner-completion.js +211 -0
  206. package/dist/runs/background/runner-config.js +1 -0
  207. package/dist/runs/background/runner-contract.js +1 -0
  208. package/dist/runs/background/runner-dynamic.js +121 -0
  209. package/dist/runs/background/runner-finalization.js +149 -0
  210. package/dist/runs/background/runner-graph.js +68 -0
  211. package/dist/runs/background/runner-initial-status.js +60 -0
  212. package/dist/runs/background/runner-lifecycle.js +142 -0
  213. package/dist/runs/background/runner-monitor.js +309 -0
  214. package/dist/runs/background/runner-parallel.js +204 -0
  215. package/dist/runs/background/runner-prepare-step.js +86 -0
  216. package/dist/runs/background/runner-reporting.js +153 -0
  217. package/dist/runs/background/runner-status.js +148 -0
  218. package/dist/runs/background/runner-step-output.js +92 -0
  219. package/dist/runs/background/runner-step-record.js +45 -0
  220. package/dist/runs/background/runner-step.js +112 -0
  221. package/dist/runs/background/runner-streaming.js +173 -0
  222. package/dist/runs/background/runner-workflow.js +213 -0
  223. package/dist/runs/background/stale-run-reconciler.js +155 -292
  224. package/dist/runs/background/subagent-runner-launcher.js +33 -13
  225. package/dist/runs/background/subagent-runner.js +51 -1842
  226. package/dist/runs/background/top-level-async.js +2 -1
  227. package/dist/runs/foreground/chain-clarify.js +293 -1017
  228. package/dist/runs/foreground/clarification-draft.js +87 -0
  229. package/dist/runs/foreground/clarify-contracts.js +1 -0
  230. package/dist/runs/foreground/clarify-invocation.js +129 -38
  231. package/dist/runs/foreground/clarify-text-editor.js +210 -0
  232. package/dist/runs/foreground/clarify-view.js +167 -0
  233. package/dist/runs/foreground/execution-input.js +226 -353
  234. package/dist/runs/foreground/execution-routing.js +33 -0
  235. package/dist/runs/foreground/execution-timeout.js +81 -0
  236. package/dist/runs/foreground/foreground-control.js +222 -763
  237. package/dist/runs/foreground/foreground-memory.js +99 -0
  238. package/dist/runs/foreground/invocation-context.js +55 -0
  239. package/dist/runs/foreground/invocation-execution.js +331 -0
  240. package/dist/runs/foreground/invocation-expansion.js +48 -0
  241. package/dist/runs/foreground/management-actions.js +295 -0
  242. package/dist/runs/foreground/nested-control.js +179 -0
  243. package/dist/runs/foreground/question-continuation.js +78 -0
  244. package/dist/runs/foreground/question-control.js +155 -50
  245. package/dist/runs/foreground/resume-target.js +221 -0
  246. package/dist/runs/foreground/revival-launch-policy.js +58 -0
  247. package/dist/runs/foreground/run-async-path.js +190 -178
  248. package/dist/runs/foreground/run-interrupt.js +164 -0
  249. package/dist/runs/foreground/run-nudge.js +278 -0
  250. package/dist/runs/foreground/saved-revival.js +251 -0
  251. package/dist/runs/foreground/subagent-executor.js +135 -424
  252. package/dist/runs/foreground/subagent-params.js +67 -30
  253. package/dist/runs/foreground/wait-observation.js +104 -0
  254. package/dist/runs/foreground/wait-registration.js +17 -0
  255. package/dist/runs/foreground/wait-run.js +283 -123
  256. package/dist/runs/shared/acceptance-contract.js +193 -99
  257. package/dist/runs/shared/acceptance-defaults.js +1 -0
  258. package/dist/runs/shared/acceptance-evaluation.js +201 -181
  259. package/dist/runs/shared/acceptance-finalization.js +256 -180
  260. package/dist/runs/shared/acceptance-outcome.js +53 -0
  261. package/dist/runs/shared/acceptance-reports.js +137 -72
  262. package/dist/runs/shared/acceptance-submission.js +167 -0
  263. package/dist/runs/shared/acceptance-verification.js +100 -0
  264. package/dist/runs/shared/chain-outputs.js +80 -52
  265. package/dist/runs/shared/child-attempt-types.js +1 -0
  266. package/dist/runs/shared/child-attempt.js +314 -667
  267. package/dist/runs/shared/child-boundaries.js +173 -0
  268. package/dist/runs/shared/child-context-filter.js +44 -0
  269. package/dist/runs/shared/child-event-observer.js +390 -0
  270. package/dist/runs/shared/child-execution-cwd.js +73 -43
  271. package/dist/runs/shared/child-execution-outcome.js +72 -0
  272. package/dist/runs/shared/child-invocation.js +54 -0
  273. package/dist/runs/shared/child-message-validation.js +215 -0
  274. package/dist/runs/shared/child-native-accounting.js +106 -0
  275. package/dist/runs/shared/child-native-observation.js +69 -0
  276. package/dist/runs/shared/child-observation-spool.js +164 -0
  277. package/dist/runs/shared/child-observations.js +157 -76
  278. package/dist/runs/shared/child-presence.js +3 -0
  279. package/dist/runs/shared/child-route-env.js +119 -0
  280. package/dist/runs/shared/child-stream-observer.js +221 -0
  281. package/dist/runs/shared/child-stream-projection.js +137 -0
  282. package/dist/runs/shared/child-structured-tool.js +27 -0
  283. package/dist/runs/shared/claude-code.js +121 -158
  284. package/dist/runs/shared/claude-model.js +42 -0
  285. package/dist/runs/shared/claude-session.js +83 -0
  286. package/dist/runs/shared/claude-types.js +1 -0
  287. package/dist/runs/shared/completion-guard.js +18 -18
  288. package/dist/runs/shared/control-event-policy.js +8 -0
  289. package/dist/runs/shared/control-notification-owner.js +13 -0
  290. package/dist/runs/shared/dynamic-fanout.js +188 -173
  291. package/dist/runs/shared/dynamic-item.js +102 -0
  292. package/dist/runs/shared/history-index.js +62 -36
  293. package/dist/runs/shared/legacy-async-recovery.js +114 -0
  294. package/dist/runs/shared/legacy-output-recovery.js +148 -0
  295. package/dist/runs/shared/mcp-config-schema.js +36 -0
  296. package/dist/runs/shared/mcp-direct-tool-allowlist.js +147 -116
  297. package/dist/runs/shared/model-fallback.js +97 -38
  298. package/dist/runs/shared/mutating-tool-guard.js +128 -88
  299. package/dist/runs/shared/native-finalization-types.js +1 -0
  300. package/dist/runs/shared/native-finalization.js +155 -76
  301. package/dist/runs/shared/native-usage.js +238 -98
  302. package/dist/runs/shared/nested-control-records.js +128 -0
  303. package/dist/runs/shared/nested-events.js +8 -700
  304. package/dist/runs/shared/nested-path.js +32 -28
  305. package/dist/runs/shared/nested-projection.js +155 -0
  306. package/dist/runs/shared/nested-protocol.js +18 -0
  307. package/dist/runs/shared/nested-registry.js +191 -0
  308. package/dist/runs/shared/nested-render.js +106 -58
  309. package/dist/runs/shared/nested-route.js +220 -0
  310. package/dist/runs/shared/nested-validation.js +216 -0
  311. package/dist/runs/shared/owned-child-evidence.js +125 -0
  312. package/dist/runs/shared/owned-run-list.js +146 -0
  313. package/dist/runs/shared/owned-run-observation.js +267 -0
  314. package/dist/runs/shared/owned-run-progress.js +109 -0
  315. package/dist/runs/shared/owned-run-read-state.js +1 -0
  316. package/dist/runs/shared/owned-run-results.js +202 -0
  317. package/dist/runs/shared/owned-run-status.js +180 -0
  318. package/dist/runs/shared/owned-run-view.js +51 -0
  319. package/dist/runs/shared/owned-workflow-details.js +17 -0
  320. package/dist/runs/shared/owner-output.js +43 -0
  321. package/dist/runs/shared/parallel-utils.js +36 -28
  322. package/dist/runs/shared/parent-receipts.js +90 -32
  323. package/dist/runs/shared/parent-usage.js +169 -57
  324. package/dist/runs/shared/pi-args.js +108 -201
  325. package/dist/runs/shared/pi-launch-input.js +1 -0
  326. package/dist/runs/shared/pi-session-launch.js +86 -0
  327. package/dist/runs/shared/pi-spawn.js +35 -21
  328. package/dist/runs/shared/run-history-queries.js +126 -35
  329. package/dist/runs/shared/run-history.js +101 -35
  330. package/dist/runs/shared/run-metadata-paths.js +14 -0
  331. package/dist/runs/shared/run-metadata.js +158 -0
  332. package/dist/runs/shared/run-persistence.js +131 -0
  333. package/dist/runs/shared/run-records.js +9 -679
  334. package/dist/runs/shared/run-restoration.js +338 -0
  335. package/dist/runs/shared/run-state-owner.js +26 -0
  336. package/dist/runs/shared/session-cwd-preload.js +27 -18
  337. package/dist/runs/shared/single-output.js +148 -70
  338. package/dist/runs/shared/stable-value.js +15 -0
  339. package/dist/runs/shared/streaming-text.js +23 -12
  340. package/dist/runs/shared/structured-output.js +25 -13
  341. package/dist/runs/shared/subagent-control.js +167 -100
  342. package/dist/runs/shared/subagent-prompt-runtime.js +27 -130
  343. package/dist/runs/shared/subagent-tool-loop-guard.js +81 -52
  344. package/dist/runs/shared/supervisor-question-state.js +145 -0
  345. package/dist/runs/shared/supervisor-questions.js +185 -254
  346. package/dist/runs/shared/workflow-graph.js +259 -167
  347. package/dist/runs/shared/workflow-policy.js +120 -48
  348. package/dist/runs/shared/worktree-contract.js +1 -0
  349. package/dist/runs/shared/worktree-hook.js +138 -0
  350. package/dist/runs/shared/worktree-location.js +47 -0
  351. package/dist/runs/shared/worktree-preservation.js +8 -0
  352. package/dist/runs/shared/worktree-process.js +107 -0
  353. package/dist/runs/shared/worktree.js +183 -322
  354. package/dist/shared/agent-context-policy.js +34 -44
  355. package/dist/shared/agent-dir.js +14 -0
  356. package/dist/shared/artifacts.js +30 -12
  357. package/dist/shared/atomic-json.js +4 -1
  358. package/dist/shared/chain-files.js +72 -0
  359. package/dist/shared/concurrency.js +18 -0
  360. package/dist/shared/content-preview.js +115 -0
  361. package/dist/shared/cooperative.js +5 -2
  362. package/dist/shared/execution-cwd.js +13 -5
  363. package/dist/shared/file-coalescer.js +2 -1
  364. package/dist/shared/fork-context.js +10 -12
  365. package/dist/shared/formatters.js +45 -19
  366. package/dist/shared/journal-frames.js +164 -0
  367. package/dist/shared/journal-projections.js +121 -0
  368. package/dist/shared/journal-reader.js +6 -407
  369. package/dist/shared/json-projection.js +191 -0
  370. package/dist/shared/lazy-tools.js +19 -8
  371. package/dist/shared/message-output.js +116 -0
  372. package/dist/shared/model-info.js +31 -14
  373. package/dist/shared/native-import.js +20 -0
  374. package/dist/shared/native-journal.js +140 -0
  375. package/dist/shared/native-session.js +5 -2
  376. package/dist/shared/native-tui.js +7 -2
  377. package/dist/shared/native-typebox.js +15 -3
  378. package/dist/shared/output-page.js +63 -0
  379. package/dist/shared/post-exit-stdio-guard.js +128 -88
  380. package/dist/shared/prompt-sections.js +2 -0
  381. package/dist/shared/session-entries.js +70 -35
  382. package/dist/shared/session-identity.js +5 -3
  383. package/dist/shared/settings.js +62 -192
  384. package/dist/shared/status-format.js +97 -37
  385. package/dist/shared/task-file-policy.js +24 -0
  386. package/dist/shared/temp-root.js +44 -15
  387. package/dist/shared/types/acceptance.js +1 -0
  388. package/dist/shared/types/async.js +1 -0
  389. package/dist/shared/types/config.js +1 -0
  390. package/dist/shared/types/details.js +1 -0
  391. package/dist/shared/types/history.js +1 -0
  392. package/dist/shared/types/inputs.js +1 -0
  393. package/dist/shared/types/intercom.js +1 -0
  394. package/dist/shared/types/launch.js +1 -0
  395. package/dist/shared/types/messages.js +1 -0
  396. package/dist/shared/types/nested.js +1 -0
  397. package/dist/shared/types/owned-runs.js +1 -0
  398. package/dist/shared/types/progress.js +1 -0
  399. package/dist/shared/types/questions.js +1 -0
  400. package/dist/shared/types/results.js +1 -0
  401. package/dist/shared/types/state.js +1 -0
  402. package/dist/shared/types/usage.js +1 -0
  403. package/dist/shared/types/workflow.js +1 -0
  404. package/dist/shared/types.js +75 -30
  405. package/dist/shared/unknown.js +52 -0
  406. package/dist/shared/utils.js +54 -260
  407. package/dist/slash/command-discovery.js +82 -0
  408. package/dist/slash/command-input.js +190 -0
  409. package/dist/slash/prompt-template-bridge.js +94 -270
  410. package/dist/slash/prompt-template-contract.js +77 -0
  411. package/dist/slash/prompt-template-results.js +125 -0
  412. package/dist/slash/request-protocol.js +68 -0
  413. package/dist/slash/result-delivery.js +237 -0
  414. package/dist/slash/saved-chain.js +78 -0
  415. package/dist/slash/slash-bridge.js +123 -89
  416. package/dist/slash/slash-commands.js +101 -572
  417. package/dist/slash/slash-live-state.js +68 -47
  418. package/dist/tui/action-hints.js +50 -22
  419. package/dist/tui/activity-lines.js +32 -0
  420. package/dist/tui/agent-browser.js +290 -0
  421. package/dist/tui/agent-controls.js +289 -0
  422. package/dist/tui/agent-conversation.js +444 -0
  423. package/dist/tui/agent-dock.js +171 -0
  424. package/dist/tui/agent-history.js +50 -183
  425. package/dist/tui/agent-model.js +62 -0
  426. package/dist/tui/agent-picker.js +262 -0
  427. package/dist/tui/agent-view.js +232 -1537
  428. package/dist/tui/compact-results.js +193 -0
  429. package/dist/tui/conversation-details.js +190 -0
  430. package/dist/tui/conversation-frame.js +135 -0
  431. package/dist/tui/conversation-history.js +201 -0
  432. package/dist/tui/conversation-menu.js +78 -0
  433. package/dist/tui/conversation-notices.js +79 -0
  434. package/dist/tui/conversation-selection.js +58 -0
  435. package/dist/tui/conversation-viewport.js +129 -0
  436. package/dist/tui/direction-message.js +27 -0
  437. package/dist/tui/display.js +282 -0
  438. package/dist/tui/editor-viewport.js +18 -0
  439. package/dist/tui/expanded-content.js +87 -0
  440. package/dist/tui/expanded-multi.js +170 -0
  441. package/dist/tui/expanded-single.js +65 -0
  442. package/dist/tui/history-cards.js +216 -0
  443. package/dist/tui/history-display.js +238 -0
  444. package/dist/tui/history-json.js +16 -0
  445. package/dist/tui/history-metadata.js +152 -0
  446. package/dist/tui/history-record.js +251 -0
  447. package/dist/tui/history-text.js +34 -0
  448. package/dist/tui/indexed-history.js +116 -0
  449. package/dist/tui/nested-widget.js +120 -0
  450. package/dist/tui/picker-page.js +59 -0
  451. package/dist/tui/render.js +58 -1501
  452. package/dist/tui/result-facts.js +64 -0
  453. package/dist/tui/task-observation.js +59 -0
  454. package/dist/tui/task-store.js +366 -0
  455. package/dist/tui/text-values.js +16 -0
  456. package/dist/tui/view-connections.js +47 -0
  457. package/dist/tui/view-model.js +94 -0
  458. package/dist/tui/view-persistence.js +51 -0
  459. package/dist/tui/view-ports.js +1 -0
  460. package/dist/tui/view-session.js +37 -0
  461. package/dist/tui/widget-details.js +144 -0
  462. package/dist/tui/widget-status.js +245 -0
  463. package/dist/tui/widget.js +145 -0
  464. package/dist/tui/workflow-labels.js +223 -0
  465. package/dist/tui/workflow-outcomes.js +124 -0
  466. package/docs/code-quality.md +38 -0
  467. package/docs/development.md +99 -0
  468. package/docs/intercom-bridge.md +23 -18
  469. package/docs/intercom.md +132 -106
  470. package/docs/performance.md +19 -17
  471. package/docs/reference.md +1472 -0
  472. package/generated/RunValidators.d.ts +28 -0
  473. package/generated/RunValidators.js +2 -0
  474. package/package.json +27 -6
  475. package/prompts/parallel-cleanup.md +3 -0
  476. package/prompts/parallel-context-build.md +3 -0
  477. package/prompts/parallel-handoff-plan.md +6 -0
  478. package/prompts/parallel-research.md +4 -0
  479. package/prompts/parallel-review.md +2 -0
  480. package/prompts/review-loop.md +2 -0
  481. package/scripts/build.mjs +129 -85
  482. package/scripts/check-compat.mjs +48 -11
  483. package/scripts/compat-host.mjs +35 -7
  484. package/scripts/generate-run-schemas.mjs +210 -0
  485. package/scripts/intercom-bridge-credentials.mjs +155 -28
  486. package/scripts/intercom-remote.mjs +175 -50
  487. package/scripts/local-install-smoke.mjs +47 -37
  488. package/scripts/native-package-smoke.mjs +135 -25
  489. package/scripts/package-smoke.mjs +360 -140
  490. package/scripts/prepare.mjs +54 -34
  491. package/scripts/real-pi-processes.mjs +261 -0
  492. package/scripts/real-pi-smoke.mjs +432 -351
  493. package/scripts/run-tests.mjs +29 -15
  494. package/skills/pi-intercom/SKILL.md +11 -11
  495. package/skills/pi-intercom/references/peer-sessions.md +7 -2
  496. package/skills/pi-subagents/SKILL.md +1 -1
  497. package/skills/pi-subagents/references/full-orchestration-guide.md +254 -112
  498. package/src/agents/agent-defaults.ts +13 -0
  499. package/src/agents/agent-definition.ts +253 -0
  500. package/src/agents/agent-management.ts +111 -808
  501. package/src/agents/agent-scope.ts +7 -3
  502. package/src/agents/agent-selection.ts +26 -16
  503. package/src/agents/agent-serializer.ts +84 -93
  504. package/src/agents/agents.ts +199 -906
  505. package/src/agents/builtin-overrides.ts +297 -0
  506. package/src/agents/chain-serializer.ts +3 -329
  507. package/src/agents/config-values.ts +57 -0
  508. package/src/agents/discovery-paths.ts +134 -0
  509. package/src/agents/frontmatter.ts +35 -17
  510. package/src/agents/identity.ts +49 -18
  511. package/src/agents/json-chain.ts +305 -0
  512. package/src/agents/management-config.ts +342 -0
  513. package/src/agents/management-format.ts +243 -0
  514. package/src/agents/management-persistence.ts +408 -0
  515. package/src/agents/management-targets.ts +260 -0
  516. package/src/agents/management-warnings.ts +115 -0
  517. package/src/agents/markdown-chain.ts +237 -0
  518. package/src/agents/package-agents.ts +231 -0
  519. package/src/agents/skill-files.ts +182 -0
  520. package/src/agents/skill-search-paths.ts +289 -0
  521. package/src/agents/skill-types.ts +31 -0
  522. package/src/agents/skills.ts +152 -610
  523. package/src/extension/acceptance-schema.ts +88 -0
  524. package/src/extension/chain-schema.ts +276 -0
  525. package/src/extension/child-runtime.ts +302 -0
  526. package/src/extension/compact-tools.ts +259 -102
  527. package/src/extension/config-schema.ts +59 -0
  528. package/src/extension/config.ts +39 -17
  529. package/src/extension/control-notice-schema.ts +55 -0
  530. package/src/extension/control-notices.ts +57 -33
  531. package/src/extension/delegation-bridges.ts +59 -0
  532. package/src/extension/doctor.ts +171 -135
  533. package/src/extension/everyday-schemas.ts +252 -0
  534. package/src/extension/fanout-child.ts +29 -308
  535. package/src/extension/index.ts +17 -642
  536. package/src/extension/message-renderers.ts +334 -0
  537. package/src/extension/nested-control-inbox.ts +196 -0
  538. package/src/extension/parent-runtime.ts +405 -0
  539. package/src/extension/primary-tool.ts +130 -0
  540. package/src/extension/result-snapshot.ts +35 -0
  541. package/src/extension/runtime-reload.ts +72 -0
  542. package/src/extension/runtime-storage.ts +49 -0
  543. package/src/extension/schema-overrides.ts +114 -0
  544. package/src/extension/schemas.ts +451 -418
  545. package/src/extension/tool-input.ts +40 -13
  546. package/src/extension/tool-result.ts +45 -14
  547. package/src/history/answer-texts.ts +74 -0
  548. package/src/history/canonical-result.ts +127 -51
  549. package/src/history/canonical-selection.ts +54 -0
  550. package/src/history/database-generation.ts +86 -0
  551. package/src/history/database.ts +86 -48
  552. package/src/history/entry-ingestion.ts +129 -0
  553. package/src/history/index.ts +426 -148
  554. package/src/history/ingest.ts +372 -342
  555. package/src/history/output-digests.ts +41 -0
  556. package/src/history/preview.ts +235 -0
  557. package/src/history/prune.ts +48 -0
  558. package/src/history/queries.ts +122 -256
  559. package/src/history/query-scope.ts +278 -0
  560. package/src/history/rows.ts +118 -0
  561. package/src/history/run-projection.ts +164 -0
  562. package/src/history/run-queries.ts +199 -0
  563. package/src/history/schema.ts +34 -0
  564. package/src/history/search-queries.ts +216 -0
  565. package/src/history/selected-record.ts +140 -0
  566. package/src/history/source-file.ts +51 -0
  567. package/src/history/source-watches.ts +156 -0
  568. package/src/history/store.ts +310 -197
  569. package/src/history/text-chunks.ts +181 -0
  570. package/src/history/text.ts +43 -0
  571. package/src/history/transcript-queries.ts +251 -0
  572. package/src/history/types.ts +27 -164
  573. package/src/history/values.ts +36 -0
  574. package/src/history/wire-inputs.ts +141 -0
  575. package/src/history/wire-models.ts +51 -0
  576. package/src/history/worker-runtime.ts +502 -0
  577. package/src/history/worker.ts +94 -336
  578. package/src/intercom/intercom-bridge.ts +136 -69
  579. package/src/intercom/live-intercom.ts +197 -92
  580. package/src/intercom/nested-result-tree.ts +213 -0
  581. package/src/intercom/result-delivery.ts +56 -0
  582. package/src/intercom/result-intercom.ts +364 -382
  583. package/src/pi-intercom/agent-dir.ts +9 -3
  584. package/src/pi-intercom/async.ts +33 -0
  585. package/src/pi-intercom/bridge-ask.ts +64 -0
  586. package/src/pi-intercom/bridge-config.ts +101 -0
  587. package/src/pi-intercom/bridge-http.ts +97 -0
  588. package/src/pi-intercom/bridge-protocol.ts +123 -0
  589. package/src/pi-intercom/bridge-routes.ts +90 -0
  590. package/src/pi-intercom/bridge-session.ts +506 -0
  591. package/src/pi-intercom/bridge.ts +258 -547
  592. package/src/pi-intercom/broker/broker.ts +326 -263
  593. package/src/pi-intercom/broker/client.ts +273 -354
  594. package/src/pi-intercom/broker/connection.ts +192 -0
  595. package/src/pi-intercom/broker/framing.ts +11 -6
  596. package/src/pi-intercom/broker/paths.ts +28 -25
  597. package/src/pi-intercom/broker/pid.ts +68 -24
  598. package/src/pi-intercom/broker/snapshot-reader.ts +91 -0
  599. package/src/pi-intercom/broker/snapshots.ts +241 -0
  600. package/src/pi-intercom/broker/spawn.ts +58 -34
  601. package/src/pi-intercom/config.ts +113 -108
  602. package/src/pi-intercom/connection.ts +346 -0
  603. package/src/pi-intercom/identity.ts +71 -0
  604. package/src/pi-intercom/inbound-delivery.ts +450 -0
  605. package/src/pi-intercom/inbound-journal.ts +350 -0
  606. package/src/pi-intercom/inbound-record.ts +141 -0
  607. package/src/pi-intercom/inbound.ts +237 -0
  608. package/src/pi-intercom/index.ts +86 -2923
  609. package/src/pi-intercom/inspection.ts +82 -0
  610. package/src/pi-intercom/interview.ts +295 -0
  611. package/src/pi-intercom/lifecycle.ts +183 -0
  612. package/src/pi-intercom/message-format.ts +77 -0
  613. package/src/pi-intercom/outbound.ts +418 -0
  614. package/src/pi-intercom/peer-awareness.ts +84 -0
  615. package/src/pi-intercom/reply-tracker.ts +121 -59
  616. package/src/pi-intercom/reply-wait.ts +247 -0
  617. package/src/pi-intercom/runtime-types.ts +97 -0
  618. package/src/pi-intercom/session-events.ts +247 -0
  619. package/src/pi-intercom/session-targets.ts +124 -72
  620. package/src/pi-intercom/subagent-events.ts +299 -0
  621. package/src/pi-intercom/subagent-live-events.ts +212 -109
  622. package/src/pi-intercom/supervisor.ts +362 -0
  623. package/src/pi-intercom/tool-arguments.ts +120 -0
  624. package/src/pi-intercom/tool-registration.ts +121 -0
  625. package/src/pi-intercom/tool-results.ts +54 -0
  626. package/src/pi-intercom/tool-schemas.ts +134 -0
  627. package/src/pi-intercom/tools.ts +80 -0
  628. package/src/pi-intercom/topic-actions.ts +243 -0
  629. package/src/pi-intercom/topics.ts +267 -121
  630. package/src/pi-intercom/transport.ts +14 -0
  631. package/src/pi-intercom/types.ts +288 -190
  632. package/src/pi-intercom/ui/compose-paste.ts +109 -0
  633. package/src/pi-intercom/ui/compose.ts +173 -157
  634. package/src/pi-intercom/ui/inline-message.ts +80 -62
  635. package/src/pi-intercom/ui/message-renderers.ts +182 -0
  636. package/src/pi-intercom/ui/overlay.ts +238 -0
  637. package/src/pi-intercom/ui/session-format.ts +146 -0
  638. package/src/pi-intercom/ui/session-list.ts +145 -62
  639. package/src/pi-intercom/ui/tool-renderers.ts +148 -0
  640. package/src/pi-intercom/ui/topic-view.ts +107 -0
  641. package/src/pi-intercom/validation.ts +69 -0
  642. package/src/runs/background/async-chain-plan.ts +451 -0
  643. package/src/runs/background/async-control.ts +201 -71
  644. package/src/runs/background/async-execution.ts +206 -829
  645. package/src/runs/background/async-job-admission.ts +54 -0
  646. package/src/runs/background/async-job-projection.ts +225 -0
  647. package/src/runs/background/async-job-tracker.ts +416 -360
  648. package/src/runs/background/async-launch.ts +319 -0
  649. package/src/runs/background/async-plan.ts +285 -0
  650. package/src/runs/background/async-result-file.ts +79 -37
  651. package/src/runs/background/async-resume.ts +338 -285
  652. package/src/runs/background/async-run-discovery.ts +270 -0
  653. package/src/runs/background/async-run-location.ts +194 -0
  654. package/src/runs/background/async-run-record.ts +14 -0
  655. package/src/runs/background/async-run-repair.ts +307 -0
  656. package/src/runs/background/async-run-summary.ts +71 -0
  657. package/src/runs/background/async-single-plan.ts +168 -0
  658. package/src/runs/background/async-status-projection.ts +188 -0
  659. package/src/runs/background/async-status-render.ts +164 -0
  660. package/src/runs/background/async-status-validation.ts +238 -0
  661. package/src/runs/background/async-status.ts +9 -481
  662. package/src/runs/background/async-value.ts +29 -0
  663. package/src/runs/background/completion-dedupe.ts +103 -54
  664. package/src/runs/background/completion-delivery.ts +313 -187
  665. package/src/runs/background/completion-receipts.ts +309 -0
  666. package/src/runs/background/legacy-status-acceptance.ts +38 -0
  667. package/src/runs/background/notify.ts +167 -106
  668. package/src/runs/background/parallel-groups.ts +95 -46
  669. package/src/runs/background/result-payload.ts +268 -0
  670. package/src/runs/background/result-watcher.ts +484 -387
  671. package/src/runs/background/run-id-resolver.ts +126 -57
  672. package/src/runs/background/run-schemas.ts +240 -0
  673. package/src/runs/background/run-status-contracts.ts +46 -0
  674. package/src/runs/background/run-status-render.ts +364 -0
  675. package/src/runs/background/run-status.ts +338 -356
  676. package/src/runs/background/runner-attempt-output.ts +281 -0
  677. package/src/runs/background/runner-attempt.ts +354 -0
  678. package/src/runs/background/runner-child-executor.ts +232 -0
  679. package/src/runs/background/runner-child-observer.ts +270 -0
  680. package/src/runs/background/runner-completion-summary.ts +106 -0
  681. package/src/runs/background/runner-completion.ts +274 -0
  682. package/src/runs/background/runner-config.ts +52 -0
  683. package/src/runs/background/runner-contract.ts +51 -0
  684. package/src/runs/background/runner-dynamic.ts +185 -0
  685. package/src/runs/background/runner-finalization.ts +203 -0
  686. package/src/runs/background/runner-graph.ts +91 -0
  687. package/src/runs/background/runner-initial-status.ts +72 -0
  688. package/src/runs/background/runner-lifecycle.ts +159 -0
  689. package/src/runs/background/runner-monitor.ts +399 -0
  690. package/src/runs/background/runner-parallel.ts +280 -0
  691. package/src/runs/background/runner-prepare-step.ts +128 -0
  692. package/src/runs/background/runner-reporting.ts +192 -0
  693. package/src/runs/background/runner-status.ts +220 -0
  694. package/src/runs/background/runner-step-output.ts +158 -0
  695. package/src/runs/background/runner-step-record.ts +76 -0
  696. package/src/runs/background/runner-step.ts +150 -0
  697. package/src/runs/background/runner-streaming.ts +220 -0
  698. package/src/runs/background/runner-workflow.ts +284 -0
  699. package/src/runs/background/stale-run-reconciler.ts +279 -336
  700. package/src/runs/background/subagent-runner-launcher.ts +67 -35
  701. package/src/runs/background/subagent-runner.ts +80 -2048
  702. package/src/runs/background/top-level-async.ts +9 -7
  703. package/src/runs/foreground/chain-clarify.ts +499 -1334
  704. package/src/runs/foreground/clarification-draft.ts +92 -0
  705. package/src/runs/foreground/clarify-contracts.ts +33 -0
  706. package/src/runs/foreground/clarify-invocation.ts +192 -65
  707. package/src/runs/foreground/clarify-text-editor.ts +229 -0
  708. package/src/runs/foreground/clarify-view.ts +222 -0
  709. package/src/runs/foreground/execution-input.ts +358 -424
  710. package/src/runs/foreground/execution-routing.ts +41 -0
  711. package/src/runs/foreground/execution-timeout.ts +92 -0
  712. package/src/runs/foreground/foreground-control.ts +333 -812
  713. package/src/runs/foreground/foreground-memory.ts +132 -0
  714. package/src/runs/foreground/invocation-context.ts +88 -0
  715. package/src/runs/foreground/invocation-execution.ts +465 -0
  716. package/src/runs/foreground/invocation-expansion.ts +58 -0
  717. package/src/runs/foreground/management-actions.ts +360 -0
  718. package/src/runs/foreground/nested-control.ts +248 -0
  719. package/src/runs/foreground/question-continuation.ts +112 -0
  720. package/src/runs/foreground/question-control.ts +297 -70
  721. package/src/runs/foreground/resume-target.ts +330 -0
  722. package/src/runs/foreground/revival-launch-policy.ts +112 -0
  723. package/src/runs/foreground/run-async-path.ts +307 -223
  724. package/src/runs/foreground/run-interrupt.ts +202 -0
  725. package/src/runs/foreground/run-nudge.ts +355 -0
  726. package/src/runs/foreground/saved-revival.ts +356 -0
  727. package/src/runs/foreground/subagent-executor.ts +195 -493
  728. package/src/runs/foreground/subagent-params.ts +314 -242
  729. package/src/runs/foreground/wait-observation.ts +160 -0
  730. package/src/runs/foreground/wait-registration.ts +18 -0
  731. package/src/runs/foreground/wait-run.ts +361 -119
  732. package/src/runs/shared/acceptance-contract.ts +410 -209
  733. package/src/runs/shared/acceptance-defaults.ts +1 -0
  734. package/src/runs/shared/acceptance-evaluation.ts +314 -240
  735. package/src/runs/shared/acceptance-finalization.ts +447 -305
  736. package/src/runs/shared/acceptance-outcome.ts +69 -0
  737. package/src/runs/shared/acceptance-reports.ts +220 -126
  738. package/src/runs/shared/acceptance-submission.ts +251 -0
  739. package/src/runs/shared/acceptance-verification.ts +118 -0
  740. package/src/runs/shared/acceptance.ts +25 -25
  741. package/src/runs/shared/chain-outputs.ts +190 -98
  742. package/src/runs/shared/child-attempt-types.ts +118 -0
  743. package/src/runs/shared/child-attempt.ts +372 -608
  744. package/src/runs/shared/child-boundaries.ts +217 -0
  745. package/src/runs/shared/child-context-filter.ts +53 -0
  746. package/src/runs/shared/child-event-observer.ts +496 -0
  747. package/src/runs/shared/child-execution-cwd.ts +139 -68
  748. package/src/runs/shared/child-execution-outcome.ts +103 -0
  749. package/src/runs/shared/child-invocation.ts +74 -0
  750. package/src/runs/shared/child-message-validation.ts +233 -0
  751. package/src/runs/shared/child-native-accounting.ts +154 -0
  752. package/src/runs/shared/child-native-observation.ts +79 -0
  753. package/src/runs/shared/child-observation-spool.ts +188 -0
  754. package/src/runs/shared/child-observations.ts +219 -89
  755. package/src/runs/shared/child-presence.ts +6 -0
  756. package/src/runs/shared/child-route-env.ts +142 -0
  757. package/src/runs/shared/child-stream-observer.ts +302 -0
  758. package/src/runs/shared/child-stream-projection.ts +160 -0
  759. package/src/runs/shared/child-structured-tool.ts +38 -0
  760. package/src/runs/shared/claude-code.ts +201 -228
  761. package/src/runs/shared/claude-model.ts +67 -0
  762. package/src/runs/shared/claude-session.ts +124 -0
  763. package/src/runs/shared/claude-types.ts +31 -0
  764. package/src/runs/shared/completion-guard.ts +35 -26
  765. package/src/runs/shared/control-event-policy.ts +15 -0
  766. package/src/runs/shared/control-notification-owner.ts +20 -0
  767. package/src/runs/shared/dynamic-fanout.ts +402 -238
  768. package/src/runs/shared/dynamic-item.ts +114 -0
  769. package/src/runs/shared/history-index.ts +112 -51
  770. package/src/runs/shared/legacy-async-recovery.ts +153 -0
  771. package/src/runs/shared/legacy-output-recovery.ts +214 -0
  772. package/src/runs/shared/mcp-config-schema.ts +46 -0
  773. package/src/runs/shared/mcp-direct-tool-allowlist.ts +396 -307
  774. package/src/runs/shared/model-fallback.ts +302 -180
  775. package/src/runs/shared/mutating-tool-guard.ts +259 -158
  776. package/src/runs/shared/native-finalization-types.ts +29 -0
  777. package/src/runs/shared/native-finalization.ts +233 -109
  778. package/src/runs/shared/native-usage.ts +379 -96
  779. package/src/runs/shared/nested-control-records.ts +178 -0
  780. package/src/runs/shared/nested-events.ts +45 -752
  781. package/src/runs/shared/nested-path.ts +47 -34
  782. package/src/runs/shared/nested-projection.ts +230 -0
  783. package/src/runs/shared/nested-protocol.ts +67 -0
  784. package/src/runs/shared/nested-registry.ts +273 -0
  785. package/src/runs/shared/nested-render.ts +198 -100
  786. package/src/runs/shared/nested-route.ts +292 -0
  787. package/src/runs/shared/nested-validation.ts +291 -0
  788. package/src/runs/shared/owned-child-evidence.ts +184 -0
  789. package/src/runs/shared/owned-run-list.ts +187 -0
  790. package/src/runs/shared/owned-run-observation.ts +368 -0
  791. package/src/runs/shared/owned-run-progress.ts +172 -0
  792. package/src/runs/shared/owned-run-read-state.ts +7 -0
  793. package/src/runs/shared/owned-run-results.ts +289 -0
  794. package/src/runs/shared/owned-run-status.ts +239 -0
  795. package/src/runs/shared/owned-run-view.ts +65 -0
  796. package/src/runs/shared/owned-workflow-details.ts +24 -0
  797. package/src/runs/shared/owner-output.ts +73 -0
  798. package/src/runs/shared/parallel-utils.ts +93 -129
  799. package/src/runs/shared/parent-receipts.ts +177 -50
  800. package/src/runs/shared/parent-usage.ts +314 -91
  801. package/src/runs/shared/pi-args.ts +241 -311
  802. package/src/runs/shared/pi-launch-input.ts +48 -0
  803. package/src/runs/shared/pi-session-launch.ts +94 -0
  804. package/src/runs/shared/pi-spawn.ts +57 -33
  805. package/src/runs/shared/run-history-queries.ts +189 -38
  806. package/src/runs/shared/run-history.ts +197 -76
  807. package/src/runs/shared/run-id-ambiguity.ts +7 -3
  808. package/src/runs/shared/run-metadata-paths.ts +17 -0
  809. package/src/runs/shared/run-metadata.ts +240 -0
  810. package/src/runs/shared/run-persistence.ts +181 -0
  811. package/src/runs/shared/run-records.ts +19 -607
  812. package/src/runs/shared/run-restoration.ts +455 -0
  813. package/src/runs/shared/run-state-owner.ts +31 -0
  814. package/src/runs/shared/session-cwd-preload.ts +39 -19
  815. package/src/runs/shared/single-output.ts +358 -205
  816. package/src/runs/shared/stable-value.ts +15 -0
  817. package/src/runs/shared/streaming-text.ts +39 -12
  818. package/src/runs/shared/structured-output.ts +76 -46
  819. package/src/runs/shared/subagent-control.ts +330 -197
  820. package/src/runs/shared/subagent-prompt-runtime.ts +92 -174
  821. package/src/runs/shared/subagent-tool-loop-guard.ts +118 -68
  822. package/src/runs/shared/supervisor-question-state.ts +221 -0
  823. package/src/runs/shared/supervisor-questions.ts +396 -401
  824. package/src/runs/shared/workflow-graph.ts +400 -198
  825. package/src/runs/shared/workflow-policy.ts +245 -110
  826. package/src/runs/shared/worktree-contract.ts +29 -0
  827. package/src/runs/shared/worktree-hook.ts +181 -0
  828. package/src/runs/shared/worktree-location.ts +61 -0
  829. package/src/runs/shared/worktree-preservation.ts +10 -0
  830. package/src/runs/shared/worktree-process.ts +132 -0
  831. package/src/runs/shared/worktree.ts +459 -587
  832. package/src/shared/agent-context-policy.ts +176 -145
  833. package/src/shared/agent-dir.ts +15 -0
  834. package/src/shared/artifacts.ts +99 -68
  835. package/src/shared/atomic-json.ts +14 -11
  836. package/src/shared/chain-files.ts +72 -0
  837. package/src/shared/concurrency.ts +27 -0
  838. package/src/shared/content-preview.ts +117 -0
  839. package/src/shared/cooperative.ts +17 -13
  840. package/src/shared/execution-cwd.ts +34 -9
  841. package/src/shared/file-coalescer.ts +24 -22
  842. package/src/shared/fork-context.ts +52 -48
  843. package/src/shared/formatters.ts +88 -47
  844. package/src/shared/journal-frames.ts +197 -0
  845. package/src/shared/journal-projections.ts +133 -0
  846. package/src/shared/journal-reader.ts +18 -333
  847. package/src/shared/json-projection.ts +224 -0
  848. package/src/shared/lazy-tools.ts +76 -27
  849. package/src/shared/message-output.ts +146 -0
  850. package/src/shared/model-info.ts +94 -58
  851. package/src/shared/native-import.ts +25 -0
  852. package/src/shared/native-journal.ts +172 -0
  853. package/src/shared/native-session.ts +8 -3
  854. package/src/shared/native-tui.ts +13 -3
  855. package/src/shared/native-typebox.ts +18 -3
  856. package/src/shared/output-page.ts +76 -0
  857. package/src/shared/post-exit-stdio-guard.ts +232 -172
  858. package/src/shared/prompt-sections.ts +13 -7
  859. package/src/shared/session-entries.ts +103 -44
  860. package/src/shared/session-identity.ts +17 -10
  861. package/src/shared/settings.ts +182 -367
  862. package/src/shared/status-format.ts +207 -98
  863. package/src/shared/stream-json.d.ts +10 -0
  864. package/src/shared/task-file-policy.ts +35 -0
  865. package/src/shared/temp-root.ts +122 -71
  866. package/src/shared/types/acceptance.ts +149 -0
  867. package/src/shared/types/async.ts +247 -0
  868. package/src/shared/types/config.ts +139 -0
  869. package/src/shared/types/details.ts +104 -0
  870. package/src/shared/types/history.ts +217 -0
  871. package/src/shared/types/inputs.ts +4 -0
  872. package/src/shared/types/intercom.ts +57 -0
  873. package/src/shared/types/launch.ts +39 -0
  874. package/src/shared/types/messages.ts +104 -0
  875. package/src/shared/types/nested.ts +153 -0
  876. package/src/shared/types/owned-runs.ts +148 -0
  877. package/src/shared/types/progress.ts +106 -0
  878. package/src/shared/types/questions.ts +103 -0
  879. package/src/shared/types/results.ts +119 -0
  880. package/src/shared/types/state.ts +56 -0
  881. package/src/shared/types/usage.ts +36 -0
  882. package/src/shared/types/workflow.ts +187 -0
  883. package/src/shared/types.ts +222 -1161
  884. package/src/shared/ui-mode.ts +1 -1
  885. package/src/shared/unknown.ts +59 -0
  886. package/src/shared/utils.ts +111 -305
  887. package/src/slash/command-discovery.ts +109 -0
  888. package/src/slash/command-input.ts +232 -0
  889. package/src/slash/prompt-template-bridge.ts +153 -400
  890. package/src/slash/prompt-template-contract.ts +152 -0
  891. package/src/slash/prompt-template-results.ts +189 -0
  892. package/src/slash/request-protocol.ts +87 -0
  893. package/src/slash/result-delivery.ts +312 -0
  894. package/src/slash/saved-chain.ts +115 -0
  895. package/src/slash/slash-bridge.ts +202 -156
  896. package/src/slash/slash-commands.ts +176 -598
  897. package/src/slash/slash-live-state.ts +287 -229
  898. package/src/tui/action-hints.ts +147 -61
  899. package/src/tui/activity-lines.ts +40 -0
  900. package/src/tui/agent-browser.ts +354 -0
  901. package/src/tui/agent-controls.ts +355 -0
  902. package/src/tui/agent-conversation.ts +514 -0
  903. package/src/tui/agent-dock.ts +250 -0
  904. package/src/tui/agent-history.ts +78 -203
  905. package/src/tui/agent-model.ts +78 -0
  906. package/src/tui/agent-picker.ts +325 -0
  907. package/src/tui/agent-view.ts +391 -1310
  908. package/src/tui/compact-results.ts +342 -0
  909. package/src/tui/conversation-details.ts +206 -0
  910. package/src/tui/conversation-frame.ts +184 -0
  911. package/src/tui/conversation-history.ts +246 -0
  912. package/src/tui/conversation-menu.ts +119 -0
  913. package/src/tui/conversation-notices.ts +116 -0
  914. package/src/tui/conversation-selection.ts +66 -0
  915. package/src/tui/conversation-viewport.ts +169 -0
  916. package/src/tui/direction-message.ts +35 -0
  917. package/src/tui/display.ts +409 -0
  918. package/src/tui/editor-viewport.ts +19 -0
  919. package/src/tui/expanded-content.ts +134 -0
  920. package/src/tui/expanded-multi.ts +264 -0
  921. package/src/tui/expanded-single.ts +92 -0
  922. package/src/tui/history-cards.ts +316 -0
  923. package/src/tui/history-display.ts +334 -0
  924. package/src/tui/history-json.ts +17 -0
  925. package/src/tui/history-metadata.ts +177 -0
  926. package/src/tui/history-record.ts +321 -0
  927. package/src/tui/history-text.ts +34 -0
  928. package/src/tui/indexed-history.ts +149 -0
  929. package/src/tui/nested-widget.ts +192 -0
  930. package/src/tui/picker-page.ts +80 -0
  931. package/src/tui/render.ts +100 -1546
  932. package/src/tui/result-facts.ts +99 -0
  933. package/src/tui/task-observation.ts +96 -0
  934. package/src/tui/task-store.ts +432 -0
  935. package/src/tui/text-values.ts +20 -0
  936. package/src/tui/view-connections.ts +64 -0
  937. package/src/tui/view-model.ts +181 -0
  938. package/src/tui/view-persistence.ts +57 -0
  939. package/src/tui/view-ports.ts +55 -0
  940. package/src/tui/view-session.ts +42 -0
  941. package/src/tui/widget-details.ts +220 -0
  942. package/src/tui/widget-status.ts +331 -0
  943. package/src/tui/widget.ts +208 -0
  944. package/src/tui/workflow-labels.ts +340 -0
  945. package/src/tui/workflow-outcomes.ts +150 -0
  946. package/tsconfig.build.json +11 -10
  947. package/tsconfig.json +30 -17
@@ -1,2929 +1,92 @@
1
- import { createHash } from "node:crypto";
2
- import { keyText, type ExtensionAPI, type ExtensionContext, type SessionEntry } from "@earendil-works/pi-coding-agent";
3
- import { randomUUID } from "crypto";
4
- import path from "node:path";
5
- import { fileURLToPath } from "node:url";
6
- import { StringEnum } from "@earendil-works/pi-ai";
7
- import { Type } from "../shared/native-typebox.ts";
8
- import { MouseRegion, Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
9
- import { IntercomClient, type SendResult } from "./broker/client.ts";
10
- import { isBrokerRunning, spawnBrokerIfNeeded } from "./broker/spawn.ts";
11
- import { SessionListOverlay } from "./ui/session-list.ts";
12
- import { ComposeOverlay, type ComposeResult } from "./ui/compose.ts";
13
- import { InlineMessageComponent } from "./ui/inline-message.ts";
14
- import { loadConfig, type IntercomConfig } from "./config.ts";
15
- import { isTopicSubscription, isTopicUpdate, type SessionInfo, type Message, type Attachment, type MessageDelivery, type QueueMode, type TopicUpdate, type TopicChange, type SessionSnapshot } from "./types.ts";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { loadConfig } from "./config.ts";
3
+ import { readChildOrchestratorMetadata } from "./identity.ts";
4
+ import { ReplyTracker } from "./reply-tracker.ts";
16
5
  import { IntercomTopics } from "./topics.ts";
17
- import { isDurableSupervisorQuestion, ReplyTracker } from "./reply-tracker.ts";
18
- import { filterProjectSessions, formatPeerAwarenessHint, formatSessionTarget, formatTargetOptions, PEER_AWARENESS_HINT, resolveSessionProjectId, targetDisplayName, resolveSessionTarget as resolveSessionTargetValue } from "./session-targets.ts";
19
- import { registerSubagentLiveEventHandlers } from "./subagent-live-events.ts";
20
- import { formatRunAction } from "../shared/status-format.ts";
21
- import { setPromptSection } from "../shared/prompt-sections.ts";
22
- import { activateTools, restoreLazyTools } from "../shared/lazy-tools.ts";
23
- import { runCooperatively } from "../shared/cooperative.ts";
24
- import { cancelSupervisorQuestion, createSupervisorQuestion, getRunMetadataDir, listSupervisorQuestionsAsync, readRunJson, readQuestionState, recordQuestionDelivery, saveQuestionAnswer, type SupervisorQuestion } from "../runs/shared/supervisor-questions.ts";
25
-
26
- const SUBAGENT_CONTROL_INTERCOM_EVENT = "subagent:control-intercom";
27
- const SUBAGENT_RESULT_INTERCOM_EVENT = "subagent:result-intercom";
28
- const SUBAGENT_RESULT_INTERCOM_DELIVERY_EVENT = "subagent:result-intercom-delivery";
29
- const SUBAGENT_INTERCOM_IDENTITY_REQUEST_EVENT = "subagent:intercom-identity-request";
30
- const SUBAGENT_INTERCOM_IDENTITY_RESPONSE_EVENT = "subagent:intercom-identity-response";
31
- const SUBAGENT_SUPERVISOR_QUESTION_RESOLVED_EVENT = "subagent:supervisor-question-resolved";
32
- const INBOUND_CHECKPOINT_TYPE = "intercom_delivery";
33
- const INTERCOM_DETACH_REQUEST_EVENT = "pi-intercom:detach-request";
34
- const INTERCOM_DETACH_RESPONSE_EVENT = "pi-intercom:detach-response";
35
- const INTERCOM_DETACH_RESPONSE_TIMEOUT_MS = 500;
36
- const INBOUND_FLUSH_DELAY_MS = 200;
37
- const INBOUND_IDLE_RETRY_MS = 500;
38
- const NON_UI_REPLACE_FLUSH_DELAY_MS = 1_600;
39
- const PEER_AWARENESS_LIST_TIMEOUT_MS = 75;
40
- const DEFAULT_UNNAMED_SESSION_ALIAS_PREFIX = "subagent-chat";
41
- const RECIPIENT_TURN_FAILED_PREFIX = "Recipient turn failed:";
42
- const RECIPIENT_TURN_FAILED_ATTACHMENT = "pi-intercom-recipient-turn-failure";
43
- const SUBAGENT_ORCHESTRATOR_TARGET_ENV = "PI_SUBAGENT_ORCHESTRATOR_TARGET";
44
- const SUBAGENT_RUN_ID_ENV = "PI_SUBAGENT_RUN_ID";
45
- const SUBAGENT_CHILD_AGENT_ENV = "PI_SUBAGENT_CHILD_AGENT";
46
- const SUBAGENT_CHILD_INDEX_ENV = "PI_SUBAGENT_CHILD_INDEX";
47
- const SUBAGENT_INTERCOM_SESSION_NAME_ENV = "PI_SUBAGENT_INTERCOM_SESSION_NAME";
48
- const PACKAGE_ROOT = fileURLToPath(new URL("../../", import.meta.url));
49
- const TOPICS_UNAVAILABLE = "Intercom topics are unavailable while an older broker is still running. Direct messaging and active sessions are unchanged. Let its sessions close normally; after it exits, reconnect with the updated package and retry. Nothing was stopped or restarted.";
50
-
51
- interface ChildOrchestratorMetadata {
52
- orchestratorTarget: string;
53
- runId: string;
54
- agent: string;
55
- index: string;
56
- sessionName?: string;
57
- }
58
-
59
- interface SubagentCompletion {
60
- completionId?: string;
61
- runId: string;
62
- status: string;
63
- children: Array<{ agent: string; index: number; status: string; intercomTarget: string }>;
64
- }
65
-
66
- interface InboundMessageEntry {
67
- from: SessionInfo;
68
- message: Message;
69
- replyCommand?: string;
70
- bodyText: string;
71
- subagentCompletion?: SubagentCompletion & { ownerSessionId: string };
72
- }
73
-
74
- interface PendingInboundMessage extends InboundMessageEntry {
75
- flushDelivery: "auto" | "passive" | "steer";
76
- stage: "queued" | "native";
77
- receivedAt: number;
78
- }
79
-
80
- type InboundCheckpoint = { entry: PendingInboundMessage }
81
- | { messageId: string; stage: PendingInboundMessage["stage"] | "discarded" | "reply-retired" };
82
-
83
- type RequestedDelivery = MessageDelivery | "auto";
84
- type InboundDelivery = "trigger" | "followUp" | "steer" | "passive";
85
-
86
- type ContactSupervisorReason = "need_decision" | "progress_update" | "interview_request";
87
- type IntercomSessionScope = "project" | "all";
88
-
89
- interface SupervisorInterviewQuestion extends Record<string, unknown> {
90
- id: string;
91
- type: "single" | "multi" | "text" | "image" | "info";
92
- question: string;
93
- options?: unknown[];
94
- }
95
-
96
- interface SupervisorInterviewRequest extends Record<string, unknown> {
97
- title?: string;
98
- description?: string;
99
- questions: SupervisorInterviewQuestion[];
100
- }
101
-
102
- interface SupervisorInterviewReply {
103
- responses: Array<{ id: string; value: unknown }>;
104
- }
105
-
106
- function getErrorMessage(error: unknown): string {
107
- return error instanceof Error ? error.message : String(error);
108
- }
109
-
110
- function toError(error: unknown): Error {
111
- return error instanceof Error ? error : new Error(String(error));
112
- }
113
-
114
- type RecoveryAction = { action: "list" | "pending" | "send" | "status"; guidance?: string };
115
-
116
- function failureDetails(reasonCode: string, nextActions: RecoveryAction[], details: Record<string, unknown> = {}): Record<string, unknown> {
117
- return { ...details, reasonCode, nextActions };
118
- }
119
-
120
- function replyFailureReason(message: string): "no_pending_reply" | "ambiguous_reply_target" | "reply_failed" {
121
- if (message.startsWith("No active intercom context") || message.startsWith("No pending ask")) return "no_pending_reply";
122
- if (message.startsWith("Multiple pending asks") || message.includes("too short")) return "ambiguous_reply_target";
123
- return "reply_failed";
124
- }
125
-
126
- function inboundIdFromCustomMessage(message: unknown): string | undefined {
127
- if (typeof message !== "object" || message === null) return undefined;
128
- const record = message as Record<string, unknown>;
129
- if (record.customType !== "intercom_message" && record.customType !== "subagent-human-message") return undefined;
130
- const details = record.details;
131
- if (typeof details !== "object" || details === null) return undefined;
132
- const inboundId = (details as { message?: { id?: unknown } }).message?.id;
133
- return typeof inboundId === "string" ? inboundId : undefined;
134
- }
135
-
136
- function getAssistantErrorMessage(message: unknown): string | null {
137
- if (typeof message !== "object" || message === null) {
138
- return null;
139
- }
140
- const record = message as Record<string, unknown>;
141
- if (record.role !== "assistant") {
142
- return null;
143
- }
144
- const errorMessage = typeof record.errorMessage === "string" && record.errorMessage.trim()
145
- ? record.errorMessage.trim()
146
- : undefined;
147
- if (record.stopReason === "error") {
148
- return errorMessage ?? "assistant turn failed";
149
- }
150
- return null;
151
- }
152
-
153
- function formatAttachments(attachments: Attachment[]): string {
154
- let text = "";
155
- for (const att of attachments) {
156
- if (att.language) {
157
- text += `\n\n---\n📎 ${att.name}\n~~~${att.language}\n${att.content}\n~~~`;
158
- } else {
159
- text += `\n\n---\n📎 ${att.name}\n${att.content}`;
160
- }
161
- }
162
- return text;
163
- }
164
- function readChildOrchestratorMetadata(): ChildOrchestratorMetadata | null {
165
- const orchestratorTarget = process.env[SUBAGENT_ORCHESTRATOR_TARGET_ENV]?.trim();
166
- const runId = process.env[SUBAGENT_RUN_ID_ENV]?.trim();
167
- const agent = process.env[SUBAGENT_CHILD_AGENT_ENV]?.trim();
168
- const index = process.env[SUBAGENT_CHILD_INDEX_ENV]?.trim();
169
- if (!orchestratorTarget || !runId || !agent || !index) {
170
- return null;
171
- }
172
- const sessionName = process.env[SUBAGENT_INTERCOM_SESSION_NAME_ENV]?.trim();
173
- return {
174
- orchestratorTarget,
175
- runId,
176
- agent,
177
- index,
178
- ...(sessionName ? { sessionName } : {}),
179
- };
180
- }
181
- function formatChildOrchestratorMessage(kind: "ask" | "update" | "interview", metadata: ChildOrchestratorMetadata, message: string): string {
182
- const heading = kind === "ask"
183
- ? "Subagent needs a supervisor decision."
184
- : kind === "interview"
185
- ? "Subagent requests a structured supervisor interview."
186
- : "Subagent progress update.";
187
- return [
188
- heading,
189
- `Run: ${metadata.runId}`,
190
- `Agent: ${metadata.agent}`,
191
- `Child index: ${metadata.index}`,
192
- metadata.sessionName ? `Child intercom target: ${metadata.sessionName}` : undefined,
193
- "",
194
- message,
195
- ].filter((line): line is string => line !== undefined).join("\n");
196
- }
197
-
198
- function validateSupervisorInterviewRequest(input: unknown): { ok: true; interview: SupervisorInterviewRequest } | { ok: false; error: string } {
199
- if (!input || typeof input !== "object" || Array.isArray(input)) {
200
- return { ok: false, error: "interview must be an object with a questions array" };
201
- }
202
-
203
- const raw = input as Record<string, unknown>;
204
- if (raw.title !== undefined && typeof raw.title !== "string") {
205
- return { ok: false, error: "interview.title must be a string when provided" };
206
- }
207
- if (raw.description !== undefined && typeof raw.description !== "string") {
208
- return { ok: false, error: "interview.description must be a string when provided" };
209
- }
210
- if (!Array.isArray(raw.questions) || raw.questions.length === 0) {
211
- return { ok: false, error: "interview.questions must be a non-empty array" };
212
- }
213
-
214
- const validTypes = new Set(["single", "multi", "text", "image", "info"]);
215
- const ids = new Set<string>();
216
- const questions: SupervisorInterviewQuestion[] = [];
217
-
218
- for (let index = 0; index < raw.questions.length; index++) {
219
- const questionInput = raw.questions[index];
220
- if (!questionInput || typeof questionInput !== "object" || Array.isArray(questionInput)) {
221
- return { ok: false, error: `interview.questions[${index}] must be an object` };
222
- }
223
- const question = questionInput as Record<string, unknown>;
224
- if (typeof question.id !== "string" || question.id.trim() === "") {
225
- return { ok: false, error: `interview.questions[${index}].id must be a non-empty string` };
226
- }
227
- const id = question.id.trim();
228
- if (ids.has(id)) {
229
- return { ok: false, error: `interview question id must be unique: ${id}` };
230
- }
231
- ids.add(id);
232
-
233
- if (typeof question.type !== "string" || !validTypes.has(question.type)) {
234
- return { ok: false, error: `interview.questions[${index}].type must be one of: single, multi, text, image, info` };
235
- }
236
- if (typeof question.question !== "string" || question.question.trim() === "") {
237
- return { ok: false, error: `interview.questions[${index}].question must be a non-empty string` };
238
- }
239
- if (question.context !== undefined && typeof question.context !== "string") {
240
- return { ok: false, error: `interview.questions[${index}].context must be a string when provided` };
241
- }
242
- let options: unknown[] | undefined;
243
- if (question.options !== undefined) {
244
- if (!Array.isArray(question.options)) {
245
- return { ok: false, error: `interview.questions[${index}].options must be an array when provided` };
246
- }
247
- options = [];
248
- for (let optionIndex = 0; optionIndex < question.options.length; optionIndex++) {
249
- const option = question.options[optionIndex];
250
- if (typeof option === "string") {
251
- const label = option.trim();
252
- if (!label) {
253
- return { ok: false, error: `interview.questions[${index}].options[${optionIndex}] must not be empty` };
254
- }
255
- options.push(label);
256
- } else if (!option || typeof option !== "object" || Array.isArray(option) || typeof (option as { label?: unknown }).label !== "string" || (option as { label: string }).label.trim() === "") {
257
- return { ok: false, error: `interview.questions[${index}].options[${optionIndex}] must be a non-empty string or an object with a non-empty label` };
258
- } else {
259
- options.push({ ...option, label: (option as { label: string }).label.trim() });
260
- }
261
- }
262
- }
263
- if ((question.type === "single" || question.type === "multi") && (!options || options.length === 0)) {
264
- return { ok: false, error: `interview.questions[${index}].options must be a non-empty array for ${question.type} questions` };
265
- }
266
- if (question.type !== "single" && question.type !== "multi" && options) {
267
- return { ok: false, error: `interview.questions[${index}].options is only valid for single and multi questions` };
268
- }
269
-
270
- questions.push({
271
- ...question,
272
- id,
273
- type: question.type as SupervisorInterviewQuestion["type"],
274
- question: question.question.trim(),
275
- ...(options ? { options } : {}),
276
- });
277
- }
278
-
279
- return {
280
- ok: true,
281
- interview: {
282
- ...raw,
283
- ...(typeof raw.title === "string" ? { title: raw.title.trim() } : {}),
284
- ...(typeof raw.description === "string" ? { description: raw.description.trim() } : {}),
285
- questions,
6
+ import { IntercomLifecycle } from "./lifecycle.ts";
7
+ import { InboundJournal } from "./inbound-journal.ts";
8
+ import { InboundDeliveryOwner } from "./inbound-delivery.ts";
9
+ import { ReplyWait } from "./reply-wait.ts";
10
+ import { IntercomConnection } from "./connection.ts";
11
+ import { IntercomInbound } from "./inbound.ts";
12
+ import { IntercomOutbound } from "./outbound.ts";
13
+ import { IntercomInspection } from "./inspection.ts";
14
+ import { IntercomTopicActions } from "./topic-actions.ts";
15
+ import { IntercomSupervisor } from "./supervisor.ts";
16
+ import { IntercomTools } from "./tools.ts";
17
+ import { IntercomOverlay } from "./ui/overlay.ts";
18
+ import { SubagentEventBridges } from "./subagent-events.ts";
19
+ import { IntercomSessionEvents } from "./session-events.ts";
20
+ import { PeerAwareness } from "./peer-awareness.ts";
21
+ import { registerMessageRenderers } from "./ui/message-renderers.ts";
22
+ import { registerIntercomTools } from "./tool-registration.ts";
23
+
24
+ export default function piIntercomExtension(pi: ExtensionAPI): void {
25
+ const config = loadConfig();
26
+ const child = readChildOrchestratorMetadata();
27
+ const lifecycle = new IntercomLifecycle(pi, config);
28
+ const replies = new ReplyTracker(config.askTimeoutMs);
29
+ const topics = new IntercomTopics(pi, () => lifecycle.live());
30
+ const journal = new InboundJournal(pi, lifecycle, replies);
31
+ const wait = new ReplyWait(pi, lifecycle, config.askTimeoutMs);
32
+ // Hooks are invoked only after construction, at a native session or event boundary.
33
+ const connection = new IntercomConnection(lifecycle, config, topics, {
34
+ restoring: () => delivery.restoring,
35
+ pendingAsks: () => replies.listPending().length,
36
+ incoming: (ctx, from, message) => inbound.receive(ctx, from, message),
37
+ peerLeft: (id) => {
38
+ wait.peerDisconnected(id);
39
+ journal.peerLeft(id);
286
40
  },
287
- };
288
- }
289
-
290
- function interviewOptionLabel(option: unknown): string {
291
- return typeof option === "string" ? option : (option as { label: string }).label;
292
- }
293
-
294
- function interviewExampleValue(question: SupervisorInterviewQuestion): unknown {
295
- if (question.type === "multi") {
296
- return question.options?.slice(0, 2).map(interviewOptionLabel) ?? [];
297
- }
298
- if (question.type === "single") {
299
- return question.options?.[0] !== undefined ? interviewOptionLabel(question.options[0]) : "option label";
300
- }
301
- if (question.type === "image") {
302
- return "image/file reference or description";
303
- }
304
- return "answer text";
305
- }
306
-
307
- function formatSupervisorInterviewRequest(interview: SupervisorInterviewRequest, message?: string): string {
308
- const lines: string[] = [];
309
- const title = interview.title?.trim();
310
- if (title) lines.push(`Interview: ${title}`);
311
- const description = interview.description?.trim();
312
- if (description) lines.push(description);
313
- const note = message?.trim();
314
- if (note) lines.push(`Child note: ${note}`);
315
- if (lines.length > 0) lines.push("");
316
-
317
- lines.push("Questions:");
318
- interview.questions.forEach((question, index) => {
319
- lines.push(`${index + 1}. [${question.id}] (${question.type}) ${question.question}`);
320
- if (typeof question.context === "string" && question.context.trim()) {
321
- lines.push(` Context: ${question.context.trim()}`);
322
- }
323
- if (question.options?.length) {
324
- lines.push(" Options:");
325
- for (const option of question.options) {
326
- lines.push(` - ${interviewOptionLabel(option)}`);
327
- }
328
- }
329
- });
330
-
331
- const responseExample = {
332
- responses: interview.questions
333
- .filter((question) => question.type !== "info")
334
- .map((question) => ({
335
- id: question.id,
336
- value: interviewExampleValue(question),
337
- })),
338
- };
339
-
340
- lines.push(
341
- "",
342
- "Supervisor reply instructions:",
343
- "Reply with plain JSON or a fenced ```json block using this stable shape. Use the question ids exactly. Info questions are context-only and do not need responses. For single questions, value is one option label. For multi questions, value is an array of option labels. For text/image questions, value is a string unless the question asks otherwise.",
344
- "",
345
- "```json",
346
- JSON.stringify(responseExample, null, 2),
347
- "```",
348
- );
349
-
350
- return lines.join("\n");
351
- }
352
-
353
- function validateSupervisorInterviewReply(value: unknown, interview: SupervisorInterviewRequest): SupervisorInterviewReply {
354
- if (!value || typeof value !== "object" || Array.isArray(value)) {
355
- throw new Error("reply JSON must be an object with a responses array");
356
- }
357
-
358
- const responsesInput = (value as Record<string, unknown>).responses;
359
- if (!Array.isArray(responsesInput)) {
360
- throw new Error("reply JSON must include a responses array");
361
- }
362
-
363
- const questionById = new Map(interview.questions
364
- .filter((question) => question.type !== "info")
365
- .map((question) => [question.id, question]));
366
- const seenIds = new Set<string>();
367
- const responses: SupervisorInterviewReply["responses"] = [];
368
-
369
- for (let index = 0; index < responsesInput.length; index++) {
370
- const response = responsesInput[index];
371
- if (!response || typeof response !== "object" || Array.isArray(response)) {
372
- throw new Error(`responses[${index}] must be an object`);
373
- }
374
-
375
- const raw = response as Record<string, unknown>;
376
- if (typeof raw.id !== "string" || raw.id.trim() === "") {
377
- throw new Error(`responses[${index}].id must be a non-empty string`);
378
- }
379
- const id = raw.id.trim();
380
- const question = questionById.get(id);
381
- if (!question) {
382
- throw new Error(`responses[${index}].id must match a non-info interview question id`);
383
- }
384
- if (seenIds.has(id)) {
385
- throw new Error(`responses[${index}].id is duplicated: ${id}`);
386
- }
387
- seenIds.add(id);
388
- if (!Object.hasOwn(raw, "value")) {
389
- throw new Error(`responses[${index}].value is required`);
390
- }
391
-
392
- const value = raw.value;
393
- if (question.type === "single") {
394
- if (typeof value !== "string") throw new Error(`responses[${index}].value must be a string for single questions`);
395
- const optionLabels = new Set(question.options?.map(interviewOptionLabel));
396
- if (!optionLabels.has(value.trim())) throw new Error(`responses[${index}].value must match one of the question options`);
397
- responses.push({ id, value: value.trim() });
398
- continue;
399
- }
400
-
401
- if (question.type === "multi") {
402
- if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
403
- throw new Error(`responses[${index}].value must be an array of strings for multi questions`);
404
- }
405
- const optionLabels = new Set(question.options?.map(interviewOptionLabel));
406
- const selected = value.map((item) => item.trim());
407
- const invalid = selected.find((item) => !optionLabels.has(item));
408
- if (invalid) throw new Error(`responses[${index}].value contains an option that is not in the question options: ${invalid}`);
409
- responses.push({ id, value: selected });
410
- continue;
411
- }
412
-
413
- if (typeof value !== "string") {
414
- throw new Error(`responses[${index}].value must be a string for ${question.type} questions`);
415
- }
416
- responses.push({ id, value });
417
- }
418
-
419
- return { responses };
420
- }
421
-
422
- function parseStructuredSupervisorReply(text: string, interview: SupervisorInterviewRequest): { value?: SupervisorInterviewReply; error?: string } | undefined {
423
- const fencedMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
424
- const candidate = (fencedMatch?.[1] ?? text).trim();
425
- if (!candidate.startsWith("{") && !candidate.startsWith("[")) {
426
- return undefined;
427
- }
428
- try {
429
- return { value: validateSupervisorInterviewReply(JSON.parse(candidate), interview) };
430
- } catch (error) {
431
- return { error: getErrorMessage(error) };
432
- }
433
- }
434
- function duplicateSessionNames(sessions: SessionInfo[]): Set<string> {
435
- return new Set(
436
- sessions
437
- .map(s => s.name?.toLowerCase())
438
- .filter((name): name is string => Boolean(name))
439
- .filter((name, index, names) => names.indexOf(name) !== index)
440
- );
441
- }
442
- function parseSubagentIntercomPayload(payload: unknown): { to: string; message: string; requestId?: string; source?: "foreground" | "async"; completion?: SubagentCompletion } | null {
443
- if (typeof payload !== "object" || payload === null) {
444
- return null;
445
- }
446
- const record = payload as Record<string, unknown>;
447
- if (typeof record.to !== "string" || typeof record.message !== "string") {
448
- return null;
449
- }
450
- const requestId = typeof record.requestId === "string" ? record.requestId : undefined;
451
- const source = record.source === "foreground" || record.source === "async" ? record.source : undefined;
452
- const children = Array.isArray(record.children) ? record.children.flatMap((value: unknown) => {
453
- if (!value || typeof value !== "object") return [];
454
- const child = value as Record<string, unknown>;
455
- return typeof child.agent === "string" && typeof child.index === "number" && Number.isSafeInteger(child.index) && child.index >= 0
456
- && typeof child.intercomTarget === "string" && child.intercomTarget.length > 0 && typeof child.status === "string"
457
- && ["completed", "failed", "blocked", "paused", "timed-out", "detached"].includes(child.status)
458
- ? [{ agent: child.agent, index: child.index, status: child.status, intercomTarget: child.intercomTarget }] : [];
459
- }) : [];
460
- const completion = source && typeof record.runId === "string" && typeof record.status === "string" && children.length
461
- ? { runId: record.runId, status: record.status, children, ...(typeof record.completionId === "string" ? { completionId: record.completionId } : {}) } : undefined;
462
- return { to: record.to, message: record.message, ...(requestId ? { requestId } : {}), ...(source ? { source } : {}), ...(completion ? { completion } : {}) };
463
- }
464
- function resolveIntercomPresenceName(sessionName: string | undefined, sessionId: string): string {
465
- const trimmedName = sessionName?.trim();
466
- if (trimmedName) {
467
- return trimmedName;
468
- }
469
- const normalizedSessionId = sessionId.startsWith("session-") ? sessionId.slice("session-".length) : sessionId;
470
- return `${DEFAULT_UNNAMED_SESSION_ALIAS_PREFIX}-${normalizedSessionId.slice(0, 8)}`;
471
- }
472
- function buildPresenceIdentity(pi: ExtensionAPI, sessionId: string): { name: string } {
473
- const subagentIntercomSessionName = process.env[SUBAGENT_INTERCOM_SESSION_NAME_ENV]?.trim();
474
- return {
475
- name: subagentIntercomSessionName || resolveIntercomPresenceName(pi.getSessionName(), sessionId),
476
- };
477
- }
478
- function formatDuration(seconds: number): string {
479
- if (seconds < 60) return `${seconds}s`;
480
- if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
481
- return `${Math.floor(seconds / 3600)}h`;
482
- }
483
- function sessionBusyState(session: SessionInfo): "idle" | "busy" | "unknown" {
484
- if (session.acceptsAsks === true) return "idle";
485
- if (session.acceptsAsks === false) return "busy";
486
- const status = session.status ?? "";
487
- if (status === "idle" || status.startsWith("idle ") || status.startsWith("idle ·")) return "idle";
488
- if (status === "thinking" || status.startsWith("thinking ") || status.startsWith("thinking ·") || status.startsWith("tool:")) return "busy";
489
- return "unknown";
490
- }
491
- function formatIntercomAge(timestamp: number | undefined, now: number): string {
492
- return typeof timestamp === "number" && timestamp > 0
493
- ? `${formatDuration(Math.max(0, Math.floor((now - timestamp) / 1000)))} ago`
494
- : "none";
495
- }
496
- function sessionDeliveryGuidance(session: SessionInfo, isSelf: boolean): string {
497
- if (isSelf) return "self target unavailable; choose a peer from Other sessions; use pending/reply for inbound asks";
498
- const state = sessionBusyState(session);
499
- if (state === "idle") return "send defaults to steer and wakes; ask only if sender must stay alive for a required reply; queue only for intentional delay; passive discouraged";
500
- if (session.acceptsAsks === false) return "send defaults to steer; ask only if sender must stay alive for a required reply (default sends without waiting when peer is busy); queue only for intentional delay; passive discouraged";
501
- if (state === "busy") return "send defaults to steer at the next tool boundary; ask only if sender must stay alive for a required reply; queue only for intentional delay; passive discouraged";
502
- return "state unknown; target is valid; send defaults to steer; ask only if sender must stay alive for a required reply; queue only for intentional delay; passive discouraged";
503
- }
504
- function formatSessionListRow(session: SessionInfo, currentCwd: string, isSelf: boolean, duplicates = new Set<string>(), allSessions: SessionInfo[] = [session], now = Date.now()): string {
505
- const name = session.name || "Unnamed session";
506
- const duplicateName = Boolean(session.name && duplicates.has(session.name.toLowerCase()));
507
- const healthTags: string[] = [];
508
- healthTags.push(`state:${sessionBusyState(session)}`);
509
- healthTags.push(`accepts_asks:${session.acceptsAsks === undefined ? "unknown" : session.acceptsAsks ? "true" : "false"}`);
510
- healthTags.push(`pending_asks:${typeof session.pendingAsks === "number" ? session.pendingAsks : "unknown"}`);
511
- healthTags.push(`last_intercom_activity:${formatIntercomAge(session.lastIntercomActivity, now)}`);
512
- if (typeof session.lastSeen === "number") {
513
- healthTags.push(`last_seen:${formatDuration(Math.max(0, Math.floor((now - session.lastSeen) / 1000)))} ago`);
514
- }
515
- const tags = [
516
- isSelf ? "self" : session.cwd === currentCwd ? "same native session cwd" : undefined,
517
- session.status,
518
- duplicateName ? `target:${formatSessionTarget(session, allSessions)}` : undefined,
519
- ...healthTags,
520
- ].filter((tag): tag is string => Boolean(tag));
521
- const target = formatSessionTarget(session, allSessions);
522
- const suffix = tags.length ? ` [${tags.join(", ")}]` : "";
523
- return `• ${name} (${target}) — Native session cwd: ${session.cwd} (${session.model})${suffix}\n ↳ ${sessionDeliveryGuidance(session, isSelf)}`;
524
- }
525
- function sessionsForScope(sessions: SessionInfo[], currentSessionId: string, scope: IntercomSessionScope): SessionInfo[] {
526
- return scope === "all" ? sessions : filterProjectSessions(sessions, currentSessionId);
527
- }
528
-
529
- function formatSessionListSections(allSessions: SessionInfo[], currentSessionId: string, scope: IntercomSessionScope = "project"): string {
530
- const currentSession = allSessions.find(s => s.id === currentSessionId);
531
- if (!currentSession) {
532
- throw new Error("Current session is missing from intercom session list.");
533
- }
534
- const sessions = sessionsForScope(allSessions, currentSessionId, scope);
535
- const hiddenSessionCount = allSessions.length - sessions.length;
536
- const duplicates = duplicateSessionNames(allSessions);
537
- const otherSessions = sessions.filter(s => s.id !== currentSessionId);
538
- const currentSection = `**Current session:**\n${formatSessionListRow(currentSession, currentSession.cwd, true, duplicates, allSessions)}`;
539
- const otherSection = otherSessions.length === 0
540
- ? `**Other sessions:**\n${hiddenSessionCount > 0
541
- ? "No other sessions connected in this project."
542
- : `No other sessions connected. Start another intercom-enabled session with \`pi --name worker\`, then run \`intercom({ action: "list" })\` again. If you are dogfooding this local fork without installing it, start the peer with \`${localForkStartCommand()}\`.`}`
543
- : `**Other sessions:**\n${otherSessions.map(s => formatSessionListRow(s, currentSession.cwd, false, duplicates, allSessions)).join("\n")}`;
544
- const scopeHint = hiddenSessionCount > 0
545
- ? `\n\n${hiddenSessionCount} session${hiddenSessionCount === 1 ? "" : "s"} in other projects hidden. Use \`intercom({ action: "list", scope: "all" })\` to show every connected session.`
546
- : "";
547
- return `${currentSection}\n\n${otherSection}${scopeHint}`;
548
- }
549
- function previewText(value: unknown, maxLength = 72): string | undefined {
550
- if (typeof value !== "string") {
551
- return undefined;
552
- }
553
- const normalized = value.replace(/\s+/g, " ").trim();
554
- if (!normalized) {
555
- return undefined;
556
- }
557
- return normalized.length > maxLength ? `${normalized.slice(0, maxLength - 1)}…` : normalized;
558
- }
559
- function pendingAskPreview(message: Message): string {
560
- const text = message.content.text;
561
- const normalized = text.replace(/\s+/g, " ").trim();
562
- if (text.startsWith("Subagent needs a supervisor decision.") || text.startsWith("Subagent requests a structured supervisor interview.")) {
563
- const run = text.match(/^Run:\s*(.+)$/m)?.[1]?.trim();
564
- const agent = text.match(/^Agent:\s*(.+)$/m)?.[1]?.trim();
565
- const childTarget = text.match(/^Child intercom target:\s*(.+)$/m)?.[1]?.trim();
566
- const body = text.split(/\n\s*\n/).slice(1).join(" ").replace(/\s+/g, " ").trim();
567
- return [
568
- text.startsWith("Subagent requests") ? "structured supervisor interview" : "supervisor decision",
569
- run ? `run=${run}` : undefined,
570
- agent ? `agent=${agent}` : undefined,
571
- childTarget ? `target=${childTarget}` : undefined,
572
- body ? `question=${previewText(body, 180)}` : undefined,
573
- ].filter((part): part is string => Boolean(part)).join(" · ");
574
- }
575
- return previewText(normalized, 180) ?? normalized;
576
- }
577
- function firstTextContent(result: { content?: Array<{ type: string; text?: string }> }): string {
578
- return result.content?.find((item) => item.type === "text" && typeof item.text === "string")?.text?.replace(/\*\*/g, "") ?? "";
579
- }
580
-
581
- interface ToolResultLike {
582
- content?: Array<{ type: string; text?: string }>;
583
- isError?: boolean;
584
- details?: Record<string, unknown>;
585
- }
586
-
587
- interface ContactSupervisorToolParams {
588
- reason: ContactSupervisorReason;
589
- message?: string;
590
- interview?: unknown;
591
- }
592
-
593
- interface IntercomToolParams {
594
- action: "list" | "send" | "ask" | "reply" | "pending" | "status" | "subscribe" | "unsubscribe" | "publish" | "topics";
595
- scope?: IntercomSessionScope;
596
- to?: string;
597
- message?: string;
598
- attachments?: Attachment[];
599
- replyTo?: string;
600
- delivery?: MessageDelivery;
601
- queueMode?: QueueMode;
602
- threadId?: string;
603
- passive?: boolean;
604
- topic?: string;
605
- event?: TopicUpdate["event"];
606
- resource?: string;
607
- ownership?: TopicUpdate["ownership"];
608
- awaitRelease?: boolean;
609
- }
610
-
611
- type ToolRenderTheme = ExtensionContext["ui"]["theme"];
612
- type ToolRenderContext = { args?: unknown; isError?: boolean; expanded?: boolean };
613
-
614
- class ToolExecutionFailure extends Error {
615
- readonly details?: Record<string, unknown>;
616
-
617
- constructor(message: string, details?: Record<string, unknown>) {
618
- super(message);
619
- this.details = details;
620
- }
621
- }
622
-
623
- class AskDeliveryError extends Error {
624
- readonly result: SendResult;
625
-
626
- constructor(result: SendResult) {
627
- super(result.reason ?? "Session may not exist or has disconnected.");
628
- this.result = result;
629
- }
630
- }
631
-
632
- function throwIfToolError<T extends ToolResultLike>(result: T): T {
633
- if (!result.isError) return result;
634
- throw new ToolExecutionFailure(firstTextContent(result) || "Tool failed", result.details);
635
- }
636
- function shellQuote(value: string): string {
637
- return `'${value.replace(/'/g, `'\\''`)}'`;
638
- }
639
- function localForkStartCommand(): string {
640
- return `pi --name worker --extension ${shellQuote(fileURLToPath(import.meta.url))} --skill ${shellQuote(path.join(PACKAGE_ROOT, "skills", "pi-intercom"))}`;
641
- }
642
- async function settleWithin<T>(operation: () => Promise<T>, timeoutMs: number): Promise<T | null> {
643
- return await new Promise((resolve) => {
644
- let settled = false;
645
- const finish = (value: T | null) => {
646
- if (settled) return;
647
- settled = true;
648
- clearTimeout(timer);
649
- resolve(value);
650
- };
651
- const timer = setTimeout(() => finish(null), timeoutMs);
652
- timer.unref?.();
653
- void Promise.resolve().then(operation).then((value) => finish(value), () => finish(null));
654
- });
655
- }
656
-
657
- export default function piIntercomExtension(pi: ExtensionAPI) {
658
- let client: IntercomClient | null = null;
659
- const config: IntercomConfig = loadConfig();
660
- const childOrchestratorMetadata = readChildOrchestratorMetadata();
661
- let runtimeContext: ExtensionContext | null = null;
662
- let currentSessionId: string | null = null;
663
- let currentModel = "unknown";
664
- let reconnectTimer: NodeJS.Timeout | null = null;
665
- let reconnectPromise: Promise<IntercomClient> | null = null;
666
- let reconnectPromiseGeneration: number | null = null;
667
- let startupConnectTimer: NodeJS.Timeout | null = null;
668
- let reconnectAttempt = 0;
669
- let disposed = true;
670
- let runtimeStarted = false;
671
- let runtimeGeneration = 0;
672
- // Once peers have been seen, the hint is pinned for the rest of the session:
673
- // the identical string is re-appended on every turn without another broker
674
- // round trip. This keeps the system prompt byte-stable across fleet churn
675
- // (session spawns/exits, subagent panels), which keeps the provider prompt
676
- // cache valid; the hint text is written so it stays true after peers exit.
677
- let peerAwarenessHintPinned = false;
678
- let agentRunning = false;
679
- let lastIntercomActivity = 0;
680
- const activeTools = new Map<string, string>();
681
- const replyTracker = new ReplyTracker(config.askTimeoutMs);
682
- const topics = new IntercomTopics(pi, () => getLiveContext());
683
- let topicSyncError: string | undefined;
684
- const pendingInbound = new Map<string, PendingInboundMessage>();
685
- const nativePendingIds = new Set<string>();
686
- const consumedInboundIds = new Set<string>();
687
- const completedChildren = new Map<string, SubagentCompletion["children"][number]>();
688
- let reconciledLeafId: string | null = null;
689
- let inboundFlushTimer: NodeJS.Timeout | null = null;
690
- let inboundRestore: Promise<void> | undefined;
691
- let inboundBatch: Promise<void> | undefined;
692
- let replyWaiter: {
693
- from: string;
694
- replyTo: string;
695
- question?: SupervisorQuestion;
696
- retryNotification?: () => void;
697
- resolve: (message: Message) => void;
698
- reject: (error: Error) => void;
699
- } | null = null;
700
- function waitForReply(from: string, replyTo: string, signal?: AbortSignal, question?: SupervisorQuestion): Promise<Message> {
701
- if (replyWaiter) {
702
- throw new Error("Already waiting for a reply");
703
- }
704
- if (signal?.aborted) {
705
- throw new Error("Cancelled");
706
- }
707
- return new Promise((resolve, reject) => {
708
- const timeout = question ? undefined : setTimeout(() => {
709
- rejectReplyWaiter(new Error(`No reply from "${from}" within ${Math.max(1, Math.round(config.askTimeoutMs / 60000))} minute(s)`));
710
- }, config.askTimeoutMs);
711
- timeout?.unref?.();
712
- const poll = question ? setInterval(() => {
713
- try {
714
- const saved = readQuestionState(question);
715
- if (saved.state === "cancelled") {
716
- getLiveContext()?.abort();
717
- return rejectReplyWaiter(new Error("Cancelled"));
718
- }
719
- if (saved.answer) replyWaiter?.resolve({ id: replyTo, replyTo, timestamp: saved.answer.answeredAt, content: { text: saved.answer.message } });
720
- else replyWaiter?.retryNotification?.();
721
- } catch (error) {
722
- rejectReplyWaiter(toError(error));
723
- }
724
- }, 250) : undefined;
725
- const cleanup = () => {
726
- clearTimeout(timeout);
727
- clearInterval(poll);
728
- signal?.removeEventListener("abort", onAbort);
729
- if (replyWaiter?.replyTo === replyTo) {
730
- replyWaiter = null;
731
- }
732
- };
733
- const onAbort = () => {
734
- try {
735
- if (question) cancelSupervisorQuestion(question);
736
- } finally {
737
- cleanup();
738
- reject(new Error("Cancelled"));
739
- }
740
- };
741
- signal?.addEventListener("abort", onAbort, { once: true });
742
- replyWaiter = {
743
- from,
744
- replyTo,
745
- question,
746
- resolve: (message) => {
747
- if (question && message.content.attachments?.some((attachment) => attachment.name === RECIPIENT_TURN_FAILED_ATTACHMENT)) {
748
- pi.appendEntry("intercom_question_notification_error", { questionId: question.questionId, error: message.content.text });
749
- return;
750
- }
751
- try {
752
- if (question) saveQuestionAnswer(question, message.content.text);
753
- cleanup();
754
- resolve(message);
755
- } catch (error) {
756
- cleanup();
757
- reject(toError(error));
758
- }
759
- },
760
- reject: (error) => {
761
- cleanup();
762
- reject(error);
763
- },
764
- };
765
- });
766
- }
767
- function currentReplyWaiter() { return replyWaiter; }
768
- function rejectReplyWaiter(error: Error): void {
769
- replyWaiter?.reject(error);
770
- }
771
- function rejectReplyWaiterForPeer(sessionId: string): void {
772
- if (replyWaiter?.from === sessionId && !replyWaiter.question) {
773
- rejectReplyWaiter(new Error(`Reply peer disconnected before answering: ${sessionId}`));
774
- }
775
- }
776
- async function sendAskTransaction(
777
- activeClient: IntercomClient,
778
- to: string,
779
- questionId: string,
780
- options: Parameters<IntercomClient["send"]>[1],
781
- signal: AbortSignal | undefined,
782
- onSent: (result: SendResult) => void,
783
- ): Promise<Message> {
784
- const replyPromise = waitForReply(to, questionId, signal);
785
- replyPromise.catch(() => undefined);
786
- try {
787
- if (signal?.aborted) throw new Error("Cancelled");
788
- const result = await activeClient.send(to, { ...options, messageId: questionId, expectsReply: true });
789
- if (!result.accepted) throw new AskDeliveryError(result);
790
- markIntercomActivity();
791
- syncPresenceStatus();
792
- onSent(result);
793
- return await replyPromise;
794
- } catch (error) {
795
- if (replyWaiter?.replyTo === questionId) rejectReplyWaiter(toError(error));
796
- try {
797
- await replyPromise;
798
- } catch {
799
- // Cleanup only; preserve the transaction failure.
800
- }
801
- throw error;
802
- }
803
- }
804
- function clearReconnectTimer(): void {
805
- if (!reconnectTimer) {
806
- return;
807
- }
808
- clearTimeout(reconnectTimer);
809
- reconnectTimer = null;
810
- }
811
- function clearStartupConnectTimer(): void {
812
- if (!startupConnectTimer) {
813
- return;
814
- }
815
- clearTimeout(startupConnectTimer);
816
- startupConnectTimer = null;
817
- }
818
- function scheduleStartupConnection(ctx: ExtensionContext, generation: number): void {
819
- clearStartupConnectTimer();
820
- startupConnectTimer = setTimeout(() => {
821
- startupConnectTimer = null;
822
- if (!getLiveContext(ctx, generation)) return;
823
- void ensureConnected("startup").catch(() => {
824
- if (!getLiveContext(ctx, generation)) return;
825
- client = null;
826
- scheduleReconnect();
827
- });
828
- }, 0);
829
- startupConnectTimer.unref?.();
830
- }
831
- function clearInboundFlushTimer(): void {
832
- if (!inboundFlushTimer) {
833
- return;
834
- }
835
- clearTimeout(inboundFlushTimer);
836
- inboundFlushTimer = null;
837
- }
838
- function getLiveContext(ctx: ExtensionContext | null = runtimeContext, generation = runtimeGeneration): ExtensionContext | null {
839
- if (disposed || generation !== runtimeGeneration || !ctx) {
840
- return null;
841
- }
842
- try {
843
- if (currentSessionId && ctx.sessionManager.getSessionId() !== currentSessionId) {
844
- return null;
845
- }
846
- void ctx.hasUI;
847
- return ctx;
848
- } catch {
849
- // A context that throws while reading session/UI state is no longer usable.
850
- return null;
851
- }
852
- }
853
- function notifyIfLive(ctx: ExtensionContext, message: string, level: "info" | "warning" | "error", generation = runtimeGeneration): void {
854
- const liveContext = getLiveContext(ctx, generation);
855
- if (!liveContext?.hasUI) {
856
- return;
857
- }
858
- try {
859
- liveContext.ui.notify(message, level);
860
- } catch {
861
- // The UI can disappear during session shutdown/reload while async overlay work is settling.
862
- }
863
- }
864
- function getReconnectDelayMs(): number {
865
- const backoffMs = [1000, 2000, 5000, 10000, 30000];
866
- return backoffMs[Math.min(reconnectAttempt, backoffMs.length - 1)]!;
867
- }
868
- function currentStatus(): string {
869
- const activeToolName = activeTools.values().next().value;
870
- const lifecycleStatus = activeToolName ? `tool:${activeToolName}` : agentRunning ? "thinking" : "idle";
871
- return config.status ? `${lifecycleStatus} · ${config.status}` : lifecycleStatus;
872
- }
873
- async function buildRegistration(): Promise<Omit<SessionInfo, "id">> {
874
- const liveContext = getLiveContext();
875
- if (!liveContext || !currentSessionId) {
876
- throw new Error("Intercom runtime not initialized");
877
- }
878
-
879
- const identity = buildPresenceIdentity(pi, currentSessionId);
880
- const cwd = liveContext.cwd ?? process.cwd();
881
- return {
882
- name: identity.name,
883
- cwd,
884
- model: currentModel,
885
- projectId: await resolveSessionProjectId(cwd),
886
- lastSeen: Date.now(),
887
- status: currentStatus(),
888
- ...buildPresenceHealth(),
889
- };
890
- }
891
- function isRecipientIdle(ctx: ExtensionContext): boolean {
892
- // ponytail: Pi 1.0 exposes no busy signal during foreign prompt preparation;
893
- // custom delivery may start a separate legitimate run. Use a public admission
894
- // signal if the host adds one, rather than reserving rejected input here.
895
- if (agentRunning || activeTools.size > 0) return false;
896
- try {
897
- return ctx.isIdle();
898
- } catch {
899
- return false;
900
- }
901
- }
902
- function canAcceptAsks(): boolean {
903
- const liveContext = getLiveContext();
904
- return liveContext ? isRecipientIdle(liveContext) : false;
905
- }
906
- /** Publish whether the recipient can accept a blocking ask. */
907
- function buildPresenceHealth(): { pendingAsks: number; acceptsAsks: boolean; lastIntercomActivity: number } {
908
- return {
909
- pendingAsks: replyTracker.listPending().length,
910
- acceptsAsks: canAcceptAsks(),
911
- lastIntercomActivity,
912
- };
913
- }
914
- function markIntercomActivity(): void {
915
- lastIntercomActivity = Date.now();
916
- }
917
- function syncPresenceIdentity(sessionId: string): void {
918
- if (!client || !getLiveContext()) {
919
- return;
920
- }
921
- client.updatePresence({ ...buildPresenceIdentity(pi, sessionId), status: currentStatus(), ...buildPresenceHealth() });
922
- }
923
- function syncPresenceStatus(): void {
924
- if (!client || !currentSessionId || !getLiveContext()) {
925
- return;
926
- }
927
- client.updatePresence({ status: currentStatus(), ...buildPresenceHealth() });
928
- }
929
- function requestedDelivery(message: Message): RequestedDelivery {
930
- if (message.passive === true || message.delivery === "passive") return "passive";
931
- return message.delivery ?? (message.expectsReply === true ? "auto" : "steer");
932
- }
933
- function shouldTriggerTurn(message: Message): boolean {
934
- return requestedDelivery(message) !== "passive";
935
- }
936
- function queuedTriggerIndex(entries: PendingInboundMessage[]): number {
937
- const canTrigger = (entry: PendingInboundMessage) => entry.flushDelivery !== "passive" && shouldTriggerTurn(entry.message);
938
- const askIndex = entries.findIndex((entry) => canTrigger(entry) && entry.message.expectsReply === true);
939
- return askIndex === -1 ? entries.findIndex(canTrigger) : askIndex;
940
- }
941
- function runInboundBatch(deliver: () => Promise<void>, generation = runtimeGeneration): Promise<void> {
942
- if (inboundBatch) return inboundBatch;
943
- // Native appends emit subscriber callbacks synchronously; publish ownership first.
944
- const batch = Promise.resolve().then(deliver);
945
- inboundBatch = batch;
946
- const finish = () => {
947
- if (inboundBatch !== batch) return;
948
- inboundBatch = undefined;
949
- if (getLiveContext(runtimeContext, generation) && queuedInbound().length) scheduleInboundFlush(0);
950
- };
951
- void batch.then(finish, finish);
952
- return batch;
953
- }
954
- function* sendTriggerLast(entries: PendingInboundMessage[], generation = runtimeGeneration): Generator<void, void> {
955
- const passives: PendingInboundMessage[] = [];
956
- const rest: PendingInboundMessage[] = [];
957
- for (const entry of entries) {
958
- if (entry.flushDelivery === "passive") passives.push(entry);
959
- else rest.push(entry);
960
- }
961
- for (const entry of passives) {
962
- if (!getLiveContext(runtimeContext, generation)) return;
963
- sendIncomingMessage(entry, "passive", generation);
964
- yield;
965
- }
966
- if (!getLiveContext(runtimeContext, generation)) return;
967
- const triggerIndex = queuedTriggerIndex(rest);
968
- const trigger = triggerIndex === -1 ? undefined : rest.splice(triggerIndex, 1)[0];
969
- if (trigger && pendingInbound.get(trigger.message.id) === trigger) replyTracker.queueTurnContext({ ...trigger, receivedAt: Date.now() });
970
- for (const entry of rest) {
971
- if (!getLiveContext(runtimeContext, generation)) return;
972
- sendIncomingMessage(entry, entry.flushDelivery === "steer" ? "steer" : "followUp", generation);
973
- yield;
974
- }
975
- if (trigger) sendIncomingMessage(trigger, "trigger", generation);
976
- }
977
- function currentSessionTargetMatches(to: string, resolvedTo?: string | null, activeClient?: IntercomClient): boolean {
978
- const targets = new Set<string>();
979
- const addTarget = (target: string | undefined | null) => {
980
- const trimmed = target?.trim();
981
- if (trimmed) targets.add(trimmed.toLowerCase());
982
- };
983
- addTarget(currentSessionId);
984
- addTarget(activeClient?.sessionId);
985
- addTarget(pi.getSessionName());
986
- if (currentSessionId) addTarget(buildPresenceIdentity(pi, currentSessionId).name);
987
- return Boolean(resolvedTo && activeClient?.sessionId && resolvedTo === activeClient.sessionId)
988
- || targets.has(to.trim().toLowerCase());
989
- }
990
- function checkpointInbound(update: InboundCheckpoint): void {
991
- if (currentSessionId) pi.appendEntry(INBOUND_CHECKPOINT_TYPE, { sessionId: currentSessionId, ...update });
992
- }
993
- function retireReply(messageId: string, stage: "reply-retired" | "discarded" = "reply-retired"): void {
994
- replyTracker.markReplied(messageId);
995
- checkpointInbound({ messageId, stage });
996
- }
997
- function keepInbound(entry: PendingInboundMessage): void {
998
- if (entry.stage === "queued" && entry.message.queueMode === "replace" && entry.message.threadId) {
999
- for (const pending of pendingInbound.values()) {
1000
- if (pending.stage === "queued" && pending.from.id === entry.from.id && pending.message.threadId === entry.message.threadId) {
1001
- pendingInbound.delete(pending.message.id);
1002
- replyTracker.markReplied(pending.message.id);
1003
- }
1004
- }
1005
- }
1006
- pendingInbound.set(entry.message.id, entry);
1007
- }
1008
- function setInboundStage(entry: PendingInboundMessage, stage: PendingInboundMessage["stage"]): void {
1009
- if (entry.stage === stage) return;
1010
- pendingInbound.set(entry.message.id, { ...entry, stage });
1011
- checkpointInbound({ messageId: entry.message.id, stage });
1012
- }
1013
- function rememberInbound(entry: InboundMessageEntry, stage: PendingInboundMessage["stage"], flushDelivery: PendingInboundMessage["flushDelivery"]): void {
1014
- const pending = pendingInbound.get(entry.message.id);
1015
- if (pending) {
1016
- setInboundStage(pending, stage);
1017
- return;
1018
- }
1019
- const saved = { ...entry, stage, flushDelivery, receivedAt: Date.now() };
1020
- keepInbound(saved);
1021
- checkpointInbound({ entry: saved });
1022
- }
1023
- function queuedInbound(): PendingInboundMessage[] {
1024
- return [...pendingInbound.values()].filter((entry) => entry.stage === "queued");
1025
- }
1026
- function rememberCompletedChildren(entry: InboundMessageEntry): void {
1027
- const completion = entry.subagentCompletion;
1028
- if (entry.from.id !== "subagent-result" || completion?.ownerSessionId !== currentSessionId) return;
1029
- for (const child of completion.children) {
1030
- const thread = `subagent-progress:${completion.runId}:${child.agent}:${child.index}`;
1031
- if (child.status === "detached") completedChildren.delete(thread);
1032
- else completedChildren.set(thread, child);
1033
- }
1034
- for (const pending of queuedInbound()) discardObsoleteProgress(pending);
1035
- }
1036
- function discardObsoleteProgress(entry: InboundMessageEntry): boolean {
1037
- const message = entry.message;
1038
- if (message.delivery !== "queue" || message.queueMode !== "replace" || !message.threadId || message.expectsReply || message.replyTo) return false;
1039
- const child = completedChildren.get(message.threadId);
1040
- if (!child || (entry.from.id !== child.intercomTarget && entry.from.name !== child.intercomTarget)) return false;
1041
- // Keep the original checkpoint as history; obsolete progress is not another model turn.
1042
- if (!pendingInbound.has(message.id)) rememberInbound(entry, "queued", "auto");
1043
- pendingInbound.delete(message.id);
1044
- replyTracker.markReplied(message.id);
1045
- checkpointInbound({ messageId: message.id, stage: "discarded" });
1046
- return true;
1047
- }
1048
- function needsIntercom(entry: Pick<InboundMessageEntry, "from" | "message">): boolean {
1049
- return entry.message.human === undefined && shouldTriggerTurn(entry.message)
1050
- && entry.from.id !== "subagent-result" && entry.from.id !== "subagent-control";
1051
- }
1052
- function reconcileIntercom(ctx: ExtensionContext): void {
1053
- if ([...pendingInbound.values()].some(needsIntercom) || replyTracker.listPending().some(needsIntercom)) activateTools(pi, ["intercom"]);
1054
- }
1055
- function* restoreInboundEntries(ctx: ExtensionContext, generation: number): Generator<void, void> {
1056
- consumedInboundIds.clear();
1057
- completedChildren.clear();
1058
- const leafId = ctx.sessionManager.getLeafId();
1059
- for (const metadata of ctx.sessionManager.getEntries()) {
1060
- yield;
1061
- if (!getLiveContext(ctx, generation)) return;
1062
- if (!(metadata.type === "custom_message" && ["intercom_message", "subagent-human-message"].includes(metadata.customType)
1063
- || metadata.type === "custom" && [INBOUND_CHECKPOINT_TYPE, "intercom_sent"].includes(metadata.customType))) continue;
1064
- const item = ctx.sessionManager.getEntry(metadata.id) ?? metadata;
1065
- if (item.type === "custom_message") {
1066
- const receipt = item.details as InboundMessageEntry | undefined;
1067
- if (receipt?.subagentCompletion) rememberCompletedChildren(receipt);
1068
- const id = receipt && typeof receipt === "object" ? receipt.message?.id : undefined;
1069
- if (typeof id === "string") {
1070
- consumedInboundIds.add(id);
1071
- pendingInbound.delete(id);
1072
- }
1073
- } else if (item.type === "custom" && item.customType === INBOUND_CHECKPOINT_TYPE) {
1074
- const data = item.data as (InboundCheckpoint & { sessionId: string }) | undefined;
1075
- // Forks inherit entries, but pending delivery belongs to the saved session UUID.
1076
- if (data?.sessionId !== currentSessionId) continue;
1077
- if ("entry" in data) {
1078
- rememberCompletedChildren(data.entry);
1079
- replyTracker.recordIncomingMessage(data.entry.from, data.entry.message, data.entry.receivedAt);
1080
- if (!consumedInboundIds.has(data.entry.message.id)) keepInbound({ ...data.entry });
1081
- } else if (data.stage === "reply-retired") {
1082
- replyTracker.markReplied(data.messageId);
1083
- } else if (data.stage === "discarded") {
1084
- pendingInbound.delete(data.messageId);
1085
- replyTracker.markReplied(data.messageId);
1086
- } else {
1087
- const pending = pendingInbound.get(data.messageId);
1088
- if (pending) pendingInbound.set(data.messageId, { ...pending, stage: data.stage });
1089
- }
1090
- } else if (item.type === "custom" && item.customType === "intercom_sent") {
1091
- const sent = item.data as { message?: { replyTo?: string } } | undefined;
1092
- if (sent?.message?.replyTo) replyTracker.markReplied(sent.message.replyTo);
1093
- }
1094
- }
1095
- if (!getLiveContext(ctx, generation)) return;
1096
- reconciledLeafId = leafId;
1097
- for (const entry of pendingInbound.values()) {
1098
- discardObsoleteProgress(entry);
1099
- }
1100
- }
1101
- async function restoreInbound(ctx: ExtensionContext, generation: number): Promise<void> {
1102
- await runCooperatively(restoreInboundEntries(ctx, generation));
1103
- if (!getLiveContext(ctx, generation)) return;
1104
- if (replyTracker.hasReplyContext) {
1105
- const questions = await listSupervisorQuestionsAsync(ctx.sessionManager.getSessionId());
1106
- if (!getLiveContext(ctx, generation)) return;
1107
- for (const question of questions) {
1108
- if (question.answer || question.state === "cancelled") replyTracker.markReplied(question.questionId);
1109
- }
1110
- }
1111
- reconcileConsumedInbound(ctx);
1112
- reconcileIntercom(ctx);
1113
- }
1114
- function sendIncomingMessage(entry: InboundMessageEntry, delivery: InboundDelivery, generation = runtimeGeneration): void {
1115
- if (runtimeStarted && !getLiveContext(runtimeContext, generation)) {
1116
- return;
1117
- }
1118
- if ("stage" in entry && pendingInbound.get(entry.message.id) !== entry) return;
1119
- if (discardObsoleteProgress(entry)) return;
1120
- rememberInbound(entry, "native", delivery === "passive" ? "passive" : "auto");
1121
- if (delivery === "steer" || delivery === "followUp") nativePendingIds.add(entry.message.id);
1122
- if (delivery !== "passive") {
1123
- replyTracker.queueTurnContext({ from: entry.from, message: entry.message, receivedAt: Date.now() });
1124
- }
1125
- if (delivery !== "passive" && needsIntercom(entry)) activateTools(pi, ["intercom"]);
1126
- const senderDisplay = entry.from.name || entry.from.id.slice(0, 8);
1127
- const bodyText = entry.bodyText;
1128
- const replyInstruction = entry.replyCommand ? `\n\nTo reply, use the intercom tool: ${entry.replyCommand}` : "";
1129
- const human = entry.message.human !== undefined;
1130
- const options = delivery === "trigger"
1131
- ? { triggerTurn: true }
1132
- : delivery === "followUp" || delivery === "steer"
1133
- ? { deliverAs: delivery }
1134
- : { triggerTurn: false };
1135
- pi.sendMessage(
1136
- {
1137
- customType: human ? "subagent-human-message" : "intercom_message",
1138
- content: human
1139
- ? `Direct user message to this agent (human origin, not peer advice). Respond in this conversation; the user sees it directly. Do not ask the parent to relay or approve it.\n\n${bodyText}`
1140
- : `**📨 From ${senderDisplay}** (Native session cwd: ${entry.from.cwd})${replyInstruction}\n\n${bodyText}`,
1141
- display: true,
1142
- details: { ...entry, bodyText },
1143
- },
1144
- options
1145
- );
1146
- if (delivery === "passive" && runtimeContext) reconcileConsumedInbound(runtimeContext);
1147
- }
1148
- function isBlockingSubagentSupervisorMessage(entry: InboundMessageEntry): boolean {
1149
- if (!entry.message.expectsReply) return false;
1150
- const text = entry.bodyText.trimStart();
1151
- return text.startsWith("Subagent needs a supervisor decision.")
1152
- || text.startsWith("Subagent requests a structured supervisor interview.");
1153
- }
1154
- async function requestSubagentDetachForBlockingSupervisorMessage(entry: InboundMessageEntry, attention = false): Promise<boolean> {
1155
- if (!attention && !isBlockingSubagentSupervisorMessage(entry)) return false;
1156
- const requestId = randomUUID();
1157
- return await new Promise<boolean>((resolve) => {
1158
- let settled = false;
1159
- let unsubscribe: (() => void) | undefined;
1160
- const finish = (accepted: boolean) => {
1161
- if (settled) return;
1162
- settled = true;
1163
- clearTimeout(timer);
1164
- unsubscribe?.();
1165
- resolve(accepted);
1166
- };
1167
- const timer = setTimeout(() => finish(false), INTERCOM_DETACH_RESPONSE_TIMEOUT_MS);
1168
- timer.unref?.();
1169
- unsubscribe = pi.events.on(INTERCOM_DETACH_RESPONSE_EVENT, (payload: unknown) => {
1170
- if (!payload || typeof payload !== "object") return;
1171
- const response = payload as { requestId?: unknown; accepted?: unknown };
1172
- if (response.requestId !== requestId) return;
1173
- finish(response.accepted === true);
1174
- });
1175
- try {
1176
- pi.events.emit(INTERCOM_DETACH_REQUEST_EVENT, { requestId, ...(attention ? { reason: "attention" } : {}) });
1177
- // Owned waits answer ordinary attention synchronously, so an unanswered probe can finish now.
1178
- if (attention) finish(false);
1179
- } catch {
1180
- finish(false);
1181
- }
1182
- });
1183
- }
1184
- function scheduleInboundFlush(delayMs = INBOUND_FLUSH_DELAY_MS): void {
1185
- if (!getLiveContext()) {
1186
- return;
1187
- }
1188
- const scheduledGeneration = runtimeGeneration;
1189
- clearInboundFlushTimer();
1190
- inboundFlushTimer = setTimeout(() => {
1191
- inboundFlushTimer = null;
1192
- void flushIdleMessages(scheduledGeneration)?.catch((error) => console.error("Intercom inbound delivery failed:", error));
1193
- }, delayMs);
1194
- inboundFlushTimer.unref?.();
1195
- }
1196
- function flushIdleMessages(generation = runtimeGeneration): Promise<void> | undefined {
1197
- if (inboundBatch) return inboundBatch;
1198
- const entries = queuedInbound();
1199
- if (entries.length === 0) return;
1200
- const ctx = getLiveContext(runtimeContext, generation);
1201
- if (!ctx) return;
1202
-
1203
- if (!isRecipientIdle(ctx)) {
1204
- const steers = entries.filter((entry) => entry.flushDelivery === "steer"
1205
- && (requestedDelivery(entry.message) === "steer" || !ctx.hasUI && activeTools.size === 0));
1206
- if (!steers.length) {
1207
- scheduleInboundFlush(INBOUND_IDLE_RETRY_MS);
1208
- return;
1209
- }
1210
- return runInboundBatch(async () => {
1211
- const detachments: Promise<boolean>[] = [];
1212
- await runCooperatively((function* () {
1213
- for (const entry of steers) {
1214
- if (!getLiveContext(runtimeContext, generation)) return;
1215
- if (pendingInbound.get(entry.message.id) !== entry) continue;
1216
- sendIncomingMessage(entry, "steer", generation);
1217
- if (requestedDelivery(entry.message) === "steer" && entry.from.id !== "subagent-result" && entry.from.id !== "subagent-control") {
1218
- detachments.push(requestSubagentDetachForBlockingSupervisorMessage(entry, !isBlockingSubagentSupervisorMessage(entry)));
1219
- }
1220
- yield;
1221
- }
1222
- })());
1223
- await Promise.all(detachments);
1224
- }, generation);
1225
- }
1226
- return runInboundBatch(() => runCooperatively(sendTriggerLast(entries, generation)), generation);
1227
- }
1228
- function queueIdleMessage(entry: InboundMessageEntry, flushDelivery: PendingInboundMessage["flushDelivery"] = "auto", delayMs = INBOUND_FLUSH_DELAY_MS): void {
1229
- rememberInbound(entry, "queued", flushDelivery);
1230
- syncPresenceStatus();
1231
- scheduleInboundFlush(delayMs);
1232
- }
1233
- function reconcileConsumedInbound(ctx: ExtensionContext, fullScan = false): void {
1234
- const leafId = ctx.sessionManager.getLeafId();
1235
- if (pendingInbound.size > 0) {
1236
- const recent: SessionEntry[] = [];
1237
- let id = leafId;
1238
- // Passive appends emit SDK events, not extension message_end. Walk only
1239
- // new entries; a moved/missing branch boundary still needs full history.
1240
- while (!fullScan && id !== reconciledLeafId) {
1241
- const entry = id ? ctx.sessionManager.getEntry(id) : undefined;
1242
- if (!entry) { fullScan = true; break; }
1243
- recent.push(entry);
1244
- id = entry.parentId;
41
+ disconnected: (error) => {
42
+ if (!wait.durable) {
43
+ wait.reject(
44
+ new Error(`Disconnected while waiting for reply: ${error.message}`, { cause: error }),
45
+ );
1245
46
  }
1246
- for (const metadata of fullScan ? ctx.sessionManager.getEntries() : recent) {
1247
- if (metadata.type !== "custom_message" || !["intercom_message", "subagent-human-message"].includes(metadata.customType)) continue;
1248
- const entry = ctx.sessionManager.getEntry(metadata.id) ?? metadata;
1249
- const inboundId = inboundIdFromCustomMessage(entry);
1250
- if (inboundId) consumedInboundIds.add(inboundId);
1251
- }
1252
- for (const pendingId of pendingInbound.keys()) {
1253
- if (consumedInboundIds.has(pendingId)) {
1254
- pendingInbound.delete(pendingId);
1255
- nativePendingIds.delete(pendingId);
1256
- }
1257
- }
1258
- }
1259
- reconciledLeafId = leafId;
1260
- }
1261
- async function redeliverUnconsumedInbound(ctx: ExtensionContext, generation: number): Promise<void> {
1262
- if (inboundBatch) await inboundBatch;
1263
- if (!getLiveContext(ctx, generation)) return;
1264
- reconcileConsumedInbound(ctx);
1265
- // Public coarse occupancy excludes custom queues on Pi 1.0; the existing
1266
- // turn boundary's occupancy distinguishes retained from fully cleared work.
1267
- if (pendingInbound.size === 0 || ctx.hasPendingMessages()) return;
1268
- const entries = [...pendingInbound.values()].filter((entry) => entry.stage === "native" && !nativePendingIds.has(entry.message.id));
1269
- await runInboundBatch(() => runCooperatively(sendTriggerLast(entries, generation)), generation);
1270
- }
1271
- function isOwnedHumanMessage(from: SessionInfo, message: Message): boolean {
1272
- const origin = message.human;
1273
- if (!origin || !childOrchestratorMetadata || origin.runId !== childOrchestratorMetadata.runId || origin.index !== Number(childOrchestratorMetadata.index)) return false;
1274
- const owner = readRunJson<{ sessionId?: string }>(path.join(getRunMetadataDir(origin.runId), "question-owner.json"));
1275
- return owner?.sessionId === origin.ownerSessionId && from.id === `pi-${createHash("sha256").update(origin.ownerSessionId).digest("hex").slice(0, 32)}`;
1276
- }
1277
- async function handleIncomingMessage(ctx: ExtensionContext, from: SessionInfo, message: Message): Promise<void> {
1278
- const messageGeneration = runtimeGeneration;
1279
- if (inboundRestore) await inboundRestore;
1280
- const liveContext = getLiveContext(ctx, messageGeneration);
1281
- if (!liveContext) {
1282
- return;
1283
- }
1284
- if (topics.receive(from, message)) return;
1285
- if (message.topic) message = { ...message, content: { ...message.content, text: `Topic ${message.topic.topic} · ${message.topic.event}\n${message.content.text}` } };
1286
- if (replyWaiter) {
1287
- const senderTarget = from.name || from.id;
1288
- const fromMatches = senderTarget.toLowerCase() === replyWaiter.from.toLowerCase()
1289
- || from.id === replyWaiter.from;
1290
- const replyMatches = message.replyTo === replyWaiter.replyTo;
1291
- if (fromMatches && replyMatches) {
1292
- markIntercomActivity();
1293
- syncPresenceStatus();
1294
- replyWaiter.resolve(message);
1295
- return;
1296
- }
1297
- }
1298
- if (message.human && !isOwnedHumanMessage(from, message)) message = { ...message, human: undefined };
1299
- const attachmentText = message.content.attachments?.length
1300
- ? formatAttachments(message.content.attachments)
1301
- : "";
1302
- const bodyText = `${message.content.text}${attachmentText}`;
1303
- const replyCommand = config.replyHint && message.expectsReply
1304
- ? `intercom({ action: "reply", message: "..." })`
1305
- : undefined;
1306
- replyTracker.recordIncomingMessage(from, message);
1307
- markIntercomActivity();
1308
- syncPresenceStatus();
1309
- const entry = { from, message, replyCommand, bodyText };
1310
- if (discardObsoleteProgress(entry)) return;
1311
- const activeContext = getLiveContext(liveContext, messageGeneration);
1312
- if (!activeContext) {
1313
- return;
1314
- }
1315
- const delivery = requestedDelivery(message);
1316
- if (inboundBatch) {
1317
- queueIdleMessage(entry, delivery === "passive" ? "passive" : delivery === "steer" ? "steer" : "auto");
1318
- return;
1319
- }
1320
- if (delivery === "queue" && message.queueMode === "replace") {
1321
- if (!isRecipientIdle(activeContext) && !activeContext.hasUI) {
1322
- queueIdleMessage(entry, "steer", NON_UI_REPLACE_FLUSH_DELAY_MS);
1323
- return;
1324
- }
1325
- queueIdleMessage(entry, "auto");
1326
- return;
1327
- }
1328
- if (!isRecipientIdle(activeContext)) {
1329
- if (isBlockingSubagentSupervisorMessage(entry)) {
1330
- const willDeliver = activeContext.hasUI || delivery !== "auto";
1331
- if (willDeliver) rememberInbound(entry, "queued", "auto");
1332
- await requestSubagentDetachForBlockingSupervisorMessage(entry);
1333
- if (!getLiveContext(liveContext, messageGeneration) || (willDeliver && pendingInbound.get(message.id)?.stage !== "queued")) {
1334
- return;
1335
- }
1336
- }
1337
- if (delivery === "steer") {
1338
- // Queue first, then release a delegated foreground wait so native Pi can consume the steer.
1339
- sendIncomingMessage(entry, "steer", messageGeneration);
1340
- if (!isBlockingSubagentSupervisorMessage(entry)) await requestSubagentDetachForBlockingSupervisorMessage(entry, true);
1341
- return;
1342
- }
1343
- if (delivery === "queue" && message.queueMode !== "replace") {
1344
- sendIncomingMessage(entry, "followUp", messageGeneration);
1345
- return;
1346
- }
1347
- if (delivery === "passive") {
1348
- queueIdleMessage(entry, "passive");
1349
- return;
1350
- }
1351
- if (!activeContext.hasUI) {
1352
- if (!message.expectsReply) {
1353
- sendIncomingMessage(entry, "steer", messageGeneration);
1354
- return;
1355
- }
1356
- const activeClient = client;
1357
- if (!message.replyTo && activeClient?.isConnected()) {
1358
- try {
1359
- const result = await activeClient.send(from.id, {
1360
- text: "This agent is running in non-interactive mode and cannot respond to intercom messages while it is working. It will continue its current task and exit when done.",
1361
- replyTo: message.id,
1362
- });
1363
- if (result.delivered && getLiveContext(liveContext, messageGeneration)) {
1364
- replyTracker.markReplied(message.id);
1365
- markIntercomActivity();
1366
- syncPresenceStatus();
1367
- }
1368
- } catch {
1369
- // Best-effort reply; keep the busy non-interactive session running either way.
1370
- }
1371
- }
1372
- return;
1373
- }
1374
- queueIdleMessage(entry, "auto");
1375
- return;
1376
- }
1377
- if (getLiveContext(liveContext, messageGeneration)) {
1378
- sendIncomingMessage(entry, delivery === "passive" ? "passive" : "trigger", messageGeneration);
1379
- }
1380
- }
1381
- function attachClientHandlers(nextClient: IntercomClient): void {
1382
- nextClient.on("message", (from, message) => {
1383
- const liveContext = getLiveContext();
1384
- if (client !== nextClient || !liveContext) {
1385
- return;
1386
- }
1387
- void handleIncomingMessage(liveContext, from, message).catch((error) => {
1388
- console.error("Intercom inbound delivery failed:", error);
1389
- });
1390
- });
1391
- nextClient.on("session_left", (sessionId: string) => {
1392
- if (client !== nextClient) {
1393
- return;
1394
- }
1395
- topics.disconnected(sessionId);
1396
- rejectReplyWaiterForPeer(sessionId);
1397
- for (const pending of replyTracker.listPending()) {
1398
- if (pending.from.id === sessionId && !isDurableSupervisorQuestion(pending.message)
1399
- && pendingInbound.get(pending.message.id)?.stage !== "queued") retireReply(pending.message.id);
1400
- }
1401
- replyTracker.expireSender(sessionId);
1402
- for (const pending of queuedInbound()) {
1403
- if (pending.from.id === sessionId && pending.message.expectsReply && !isDurableSupervisorQuestion(pending.message)) {
1404
- pendingInbound.delete(pending.message.id);
1405
- checkpointInbound({ messageId: pending.message.id, stage: "discarded" });
1406
- }
1407
- }
1408
- });
1409
- nextClient.on("disconnected", (error: Error) => {
1410
- if (client !== nextClient) {
1411
- return;
1412
- }
1413
- if (!replyWaiter?.question) rejectReplyWaiter(new Error(`Disconnected while waiting for reply: ${error.message}`, { cause: error }));
1414
- client = null;
1415
- topics.disconnected();
1416
- if (!disposed) {
1417
- clearReconnectTimer();
1418
- scheduleReconnect();
1419
- }
1420
- });
1421
- nextClient.on("error", () => {
1422
- // Keep broker/socket noise out of the TUI. Reconnect logic runs from the disconnect path.
1423
- });
1424
- }
1425
- function scheduleReconnect(): void {
1426
- if (disposed || reconnectTimer || reconnectPromise || !getLiveContext()) {
1427
- return;
1428
- }
1429
- const scheduledGeneration = runtimeGeneration;
1430
- reconnectTimer = setTimeout(() => {
1431
- reconnectTimer = null;
1432
- if (scheduledGeneration !== runtimeGeneration || !getLiveContext()) {
1433
- return;
1434
- }
1435
- reconnectAttempt += 1;
1436
- void ensureConnected("background").catch(() => {
1437
- // ensureConnected("background") already queued the next retry.
1438
- });
1439
- }, getReconnectDelayMs());
1440
- reconnectTimer.unref?.();
1441
- }
1442
- async function restoreTopics(activeClient: IntercomClient): Promise<string | undefined> {
1443
- const changes: TopicChange[] = [
1444
- ...[...topics.published.values()].map((topic): TopicChange => ({ action: "restore", topic })),
1445
- ...[...topics.subscriptions.values()].map((subscription): TopicChange => ({ action: "restore", subscription })),
1446
- ];
1447
- let failure: string | undefined;
1448
- for (const change of changes) {
1449
- try { await activeClient.updateTopics(change); }
1450
- catch (error) {
1451
- if (!activeClient.isConnected()) throw error;
1452
- failure ??= getErrorMessage(error);
1453
- }
1454
- }
1455
- return failure;
1456
- }
1457
- async function ensureConnected(reason: "startup" | "background" | "tool" | "overlay" | "peer-awareness"): Promise<IntercomClient> {
1458
- const generationAtStart = runtimeGeneration;
1459
- if (inboundRestore) await inboundRestore;
1460
- if (disposed || generationAtStart !== runtimeGeneration) {
1461
- throw new Error("Intercom shutting down");
1462
- }
1463
- if (reconnectPromise && reconnectPromiseGeneration === runtimeGeneration) return reconnectPromise;
1464
- if (client && client.isConnected()) {
1465
- return client;
1466
- }
1467
- const contextAtStart = getLiveContext();
1468
- if (!contextAtStart || !currentSessionId) {
1469
- throw new Error("Intercom runtime not initialized");
1470
- }
1471
- clearReconnectTimer();
1472
- if (reconnectPromise && reconnectPromiseGeneration === generationAtStart) {
1473
- return reconnectPromise;
1474
- }
1475
- let nextReconnectPromise!: Promise<IntercomClient>;
1476
- let retryAfterSettle = false;
1477
- nextReconnectPromise = (async () => {
1478
- const nextClient = new IntercomClient({ sendTimeoutMs: config.sendTimeoutMs, listTimeoutMs: config.listTimeoutMs });
1479
- client = nextClient;
1480
- attachClientHandlers(nextClient);
1481
- try {
1482
- if (reason !== "peer-awareness") {
1483
- await spawnBrokerIfNeeded(config.brokerCommand, config.brokerArgs);
1484
- }
1485
- await nextClient.connect(await buildRegistration(), `pi-${createHash("sha256").update(currentSessionId).digest("hex").slice(0, 32)}`);
1486
- if (!getLiveContext(contextAtStart, generationAtStart)) {
1487
- await nextClient.disconnect();
1488
- throw new Error("Intercom runtime no longer active");
1489
- }
1490
- client = nextClient;
1491
- // Startup can change the name before registration accepts presence updates.
1492
- syncPresenceIdentity(contextAtStart.sessionManager.getSessionId());
1493
- reconnectAttempt = 0;
1494
- if (nextClient.supportsTopics) {
1495
- const failure = await restoreTopics(nextClient);
1496
- const sessions = await nextClient.listSessions();
1497
- if (getLiveContext(contextAtStart, generationAtStart)) {
1498
- topicSyncError = failure;
1499
- topics.refresh(sessions);
1500
- if (failure) topics.disconnected(nextClient.sessionId!);
1501
- }
1502
- } else topics.disconnected();
1503
- return nextClient;
1504
- } catch (error) {
1505
- if (client === nextClient) {
1506
- client = null;
1507
- }
1508
- // Retry intent must not depend on the caller: a foreground tool or
1509
- // overlay attempt clears any pending reconnect timer on entry, so a
1510
- // failed attempt that does not re-arm would silently end recovery
1511
- // (background reasons coalescing behind a foreground promise lose
1512
- // their reason too). scheduleReconnect() self-guards against
1513
- // duplicates via the timer, promise, and liveness checks.
1514
- retryAfterSettle = Boolean(getLiveContext(contextAtStart, generationAtStart));
1515
- throw toError(error);
1516
- } finally {
1517
- if (reconnectPromise === nextReconnectPromise) {
1518
- reconnectPromise = null;
1519
- reconnectPromiseGeneration = null;
1520
- }
1521
- // scheduleReconnect() refuses to schedule while reconnectPromise is
1522
- // set, so a failed background attempt must queue its next retry only
1523
- // after the promise slot clears. Scheduling from the catch block ran
1524
- // before that and silently killed the retry chain on first failure.
1525
- if (retryAfterSettle) {
1526
- scheduleReconnect();
1527
- }
1528
- }
1529
- })();
1530
- reconnectPromise = nextReconnectPromise;
1531
- reconnectPromiseGeneration = generationAtStart;
1532
- return nextReconnectPromise;
1533
- }
1534
- async function resolveSessionTarget(activeClient: IntercomClient, nameOrId: string): Promise<string | null> {
1535
- const sessions = await activeClient.listSessions();
1536
- const resolution = resolveSessionTargetValue(sessions, nameOrId);
1537
- if (resolution.status === "found") {
1538
- return resolution.target!.id;
1539
- }
1540
- if (resolution.status === "ambiguous") {
1541
- throw new Error(`Target "${nameOrId}" matches multiple sessions. Use one of these targets: ${formatTargetOptions(resolution.matches, sessions)}.`);
1542
- }
1543
- if (resolution.status === "prefix_too_short") {
1544
- throw new Error(`Target "${nameOrId}" is too short. Use the displayed target from intercom list, such as ${formatTargetOptions(resolution.matches, sessions)}.`);
1545
- }
1546
- return null;
1547
- }
1548
- /** Look up a connected peer's latest published health by session id. Returns null if unavailable. */
1549
- async function resolvePeerHealth(activeClient: IntercomClient, sessionId: string): Promise<SessionInfo | null> {
1550
- try {
1551
- const sessions = await activeClient.listSessions();
1552
- return sessions.find((session) => session.id === sessionId) ?? null;
1553
- } catch {
1554
- // If health cannot be resolved, callers keep the normal reply wait.
1555
- return null;
1556
- }
1557
- }
1558
- /** Only an explicit refusal skips the default reply wait. */
1559
- function peerDeclinesAsks(health: SessionInfo | null): boolean {
1560
- return health?.acceptsAsks === false;
1561
- }
1562
- function deliverLocalSubagentRelayMessage(sender: "subagent-control" | "subagent-result", status: string, messageText: string, completion?: SubagentCompletion): void {
1563
- const now = Date.now();
1564
- const entry: InboundMessageEntry = {
1565
- from: {
1566
- id: sender,
1567
- name: sender,
1568
- cwd: runtimeContext?.cwd ?? process.cwd(),
1569
- model: sender,
1570
- status,
1571
- },
1572
- message: {
1573
- id: completion?.completionId ? `subagent-completion:${completion.completionId}` : randomUUID(),
1574
- timestamp: now,
1575
- content: { text: messageText },
1576
- },
1577
- bodyText: messageText,
1578
- ...(sender === "subagent-result" && completion && currentSessionId ? { subagentCompletion: { ...completion, ownerSessionId: currentSessionId } } : {}),
1579
- };
1580
- if (consumedInboundIds.has(entry.message.id) || pendingInbound.has(entry.message.id)) return;
1581
- rememberCompletedChildren(entry);
1582
- if (inboundBatch) queueIdleMessage(entry, "steer");
1583
- else sendIncomingMessage(entry, "trigger");
1584
- }
1585
- function recordSubagentDeliveryError(entryType: string, to: string, message: string, error: unknown): void {
1586
- pi.appendEntry(entryType, {
1587
- to,
1588
- message,
1589
- error: getErrorMessage(error),
1590
- timestamp: Date.now(),
1591
- });
1592
- }
1593
- function emitResultDelivery(requestId: string | undefined, delivered: boolean, error?: unknown): void {
1594
- if (!requestId) return;
1595
- pi.events.emit(SUBAGENT_RESULT_INTERCOM_DELIVERY_EVENT, {
1596
- requestId,
1597
- delivered,
1598
- ...(error ? { error: getErrorMessage(error) } : {}),
1599
- });
1600
- }
1601
- async function relaySubagentIntercomPayload(payload: unknown, options: {
1602
- sender: "subagent-control" | "subagent-result";
1603
- status: string;
1604
- errorEntryType: string;
1605
- acknowledge?: boolean;
1606
- }): Promise<void> {
1607
- const parsed = parseSubagentIntercomPayload(payload);
1608
- if (!parsed) return;
1609
-
1610
- const relayGeneration = runtimeGeneration;
1611
- const relayStillLive = () => !runtimeStarted || Boolean(getLiveContext(runtimeContext, relayGeneration));
1612
- try {
1613
- if (inboundRestore) await inboundRestore;
1614
- if (!relayStillLive()) return;
1615
- if (!currentSessionTargetMatches(parsed.to)) {
1616
- const activeClient = await ensureConnected("background");
1617
- const target = await resolveSessionTarget(activeClient, parsed.to) ?? parsed.to;
1618
- if (!relayStillLive()) return;
1619
- if (!currentSessionTargetMatches(parsed.to, target, activeClient)) {
1620
- const result = await activeClient.send(target, { text: parsed.message });
1621
- if (!relayStillLive()) return;
1622
- if (!result.accepted) throw new Error(result.reason ?? "Session may not exist or has disconnected.");
1623
- if (options.acknowledge) emitResultDelivery(parsed.requestId, true);
1624
- return;
1625
- }
1626
- }
1627
- if (!(options.sender === "subagent-control" && (parsed.source === "foreground" || parsed.source === "async"))) {
1628
- deliverLocalSubagentRelayMessage(options.sender, options.status, parsed.message, parsed.completion);
1629
- }
1630
- if (options.acknowledge) emitResultDelivery(parsed.requestId, true);
1631
- } catch (error) {
1632
- if (!relayStillLive()) return;
1633
- recordSubagentDeliveryError(options.errorEntryType, parsed.to, parsed.message, error);
1634
- if (options.acknowledge) emitResultDelivery(parsed.requestId, false, error);
1635
- }
1636
- }
1637
- // Subagent event bridges (live/health/control/result) are torn down on session_shutdown.
1638
- // Re-register them on session_start so they survive an in-process restart; the guard skips
1639
- // the first start (already registered at load) and only restores after a shutdown cleared them.
1640
- function registerSubagentEventBridges(): Array<() => void> {
1641
- return [
1642
- ...registerSubagentLiveEventHandlers({
1643
- events: pi.events,
1644
- ensureConnected: () => ensureConnected("background"),
1645
- getConnection: () => ({ client, connecting: Boolean(startupConnectTimer || reconnectPromise), started: runtimeStarted }),
1646
- resolveSessionTarget,
1647
- currentSessionTargetMatches,
1648
- getLivenessCheck: () => {
1649
- const generation = runtimeGeneration;
1650
- return () => !runtimeStarted || Boolean(getLiveContext(runtimeContext, generation));
1651
- },
1652
- }),
1653
- pi.events.on(SUBAGENT_SUPERVISOR_QUESTION_RESOLVED_EVENT, (payload) => {
1654
- const questionId = payload && typeof payload === "object" ? (payload as { questionId?: unknown }).questionId : undefined;
1655
- if (typeof questionId !== "string") return;
1656
- retireReply(questionId, pendingInbound.delete(questionId) ? "discarded" : "reply-retired");
1657
- syncPresenceStatus();
1658
- }),
1659
- pi.events.on("intercom:open", () => {
1660
- const ctx = getLiveContext();
1661
- if (ctx?.mode === "tui") return openIntercomOverlay(ctx, "all");
1662
- }),
1663
- pi.events.on(SUBAGENT_INTERCOM_IDENTITY_REQUEST_EVENT, (payload) => {
1664
- const requestId = payload && typeof payload === "object" ? (payload as { requestId?: unknown }).requestId : undefined;
1665
- if (typeof requestId === "string" && client?.isConnected() && client.sessionId) {
1666
- pi.events.emit(SUBAGENT_INTERCOM_IDENTITY_RESPONSE_EVENT, { requestId, sessionId: client.sessionId });
1667
- }
1668
- }),
1669
- pi.events.on(SUBAGENT_CONTROL_INTERCOM_EVENT, (payload) => {
1670
- return relaySubagentIntercomPayload(payload, {
1671
- sender: "subagent-control",
1672
- status: "needs_attention",
1673
- errorEntryType: "intercom_control_error",
1674
- });
1675
- }),
1676
- pi.events.on(SUBAGENT_RESULT_INTERCOM_EVENT, (payload) => {
1677
- return relaySubagentIntercomPayload(payload, {
1678
- sender: "subagent-result",
1679
- status: "result",
1680
- errorEntryType: "intercom_result_error",
1681
- acknowledge: true,
1682
- });
1683
- }),
1684
- ];
1685
- }
1686
- let eventUnsubscribes = registerSubagentEventBridges();
1687
- let eventBridgesActive = true;
1688
- pi.on("session_start", async (event, ctx) => {
1689
- if (!eventBridgesActive) {
1690
- eventUnsubscribes = registerSubagentEventBridges();
1691
- eventBridgesActive = true;
1692
- }
1693
- disposed = false;
1694
- runtimeStarted = true;
1695
- runtimeGeneration += 1;
1696
- const generation = runtimeGeneration;
1697
- // A session boundary starts from a cold prompt cache, so re-detect peers
1698
- // instead of carrying a pin from a previous session in this process.
1699
- peerAwarenessHintPinned = false;
1700
- reconnectAttempt = 0;
1701
- clearReconnectTimer();
1702
- clearStartupConnectTimer();
1703
- runtimeContext = ctx;
1704
- currentSessionId = ctx.sessionManager.getSessionId();
1705
- topics.start(ctx);
1706
- topicSyncError = undefined;
1707
- currentModel = ctx.model?.id ?? "unknown";
1708
- agentRunning = false;
1709
- lastIntercomActivity = 0;
1710
- activeTools.clear();
1711
- pendingInbound.clear();
1712
- nativePendingIds.clear();
1713
- replyTracker.reset();
1714
- if (event.reason !== "reload") restoreLazyTools(pi, ctx, "load_intercom", ["intercom"]);
1715
- const restoration = Promise.resolve().then(() => restoreInbound(ctx, generation));
1716
- inboundRestore = restoration;
1717
- await restoration;
1718
- if (!getLiveContext(ctx, generation)) return;
1719
- if (inboundRestore === restoration) inboundRestore = undefined;
1720
- if (event.reason === "reload") {
1721
- for (const entry of pendingInbound.values()) if (entry.stage === "native") nativePendingIds.add(entry.message.id);
1722
- }
1723
- // A fresh runtime has no inherited native queues. Reload keeps those queues
1724
- // and in-flight prompts, so only its intercom-staged entries are flushed here.
1725
- if (event.reason !== "reload" && !ctx.signal && !ctx.hasPendingMessages()) {
1726
- for (const entry of [...pendingInbound.values()]) setInboundStage(entry, "queued");
1727
- }
1728
- scheduleStartupConnection(ctx, generation);
1729
- if (queuedInbound().length > 0) scheduleInboundFlush();
1730
- });
1731
-
1732
- pi.on("session_shutdown", async () => {
1733
- for (const unsubscribe of eventUnsubscribes) {
1734
- try {
1735
- unsubscribe();
1736
- } catch {
1737
- // Best effort cleanup for reload/session replacement.
1738
- }
1739
- }
1740
- eventUnsubscribes = [];
1741
- eventBridgesActive = false;
1742
- clearStartupConnectTimer();
1743
- clearReconnectTimer();
1744
- clearInboundFlushTimer();
1745
- rejectReplyWaiter(new Error("Session shutting down"));
1746
- // Native reload keeps this runner valid until shutdown completes. Finish the
1747
- // owned handoff before invalidating it so cleared native messages cannot strand.
1748
- await Promise.allSettled([inboundRestore, inboundBatch]);
1749
- disposed = true;
1750
- runtimeGeneration += 1;
1751
- // ponytail: joins the bounded attempt only; overdue custom launchers need lifecycle ownership in broker/spawn.ts.
1752
- await Promise.allSettled([reconnectPromise]);
1753
- inboundRestore = undefined;
1754
- inboundBatch = undefined;
1755
- replyTracker.reset();
1756
- pendingInbound.clear();
1757
- nativePendingIds.clear();
1758
- consumedInboundIds.clear();
1759
- completedChildren.clear();
1760
- reconciledLeafId = null;
1761
- clearInboundFlushTimer();
1762
- agentRunning = false;
1763
- activeTools.clear();
1764
- if (client) {
1765
- await client.disconnect();
1766
- client = null;
1767
- }
1768
- runtimeContext = null;
1769
- currentSessionId = null;
1770
- });
1771
- pi.on("session_info_changed", (_event, ctx) => {
1772
- if (getLiveContext(ctx) && currentSessionId) syncPresenceIdentity(currentSessionId);
1773
- });
1774
- pi.on("session_tree", (_event, ctx) => {
1775
- if (getLiveContext(ctx)) reconcileConsumedInbound(ctx, true);
1776
- reconcileIntercom(ctx);
1777
- });
1778
- pi.on("session_compact", (_event, ctx) => reconcileIntercom(ctx));
1779
- pi.on("turn_end", (event) => {
1780
- if (!getLiveContext()) {
1781
- return;
1782
- }
1783
- // Pi exposes the next queued batch, not every custom queue item. Empty
1784
- // proves both queues cleared; a nonempty batch must retain all admitted IDs.
1785
- // ponytail: selective removal while other items remain is not observable;
1786
- // use a complete public queue snapshot if the host adds one.
1787
- if (event.context.pendingMessages.length === 0) nativePendingIds.clear();
1788
- replyTracker.endTurn();
1789
- scheduleInboundFlush(0);
1790
- });
1791
- pi.on("message_end", (event) => {
1792
- const message = (event as { message?: unknown }).message;
1793
- const inboundId = inboundIdFromCustomMessage(message);
1794
- if (inboundId) {
1795
- consumedInboundIds.add(inboundId);
1796
- pendingInbound.delete(inboundId);
1797
- nativePendingIds.delete(inboundId);
1798
- }
1799
- const activeClient = client;
1800
- const context = replyTracker.currentTurn();
1801
- const errorMessage = getAssistantErrorMessage(message);
1802
- if (!activeClient?.isConnected() || !context?.message.expectsReply || !errorMessage) {
1803
- return;
1804
- }
1805
- const replyTo = context.message.id;
1806
- return activeClient.send(context.from.id, {
1807
- text: `${RECIPIENT_TURN_FAILED_PREFIX} ${errorMessage}`,
1808
- replyTo,
1809
- attachments: [{
1810
- type: "context",
1811
- name: RECIPIENT_TURN_FAILED_ATTACHMENT,
1812
- content: errorMessage,
1813
- }],
1814
- }).then((result) => {
1815
- if (result.delivered && !isDurableSupervisorQuestion(context.message)) {
1816
- retireReply(replyTo);
1817
- }
1818
- }).catch(() => {
1819
- // Best-effort failure propagation; the local error remains visible in the recipient session.
1820
- });
1821
- });
1822
- pi.on("agent_start", () => {
1823
- if (!getLiveContext()) {
1824
- return;
1825
- }
1826
- agentRunning = true;
1827
- activeTools.clear();
1828
- syncPresenceStatus();
1829
- return flushIdleMessages();
1830
- });
1831
- pi.on("tool_execution_start", (event) => {
1832
- if (!getLiveContext()) {
1833
- return;
1834
- }
1835
- activeTools.set(event.toolCallId, event.toolName);
1836
- syncPresenceStatus();
1837
- });
1838
- pi.on("tool_execution_end", (event) => {
1839
- if (!getLiveContext()) {
1840
- return;
1841
- }
1842
- activeTools.delete(event.toolCallId);
1843
- syncPresenceStatus();
1844
- return flushIdleMessages();
1845
- });
1846
- pi.on("agent_settled", async (_event, ctx) => {
1847
- // A rejected concurrent prompt may settle while the original run is active.
1848
- if (!getLiveContext(ctx) || ctx.signal) {
1849
- return;
1850
- }
1851
- const generation = runtimeGeneration;
1852
- agentRunning = false;
1853
- activeTools.clear();
1854
- replyTracker.endAgent();
1855
- syncPresenceStatus();
1856
- await redeliverUnconsumedInbound(ctx, generation);
1857
- if (getLiveContext(ctx, generation)) scheduleInboundFlush(0);
1858
- });
1859
- pi.on("turn_start", (_event, ctx) => {
1860
- if (!getLiveContext(ctx)) {
1861
- return;
1862
- }
1863
- currentSessionId = ctx.sessionManager.getSessionId();
1864
- syncPresenceIdentity(ctx.sessionManager.getSessionId());
1865
- replyTracker.beginTurn();
1866
- });
1867
- pi.on("model_select", (event, ctx) => {
1868
- if (!getLiveContext(ctx)) {
1869
- return;
1870
- }
1871
- currentModel = event.model.id;
1872
- if (client) {
1873
- client.updatePresence({
1874
- ...buildPresenceIdentity(pi, ctx.sessionManager.getSessionId()),
1875
- model: event.model.id,
1876
- status: currentStatus(),
1877
- });
1878
- }
1879
- });
1880
- pi.on("before_agent_start", async (event, ctx) => {
1881
- if (childOrchestratorMetadata) return;
1882
- const generation = runtimeGeneration;
1883
- if (!getLiveContext(ctx, generation)) return;
1884
- if (peerAwarenessHintPinned) {
1885
- setPromptSection(event.systemPromptOptions, "intercom_peers", PEER_AWARENESS_HINT);
1886
- return;
1887
- }
1888
-
1889
- const deadline = Date.now() + PEER_AWARENESS_LIST_TIMEOUT_MS;
1890
- let activeClient = client?.isConnected() ? client : null;
1891
- if (!activeClient) {
1892
- clearStartupConnectTimer();
1893
- const brokerAvailable = await settleWithin(() => isBrokerRunning(), PEER_AWARENESS_LIST_TIMEOUT_MS);
1894
- const connectBudgetMs = deadline - Date.now();
1895
- if (brokerAvailable && connectBudgetMs > 0) {
1896
- activeClient = await settleWithin(() => ensureConnected("peer-awareness"), connectBudgetMs);
1897
- }
1898
- if (!activeClient) {
1899
- if (getLiveContext(ctx, generation)) scheduleStartupConnection(ctx, generation);
1900
- return;
1901
- }
1902
- }
1903
-
1904
- const currentBrokerSessionId = activeClient.sessionId;
1905
- const remainingMs = deadline - Date.now();
1906
- if (!currentBrokerSessionId || remainingMs <= 0 || !getLiveContext(ctx, generation)) return;
1907
-
1908
- const sessions = await settleWithin(() => activeClient.listSessions(), remainingMs);
1909
- if (!sessions || client !== activeClient || !getLiveContext(ctx, generation)) return;
1910
- const hint = formatPeerAwarenessHint(sessions, currentBrokerSessionId);
1911
- if (!hint) return;
1912
- peerAwarenessHintPinned = true;
1913
- setPromptSection(event.systemPromptOptions, "intercom_peers", hint);
1914
- });
1915
-
1916
- pi.registerMessageRenderer("subagent-human-message", (message, _options, theme) => {
1917
- const details = message.details as InboundMessageEntry | undefined;
1918
- return new Text(`${theme.fg("accent", theme.bold("User → this agent"))}\n${details?.bodyText ?? message.content}`, 0, 0);
1919
- });
1920
-
1921
- const messageExpansion = new WeakMap<object, { expanded: boolean; globalExpanded: boolean }>();
1922
- pi.registerMessageRenderer("intercom_message", (message, options, theme) => {
1923
- const details = message.details as { from: SessionInfo; message: Message; replyCommand?: string; bodyText?: string; subagentCompletion?: SubagentCompletion } | undefined;
1924
- if (!details) return undefined;
1925
- const compact = "compactView" in options && options.compactView === true;
1926
- const attention = details.from.id === "subagent-control" || details.from.status === "needs_attention" || details.message.expectsReply === true || Boolean(details.replyCommand);
1927
- const renderContent = (expanded: boolean) => {
1928
- if (compact && !expanded && !attention) {
1929
- const sender = details.from.name || details.from.id.slice(0, 8);
1930
- const body = details.bodyText || details.message.content.text;
1931
- const summary = details.subagentCompletion
1932
- ? `${details.subagentCompletion.status} [${details.subagentCompletion.runId.slice(0, 8)}]`
1933
- : details.from.id === "subagent-result"
1934
- ? body.split("\n").slice(2, 6).join(" · ")
1935
- : body.split("\n").find((line) => line.trim()) || "(no text)";
1936
- const preview = `${sender}: ${summary}`.replace(/\s+/g, " ").trim();
1937
- let cachedWidth: number | undefined;
1938
- let cachedKey: string | undefined;
1939
- let cachedLines: string[] | undefined;
1940
- return {
1941
- render(width: number) {
1942
- const key = keyText("app.tools.expand");
1943
- if (cachedLines && cachedWidth === width && cachedKey === key) return cachedLines;
1944
- const hint = key ? theme.fg("dim", ` · ${key}`) : "";
1945
- const text = `${" ".repeat(options.outputPad)}${theme.fg("accent", "📨 ")}${theme.fg("muted", preview)}`;
1946
- cachedWidth = width;
1947
- cachedKey = key;
1948
- cachedLines = [truncateToWidth(truncateToWidth(text, width - visibleWidth(hint)) + hint, width)];
1949
- return cachedLines;
1950
- },
1951
- invalidate() { cachedLines = undefined; },
1952
- };
1953
- }
1954
- return new InlineMessageComponent(details.from, details.message, theme, details.replyCommand, details.bodyText, expanded || details.from.id !== "subagent-result" || attention);
1955
- };
1956
- if (attention || (!compact && details.from.id !== "subagent-result")) return renderContent(options.expanded);
1957
- let state = messageExpansion.get(message);
1958
- if (!state || state.globalExpanded !== options.expanded) {
1959
- state = { expanded: options.expanded, globalExpanded: options.expanded };
1960
- messageExpansion.set(message, state);
1961
- }
1962
- const expansion = state;
1963
- let component = renderContent(expansion.expanded);
1964
- return new MouseRegion({ render: (width) => component.render(width), invalidate: () => component.invalidate() }, (event) => {
1965
- if (event.type !== "click" || event.button !== "left") return;
1966
- expansion.expanded = !expansion.expanded;
1967
- component = renderContent(expansion.expanded);
1968
- return { handled: true };
1969
- });
1970
- });
1971
-
1972
- async function requestSupervisorDecision(reason: "need_decision" | "interview_request", message: string | undefined, interview: SupervisorInterviewRequest | undefined, signal: AbortSignal | undefined, ctx: ExtensionContext) {
1973
- if (replyWaiter) throw new Error("Already waiting for a reply");
1974
- if (signal?.aborted) throw new Error("Cancelled");
1975
- const metadata = childOrchestratorMetadata!;
1976
- const body = interview ? formatSupervisorInterviewRequest(interview, message) : message!;
1977
- const question = createSupervisorQuestion({
1978
- runId: metadata.runId,
1979
- ownerTarget: metadata.orchestratorTarget,
1980
- agent: metadata.agent,
1981
- index: Number(metadata.index),
1982
- childSessionId: ctx.sessionManager.getSessionId(),
1983
- childTarget: metadata.sessionName ?? buildPresenceIdentity(pi, ctx.sessionManager.getSessionId()).name,
1984
- sessionFile: ctx.sessionManager.getSessionFile() ?? "",
1985
- cwd: ctx.cwd,
1986
- pid: process.pid,
1987
- reason,
1988
- message: body,
1989
- ...(interview ? { interview } : {}),
1990
- });
1991
- const requestText = formatChildOrchestratorMessage(interview ? "interview" : "ask", metadata, [
1992
- `Question ID: ${question.questionId}`,
1993
- `Answer after reconnect/reload: ${formatRunAction("answer", question.runId, { questionId: question.questionId, message: "..." }, Number(process.env.PI_SUBAGENT_DEPTH) > 1)}`,
1994
- body,
1995
- ].join("\n"));
1996
- const replyPromise = waitForReply(metadata.orchestratorTarget, question.questionId, signal, question);
1997
- // Persistence is the delivery path; intercom only wakes the supervisor. Going offline does not end the wait.
1998
- let retryAfter = 0;
1999
- let notificationFailureLogged = false;
2000
- const notifySupervisor = async () => {
2001
- const waiter = currentReplyWaiter();
2002
- if (waiter?.replyTo !== question.questionId || Date.now() < retryAfter) return;
2003
- waiter.retryNotification = undefined;
2004
- let retry = true;
2005
- try {
2006
- const connectedClient = await ensureConnected("tool");
2007
- if (currentReplyWaiter() !== waiter) return;
2008
- const to = await resolveSessionTarget(connectedClient, metadata.orchestratorTarget) ?? metadata.orchestratorTarget;
2009
- if (currentReplyWaiter() !== waiter) return;
2010
- waiter.from = to;
2011
- // Once dispatched, an exception may mean a lost acknowledgement. Only explicit rejection is safe to replay.
2012
- retry = false;
2013
- const sent = await connectedClient.send(to, { text: requestText, messageId: question.questionId, expectsReply: true, delivery: "steer" });
2014
- if (currentReplyWaiter() !== waiter) return;
2015
- retry = !sent.accepted;
2016
- if (sent.accepted || !notificationFailureLogged) {
2017
- pi.appendEntry("intercom_sent", { to, messageId: question.questionId, message: { text: requestText, reason }, accepted: sent.accepted, timestamp: Date.now() });
2018
- }
2019
- notificationFailureLogged ||= !sent.accepted;
2020
- } catch (error) {
2021
- if (currentReplyWaiter() === waiter && !notificationFailureLogged) {
2022
- pi.appendEntry("intercom_question_notification_error", { questionId: question.questionId, error: getErrorMessage(error) });
2023
- notificationFailureLogged = true;
2024
- }
2025
- } finally {
2026
- if (retry && currentReplyWaiter() === waiter) {
2027
- retryAfter = Date.now() + 1000;
2028
- waiter.retryNotification = () => { void notifySupervisor(); };
2029
- }
2030
- }
2031
- };
2032
- void notifySupervisor();
2033
- const replyMessage = await replyPromise;
2034
- const replyText = replyMessage.content.text;
2035
- const replyAttachments = replyMessage.content.attachments?.length ? formatAttachments(replyMessage.content.attachments) : "";
2036
- const structuredReply = interview ? parseStructuredSupervisorReply(replyText, interview) : undefined;
2037
- pi.appendEntry("intercom_received", { from: metadata.orchestratorTarget, questionId: question.questionId, message: { text: replyText, attachments: replyMessage.content.attachments }, messageId: replyMessage.id, timestamp: replyMessage.timestamp });
2038
- recordQuestionDelivery(question, { kind: "live", runId: question.runId, deliveredAt: Date.now() });
2039
- return {
2040
- content: [{ type: "text" as const, text: `**${readQuestionState(question).answer?.origin === "human" ? "Direct user answer (human origin)" : "Reply from supervisor"}:**\n${replyText}${replyAttachments}` }],
2041
- isError: false,
2042
- details: { questionId: question.questionId, ...(structuredReply?.value ? { structuredReply: structuredReply.value } : structuredReply?.error ? { structuredReplyParseError: structuredReply.error } : {}) },
2043
- };
2044
- }
2045
-
2046
- if (childOrchestratorMetadata) {
2047
- pi.registerTool({
2048
- name: "contact_supervisor",
2049
- label: "Contact Supervisor",
2050
- description: "Subagent-only tool for contacting the supervisor agent that delegated this task. Use need_decision only when this child cannot safely continue without a decision, approval, or product/API/scope clarification; this steers the supervisor at its next tool boundary and keeps the child alive for the reply. Use interview_request only when multiple structured answers are all required before safe progress; this also steers and waits. Use progress_update only for a discovery or change the supervisor needs while working; it steers at the next tool boundary without waiting for a reply. Skip starts, redundant status, and routine completion; retain material findings in the final result.",
2051
- promptSnippet: "Subagent-only: steer the supervisor for required decisions, structured interviews, or material discoveries needed during active work. Skip routine status and completion messages.",
2052
- promptGuidelines: [
2053
- "Use contact_supervisor with reason='need_decision' when a subagent cannot safely continue without a decision, approval, or product/API/scope clarification; it steers the supervisor and waits for the reply.",
2054
- "Use contact_supervisor with reason='interview_request' only when the child cannot safely continue until it receives multiple structured answers in one blocking steered exchange.",
2055
- "Use contact_supervisor with reason='progress_update' only for a discovery or change the supervisor needs while working. It steers at the next tool boundary without waiting. Skip starts, redundant narration, and routine completion; keep material findings in the final result.",
2056
- "Do not use contact_supervisor for routine completion handoffs; return the final subagent result normally.",
2057
- ],
2058
- parameters: Type.Object({
2059
- reason: StringEnum(["need_decision", "progress_update", "interview_request"] as const, {
2060
- description: "Contact reason: 'need_decision' and 'interview_request' steer the supervisor and wait for a reply; 'progress_update' steers a material discovery without waiting for a reply",
2061
- }),
2062
- message: Type.Optional(Type.String({
2063
- description: "Decision request, optional interview note, or meaningful progress update for the supervisor",
2064
- })),
2065
- interview: Type.Optional(Type.Object({
2066
- title: Type.Optional(Type.String()),
2067
- description: Type.Optional(Type.String()),
2068
- questions: Type.Array(Type.Object({
2069
- id: Type.String(),
2070
- type: StringEnum(["single", "multi", "text", "image", "info"] as const, { description: "Question type: single, multi, text, image, or info" }),
2071
- question: Type.String(),
2072
- options: Type.Optional(Type.Array(Type.Any())),
2073
- context: Type.Optional(Type.String()),
2074
- })),
2075
- }, { description: "Structured interview request for reason='interview_request'" })),
2076
- }),
2077
- async execute(_toolCallId: string, params: ContactSupervisorToolParams, signal: AbortSignal | undefined, _onUpdate: unknown, ctx: ExtensionContext) {
2078
- return throwIfToolError(await (async () => {
2079
- const reason = params.reason as ContactSupervisorReason;
2080
- if (reason !== "need_decision" && reason !== "progress_update" && reason !== "interview_request") {
2081
- return {
2082
- content: [{ type: "text", text: "Invalid reason. Use 'need_decision', 'interview_request', or 'progress_update'." }],
2083
- isError: true,
2084
- details: { error: true },
2085
- };
2086
- }
2087
- if ((reason === "need_decision" || reason === "progress_update") && typeof params.message !== "string") {
2088
- return {
2089
- content: [{ type: "text", text: `Missing 'message' parameter for reason '${reason}'.` }],
2090
- isError: true,
2091
- details: { error: true },
2092
- };
2093
- }
2094
- const interviewValidation = reason === "interview_request"
2095
- ? validateSupervisorInterviewRequest(params.interview)
2096
- : undefined;
2097
- if (interviewValidation?.ok === false) {
2098
- return {
2099
- content: [{ type: "text", text: `Invalid interview request: ${interviewValidation.error}` }],
2100
- isError: true,
2101
- details: { error: true },
2102
- };
2103
- }
2104
- const supervisorInterview = interviewValidation?.ok === true ? interviewValidation.interview : undefined;
2105
- if (reason !== "progress_update") {
2106
- return await requestSupervisorDecision(reason, params.message, supervisorInterview, signal, ctx);
2107
- }
2108
-
2109
- let connectedClient: IntercomClient;
2110
- try {
2111
- connectedClient = await ensureConnected("tool");
2112
- } catch (error) {
2113
- return {
2114
- content: [{ type: "text", text: `Intercom not connected: ${getErrorMessage(error)}` }],
2115
- isError: true,
2116
- details: { error: true },
2117
- };
2118
- }
2119
-
2120
- syncPresenceIdentity(ctx.sessionManager.getSessionId());
2121
-
2122
- if (signal?.aborted) {
2123
- return {
2124
- content: [{ type: "text", text: "Cancelled" }],
2125
- isError: true,
2126
- details: { error: true },
2127
- };
2128
- }
2129
-
2130
- const metadata = childOrchestratorMetadata;
2131
- let sendTo: string;
2132
- try {
2133
- sendTo = await resolveSessionTarget(connectedClient, metadata.orchestratorTarget) ?? metadata.orchestratorTarget;
2134
- } catch (error) {
2135
- return {
2136
- content: [{ type: "text", text: `Failed to resolve supervisor target: ${getErrorMessage(error)}` }],
2137
- isError: true,
2138
- details: { error: true },
2139
- };
2140
- }
2141
- if (signal?.aborted) {
2142
- return {
2143
- content: [{ type: "text", text: "Cancelled" }],
2144
- isError: true,
2145
- details: { error: true },
2146
- };
2147
- }
2148
- if (sendTo === connectedClient.sessionId) {
2149
- return {
2150
- content: [{ type: "text", text: "Cannot message the current session" }],
2151
- isError: true,
2152
- details: { error: true },
2153
- };
2154
- }
2155
-
2156
- const message = params.message as string;
2157
- try {
2158
- const result = await connectedClient.send(sendTo, {
2159
- text: formatChildOrchestratorMessage("update", metadata, message),
2160
- delivery: "steer",
2161
- });
2162
- if (!result.accepted) {
2163
- const errorText = result.reason ?? "Session may not exist or has disconnected.";
2164
- return {
2165
- content: [{ type: "text", text: `Message to "${metadata.orchestratorTarget}" was not delivered: ${errorText}` }],
2166
- isError: true,
2167
- details: { messageId: result.id, accepted: result.accepted, delivered: false, reason: result.reason },
2168
- };
2169
- }
2170
- markIntercomActivity();
2171
- syncPresenceStatus();
2172
- pi.appendEntry("intercom_sent", {
2173
- to: metadata.orchestratorTarget,
2174
- message: { text: message, reason },
2175
- messageId: result.id,
2176
- timestamp: Date.now(),
2177
- subagent: { runId: metadata.runId, agent: metadata.agent, index: metadata.index },
2178
- });
2179
- return {
2180
- content: [{ type: "text", text: `Progress update accepted for supervisor ${metadata.orchestratorTarget}. Queued for the next tool boundary; broker acceptance does not confirm the supervisor has read or acted on it.` }],
2181
- isError: false,
2182
- details: { messageId: result.id, accepted: result.accepted, delivered: result.delivered, queued: result.queued === true },
2183
- };
2184
- } catch (error) {
2185
- return {
2186
- content: [{ type: "text", text: `Failed to send progress update: ${getErrorMessage(error)}` }],
2187
- isError: true,
2188
- details: { error: true },
2189
- };
2190
- }
2191
- })());
2192
- },
2193
- renderCall(args: ContactSupervisorToolParams, theme: ToolRenderTheme) {
2194
- const reason = typeof args.reason === "string" ? args.reason : "contact";
2195
- const messagePreview = previewText(args.message, 96);
2196
- const interview = args.interview && typeof args.interview === "object" ? args.interview as { title?: unknown } : undefined;
2197
- let text = theme.fg("toolTitle", theme.bold("contact_supervisor "));
2198
- text += theme.fg(reason === "need_decision" ? "warning" : reason === "progress_update" ? "muted" : "accent", reason);
2199
- if (typeof interview?.title === "string" && interview.title.trim()) {
2200
- text += " " + theme.fg("accent", interview.title.trim());
2201
- }
2202
- if (messagePreview) {
2203
- text += "\n " + theme.fg("dim", messagePreview);
2204
- }
2205
- return new Text(text, 0, 0);
2206
- },
2207
- renderResult(result: ToolResultLike, { isPartial }: { isPartial: boolean }, theme: ToolRenderTheme, context: ToolRenderContext) {
2208
- if (isPartial) {
2209
- return new Text(theme.fg("warning", "Waiting for supervisor..."), 0, 0);
2210
- }
2211
- const details = result.details as { accepted?: boolean; delivered?: boolean; queued?: boolean; error?: boolean; messageId?: string; reason?: string; structuredReplyParseError?: string } | undefined;
2212
- const textContent = firstTextContent(result);
2213
- const failed = Boolean(context.isError || details?.error === true || details?.accepted === false);
2214
- const parseWarning = typeof details?.structuredReplyParseError === "string";
2215
- let text = failed
2216
- ? theme.fg("error", "✗ ")
2217
- : parseWarning
2218
- ? theme.fg("warning", "⚠ ")
2219
- : theme.fg("success", "✓ ");
2220
- text += theme.fg(failed ? "error" : "text", textContent);
2221
- if (parseWarning) {
2222
- text += "\n" + theme.fg("warning", `Structured reply parse issue: ${details.structuredReplyParseError}`);
2223
- }
2224
- return new Text(text, 0, 0);
2225
- },
2226
- } as never);
2227
- }
2228
-
2229
- pi.registerTool({
2230
- name: "load_intercom",
2231
- label: "Load Intercom",
2232
- description: "Enable local peer coordination: list/check sessions before shared-state work, send or reply to messages, and inspect/publish topics. Does not send a message or check peers itself.",
2233
- parameters: Type.Object({}),
2234
- async execute() {
2235
- if (!pi.getAllTools().some((tool) => tool.name === "intercom" && !("namespace" in tool && tool.namespace))) throw new Error("Intercom is excluded from this session.");
2236
- activateTools(pi, ["intercom"]);
2237
- return { content: [{ type: "text" as const, text: "Intercom enabled. Use intercom({action:'list'}) to check peers, or status, pending, reply, send, ask and topics for coordination." }], details: {} };
2238
- },
2239
- });
2240
-
2241
- pi.registerTool({
2242
- name: "intercom",
2243
- defaultActive: false,
2244
- label: "Intercom",
2245
- description: `Send a message to another pi session running on this machine.
2246
- Use this to communicate findings, request help, or coordinate work with other sessions.
2247
- Non-blocking send defaults to steer for guidance, answers, corrections, or blockers that may affect active work. Use queue only when delay is intentional; use ask only when this process must remain alive waiting for the reply.
2248
-
2249
- Usage:
2250
- intercom({ action: "list" }) → List sessions in this repository and its worktrees
2251
- intercom({ action: "list", scope: "all" }) → List connected sessions across every project
2252
- intercom({ action: "send", to: "session-name", message: "..." }) → Send live coordination (defaults to steer)
2253
- intercom({ action: "ask", to: "session-name", delivery: "steer", message: "..." }) → Blocking wait only when sender must stay alive
2254
- intercom({ action: "reply", message: "..." }) → Reply to the active/single pending ask
2255
- intercom({ action: "pending" }) → List unresolved inbound asks
2256
- intercom({ action: "status" }) → Show connection status
2257
- intercom({ action: "subscribe", topic: "project/work", awaitRelease: true }) → Opt into a topic
2258
- intercom({ action: "publish", topic: "project/work", message: "Current self-contained state" }) → Quiet latest state
2259
- intercom({ action: "topics" }) → Inspect topics and current resource owners`,
2260
- promptSnippet:
2261
- "Coordinate with local Pi sessions. Non-blocking send defaults to steer for live agent guidance; queue only for intentional delay and ask only for a required blocking reply.",
2262
- promptGuidelines: [
2263
- "Intercom list/status defaults to the current Git repository and its worktrees. Use scope='all' only when intentionally discovering sessions in other projects.",
2264
- "Action='send' defaults to delivery='steer' for agent-to-agent guidance, answers, corrections, blockers, or other context that may affect active work.",
2265
- "Use delivery='queue' only when delay is intentional. For routine status, explicitly subscribe/publish topics: updates replace quiet inspectable state outside model context, not passive conversation messages. Topic blockers/decisions and awaited ownership releases interrupt; direct messages always bypass subscriptions.",
2266
- "Treat inbound steered messages as supplemental coordination within the active task: incorporate relevant context and continue; replace the task only when the message explicitly says so.",
2267
- "Use action='reply' for an active inbound ask. Otherwise respond with send plus steer; use blocking ask only when this process must stay alive and cannot safely continue without the answer.",
2268
- ],
2269
-
2270
- parameters: Type.Object({
2271
- action: StringEnum(["list", "send", "ask", "reply", "pending", "status", "subscribe", "unsubscribe", "publish", "topics"] as const, {
2272
- description: "Direct messages: list/send/ask/reply/pending/status. Quiet state: subscribe/unsubscribe/publish/topics.",
2273
- }),
2274
- scope: Type.Optional(StringEnum(["project", "all"] as const, {
2275
- description: "For list/status: 'project' (default) shows this Git repository and its worktrees; 'all' includes sessions in other projects.",
2276
- })),
2277
- to: Type.Optional(Type.String({
2278
- description: "Target session name or ID (for 'send', 'ask', or disambiguating 'reply')",
2279
- })),
2280
- topic: Type.Optional(Type.String({ description: "Exact opt-in topic for subscribe/unsubscribe/publish/topics; no automatic subscriptions." })),
2281
- event: Type.Optional(StringEnum(["update", "blocker", "decision", "release"] as const, { description: "Publish: update is quiet current state. Blockers/decisions interrupt subscribers; release interrupts only those awaiting release." })),
2282
- resource: Type.Optional(Type.String({ description: "Publish: resource this session is using (advisory ownership, not a lock)." })),
2283
- ownership: Type.Optional(StringEnum(["held", "released"] as const, { description: "Publish: this session's declared resource state. Disconnect never implies released." })),
2284
- awaitRelease: Type.Optional(Type.Boolean({ description: "Subscribe: interrupt when this topic explicitly releases ownership." })),
2285
- message: Type.Optional(Type.String({
2286
- description: "Message for send/ask/reply, or self-contained latest state for publish",
2287
- })),
2288
- attachments: Type.Optional(Type.Array(Type.Object({
2289
- type: StringEnum(["file", "snippet", "context"] as const),
2290
- name: Type.String(),
2291
- content: Type.String(),
2292
- language: Type.Optional(Type.String()),
2293
- }))),
2294
- replyTo: Type.Optional(Type.String({
2295
- description: "Message ID to reply to (for threading or responding to an 'ask')",
2296
- })),
2297
- delivery: Type.Optional(StringEnum(["queue", "steer", "passive"] as const, {
2298
- description: "Delivery mode. Omitted send delivery defaults to 'steer', which injects after the current tool call. Use 'queue' only to intentionally wait behind active work; 'passive' does not wake the recipient model.",
2299
- })),
2300
- queueMode: Type.Optional(StringEnum(["stack", "replace"] as const, {
2301
- description: "For delivery='queue': 'stack' keeps all messages; 'replace' keeps only the latest undelivered message for the same threadId.",
2302
- })),
2303
- threadId: Type.Optional(Type.String({
2304
- description: "Stable topic key for queueMode='replace'.",
2305
- })),
2306
- passive: Type.Optional(Type.Boolean({
2307
- description: "For action='send' only: legacy alias for delivery='passive'. Discouraged for agent-to-agent messages.",
2308
- })),
2309
- }),
2310
-
2311
- async execute(_toolCallId: string, params: IntercomToolParams, _signal: AbortSignal | undefined, _onUpdate: unknown, ctx: ExtensionContext) {
2312
- return throwIfToolError(await (async () => {
2313
- let connectedClient: IntercomClient;
2314
- try {
2315
- connectedClient = await ensureConnected("tool");
2316
- } catch (error) {
2317
- if (params.action === "topics") { topics.disconnected(); return { content: [{ type: "text", text: `Broker unavailable; last saved records follow.\n${topics.inspect(params.topic)}` }], details: { connection: "disconnected" } }; }
2318
- return {
2319
- content: [{ type: "text", text: `Intercom not connected: ${getErrorMessage(error)}` }],
2320
- isError: true,
2321
- details: { error: true },
2322
- };
2323
- }
2324
-
2325
- syncPresenceIdentity(ctx.sessionManager.getSessionId());
2326
-
2327
- const { action, scope = "project", to, message, attachments, replyTo, delivery, queueMode, threadId, passive } = params;
2328
- if (["subscribe", "unsubscribe", "publish", "topics"].includes(action)) {
2329
- if (!connectedClient.supportsTopics) {
2330
- topics.disconnected();
2331
- return { content: [{ type: "text", text: `${TOPICS_UNAVAILABLE}${action === "topics" ? `\n\nLast saved records:\n${topics.inspect(params.topic)}` : ""}` }], isError: true, details: { topicsSupported: false } };
2332
- }
2333
- const generation = runtimeGeneration;
2334
- const topic = params.topic?.trim();
2335
- if (topic && !isTopicSubscription({ topic, awaitRelease: params.awaitRelease })) return { content: [{ type: "text", text: "Topic must be a plain nonempty label; awaitRelease must be boolean." }], isError: true, details: {} };
2336
- if (action !== "topics" && !topic) return { content: [{ type: "text", text: `${action} requires an exact topic.` }], isError: true, details: {} };
2337
- let change: TopicChange | undefined;
2338
- if (action === "subscribe") change = { action: "subscribe", subscription: { topic: topic!, ...(params.awaitRelease ? { awaitRelease: true } : {}) } };
2339
- if (action === "unsubscribe") change = { action: "unsubscribe", topic: topic! };
2340
- if (action === "publish") {
2341
- const event = params.event ?? (params.ownership === "released" ? "release" : "update");
2342
- const update: TopicUpdate = { topic: topic!, text: message ?? "", event, resource: params.resource, ownership: params.ownership,
2343
- revision: (topics.published.get(topic!)?.revision ?? 0) + 1, updatedAt: Date.now() };
2344
- if (!update.text.trim() || !isTopicUpdate(update) || (event === "release" && (!update.resource || update.ownership !== "released"))) return { content: [{ type: "text", text: "Publish requires self-contained message text; ownership needs resource, and release needs ownership:'released'." }], isError: true, details: {} };
2345
- change = { action: "publish", topic: update };
2346
- }
2347
- const commit = (snapshot: SessionSnapshot) => {
2348
- if (!getLiveContext(ctx, generation)) throw new Error("Session changed; no topic state saved in this session.");
2349
- if (action === "subscribe") topics.subscribe(topic!, params.awaitRelease);
2350
- if (action === "unsubscribe") topics.unsubscribe(topic!);
2351
- if (action === "publish") {
2352
- const update = snapshot.sessions.find((session) => session.id === connectedClient.sessionId)?.topics?.find((entry) => entry.topic === topic);
2353
- if (!update) throw new Error("Broker did not confirm the topic publication; prior saved state is unchanged.");
2354
- topics.publish(update, { id: connectedClient.sessionId!, name: pi.getSessionName() });
2355
- }
2356
- topics.refresh(snapshot.sessions);
2357
- };
2358
- const snapshot = change ? await connectedClient.updateTopics(change, commit) : { sessions: await connectedClient.listSessions(), receipts: [] };
2359
- if (!getLiveContext(ctx, generation)) return { content: [{ type: "text", text: "Session changed; no topic state saved in this session." }], isError: true, details: {} };
2360
- if (!change) topics.refresh(snapshot.sessions);
2361
- if (topicSyncError) {
2362
- const own = snapshot.sessions.find((session) => session.id === connectedClient.sessionId);
2363
- const published = new Map(own?.topics?.map((update) => [update.topic, update]));
2364
- const subscribed = new Map(own?.subscriptions?.map((entry) => [entry.topic, entry]));
2365
- if (own && published.size === topics.published.size && subscribed.size === topics.subscriptions.size
2366
- && [...topics.published].every(([key, update]) => JSON.stringify(published.get(key)) === JSON.stringify(update))
2367
- && [...topics.subscriptions].every(([key, entry]) => subscribed.has(key) && Boolean(subscribed.get(key)?.awaitRelease) === Boolean(entry.awaitRelease))) topicSyncError = undefined;
2368
- else topics.disconnected(connectedClient.sessionId!);
2369
- }
2370
- if (action === "publish") {
2371
- const receipts = snapshot.receipts;
2372
- return { content: [{ type: "text", text: `Current state saved for ${topic}. ${receipts.filter((receipt) => receipt.accepted).length}/${receipts.length} subscribed deliveries accepted; this does not confirm reading or action. Routine updates stay outside conversation context.` }], details: { topic, receipts } };
2373
- }
2374
- return { content: [{ type: "text", text: `${topicSyncError ? `Some saved topic state could not be restored: ${topicSyncError}\nOrdinary messaging remains available. Correct the saved publication or subscription.\n\n` : ""}${action === "topics" ? "" : `${action === "subscribe" ? "Subscribed to" : "Unsubscribed from"} ${topic}.\n`}${topics.inspect(topic)}` }], details: { topic } };
2375
- }
2376
- if (params.scope !== undefined && action !== "list" && action !== "status") {
2377
- return {
2378
- content: [{ type: "text", text: "'scope' is only valid for action='list' or action='status'" }],
2379
- isError: true,
2380
- details: { error: true },
2381
- };
2382
- }
2383
- if (passive !== undefined && action !== "send") {
2384
- return {
2385
- content: [{ type: "text", text: "'passive' is only valid for action='send'" }],
2386
- isError: true,
2387
- details: { error: true },
2388
- };
2389
- }
2390
- if (delivery === "passive" && action !== "send") {
2391
- return {
2392
- content: [{ type: "text", text: "delivery='passive' is only valid for action='send'. Passive delivery is for human-visible breadcrumbs and is discouraged for agent-to-agent coordination; normal send defaults to steer. Use ask with delivery='steer' only when the sender must stay alive and cannot safely continue without the reply." }],
2393
- isError: true,
2394
- details: { error: true },
2395
- };
2396
- }
2397
- if ((delivery !== undefined || queueMode !== undefined || threadId !== undefined) && action !== "send" && action !== "ask") {
2398
- return {
2399
- content: [{ type: "text", text: "'delivery', 'queueMode', and 'threadId' are only valid for action='send' or action='ask'" }],
2400
- isError: true,
2401
- details: failureDetails(queueMode !== undefined || delivery === "queue" || threadId !== undefined ? "invalid_queue_arguments" : "invalid_delivery_arguments", [{ action: "send", guidance: "Use delivery options only with send or ask." }], { error: true }),
2402
- };
2403
- }
2404
- if (passive === true && delivery !== undefined && delivery !== "passive") {
2405
- return {
2406
- content: [{ type: "text", text: "'passive' cannot be combined with a non-passive delivery mode" }],
2407
- isError: true,
2408
- details: { error: true },
2409
- };
2410
- }
2411
- if (threadId !== undefined && queueMode !== "replace") {
2412
- return {
2413
- content: [{ type: "text", text: "'threadId' is only valid with queueMode='replace'" }],
2414
- isError: true,
2415
- details: failureDetails("invalid_queue_arguments", [{ action: "send", guidance: "Use threadId only with delivery='queue' and queueMode='replace'." }], { error: true }),
2416
- };
2417
- }
2418
- if (queueMode === "replace" && (!threadId || !threadId.trim())) {
2419
- return {
2420
- content: [{ type: "text", text: "queueMode='replace' requires a non-empty threadId" }],
2421
- isError: true,
2422
- details: failureDetails("invalid_queue_arguments", [{ action: "send", guidance: "Provide a non-empty threadId with delivery='queue' and queueMode='replace'." }], { error: true }),
2423
- };
2424
- }
2425
- const deliveryMode = (passive === true ? "passive" : delivery) as MessageDelivery | undefined;
2426
- const cleanedThreadId = typeof threadId === "string" ? threadId.trim() : undefined;
2427
- if (queueMode !== undefined && deliveryMode !== "queue") {
2428
- return {
2429
- content: [{ type: "text", text: "'queueMode' is only valid with delivery='queue'. Use queue only for intentionally deferred work; otherwise omit queueMode and use default-steered send for live agent coordination. Avoid passive for agent-to-agent coordination." }],
2430
- isError: true,
2431
- details: failureDetails("invalid_queue_arguments", [{ action: "send", guidance: "Set delivery='queue', or omit queueMode and threadId." }], { error: true }),
2432
- };
2433
- }
2434
-
2435
- switch (action) {
2436
- case "list": {
2437
- try {
2438
- const mySessionId = connectedClient.sessionId;
2439
- if (!mySessionId) throw new Error("Current intercom session id is unavailable.");
2440
- const allSessions = await connectedClient.listSessions();
2441
- const sessions = sessionsForScope(allSessions, mySessionId, scope);
2442
- return {
2443
- content: [{ type: "text", text: formatSessionListSections(allSessions, mySessionId, scope) }],
2444
- isError: false,
2445
- details: { sessionCount: sessions.length },
2446
- };
2447
- } catch (error) {
2448
- return {
2449
- content: [{ type: "text", text: `Failed to list sessions: ${getErrorMessage(error)}` }],
2450
- isError: true,
2451
- details: { error: true },
2452
- };
2453
- }
2454
- }
2455
-
2456
- case "send": {
2457
- if (!to || !message) {
2458
- return {
2459
- content: [{ type: "text", text: "Missing 'to' or 'message' parameter" }],
2460
- isError: true,
2461
- details: { error: true },
2462
- };
2463
- }
2464
- try {
2465
- const sendTo = await resolveSessionTarget(connectedClient, to) ?? to;
2466
- if (sendTo === connectedClient.sessionId) {
2467
- return {
2468
- content: [{ type: "text", text: "Cannot message the current session" }],
2469
- isError: true,
2470
- details: { error: true },
2471
- };
2472
- }
2473
- if (!replyTo && config.confirmSend && ctx.hasUI) {
2474
- const attachmentText = attachments?.length ? formatAttachments(attachments) : "";
2475
- const confirmed = await ctx.ui.confirm(
2476
- "Send Message",
2477
- `Send to "${to}":\n\n${message}${attachmentText}`,
2478
- );
2479
- if (!confirmed) {
2480
- return {
2481
- content: [{ type: "text", text: "Message cancelled by user" }],
2482
- isError: false,
2483
- };
2484
- }
2485
- }
2486
- const result = await connectedClient.send(sendTo, {
2487
- text: message,
2488
- attachments,
2489
- replyTo,
2490
- delivery: deliveryMode,
2491
- queueMode: queueMode as QueueMode | undefined,
2492
- threadId: cleanedThreadId,
2493
- passive: passive === true,
2494
- });
2495
- if (!result.accepted) {
2496
- const errorText = result.reason ?? "Session may not exist or has disconnected.";
2497
- return {
2498
- content: [{ type: "text", text: `Message to "${to}" was not delivered: ${errorText}` }],
2499
- isError: true,
2500
- details: failureDetails("delivery_failed", [{ action: "list" }, { action: "send", guidance: "Retry with an exact active recipient target." }], { messageId: result.id, accepted: result.accepted, delivered: false, reason: result.reason }),
2501
- };
2502
- }
2503
- markIntercomActivity();
2504
- pi.appendEntry("intercom_sent", {
2505
- to,
2506
- message: { text: message, attachments, replyTo, delivery: deliveryMode, queueMode, threadId: cleanedThreadId, passive: passive === true },
2507
- messageId: result.id,
2508
- timestamp: Date.now(),
2509
- });
2510
- if (replyTo) {
2511
- replyTracker.markReplied(replyTo);
2512
- }
2513
- syncPresenceStatus();
2514
- const replyModeHint = replyTo
2515
- ? ""
2516
- : deliveryMode === "passive"
2517
- ? " (passive; recipient model was not woken)"
2518
- : deliveryMode === "steer"
2519
- ? " (steers active recipient after the current tool call)"
2520
- : deliveryMode === "queue"
2521
- ? " (intentionally deferred behind active recipient work)"
2522
- : " (defaults to steer; wakes idle recipients and steers active recipients after the current tool call)";
2523
- return {
2524
- content: [{ type: "text", text: result.queued ? `Message queued for ${to} (${result.reason ?? "queued"})` : `Message sent to ${to}${replyModeHint}` }],
2525
- isError: false,
2526
- details: { messageId: result.id, accepted: result.accepted, delivered: result.delivered, queued: result.queued === true, reasonCode: result.queued ? "message_queued" : "message_accepted" },
2527
- };
2528
- } catch (error) {
2529
- return {
2530
- content: [{ type: "text", text: `Failed to send: ${getErrorMessage(error)}` }],
2531
- isError: true,
2532
- details: failureDetails(getErrorMessage(error).startsWith("Target ") ? "ambiguous_target" : "send_failed", [{ action: "list" }, { action: "send", guidance: "Retry with an exact active recipient target." }], { error: true }),
2533
- };
2534
- }
2535
- }
2536
-
2537
- case "ask": {
2538
- if (!to || !message) {
2539
- return {
2540
- content: [{ type: "text", text: "Missing 'to' or 'message' parameter" }],
2541
- isError: true,
2542
- details: { error: true },
2543
- };
2544
- }
2545
-
2546
- if (replyWaiter) {
2547
- return {
2548
- content: [{ type: "text", text: "Already waiting for a reply" }],
2549
- isError: true,
2550
- details: { error: true },
2551
- };
2552
- }
2553
-
2554
- if (_signal?.aborted) {
2555
- return {
2556
- content: [{ type: "text", text: "Cancelled" }],
2557
- isError: true,
2558
- details: { error: true },
2559
- };
2560
- }
2561
- let questionId: string | undefined;
2562
-
2563
- try {
2564
- const sendTo = await resolveSessionTarget(connectedClient, to) ?? to;
2565
- if (_signal?.aborted) {
2566
- return {
2567
- content: [{ type: "text", text: "Cancelled" }],
2568
- isError: true,
2569
- details: { error: true },
2570
- };
2571
- }
2572
- if (sendTo === connectedClient.sessionId) {
2573
- return {
2574
- content: [{ type: "text", text: "Cannot message the current session" }],
2575
- isError: true,
2576
- details: { error: true },
2577
- };
2578
- }
2579
- const peerHealth = await resolvePeerHealth(connectedClient, sendTo);
2580
- const skipReplyWait = peerDeclinesAsks(peerHealth) && deliveryMode === undefined;
2581
- if (_signal?.aborted) {
2582
- return {
2583
- content: [{ type: "text", text: "Cancelled" }],
2584
- isError: true,
2585
- details: { error: true },
2586
- };
2587
- }
2588
- questionId = randomUUID();
2589
- const sendOptions = {
2590
- text: message,
2591
- attachments,
2592
- replyTo,
2593
- delivery: deliveryMode,
2594
- queueMode: queueMode as QueueMode | undefined,
2595
- threadId: cleanedThreadId,
2596
- };
2597
- const recordSent = (sendResult: SendResult) => pi.appendEntry("intercom_sent", {
2598
- to,
2599
- message: { text: message, attachments, replyTo, delivery: deliveryMode, queueMode, threadId: cleanedThreadId },
2600
- messageId: sendResult.id,
2601
- timestamp: Date.now(),
2602
- });
2603
- let replyMessage: Message;
2604
- if (skipReplyWait) {
2605
- const sendResult = await connectedClient.send(sendTo, { ...sendOptions, messageId: questionId, expectsReply: true });
2606
- if (!sendResult.accepted) throw new AskDeliveryError(sendResult);
2607
- markIntercomActivity();
2608
- syncPresenceStatus();
2609
- recordSent(sendResult);
2610
- return {
2611
- content: [{ type: "text", text: `${sendResult.delivered ? "Delivered" : "Queued"} ask to ${to}; peer is busy and not accepting blocking asks (peer_busy). Not waiting for a reply; delivery does not mean the question was consumed.` }],
2612
- isError: false,
2613
- details: { messageId: sendResult.id, accepted: sendResult.accepted, delivered: sendResult.delivered, queued: sendResult.queued === true, replied: false, reason: "peer_busy", reasonCode: "recipient_not_accepting_asks", nextActions: [{ action: "send", guidance: "Use default-steered send for non-blocking live coordination; queue only when delay is intentional." }] },
2614
- };
2615
- }
2616
- replyMessage = await sendAskTransaction(connectedClient, sendTo, questionId, sendOptions, _signal, recordSent);
2617
- const replyText = replyMessage.content.text;
2618
- const replyAttachments = replyMessage.content.attachments?.length
2619
- ? formatAttachments(replyMessage.content.attachments)
2620
- : "";
2621
- pi.appendEntry("intercom_received", {
2622
- from: to,
2623
- message: { text: replyText, attachments: replyMessage.content.attachments },
2624
- messageId: replyMessage.id,
2625
- timestamp: replyMessage.timestamp,
2626
- });
2627
- const recipientTurnFailure = replyMessage.content.attachments?.some((attachment) => attachment.name === RECIPIENT_TURN_FAILED_ATTACHMENT);
2628
- if (recipientTurnFailure) {
2629
- return {
2630
- content: [{ type: "text", text: replyText }],
2631
- isError: true,
2632
- details: failureDetails("recipient_turn_failed", [{ action: "status" }, { action: "send", guidance: "Send recovery context after the recipient is healthy." }], { error: true, recipientTurnFailed: true, messageId: replyMessage.id, replyTo: questionId }),
2633
- };
2634
- }
2635
- return {
2636
- content: [{ type: "text", text: `**Reply from ${to}:**\n${replyText}${replyAttachments}` }],
2637
- isError: false,
2638
- };
2639
- } catch (error) {
2640
- if (error instanceof AskDeliveryError) {
2641
- return {
2642
- content: [{ type: "text", text: `Message to "${to}" was not delivered: ${error.message}` }],
2643
- isError: true,
2644
- details: failureDetails("delivery_failed", [{ action: "list" }, { action: "send", guidance: "Retry with an exact active recipient target." }], { error: true, messageId: error.result.id, accepted: false, delivered: false, reason: error.result.reason }),
2645
- };
2646
- }
2647
- const errorMessage = getErrorMessage(error);
2648
- const timedOut = errorMessage.startsWith("No reply from");
2649
- return {
2650
- content: [{ type: "text", text: `Failed: ${errorMessage}` }],
2651
- isError: true,
2652
- details: failureDetails(timedOut ? "reply_timeout" : errorMessage.startsWith("Target ") ? "ambiguous_target" : "ask_failed", [{ action: "status" }, { action: "list" }, { action: "send", guidance: "Check the recipient before retrying; the original ask may still be seen." }], { error: true, ...(questionId ? { messageId: questionId } : {}) }),
2653
- };
2654
- }
2655
- }
2656
-
2657
- case "reply": {
2658
- if (!message) {
2659
- return {
2660
- content: [{ type: "text", text: "Missing 'message' parameter" }],
2661
- isError: true,
2662
- details: { error: true },
2663
- };
2664
- }
2665
-
2666
- try {
2667
- const target = replyTracker.resolveReplyTarget({ to, replyTo });
2668
- if (target.from.id === connectedClient.sessionId) {
2669
- return {
2670
- content: [{ type: "text", text: "Cannot message the current session" }],
2671
- isError: true,
2672
- details: { error: true },
2673
- };
2674
- }
2675
- const result = await connectedClient.send(target.from.id, {
2676
- text: message,
2677
- attachments,
2678
- replyTo: target.message.id,
2679
- });
2680
- if (!result.accepted) {
2681
- const errorText = result.reason ?? "Session may not exist or has disconnected.";
2682
- return {
2683
- content: [{ type: "text", text: `Reply to "${target.from.name || target.from.id}" was not delivered: ${errorText}` }],
2684
- isError: true,
2685
- details: failureDetails("delivery_failed", [{ action: "pending" }, { action: "send", guidance: "Send a new message if the original sender is no longer available." }], { messageId: result.id, accepted: result.accepted, delivered: false, queued: result.queued === true, reason: result.reason, replyTo: target.message.id }),
2686
- };
2687
- }
2688
- replyTracker.markReplied(target.message.id);
2689
- markIntercomActivity();
2690
- syncPresenceStatus();
2691
- pi.appendEntry("intercom_sent", {
2692
- to: target.from.name || target.from.id,
2693
- message: { text: message, replyTo: target.message.id },
2694
- messageId: result.id,
2695
- timestamp: Date.now(),
2696
- });
2697
- return {
2698
- content: [{ type: "text", text: `Reply sent to ${target.from.name || target.from.id}` }],
2699
- isError: false,
2700
- details: { messageId: result.id, delivered: true, replyTo: target.message.id, reasonCode: "reply_sent" },
2701
- };
2702
- } catch (error) {
2703
- const errorMessage = getErrorMessage(error);
2704
- const reasonCode = replyFailureReason(errorMessage);
2705
- return {
2706
- content: [{ type: "text", text: `Failed to reply: ${errorMessage}` }],
2707
- isError: true,
2708
- details: failureDetails(reasonCode, reasonCode === "no_pending_reply"
2709
- ? [{ action: "pending" }, { action: "send", guidance: "Use send with an explicit recipient when there is no inbound ask to reply to." }]
2710
- : [{ action: "pending" }, { action: "list" }], { error: true, ...(replyTo ? { replyTo } : {}) }),
2711
- };
2712
- }
2713
- }
2714
-
2715
- case "pending": {
2716
- const pendingAsks = replyTracker.listPending();
2717
- if (pendingAsks.length === 0) {
2718
- return {
2719
- content: [{ type: "text", text: "No unresolved inbound asks." }],
2720
- isError: false,
2721
- };
2722
- }
2723
-
2724
- const now = Date.now();
2725
- const lines = pendingAsks.map(({ from, message, receivedAt }) => {
2726
- const preview = pendingAskPreview(message);
2727
- const elapsedSeconds = Math.max(0, Math.floor((now - receivedAt) / 1000));
2728
- const sender = from.name ? `${from.name} (${formatSessionTarget(from, pendingAsks.map((ask) => ask.from))})` : from.id;
2729
- const replyTo = JSON.stringify(message.id);
2730
- return `- ${sender} · replyTo: ${replyTo} · ${elapsedSeconds}s ago · ${preview}\n Reply: intercom({ action: "reply", replyTo: ${replyTo}, message: "..." })`;
2731
- });
2732
- return {
2733
- content: [{ type: "text", text: `**Pending asks:**\n${lines.join("\n")}` }],
2734
- isError: false,
2735
- };
2736
- }
2737
-
2738
- case "status": {
2739
- try {
2740
- const mySessionId = connectedClient.sessionId;
2741
- if (!mySessionId) throw new Error("Current intercom session id is unavailable.");
2742
- const allSessions = await connectedClient.listSessions();
2743
- const sessions = sessionsForScope(allSessions, mySessionId, scope);
2744
- reconcileConsumedInbound(ctx);
2745
- const pending = [...pendingInbound.values()].map((entry) => ({
2746
- entry,
2747
- state: entry.stage === "queued" ? "queued; not yet delivered to model" : "delivered to model queue; not yet consumed",
2748
- }));
2749
- const pendingText = pending.length
2750
- ? `\n\nPending inbound messages: ${pending.length}\n${pending.map(({ entry, state }) => `- ${entry.from.name || entry.from.id} [${entry.message.id}] (${state})\n${entry.bodyText}`).join("\n\n")}`
2751
- : "\n\nPending inbound messages: 0";
2752
- return {
2753
- content: [{
2754
- type: "text",
2755
- text: `**Intercom Status:**\nConnected: Yes\nSession ID: ${mySessionId}\nConnected sessions in scope: ${sessions.length}\n\n${formatSessionListSections(allSessions, mySessionId, scope)}${pendingText}`,
2756
- }],
2757
- isError: false,
2758
- };
2759
- } catch (error) {
2760
- return {
2761
- content: [{ type: "text", text: `Failed to get status: ${getErrorMessage(error)}` }],
2762
- isError: true,
2763
- details: { error: true },
2764
- };
2765
- }
2766
- }
2767
-
2768
- default:
2769
- return {
2770
- content: [{ type: "text", text: `Unknown action: ${action}` }],
2771
- isError: true,
2772
- details: { error: true },
2773
- };
2774
- }
2775
- })());
2776
- },
2777
- renderCall(args: IntercomToolParams, theme: ToolRenderTheme) {
2778
- const action = typeof args.action === "string" ? args.action : "intercom";
2779
- const target = typeof args.to === "string" && args.to.trim() ? args.to.trim() : undefined;
2780
- const messagePreview = previewText(args.message, 96);
2781
- const attachmentCount = Array.isArray(args.attachments) ? args.attachments.length : 0;
2782
- let text = theme.fg("toolTitle", theme.bold("intercom "));
2783
- text += theme.fg(action === "ask" ? "warning" : action === "reply" ? "success" : "accent", action);
2784
- if (target) {
2785
- text += " " + theme.fg("muted", "→") + " " + theme.fg("accent", target);
2786
- }
2787
- if (attachmentCount > 0) {
2788
- text += " " + theme.fg("dim", `(${attachmentCount} attachment${attachmentCount === 1 ? "" : "s"})`);
2789
- }
2790
- if (messagePreview) {
2791
- text += "\n " + theme.fg("dim", messagePreview);
2792
- }
2793
- return new Text(text, 0, 0);
2794
- },
2795
- renderResult(result: ToolResultLike, { expanded, isPartial }: { expanded?: boolean; isPartial: boolean }, theme: ToolRenderTheme, context: ToolRenderContext) {
2796
- if (isPartial) {
2797
- return new Text(theme.fg("warning", "Intercom working..."), 0, 0);
2798
- }
2799
- const details = result.details as { accepted?: boolean; delivered?: boolean; queued?: boolean; error?: boolean; messageId?: string; reason?: string; sessionCount?: number } | undefined;
2800
- const failed = Boolean(context.isError || details?.error === true || details?.accepted === false);
2801
- const action = context.args && typeof context.args === "object" && "action" in context.args
2802
- ? (context.args as { action?: unknown }).action
2803
- : undefined;
2804
- if (!failed && action === "list" && !expanded) {
2805
- const summary = details?.sessionCount === undefined
2806
- ? "Sessions listed"
2807
- : `${details.sessionCount} session${details.sessionCount === 1 ? "" : "s"}`;
2808
- const key = keyText("app.tools.expand");
2809
- return new Text(`${theme.fg("success", "✓ ")}${theme.fg("text", summary)}${key ? ` ${theme.fg("dim", `(${key} to expand)`)}` : ""}`, 0, 0);
2810
- }
2811
- let text = failed ? theme.fg("error", "✗ ") : theme.fg("success", "✓ ");
2812
- text += theme.fg(failed ? "error" : "text", firstTextContent(result));
2813
- if (details?.messageId && !expanded) {
2814
- text += theme.fg("dim", ` (${details.messageId.slice(0, 8)})`);
2815
- }
2816
- if (details?.reason && expanded) {
2817
- text += "\n" + theme.fg("dim", `Reason: ${details.reason}`);
2818
- }
2819
- return new Text(text, 0, 0);
2820
- },
2821
- } as never);
2822
-
2823
- async function openIntercomOverlay(ctx: ExtensionContext, scope: IntercomSessionScope = "project"): Promise<void> {
2824
- const overlayGeneration = runtimeGeneration;
2825
- const liveContext = getLiveContext(ctx, overlayGeneration);
2826
- if (!liveContext?.hasUI) return;
2827
-
2828
- let overlayClient: IntercomClient;
2829
- try {
2830
- overlayClient = await ensureConnected("overlay");
2831
- } catch (error) {
2832
- notifyIfLive(ctx, `Intercom unavailable: ${getErrorMessage(error)}`, "error", overlayGeneration);
2833
- return;
2834
- }
2835
- if (!getLiveContext(ctx, overlayGeneration)) return;
2836
-
2837
- syncPresenceIdentity(ctx.sessionManager.getSessionId());
2838
-
2839
- let currentSession: SessionInfo;
2840
- let sessions: SessionInfo[];
2841
- let allSessions: SessionInfo[];
2842
- let hiddenSessionCount = 0;
2843
- try {
2844
- const mySessionId = overlayClient.sessionId;
2845
- if (!mySessionId) throw new Error("Current intercom session id is unavailable.");
2846
- allSessions = await overlayClient.listSessions();
2847
- if (!getLiveContext(ctx, overlayGeneration)) return;
2848
- const foundCurrentSession = allSessions.find(s => s.id === mySessionId);
2849
- if (!foundCurrentSession) {
2850
- notifyIfLive(ctx, "Current session is missing from intercom session list", "error", overlayGeneration);
2851
- return;
2852
- }
2853
- currentSession = foundCurrentSession;
2854
- const scopedSessions = sessionsForScope(allSessions, mySessionId, scope);
2855
- hiddenSessionCount = allSessions.length - scopedSessions.length;
2856
- sessions = scopedSessions.filter(s => s.id !== mySessionId);
2857
- } catch (error) {
2858
- notifyIfLive(ctx, `Failed to list sessions: ${getErrorMessage(error)}`, "error", overlayGeneration);
2859
- return;
2860
- }
2861
-
2862
- const selectedSession = await ctx.ui.custom<SessionInfo | undefined>(
2863
- (tui, theme, keybindings, done) => new SessionListOverlay(tui, theme, keybindings, currentSession, sessions, done, hiddenSessionCount, allSessions),
2864
- { overlay: true }
2865
- ).catch(() => undefined);
2866
-
2867
- if (!selectedSession || !getLiveContext(ctx, overlayGeneration)) return;
2868
-
2869
- try {
2870
- overlayClient = await ensureConnected("overlay");
2871
- } catch (error) {
2872
- notifyIfLive(ctx, `Intercom unavailable: ${getErrorMessage(error)}`, "error", overlayGeneration);
2873
- return;
2874
- }
2875
- if (!getLiveContext(ctx, overlayGeneration)) return;
2876
-
2877
- const targetLabel = targetDisplayName(selectedSession, allSessions);
2878
-
2879
- const result = await ctx.ui.custom<ComposeResult>(
2880
- (tui, theme, keybindings, done) => new ComposeOverlay(tui, theme, keybindings, selectedSession, targetLabel, overlayClient, done),
2881
- { overlay: true }
2882
- ).catch(() => undefined);
2883
-
2884
- if (result?.sent && result.messageId && result.text && getLiveContext(ctx, overlayGeneration)) {
2885
- pi.appendEntry("intercom_sent", {
2886
- to: selectedSession.name || selectedSession.id,
2887
- message: { text: result.text, expectsReply: result.expectsReply },
2888
- messageId: result.messageId,
2889
- timestamp: Date.now(),
2890
- });
2891
- notifyIfLive(ctx, `${result.expectsReply ? "Ask sent" : "Message sent"} to ${targetLabel}`, "info", overlayGeneration);
2892
- }
2893
- }
2894
-
2895
- pi.registerCommand("intercom", {
2896
- description: "Open peer messaging; 'all' includes other projects, 'topics' inspects quiet topic/owner state",
2897
- handler: async (args, ctx) => {
2898
- const requestedScope = args.trim();
2899
- if (requestedScope === "topics") {
2900
- const generation = runtimeGeneration;
2901
- let notice: string | undefined;
2902
- try {
2903
- const active = await ensureConnected("overlay");
2904
- if (!active.supportsTopics) { notice = TOPICS_UNAVAILABLE; topics.disconnected(); }
2905
- else {
2906
- const sessions = await active.listSessions();
2907
- if (getLiveContext(ctx, generation)) topics.refresh(sessions);
2908
- }
2909
- } catch { if (getLiveContext(ctx, generation)) { topics.disconnected(); notice = "Broker unavailable; last saved topic records follow."; } }
2910
- if (getLiveContext(ctx, generation)) await topics.open(ctx, notice);
2911
- return;
2912
- }
2913
- if (requestedScope && requestedScope !== "all") {
2914
- ctx.ui.notify("Usage: /intercom [all|topics]", "warning");
2915
- return;
2916
- }
2917
- await openIntercomOverlay(ctx, requestedScope === "all" ? "all" : "project");
2918
- },
2919
- });
2920
-
2921
- pi.registerShortcut(config.shortcut, {
2922
- description: "Toggle your agents, or open connected sessions",
2923
- handler: async (ctx) => {
2924
- const request = { ctx, handled: false };
2925
- pi.events.emit("subagent:open-agents", request);
2926
- if (!request.handled) await openIntercomOverlay(ctx);
2927
47
  },
2928
48
  });
49
+ const delivery = new InboundDeliveryOwner(pi, lifecycle, journal, {
50
+ replies,
51
+ syncStatus: () => connection.syncStatus(),
52
+ });
53
+ const inbound = new IntercomInbound(lifecycle, config, {
54
+ journal,
55
+ delivery,
56
+ replies,
57
+ wait,
58
+ connection,
59
+ topics,
60
+ child,
61
+ });
62
+ const outbound = new IntercomOutbound({ pi, lifecycle, connection, replies, wait }, config);
63
+ const inspection = new IntercomInspection(journal, replies);
64
+ const topicActions = new IntercomTopicActions({ pi, lifecycle, connection, topics });
65
+ const tools = new IntercomTools({ connection, outbound, topicActions, topics, inspection });
66
+ const supervisor = child
67
+ ? new IntercomSupervisor({ pi, lifecycle, connection, wait }, child)
68
+ : null;
69
+ const overlay = new IntercomOverlay({ pi, lifecycle, connection, topics });
70
+ const bridges = new SubagentEventBridges(lifecycle, connection, delivery, {
71
+ pi,
72
+ journal,
73
+ open: (ctx, scope) => overlay.open(ctx, scope),
74
+ });
75
+ const awareness = child ? null : new PeerAwareness(connection, lifecycle);
76
+ bridges.start();
77
+ new IntercomSessionEvents(pi, {
78
+ lifecycle,
79
+ connection,
80
+ delivery,
81
+ journal,
82
+ replies,
83
+ wait,
84
+ topics,
85
+ bridges,
86
+ supervisor,
87
+ awareness,
88
+ }).register();
89
+ registerMessageRenderers(pi);
90
+ registerIntercomTools(pi, tools, supervisor);
91
+ overlay.register(config.shortcut);
2929
92
  }