gentle-pi 4.0.0 → 4.2.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 (425) hide show
  1. package/README.md +157 -18
  2. package/assets/agents/gentle-ai-explore.md +11 -4
  3. package/assets/agents/gentle-ai-verify.md +23 -4
  4. package/assets/agents/gentle-ai-worker.md +6 -6
  5. package/assets/install-wizard/index.html +47 -0
  6. package/assets/install-wizard/wizard.css +1279 -0
  7. package/assets/install-wizard/wizard.js +951 -0
  8. package/assets/orchestrator-delegation.md +70 -163
  9. package/assets/orchestrator-memory.md +11 -4
  10. package/assets/orchestrator-prompts.md +48 -0
  11. package/assets/orchestrator-tracking.md +53 -0
  12. package/assets/orchestrator-verification.md +28 -0
  13. package/assets/orchestrator-writer.md +38 -0
  14. package/assets/orchestrator.md +29 -25
  15. package/assets/sounds/LICENSE.md +52 -0
  16. package/assets/sounds/attention.wav +0 -0
  17. package/assets/sounds/error.wav +0 -0
  18. package/assets/sounds/success.wav +0 -0
  19. package/bin/engram-guard +9 -0
  20. package/bin/gentle-shell-install.mjs +302 -0
  21. package/bin/gentle-shell.mjs +708 -22
  22. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
  23. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
  24. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
  25. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
  26. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
  27. package/docs/auto-permission-answers.md +25 -0
  28. package/docs/auto-permission-evidence.md +39 -0
  29. package/docs/bundled-install.md +399 -0
  30. package/docs/delegated-verification.md +3 -5
  31. package/docs/gentle-agents-activity.md +513 -0
  32. package/docs/gentle-shell-uninstall.md +136 -0
  33. package/docs/gentle-shell.md +90 -33
  34. package/docs/install-wizard.md +2040 -0
  35. package/docs/native-authority-architecture.md +1 -1
  36. package/docs/permission-modes.md +127 -0
  37. package/docs/prompt-history.md +1 -1
  38. package/docs/readme-reference.md +310 -47
  39. package/docs/render-cache-performance.md +28 -0
  40. package/docs/session-profile-format.md +39 -0
  41. package/docs/sound-notifications-proposal.md +191 -0
  42. package/docs/sound-notifications.md +114 -0
  43. package/docs/yolo-mode.md +11 -10
  44. package/extensions/ask-user-choice.ts +4 -0
  45. package/extensions/ask-user-question.ts +6 -1
  46. package/extensions/child-context.ts +11 -2
  47. package/extensions/child-safety.ts +4 -2
  48. package/extensions/codegraph-tools.ts +18 -5
  49. package/extensions/gentle-agents.ts +718 -87
  50. package/extensions/gentle-ai.ts +991 -263
  51. package/extensions/gentle-herdr-activity.ts +81 -0
  52. package/extensions/gentle-notifications.ts +104 -0
  53. package/extensions/gentle-shell.ts +271 -80
  54. package/extensions/gentle-todo.ts +123 -18
  55. package/extensions/history/index.ts +11 -6
  56. package/extensions/pi-pretty.ts +39 -3
  57. package/extensions/quiet-tools.ts +91 -21
  58. package/extensions/skill-registry.ts +175 -46
  59. package/extensions/startup-banner.ts +11 -2
  60. package/lib/agent-home.ts +10 -1
  61. package/lib/agent-model-resolution.ts +80 -0
  62. package/lib/agent-profiles.ts +13 -3
  63. package/lib/agents-completion-delivery.ts +11 -3
  64. package/lib/agents-messaging.ts +1 -1
  65. package/lib/agents-protocol.ts +40 -1
  66. package/lib/agents-runner.ts +90 -15
  67. package/lib/auto-permission-answers.ts +133 -0
  68. package/lib/auto-permission-classifier.ts +459 -0
  69. package/lib/auto-permission-evidence.ts +88 -0
  70. package/lib/auto-permission-memory.ts +69 -0
  71. package/lib/auto-permission-runtime.ts +484 -0
  72. package/lib/auto-permission-tool-context.ts +61 -0
  73. package/lib/background-jobs-tools.ts +202 -0
  74. package/lib/background-jobs.ts +253 -0
  75. package/lib/background-monitor.ts +154 -0
  76. package/lib/bounded-writer-admission.ts +83 -10
  77. package/lib/builtin-codemode-optout.ts +166 -0
  78. package/lib/card-content-policy.ts +85 -0
  79. package/lib/child-package-injection.ts +62 -0
  80. package/lib/codemode-renderer.ts +14 -4
  81. package/lib/command-palette-catalog.ts +3 -2
  82. package/lib/context-threshold.ts +26 -0
  83. package/lib/destructive-command-guard.ts +104 -28
  84. package/lib/gentle-ai-binary.ts +109 -4
  85. package/lib/gentle-shell-bundled.ts +535 -0
  86. package/lib/gentle-shell-launcher.ts +75 -24
  87. package/lib/gentle-shell-startup-flow.ts +54 -0
  88. package/lib/gentle-shell-startup-update.ts +92 -0
  89. package/lib/gentle-shell-uninstall.ts +840 -0
  90. package/lib/gentle-shell-update-panel.ts +231 -0
  91. package/lib/herdr-activity.ts +183 -0
  92. package/lib/inprocess-reviewer.ts +3 -0
  93. package/lib/jobs-sidebar-state.ts +20 -0
  94. package/lib/jobs-view.ts +161 -0
  95. package/lib/model-price-ratio.ts +83 -0
  96. package/lib/model-routing-authority.ts +6 -1
  97. package/lib/nan-provider.ts +33 -11
  98. package/lib/native-review-cli.ts +194 -73
  99. package/lib/notification-audio-native.ts +162 -0
  100. package/lib/notification-audio-windows.ts +399 -0
  101. package/lib/notification-audio.ts +332 -0
  102. package/lib/notification-customize.ts +241 -0
  103. package/lib/notification-events.ts +113 -0
  104. package/lib/notification-policy.ts +148 -0
  105. package/lib/notification-pulse-client.ts +367 -0
  106. package/lib/notification-pulse-protocol.ts +305 -0
  107. package/lib/notification-pulse-stream.ts +213 -0
  108. package/lib/notification-pulse-worker.ts +66 -0
  109. package/lib/notification-scheduler.ts +173 -0
  110. package/lib/notification-service.ts +202 -0
  111. package/lib/notification-ui.ts +87 -0
  112. package/lib/odd-phase.ts +1 -1
  113. package/lib/orchestrator-alias.ts +20 -0
  114. package/lib/orchestrator-catalog.ts +79 -0
  115. package/lib/orchestrator-consultation.ts +61 -0
  116. package/lib/orchestrator-discovery.ts +49 -0
  117. package/lib/orchestrator-helper-consent.ts +122 -0
  118. package/lib/orchestrator-helper.ts +167 -0
  119. package/lib/orchestrator-message-card.ts +105 -0
  120. package/lib/orchestrator-presence.ts +115 -14
  121. package/lib/orchestrator-scope.ts +80 -0
  122. package/lib/orchestrator-state.ts +107 -0
  123. package/lib/orchestrator-tool-card.ts +280 -0
  124. package/lib/orchestrator-work-search.ts +218 -0
  125. package/lib/orchestrator-work.ts +79 -0
  126. package/lib/permission-mode-policy.ts +59 -0
  127. package/lib/review-candidate-view-owner.ts +8 -6
  128. package/lib/review-candidate-view.ts +3 -2
  129. package/lib/review-compact-contract.ts +2 -2
  130. package/lib/review-host-relay.ts +7 -2
  131. package/lib/review-integration-v2.ts +100 -22
  132. package/lib/review-publication-gate.ts +1 -1
  133. package/lib/review-risk-assessment.ts +77 -51
  134. package/lib/review-sidebar-state.ts +2 -1
  135. package/lib/review-transaction.ts +72 -3
  136. package/lib/runtime-metrics-children.ts +1 -1
  137. package/lib/runtime-metrics-native.ts +3 -2
  138. package/lib/selection-engine.ts +22 -7
  139. package/lib/session-change-capture.ts +5 -4
  140. package/lib/session-changes.ts +27 -22
  141. package/lib/session-profile-binding.ts +83 -0
  142. package/lib/session-profile-freeze.ts +261 -0
  143. package/lib/session-profile-persistence.ts +187 -0
  144. package/lib/session-worktree-registry.ts +74 -8
  145. package/lib/shell-bar.ts +41 -11
  146. package/lib/shell-changes-view.ts +40 -13
  147. package/lib/shell-changes.ts +88 -71
  148. package/lib/shell-prompt.ts +6 -4
  149. package/lib/shell-sidebar-layout.ts +5 -1
  150. package/lib/shell-todo.ts +81 -26
  151. package/lib/shell-usage.ts +4 -3
  152. package/lib/status-timing-diagnostics.ts +201 -0
  153. package/lib/telemetry-trigger.ts +3 -1
  154. package/lib/vim-editor-adapter.ts +27 -5
  155. package/lib/visual-customization-policy.ts +20 -9
  156. package/lib/visual-customize-view.ts +171 -20
  157. package/lib/writer-surfaces.ts +162 -0
  158. package/lib/yolo-session-policy.ts +198 -121
  159. package/package.json +5 -5
  160. package/runtime/child-package-injection.mjs +63 -0
  161. package/runtime/gentle-ai-binary.mjs +109 -4
  162. package/runtime/gentle-shell-bundled.mjs +536 -0
  163. package/runtime/gentle-shell-launcher.mjs +74 -23
  164. package/runtime/gentle-shell-startup-flow.mjs +55 -0
  165. package/runtime/gentle-shell-startup-update.mjs +93 -0
  166. package/runtime/gentle-shell-uninstall.mjs +841 -0
  167. package/runtime/gentle-shell-update-panel.mjs +232 -0
  168. package/runtime/native-review-cli.mjs +191 -70
  169. package/runtime/review-integration-v2.mjs +100 -22
  170. package/runtime/review-risk-assessment.mjs +77 -51
  171. package/runtime/status-timing-diagnostics.mjs +202 -0
  172. package/runtime/telemetry-trigger.mjs +3 -1
  173. package/scripts/bootstrap.cmd +255 -0
  174. package/scripts/bootstrap.sh +168 -0
  175. package/scripts/build-distribution.mjs +157 -0
  176. package/scripts/build-installer-bundles.mjs +136 -0
  177. package/scripts/build-runtime-modules.mjs +7 -0
  178. package/scripts/bundled-install.mjs +1308 -0
  179. package/scripts/bundled-wizard.mjs +399 -0
  180. package/scripts/check-types.mjs +4 -6
  181. package/scripts/gentle-ai-installer.mjs +79 -32
  182. package/scripts/install-gentle-ai.mjs +48 -1
  183. package/scripts/install-tui-mode-setting.mjs +10 -1
  184. package/scripts/install-wizard-preview.mjs +154 -0
  185. package/scripts/installer-downloads.mjs +648 -0
  186. package/scripts/installer-preflight.mjs +387 -0
  187. package/scripts/installer-probes.mjs +564 -0
  188. package/scripts/installer-runner.mjs +1102 -0
  189. package/scripts/installer-server.mjs +881 -0
  190. package/scripts/installer-windows-artifacts.json +15 -0
  191. package/scripts/installer-windows.mjs +752 -0
  192. package/scripts/main-channel.mjs +452 -0
  193. package/scripts/npm-pack-result.mjs +31 -0
  194. package/scripts/pi-update.mjs +223 -0
  195. package/scripts/render-cache-performance-worker.mjs +205 -0
  196. package/scripts/render-cache-performance.mjs +115 -0
  197. package/scripts/run-test-suite.mjs +22 -8
  198. package/scripts/test-installer-acceptance.sh +161 -0
  199. package/scripts/test-packed-runner.mjs +23 -5
  200. package/scripts/types-baseline.json +2 -92
  201. package/scripts/verify-package-files.mjs +82 -18
  202. package/skills/gentle-ai/SKILL.md +12 -11
  203. package/tests/agent-home.test.ts +56 -3
  204. package/tests/agent-model-resolution.test.ts +113 -0
  205. package/tests/agent-profiles.test.ts +12 -0
  206. package/tests/agent-risk-escalation.test.ts +123 -0
  207. package/tests/agents-completion-delivery.test.ts +17 -0
  208. package/tests/agents-protocol.test.ts +57 -0
  209. package/tests/agents-queries.test.ts +9 -9
  210. package/tests/agents-runner-process.test.ts +31 -0
  211. package/tests/agents-runner.test.ts +89 -4
  212. package/tests/append-system-prompt-route.test.ts +18 -1
  213. package/tests/auto-mode-human-answers.test.ts +68 -0
  214. package/tests/auto-mode-invariants.test.ts +276 -0
  215. package/tests/auto-mode-sdk.test.ts +84 -0
  216. package/tests/auto-mode.test.ts +49 -0
  217. package/tests/auto-permission-answers.test.ts +170 -0
  218. package/tests/auto-permission-classifier.test.ts +634 -0
  219. package/tests/auto-permission-compound.test.ts +24 -0
  220. package/tests/auto-permission-evidence.test.ts +88 -0
  221. package/tests/auto-permission-memory.test.ts +46 -0
  222. package/tests/auto-permission-public-regressions.test.ts +111 -0
  223. package/tests/auto-permission-runtime.test.ts +454 -0
  224. package/tests/autonomous-guard.test.ts +86 -16
  225. package/tests/background-cache-warming.test.ts +2 -1
  226. package/tests/background-jobs.test.ts +227 -0
  227. package/tests/background-monitor.test.ts +195 -0
  228. package/tests/background-subagents-default-mode.test.ts +28 -5
  229. package/tests/bounded-stop-contract.test.ts +85 -0
  230. package/tests/bounded-writer-admission.test.ts +52 -1
  231. package/tests/bridge-wake-lifecycle.test.ts +129 -0
  232. package/tests/bridge-wake-rendering.test.ts +56 -0
  233. package/tests/build-distribution.test.ts +269 -0
  234. package/tests/builtin-codemode-optout.test.ts +242 -0
  235. package/tests/bundled-install.test.ts +1319 -0
  236. package/tests/bundled-wizard.test.ts +505 -0
  237. package/tests/card-content-policy.test.ts +68 -0
  238. package/tests/card-style-policy.test.ts +34 -2
  239. package/tests/child-codegraph-bootstrap.test.ts +74 -0
  240. package/tests/child-package-entrypoints.test.ts +65 -0
  241. package/tests/child-package-injection.test.ts +74 -0
  242. package/tests/child-safety.test.ts +12 -0
  243. package/tests/child-web-package.test.ts +224 -0
  244. package/tests/codegraph-tools.test.ts +35 -0
  245. package/tests/codemode-rendering.test.ts +107 -2
  246. package/tests/command-palette.test.ts +5 -5
  247. package/tests/context-threshold.test.ts +31 -0
  248. package/tests/delegate-for-reason-contract.test.ts +327 -0
  249. package/tests/delegated-key-learnings-contract.test.ts +3 -2
  250. package/tests/devbinary/native-review-parity.devtest.ts +1 -1
  251. package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
  252. package/tests/engram-safety.test.ts +316 -0
  253. package/tests/generic-agent-tools.test.ts +16 -1
  254. package/tests/gentle-agents.test.ts +1895 -76
  255. package/tests/gentle-ai-binary-isolation.test.ts +135 -0
  256. package/tests/gentle-ai-binary.test.ts +19 -2
  257. package/tests/gentle-ai-child-guards.test.ts +202 -0
  258. package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
  259. package/tests/gentle-ai-installer.test.ts +548 -58
  260. package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
  261. package/tests/gentle-ai-renderer.test.ts +6 -6
  262. package/tests/gentle-ai.test.ts +999 -31
  263. package/tests/gentle-herdr-activity.test.ts +100 -0
  264. package/tests/gentle-notifications.test.ts +427 -0
  265. package/tests/gentle-shell-bin.test.ts +263 -19
  266. package/tests/gentle-shell-bundled.test.ts +1036 -0
  267. package/tests/gentle-shell-launcher.test.ts +251 -25
  268. package/tests/gentle-shell-startup-flow.test.ts +75 -0
  269. package/tests/gentle-shell-startup-update.test.ts +85 -0
  270. package/tests/gentle-shell-uninstall.test.ts +978 -0
  271. package/tests/gentle-shell-update-panel.test.ts +158 -0
  272. package/tests/gentle-shell.test.ts +611 -100
  273. package/tests/gentle-todo.test.ts +317 -14
  274. package/tests/herdr-activity.test.ts +243 -0
  275. package/tests/history-agent-home.test.ts +81 -0
  276. package/tests/history-child-guard.test.ts +70 -0
  277. package/tests/inprocess-reviewer.test.ts +20 -0
  278. package/tests/install-gentle-ai-failure.test.ts +69 -0
  279. package/tests/install-tui-mode-setting.test.ts +15 -0
  280. package/tests/install-wizard.test.ts +1223 -0
  281. package/tests/installer-bundles.test.ts +122 -0
  282. package/tests/installer-posix-bootstrap.test.ts +1201 -0
  283. package/tests/installer-preflight.test.ts +750 -0
  284. package/tests/installer-probes.test.ts +1106 -0
  285. package/tests/installer-runner.test.ts +2159 -0
  286. package/tests/installer-server.test.ts +1567 -0
  287. package/tests/installer-windows-bootstrap.test.ts +2410 -0
  288. package/tests/jobs-view.test.ts +160 -0
  289. package/tests/main-channel-upgrade.test.ts +395 -0
  290. package/tests/main-channel.test.ts +344 -0
  291. package/tests/maintainer/provider-relay.maintest.ts +13 -12
  292. package/tests/model-price-ratio.test.ts +116 -0
  293. package/tests/model-profiles-preservation.test.ts +153 -0
  294. package/tests/model-routing-package.test.ts +10 -0
  295. package/tests/nan-provider-runtime.test.ts +156 -0
  296. package/tests/nan-provider.test.ts +28 -18
  297. package/tests/native-binary-gate.test.ts +2 -0
  298. package/tests/native-review-capability-contract.test.ts +7 -1
  299. package/tests/native-review-cli.test.ts +236 -1
  300. package/tests/native-review-consent.test.ts +2 -2
  301. package/tests/native-review-parity-runtime.test.ts +1 -1
  302. package/tests/native-review-parity.test.ts +12 -48
  303. package/tests/native-risk-evidence-parity.test.ts +21 -0
  304. package/tests/no-self-pause-contract.test.ts +87 -0
  305. package/tests/notification-audio-native.test.ts +281 -0
  306. package/tests/notification-audio-windows.test.ts +581 -0
  307. package/tests/notification-audio.test.ts +548 -0
  308. package/tests/notification-customize.test.ts +655 -0
  309. package/tests/notification-events.test.ts +115 -0
  310. package/tests/notification-policy.test.ts +201 -0
  311. package/tests/notification-pulse-client.test.ts +356 -0
  312. package/tests/notification-pulse-protocol.test.ts +213 -0
  313. package/tests/notification-pulse-stream.test.ts +320 -0
  314. package/tests/notification-scheduler.test.ts +258 -0
  315. package/tests/notification-ui.test.ts +157 -0
  316. package/tests/npm-pack-result.test.ts +41 -0
  317. package/tests/odd-integration.test.ts +14 -5
  318. package/tests/odd-routing-canonical-ratchet.test.ts +53 -12
  319. package/tests/odd-routing-contract.test.ts +80 -23
  320. package/tests/odd-runtime-delegation-gate.test.ts +4 -1
  321. package/tests/orchestrator-budget.test.ts +29 -6
  322. package/tests/orchestrator-catalog.test.ts +146 -0
  323. package/tests/orchestrator-consultation-sdk.test.ts +614 -0
  324. package/tests/orchestrator-consultation.test.ts +111 -0
  325. package/tests/orchestrator-discovery.test.ts +241 -0
  326. package/tests/orchestrator-helper-consent.test.ts +171 -0
  327. package/tests/orchestrator-helper.test.ts +297 -0
  328. package/tests/orchestrator-message-card.test.ts +190 -0
  329. package/tests/orchestrator-presence.test.ts +23 -0
  330. package/tests/orchestrator-rdd-ownership.test.ts +6 -5
  331. package/tests/orchestrator-scope.test.ts +107 -0
  332. package/tests/orchestrator-state.test.ts +114 -0
  333. package/tests/orchestrator-status-publication-contract.test.ts +34 -0
  334. package/tests/orchestrator-tool-card.test.ts +398 -0
  335. package/tests/orchestrator-work-search.test.ts +318 -0
  336. package/tests/orchestrator-work.test.ts +147 -0
  337. package/tests/package-manifest.test.ts +105 -21
  338. package/tests/packed-runner-owned-path.test.ts +35 -1
  339. package/tests/parallel-reason-contract.test.ts +21 -0
  340. package/tests/parallel-writer-admission.test.ts +210 -0
  341. package/tests/permission-mode-policy.test.ts +194 -0
  342. package/tests/persona-single-channel.test.ts +4 -12
  343. package/tests/pi-pretty.test.ts +56 -0
  344. package/tests/pi-update-launcher.test.ts +87 -0
  345. package/tests/pi-update.test.ts +186 -0
  346. package/tests/provider-defect-handoff.test.ts +4 -3
  347. package/tests/quiet-tool-rendering.test.ts +160 -3
  348. package/tests/rdd-aware-verification-contract.test.ts +29 -41
  349. package/tests/rdd-status-line.test.ts +52 -0
  350. package/tests/render-cache-performance.test.ts +48 -0
  351. package/tests/review-agent-end-preflight.test.ts +62 -10
  352. package/tests/review-candidate-view.test.ts +101 -12
  353. package/tests/review-contract-prompt.test.ts +33 -4
  354. package/tests/review-controller-native-recovery.test.ts +327 -17
  355. package/tests/review-controller-native-routing.test.ts +255 -1
  356. package/tests/review-controller-workspace-root.test.ts +43 -2
  357. package/tests/review-controller.test.ts +2 -2
  358. package/tests/review-correction-lifecycle.test.ts +2 -1
  359. package/tests/review-gate.test.ts +7 -4
  360. package/tests/review-graph-schema.test.ts +2 -2
  361. package/tests/review-host-relay-routing.test.ts +5 -3
  362. package/tests/review-host-relay.test.ts +49 -2
  363. package/tests/review-integration-v2-forward.test.ts +21 -1
  364. package/tests/review-json-arguments.test.ts +167 -0
  365. package/tests/review-last-event-closure.test.ts +157 -8
  366. package/tests/review-ledger-contract.test.ts +5 -4
  367. package/tests/review-policy-judgment-day.test.ts +1 -1
  368. package/tests/review-policy-ordinary.test.ts +2 -2
  369. package/tests/review-relay-transport-agent.test.ts +66 -1
  370. package/tests/review-risk-assessment.test.ts +109 -33
  371. package/tests/review-session-standing-permission-controller.test.ts +16 -17
  372. package/tests/review-session-standing-permission-ipc.test.ts +6 -5
  373. package/tests/review-test-fixtures.ts +7 -1
  374. package/tests/review-transaction.test.ts +18 -1
  375. package/tests/run-test-suite.test.ts +83 -5
  376. package/tests/runtime-harness.mjs +42 -15
  377. package/tests/runtime-metrics-extension.test.ts +1 -1
  378. package/tests/runtime-metrics-native.test.ts +31 -1
  379. package/tests/selection-engine.test.ts +65 -5
  380. package/tests/session-change-capture.test.ts +10 -10
  381. package/tests/session-changes-shell.test.ts +242 -48
  382. package/tests/session-changes.test.ts +17 -24
  383. package/tests/session-profile-binding.test.ts +163 -0
  384. package/tests/session-profile-freeze.test.ts +394 -0
  385. package/tests/session-profile-persistence.test.ts +334 -0
  386. package/tests/session-subject-routing-contract.test.ts +34 -0
  387. package/tests/session-worktree-registry.test.ts +57 -2
  388. package/tests/shell-bar.test.ts +40 -0
  389. package/tests/shell-changes-view.test.ts +85 -34
  390. package/tests/shell-changes.test.ts +79 -62
  391. package/tests/shell-render-cache-runtime.test.ts +69 -0
  392. package/tests/shell-render-cache.test.ts +129 -0
  393. package/tests/shell-sidebar-layout.test.ts +27 -2
  394. package/tests/shell-todo.test.ts +132 -11
  395. package/tests/shell-usage-view.test.ts +1 -1
  396. package/tests/shell-usage.test.ts +35 -0
  397. package/tests/short-check-routing-contract.test.ts +49 -0
  398. package/tests/skill-registry-watcher.test.ts +119 -0
  399. package/tests/skill-registry.test.ts +311 -5
  400. package/tests/startup-banner.test.ts +117 -3
  401. package/tests/startup-home-repo.test.ts +170 -0
  402. package/tests/status-timing-diagnostics.test.ts +283 -0
  403. package/tests/support/orchestrator-modules.ts +19 -0
  404. package/tests/task-size-routing-contract.test.ts +241 -0
  405. package/tests/telemetry-trigger.test.ts +27 -1
  406. package/tests/test-strength-contract.test.ts +124 -0
  407. package/tests/verify-agent-contract.test.ts +90 -0
  408. package/tests/verify-first-launch-contract.test.ts +139 -0
  409. package/tests/verify-package-files.test.ts +56 -0
  410. package/tests/vim-editor-adapter.test.ts +47 -21
  411. package/tests/visual-customization-policy.test.ts +32 -3
  412. package/tests/visual-customize-view.test.ts +288 -10
  413. package/tests/writer-edit-surface-scope.test.ts +59 -2
  414. package/tests/writer-surfaces.test.ts +199 -0
  415. package/tests/yolo-customize.test.ts +18 -3
  416. package/tests/yolo-mode-runtime.test.ts +37 -10
  417. package/tests/yolo-mode.test.ts +38 -5
  418. package/tests/yolo-session-policy.test.ts +1 -1
  419. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
  420. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
  421. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
  422. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  423. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
  424. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
  425. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -1,18 +1,25 @@
