@oh-my-pi/pi-coding-agent 17.0.8 → 17.1.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 (340) hide show
  1. package/CHANGELOG.md +87 -1
  2. package/README.md +1 -1
  3. package/dist/cli.js +5495 -5021
  4. package/dist/types/async/job-manager.d.ts +37 -1
  5. package/dist/types/cli/args.d.ts +2 -0
  6. package/dist/types/cli/bench-cli.d.ts +41 -0
  7. package/dist/types/commands/bench.d.ts +15 -3
  8. package/dist/types/commands/launch.d.ts +4 -0
  9. package/dist/types/config/__tests__/model-registry.test.d.ts +1 -0
  10. package/dist/types/config/model-registry.d.ts +30 -1
  11. package/dist/types/config/models-config-schema.d.ts +18 -6
  12. package/dist/types/config/models-config.d.ts +14 -5
  13. package/dist/types/config/provider-globals.d.ts +2 -2
  14. package/dist/types/config/service-tier.d.ts +5 -1
  15. package/dist/types/config/settings-schema.d.ts +350 -50
  16. package/dist/types/config/settings.d.ts +7 -0
  17. package/dist/types/dap/client.d.ts +20 -0
  18. package/dist/types/exec/bash-executor.d.ts +38 -0
  19. package/dist/types/exec/direnv.d.ts +33 -0
  20. package/dist/types/export/html/args.d.ts +15 -0
  21. package/dist/types/export/html/index.d.ts +11 -26
  22. package/dist/types/export/html/web-palette.d.ts +55 -116
  23. package/dist/types/extensibility/extensions/loader.d.ts +3 -0
  24. package/dist/types/extensibility/extensions/types.d.ts +17 -2
  25. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -0
  26. package/dist/types/extensibility/legacy-pi-tui-shim.d.ts +10 -0
  27. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +14 -1
  28. package/dist/types/internal-urls/registry-helpers.d.ts +11 -0
  29. package/dist/types/live/controller.d.ts +40 -0
  30. package/dist/types/live/protocol.d.ts +85 -0
  31. package/dist/types/live/protocol.test.d.ts +1 -0
  32. package/dist/types/live/transport.d.ts +48 -0
  33. package/dist/types/live/visualizer.d.ts +33 -0
  34. package/dist/types/lsp/client.d.ts +18 -1
  35. package/dist/types/mcp/config-writer.test.d.ts +1 -0
  36. package/dist/types/mcp/manager.d.ts +6 -2
  37. package/dist/types/mcp/oauth-discovery.d.ts +2 -0
  38. package/dist/types/mcp/render.d.ts +2 -2
  39. package/dist/types/mcp/smithery-auth.d.ts +1 -1
  40. package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
  41. package/dist/types/mcp/tool-bridge.d.ts +7 -3
  42. package/dist/types/mcp/types.d.ts +6 -0
  43. package/dist/types/memory-backend/tool-names.d.ts +2 -0
  44. package/dist/types/modes/components/login-dialog.d.ts +2 -2
  45. package/dist/types/modes/components/oauth-selector.d.ts +8 -0
  46. package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
  47. package/dist/types/modes/components/settings-defs.d.ts +8 -1
  48. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +1 -0
  49. package/dist/types/modes/components/tool-execution.d.ts +9 -1
  50. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  51. package/dist/types/modes/controllers/event-controller.d.ts +6 -1
  52. package/dist/types/modes/controllers/live-command-controller.d.ts +14 -0
  53. package/dist/types/modes/controllers/mcp-command-controller.d.ts +3 -0
  54. package/dist/types/modes/interactive-mode.d.ts +5 -0
  55. package/dist/types/modes/rpc/rpc-client.d.ts +10 -1
  56. package/dist/types/modes/rpc/rpc-frame.d.ts +16 -0
  57. package/dist/types/modes/rpc/rpc-messages.d.ts +26 -0
  58. package/dist/types/modes/rpc/rpc-types.d.ts +40 -0
  59. package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
  60. package/dist/types/modes/setup-wizard/scenes/types.d.ts +9 -1
  61. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
  62. package/dist/types/modes/theme/theme.d.ts +1 -1
  63. package/dist/types/modes/types.d.ts +3 -1
  64. package/dist/types/modes/utils/context-usage.d.ts +12 -2
  65. package/dist/types/plan-mode/plan-files.d.ts +10 -0
  66. package/dist/types/registry/agent-lifecycle.d.ts +21 -9
  67. package/dist/types/registry/agent-registry.d.ts +11 -4
  68. package/dist/types/sdk.d.ts +15 -1
  69. package/dist/types/secrets/index.d.ts +15 -2
  70. package/dist/types/secrets/obfuscator.d.ts +113 -15
  71. package/dist/types/session/acp-permission-gate.d.ts +19 -0
  72. package/dist/types/session/agent-session-events.d.ts +84 -0
  73. package/dist/types/session/agent-session-types.d.ts +314 -0
  74. package/dist/types/session/agent-session.d.ts +120 -750
  75. package/dist/types/session/async-job-delivery.d.ts +28 -0
  76. package/dist/types/session/bash-runner.d.ts +75 -0
  77. package/dist/types/session/blob-store.d.ts +11 -1
  78. package/dist/types/session/blob-store.test.d.ts +1 -0
  79. package/dist/types/session/checkpoint-entries.d.ts +28 -0
  80. package/dist/types/session/eval-runner.d.ts +49 -0
  81. package/dist/types/session/irc-bridge.d.ts +46 -0
  82. package/dist/types/session/messages.d.ts +22 -0
  83. package/dist/types/session/model-controls.d.ts +180 -0
  84. package/dist/types/session/prewalk.d.ts +46 -0
  85. package/dist/types/session/queued-messages.d.ts +22 -0
  86. package/dist/types/session/redis-session-storage.d.ts +1 -0
  87. package/dist/types/session/retry-fallback-chains.d.ts +74 -0
  88. package/dist/types/session/role-models.d.ts +15 -0
  89. package/dist/types/session/session-advisors.d.ts +228 -0
  90. package/dist/types/session/session-entries.d.ts +8 -0
  91. package/dist/types/session/session-handoff.d.ts +70 -0
  92. package/dist/types/session/session-history-format.d.ts +6 -1
  93. package/dist/types/session/session-loader.d.ts +6 -4
  94. package/dist/types/session/session-maintenance.d.ts +246 -0
  95. package/dist/types/session/session-manager.d.ts +41 -0
  96. package/dist/types/session/session-memory.d.ts +56 -0
  97. package/dist/types/session/session-provider-boundary.d.ts +62 -0
  98. package/dist/types/session/session-stats.d.ts +48 -0
  99. package/dist/types/session/session-tools.d.ts +143 -0
  100. package/dist/types/session/session-workspace.d.ts +32 -0
  101. package/dist/types/session/stream-guards.d.ts +44 -0
  102. package/dist/types/session/todo-tracker.d.ts +56 -0
  103. package/dist/types/session/ttsr-coordinator.d.ts +51 -0
  104. package/dist/types/session/turn-recovery.d.ts +225 -0
  105. package/dist/types/system-prompt.d.ts +2 -0
  106. package/dist/types/task/executor.d.ts +4 -1
  107. package/dist/types/task/output-manager.d.ts +3 -1
  108. package/dist/types/task/types.d.ts +14 -0
  109. package/dist/types/tools/approval.d.ts +1 -0
  110. package/dist/types/tools/browser/launch.d.ts +5 -0
  111. package/dist/types/tools/hub/jobs.d.ts +1 -1
  112. package/dist/types/tools/image-gen.d.ts +4 -4
  113. package/dist/types/tools/image-providers.d.ts +38 -0
  114. package/dist/types/tools/index.d.ts +8 -0
  115. package/dist/types/tools/path-utils.d.ts +8 -0
  116. package/dist/types/tools/report-tool-issue.d.ts +24 -9
  117. package/dist/types/tools/todo.d.ts +10 -4
  118. package/dist/types/tools/xdev.d.ts +5 -1
  119. package/dist/types/tts/streaming-player.d.ts +14 -0
  120. package/dist/types/tts/vocalizer.d.ts +5 -0
  121. package/dist/types/utils/jj.d.ts +29 -0
  122. package/dist/types/utils/lang-from-path.d.ts +1 -0
  123. package/dist/types/utils/title-generator.d.ts +30 -0
  124. package/dist/types/vibe/runtime.d.ts +36 -8
  125. package/dist/types/web/search/index.d.ts +1 -1
  126. package/dist/types/web/search/provider.d.ts +15 -5
  127. package/dist/types/web/search/providers/firecrawl.d.ts +9 -0
  128. package/dist/types/web/search/types.d.ts +95 -1
  129. package/package.json +13 -12
  130. package/scripts/generate-share-viewer.ts +4 -6
  131. package/scripts/legacy-pi-virtual-module.ts +1 -1
  132. package/src/advisor/__tests__/advisor.test.ts +643 -0
  133. package/src/advisor/runtime.ts +144 -43
  134. package/src/async/job-manager.ts +90 -2
  135. package/src/cli/args.ts +2 -0
  136. package/src/cli/auth-gateway-cli.ts +18 -3
  137. package/src/cli/bench-cli.ts +395 -29
  138. package/src/cli/flag-tables.ts +3 -0
  139. package/src/cli/gc-cli.ts +2 -2
  140. package/src/cli/grievances-cli.ts +2 -2
  141. package/src/cli/usage-cli.ts +11 -2
  142. package/src/collab/host.ts +3 -2
  143. package/src/commands/bench.ts +18 -4
  144. package/src/commands/launch.ts +4 -0
  145. package/src/config/__tests__/model-registry.test.ts +147 -0
  146. package/src/config/model-registry.ts +50 -2
  147. package/src/config/model-resolver.ts +0 -1
  148. package/src/config/models-config-schema.ts +15 -4
  149. package/src/config/provider-globals.ts +9 -9
  150. package/src/config/service-tier.ts +26 -1
  151. package/src/config/settings-schema.ts +250 -54
  152. package/src/config/settings.ts +81 -2
  153. package/src/dap/client.ts +37 -4
  154. package/src/edit/index.ts +49 -12
  155. package/src/eval/py/executor.ts +103 -33
  156. package/src/exec/bash-executor.ts +89 -6
  157. package/src/exec/direnv.ts +145 -0
  158. package/src/export/html/args.ts +20 -0
  159. package/src/export/html/index.ts +45 -47
  160. package/src/export/html/template.css +19 -1
  161. package/src/export/html/template.html +6 -0
  162. package/src/export/html/template.js +21 -0
  163. package/src/export/html/tool-views.generated.js +31 -31
  164. package/src/export/html/web-palette.ts +116 -132
  165. package/src/export/share.ts +249 -49
  166. package/src/extensibility/extensions/loader.ts +29 -1
  167. package/src/extensibility/extensions/runner.ts +6 -0
  168. package/src/extensibility/extensions/types.ts +33 -2
  169. package/src/extensibility/legacy-pi-coding-agent-shim.ts +1 -0
  170. package/src/extensibility/legacy-pi-tui-shim.ts +10 -0
  171. package/src/extensibility/plugins/legacy-pi-compat.ts +870 -311
  172. package/src/hindsight/state.ts +64 -6
  173. package/src/internal-urls/local-protocol.ts +2 -1
  174. package/src/internal-urls/memory-protocol.ts +2 -2
  175. package/src/internal-urls/registry-helpers.ts +40 -0
  176. package/src/internal-urls/skill-protocol.ts +2 -2
  177. package/src/internal-urls/ssh-protocol.ts +2 -1
  178. package/src/internal-urls/vault-protocol.ts +2 -1
  179. package/src/live/audio-worklet.txt +59 -0
  180. package/src/live/browser-runtime.txt +221 -0
  181. package/src/live/controller.ts +464 -0
  182. package/src/live/prompts/agent-final-message.md +3 -0
  183. package/src/live/prompts/live-instructions.md +23 -0
  184. package/src/live/protocol.test.ts +140 -0
  185. package/src/live/protocol.ts +233 -0
  186. package/src/live/transport.ts +490 -0
  187. package/src/live/visualizer.ts +234 -0
  188. package/src/lsp/client.ts +52 -9
  189. package/src/lsp/clients/biome-client.ts +3 -4
  190. package/src/lsp/index.ts +44 -11
  191. package/src/main.ts +9 -1
  192. package/src/mcp/config-writer.test.ts +43 -0
  193. package/src/mcp/config-writer.ts +109 -82
  194. package/src/mcp/manager.ts +39 -8
  195. package/src/mcp/oauth-discovery.ts +10 -2
  196. package/src/mcp/render.ts +94 -35
  197. package/src/mcp/smithery-auth.test.ts +29 -0
  198. package/src/mcp/smithery-auth.ts +3 -2
  199. package/src/mcp/tool-bridge.ts +107 -11
  200. package/src/mcp/types.ts +7 -0
  201. package/src/memories/index.ts +40 -20
  202. package/src/memory-backend/tool-names.ts +2 -0
  203. package/src/mnemopi/backend.ts +24 -7
  204. package/src/modes/acp/acp-agent.ts +56 -10
  205. package/src/modes/acp/acp-event-mapper.ts +8 -1
  206. package/src/modes/components/agent-hub.ts +27 -10
  207. package/src/modes/components/login-dialog.ts +14 -4
  208. package/src/modes/components/oauth-selector.ts +24 -7
  209. package/src/modes/components/plan-review-overlay.ts +350 -35
  210. package/src/modes/components/settings-defs.ts +28 -3
  211. package/src/modes/components/settings-selector.ts +178 -6
  212. package/src/modes/components/status-line/component.jj-cache.test.ts +229 -0
  213. package/src/modes/components/status-line/component.ts +123 -2
  214. package/src/modes/components/tool-execution.test.ts +63 -2
  215. package/src/modes/components/tool-execution.ts +11 -2
  216. package/src/modes/controllers/command-controller.ts +70 -22
  217. package/src/modes/controllers/event-controller.ts +111 -16
  218. package/src/modes/controllers/extension-ui-controller.test.ts +1 -0
  219. package/src/modes/controllers/extension-ui-controller.ts +20 -4
  220. package/src/modes/controllers/live-command-controller.ts +178 -0
  221. package/src/modes/controllers/mcp-command-controller.ts +80 -41
  222. package/src/modes/controllers/omfg-controller.ts +44 -40
  223. package/src/modes/controllers/selector-controller.ts +18 -10
  224. package/src/modes/controllers/session-focus-controller.ts +6 -1
  225. package/src/modes/interactive-mode.ts +160 -38
  226. package/src/modes/rpc/rpc-client.ts +94 -3
  227. package/src/modes/rpc/rpc-frame.ts +164 -4
  228. package/src/modes/rpc/rpc-messages.ts +127 -0
  229. package/src/modes/rpc/rpc-mode.ts +79 -7
  230. package/src/modes/rpc/rpc-types.ts +34 -2
  231. package/src/modes/runtime-init.ts +2 -0
  232. package/src/modes/setup-wizard/scenes/model.ts +5 -7
  233. package/src/modes/setup-wizard/scenes/providers.ts +3 -2
  234. package/src/modes/setup-wizard/scenes/sign-in.ts +8 -2
  235. package/src/modes/setup-wizard/scenes/theme.ts +13 -3
  236. package/src/modes/setup-wizard/scenes/types.ts +9 -1
  237. package/src/modes/setup-wizard/scenes/web-search.ts +23 -10
  238. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  239. package/src/modes/theme/theme.ts +1 -1
  240. package/src/modes/types.ts +3 -1
  241. package/src/modes/utils/context-usage.ts +23 -7
  242. package/src/modes/utils/ui-helpers.ts +10 -5
  243. package/src/plan-mode/plan-files.ts +40 -0
  244. package/src/prompts/bench/cache-prefix-chunk.md +1 -0
  245. package/src/prompts/bench/cache-prefix.md +3 -0
  246. package/src/prompts/bench/cache-suffix.md +1 -0
  247. package/src/prompts/goals/guided-goal-system.md +24 -3
  248. package/src/prompts/system/custom-system-prompt.md +1 -1
  249. package/src/prompts/system/project-prompt.md +8 -1
  250. package/src/prompts/system/subagent-async-pending.md +6 -0
  251. package/src/prompts/system/system-prompt.md +1 -1
  252. package/src/prompts/system/workflow-notice.md +48 -40
  253. package/src/prompts/system/xdev-mount-notice.md +4 -0
  254. package/src/prompts/tools/task.md +12 -2
  255. package/src/prompts/tools/todo.md +3 -1
  256. package/src/registry/agent-lifecycle.ts +116 -46
  257. package/src/registry/agent-registry.ts +35 -8
  258. package/src/registry/persisted-agents.ts +26 -2
  259. package/src/sdk.ts +255 -139
  260. package/src/secrets/index.ts +127 -4
  261. package/src/secrets/obfuscator.ts +2328 -105
  262. package/src/session/acp-permission-gate.ts +165 -0
  263. package/src/session/agent-session-error-log.test.ts +1 -1
  264. package/src/session/agent-session-events.ts +66 -0
  265. package/src/session/agent-session-types.ts +334 -0
  266. package/src/session/agent-session.ts +1910 -12213
  267. package/src/session/artifacts.ts +6 -5
  268. package/src/session/async-job-delivery.ts +74 -0
  269. package/src/session/bash-runner.ts +326 -0
  270. package/src/session/blob-store.test.ts +56 -0
  271. package/src/session/blob-store.ts +18 -2
  272. package/src/session/checkpoint-entries.ts +81 -0
  273. package/src/session/eval-runner.ts +212 -0
  274. package/src/session/indexed-session-storage.ts +9 -2
  275. package/src/session/irc-bridge.ts +203 -0
  276. package/src/session/messages.ts +263 -1
  277. package/src/session/model-controls.ts +714 -0
  278. package/src/session/prewalk.ts +252 -0
  279. package/src/session/queued-messages.ts +93 -0
  280. package/src/session/redis-session-storage.ts +51 -11
  281. package/src/session/retry-fallback-chains.ts +455 -0
  282. package/src/session/role-models.ts +85 -0
  283. package/src/session/session-advisors.ts +1679 -0
  284. package/src/session/session-context.test.ts +224 -1
  285. package/src/session/session-context.ts +47 -5
  286. package/src/session/session-entries.ts +8 -0
  287. package/src/session/session-handoff.ts +308 -0
  288. package/src/session/session-history-format.ts +20 -9
  289. package/src/session/session-loader.ts +10 -51
  290. package/src/session/session-maintenance.ts +2983 -0
  291. package/src/session/session-manager.ts +390 -30
  292. package/src/session/session-memory.ts +222 -0
  293. package/src/session/session-provider-boundary.ts +307 -0
  294. package/src/session/session-stats.ts +293 -0
  295. package/src/session/session-tools.ts +943 -0
  296. package/src/session/session-workspace.ts +53 -0
  297. package/src/session/stream-guards.ts +417 -0
  298. package/src/session/todo-tracker.ts +380 -0
  299. package/src/session/ttsr-coordinator.ts +496 -0
  300. package/src/session/turn-recovery.ts +1629 -0
  301. package/src/slash-commands/builtin-registry.ts +92 -11
  302. package/src/slash-commands/helpers/usage-report.ts +25 -4
  303. package/src/system-prompt.ts +41 -17
  304. package/src/task/executor.ts +288 -70
  305. package/src/task/index.ts +148 -111
  306. package/src/task/output-manager.ts +25 -3
  307. package/src/task/persisted-revive.ts +4 -3
  308. package/src/task/structured-subagent.ts +4 -1
  309. package/src/task/types.ts +16 -0
  310. package/src/tools/approval.ts +55 -10
  311. package/src/tools/bash-interactive.ts +26 -0
  312. package/src/tools/bash-skill-urls.ts +28 -2
  313. package/src/tools/bash.ts +339 -123
  314. package/src/tools/browser/launch.ts +9 -1
  315. package/src/tools/browser/tab-supervisor.ts +8 -5
  316. package/src/tools/browser.ts +94 -47
  317. package/src/tools/hub/index.ts +1 -1
  318. package/src/tools/hub/jobs.ts +67 -8
  319. package/src/tools/image-gen.ts +20 -28
  320. package/src/tools/image-providers.ts +50 -0
  321. package/src/tools/index.ts +12 -0
  322. package/src/tools/path-utils.ts +49 -2
  323. package/src/tools/read.ts +156 -88
  324. package/src/tools/renderers.ts +7 -1
  325. package/src/tools/report-tool-issue.ts +79 -28
  326. package/src/tools/todo.ts +101 -11
  327. package/src/tools/vibe.ts +1 -2
  328. package/src/tools/write.ts +51 -1
  329. package/src/tools/xdev.ts +67 -3
  330. package/src/tts/streaming-player.ts +62 -4
  331. package/src/tts/vocalizer.ts +18 -1
  332. package/src/utils/jj.ts +125 -4
  333. package/src/utils/lang-from-path.ts +7 -0
  334. package/src/utils/title-generator.ts +137 -1
  335. package/src/vibe/runtime.ts +852 -72
  336. package/src/web/search/index.ts +3 -6
  337. package/src/web/search/provider.ts +31 -16
  338. package/src/web/search/providers/firecrawl.ts +46 -13
  339. package/src/web/search/providers/xai.ts +7 -1
  340. package/src/web/search/types.ts +8 -1
@@ -14,13 +14,23 @@
14
14
  * - ensureLive during an in-flight park either cancels the park (session still
15
15
  * live) or waits for detach+park and then revives.
16
16
  * - Concurrent ensureLive/park operations coalesce per id.
17
+ *
18
+ * Every adoption, park, and revival is bound to the exact {@link AgentRef} it
19
+ * started from, so stale async work (a late finalizer, a cancelled initializer,
20
+ * a superseded revive) can never clobber a newer same-id ref.
17
21
  */
18
22
 
19
23
  import { logger } from "@oh-my-pi/pi-utils";
20
24
  import type { AgentSession } from "../session/agent-session";
21
- import { type AgentRef, AgentRegistry, MAIN_AGENT_ID, type RegistryEvent } from "./agent-registry";
25
+ import {
26
+ type AgentRef,
27
+ type AgentRefExpectation,
28
+ AgentRegistry,
29
+ MAIN_AGENT_ID,
30
+ type RegistryEvent,
31
+ } from "./agent-registry";
22
32
 
23
- export type AgentReviver = () => Promise<AgentSession>;
33
+ export type AgentReviver = (expected: AgentRef) => Promise<AgentSession>;
24
34
 
25
35
  /**
26
36
  * Builds a reviver for a `parked` ref restored from disk (Agent Hub scan,
@@ -39,12 +49,15 @@ export interface AdoptOptions {
39
49
  }
40
50
 
41
51
  interface AdoptedAgent {
52
+ ref: AgentRef;
42
53
  idleTtlMs: number;
43
54
  revive?: AgentReviver;
44
55
  timer?: NodeJS.Timeout;
45
56
  }
46
57
 
47
58
  interface ParkInFlight {
59
+ /** The exact ref this park was started for. */
60
+ ref: AgentRef;
48
61
  /** Resolves when the park attempt finishes (success, cancel, or dispose error). */