1
1
  import { agentsViewKey, agentsCollapseKey, agentsStopKey } from "../lib/agents-keys.ts";
2
2
  import { spawn } from "node:child_process";
3
+ import { Type } from "typebox";
3
4
  import { recordReviewMutation } from "../lib/review-reminder-receipt.ts";
4
5
  import { SESSION_CHANGE_RELAY } from "../lib/session-changes.ts";
5
6
  import { publishForeignSessionChange } from "../lib/session-change-capture.ts";
6
- import { SessionWorktreeRegistry, resolveSessionWorktree, type WorktreeResolver } from "../lib/session-worktree-registry.ts";
7
+ import { SESSION_WORKTREE_ENTRY, SESSION_WORKTREE_CHANGED, SessionWorktreeRegistry, resolveSessionWorktree, type WorktreeResolver } from "../lib/session-worktree-registry.ts";
8
+ import { canonicalWriterRoot } from "../lib/writer-surfaces.ts";
7
9
  import { ForeignTargetGrants } from "../lib/foreign-target-grants.ts";
8
10
  import { MESSAGING_REASON_MAX_UTF8_BYTES, MESSAGING_REASON_MIN_CHARACTERS, normalizeMessagingReason, SessionMessagingGrants } from "../lib/session-messaging-grants.ts";
9
- import { existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
11
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync } from "node:fs";
10
12
  import { randomUUID } from "node:crypto";
11
13
  import { mkdir, readFile, writeFile } from "node:fs/promises";
12
14
  import os from "node:os";
13
15
  import { join, resolve, isAbsolute } from "node:path";
14
16
  import { fileURLToPath } from "node:url";