49
62
  promise: Promise<void>;
50
63
  /** Cancel before the session is detached. Returns true if cancel took effect. */
@@ -55,6 +68,11 @@ interface ParkInFlight {
55
68
  detached: boolean;
56
69
  }
57
70
 
71
+ interface RevivingAgent {
72
+ ref: AgentRef;
73
+ promise: Promise<AgentSession>;
74
+ }
75
+
58
76
  export class AgentLifecycleManager {
59
77
  static #global: AgentLifecycleManager | undefined;
60
78
 
@@ -85,12 +103,13 @@ export class AgentLifecycleManager {
85
103
  readonly #registry: AgentRegistry;
86
104
  readonly #adopted = new Map<string, AdoptedAgent>();
87
105
  /**
88
- * In-flight park attempts. A park is cancelable until the live session is
89
- * detached; after detach, ensureLive waits for the park and revives.
106
+ * In-flight park attempts, each bound to the ref it started from. A park is
107
+ * cancelable until the live session is detached; after detach, ensureLive
108
+ * waits for the park and revives.
90
109
  */
91
110
  readonly #parks = new Map<string, ParkInFlight>();
92
- /** In-flight revives, so concurrent {@link ensureLive} calls coalesce. */
93
- readonly #revivals = new Map<string, Promise<AgentSession>>();
111
+ /** In-flight revives, bound to the parked ref that initiated them, so concurrent {@link ensureLive} calls coalesce. */
112
+ readonly #revivals = new Map<string, RevivingAgent>();
94
113
  #unsubscribe: (() => void) | undefined;
95
114
  #persistedReviverFactory: PersistedSubagentReviverFactory | undefined;
96
115
  /** TTL applied when a cold-revived ref is adopted on demand. */
@@ -115,23 +134,29 @@ export class AgentLifecycleManager {
115
134
  /**
116
135
  * Take ownership of a finished subagent. Caller has already set registry
117
136
  * status to "idle". Arms the TTL timer (idleTtlMs <= 0 adopts without one).
137
+ * When `expected` is given, the adoption is refused if the id no longer
138
+ * resolves to that ref (or that ref's session).
118
139
  */
119
- adopt(id: string, opts: AdoptOptions): void {
140
+ adopt(id: string, opts: AdoptOptions, expected?: AgentRefExpectation): void {
120
141
  if (id === MAIN_AGENT_ID) return;
121
- if (!this.#registry.get(id)) {
122
- logger.warn("AgentLifecycleManager.adopt: unknown agent id", { id });
142
+ const ref = this.#registry.get(id);
143
+ if (!ref || (expected !== undefined && ref !== expected && ref.session !== expected)) {
144
+ logger.warn("AgentLifecycleManager.adopt: unknown or replaced agent id", { id });
123
145
  return;
124
146
  }
125
147
  const existing = this.#adopted.get(id);
126
148
  clearTimeout(existing?.timer);
127
- const adopted: AdoptedAgent = { idleTtlMs: opts.idleTtlMs, revive: opts.revive };
149
+ const adopted: AdoptedAgent = { ref, idleTtlMs: opts.idleTtlMs, revive: opts.revive };
128
150
  this.#adopted.set(id, adopted);
129
151
  this.#armTimer(id, adopted);
130
152
  }
131
153
 
132
- /** True if the id is adopted (parked or live). */
133
- has(id: string): boolean {
134
- return this.#adopted.has(id);
154
+ /** True if the id is adopted (parked or live) — and, when `expected` is given, still bound to that ref. */
155
+ has(id: string, expected?: AgentRefExpectation): boolean {
156
+ const adopted = this.#adopted.get(id);
157
+ return Boolean(
158
+ adopted && (expected === undefined || adopted.ref === expected || adopted.ref.session === expected),
159
+ );
135
160
  }
136
161
 
137
162
  /**
@@ -147,11 +172,14 @@ export class AgentLifecycleManager {
147
172
  /**
148
173
  * True while {@link park} is disposing this agent's session (lets dispose
149
174
  * hooks distinguish park from teardown). False once the park is cancelled
150
- * by ensureLive or after detach+dispose completes.
175
+ * by ensureLive or after detach+dispose completes. When `expected` is
176
+ * given, only a park bound to that ref (or its session) counts.
151
177
  */
152
- isParking(id: string): boolean {
178
+ isParking(id: string, expected?: AgentRefExpectation): boolean {
153
179
  const park = this.#parks.get(id);
154
- return Boolean(park && !park.cancelled);
180
+ return Boolean(
181
+ park && !park.cancelled && (expected === undefined || park.ref === expected || park.ref.session === expected),
182
+ );
155
183
  }
156
184
 
157
185
  /**
@@ -170,7 +198,8 @@ export class AgentLifecycleManager {
170
198
  const adopted = this.#adopted.get(id);
171
199
  if (!adopted) return;
172
200
  const ref = this.#registry.get(id);
173
- const session = ref?.session;
201
+ if (!ref || adopted.ref !== ref) return;
202
+ const session = ref.session;
174
203
  if (!session) return;
175
204
 
176
205
  if (adopted.timer) {
@@ -180,6 +209,7 @@ export class AgentLifecycleManager {
180
209
 
181
210
  let cancelled = false;
182
211
  const park: ParkInFlight = {
212
+ ref,
183
213
  promise: undefined as unknown as Promise<void>,
184
214
  cancel: () => {
185
215
  // Cancel only before detach — once detached the old session is already
@@ -200,16 +230,16 @@ export class AgentLifecycleManager {
200
230
  await Promise.resolve();
201
231
  if (cancelled) return;
202
232
 
203
- // Re-check liveness: release/unregister may have raced us.
233
+ // Re-check liveness: release/unregister/replace may have raced us.
204
234
  const live = this.#registry.get(id);
205
- if (!live?.session || live.session !== session) return;
206
- if (!this.#adopted.has(id)) return;
235
+ if (live !== ref || !live.session || live.session !== session) return;
236
+ if (this.#adopted.get(id)?.ref !== ref) return;
207
237
 
208
238
  // Commit: detach + parked *before* dispose so callers never see a
209
239
  // dying session via ref.session / idle status.
210
240
  park.detached = true;
211
- this.#registry.detachSession(id);
212
- this.#registry.setStatus(id, "parked");
241
+ this.#registry.detachSession(id, ref);
242
+ this.#registry.setStatus(id, "parked", ref);
213
243
 
214
244
  try {
215
245
  await session.dispose();
@@ -238,16 +268,16 @@ export class AgentLifecycleManager {
238
268
  async ensureLive(id: string): Promise<AgentSession> {
239
269
  const park = this.#parks.get(id);
240
270
  if (park) {
241
- const ref = this.#registry.get(id);
271
+ const parked = this.#registry.get(id);
242
272
  // Cancel if the live session is still attached — keep it instead of
243
273
  // thrashing dispose + revive.
244
- if (ref?.session && !park.detached && park.cancel()) {
274
+ if (parked?.session && !park.detached && park.cancel()) {
245
275
  await park.promise;
246
276
  const kept = this.#registry.get(id)?.session;
247
277
  if (kept) {
248
278
  // Park cleared the idle timer; re-arm so TTL park still works.
249
279
  const adopted = this.#adopted.get(id);
250
- if (adopted && ref.status === "idle") this.#armTimer(id, adopted);
280
+ if (adopted && adopted.ref === parked && parked.status === "idle") this.#armTimer(id, adopted);
251
281
  return kept;
252
282
  }
253
283
  } else {
@@ -265,13 +295,14 @@ export class AgentLifecycleManager {
265
295
  }
266
296
  if (ref.session) return ref.session;
267
297
  const inflight = this.#revivals.get(id);
268
- if (inflight) return inflight;
298
+ if (inflight?.ref === ref) return inflight.promise;
269
299
  const revival = this.#resolveAndRevive(id, ref);
270
- this.#revivals.set(id, revival);
300
+ const pending: RevivingAgent = { ref, promise: revival };
301
+ this.#revivals.set(id, pending);
271
302
  try {
272
303
  return await revival;
273
304
  } finally {
274
- this.#revivals.delete(id);
305
+ if (this.#revivals.get(id) === pending) this.#revivals.delete(id);
275
306
  }
276
307
  }
277
308
 
@@ -283,53 +314,72 @@ export class AgentLifecycleManager {
283
314
  * when the agent is not revivable or no reviver can be produced.
284
315
  */
285
316
  async #resolveAndRevive(id: string, ref: AgentRef): Promise<AgentSession> {
286
- let revive = this.#adopted.get(id)?.revive;
317
+ let adoption = this.#adopted.get(id);
318
+ let revive = adoption?.ref === ref ? adoption.revive : undefined;
287
319
  let coldAdopted = false;
288
320
  if (!revive && ref.status === "parked" && ref.sessionFile && this.#persistedReviverFactory) {
289
321
  revive = await this.#persistedReviverFactory(ref);
290
322
  if (revive) {
291
- this.#adopted.set(id, { idleTtlMs: this.#persistedReviveTtlMs, revive });
323
+ adoption = { ref, idleTtlMs: this.#persistedReviveTtlMs, revive };
324
+ this.#adopted.set(id, adoption);
292
325
  coldAdopted = true;
293
326
  }
294
327
  }
295
- if (ref.status !== "parked" || !revive) {
328
+ if (this.#registry.get(id) !== ref) {
329
+ throw new Error(`Agent "${id}" changed while its persisted session was being prepared.`);
330
+ }
331
+ if (ref.status !== "parked" || !revive || !adoption) {
296
332
  throw new Error(
297
333
  `Agent "${id}" is ${ref.status} and cannot be revived${revive ? "" : " (no reviver registered)"}. Its transcript remains readable at history://${id}.`,
298
334
  );
299
335
  }
300
336
  try {
301
- return await this.#revive(id, revive, ref.sessionFile);
337
+ return await this.#revive(id, revive, ref, adoption);
302
338
  } catch (error) {
303
339
  // A failed cold revive (stale ctx, missing cwd, bad MCP) must not leave a
304
340
  // poisoned reviver stuck in #adopted — drop it so a later ensureLive
305
341
  // rebuilds via the factory (which may have fresher context by then).
306
- if (coldAdopted) this.#adopted.delete(id);
342
+ if (coldAdopted && this.#adopted.get(id) === adoption) this.#adopted.delete(id);
307
343
  throw error;
308
344
  }
309
345
  }
310
346
 
311
- /** Hard removal: dispose if live, unregister from registry, drop timers. */
312
- async release(id: string): Promise<void> {
347
+ /**
348
+ * Hard removal: dispose if live, unregister from registry, drop timers.
349
+ * When `expected` is given, only a ref matching it is released; a stale
350
+ * release can never take down a newer same-id ref. Returns true when a
351
+ * matching ref was released.
352
+ */
353
+ async release(id: string, expected?: AgentRefExpectation): Promise<boolean> {
313
354
  const adopted = this.#adopted.get(id);
314
- clearTimeout(adopted?.timer);
315
- this.#adopted.delete(id);
355
+ const current = this.#registry.get(id);
356
+ const currentMatches =
357
+ current && (expected === undefined || current === expected || current.session === expected);
358
+ const adoptedMatches =
359
+ adopted && (expected === undefined || adopted.ref === expected || adopted.ref.session === expected);
360
+ const ref = currentMatches ? current : adoptedMatches ? adopted.ref : undefined;
361
+ if (!ref) return false;
362
+ if (adopted?.ref === ref) {
363
+ clearTimeout(adopted.timer);
364
+ this.#adopted.delete(id);
365
+ }
316
366
 
317
367
  const park = this.#parks.get(id);
318
- if (park) {
368
+ if (park && park.ref === ref) {
319
369
  // Prefer cancel when the session is still live so release owns dispose.
320
370
  if (!park.detached) park.cancel();
321
371
  await park.promise;
322
372
  }
323
373
 
324
- const ref = this.#registry.get(id);
325
- if (ref?.session) {
374
+ if (this.#registry.get(id) === ref && ref.session) {
326
375
  try {
327
376
  await ref.session.dispose();
328
377
  } catch (error) {
329
378
  logger.warn("AgentLifecycleManager.release: session dispose failed", { id, error: String(error) });
330
379
  }
331
380
  }
332
- this.#registry.unregister(id);
381
+ this.#registry.unregister(id, ref);
382
+ return true;
333
383
  }
334
384
 
335
385
  /** Teardown everything (process exit / main session dispose). */
@@ -343,11 +393,31 @@ export class AgentLifecycleManager {
343
393
  this.#persistedReviverFactory = undefined;
344
394
  }
345
395
 
346
- async #revive(id: string, revive: AgentReviver, sessionFile: string | null): Promise<AgentSession> {
347
- const session = await revive();
348
- this.#registry.attachSession(id, session, sessionFile);
396
+ async #revive(id: string, revive: AgentReviver, ref: AgentRef, adopted: AdoptedAgent): Promise<AgentSession> {
397
+ const session = await revive(ref);
398
+ let liveRef = this.#registry.get(id);
399
+ if (liveRef === ref) {
400
+ if (!this.#registry.attachSession(id, session, ref.sessionFile, ref)) {
401
+ await session.dispose();
402
+ throw new Error(`Agent "${id}" changed before its persisted session could attach.`);
403
+ }
404
+ liveRef = ref;
405
+ } else if (
406
+ !liveRef ||
407
+ liveRef.session !== session ||
408
+ liveRef.kind !== ref.kind ||
409
+ liveRef.parentId !== ref.parentId ||
410
+ liveRef.sessionFile !== ref.sessionFile
411
+ ) {
412
+ await session.dispose();
413
+ throw new Error(`Agent "${id}" was replaced while its persisted session was reviving.`);
414
+ }
415
+ adopted.ref = liveRef;
349
416
  // Emits status_changed → "idle", which re-arms the TTL timer below.
350
- this.#registry.setStatus(id, "idle");
417
+ if (!this.#registry.setStatus(id, "idle", liveRef)) {
418
+ await session.dispose();
419
+ throw new Error(`Agent "${id}" changed before its persisted session became idle.`);
420
+ }
351
421
  return session;
352
422
  }
353
423
 
@@ -364,7 +434,7 @@ export class AgentLifecycleManager {
364
434
 
365
435
  #onRegistryEvent(event: RegistryEvent): void {
366
436
  const adopted = this.#adopted.get(event.ref.id);
367
- if (!adopted) return;
437
+ if (!adopted || adopted.ref !== event.ref) return;
368
438
  if (event.type === "removed") {
369
439
  clearTimeout(adopted.timer);
370
440
  this.#adopted.delete(event.ref.id);
@@ -45,6 +45,8 @@ export interface AgentRef {
45
45
  activity?: string;
46
46
  }
47
47
 
48
+ export type AgentRefExpectation = AgentRef | AgentSession;
49
+
48
50
  export type RegistryEvent =
49
51
  | { type: "registered"; ref: AgentRef }
50
52
  | { type: "status_changed"; ref: AgentRef }
@@ -80,6 +82,10 @@ export class AgentRegistry {
80
82
  readonly #refs = new Map<string, AgentRef>();
81
83
  readonly #listeners = new Set<RegistryListener>();
82
84
 
85
+ #matchesExpected(ref: AgentRef, expected?: AgentRefExpectation): boolean {
86
+ return expected === undefined || ref === expected || ref.session === expected;
87
+ }
88
+
83
89
  register(input: RegisterInput): AgentRef {
84
90
  const now = Date.now();
85
91
  const ref: AgentRef = {
@@ -98,15 +104,28 @@ export class AgentRegistry {
98
104
  return ref;
99
105
  }
100
106
 
101
- setStatus(id: string, status: AgentStatus): void {
107
+ /**
108
+ * Register a new id only when it is absent, or reuse the exact ref a parked
109
+ * revival was authorized to revive. A missing expected ref is a failed CAS:
110
+ * callers must never claim an id after its prior generation disappeared.
111
+ */
112
+ registerIfAvailable(input: RegisterInput, expected: AgentRef | null): AgentRef | undefined {
113
+ const current = this.#refs.get(input.id);
114
+ if (expected === null) return current ? undefined : this.register(input);
115
+ return current === expected ? current : undefined;
116
+ }
117
+
118
+ setStatus(id: string, status: AgentStatus, expected?: AgentRefExpectation): boolean {
102
119
  const ref = this.#refs.get(id);
103
- if (!ref || ref.status === status) return;
120
+ if (!ref || !this.#matchesExpected(ref, expected)) return false;
121
+ if (ref.status === status) return true;
104
122
  ref.status = status;
105
123
  // Activity describes current work; it is meaningless once the agent
106
124
  // leaves `running`, so drop it to avoid showing stale work in rosters.
107
125
  if (status !== "running") ref.activity = undefined;
108
126
  ref.lastActivity = Date.now();
109
127
  this.#emit({ type: "status_changed", ref });
128
+ return true;
110
129
  }
111
130
 
112
131
  /**
@@ -133,25 +152,33 @@ export class AgentRegistry {
133
152
  ref.activity = gist;
134
153
  }
135
154
 
136
- attachSession(id: string, session: AgentSession, sessionFile?: string | null): void {
155
+ attachSession(
156
+ id: string,
157
+ session: AgentSession,
158
+ sessionFile?: string | null,
159
+ expected?: AgentRefExpectation,
160
+ ): boolean {
137
161
  const ref = this.#refs.get(id);
138
- if (!ref) return;
162
+ if (!ref || !this.#matchesExpected(ref, expected)) return false;
139
163
  ref.session = session;
140
164
  if (sessionFile !== undefined) ref.sessionFile = sessionFile;
141
165
  ref.lastActivity = Date.now();
166
+ return true;
142
167
  }
143
168
 
144
- detachSession(id: string): void {
169
+ detachSession(id: string, expected?: AgentRefExpectation): boolean {
145
170
  const ref = this.#refs.get(id);
146
- if (!ref) return;
171
+ if (!ref || !this.#matchesExpected(ref, expected)) return false;
147
172
  ref.session = null;
173
+ return true;
148
174
  }
149
175
 
150
- unregister(id: string): void {
176
+ unregister(id: string, expected?: AgentRefExpectation): boolean {
151
177
  const ref = this.#refs.get(id);
152
- if (!ref) return;
178
+ if (!ref || !this.#matchesExpected(ref, expected)) return false;
153
179
  this.#refs.delete(id);
154
180
  this.#emit({ type: "removed", ref });
181
+ return true;
155
182
  }
156
183
 
157
184
  get(id: string): AgentRef | undefined {
@@ -1,22 +1,45 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { ADVISOR_TRANSCRIPT_FILENAME, isAdvisorTranscriptName } from "../advisor/transcript-recorder";
4
+ import { SessionManager } from "../session/session-manager";
5
+ import { persistedVibeChildIds } from "../vibe/runtime";
4
6
  import { type AgentRegistry, MAIN_AGENT_ID } from "./agent-registry";
5
7
 
8
+ /**
9
+ * Child ids owned by the Vibe roster persisted in this session file. Vibe
10
+ * workers are revived through the Vibe registry's own journal, so the generic
11
+ * persisted-subagent scan must not register them as plain `sub` refs.
12
+ */
13
+ async function readPersistedVibeChildIds(sessionFile: string): Promise<Set<string>> {
14
+ let sessionManager: SessionManager;
15
+ try {
16
+ sessionManager = await SessionManager.open(sessionFile, undefined, undefined, { suppressBreadcrumb: true });
17
+ } catch {
18
+ return new Set();
19
+ }
20
+ try {
21
+ return persistedVibeChildIds(sessionManager.getEntries());
22
+ } finally {
23
+ await sessionManager.close();
24
+ }
25
+ }
26
+
6
27
  /** Register persisted subagent and advisor transcripts as parked registry refs. */
7
28
  export async function registerPersistedSubagents(
8
29
  registry: AgentRegistry,
9
30
  sessionFile: string | null | undefined,
10
31
  ): Promise<void> {
11
32
  if (!sessionFile?.endsWith(".jsonl")) return;
33
+ const vibeOwnedIds = await readPersistedVibeChildIds(sessionFile);
12
34
  const root = sessionFile.slice(0, -6);
13
- await registerPersistedSubagentsFromDir(registry, root, undefined);
35
+ await registerPersistedSubagentsFromDir(registry, root, undefined, vibeOwnedIds);
14
36
  }
15
37
 
16
38
  async function registerPersistedSubagentsFromDir(
17
39
  registry: AgentRegistry,
18
40
  dir: string,
19
41
  parentId: string | undefined,
42
+ vibeOwnedIds: ReadonlySet<string>,
20
43
  ): Promise<void> {
21
44
  let entries: fs.Dirent[];
22
45
  try {
@@ -58,6 +81,7 @@ async function registerPersistedSubagentsFromDir(
58
81
  continue;
59
82
  }
60
83
  const id = entry.name.slice(0, -6);
84
+ if (vibeOwnedIds.has(id) && registry.get(id)?.sessionFile !== sessionFile) continue;
61
85
  if (!registry.get(id)) {
62
86
  registry.register({
63
87
  id,
@@ -69,6 +93,6 @@ async function registerPersistedSubagentsFromDir(
69
93
  status: "parked",
70
94
  });
71
95
  }
72
- await registerPersistedSubagentsFromDir(registry, path.join(dir, id), id);
96
+ await registerPersistedSubagentsFromDir(registry, path.join(dir, id), id, vibeOwnedIds);
73
97
  }
74
98
  }