15
- import { keyHint, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
17
+ import { createLocalBashOperations, keyHint, SettingsManager, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
18
+ import { createJobRegistry, isStandaloneSleep, type JobExecOperations, type JobRecord } from "../lib/background-jobs.ts";
19
+ import { JOB_GLYPH, JOB_NOTICE_TYPE, jobDetails, jobNoticeText, monitorEventsText, monitorStoppedText, registerBackgroundJobTools } from "../lib/background-jobs-tools.ts";
20
+ import { createMonitorController, mergeMonitorBatches, type MonitorNotice } from "../lib/background-monitor.ts";
21
+ import { JobsView } from "../lib/jobs-view.ts";
22
+ import { JOBS_SIDEBAR_EVENT, JOBS_STATUS_KEY } from "../lib/jobs-sidebar-state.ts";
16
23
  import { Text, type TUI } from "@earendil-works/pi-tui";
17
24
  import { invalidateSidebar } from "../lib/shell-sidebar-layout.ts";
18
25
  import { VISUAL_SETTINGS_CHANGED } from "../lib/shell-sidebar.ts";
@@ -20,10 +27,12 @@ import { resolveVisualSettings } from "../lib/visual-customization-policy.ts";
20
27
  import { createCompletionQueue } from "../lib/agents-completion-delivery.ts";
21
28
  import { createAgentMessageQueue, type PendingAgentMessage } from "../lib/agents-message-delivery.ts";
22
29
  import { AGENT_MODE, discoverAgents, formatModelRef, loadAgentsConfig, resolveAgentProfile, withPinnedModelProfiles, type AgentDefinition, type AgentMode } from "../lib/agents-config.ts";
30
+ import { freezeSessionProfileAtStartup, inheritedProfileDriftNotice, resolveSessionProfile, sessionProfileRoutingAt } from "../lib/session-profile-freeze.ts";
23
31
  import { resolveBackgroundSubagentsPolicy } from "../lib/background-subagents-policy.ts";
24
32
  import { installBackgroundCacheWarming } from "../lib/background-cache-warming.ts";
25
- import { isFinished, TASK_EVENT, TASK_STATUS, TaskStore, type AskRequest, type TaskRecord } from "../lib/agents-protocol.ts";
26
- import { AgentRunner, piCommand, abortReasonText, type AskAnswer, type RunnerDeps, type TaskRequest } from "../lib/agents-runner.ts";
33
+ import { isFinished, MISSING_TOOLS_NOTE_PREFIX, TASK_EVENT, TASK_STATUS, TaskStore, type AskRequest, type TaskRecord } from "../lib/agents-protocol.ts";
34
+ import { AgentRunner, piCommand, abortReasonText, REQUESTED_TOOLS_ENV, type AskAnswer, type RunnerDeps, type TaskRequest } from "../lib/agents-runner.ts";
35
+ import { parseChildPackageInjection } from "../lib/child-package-injection.ts";
27
36
  import { ChildMessenger, type IpcEndpoint } from "../lib/agents-messaging.ts";
28
37
  import { ActiveSessionClient, ActiveSessionListener, SessionPresenceRegistry, type PresenceRecord, type ReceivedNotification, type SentNotification, type SessionPresenceCandidate } from "../lib/agents-session-transport.ts";
29
38
  import { WindowsActiveSessionClient, WindowsActiveSessionListener, WindowsSessionPresenceRegistry, type WindowsSessionRegistryPhaseObserver } from "../lib/windows-session-transport.ts";
@@ -33,18 +42,29 @@ import { historyDir, loadHistory, loadStoredTask, pruneHistory, saveTask } from
33
42
  import { sessionToMarkdown } from "../lib/agents-transcript.ts";
34
43
  import { AgentsView } from "../lib/agents-view.ts";
35
44
  import { withOverlayRepaint } from "../lib/overlay-repaint.ts";
36
- import { PresencePublisher } from "../lib/orchestrator-presence.ts";
45
+ import { PresencePublisher, sanitizeDisplayLabel, listPresence, sessionHash } from "../lib/orchestrator-presence.ts";
46
+ import { incomingMessageCard, outgoingMessageCall, outgoingMessageResult, type OrchestratorMessageDetails } from "../lib/orchestrator-message-card.ts";
47
+ import { orchestratorToolRenderers } from "../lib/orchestrator-tool-card.ts";
48
+ import { discoverOrchestrators } from "../lib/orchestrator-discovery.ts";
49
+ import { consultPublishedMetadata, unavailableMetadata, type MetadataReceipt } from "../lib/orchestrator-consultation.ts";
50
+ import { HelperCostPermission } from "../lib/orchestrator-helper-consent.ts";
51
+ import { OrchestratorStateCache } from "../lib/orchestrator-state.ts";
52
+ import { normalizeTaskSubject } from "../lib/orchestrator-alias.ts";
53
+ import { decodeWorkDescriptor } from "../lib/orchestrator-work.ts";
54
+ import { validateWorkFilter, searchPublishedWork } from "../lib/orchestrator-work-search.ts";
55
+ import { OrchestratorScopeCache, type RepositoryFact } from "../lib/orchestrator-scope.ts";
37
56
  import { createRpcActivityPublisher, type RpcActivityPublisher } from "../lib/agents-rpc-publisher.ts";
38
57
  import { isInteractiveRpcHost } from "../lib/rpc-host.ts";
39
58
  import { createNativeFullscreenInteraction } from "../lib/native-fullscreen-interaction.ts";
40
59
  import { AGENTS_GLYPH, renderAgentsCard, widgetExpiryMs, widgetRows } from "../lib/agents-widget.ts";
41
60
  import { CARD_TONE, renderCard } from "../lib/shell-card.ts";
42
61
  import { openInExternalEditor } from "./gentle-shell.ts";
43
- import { resolveGentlePiAgentHome, gentlePiConfigHome } from "../lib/agent-home.ts";
44
- import { resolveProfilePin, resolveUnversionedProjectProfile } from "../lib/agent-profile-pin.ts";
45
- import { allowedEditSurfaces, isDevelopmentSurface, isGenericBoundedWriter, prepareBoundSessionRepository, rejectUnscopedBoundedWriterDispatch, safeBootstrapDirectory, sessionRepositoryAuthority } from "../lib/bounded-writer-admission.ts";
62
+ import { gentlePiConfigHome } from "../lib/agent-home.ts";
63
+ import { resolveAgentHomeDirectory } from "../lib/agent-model-resolution.ts";
64
+ import { allowedEditSurfaces, inheritAllowedEditSurfaces, isBoundedWriter, isDevelopmentSurface, isGenericBoundedWriter, prepareBoundSessionRepository, rejectUnscopedBoundedWriterDispatch, safeBootstrapDirectory, sessionRepositoryAuthority } from "../lib/bounded-writer-admission.ts";
46
65
  import { CHILD_METRICS_EVENT, CHILD_METRICS_REVOKED, childEvent, launchSelection, type LaunchSelection } from "../lib/runtime-metrics-children.ts";
47
66
  import { runtimeMetricsEnvAllows, type RuntimeMetricsPolicyDeps } from "../lib/runtime-metrics-policy.ts";
67
+ import { publishTaskStatusChanges } from "../lib/notification-events.ts";
48
68
 
49
69
  // Gentle Agents: subagents as isolated `pi --mode rpc` children, a task
50
70
  // store that notifies per task, and a Gentle Shell card above the editor.
@@ -53,19 +73,46 @@ import { runtimeMetricsEnvAllows, type RuntimeMetricsPolicyDeps } from "../lib/r
53
73
 
54
74
  export const AGENTS_WIDGET_KEY = "gentle-agents";
55
75
  export const AGENTS_COMMAND_NAME = "gentle:agents";
76
+ export const JOBS_COMMAND_NAME = "gentle:jobs";
77
+ // How long session shutdown waits for stopped jobs to close their logs.
78
+ const JOBS_SHUTDOWN_GRACE_MS = 2000;
56
79
  export const AGENTS_RESULT_TYPE = "gentle-agents.result";
57
80
  export const AGENTS_MESSAGE_TYPE = "gentle-agents.message";
58
81
  export const AGENTS_ORCHESTRATOR_MESSAGE_TYPE = "gentle-agents.orchestrator-message";
82
+
83
+ // Display labels are best-effort local snapshots, never routing or authority.
84
+ function messagePeerLabel(profile: string, sessionId: string): string | undefined {
85
+ try {
86
+ const page = listPresence(profile);
87
+ if (page.unavailable || page.overflow || page.rejected) return undefined;
88
+ const matches = page.entries.filter(header => header.sessionHash === sessionHash(sessionId));
89
+ return matches.length === 1 && matches[0].recent ? sanitizeDisplayLabel(matches[0].label) || undefined : undefined;
90
+ } catch { return undefined; }
91
+ }
59
92
  export const AGENTS_STALE_RESULT_TYPE = "gentle-agents.stale-result";
93
+ export const AGENTS_STALE_NOTICE_TYPE = "gentle-agents.stale-notice";
60
94
  const RENDER_COALESCE_MS = 400;
61
95
  const CLOCK_TICK_MS = 1000;
62
96
  const TOOL_PREFIX = "subagent_";
63
97
  // Wakes an idle parent after child content was stored as a custom message.
64
98
  // It names itself as automated so the model never attributes it to the human.
65
99
  const PARENT_WAKE_TEXT = "[System-generated Gentle Agents notification, not written by the user] Subagent output was delivered to this session above. Review it and continue.";
100
+ const PARENT_WAKE_TYPE = "gentle-agents.wake";
101
+ const BRIDGE_WAKE_IDENTITY_TYPE = "gentle-agents.wake-identity";
102
+ interface BridgeWakeIdentity {
103
+ sessionId: string;
104
+ nonce: string;
105
+ text: string;
106
+ }
107
+ const bridgeWakeText = (nonce: string): string => `${PARENT_WAKE_TEXT} [gentle-agents wake: ${nonce}]`;
108
+ const NATIVE_PARENT_WAKE_TEXT = "Review the delivered subagent output and continue.";
66
109
  // How long a dispatched wake may take to start a parent run before a later
67
110
  // delivery may send another one.
68
111
  export const PARENT_WAKE_GRACE_MS = 30_000;
112
+ // A delivery or wake the host rejected while the parent was idle is retried
113
+ // after the grace at most this many times in a row, so a permanently broken
114
+ // runtime never retries forever.
115
+ const PARENT_DELIVERY_RETRY_LIMIT = 3;
69
116
 
70
117
  const retiredSddAgent = (name: string): boolean => /^sdd(?:-|$)/.test(name);
71
118
 
@@ -76,6 +123,7 @@ export interface SessionTransportRegistry {
76
123
  }
77
124
 
78
125
  export interface SessionTransportListener {
126
+ readonly record?: PresenceRecord;
79
127
  readonly registry: SessionTransportRegistry;
80
128
  readonly closesRegistry?: boolean;
81
129
  start(): Promise<void>;
@@ -105,15 +153,22 @@ export interface AgentsDeps extends RunnerDeps {
105
153
  metricsSchedule?: RunnerDeps["schedule"];
106
154
  // Extensions every child loads with --extension (gentle-shell#1587).
107
155
  childExtensionPaths?: string[];
156
+ /** Shell for background jobs; defaults to Pi's configured Bash. */
157
+ jobShell?: (cwd: string) => { operations: JobExecOperations; commandPrefix?: string };
158
+ /** Directory for background job output; defaults to a private temp dir. */
159
+ jobOutputDir?: () => string;
108
160
  }
109
161
 
110
- // gentle-shell#1587: children do not load the gentle-pi package in the
111
- // isolated Gentle Shell home, so context filtering and destructive-command
112
- // safety are passed to every child explicitly. Missing files are omitted;
113
- // installations must include both entries to provide the delegated boundary.
162
+ // gentle-shell#1587: fallback for a parent without the launcher's package
163
+ // injection signal (#1690), e.g. a manual `pi -e <package>` launch. Children
164
+ // then get context filtering, destructive-command safety, and the nan
165
+ // provider (gentle-shell#1731 T32: a model routed to nan/* is not found
166
+ // without it). When the signal is present, children load the whole package
167
+ // instead. This list is frozen: new child-facing behavior ships inside the
168
+ // package, never here. Missing files are omitted.
114
169
  export function childContextExtensionPaths(exists: (path: string) => boolean = existsSync): string[] {
115
170
  try {
116
- return ["./child-context.ts", "./child-safety.ts"]
171
+ return ["./child-context.ts", "./child-safety.ts", "./nan-provider.ts"]
117
172
  .map((path) => fileURLToPath(new URL(path, import.meta.url)))
118
173
  .filter(exists);
119
174
  } catch {
@@ -121,11 +176,37 @@ export function childContextExtensionPaths(exists: (path: string) => boolean = e
121
176
  }
122
177
  }
123
178
 
179
+ // #1690: when the gentle-shell launcher injected the package with -e, the
180
+ // host env carries that exact set and every child loads it too, which already
181
+ // includes the curated entries. Without a valid signal (declared package,
182
+ // regular gentle-pi, malformed value) children keep the curated entries.
183
+ function childExtensionRequest(env: NodeJS.ProcessEnv, curated: string[] | undefined): Pick<TaskRequest, "extensionPaths" | "noExtensions"> {
184
+ const injection = parseChildPackageInjection(env);
185
+ if (injection) return { extensionPaths: [...injection.extensionPaths], ...(injection.noExtensions ? { noExtensions: true } : {}) };
186
+ return curated && curated.length > 0 ? { extensionPaths: [...curated] } : {};
187
+ }
188
+
124
189
  export function agentRuntimePaths(home: string, agentHome = join(home, ".pi", "agent")): { sessions: string; transcripts: string } {
125
190
  const root = join(agentHome, "gentle-agents");
126
191
  return { sessions: join(root, "sessions"), transcripts: join(root, "transcripts") };
127
192
  }
128
193
 
194
+ const workDescriptorSchema = {
195
+ type: "object", additionalProperties: false,
196
+ description: "Explicit non-authoritative classification, not child context. Topic requires area; limits are UTF-8 bytes. No nested tasks.",
197
+ properties: {
198
+ area: { type: "string", maxLength: 64 }, topic: { type: "string", maxLength: 64 },
199
+ tags: { type: "array", maxItems: 8, uniqueItems: true, items: { type: "string", maxLength: 64 } },
200
+ refs: { type: "array", maxItems: 8, uniqueItems: true, items: {
201
+ type: "object", additionalProperties: false, required: ["kind", "repository", "id"], properties: {
202
+ kind: { type: "string", enum: ["issue", "pr", "task"] },
203
+ repository: { type: "string", maxLength: 256, description: "Explicit public host/owner/repo; not a URL or inferred identity." },
204
+ id: { type: "string", maxLength: 256, description: "Canonical positive decimal issue/PR ID or opaque historical task ID; never a peer route." },
205
+ },
206
+ } },
207
+ },
208
+ };
209
+
129
210
  interface ToolText {
130
211
  content: Array<{ type: "text"; text: string }>;
131
212
  details: Record<string, unknown>;
@@ -190,6 +271,43 @@ function ownedChildIpc(env: NodeJS.ProcessEnv, candidate: IpcEndpoint | undefine
190
271
  return candidate;
191
272
  }
192
273
 
274
+ // Catalog declarations are routing hints, not proof of child availability.
275
+ // Filter non-name entries such as `"*": false`, but do not claim that a
276
+ // syntactically valid name is registered. Empty declarations omit --tools and
277
+ // use Pi defaults. The runner adds parent messaging separately.
278
+ const TOOL_NAME = /^[A-Za-z0-9_.:-]+$/;
279
+
280
+ function declaredToolInventory(agent: AgentDefinition): string {
281
+ const usable = agent.tools.filter((name) => TOOL_NAME.test(name));
282
+ if (usable.length > 0) return `[declared tools (not verified): ${usable.join(", ")}]`;
283
+ return agent.tools.length === 0
284
+ ? "[declared tools: Pi defaults (no allowlist)]"
285
+ : "[declared tools: no tool names declared]";
286
+ }
287
+
288
+ // Pi drops unknown --tools names without a diagnostic (#1690), so the child
289
+ // reports them once through the one notify the parent keeps. The check runs
290
+ // at the first before_agent_start, not session_start: Pi runs session_start
291
+ // handlers in extension load order, so an extension loaded after this one may
292
+ // still register its tools there. MCP tools register later still, and entries
293
+ // that cannot be tool names (the `"*": false` frontmatter entry) are not
294
+ // checkable.
295
+ function registerMissingToolsCheck(pi: ExtensionAPI, env: NodeJS.ProcessEnv): void {
296
+ const requested = (env[REQUESTED_TOOLS_ENV] ?? "").split(",").map((name) => name.trim())
297
+ .filter((name) => /^[A-Za-z0-9_.:-]+$/.test(name) && !name.startsWith("mcp__"));
298
+ if (requested.length === 0) return;
299
+ let reported = false;
300
+ pi.on("before_agent_start", (_event, ctx) => {
301
+ if (reported) return;
302
+ reported = true;
303
+ try {
304
+ const available = new Set(pi.getAllTools().map((tool) => tool.name));
305
+ const missing = [...new Set(requested.filter((name) => !available.has(name)))];
306
+ if (missing.length > 0) ctx.ui.notify(`${MISSING_TOOLS_NOTE_PREFIX} ${missing.join(", ")}`, "warning");
307
+ } catch { /* A diagnostic must never break the child's first prompt. */ }
308
+ });
309
+ }
310
+
193
311
  function registerChildMessaging(pi: ExtensionAPI, ipc: IpcEndpoint): void {
194
312
  const messenger = new ChildMessenger(ipc);
195
313
  pi.registerTool({
@@ -197,7 +315,7 @@ function registerChildMessaging(pi: ExtensionAPI, ipc: IpcEndpoint): void {
197
315
  label: "Agent parent message",
198
316
  description: "Send a bounded notification or correlated query to this subagent's parent.",
199
317
  parameters: { type: "object", additionalProperties: false, required: ["message"], properties: { kind: { type: "string", enum: ["notification", "query"] }, message: { type: "string" } } } as never,
200
- async execute(_id, params) {
318
+ async execute(_id, params): Promise<ToolText> {
201
319
  const input = params as { kind?: unknown; message?: unknown };
202
320
  if (typeof input.message !== "string") throw new Error("parent messages require text");
203
321
  if (input.kind === undefined || input.kind === "notification") {
@@ -272,18 +390,27 @@ function expandHint(expanded: boolean): string {
272
390
  }
273
391
  }
274
392
 
393
+ // pi runs `-p` and `--mode json` through one one-shot runner that disposes
394
+ // the runtime as soon as the prompt returns, so a background result has no
395
+ // parent session left to reach (gentle-shell#1731 T18).
396
+ export function isSingleShotMode(mode: string | undefined): boolean {
397
+ return mode === "print" || mode === "json";
398
+ }
399
+
400
+ const SINGLE_SHOT_BACKGROUND_ERROR = "Background subagents are unavailable in single-shot modes: pi -p and pi --mode json exit before a parent session can receive results. Use task mode, RPC mode, or interactive Pi.";
401
+
275
402
  // The default mode for a subagent_run request that named neither an explicit
276
403
  // mode nor an agent-defined one. Background is a runtime default only when
277
404
  // the background-subagents policy is on AND the parent can receive results:
278
- // print mode exits before a parent session exists to deliver them to (see
279
- // the `ctx.mode === "print"` guard in `launch` below), so it must keep the
280
- // configured default (normally task) even when the policy is on.
405
+ // single-shot modes exit before a parent session exists to deliver them to
406
+ // (see the guard in `launch` below), so they keep the configured default
407
+ // (normally task) even when the policy is on.
281
408
  export function resolveDefaultSubagentMode(input: {
282
409
  configuredDefault: AgentMode;
283
410
  policy: "on" | "off";
284
411
  parentMode: string | undefined;
285
412
  }): AgentMode {
286
- return input.policy === "on" && input.parentMode !== "print"
413
+ return input.policy === "on" && !isSingleShotMode(input.parentMode)
287
414
  ? AGENT_MODE.BACKGROUND
288
415
  : input.configuredDefault;
289
416
  }
@@ -342,19 +469,15 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
342
469
  return;
343
470
  }
344
471
  if (childIpc) registerChildMessaging(pi, childIpc);
472
+ registerMissingToolsCheck(pi, env);
345
473
  return;
346
474
  }
347
475
  if (!agentsEnabled(env)) return;
348
476
  const deps: AgentsDeps = { ...defaultDeps(env), ...overrides };
349
477
  // An explicitly injected pi command wins over the default per-spawn resolver.
350
478
  if (overrides?.pi && !overrides.resolvePi) delete deps.resolvePi;
351
- const selectedHome = overrides.agentHome ?? (overrides.home === undefined ? resolveGentlePiAgentHome(deps.env) : join(deps.home, ".pi", "agent"));
352
- // Expand environment tildes like Pi, but leave explicit path APIs literal.
353
- const environmentHome = overrides.agentHome === undefined && overrides.home === undefined;
354
- const expandedHome = environmentHome && selectedHome === "~" ? deps.home
355
- : environmentHome && (selectedHome.startsWith("~/") || (process.platform === "win32" && selectedHome.startsWith("~\\"))) ? join(deps.home, selectedHome.slice(2)) : selectedHome;
356
479
  // Freeze the host's root before a child uses a different session cwd.
357
- const agentHome = resolve(expandedHome);
480
+ const agentHome = resolveAgentHomeDirectory({ env: deps.env, home: deps.home, agentHome: overrides.agentHome, homeOverridden: overrides.home !== undefined });
358
481
  const sessionTransport = deps.sessionTransport ?? createDefaultSessionTransport();
359
482
  if (legacySubagentsInstalledAt(agentHome)) {
360
483
  pi.on("session_start", (_event, ctx) => {
@@ -366,6 +489,10 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
366
489
  const viewKey = agentsViewKey(env);
367
490
  const stopKey = agentsStopKey(env);
368
491
  const store = new TaskStore();
492
+ // Attach before any live add/start; restore and unchanged updates are silent.
493
+ // TaskStore contains bus listener failures so notifications cannot fail tools.
494
+ const stopStatusNotifications = publishTaskStatusChanges(store, pi.events, () => performance.now());
495
+ pi.on("session_shutdown", () => { stopStatusNotifications(); });
369
496
  const restoredTaskIds = new Set<string>();
370
497
  const tasksDir = historyDir(deps.home, agentHome);
371
498
  let ui: ExtensionContext["ui"] | undefined;
@@ -376,6 +503,8 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
376
503
  let stopVisualUpdates: (() => void) | undefined;
377
504
  let sessions: ExtensionContext["sessionManager"] | undefined;
378
505
  let presence: PresencePublisher | undefined;
506
+ const scopeCache = new OrchestratorScopeCache(deps.resolveWorktree);
507
+ const stateCache = new OrchestratorStateCache();
379
508
  let rpcActivityPublisher: RpcActivityPublisher | undefined;
380
509
  // Messages already surfaced to the user this session through the RPC
381
510
  // activity publisher's `onError`, so a recurring push failure (the
@@ -383,16 +512,43 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
383
512
  // session instead of flooding the UI. Reset on every `session_start`.
384
513
  let notifiedRpcActivityErrors: Set<string> | undefined;
385
514
  const overlays = new Set<AgentsView>();
515
+ const startPresence = (manager: ExtensionContext["sessionManager"]) => {
516
+ scopeCache.clear();
517
+ const sessionId = manager.getSessionId() ?? "";
518
+ const labelSource = () => {
519
+ if (sessions !== manager || (manager.getSessionId() ?? "") !== sessionId) throw new Error("stale-session");
520
+ return manager.getSessionName?.() || manager.getCwd().split(/[\\/]/).pop() || "Orchestrator";
521
+ };
522
+ return PresencePublisher.start({ profile: agentHome, sessionId, label: labelSource(), labelSource, activity: [] });
523
+ };
386
524
  const publishActivity = () => {
387
525
  if (!sessions) return;
388
526
  try {
389
527
  if (!presence || presence.error) {
390
- presence = PresencePublisher.start({ profile: agentHome, sessionId: activeSessionId() ?? "",
391
- label: sessions.getSessionName?.() || sessions.getCwd().split(/[\\/]/).pop() || "Orchestrator", activity: [] });
528
+ presence?.dispose();
529
+ presence = startPresence(sessions);
530
+ }
531
+ const tasks = store.list(activeSessionId()).filter((task) => ownedTaskIds.has(task.id) && !isFinished(task.status) && !restoredTaskIds.has(task.id));
532
+ presence?.update(tasks.map((task) => ({ task, thread: store.thread(task.id) })));
533
+ const transport = activeSessionTransport;
534
+ if (transport?.sessionManager === sessions && transport.sessionId === activeSessionId() && transport.listener.record) {
535
+ // Read the existing durable registry, without roots()'s repeated Git validation.
536
+ // These are recorded contexts, never authority for admission or messaging.
537
+ const registered = sessions.getEntries().flatMap(entry => {
538
+ if (entry.type !== "custom" || entry.customType !== SESSION_WORKTREE_ENTRY) return [];
539
+ const data = entry.data as { sessionId?: string; root?: string; evidence?: string } | undefined;
540
+ return data?.sessionId === activeSessionId() && typeof data.root === "string" && typeof data.evidence === "string" ? [data.root] : [];
541
+ });
542
+ presence?.updateDiscovery(transport.listener.record, { workspace: sessions.getCwd(), tasks, registered,
543
+ scope: scopeCache.project(sessions.getCwd(), tasks, registered), state: stateCache.get(sessions) });
392
544
  }
393
- presence?.update(store.list(activeSessionId()).filter((task) => !isFinished(task.status) && !restoredTaskIds.has(task.id)).map((task) => ({ task, thread: store.thread(task.id) })));
394
545
  } catch { presence?.dispose(); presence = undefined; }
395
546
  };
547
+ const unsubscribeScope = pi.events.on(SESSION_WORKTREE_CHANGED, (event) => {
548
+ if ((event as { sessionId?: string } | undefined)?.sessionId !== sessions?.getSessionId()) return;
549
+ scopeCache.clear();
550
+ publishActivity();
551
+ });
396
552
  let worktrees: SessionWorktreeRegistry | undefined;
397
553
  let worktreeManager: ExtensionContext["sessionManager"] | undefined;
398
554
  let worktreeAuthority: (() => boolean) | undefined;
@@ -437,6 +593,8 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
437
593
  pi.on("session_shutdown", () => {
438
594
  clearTaskMetrics();
439
595
  unsubscribeMetrics();
596
+ unsubscribeScope();
597
+ scopeCache.clear();
440
598
  });
441
599
  let stopAllConfirmation: Promise<void> | undefined;
442
600
 
@@ -498,7 +656,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
498
656
  listener = sessionTransport.createListener(registry, sessionId, async (notification) => {
499
657
  const active = activeSessionTransport;
500
658
  if (!active || active.generation !== generation || active.sessionManager !== sessionManager || active.sessionId !== sessionId || sessions !== sessionManager || activeSessionId() !== sessionId) throw new Error("stale session transport");
501
- pi.sendMessage({ customType: AGENTS_ORCHESTRATOR_MESSAGE_TYPE, content: `Session message from ${notification.senderSessionId} (correlation ${notification.id}): ${notification.message}`, display: true, details: { gentleAgents: { senderSessionId: notification.senderSessionId, recipientSessionId: sessionId, correlationId: notification.id, direction: "incoming" } } }, { deliverAs: "followUp", triggerTurn: true });
659
+ pi.sendMessage({ customType: AGENTS_ORCHESTRATOR_MESSAGE_TYPE, content: `Session message from ${notification.senderSessionId} (correlation ${notification.id}): ${notification.message}`, display: true, details: { gentleAgents: { senderSessionId: notification.senderSessionId, recipientSessionId: sessionId, correlationId: notification.id, direction: "incoming", message: notification.message, senderLabel: messagePeerLabel(agentHome, notification.senderSessionId), recipientLabel: sanitizeDisplayLabel(sessionManager.getSessionName?.() ?? "") } } }, { deliverAs: "followUp", triggerTurn: true });
502
660
  });
503
661
  client = sessionTransport.createClient(registry, sessionId);
504
662
  if (sessions !== sessionManager || generation !== transportGeneration || activeSessionId() !== sessionId) {
@@ -515,6 +673,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
515
673
  await closeStartupTransport();
516
674
  return;
517
675
  }
676
+ publishActivity();
518
677
  } catch {
519
678
  if (activeSessionTransport?.generation === generation) activeSessionTransport = undefined;
520
679
  await closeStartupTransport();
@@ -589,12 +748,102 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
589
748
  // conversation.
590
749
  const completions = createCompletionQueue<TaskRecord>();
591
750
  const messages = createAgentMessageQueue();
751
+ // A job's exit, or a monitor's line events and harness stop, waiting for
752
+ // the parent delivery router.
753
+ type JobNotice = { job: JobRecord; kind: "exit" } | MonitorNotice;
754
+ const jobNotices: JobNotice[] = [];
755
+ let jobOutputDir: string | undefined;
756
+ const jobs = createJobRegistry({
757
+ outputDir: deps.jobOutputDir ?? (() => (jobOutputDir ??= mkdtempSync(join(os.tmpdir(), "gentle-jobs-")))),
758
+ now: deps.now,
759
+ shell: deps.jobShell ?? ((cwd) => {
760
+ const settings = SettingsManager.create(cwd, agentHome);
761
+ return { operations: createLocalBashOperations({ shellPath: settings.getShellPath() }), commandPrefix: settings.getShellCommandPrefix() };
762
+ }),
763
+ onSettled: (job) => settleJob(job),
764
+ });
765
+ const monitors = createMonitorController({
766
+ registry: jobs,
767
+ schedule: (fn, ms) => deps.schedule(fn, ms),
768
+ now: deps.now,
769
+ deliver: (notice) => queueJobNotice(notice),
770
+ });
771
+ // A parent that sleeps while background work runs only burns the turn: each
772
+ // completion already arrives as a session message (#1903). bash_background
773
+ // and monitor stay allowed because they are the sanctioned way to wait.
774
+ pi.on("tool_call", (event) => {
775
+ if (event.toolName !== "bash") return undefined;
776
+ const command = (event.input as { command?: unknown }).command;
777
+ if (typeof command !== "string" || !isStandaloneSleep(command)) return undefined;
778
+ const sessionId = activeSessionId();
779
+ // Same active-task predicate as publishActivity.
780
+ const tasks = store.list(sessionId).filter((task) => ownedTaskIds.has(task.id) && !isFinished(task.status) && !restoredTaskIds.has(task.id)).length;
781
+ const running = jobs.list(sessionId ?? "").filter((job) => job.status === "running").length;
782
+ if (tasks + running === 0) return undefined;
783
+ return {
784
+ block: true,
785
+ reason: `Standalone sleep is blocked while background work is active (${tasks} task(s), ${running} job(s)). End the turn now; each completion arrives automatically as a session message and starts a new turn.`,
786
+ };
787
+ });
592
788
  let activeAgentRuns = 0;
593
789
  // ExtensionAPI has no idle probe; ctx.isIdle() is the only one. It is live
594
790
  // and also reports compaction, which activeAgentRuns cannot see. The
595
791
  // session_start context is kept for it and dropped at shutdown; a stale
596
792
  // context throws instead of answering, so delivery fails closed.
597
793
  let parentCtx: ExtensionContext | undefined;
794
+ const helperPermission = new HelperCostPermission(() => parentCtx && activeTransportFor(parentCtx) ? parentCtx : undefined);
795
+ pi.on("session_before_switch", () => helperPermission.clear());
796
+ pi.on("session_before_fork", () => helperPermission.clear());
797
+ pi.on("session_before_tree", () => helperPermission.clear());
798
+ pi.on("model_select", () => helperPermission.clear());
799
+ pi.on("resources_discover", () => helperPermission.clear());
800
+ let bridgeWakeIdentity: BridgeWakeIdentity | undefined;
801
+ let wakeVisibilityWarning = false;
802
+ const restoreBridgeWakeIdentity = (ctx: ExtensionContext | undefined) => {
803
+ bridgeWakeIdentity = undefined;
804
+ if (!ctx) return;
805
+ // Rendering is session-wide, not model/branch state: an identity on an
806
+ // abandoned branch still owns its generated bubbles in the session tree.
807
+ for (const entry of ctx.sessionManager.getEntries()) {
808
+ if (entry.type !== "custom" || entry.customType !== BRIDGE_WAKE_IDENTITY_TYPE) continue;
809
+ const data = entry.data as Partial<BridgeWakeIdentity> | undefined;
810
+ if (data?.sessionId === ctx.sessionManager.getSessionId() && typeof data.nonce === "string"
811
+ && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(data.nonce)
812
+ && data.text === bridgeWakeText(data.nonce)) {
813
+ bridgeWakeIdentity = data as BridgeWakeIdentity;
814
+ break;
815
+ }
816
+ }
817
+ };
818
+ const hasWakeTransformer = typeof pi.registerMarkdownTransformer === "function";
819
+ if (hasWakeTransformer) {
820
+ // Register once in this runtime. Reload replaces the runtime; session
821
+ // replacement changes the identity, never the transformer registration.
822
+ pi.registerMarkdownTransformer((markdown, context) =>
823
+ context.messageType === "user" && bridgeWakeIdentity?.sessionId === parentCtx?.sessionManager.getSessionId()
824
+ && markdown === bridgeWakeIdentity?.text ? "" : markdown);
825
+ }
826
+ const bridgeWake = (): string => {
827
+ if (!parentCtx) return PARENT_WAKE_TEXT;
828
+ try {
829
+ if (!hasWakeTransformer) throw new Error("Markdown transformer API unavailable");
830
+ if (!bridgeWakeIdentity || bridgeWakeIdentity.sessionId !== parentCtx.sessionManager.getSessionId()) {
831
+ const nonce = randomUUID();
832
+ const identity = { sessionId: parentCtx.sessionManager.getSessionId(), nonce, text: bridgeWakeText(nonce) };
833
+ // Persist before sending so a restart can reconstruct exact ownership.
834
+ pi.appendEntry(BRIDGE_WAKE_IDENTITY_TYPE, identity);
835
+ bridgeWakeIdentity = identity;
836
+ }
837
+ return bridgeWakeIdentity.text;
838
+ } catch {
839
+ // Compatibility fallback preserves continuation, not invisibility.
840
+ if (!wakeVisibilityWarning) {
841
+ wakeVisibilityWarning = true;
842
+ try { parentCtx.ui.notify("Gentle Agents cannot hide Claude Bridge continuation on this runtime; the generated user wake remains visible.", "warning"); } catch { /* UI failure must not drop continuation. */ }
843
+ }
844
+ return PARENT_WAKE_TEXT;
845
+ }
846
+ };
598
847
  // Mirrors the host's agent run, which spans agent_start through
599
848
  // agent_settled, including post-run retries and in-run compaction. Unlike
600
849
  // activeAgentRuns it stays set between agent_end and agent_settled, where
@@ -614,6 +863,22 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
614
863
  let promptStarting = false;
615
864
  let cancelPromptGrace: (() => void) | undefined;
616
865
  let cancelBoundaryFlush: (() => void) | undefined;
866
+ // An idle parent has no turn boundary coming, so a failed idle delivery or
867
+ // wake is retried by one bounded timer that runs a full flush.
868
+ let cancelDeliveryRetry: (() => void) | undefined;
869
+ let deliveryRetries = 0;
870
+ const endDeliveryRetry = () => {
871
+ cancelDeliveryRetry?.();
872
+ cancelDeliveryRetry = undefined;
873
+ };
874
+ const armDeliveryRetry = () => {
875
+ if (cancelDeliveryRetry || deliveryRetries >= PARENT_DELIVERY_RETRY_LIMIT) return;
876
+ deliveryRetries += 1;
877
+ cancelDeliveryRetry = deps.schedule(() => {
878
+ cancelDeliveryRetry = undefined;
879
+ flushAll();
880
+ }, PARENT_WAKE_GRACE_MS);
881
+ };
617
882
 
618
883
  const isTaskLive = (id: string): boolean => {
619
884
  const task = store.get(id);
@@ -629,13 +894,10 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
629
894
  // branch, so a parent that keeps calling tools would see the content only
630
895
  // when the whole run ends — the original #867 delay.
631
896
  //
632
- // An idle parent must not get triggerTurn: the host would run the custom
633
- // message as a direct turn that skips the prompt lifecycle
634
- // (before_agent_start and the prompt refresh), and prompt-capture
635
- // integrations such as the Claude bridge reject that turn. The structured
636
- // message is stored durably without a turn instead, and a short
637
- // system-generated user message wakes the parent through the normal prompt
638
- // path. The wake never repeats child content, so the model sees it once.
897
+ // Idle child content is stored durably without a turn, then a separate
898
+ // coalesced wake requests continuation without repeating that content.
899
+ // Claude Bridge needs a user wake through the prompt lifecycle for capture;
900
+ // native providers can use a hidden custom-message turn instead.
639
901
  //
640
902
  // A parent that is busy without a run (compaction, or a prompt's pre-run
641
903
  // compaction) is not streaming, so steer + triggerTurn would also start a
@@ -684,14 +946,31 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
684
946
  // without a run keeps the wake owed until a later boundary flush.
685
947
  if (route !== "idle") return;
686
948
  wakeOwed = false;
949
+ endDeliveryRetry();
687
950
  beginPromptStart();
688
951
  try {
689
952
  // "steer" matters only when a run started in between: the wake is then
690
953
  // queued into it instead of being rejected as a concurrent prompt.
691
- pi.sendUserMessage(PARENT_WAKE_TEXT, { deliverAs: "steer" });
954
+ // Read the live selection at dispatch, not when child content arrived.
955
+ // Only Claude Bridge is currently evidenced to require prompt capture;
956
+ // registering a custom provider alone does not make it a bridge.
957
+ if (parentCtx?.model?.provider === "claude-bridge") {
958
+ pi.sendUserMessage(bridgeWake(), { deliverAs: "steer" });
959
+ } else {
960
+ pi.sendMessage({
961
+ customType: PARENT_WAKE_TYPE,
962
+ content: NATIVE_PARENT_WAKE_TEXT,
963
+ display: false,
964
+ }, { deliverAs: "steer", triggerTurn: true });
965
+ }
966
+ deliveryRetries = 0;
692
967
  } catch {
693
968
  // A stale runtime fails closed instead of throwing from a microtask.
969
+ // The stored content still needs a turn, so the wake stays owed and is
970
+ // retried once per grace, a bounded number of times in a row.
694
971
  endPromptStart();
972
+ wakeOwed = true;
973
+ armDeliveryRetry();
695
974
  }
696
975
  };
697
976
 
@@ -712,12 +991,21 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
712
991
  sendToParent({ customType: AGENTS_RESULT_TYPE, content: completionText(task), display: true, details: taskDetails(task) }, route);
713
992
  };
714
993
 
715
- // A stale completion must not re-enter the LLM conversation, so it is
716
- // delivered as durable TUI-only content and the human still sees it.
717
- const deliverStale = (task: TaskRecord, settledAt: number) => {
994
+ // A stale report must not re-enter the LLM conversation, but the parent is
995
+ // told never to poll: it gets a compact model-facing notice naming the task,
996
+ // and the human gets durable TUI-only content.
997
+ const deliverStale = (task: TaskRecord, settledAt: number, route: Exclude<ParentRoute, "hold">) => {
718
998
  if (activeSessionId() !== task.parentSessionId) return;
719
999
  const ageSeconds = Math.max(0, Math.round((deps.now() - settledAt) / 1000));
720
- pi.appendEntry(AGENTS_STALE_RESULT_TYPE, { taskId: task.id, agent: task.agent, label: task.label, status: task.status, ageSeconds });
1000
+ const status = task.status.replace("_", " ");
1001
+ sendToParent({
1002
+ customType: AGENTS_STALE_NOTICE_TYPE,
1003
+ content: `Subagent ${task.agent} (task ${task.id}, "${task.label}") ${status} about ${ageSeconds}s ago while you were busy. Its report was not replayed here; call subagent_result with task_id ${task.id} to read it.`,
1004
+ display: false,
1005
+ details: taskDetails(task),
1006
+ }, route);
1007
+ // The notice already reached the parent; a failed card must not resend it.
1008
+ try { pi.appendEntry(AGENTS_STALE_RESULT_TYPE, { taskId: task.id, agent: task.agent, label: task.label, status: task.status, ageSeconds }); } catch { /* TUI-only content. */ }
721
1009
  };
722
1010
 
723
1011
  const deliverMessage = (msg: PendingAgentMessage, route: Exclude<ParentRoute, "hold">) => {
@@ -754,17 +1042,48 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
754
1042
  const flushCompletions = () => {
755
1043
  const route = deliveryRoute(false);
756
1044
  if (!route) return;
757
- for (const { task, settledAt, stale } of completions.takeDeliverable(deps.now())) {
1045
+ for (const entry of completions.takeDeliverable(deps.now())) {
1046
+ try {
1047
+ if (entry.stale) deliverStale(entry.task, entry.settledAt, route);
1048
+ else deliver(entry.task, route);
1049
+ } catch {
1050
+ // The parent is told never to poll, so a failed forward must not
1051
+ // lose the completion: it waits for the next boundary instead, and
1052
+ // an idle parent, which has none coming, gets a bounded retry.
1053
+ completions.requeue(entry);
1054
+ if (route === "idle") armDeliveryRetry();
1055
+ }
1056
+ }
1057
+ };
1058
+
1059
+ // A background job's exit notice rides the same router as a completion. It
1060
+ // is compact (no full output), so it has no stale form; a failed forward
1061
+ // requeues it like a completion does.
1062
+ const flushJobNotices = () => {
1063
+ const route = deliveryRoute(false);
1064
+ if (!route) return;
1065
+ const failed: JobNotice[] = [];
1066
+ for (const notice of jobNotices.splice(0)) {
1067
+ const { job } = notice;
1068
+ if (activeSessionId() !== job.ownerSessionId) continue;
1069
+ const content = notice.kind === "events" ? monitorEventsText(job, notice.batch)
1070
+ : notice.kind === "stopped" ? monitorStoppedText(job, notice.reason) : jobNoticeText(job);
1071
+ const event = notice.kind === "events" ? "events" : notice.kind === "stopped" ? notice.reason : undefined;
758
1072
  try {
759
- if (stale) deliverStale(task, settledAt);
760
- else deliver(task, route);
761
- } catch { /* Best-effort delivery: at most once, even if forwarding fails. */ }
1073
+ sendToParent({ customType: JOB_NOTICE_TYPE, content, display: true, details: jobDetails(job, event) }, route);
1074
+ } catch {
1075
+ failed.push(notice);
1076
+ }
762
1077
  }
1078
+ if (failed.length === 0) return;
1079
+ jobNotices.unshift(...failed);
1080
+ if (route === "idle") armDeliveryRetry();
763
1081
  };
764
1082
 
765
1083
  const flushAll = () => {
766
1084
  flushMessages();
767
1085
  flushCompletions();
1086
+ flushJobNotices();
768
1087
  // A wake left owed by an earlier held or expired attempt is retried here.
769
1088
  requestWake();
770
1089
  };
@@ -783,12 +1102,52 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
783
1102
 
784
1103
  // Session changes discard every pending wake and boundary flush.
785
1104
  const resetParentDelivery = (ctx: ExtensionContext | undefined) => {
1105
+ helperPermission.clear(); // Revoke before dropping old callback authority.
786
1106
  parentCtx = ctx;
1107
+ restoreBridgeWakeIdentity(ctx);
1108
+ wakeVisibilityWarning = false;
787
1109
  parentRunActive = false;
788
1110
  wakeOwed = false;
789
1111
  endPromptStart();
790
1112
  cancelBoundaryFlush?.();
791
1113
  cancelBoundaryFlush = undefined;
1114
+ endDeliveryRetry();
1115
+ deliveryRetries = 0;
1116
+ };
1117
+
1118
+ // Structured descriptions feed Status; native hosts keep the compact count.
1119
+ const refreshJobStatus = () => {
1120
+ const sessionId = activeSessionId();
1121
+ const running = jobs.list(sessionId ?? "").filter((job) => job.status === "running")
1122
+ .sort((a, b) => b.startedAt - a.startedAt);
1123
+ try { parentCtx?.ui.setStatus(JOBS_STATUS_KEY, running.length === 0 ? undefined : `${JOB_GLYPH} ${running.length} job${running.length === 1 ? "" : "s"}`); } catch { /* Status is cosmetic. */ }
1124
+ if (sessionId) {
1125
+ try { pi.events.emit(JOBS_SIDEBAR_EVENT, { sessionId, snapshot: { jobs: running.map(({ id, label, startedAt }) => ({ id, label, startedAt })) } }); } catch { /* Display only. */ }
1126
+ }
1127
+ };
1128
+
1129
+ // A job exit settles like a completion below: flushed at once for an idle
1130
+ // parent, at the next turn boundary for a busy one.
1131
+ // Line events still waiting for a busy parent coalesce per monitor.
1132
+ const queueJobNotice = (notice: JobNotice) => {
1133
+ // Merge into this monitor's latest pending notice when that is still
1134
+ // an events notice, so events never move past its own end.
1135
+ let index = -1;
1136
+ for (let i = jobNotices.length - 1; i >= 0; i--) if (jobNotices[i]!.job === notice.job) { index = i; break; }
1137
+ const pending = index >= 0 ? jobNotices[index]! : undefined;
1138
+ if (notice.kind === "events" && pending?.kind === "events") {
1139
+ jobNotices[index] = { ...pending, batch: mergeMonitorBatches(pending.batch, notice.batch) };
1140
+ } else {
1141
+ jobNotices.push(notice);
1142
+ }
1143
+ refreshJobStatus();
1144
+ requestRender();
1145
+ if (activeAgentRuns === 0) flushAll();
1146
+ };
1147
+ const settleJob = (job: JobRecord) => {
1148
+ // A monitor's pending lines are delivered before its end.
1149
+ if (job.kind === "monitor") monitors.finish(job);
1150
+ queueJobNotice({ job, kind: "exit" });
792
1151
  };
793
1152
 
794
1153
  // A completion settles into our queue. An idle parent flushes right away so
@@ -821,6 +1180,8 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
821
1180
  parentRunActive = true;
822
1181
  // The run carries everything stored while the parent was idle.
823
1182
  wakeOwed = false;
1183
+ endDeliveryRetry();
1184
+ deliveryRetries = 0;
824
1185
  endPromptStart();
825
1186
  });
826
1187
  pi.on("agent_end", () => {
@@ -936,6 +1297,8 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
936
1297
  },
937
1298
  });
938
1299
 
1300
+ registerBackgroundJobTools(pi, { registry: jobs, monitors, sessionId: (ctx) => ctx.sessionManager.getSessionId() ?? "", now: deps.now, onChange: () => { refreshJobStatus(); requestRender(); } });
1301
+
939
1302
  pi.registerMessageRenderer(AGENTS_MESSAGE_TYPE, (message, options, theme) => {
940
1303
  const details = (message.details as { gentleAgents?: { taskId?: unknown; agent?: unknown } } | undefined)?.gentleAgents;
941
1304
  const taskId = typeof details?.taskId === "string" ? details.taskId : "unknown";
@@ -946,11 +1309,8 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
946
1309
  });
947
1310
 
948
1311
  pi.registerMessageRenderer(AGENTS_ORCHESTRATOR_MESSAGE_TYPE, (message, options, theme) => {
949
- const details = (message.details as { gentleAgents?: { senderSessionId?: unknown } } | undefined)?.gentleAgents;
950
- const sender = typeof details?.senderSessionId === "string" ? details.senderSessionId : "unknown";
951
- const heading = `⇄ Orchestrator message · Received · From ${sanitizeTerminalText(sender)}`;
952
- const body = sanitizeTerminalText(messageText(message.content));
953
- return new Text(`${theme.fg("customMessageLabel", heading)}\n${theme.fg("customMessageText", body)}`, options.outputPad, 0);
1312
+ const details = (message.details as { gentleAgents?: OrchestratorMessageDetails } | undefined)?.gentleAgents ?? {};
1313
+ return incomingMessageCard(details, messageText(message.content), options.expanded, theme, expandHint(options.expanded));
954
1314
  });
955
1315
 
956
1316
  pi.registerMessageRenderer(AGENTS_RESULT_TYPE, (message, options, theme) => {
@@ -1051,6 +1411,14 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1051
1411
  return stored?.task;
1052
1412
  };
1053
1413
 
1414
+ // A guessed id ("1") leads back to real ids instead of a dead end, so the
1415
+ // parent retries the same call rather than re-summarizing it (gentle-shell#1713).
1416
+ const unknownTask = (id: unknown, ctx: ExtensionContext | undefined) => {
1417
+ const recent = [...store.list(ctx?.sessionManager?.getSessionId() ?? "")].sort((a, b) => b.createdAt - a.createdAt).slice(0, 5);
1418
+ const hint = recent.length ? ` Recent task ids: ${recent.map((task) => `${task.id} (${task.agent})`).join(", ")}.` : "";
1419
+ return text(`Error: no task ${String(id)}.${hint}`, { error: "unknown task" });
1420
+ };
1421
+
1054
1422
  // Restores this exact session's own finished subagents as visible history
1055
1423
  // on an explicit resume, or on startup into a session that already has
1056
1424
  // entries -- never on new, fork, or reload (see the session_start handler's
@@ -1065,12 +1433,12 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1065
1433
  } catch {
1066
1434
  return;
1067
1435
  }
1068
- // The session may have moved on while disk was read; a stale restore
1069
- // must never land in the wrong session's store.
1070
- if (ctx.sessionManager.getSessionId() !== sessionId) return;
1071
- // Fire-and-forget from session_start: a throwing summary subscriber must
1072
- // never surface as an unhandled rejection. History is best-effort.
1436
+ // Fire-and-forget from session_start: a stale SDK context or throwing
1437
+ // summary subscriber must never surface as an unhandled rejection.
1073
1438
  try {
1439
+ // The session may have moved on while disk was read; a stale restore
1440
+ // must never land in the wrong session's store.
1441
+ if (ctx.sessionManager.getSessionId() !== sessionId) return;
1074
1442
  for (const { task, thread } of history) {
1075
1443
  if (task.parentSessionId !== sessionId) continue;
1076
1444
  if (store.restore(task, thread)) restoredTaskIds.add(task.id);
@@ -1188,7 +1556,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1188
1556
  const buildRequest = async (ctx: ExtensionContext, agent: AgentDefinition, prompt: string, label: string | undefined, context: string | undefined, mode: AgentMode, resume?: string, workspaceRoot?: string, signal?: AbortSignal, repositoryRoot?: string): Promise<TaskRequest> => {
1189
1557
  if (retiredSddAgent(agent.name)) throw new Error("Retired SDD agents cannot be dispatched.");
1190
1558
  if (![AGENT_MODE.TASK, AGENT_MODE.BACKGROUND].includes(mode)) throw new Error("Subagent mode must be task or background.");
1191
- if (ctx.mode === "print" && mode === AGENT_MODE.BACKGROUND) throw new Error("Background subagents are unavailable in print mode: pi -p exits before a parent session can receive results. Use task mode, RPC mode, or interactive Pi.");
1559
+ if (isSingleShotMode(ctx.mode) && mode === AGENT_MODE.BACKGROUND) throw new Error(SINGLE_SHOT_BACKGROUND_ERROR);
1192
1560
  const scopeDenied = rejectUnscopedBoundedWriterDispatch({ agent: agent.name, task: prompt, context });
1193
1561
  if (scopeDenied) throw new Error(scopeDenied.reason);
1194
1562
  if (signal?.aborted) throw new Error("Subagent launch aborted before authorization.");
@@ -1201,10 +1569,27 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1201
1569
  if (isGenericBoundedWriter(agent.name) && !current()) throw new Error("Writer session Git authority changed before admission.");
1202
1570
  let admittedModel: string | undefined;
1203
1571
  const surfaces = allowedEditSurfaces(prompt, context);
1572
+ // gentle-shell#1064 slice 2: the session profile is resolved once per task
1573
+ // request, before admission, so the admitted model and the launch routing
1574
+ // resolve the same session layer and can never disagree about it (#1558: a
1575
+ // bound session used to kill its own non-git writer mid-preparation because
1576
+ // admission still read only the pin/global layers). Slice 3b-ii: the same
1577
+ // read covers the frozen inherited profile and `follow` mode.
1578
+ const profileConfigHome = gentlePiConfigHome(deps.env);
1579
+ const sessionProfile = resolveSessionProfile({
1580
+ sessionId: ctx.sessionManager.getSessionId(),
1581
+ cwd: originalCwd,
1582
+ configHome: profileConfigHome,
1583
+ resolveWorktree: deps.resolveWorktree,
1584
+ env: deps.env,
1585
+ });
1204
1586
  if (!resume && isGenericBoundedWriter(agent.name) && surfaces?.some(isDevelopmentSurface) && !deps.resolveWorktree(originalCwd, originalCwd) && repositoryRoot === undefined) {
1205
1587
  const root = safeBootstrapDirectory(originalCwd);
1206
1588
  if (!root || (workspaceRoot !== undefined && (!isAbsolute(workspaceRoot) || safeBootstrapDirectory(workspaceRoot) !== root))) throw new Error("Writer bootstrap requires the original safe project root.");
1207
- const config = withPinnedModelProfiles(loadAgentsConfig(roots(ctx)), resolveUnversionedProjectProfile(root, gentlePiConfigHome(deps.env))?.modelProfiles);
1589
+ const config = withPinnedModelProfiles(
1590
+ loadAgentsConfig(roots(ctx)),
1591
+ sessionProfileRoutingAt(sessionProfile, { cwd: root, configHome: profileConfigHome, resolveWorktree: deps.resolveWorktree }),
1592
+ );
1208
1593
  const model = resolveAgentProfile(agent, config).model ?? ctx.model;
1209
1594
  const catalogModel = model?.provider ? ctx.modelRegistry?.find(model.provider, model.id) : ctx.modelRegistry?.getAll().find(candidate => candidate.id === model?.id);
1210
1595
  if (!catalogModel) throw new Error("Writer bootstrap requires a valid effective model in this session's catalog.");
@@ -1249,14 +1634,34 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1249
1634
  const pinIdentity: WorktreeResolver = foreign ? resolveSessionWorktree : target !== undefined && parentIdentity !== undefined && target === parentIdentity.root
1250
1635
  ? () => parentIdentity
1251
1636
  : deps.resolveWorktree;
1637
+ // gentle-shell#1064: the session profile outranks the shared defaults for
1638
+ // launches from that session (`session → frozen p → P → G`), with the same
1639
+ // wholesale-replacement contract as the pin. Explicit and frozen profiles
1640
+ // belong to the session and apply to every target, a foreign repository
1641
+ // included; only `follow` reads the defaults of the target. The profile is
1642
+ // resolved here, at task-request creation, so queued and running children
1643
+ // keep the routing frozen into their requests even if the session rebinds.
1252
1644
  const config = withPinnedModelProfiles(
1253
1645
  loadAgentsConfig(roots(ctx)),
1254
- resolveProfilePin({
1646
+ sessionProfileRoutingAt(sessionProfile, {
1255
1647
  cwd: target ?? parentCwd,
1256
- configHome: gentlePiConfigHome(deps.env),
1648
+ configHome: profileConfigHome,
1257
1649
  resolveWorktree: pinIdentity,
1258
- })?.modelProfiles,
1650
+ foreignRepository: foreign,
1651
+ }),
1259
1652
  );
1653
+ // Drift is measured against the defaults of the session's own directory,
1654
+ // where the profile was frozen, never against a delegated target.
1655
+ if (ctx.hasUI) {
1656
+ const drift = inheritedProfileDriftNotice({
1657
+ sessionId: ctx.sessionManager.getSessionId(),
1658
+ cwd: parentCwd,
1659
+ configHome: profileConfigHome,
1660
+ resolveWorktree: deps.resolveWorktree,
1661
+ env: deps.env,
1662
+ });
1663
+ if (drift !== undefined) ctx.ui.notify(drift, "info");
1664
+ }
1260
1665
  const profile = resolveAgentProfile(agent, config);
1261
1666
  if (admittedModel !== undefined) {
1262
1667
  const model = profile.model ?? ctx.model;
@@ -1301,7 +1706,10 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1301
1706
  thinking: profile.thinking,
1302
1707
  sessionDir,
1303
1708
  resumeSessionPath: resume,
1304
- ...(deps.childExtensionPaths && deps.childExtensionPaths.length > 0 ? { extensionPaths: [...deps.childExtensionPaths] } : {}),
1709
+ ...childExtensionRequest(deps.env, deps.childExtensionPaths),
1710
+ // A writer claims its surfaces in its canonical worktree root for its queued
1711
+ // and running lifetime; a continuation is a new task and claims them again.
1712
+ ...(isBoundedWriter(agent.name) && surfaces ? { writerSurfaces: surfaces, writerRoot: canonicalWriterRoot(target ?? parentWorktreeRoot, foreign ? resolveSessionWorktree : deps.resolveWorktree) } : {}),
1305
1713
  env: childEnv,
1306
1714
  ...(foreign || parentRepositoryIdentity === undefined ? {} : {
1307
1715
  authorizeParentStandingReviewPermission: (repositoryIdentity: string) => {
@@ -1325,10 +1733,8 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1325
1733
  return request;
1326
1734
  };
1327
1735
 
1328
- const launch = async (ctx: ExtensionContext, request: TaskRequest, signal?: AbortSignal): Promise<ToolText> => {
1329
- if (ctx.mode === "print" && request.mode === AGENT_MODE.BACKGROUND) {
1330
- throw new Error("Background subagents are unavailable in print mode: pi -p exits before a parent session can receive results. Use task mode, RPC mode, or interactive Pi.");
1331
- }
1736
+ const launch = async (ctx: ExtensionContext, request: TaskRequest, signal?: AbortSignal, publishWork?: (id: string) => void): Promise<ToolText> => {
1737
+ if (isSingleShotMode(ctx.mode) && request.mode === AGENT_MODE.BACKGROUND) throw new Error(SINGLE_SHOT_BACKGROUND_ERROR);
1332
1738
  if (retiredSddAgent(request.agent.name)) throw new Error("Retired SDD agents cannot be dispatched.");
1333
1739
 
1334
1740
  // Bounded live observation only. Native send owns the fresh policy decision;
@@ -1362,6 +1768,8 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1362
1768
  const foreignRequest = foreignRequests.get(request);
1363
1769
  if (launched && foreignRequest) foreignTasks.set(task.id, foreignRequest);
1364
1770
  ownedTaskIds.add(task.id);
1771
+ publishWork?.(task.id);
1772
+ publishActivity(); // Admission's summary notification precedes runtime ownership.
1365
1773
  store.subscribe(task.id, () => { publishActivity(); requestRender(); });
1366
1774
  if (request.mode === AGENT_MODE.BACKGROUND) return text(`Started ${task.agent} in the background as task ${task.id}. Retain that id; completion is pushed automatically. Never sleep or periodically poll subagent_status/subagent_result for completion or cache maintenance. Inspect status only at a real orchestration decision boundary; never relaunch equivalent queued/running work.`, taskDetails(task));
1367
1775
  // A tool call aborted by the host (a human interrupting the turn, a timeout)
@@ -1393,7 +1801,9 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1393
1801
  const finished = await runner.waitFor(task.id);
1394
1802
  completions.consume(finished.id);
1395
1803
  messages.invalidateTask(finished.id);
1396
- return text(finishedText(finished), taskDetails(finished));
1804
+ // The id rides in the text too: details never reach the model, which
1805
+ // otherwise guesses ordinal ids for subagent_continue (#1731 T19).
1806
+ return text(completionText(finished), taskDetails(finished));
1397
1807
  } finally {
1398
1808
  signal?.removeEventListener("abort", onAbort);
1399
1809
  }
@@ -1419,7 +1829,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1419
1829
  return { render: () => [], invalidate() {} };
1420
1830
  }
1421
1831
  const body = result.content.map((part) => (part.type === "text" ? part.text : "")).join("\n");
1422
- const visibleBody = options.expanded ? body : theme.fg("muted", body.split("\n")[0] ?? "");
1832
+ const visibleBody = options.expanded ? body : theme.fg("muted", agentResultPreview(body).split("\n")[0] ?? "");
1423
1833
  const title = `${AGENTS_GLYPH} agent result${task?.taskId ? ` · ${sanitizeTerminalText(task.taskId)}` : ""}`;
1424
1834
  return new Text(name === "result" ? `${theme.fg("toolTitle", title)}\n${visibleBody}` : visibleBody, 0, 0);
1425
1835
  },
@@ -1432,25 +1842,150 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1432
1842
  pi.registerTool({
1433
1843
  name: "orchestrator_session_id",
1434
1844
  label: "Orchestrator session ID",
1435
- description: "Return this host session's active ID.",
1436
- parameters: { type: "object", additionalProperties: false, properties: {} } as never,
1437
- async execute(_id, _params, _signal, _onUpdate, ctx) {
1845
+ description: "Return this host session's stable routing ID, Initial alias (FIRST TASK), Current alias (CURRENT TASK), and separate human Session name. Explicit subjects update Current even when named; Initial stays the first declared branch topic, unknown for legacy history. Before delegation or cross-session coordination, declare a short recognizable subject here. Do not require a subject declaration for small direct tasks; do not query all peers. Names never authenticate. Existing Pi names and human renames are preserved. Use a concise non-sensitive label, not a prompt. For work beyond small direct tasks, publish subject and current/next status here; refresh at task changes/completion. Owner-curated state (2048 UTF-8 bytes total): null withdraws, omission leaves unchanged. Never include credentials, internal instructions, or raw prompts. Historical notes are not consent or an owner reply.",
1846
+ ...orchestratorToolRenderers("session", expandHint),
1847
+ parameters: { type: "object", additionalProperties: false, properties: {
1848
+ subject: { type: "string", maxLength: 120, description: "Optional short task subject; updates task aliases independently of human names. Also names an unnamed Pi session." },
1849
+ state: { anyOf: [{ type: "null" }, { type: "object", additionalProperties: false, properties: {
1850
+ objective: { type: "string" }, progress: { type: "string" }, decisions: { type: "string" }, blockers: { type: "string" },
1851
+ work: { type: "object", additionalProperties: false,
1852
+ description: "Optional historical, non-authoritative classification; topic requires area. Text plus work JSON fits 2048 UTF-8 bytes. Exact duplicate tags/refs are rejected; no ref resolution or routing.",
1853
+ properties: {
1854
+ ...workDescriptorSchema.properties,
1855
+ tasks: { type: "object", maxProperties: 8, propertyNames: { type: "string", maxLength: 256 },
1856
+ additionalProperties: workDescriptorSchema,
1857
+ description: "Exact actual owner-declared task IDs, at most 256 UTF-8 bytes; controls, surrogates and __proto__/prototype/constructor rejected. Root-only; nonempty tasks alone are valid. Replacement omitting tasks clears annotations." },
1858
+ },
1859
+ },
1860
+ } }] },
1861
+ } } as never,
1862
+ async execute(_id, params, _signal, _onUpdate, ctx) {
1438
1863
  const transport = activeTransportFor(ctx);
1439
- return transport ? text(`Active session ID: ${transport.sessionId}`, { gentleAgents: { senderSessionId: transport.sessionId } }) : text("Error: session messaging is not ready.", { error: "not ready" });
1864
+ if (!transport) return text("Error: session messaging is not ready.", { error: "not ready" });
1865
+ const { subject, state } = params as { subject?: unknown; state?: unknown };
1866
+ const declared = normalizeTaskSubject(subject);
1867
+ if (state !== undefined || declared) stateCache.publish(ctx.sessionManager, state, (type, data) => pi.appendEntry(type, data), Date.now(), declared);
1868
+ if (activeTransportFor(ctx) !== transport) throw new Error("stale-published-state");
1869
+ if (declared && !ctx.sessionManager.getSessionName?.()) pi.setSessionName(declared);
1870
+ const alias = sanitizeDisplayLabel(ctx.sessionManager.getSessionName?.() ?? "");
1871
+ const aliases = stateCache.get(ctx.sessionManager)?.aliases;
1872
+ if (state !== undefined || declared) publishActivity();
1873
+ presence?.refreshLabel();
1874
+ return text(`Active session ID: ${transport.sessionId}\nCurrent alias (CURRENT TASK): ${aliases?.currentAlias ?? "unknown"}\nInitial alias (FIRST TASK): ${aliases?.initialAlias ?? "unknown"}\nSession name: ${alias || "unnamed"}`,
1875
+ { gentleAgents: { senderSessionId: transport.sessionId, alias, sessionName: alias, initialAlias: aliases?.initialAlias ?? null, currentAlias: aliases?.currentAlias ?? null } });
1876
+ },
1877
+ });
1878
+ pi.registerTool({
1879
+ name: "orchestrator_consult",
1880
+ label: "Consult published context",
1881
+ ...orchestratorToolRenderers("consult", expandHint),
1882
+ description: "Read published metadata (default), request bounded helper reasoning with explicit UI model-cost permission and a question, or revoke-reasoning for an exact target. Never an owner reply, consent or private context access.",
1883
+ parameters: { type: "object", additionalProperties: false, required: ["recipient_session_id"], properties: {
1884
+ kind: { type: "string", enum: ["metadata", "reasoning", "revoke-reasoning"] },
1885
+ question: { type: "string", maxLength: 1024 },
1886
+ recipient_session_id: { type: "string", pattern: "^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$" },
1887
+ cursor: { type: "string", maxLength: 1024 },
1888
+ } } as never,
1889
+ async execute(_id, params, _signal, _onUpdate, ctx) {
1890
+ const input = params as { kind?: unknown; recipient_session_id?: unknown; cursor?: unknown; question?: unknown };
1891
+ const kind = input?.kind === undefined ? "metadata" : input.kind;
1892
+ if (!input || Object.keys(input).some(k => !["kind", "recipient_session_id", "cursor", "question"].includes(k))
1893
+ || !["metadata", "reasoning", "revoke-reasoning"].includes(kind as string) || !validTransportSessionId(input.recipient_session_id)
1894
+ || (kind === "revoke-reasoning" && input.cursor !== undefined)
1895
+ || (kind !== "reasoning" && input.question !== undefined)
1896
+ || (kind === "reasoning" && (typeof input.question !== "string" || !input.question.trim()
1897
+ || Buffer.byteLength(input.question) > 1024 || /[\p{Cc}\p{Cf}\p{Cs}]/u.test(input.question)))
1898
+ || (input.cursor !== undefined && (typeof input.cursor !== "string" || input.cursor.length > 1024))) throw new Error("Invalid metadata consultation parameters.");
1899
+ const selection = { recipientSessionId: input.recipient_session_id, cursor: input.cursor as string | undefined };
1900
+ const transport = activeTransportFor(ctx);
1901
+ const result = (receipt: MetadataReceipt) => {
1902
+ const output = kind === "metadata" ? receipt : { status: "unavailable", code: receipt.unknowns[0], source: "helper_advice", ownerReply: false, authority: "none" };
1903
+ return text(JSON.stringify(output), { gentleAgents: { senderSessionId: transport?.sessionId, receipt: output } });
1904
+ };
1905
+ if (!transport) return result(unavailableMetadata(selection.recipientSessionId, "not-ready"));
1906
+ const reply = (receipt: unknown) => text(JSON.stringify(receipt), { gentleAgents: { senderSessionId: transport.sessionId, receipt } });
1907
+ if (kind === "revoke-reasoning") {
1908
+ helperPermission.revoke(selection.recipientSessionId);
1909
+ return reply({ status: "revoked", source: "helper_advice", ownerReply: false, authority: "none" });
1910
+ }
1911
+ try {
1912
+ const activations = await transport.listener.registry.listActivations(transport.sessionId);
1913
+ if (activeTransportFor(ctx) !== transport) return result(unavailableMetadata(selection.recipientSessionId, "source-session-changed"));
1914
+ const receipt = consultPublishedMetadata(agentHome, activations, selection);
1915
+ if (kind === "metadata") return result(receipt);
1916
+ const current = () => activeTransportFor(ctx) === transport && parentCtx?.sessionManager === ctx.sessionManager;
1917
+ const readSource = async () => {
1918
+ const peers = await transport.listener.registry.listActivations(transport.sessionId);
1919
+ return current() ? consultPublishedMetadata(agentHome, peers, selection)
1920
+ : unavailableMetadata(selection.recipientSessionId, "source-session-changed");
1921
+ };
1922
+ const advice = await helperPermission.run({ receipt, question: input.question as string, signal: _signal,
1923
+ readSource, isSourceCurrent: () => current() && consultPublishedMetadata(agentHome, activations, selection).digest === receipt.digest });
1924
+ return reply(advice);
1925
+ } catch { return result(unavailableMetadata(selection.recipientSessionId, "discovery-unavailable")); }
1440
1926
  },
1441
1927
  });
1442
1928
  pi.registerTool({
1443
1929
  name: "orchestrator_list",
1444
1930
  label: "List orchestrators",
1445
- description: "List other sessions advertised by the trusted local profile. Advertised reachability is unknown and does not prove a session is live.",
1446
- parameters: { type: "object", additionalProperties: false, properties: {} } as never,
1931
+ ...orchestratorToolRenderers("list", expandHint),
1932
+ description: "List other sessions advertised by the trusted local profile. Optional filter searches only published classified work with bounded, non-exhaustive coverage, no authority and unknown reachability. Omit filter for the existing session list.",
1933
+ parameters: Type.Object({
1934
+ recipient_session_id: Type.Optional(Type.String({ description: "Exact routing ID of the peer to inspect." })),
1935
+ cursor: Type.Optional(Type.String({ description: "Opaque catalog continuation from that peer; requires recipient_session_id." })),
1936
+ filter: Type.Optional(Type.Object({
1937
+ area: Type.Optional(Type.String()),
1938
+ topic: Type.Optional(Type.String({ description: "Requires area." })),
1939
+ tag: Type.Optional(Type.String()),
1940
+ text: Type.Optional(Type.String({ description: "Literal label/descriptor search, never state prose or history." })),
1941
+ ref: Type.Optional(Type.Object({
1942
+ kind: Type.Union([Type.Literal("issue"), Type.Literal("pr"), Type.Literal("task")]),
1943
+ repository: Type.String({ description: "Exact public host/owner/repo scope." }),
1944
+ id: Type.String(),
1945
+ }, { additionalProperties: false })),
1946
+ repository_root: Type.Optional(Type.String({ description: "Recorded absolute Git root; no new Git probe." })),
1947
+ related_to: Type.Optional(Type.Object({
1948
+ session_id: Type.String({ description: "Exact stable source owner session ID." }),
1949
+ task_id: Type.Optional(Type.String({ description: "Actual task ID on the current catalog page, not child session ID." })),
1950
+ }, { additionalProperties: false })),
1951
+ }, { additionalProperties: false, description: "Explicit {} indexes classified work; criteria combine with AND." })),
1952
+ }, { additionalProperties: false }),
1447
1953
  async execute(_id, _params, _signal, _onUpdate, ctx) {
1954
+ const params = _params as { recipient_session_id?: string; cursor?: string; filter?: unknown };
1955
+ let filter;
1956
+ try {
1957
+ if (!params || typeof params !== "object" || Array.isArray(params)
1958
+ || Object.keys(params).some(key => !["recipient_session_id", "cursor", "filter"].includes(key))
1959
+ || (params.recipient_session_id !== undefined && !validTransportSessionId(params.recipient_session_id))
1960
+ || (params.cursor !== undefined && (typeof params.cursor !== "string" || params.cursor.length > 1024))) throw new Error();
1961
+ if (Object.hasOwn(params, "filter")) filter = validateWorkFilter(params.filter);
1962
+ } catch { throw new Error("Invalid orchestrator list parameters."); }
1963
+ if (params.cursor !== undefined && !params.recipient_session_id) return text("Error: cursor requires recipient_session_id.", { error: "invalid-cursor" });
1448
1964
  const transport = activeTransportFor(ctx);
1449
1965
  if (!transport) return text("Error: session discovery is not ready.", { error: "not ready" });
1450
1966
  try {
1451
- const peers = await transport.listener.registry.list(transport.sessionId);
1967
+ const activations = await transport.listener.registry.listActivations(transport.sessionId);
1452
1968
  if (activeTransportFor(ctx) !== transport) return text("Error: session discovery became unavailable before results were confirmed.", { error: "stale" });
1453
- return peers.length === 0 ? text("No other sessions are currently advertised. Advertisements have unknown reachability and do not guarantee a live session.") : text(`Advertised sessions (reachability is unknown):\n${peers.map((peer) => `- ${peer.sessionId}`).join("\n")}`, { gentleAgents: { candidates: peers } });
1969
+ if (filter !== undefined) {
1970
+ const workSearch = searchPublishedWork(agentHome, activations, filter, params.recipient_session_id
1971
+ ? { recipientSessionId: params.recipient_session_id, cursor: params.cursor } : undefined);
1972
+ return text(JSON.stringify(workSearch), { gentleAgents: { workSearch } });
1973
+ }
1974
+ const peers = discoverOrchestrators(agentHome, activations, Date.now(), params.recipient_session_id
1975
+ ? { recipientSessionId: params.recipient_session_id, cursor: params.cursor } : undefined);
1976
+ const repository = (fact?: RepositoryFact) => fact?.root
1977
+ ? `repository: ${fact.root} · clone: ${fact.cloneHash} · Git resolved at: ${fact.resolvedAt} (${fact.source})`
1978
+ : "repository: unknown";
1979
+ const rows = peers.map(peer => {
1980
+ const context = peer.freshness === "recent" ? ` · ${peer.aliases?.currentAlias || peer.label || "unnamed"} · Session name: ${peer.label || "unnamed"} · Current alias (CURRENT TASK): ${peer.aliases?.currentAlias ?? "unknown"} · Initial alias (FIRST TASK): ${peer.aliases?.initialAlias ?? "unknown"} · recorded workspace: ${peer.workspace || "unknown"}` : " · context: unknown";
1981
+ const tasks = peer.tasks?.map(task => `\n - ${task.label || task.id} [${task.status}] · launch workspace: ${task.workspace || "unknown"} · ${repository(peer.scope?.tasks.find(t => t.id === task.id)?.repository)}`).join("") ?? "";
1982
+ const registered = peer.scope?.registered.map(fact => `\n registered: ${repository(fact)}`).join("") ?? "";
1983
+ const gaps = peer.scope && !peer.scope.complete ? `\n (scope incomplete: ${peer.scope.omittedTasks} tasks, ${peer.scope.omittedRegistered} registered roots omitted)` : "";
1984
+ const catalog = peer.catalog ? `\n recorded catalog (not Git identity): ${JSON.stringify(peer.catalog)}` : "\n recorded catalog: unknown";
1985
+ const note = params.recipient_session_id ? `\n owner-curated recorded state (not consent or owner reply; authority none): ${peer.state ? JSON.stringify(peer.state) : "unknown"}` : "";
1986
+ return `- ${peer.sessionId}${context} · ${repository(peer.scope?.host)} · metadata: ${peer.freshness}${tasks}${registered}${gaps}${catalog}${note}${peer.omitted ? `\n (${peer.omitted} more tasks omitted)` : ""}`;
1987
+ });
1988
+ return peers.length === 0 ? text("No other sessions are currently advertised. Advertisements have unknown reachability and do not guarantee a live session.") : text(`Advertised sessions (reachability is unknown):\n${rows.join("\n")}`, { gentleAgents: { candidates: peers } });
1454
1989
  } catch {
1455
1990
  return text("Error: session discovery is unavailable.", { error: "unavailable" });
1456
1991
  }
@@ -1459,6 +1994,13 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1459
1994
  pi.registerTool({
1460
1995
  name: "orchestrator_send_message",
1461
1996
  label: "Send orchestrator message",
1997
+ renderShell: "self",
1998
+ renderCall(args, theme, context) {
1999
+ return outgoingMessageCall(args as { message?: unknown }, theme, context, expandHint(context.expanded));
2000
+ },
2001
+ renderResult(result, options, theme, context) {
2002
+ return outgoingMessageResult(result, options.expanded, options.isPartial, theme, context);
2003
+ },
1462
2004
  description: "Send a notification to another active session in this trusted local profile. If recipient_session_id is omitted, the sole peer is selected or the user selects one. Acceptance means enqueued, not read or completed.",
1463
2005
  parameters: {
1464
2006
  type: "object",
@@ -1517,16 +2059,16 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1517
2059
  }
1518
2060
  try {
1519
2061
  const accepted = await transport.client.sendNotification(recipient, message, { signal, expectedActivation: activation, beforeConnect: () => activeTransportFor(ctx) === transport });
1520
- return activeTransportFor(ctx) === transport ? text(`Message ${accepted.id} from ${transport.sessionId} to ${recipient} accepted for delivery; it is not a delivery or read receipt.`, { gentleAgents: { messageId: accepted.id, senderSessionId: transport.sessionId, recipientSessionId: recipient, state: "accepted" } }) : text("Error: session messaging is not ready.", { error: "stale" });
2062
+ return activeTransportFor(ctx) === transport ? text(`Message ${accepted.id} from ${transport.sessionId} to ${recipient} accepted for delivery; it is not a delivery or read receipt.`, { gentleAgents: { messageId: accepted.id, senderSessionId: transport.sessionId, recipientSessionId: recipient, state: "accepted", message, reason, senderLabel: sanitizeDisplayLabel(ctx.sessionManager.getSessionName?.() ?? ""), recipientLabel: messagePeerLabel(agentHome, recipient) } }) : text("Error: session messaging is not ready.", { error: "stale" });
1521
2063
  } catch {
1522
2064
  return text("Error: session message was not accepted.", { error: "not accepted" });
1523
2065
  }
1524
2066
  },
1525
2067
  });
1526
2068
 
1527
- tool("list_agents", "List the subagents defined for this project and user, with their descriptions.", { properties: {} }, async (_params, ctx) => {
2069
+ tool("list_agents", "List the subagents defined for this project and user, with their descriptions and declared tool inventory.", { properties: {} }, async (_params, ctx) => {
1528
2070
  const { agents, errors } = discoverAgents(roots(ctx));
1529
- const lines = agents.map((agent) => `- ${agent.name} (${agent.scope}): ${agent.description || "no description"}`);
2071
+ const lines = agents.map((agent) => `- ${agent.name} (${agent.scope}): ${agent.description || "no description"} ${declaredToolInventory(agent)}`);
1530
2072
  const problems = errors.map((error) => `! ${error}`);
1531
2073
  return text(lines.length === 0 ? "No subagents defined." : [...lines, ...problems].join("\n"));
1532
2074
  });
@@ -1541,12 +2083,15 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1541
2083
  task: { type: "string", description: "What the subagent must do, self-contained." },
1542
2084
  label: { type: "string", description: "Three to six words naming the work, shown on the agents card, e.g. 'map footer data sources'." },
1543
2085
  context: { type: "string", description: "Optional extra context appended to the task." },
2086
+ work: workDescriptorSchema,
1544
2087
  workspace_root: { type: "string", description: "Optional canonical main or linked Git worktree within the parent's same clone only; mutually exclusive with repository_root." },
1545
2088
  repository_root: { type: "string", description: "Optional canonical independent Git repository; requires direct interactive session-scoped consent before queueing; mutually exclusive with workspace_root." },
1546
2089
  mode: { type: "string", enum: ["task", "background"], description: "task waits for the result (default); background returns immediately." },
1547
2090
  },
1548
2091
  },
1549
2092
  async (params, ctx, signal) => {
2093
+ const work = Object.hasOwn(params, "work") ? decodeWorkDescriptor(params.work) : undefined;
2094
+ const manager = ctx.sessionManager, sessionId = manager.getSessionId();
1550
2095
  if (typeof params.agent !== "string" || !params.agent.trim() || typeof params.task !== "string" || !params.task.trim() || (params.context !== undefined && typeof params.context !== "string") || (params.label !== undefined && typeof params.label !== "string")) throw new Error("Subagent dispatch requires a named agent, non-empty task and string context/label.");
1551
2096
  if (params.mode !== undefined && params.mode !== AGENT_MODE.TASK && params.mode !== AGENT_MODE.BACKGROUND) throw new Error("Subagent mode must be task or background.");
1552
2097
  // An empty selector names no destination: only real roots are mutually
@@ -1566,18 +2111,33 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1566
2111
  });
1567
2112
  const workspaceRoot = typeof params.workspace_root === "string" && params.workspace_root !== "" ? params.workspace_root : undefined;
1568
2113
  const repositoryRoot = typeof params.repository_root === "string" && params.repository_root !== "" ? params.repository_root : undefined;
1569
- return launch(ctx, await buildRequest(ctx, agent, String(params.task ?? ""), typeof params.label === "string" ? params.label : undefined, typeof params.context === "string" ? params.context : undefined, mode, undefined, workspaceRoot, signal, repositoryRoot), signal);
2114
+ const publication: { status: "recorded" | "unavailable" } = { status: "unavailable" };
2115
+ const current = () => ctx.sessionManager === manager && manager.getSessionId() === sessionId && !!activeTransportFor(ctx);
2116
+ const result = await launch(ctx, await buildRequest(ctx, agent, String(params.task ?? ""), typeof params.label === "string" ? params.label : undefined, typeof params.context === "string" ? params.context : undefined, mode, undefined, workspaceRoot, signal, repositoryRoot), signal, work ? id => {
2117
+ try {
2118
+ if (!current()) return;
2119
+ const state = stateCache.get(manager)?.state ?? {};
2120
+ stateCache.publish(manager, { ...state, work: { ...state.work, tasks: { ...state.work?.tasks, [id]: work } } }, (type, data) => pi.appendEntry(type, data));
2121
+ if (current()) publication.status = "recorded";
2122
+ } catch { /* Optional metadata cannot invalidate an allocated task. */ }
2123
+ } : undefined);
2124
+ if (!work) return result;
2125
+ if (!current()) publication.status = "unavailable";
2126
+ const note = publication.status === "recorded"
2127
+ ? "Work recorded in local curated state; peer advertisement is best-effort."
2128
+ : "Work publication unavailable/unknown. Do not relaunch this allocated task. Use orchestrator_session_id with a bounded replacement state and this actual task ID when the owner session is active.";
2129
+ return { ...result, content: [...result.content, { type: "text", text: note }], details: { ...result.details, workPublication: publication } };
1570
2130
  },
1571
2131
  );
1572
2132
 
1573
- tool("status", "Report the status of one subagent task.", { required: ["task_id"], properties: { task_id: { type: "string" } } }, async (params) => {
2133
+ tool("status", "Report the status of one subagent task.", { required: ["task_id"], properties: { task_id: { type: "string" } } }, async (params, ctx) => {
1574
2134
  const task = await resolveTask(String(params.task_id));
1575
- return task ? text(describeTask(task), taskDetails(task)) : text(`Error: no task ${String(params.task_id)}`, { error: "unknown task" });
2135
+ return task ? text(describeTask(task), taskDetails(task)) : unknownTask(params.task_id, ctx);
1576
2136
  });
1577
2137
 
1578
- tool("result", "Return the final answer of a finished subagent task, or its current state if it is still running.", { required: ["task_id"], properties: { task_id: { type: "string" } } }, async (params) => {
2138
+ tool("result", "Return the final answer of a finished subagent task, or its current state if it is still running.", { required: ["task_id"], properties: { task_id: { type: "string" } } }, async (params, ctx) => {
1579
2139
  const task = await resolveTask(String(params.task_id));
1580
- if (!task) return text(`Error: no task ${String(params.task_id)}`, { error: "unknown task" });
2140
+ if (!task) return unknownTask(params.task_id, ctx);
1581
2141
  // The parent just pulled a finished result; its pending completion must
1582
2142
  // never be replayed on top of it.
1583
2143
  if (isFinished(task.status)) {
@@ -1617,7 +2177,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1617
2177
  async (params, ctx, signal) => {
1618
2178
  if (Object.hasOwn(params, "sdd_change") || Object.hasOwn(params, "remediation") || Object.hasOwn(params, "research_selection")) return text("Error: retired SDD delegation is not supported.", { error: "retired SDD delegation" });
1619
2179
  const previous = await resolveTask(String(params.task_id));
1620
- if (!previous) return text(`Error: no task ${String(params.task_id)}`, { error: "unknown task" });
2180
+ if (!previous) return unknownTask(params.task_id, ctx);
1621
2181
  if (retiredSddAgent(previous.agent)) return text("Error: retired SDD agents cannot be continued.", { error: "retired SDD delegation" });
1622
2182
  if (!isFinished(previous.status) || !previous.sessionPath) return text(`Error: task ${previous.id} cannot be continued yet (${previous.status}).`, { error: "not continuable" });
1623
2183
  // Continuing acts on the previous result, so any pending completion for
@@ -1628,7 +2188,8 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1628
2188
  if (!agent) return text(`Error: subagent "${previous.agent}" is no longer defined.`, { error: "unknown agent" });
1629
2189
  const mode = (params.mode as AgentMode | undefined) ?? (previous.mode as AgentMode);
1630
2190
  const foreignContinuation = foreignTasks.has(previous.id);
1631
- return launch(ctx, await buildRequest(ctx, agent, String(params.prompt ?? ""), typeof params.label === "string" ? params.label : undefined, typeof params.context === "string" ? params.context : undefined, mode, previous.sessionPath, foreignContinuation ? undefined : previous.cwd, signal, foreignContinuation ? previous.cwd : undefined), signal);
2191
+ const prompt = inheritAllowedEditSurfaces(previous.agent, String(params.prompt ?? ""), params.context, previous.prompt);
2192
+ return launch(ctx, await buildRequest(ctx, agent, prompt, typeof params.label === "string" ? params.label : undefined, typeof params.context === "string" ? params.context : undefined, mode, previous.sessionPath, foreignContinuation ? undefined : previous.cwd, signal, foreignContinuation ? previous.cwd : undefined), signal);
1632
2193
  },
1633
2194
  );
1634
2195
 
@@ -1643,6 +2204,37 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1643
2204
  });
1644
2205
  }
1645
2206
 
2207
+ pi.registerCommand(JOBS_COMMAND_NAME, {
2208
+ description: "Show background jobs: running first, newest first within each group; status, command, output tail; s stops the selected job.",
2209
+ handler: async (_args, ctx) => {
2210
+ if (!ctx.hasUI) return;
2211
+ if (ctx.mode !== "tui") {
2212
+ ctx.ui.notify("The jobs overlay requires TUI mode.", "warning");
2213
+ return;
2214
+ }
2215
+ let view: JobsView | undefined;
2216
+ await ctx.ui.custom<null>((tui, theme, _keybindings, done) => {
2217
+ const close = withOverlayRepaint(tui, done);
2218
+ view = new JobsView({
2219
+ theme,
2220
+ rows: () => Math.max(0, tui.terminal.rows),
2221
+ jobs: () => jobs.list(ctx.sessionManager.getSessionId() ?? ""),
2222
+ now: () => deps.now(),
2223
+ // The agent was promised a notice, so a human stop is reported;
2224
+ // a job_stop needs none because the agent itself stopped it.
2225
+ onStop: (job) => {
2226
+ // Lines already seen are delivered before the stop notice.
2227
+ monitors.finish(job);
2228
+ const stopped = jobs.stop(job.id);
2229
+ if (stopped) queueJobNotice({ job: stopped, kind: "exit" });
2230
+ },
2231
+ onClose: () => close(null),
2232
+ requestRender: () => tui.requestRender(),
2233
+ });
2234
+ return view;
2235
+ }, { overlay: true, overlayOptions: { width: "100%", maxHeight: "100%", margin: 0, anchor: "center" } }).finally(() => view?.dispose());
2236
+ },
2237
+ });
1646
2238
  pi.registerCommand(AGENTS_COMMAND_NAME, {
1647
2239
  description: "Show this session's active subagents; a lists open orchestrators in this profile. Peer threads are read-only; o opens a local task's transcript in $EDITOR.",
1648
2240
  handler: async (_args, ctx) => openOverlay(ctx),
@@ -1660,11 +2252,34 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1660
2252
  });
1661
2253
  }
1662
2254
 
2255
+ pi.on("session_tree", (_event, ctx) => {
2256
+ helperPermission.clear();
2257
+ if (sessions !== ctx.sessionManager) return;
2258
+ stateCache.load(ctx.sessionManager);
2259
+ publishActivity();
2260
+ });
1663
2261
  pi.on("session_start", async (event, ctx) => {
2262
+ // gentle-shell#1064 slice 3b-ii: a parent session that has no explicit
2263
+ // profile binding freezes `p → P → G` now. This handler is parent-only (a
2264
+ // child returns before registering it) and runs for every reason Pi
2265
+ // reports, including `startup` for --resume/--continue/--session. The
2266
+ // freeze is in-memory and idempotent per session id: /reload keeps it, a
2267
+ // new process freezes again until slice 3b-i persists it as an entry.
2268
+ try {
2269
+ freezeSessionProfileAtStartup({
2270
+ sessionId: ctx.sessionManager.getSessionId(),
2271
+ cwd: ctx.sessionManager.getCwd(),
2272
+ configHome: gentlePiConfigHome(deps.env),
2273
+ resolveWorktree: deps.resolveWorktree,
2274
+ env: deps.env,
2275
+ });
2276
+ } catch { /* A launch freezes lazily through the same resolver. */ }
2277
+ stateCache.load(ctx.sessionManager);
1664
2278
  // A resumed, reloaded, or replaced session starts with an empty completion
1665
2279
  // queue so nothing pending from another session can replay here.
1666
2280
  completions.dropAll();
1667
2281
  messages.dropAll();
2282
+ jobNotices.length = 0;
1668
2283
  resetParentDelivery(ctx);
1669
2284
  presence?.dispose();
1670
2285
  registryFor(ctx);
@@ -1682,8 +2297,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1682
2297
  const preexisting = event.reason === "resume" || (event.reason === "startup" && ctx.sessionManager.getEntries().length > 0);
1683
2298
  if (preexisting && sessionId) void restoreSessionHistory(ctx, sessionId);
1684
2299
  try {
1685
- presence = PresencePublisher.start({ profile: agentHome, sessionId: activeSessionId() ?? "",
1686
- label: ctx.sessionManager.getSessionName?.() || ctx.sessionManager.getCwd().split(/[\\/]/).pop() || "Orchestrator", activity: [] });
2300
+ presence = startPresence(ctx.sessionManager);
1687
2301
  publishActivity();
1688
2302
  } catch { presence = undefined; }
1689
2303
  void startSessionTransport(ctx);
@@ -1714,10 +2328,16 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1714
2328
  pi.on("session_shutdown", async () => {
1715
2329
  completions.dropAll();
1716
2330
  messages.dropAll();
2331
+ // Jobs never outlive their session: a stopped job sends no notice.
2332
+ monitors.cancelAll();
2333
+ const jobsStopped = jobs.stopAll();
2334
+ jobNotices.length = 0;
2335
+ refreshJobStatus();
1717
2336
  activeAgentRuns = 0;
1718
2337
  resetParentDelivery(undefined);
1719
2338
  presence?.dispose();
1720
2339
  presence = undefined;
2340
+ stateCache.clear();
1721
2341
  rpcActivityPublisher?.stop();
1722
2342
  rpcActivityPublisher = undefined;
1723
2343
  cancelClock?.();
@@ -1733,5 +2353,16 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1733
2353
  const stopped = shutdownSessionTransport();
1734
2354
  runner.cancelAll("cancelled: parent session shut down");
1735
2355
  await stopped;
2356
+ // The default log directory goes with the session once every log is
2357
+ // closed, but a process that ignores its stop must not hang shutdown.
2358
+ await new Promise<void>((done) => {
2359
+ const cancel = deps.schedule(done, JOBS_SHUTDOWN_GRACE_MS);
2360
+ void jobsStopped.then(() => { cancel(); done(); });
2361
+ });
2362
+ if (jobOutputDir) {
2363
+ const dir = jobOutputDir;
2364
+ jobOutputDir = undefined;
2365
+ try { rmSync(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 50 }); } catch { /* A leftover temp directory is harmless. */ }
2366
+ }
1736
2367
  });
1737
2368
  }