mixdog 1.4.1 → 1.4.2

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 (326) hide show
  1. package/package.json +3 -2
  2. package/scripts/build-shipped-history.mjs +115 -33
  3. package/scripts/gate-local.mjs +172 -61
  4. package/scripts/lib/gate-scheduler.mjs +138 -32
  5. package/scripts/lib/gate-timing-reporter.mjs +16 -0
  6. package/scripts/lib/run-node-tests.mjs +54 -29
  7. package/scripts/lib/scoped-temp.mjs +1 -0
  8. package/scripts/lib/test-failure-records.mjs +14 -1
  9. package/scripts/lib/test-failure-summary.mjs +16 -4
  10. package/scripts/lib/test-spawn-ledger.mjs +12 -3
  11. package/scripts/pptx-kit-index.mjs +5 -6
  12. package/scripts/suite-health.mjs +110 -28
  13. package/scripts/test.mjs +9 -14
  14. package/src/agents/advisor/AGENT.md +4 -4
  15. package/src/agents/advisor/agent.json +1 -1
  16. package/src/agents/explore/AGENT.md +22 -0
  17. package/src/agents/explore/agent.json +6 -0
  18. package/src/cli.mjs +7 -0
  19. package/src/defaults/agents.json +7 -0
  20. package/src/defaults/shipped-history.json +18 -2
  21. package/src/defaults/skills/browser-use/SKILL.md +4 -1
  22. package/src/defaults/skills/code-tidy/SKILL.md +71 -102
  23. package/src/defaults/skills/code-tidy/references/agent-cleanup.md +66 -114
  24. package/src/defaults/skills/code-tidy/references/cleanup-report.md +19 -35
  25. package/src/defaults/skills/code-tidy/references/dead-code.md +3 -9
  26. package/src/defaults/skills/code-tidy/references/test-suite-slop.md +16 -27
  27. package/src/defaults/skills/computer-use/SKILL.md +12 -2
  28. package/src/defaults/skills/goal-management/SKILL.md +1 -1
  29. package/src/defaults/skills/setup/references/actions.md +12 -6
  30. package/src/defaults/skills/setup/references/surfaces.md +2 -2
  31. package/src/defaults/skills/setup-guide/SKILL.md +5 -2
  32. package/src/defaults/skills/setup-guide/model-recommendations.json +13 -0
  33. package/src/defaults/skills/skill-creator/SKILL.md +85 -190
  34. package/src/defaults/skills/skill-creator/references/design.md +70 -162
  35. package/src/defaults/skills/skill-creator/references/evaluation.md +33 -92
  36. package/src/defaults/skills/test-suite/SKILL.md +194 -0
  37. package/src/defaults/skills/test-suite/references/case-management.md +51 -0
  38. package/src/defaults/skills/test-suite/references/flaky-policy.md +39 -0
  39. package/src/defaults/skills/test-suite/references/reliability.md +49 -0
  40. package/src/defaults/skills/test-suite/references/suite-design.md +78 -0
  41. package/src/defaults/skills/test-suite/scripts/inventory.mjs +335 -0
  42. package/src/defaults/skills/visualize/SKILL.md +6 -2
  43. package/src/lib/rules-builder.cjs +6 -30
  44. package/src/rules/lead/LEAD.md +3 -3
  45. package/src/rules/shared/20-research.md +3 -6
  46. package/src/rules/shared/40-editing.md +1 -0
  47. package/src/rules/shared/60-verification.md +10 -9
  48. package/src/rules/shared/70-delivery.md +4 -3
  49. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/abort-link.mjs +5 -1
  50. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +10 -1
  51. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +130 -21
  52. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +13 -0
  53. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +4 -1
  54. package/src/runtime/agent/orchestrator/config.mjs +12 -7
  55. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +7 -7
  56. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +5 -26
  57. package/src/runtime/agent/orchestrator/internal-agents.mjs +3 -14
  58. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +122 -12
  59. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +5 -3
  60. package/src/runtime/agent/orchestrator/mcp/client-config.mjs +13 -2
  61. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +152 -58
  62. package/src/runtime/agent/orchestrator/mcp/client.mjs +51 -18
  63. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +83 -52
  64. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +42 -26
  65. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +43 -8
  66. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +30 -28
  67. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  68. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +31 -21
  69. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +3 -1
  70. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +2 -2
  71. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
  72. package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +1 -1
  73. package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +40 -0
  74. package/src/runtime/agent/orchestrator/providers/custom-provider-keys.mjs +34 -0
  75. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +3 -0
  76. package/src/runtime/agent/orchestrator/providers/devin-auth.mjs +14 -7
  77. package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +3 -2
  78. package/src/runtime/agent/orchestrator/providers/gemini-response.mjs +2 -2
  79. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +4 -1
  80. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +48 -31
  81. package/src/runtime/agent/orchestrator/providers/lib/oauth-page.mjs +0 -18
  82. package/src/runtime/agent/orchestrator/providers/lib/oauth-token-utils.mjs +138 -6
  83. package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +1 -1
  84. package/src/runtime/agent/orchestrator/providers/model-cache.mjs +100 -33
  85. package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +1 -1
  86. package/src/runtime/agent/orchestrator/providers/oauth-secret-fields.fixtures.mjs +45 -0
  87. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +26 -28
  88. package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +3 -1
  89. package/src/runtime/agent/orchestrator/providers/openai-ws-watchdogs.mjs +1 -0
  90. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +1 -1
  91. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +3 -1
  92. package/src/runtime/agent/orchestrator/providers/registry.mjs +33 -2
  93. package/src/runtime/agent/orchestrator/providers/vertex-ai-auth.mjs +622 -0
  94. package/src/runtime/agent/orchestrator/providers/vertex-ai-maas.mjs +185 -0
  95. package/src/runtime/agent/orchestrator/providers/vertex-ai-models.mjs +445 -0
  96. package/src/runtime/agent/orchestrator/providers/vertex-ai.mjs +567 -0
  97. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +2 -0
  98. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +2 -1
  99. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
  100. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +3 -0
  101. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +14 -5
  102. package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +1 -0
  103. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +30 -5
  104. package/src/runtime/agent/orchestrator/session/agent-room.mjs +171 -37
  105. package/src/runtime/agent/orchestrator/session/cache-warm.mjs +94 -10
  106. package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +3 -3
  107. package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +10 -5
  108. package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +5 -5
  109. package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +11 -1
  110. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +4 -3
  111. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +36 -6
  112. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +5 -3
  113. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +21 -2
  114. package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +2 -0
  115. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +23 -1
  116. package/src/runtime/agent/orchestrator/session/loop/turn-limits.mjs +13 -8
  117. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +50 -28
  118. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +1 -0
  119. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +3 -4
  120. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +2 -0
  121. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +7 -0
  122. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +15 -0
  123. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +25 -14
  124. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +17 -14
  125. package/src/runtime/agent/orchestrator/session/manager/session-scratch-environment.mjs +70 -0
  126. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +0 -3
  127. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +7 -3
  128. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +47 -29
  129. package/src/runtime/agent/orchestrator/session/manager/turn-reap.mjs +125 -0
  130. package/src/runtime/agent/orchestrator/session/mixy-name.mjs +11 -5
  131. package/src/runtime/agent/orchestrator/session/restart-prefix-parity.fixture.mjs +1 -1
  132. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +243 -38
  133. package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +163 -64
  134. package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +164 -4
  135. package/src/runtime/agent/orchestrator/session/store-transcript-worker.mjs +142 -18
  136. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +6 -2
  137. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +36 -26
  138. package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +3 -5
  139. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +58 -20
  140. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +20 -15
  141. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +96 -20
  142. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +59 -10
  143. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
  144. package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +99 -2
  145. package/src/runtime/agent/orchestrator/tools/lib/owned-shell-trees.mjs +2 -0
  146. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +103 -13
  147. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +3 -1
  148. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +49 -15
  149. package/src/runtime/agent/orchestrator/tools/lib/shell-run-spawn.mjs +45 -27
  150. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +34 -10
  151. package/src/runtime/agent/orchestrator/tools/lib/shell-warm-standby.mjs +2 -0
  152. package/src/runtime/agent/orchestrator/tools/lib/wait-until-fixture.mjs +26 -0
  153. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +7 -2
  154. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +13 -13
  155. package/src/runtime/channels/lib/whisper-port.mjs +19 -10
  156. package/src/runtime/channels/lib/whisper-server.mjs +10 -1
  157. package/src/runtime/connectors/connector-accounts-mcp.mjs +236 -103
  158. package/src/runtime/connectors/gmail-app-password.mjs +3 -3
  159. package/src/runtime/explore/explorer.mjs +93 -0
  160. package/src/runtime/explore/tool-defs.mjs +35 -0
  161. package/src/runtime/explore/tool.mjs +123 -0
  162. package/src/runtime/memory/index.mjs +4 -0
  163. package/src/runtime/memory/lib/embedding-provider.mjs +4 -1
  164. package/src/runtime/memory/lib/embedding-worker.mjs +4 -0
  165. package/src/runtime/office/quality/assurance-structure-docx.mjs +1 -1
  166. package/src/runtime/shared/agent-route-config.mjs +17 -2
  167. package/src/runtime/shared/atomic-file.mjs +103 -2
  168. package/src/runtime/shared/background-tasks.mjs +15 -6
  169. package/src/runtime/shared/connector-accounts.mjs +5 -5
  170. package/src/runtime/shared/connector-catalog.d.mts +11 -4
  171. package/src/runtime/shared/connector-catalog.mjs +60 -52
  172. package/src/runtime/shared/custom-provider-keys.mjs +20 -0
  173. package/src/runtime/shared/file-has-data.mjs +26 -2
  174. package/src/runtime/shared/file-lock.mjs +13 -15
  175. package/src/runtime/shared/git-launch.d.mts +9 -0
  176. package/src/runtime/shared/git-launch.mjs +213 -0
  177. package/src/runtime/shared/git-runner-worker.mjs +2 -1
  178. package/src/runtime/shared/llm/cost.mjs +6 -1
  179. package/src/runtime/shared/llm/http-agent.mjs +313 -29
  180. package/src/runtime/shared/llm/model-catalog-projection.mjs +3 -2
  181. package/src/runtime/shared/llm/model-catalog.mjs +46 -6
  182. package/src/runtime/shared/llm/provider-catalog-registry.mjs +3 -0
  183. package/src/runtime/shared/llm/system-proxy.mjs +452 -0
  184. package/src/runtime/shared/provider-accounts.mjs +52 -2
  185. package/src/runtime/shared/staged-install-worker.mjs +5 -0
  186. package/src/runtime/shared/staged-update.mjs +47 -2
  187. package/src/runtime/shared/tool-surface.mjs +110 -36
  188. package/src/runtime/shared/tool-work-units.mjs +4 -0
  189. package/src/runtime/shared/turn-worktree-snapshot.mjs +414 -54
  190. package/src/runtime/shared/worktrees/activity-gate.mjs +215 -318
  191. package/src/runtime/shared/worktrees/activity-ledger-worker.mjs +56 -0
  192. package/src/runtime/shared/worktrees/activity-ledger.mjs +391 -0
  193. package/src/runtime/shared/worktrees/branch-management.mjs +9 -3
  194. package/src/runtime/shared/worktrees/git.mjs +57 -0
  195. package/src/runtime/shared/worktrees/manager.mjs +205 -137
  196. package/src/runtime/tidy/install.mjs +95 -15
  197. package/src/runtime/tidy/languages.mjs +17 -15
  198. package/src/runtime/tidy/process.mjs +7 -4
  199. package/src/runtime/tidy/resolve.mjs +19 -16
  200. package/src/runtime/tidy/runners/biome.mjs +3 -3
  201. package/src/runtime/tidy/tool.mjs +1 -1
  202. package/src/runtime/tidy/worktree.mjs +10 -6
  203. package/src/runtime/visualize-bridge/client.mjs +4 -1
  204. package/src/runtime/visualize-bridge/tool-defs.mjs +7 -2
  205. package/src/runtime/web-search/index.mjs +1 -1
  206. package/src/session-runtime/agent-task-sessions.mjs +410 -353
  207. package/src/session-runtime/boot/tools.mjs +2 -0
  208. package/src/session-runtime/bot-status.mjs +103 -103
  209. package/src/session-runtime/connector-merge-migration.mjs +18 -15
  210. package/src/session-runtime/conversation-import/discovery.mjs +159 -40
  211. package/src/session-runtime/conversation-import/service.mjs +9 -3
  212. package/src/session-runtime/handoff-attachments.mjs +1 -1
  213. package/src/session-runtime/held-requests.mjs +3 -1
  214. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +2 -0
  215. package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +8 -0
  216. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +143 -116
  217. package/src/session-runtime/lifecycle/teardown/session-close.mjs +6 -1
  218. package/src/session-runtime/lifecycle/teardown.mjs +1 -1
  219. package/src/session-runtime/mcp-glue/server-catalog.mjs +16 -10
  220. package/src/session-runtime/mcp-glue/server-input.mjs +4 -1
  221. package/src/session-runtime/mixy-heartbeat.mjs +1 -1
  222. package/src/session-runtime/provider-auth/login.mjs +2 -2
  223. package/src/session-runtime/resource-mcp-api.mjs +98 -35
  224. package/src/session-runtime/resource-mcp-connector-accounts.mjs +44 -31
  225. package/src/session-runtime/resource-mcp-secrets.mjs +28 -11
  226. package/src/session-runtime/resource-mcp-servers.mjs +5 -4
  227. package/src/session-runtime/services/agent-tool/send-flow.mjs +10 -2
  228. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +8 -3
  229. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +4 -1
  230. package/src/session-runtime/services/agent-watchdog-registry.mjs +32 -4
  231. package/src/session-runtime/services/channel-worker/daemon-spawn.mjs +2 -0
  232. package/src/session-runtime/services/daemon-candidate.mjs +4 -1
  233. package/src/session-runtime/services/memory-runtime-proxy/daemon-spawn.mjs +4 -0
  234. package/src/session-runtime/services/plugin-admin.mjs +10 -6
  235. package/src/session-runtime/services/provider-admin.mjs +281 -10
  236. package/src/session-runtime/session-turn-api.mjs +2 -0
  237. package/src/session-runtime/settings-builtin-tools-api.mjs +4 -1
  238. package/src/session-runtime/settings-system-api.mjs +1 -1
  239. package/src/session-runtime/setup-tool/agent-settings-actions.mjs +13 -3
  240. package/src/session-runtime/setup-tool/connector-widen-approval.mjs +96 -0
  241. package/src/session-runtime/setup-tool/executor.mjs +6 -2
  242. package/src/session-runtime/setup-tool/tool-defs.mjs +2 -1
  243. package/src/session-runtime/turn/agent-ops.mjs +23 -0
  244. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +49 -11
  245. package/src/session-runtime/workflow-agents-api/agent-route.mjs +10 -1
  246. package/src/session-runtime/workspace/activity-guards.mjs +176 -0
  247. package/src/session-runtime/workspace/session-view.mjs +149 -0
  248. package/src/session-runtime/workspace/side-checkouts.mjs +203 -0
  249. package/src/session-runtime/workspace/transition-runner.mjs +188 -0
  250. package/src/session-runtime/workspace/transitions.mjs +108 -1066
  251. package/src/session-runtime/workspace/workspace-merge.mjs +132 -0
  252. package/src/session-runtime/workspace/worktree-intents.mjs +242 -0
  253. package/src/session-runtime/workspace/worktree-maintenance.mjs +80 -0
  254. package/src/session-runtime/workspace/worktree-setup.mjs +85 -0
  255. package/src/session-runtime/workspace/worktree-tool.mjs +54 -54
  256. package/src/standalone/daemon-desktop-runtime.mjs +2 -0
  257. package/src/standalone/daemon-session-control.mjs +151 -136
  258. package/src/standalone/daemon.mjs +7 -0
  259. package/src/standalone/desktop-service-registry.mjs +9 -0
  260. package/src/standalone/local-session-runtime.mjs +5 -1
  261. package/src/standalone/session-client/daemon-unavailable.mjs +3 -0
  262. package/src/standalone/session-client.mjs +5 -1
  263. package/src/standalone/session-frame-stream.mjs +83 -11
  264. package/src/standalone/session-protocol.mjs +4 -0
  265. package/src/standalone/session-service/agent-tree.mjs +45 -9
  266. package/src/standalone/session-service/projection/transcript-window.mjs +700 -64
  267. package/src/standalone/session-service/session-calls/session-views.mjs +160 -104
  268. package/src/standalone/session-service/stored-reader.mjs +127 -40
  269. package/src/tui/App.jsx +154 -164
  270. package/src/tui/app/app-view/floating-panel.jsx +3 -0
  271. package/src/tui/app/app-view/text-entry-labels.mjs +1 -0
  272. package/src/tui/app/clipboard.mjs +21 -10
  273. package/src/tui/app/prompt-handlers/use-prompt-history-nav.mjs +1 -1
  274. package/src/tui/app/prompt-handlers/use-prompt-interrupt.mjs +3 -0
  275. package/src/tui/app/prompt-submit/provider-prompts.mjs +3 -1
  276. package/src/tui/app/provider-setup/api-key-actions.mjs +31 -1
  277. package/src/tui/app/provider-setup/oauth-actions.mjs +25 -6
  278. package/src/tui/app/provider-setup-picker.mjs +20 -13
  279. package/src/tui/app/shell-layout/row-budgets.mjs +12 -10
  280. package/src/tui/app/slash-commands.mjs +1 -0
  281. package/src/tui/app/slash-dispatch/session-commands.mjs +46 -0
  282. package/src/tui/app/slash-dispatch.mjs +1 -1
  283. package/src/tui/app/use-btw-panel.mjs +29 -0
  284. package/src/tui/app/use-context-panel-refresh.mjs +31 -0
  285. package/src/tui/app/use-model-catalog-cache.mjs +13 -10
  286. package/src/tui/app/use-mouse-input.mjs +11 -0
  287. package/src/tui/app/use-onboarding-start.mjs +8 -3
  288. package/src/tui/app/use-panel-transition.mjs +10 -1
  289. package/src/tui/app/use-pasted-buffers.mjs +28 -20
  290. package/src/tui/app/use-prompt-draft-flow.mjs +7 -0
  291. package/src/tui/app/use-prompt-layout-rows.mjs +3 -3
  292. package/src/tui/app/use-prompt-queue-history.mjs +3 -2
  293. package/src/tui/app/use-selection-focus.mjs +11 -2
  294. package/src/tui/app/use-selection-geometry.mjs +18 -9
  295. package/src/tui/app/use-selection-paint.mjs +4 -4
  296. package/src/tui/app/use-selection-stitch.mjs +3 -3
  297. package/src/tui/app/use-slash-palette.mjs +62 -0
  298. package/src/tui/app/use-terminal-chrome.mjs +2 -2
  299. package/src/tui/app/use-transcript-activity.mjs +47 -32
  300. package/src/tui/app/use-transcript-follow.mjs +12 -4
  301. package/src/tui/app/use-transcript-scroll.mjs +46 -17
  302. package/src/tui/app/use-transcript-swap-reset.mjs +27 -0
  303. package/src/tui/app/use-transcript-window/geometry.mjs +19 -5
  304. package/src/tui/app/use-transcript-window/instance-state.mjs +4 -0
  305. package/src/tui/app/use-transcript-window/selection-shift.mjs +5 -1
  306. package/src/tui/app/use-transcript-window.mjs +21 -11
  307. package/src/tui/app/use-tui-ready.mjs +18 -0
  308. package/src/tui/app/use-welcome-prompt-hint.mjs +5 -0
  309. package/src/tui/app/use-workflow-tab-cycle.mjs +5 -3
  310. package/src/tui/components/Picker.jsx +27 -3
  311. package/src/tui/components/SignInUrl.jsx +68 -0
  312. package/src/tui/components/TextEntryPanel.jsx +76 -46
  313. package/src/tui/components/sign-in-url-layout.mjs +24 -0
  314. package/src/tui/dist/index.mjs +1896 -1341
  315. package/src/tui/session/goal-turn-state.mjs +6 -2
  316. package/src/tui/session/session-api/controls.mjs +4 -0
  317. package/src/tui/session/session-api/extensions.mjs +8 -0
  318. package/src/tui/session/session-api/intake/abort.mjs +2 -1
  319. package/src/tui/session/session-api/intake.mjs +2 -0
  320. package/src/tui/session/session-api/integrations.mjs +2 -2
  321. package/src/tui/session/session-api/routes-agent-definitions.mjs +1 -0
  322. package/src/tui/session/session-api/settings.mjs +2 -2
  323. package/src/tui/session/transcript-spill/spill-writer.mjs +15 -0
  324. package/src/tui/session/transcript-spill.mjs +92 -4
  325. package/src/tui/session/transcript-store.mjs +2 -0
  326. package/src/defaults/skills/skill-creator/references/source-review.md +0 -94
@@ -54,6 +54,17 @@ export function killAfterSnapshot(snapshot, kill) {
54
54
 
55
55
  const identityKey = (entry) => `${entry.pid}:${entry.token ?? ''}`;
56
56
 
57
+ // Windows keeps a process's parent id after that parent exits and hands the
58
+ // pid to a new process within milliseconds, so a parent link can name a
59
+ // process created after the one listing it: an unrelated process then looks
60
+ // like the new holder's child. A child is never created before its parent.
61
+ // (POSIX reparents orphans, so its links never dangle.) Only kernel creation
62
+ // times are compared — the table's tokens and the spawn server's root
63
+ // creation time, both FILETIME ticks — at the table's microsecond precision;
64
+ // a parent whose time is unknown excludes nothing. Any other token (POSIX
65
+ // lstart, scripted test tokens) is unknown.
66
+ const createdUs = (ticks) => (typeof ticks === 'string' && /^\d+$/.test(ticks) ? BigInt(ticks) / 10n : null);
67
+
57
68
  /** Tracks one command's process tree across its termination.
58
69
  *
59
70
  * `capture()` records the identities of the root and every descendant while
@@ -66,8 +77,11 @@ const identityKey = (entry) => `${entry.pid}:${entry.token ?? ''}`;
66
77
  * or (POSIX) a member of the command's group is alive → running
67
78
  * - no complete pre-exit capture exists → unknown
68
79
  * - a tracked process without a creation token is present → unknown
69
- * A failed table read rejects; callers treat it as unknown. */
70
- export function createProcessTreeTracker({ pid, members = [], exitObserved = false } = {}) {
80
+ * A failed table read rejects; callers treat it as unknown.
81
+ *
82
+ * `rootCreationTime`: the root's kernel creation time from its spawn (see
83
+ * native-spawn-client), known even once the root is gone uncaptured. */
84
+ export function createProcessTreeTracker({ pid, members = [], exitObserved = false, rootCreationTime = null } = {}) {
71
85
  const rootPid = Number(pid) || 0;
72
86
  let rootToken = null;
73
87
  let complete = false;
@@ -92,6 +106,12 @@ export function createProcessTreeTracker({ pid, members = [], exitObserved = fal
92
106
  // are ours. A different process under that pid is a reuse — not ours.
93
107
  const rootPidIsOurs = (table, rootAlive) =>
94
108
  rootAlive || !table.tokenOf.has(rootPid) || (rootToken != null && table.tokenOf.get(rootPid) === rootToken);
109
+ // When `cur` (a live root, or a present identity) was created, if known.
110
+ const createdAt = (table, cur, rootAlive) => {
111
+ if (!isWin) return null;
112
+ if (cur !== rootPid) return createdUs(table.tokenOf.get(cur));
113
+ return createdUs(rootCreationTime) ?? createdUs(rootAlive ? table.tokenOf.get(rootPid) : rootToken);
114
+ };
95
115
 
96
116
  function absorb(table, rootAlive) {
97
117
  const from = [];
@@ -100,8 +120,11 @@ export function createProcessTreeTracker({ pid, members = [], exitObserved = fal
100
120
  const seen = new Set(from);
101
121
  while (from.length) {
102
122
  const cur = from.pop();
123
+ const parentCreated = createdAt(table, cur, rootAlive);
103
124
  for (const child of table.childrenOf.get(cur) ?? []) {
104
125
  if (seen.has(child)) continue;
126
+ const childCreated = parentCreated == null ? null : createdUs(table.tokenOf.get(child));
127
+ if (childCreated != null && childCreated < parentCreated) continue;
105
128
  seen.add(child);
106
129
  track(table, child);
107
130
  from.push(child);
@@ -173,19 +196,77 @@ export function createProcessTreeTracker({ pid, members = [], exitObserved = fal
173
196
  };
174
197
  }
175
198
 
199
+ /** The tracker of a KILLED tree the native spawn server contains (a Windows
200
+ * job object nothing can break away from), driven only by the server's
201
+ * outcome for that job (`outcome()`, see native-spawn-client treeOutcome).
202
+ * It never reads the process table: membership is the job's, never a guess
203
+ * from parent links, which a reused root pid makes point at the wrong
204
+ * processes. So the kill never waits for a snapshot.
205
+ * 'empty' gone
206
+ * 'preserved' the root exited on its own before the kill
207
+ * reached the server, which kept its survivors in
208
+ * the job: `cancel()` re-sends the kill to exactly
209
+ * that job (once), and its verdict follows
210
+ * 'unverified'/'released' unknown for good, and incomplete, so a
211
+ * force-release ends the watch: never gone
212
+ * anything else unknown until the server reports (a lost server
213
+ * never does) */
214
+ export function createContainedTreeTracker({ outcome, cancel }) {
215
+ let cancelled = false;
216
+ return {
217
+ contained: true,
218
+ get complete() {
219
+ const value = outcome();
220
+ return value !== 'unverified' && value !== 'released';
221
+ },
222
+ live: [],
223
+ capture: async () => true,
224
+ acceptExitObservation() {},
225
+ async observe() {
226
+ const value = outcome();
227
+ if (value === 'empty') return 'gone';
228
+ if (value === 'preserved' && !cancelled) {
229
+ cancelled = true;
230
+ try {
231
+ cancel();
232
+ } catch {
233
+ /* the server link is gone: the outcome stays unknown */
234
+ }
235
+ }
236
+ return 'unknown';
237
+ },
238
+ };
239
+ }
240
+
176
241
  /** Terminate every identity the last observation found alive (console hosts
177
242
  * excepted: they are witnesses, never workers). These are processes of a tree
178
243
  * that was KILLED — they escaped its process group (setsid, job control, a
179
244
  * double fork that kept its parent link) or outlived the root on Windows —
180
245
  * so the kill is not finished while they run. Windows: taskkill /T /F;
181
246
  * POSIX: SIGTERM, then SIGKILL after a short grace. */
182
- function sweepLiveIdentities(tracker) {
247
+ export function sweepLiveIdentities(tracker) {
183
248
  const targets = tracker.live
184
249
  .filter((entry) => entry.token != null && !CONSOLE_HOST.test(entry.name))
185
250
  .map((entry) => entry.pid);
186
251
  return targets.length > 0 ? killProcessTrees(targets) : Promise.resolve();
187
252
  }
188
253
 
254
+ /** A tracker for a watch that goes on after its hold was force-released: an
255
+ * observation that stays 'unknown' on a tracker that can never complete (the
256
+ * root is gone and its pre-kill capture was incomplete) ends the watch via
257
+ * `stop`. Termination is never reported by it. */
258
+ export function endWatchWhenUnconfirmable(tracker, { released, stop }) {
259
+ return Object.create(tracker, {
260
+ observe: {
261
+ value: async (options) => {
262
+ const state = await tracker.observe(options);
263
+ if (released() && state === 'unknown' && !tracker.complete) stop();
264
+ return state;
265
+ },
266
+ },
267
+ });
268
+ }
269
+
189
270
  /** One observation loop: observe now, then every `intervalMs` until the tree
190
271
  * is confirmed gone, then run `onTerminated` once. 'running', 'unknown', a
191
272
  * failed or unanswered (by the next cadence tick) observation and a failing
@@ -194,8 +275,9 @@ function sweepLiveIdentities(tracker) {
194
275
  * timer; nothing else (listener, timer) outlives the loop. Nothing in it ever
195
276
  * releases on time alone.
196
277
  *
197
- * `sweep` (a killed tree): a pass that finds captured identities alive
198
- * terminates them and observes again in the same pass, so a cancellation
278
+ * `sweep` (a killed tree; a boolean, or a predicate read on every pass so a
279
+ * cancel that arrives later is retried each cadence): a pass that finds
280
+ * captured identities alive terminates them and observes again in the same pass, so a cancellation
199
281
  * ends with the tree gone instead of a held folder over live processes.
200
282
  * A missing pre-kill capture stays unknown even if the owning session
201
283
  * closes: its lifetime is not evidence that every descendant has exited. */
@@ -230,7 +312,7 @@ export function watchTermination({
230
312
  observation.catch(() => {});
231
313
  // A pass that has not answered by the next cadence tick is unknown.
232
314
  state = await withinBound(observation, intervalMs, 'unknown');
233
- if (state === 'running' && sweep && !stopped) {
315
+ if (state === 'running' && !stopped && (typeof sweep === 'function' ? sweep() : sweep)) {
234
316
  await withinBound(
235
317
  sweepLiveIdentities(tracker).catch(() => {}),
236
318
  intervalMs,
@@ -315,24 +397,26 @@ function delay(ms) {
315
397
  * by pid (Windows re-parents nothing: a process whose parent
316
398
  * exited keeps a dangling parent id, so an intermediate that
317
399
  * already exited breaks the link).
318
- * Rejects when the process table cannot be read completely. */
319
- export async function probeShellDescendants({ pid } = {}) {
400
+ * Rejects when the process table cannot be read completely.
401
+ * `rootCreationTime`: see createProcessTreeTracker. */
402
+ export async function probeShellDescendants({ pid, rootCreationTime = null } = {}) {
320
403
  const root = Number(pid) || 0;
321
404
  if (root <= 0) return null;
322
405
  // POSIX fast path: no member of the command's group at all.
323
406
  if (!isWin && !groupAlive(root)) return null;
324
- const tracker = createProcessTreeTracker({ pid: root, exitObserved: true });
407
+ const tracker = createProcessTreeTracker({ pid: root, exitObserved: true, rootCreationTime });
325
408
  const state = await tracker.observe({ rootExited: true });
326
409
  if (state === 'gone') return null;
327
410
  if (state === 'unknown') throw new Error('Remaining child processes could not be verified.');
328
- return survivorHandle(root, tracker.live);
411
+ return survivorHandle(root, tracker.live, rootCreationTime);
329
412
  }
330
413
 
331
- function survivorHandle(rootPid, live) {
414
+ function survivorHandle(rootPid, live, rootCreationTime) {
332
415
  const watch = live.map(({ pid, token, name }) => ({ pid, token, name }));
333
416
  const killPids = watch.filter((entry) => !CONSOLE_HOST.test(entry.name)).map((entry) => entry.pid);
334
417
  return {
335
418
  rootPid,
419
+ rootCreationTime,
336
420
  groupPid: isWin ? null : rootPid,
337
421
  killPids,
338
422
  watchPids: watch.map((entry) => entry.pid),
@@ -342,13 +426,18 @@ function survivorHandle(rootPid, live) {
342
426
  }
343
427
 
344
428
  const trackerFor = (handle) =>
345
- createProcessTreeTracker({ pid: handle.rootPid, members: handle.watch, exitObserved: true });
429
+ createProcessTreeTracker({
430
+ pid: handle.rootPid,
431
+ members: handle.watch,
432
+ exitObserved: true,
433
+ rootCreationTime: handle.rootCreationTime,
434
+ });
346
435
 
347
436
  /** Resolve once every observed survivor (by identity) has exited, or false
348
437
  * once `signal` aborts. These processes are not our children any more, so
349
438
  * there is no exit event to subscribe to; the shared observation loop
350
439
  * re-checks them at the termination cadence. */
351
- export function waitForShellDescendants(handle, { signal = null } = {}) {
440
+ export function waitForShellDescendants(handle, { signal = null, sweep = false } = {}) {
352
441
  if (!handle) return Promise.resolve(true);
353
442
  return new Promise((resolve) => {
354
443
  if (signal?.aborted) {
@@ -361,6 +450,7 @@ export function waitForShellDescendants(handle, { signal = null } = {}) {
361
450
  };
362
451
  const stop = watchTermination({
363
452
  tracker: trackerFor(handle),
453
+ sweep,
364
454
  onTerminated: () => {
365
455
  signal?.removeEventListener('abort', onAbort);
366
456
  resolve(true);
@@ -92,6 +92,8 @@ export async function handOverLease(run) {
92
92
  const lease = run.resourceLease;
93
93
  run.resourceLease = null;
94
94
  await lease?.release();
95
- run.workspaceAdmission?.();
95
+ const released = run.workspaceAdmission?.();
96
96
  run.workspaceAdmission = null;
97
+ // Fails only when workspace admission stopped (reported there), which keeps the folder held.
98
+ await Promise.resolve(released).catch(() => {});
97
99
  }
@@ -3,7 +3,7 @@
3
3
  // lease cleanup that a promoted or unconfirmed-kill child defers to its own
4
4
  // real exit.
5
5
  import { ExecResult, SHELL_OUTPUT_INLINE_CAP } from '../shell-exec-output.mjs';
6
- import { createProcessTreeTracker, probeShellDescendants, watchTermination } from './shell-descendants.mjs';
6
+ import { endWatchWhenUnconfirmable, probeShellDescendants, watchTermination } from './shell-descendants.mjs';
7
7
  import { unregisterOwnedShellTree } from './owned-shell-trees.mjs';
8
8
  import {
9
9
  adoptShellWorkspaceAdmission,
@@ -17,6 +17,7 @@ import {
17
17
  detachAbortHandler,
18
18
  keepTerminationUnconfirmedRecord,
19
19
  releaseResourceLease,
20
+ runTreeTracker,
20
21
  } from './shell-run-state.mjs';
21
22
 
22
23
  // Unconfirmed-kill cleanup ceiling: the result is already reported, but the
@@ -26,8 +27,8 @@ import {
26
27
  // it follows confirmed termination only (see below).
27
28
  const UNCONFIRMED_KILL_CLEANUP_CEILING_MS = 60_000;
28
29
 
29
- /** Settles once a held run's whole tree is confirmed gone (or its hold was
30
- * force-released): what its result left in use defers to this. */
30
+ /** Settles once a held run's whole tree is confirmed gone: what its result
31
+ * left in use defers to this. A force-release does not settle it. */
31
32
  function treeSettled(run) {
32
33
  if (!run.treeSettled) {
33
34
  run.treeSettled = new Promise((resolve) => {
@@ -52,28 +53,56 @@ export function releaseAdmissionOnConfirmedTermination(run, { markNow = true, sw
52
53
  // gone only once its first observation is not 'gone'.
53
54
  if (markNow) keepTerminationUnconfirmedRecord(run);
54
55
  treeSettled(run);
56
+ // The workspace leases stop counting here; the returned promise settles once
57
+ // their removal is on disk. A force-release only drops the hold: the tree
58
+ // stays observed, and only its confirmed exit settles it (`terminated`).
59
+ let released;
60
+ let holdDropped = false;
55
61
  const release = () => {
56
- run.workspaceAdmission?.();
57
- run.workspaceAdmission = null;
58
- run.stopTerminationWatch?.();
59
62
  clearTerminationHold(run.foregroundRecordId);
60
- clearForegroundRecord(run);
63
+ if (!holdDropped) {
64
+ released = run.workspaceAdmission?.();
65
+ run.workspaceAdmission = null;
66
+ clearForegroundRecord(run);
67
+ holdDropped = true;
68
+ }
69
+ return released;
70
+ };
71
+ const terminated = () => {
72
+ const dropped = release();
73
+ run.stopTerminationWatch?.();
61
74
  run.resolveTreeSettled();
75
+ return dropped;
62
76
  };
63
77
  registerTerminationHold(run.foregroundRecordId, {
64
78
  ownerSessionId: run.foregroundRecordInput?.ownerSessionId || null,
65
79
  cwd: run.foregroundRecordInput?.cwd || null,
66
80
  release,
67
81
  });
68
- run.terminationTracker = run.terminationTracker || createProcessTreeTracker({ pid: child.pid });
82
+ runTreeTracker(run);
83
+ // After a force-release the watch only waits for a confirmed exit. An
84
+ // incomplete pre-kill capture of a root that is gone can never be completed,
85
+ // so an observation that stays unknown on it ends the watch (never settling
86
+ // the tree: `treeSettled` follows a confirmed exit only).
69
87
  run.stopTerminationWatch = watchTermination({
70
- tracker: run.terminationTracker,
88
+ tracker: endWatchWhenUnconfirmable(run.terminationTracker, {
89
+ released: () => holdDropped,
90
+ stop: () => run.stopTerminationWatch?.(),
91
+ }),
71
92
  rootExited: () => child.exitCode != null || child.signalCode != null,
72
93
  // A killed run is not over while processes it started still run.
73
94
  sweep,
74
- onTerminated: release,
75
- onUnconfirmed: markNow ? null : () => keepTerminationUnconfirmedRecord(run),
76
- releaseRequested: () => takeReleaseRequest(run.foregroundRecordId),
95
+ onTerminated: terminated,
96
+ onUnconfirmed: markNow
97
+ ? null
98
+ : () => {
99
+ if (!holdDropped) keepTerminationUnconfirmedRecord(run);
100
+ },
101
+ // A release requested from another process is the same force-release.
102
+ releaseRequested: () => {
103
+ if (takeReleaseRequest(run.foregroundRecordId)) void release();
104
+ return false;
105
+ },
77
106
  });
78
107
  }
79
108
 
@@ -165,8 +194,11 @@ export async function readCapturedOutput(taskOutput, final = false) {
165
194
  // instead of a clean finish; null means nothing survived.
166
195
  async function probeSurvivingDescendants(run) {
167
196
  if (run.killed || run.timedOut || !run.child?.pid) return null;
197
+ // The spawn server verified the command's job empty: nothing survived.
198
+ if (run.child.treeOutcome === 'empty') return null;
168
199
  const descendants = await probeShellDescendants({
169
200
  pid: run.child.pid,
201
+ rootCreationTime: run.child.creationTime,
170
202
  });
171
203
  if (descendants) descendants.taskId = run.foregroundRecordId;
172
204
  return descendants;
@@ -240,8 +272,7 @@ export function createSettle(run, releasePromotedCapture) {
240
272
  // explicit force-release, and the record is kept, marked unconfirmed.
241
273
  // (The probe runs only for an unkilled root, so the lifecycle is not
242
274
  // already held here.)
243
- run.terminationTracker = run.terminationTracker || createProcessTreeTracker({ pid: run.child.pid });
244
- run.terminationTracker.acceptExitObservation();
275
+ runTreeTracker(run).acceptExitObservation();
245
276
  releaseAdmissionOnConfirmedTermination(run);
246
277
  }
247
278
  // Marked unconfirmed (with its force-release) only once an observation
@@ -249,9 +280,12 @@ export function createSettle(run, releasePromotedCapture) {
249
280
  if (confirmKill) releaseAdmissionOnConfirmedTermination(run, { markNow: false });
250
281
  if (!lifecycleHeld && !descendantsUnknown && run.workspaceAdmission) {
251
282
  try {
283
+ let released = null;
252
284
  if (descendants) adoptShellWorkspaceAdmission(run.foregroundRecordId, run.workspaceAdmission);
253
- else run.workspaceAdmission();
285
+ else released = run.workspaceAdmission();
254
286
  run.workspaceAdmission = null;
287
+ // The result is reported once the removal is on disk, as when it was written synchronously.
288
+ await released;
255
289
  } catch (error) {
256
290
  taskOutput.writeError = taskOutput.writeError || error;
257
291
  exitCode = exitCode || 1;
@@ -18,8 +18,9 @@ import { _maybeEncodePowerShellCommand } from '../shell-powershell.mjs';
18
18
  import { spawnShellWithRetry as _spawnShellWithRetry } from './shell-spawn-retry.mjs';
19
19
  import { STANDBY_ARGS, takeWarmShellStandby } from './shell-warm-standby.mjs';
20
20
  import { SHELL_OUTPUT_DISK_CAP } from '../shell-exec-output.mjs';
21
- import { admitWorkspaceActivity, tryAdmitWorkspaceActivity } from '../../../../shared/worktrees/activity-gate.mjs';
21
+ import { admitWorkspaceActivity, tryAdmitWorkspaceActivityAsync } from '../../../../shared/worktrees/activity-gate.mjs';
22
22
  import { worktreeError } from '../../../../shared/worktrees/contract.mjs';
23
+ import { bothReleased } from '../builtin/lib/shell-job-records.mjs';
23
24
  import { capturePreKill, registerForegroundOwnedTree, treeKillForceSettle } from './shell-run-state.mjs';
24
25
  import { releaseAdmissionOnConfirmedTermination } from './shell-run-settle.mjs';
25
26
  import { killAfterSnapshot } from './shell-descendants.mjs';
@@ -135,6 +136,8 @@ async function gatedSpawn({
135
136
  command,
136
137
  ownerSessionId,
137
138
  clientHostPid,
139
+ // Windows: the whole tree stays in the command's job (native-spawn-client).
140
+ contain: true,
138
141
  ...(stdinPipe ? { stdinPipe: true } : {}),
139
142
  ...(capture
140
143
  ? {
@@ -223,20 +226,25 @@ async function spawnAdmittedShellChild({
223
226
  }));
224
227
  run.child = spawned.child;
225
228
  registerForegroundOwnedTree(run, ownerSessionId);
229
+ // Attached before the admission below yields: until the caller attaches its
230
+ // listeners, the child's output is buffered and its close replayed.
231
+ spawned.attachErrorHandler(onChildError);
226
232
  const parentAdmission = run.workspaceAdmission;
227
- const childAdmission = tryAdmitWorkspaceActivity(ownerSessionId || run.foregroundRecordId, cwd || process.cwd(), {
228
- pid: Number(run.child.pid) || process.pid,
229
- kind: 'command',
230
- });
233
+ // The command's own lease (under its pid: what keeps the folder reserved
234
+ // should this runtime die). The parent's lease covers the folder until this
235
+ // one is written to both copies, and stays the only release until then.
236
+ const childAdmission = await tryAdmitWorkspaceActivityAsync(
237
+ ownerSessionId || run.foregroundRecordId,
238
+ cwd || process.cwd(),
239
+ {
240
+ pid: Number(run.child.pid) || process.pid,
241
+ kind: 'command',
242
+ }
243
+ );
231
244
  if (!childAdmission) throw worktreeError('WORKTREE_BUSY_JOBS', 'The command workspace changed during spawn.');
232
- run.workspaceAdmission = () => {
233
- childAdmission();
234
- parentAdmission();
235
- };
236
- spawned.attachErrorHandler(onChildError);
237
- // Feed the script only after the error handler is attached; server
238
- // messages cannot be processed before this synchronous block yields.
239
- // Single atomic write+EOF, as for the standby.
245
+ run.workspaceAdmission = () => bothReleased(childAdmission, parentAdmission);
246
+ // Feed the script only once its lease is written. Single atomic write+EOF,
247
+ // as for the standby.
240
248
  abortSignal?.throwIfAborted();
241
249
  if (standby) standby.feed(spawnCommand, cwd);
242
250
  else if (stdinScript !== null) spawned.child.writeStdin(stdinScript, { close: true });
@@ -249,18 +257,23 @@ async function spawnAdmittedShellChild({
249
257
  run.foregroundRecordTimer = null;
250
258
  if (run.settled || run.autoBackgrounded || !run.child?.pid) return;
251
259
  run.foregroundRecordPublished = true;
252
- try {
253
- publishForegroundShellRecord({
254
- ...run.foregroundRecordInput,
255
- jobId: run.foregroundRecordId,
256
- pid: run.child.pid,
257
- startedAtMs: run.commandStartedAtMs,
258
- });
259
- } catch (error) {
260
- run.spawnError = error;
261
- run.failureReason = 'workspace publication failed';
262
- treeKillForceSettle(run, 'workspace-publication-failed');
263
- }
260
+ void (async () => {
261
+ try {
262
+ await publishForegroundShellRecord({
263
+ ...run.foregroundRecordInput,
264
+ jobId: run.foregroundRecordId,
265
+ pid: run.child.pid,
266
+ startedAtMs: run.commandStartedAtMs,
267
+ });
268
+ } catch (error) {
269
+ // The record's lease could not be written: the command is stopped, as
270
+ // before — unless it ended or was promoted (its own leases) meanwhile.
271
+ if (run.settled || run.autoBackgrounded) return;
272
+ run.spawnError = error;
273
+ run.failureReason = 'workspace publication failed';
274
+ treeKillForceSettle(run, 'workspace-publication-failed');
275
+ }
276
+ })();
264
277
  }, FOREGROUND_RECORD_DELAY_MS);
265
278
  run.foregroundRecordTimer.unref?.();
266
279
  }
@@ -278,11 +291,16 @@ export async function spawnShellChild(input) {
278
291
  return await spawnAdmittedShellChild(input);
279
292
  } catch (error) {
280
293
  if (!input.run.child) {
281
- input.run.workspaceAdmission();
294
+ const released = input.run.workspaceAdmission();
282
295
  input.run.workspaceAdmission = null;
296
+ // The spawn's own error is the one reported; a release fails only when
297
+ // workspace admission stopped (which keeps the folder held).
298
+ await Promise.resolve(released).catch(() => {});
283
299
  } else {
284
300
  try {
285
- // Identities first: the kill may break the parent chain.
301
+ input.run.killed = true;
302
+ // Identities first: the kill may break the parent chain (a contained
303
+ // tree needs none: its kill is at once and verified by the server).
286
304
  // Bounded: the kill is sent even if the snapshot hangs or fails.
287
305
  await killAfterSnapshot(capturePreKill(input.run), () => killShellJob(input.run.foregroundRecordId));
288
306
  const detail = await waitForShellJob(input.run.foregroundRecordId);
@@ -6,13 +6,31 @@
6
6
  import { randomUUID } from 'node:crypto';
7
7
  import { publishForegroundShellRecord, retireForegroundShellRecord } from '../builtin/shell-jobs.mjs';
8
8
  import { ShellTextDecoder, TaskOutput, treeKill } from '../shell-exec-output.mjs';
9
- import { createProcessTreeTracker, killAfterSnapshot } from './shell-descendants.mjs';
9
+ import { createContainedTreeTracker, createProcessTreeTracker } from './shell-descendants.mjs';
10
10
  import { registerOwnedShellTree } from './owned-shell-trees.mjs';
11
11
 
12
+ /** The tracker of the run's process tree, created once. A killed tree the
13
+ * spawn server contains is gone on its verified-empty report alone; every
14
+ * other tree (no containment, or a root that exited on its own and left
15
+ * survivors) is observed through the process table. */
16
+ export function runTreeTracker(run) {
17
+ run.terminationTracker =
18
+ run.terminationTracker ||
19
+ (run.killed === true && run.child.contained === true
20
+ ? createContainedTreeTracker({
21
+ outcome: () => run.child.treeOutcome,
22
+ // Survivors the server kept in the job: the kill, by request id.
23
+ cancel: () => treeKill(run.child),
24
+ })
25
+ : createProcessTreeTracker({ pid: run.child.pid, rootCreationTime: run.child.creationTime }));
26
+ return run.terminationTracker;
27
+ }
28
+
12
29
  /** One pre-kill identity read of the run's tree (every kill path shares it):
13
- * resolves true once the capture is complete, false when it failed. */
30
+ * resolves true once the capture is complete, false when it failed. A
31
+ * contained kill needs none: true at once, so the kill is never delayed. */
14
32
  export function capturePreKill(run) {
15
- run.terminationTracker = run.terminationTracker || createProcessTreeTracker({ pid: run.child.pid });
33
+ if (runTreeTracker(run).contained) return true;
16
34
  const rootExited = run.child.exitCode != null || run.child.signalCode != null;
17
35
  return run.terminationTracker.capture({ rootExited }).catch(() => false);
18
36
  }
@@ -32,6 +50,7 @@ export function registerForegroundOwnedTree(run, ownerSessionId) {
32
50
  run.killCause = run.killCause || 'session-close';
33
51
  // Always a fresh read: an earlier one predates what the run started
34
52
  // since. Pending, the kill is sent at the snapshot bound regardless.
53
+ // A contained tree answers true at once: its cancel is sent at once.
35
54
  return capturePreKill(run);
36
55
  },
37
56
  };
@@ -219,18 +238,23 @@ export function keepTerminationUnconfirmedRecord(run) {
219
238
  // Record the tree's identities while the root is still alive, THEN kill: once
220
239
  // an intermediate dies its children's parent links dangle and a snapshot taken
221
240
  // afterwards can no longer find them.
222
- // The kill waits for the snapshot at most PROCESS_TABLE_READ_BOUND_MS and is
223
- // sent whether the snapshot succeeded, failed or is still pending (a late
224
- // snapshot is still absorbed; a missing one leaves the outcome unknown, so the
225
- // workspace stays held under the observation loop). The confirmation deadline
226
- // starts once the kill is actually issued.
241
+ // The kill waits until the snapshot succeeded or failed, never just for a
242
+ // while: a read still in flight takes its table after a kill sent beside it,
243
+ // finds the root gone, and the tree could never be confirmed terminated (the
244
+ // workspace then stays held until a force-release). A healthy table read takes
245
+ // seconds on a loaded host; a stuck one ends at its enumeration deadline
246
+ // (ENUMERATION_DEADLINE_MS), and the kill is sent then, the outcome unknown.
247
+ // A tree the spawn server contains is killed at once instead: its job holds
248
+ // every descendant, and the server reports when that job is empty.
249
+ // The confirmation deadline starts once the kill is actually issued.
227
250
  function snapshotThenKill(run, afterKill) {
228
- if (!run.child?.pid) {
251
+ const snapshot = run.child?.pid ? capturePreKill(run) : true;
252
+ if (snapshot === true) {
229
253
  treeKill(run.child);
230
254
  afterKill();
231
255
  return;
232
256
  }
233
- void killAfterSnapshot(capturePreKill(run), () => {
257
+ void snapshot.then(() => {
234
258
  treeKill(run.child);
235
259
  afterKill();
236
260
  });
@@ -102,6 +102,8 @@ function ensureWarmShellStandby({ shell, env }) {
102
102
  outputLimit: SHELL_OUTPUT_DISK_CAP,
103
103
  rawOutput: true,
104
104
  stdinPipe: true,
105
+ // The command it is fed runs contained, like a fresh spawn's.
106
+ contain: true,
105
107
  shellType: 'powershell',
106
108
  command: '(warm shell standby)',
107
109
  },
@@ -0,0 +1,26 @@
1
+ // Test support: wait for a real condition on a wall-clock budget. A fixed
2
+ // number of event-loop turns is load-dependent (async record writes and
3
+ // process-table reads take more turns on a busy machine); elapsed time is not.
4
+ // Polls on a short real timer (never a busy loop). The real setTimeout is
5
+ // captured at load, before any test enables mock timers, so a mocked clock
6
+ // cannot freeze the wait.
7
+ import assert from 'node:assert/strict';
8
+
9
+ const realSetTimeout = setTimeout;
10
+ const POLL_MS = 5;
11
+ const tick = () => new Promise((resolve) => realSetTimeout(resolve, POLL_MS));
12
+
13
+ /** Resolves with the first truthy `read()` (sync or async); fails with `what`
14
+ * once `timeoutMs` has passed without one. */
15
+ export async function waitUntil(
16
+ read,
17
+ { timeoutMs = 10_000, what = String(read).replace(/\s+/g, ' ').slice(0, 160) } = {}
18
+ ) {
19
+ const deadline = Date.now() + timeoutMs;
20
+ for (;;) {
21
+ const value = await read();
22
+ if (value) return value;
23
+ if (Date.now() >= deadline) assert.fail(`condition not reached within ${timeoutMs}ms: ${what}`);
24
+ await tick();
25
+ }
26
+ }
@@ -432,7 +432,7 @@ function attachChildExit(run, releasePromotedCapture) {
432
432
  // child termination regardless — give 'close' a 3 s grace then
433
433
  // settle anyway. The grace outlives the spawn server's own 2 s drain
434
434
  // deadline, so the real exit status normally still arrives first.
435
- child.once('exit', (code, signal) => {
435
+ const onExit = (code, signal) => {
436
436
  if (!run.rootExitAtMs) run.rootExitAtMs = Date.now();
437
437
  const grace = setTimeout(() => {
438
438
  if (run.settled || run.promotionClose) return;
@@ -449,7 +449,12 @@ function attachChildExit(run, releasePromotedCapture) {
449
449
  run.settle(code == null ? 1 : code, signal);
450
450
  }, 3000);
451
451
  if (grace.unref) grace.unref();
452
- });
452
+ };
453
+ // The spawn awaits the command's workspace lease before these listeners
454
+ // exist; a root that exited meanwhile ('close' is replayed, 'exit' is not)
455
+ // starts its grace now.
456
+ if (child.exitCode != null || child.signalCode != null) onExit(child.exitCode, child.signalCode);
457
+ else child.once('exit', onExit);
453
458
  }
454
459
 
455
460
  // Live-progress heartbeat: every 2 s while the foreground command runs, emit
@@ -1,30 +1,30 @@
1
1
  {
2
- "version": "0.1.11",
2
+ "version": "0.1.12",
3
3
  "_comment": "Synced from immutable spawn-v release assets.",
4
4
  "assets": {
5
5
  "darwin-arm64": {
6
- "url": "https://github.com/tribgames/mixdog/releases/download/spawn-v0.1.11/mixdog-spawn-darwin-arm64",
7
- "sha256": "6f1a7e6597dcc8785d12c5a13c42388110d9f4a2e5378ca2773172adbf50de60"
6
+ "url": "https://github.com/tribgames/mixdog/releases/download/spawn-v0.1.12/mixdog-spawn-darwin-arm64",
7
+ "sha256": "bc2307b1c089e07f829a8a1daf4dfdfc128e3221d8f70609ffbdcc3f8b596806"
8
8
  },
9
9
  "darwin-x64": {
10
- "url": "https://github.com/tribgames/mixdog/releases/download/spawn-v0.1.11/mixdog-spawn-darwin-x64",
11
- "sha256": "ff755eb9848cb3ce90f3df24136bb84a52f9ff6c67002c4d764beb6d7906e6b2"
10
+ "url": "https://github.com/tribgames/mixdog/releases/download/spawn-v0.1.12/mixdog-spawn-darwin-x64",
11
+ "sha256": "4b1d221a29128046c417cfae5617e2898e1b7bc564dca686b2608b6cb14bf3af"
12
12
  },
13
13
  "linux-arm64": {
14
- "url": "https://github.com/tribgames/mixdog/releases/download/spawn-v0.1.11/mixdog-spawn-linux-arm64",
15
- "sha256": "3bc0fccde46537046d498a2b953aa92ef4afacc95c96c563ce1d6fb92d74e593"
14
+ "url": "https://github.com/tribgames/mixdog/releases/download/spawn-v0.1.12/mixdog-spawn-linux-arm64",
15
+ "sha256": "2f577f54693726db0d285a16c3af2ad25b73fd705fd2fd4ac5c18ce795f657a9"
16
16
  },
17
17
  "linux-x64": {
18
- "url": "https://github.com/tribgames/mixdog/releases/download/spawn-v0.1.11/mixdog-spawn-linux-x64",
19
- "sha256": "16dc4e3bf6636335c1325c88bbb5245529731525953466e761d363897b59aac7"
18
+ "url": "https://github.com/tribgames/mixdog/releases/download/spawn-v0.1.12/mixdog-spawn-linux-x64",
19
+ "sha256": "83b52edfe2b810d5cf6544b03ab263eedcf7b2ef3c73107a45a33f6f7871e59e"
20
20
  },
21
21
  "win32-arm64": {
22
- "url": "https://github.com/tribgames/mixdog/releases/download/spawn-v0.1.11/mixdog-spawn-win32-arm64.exe",
23
- "sha256": "c2fb5499d3d44405e27c79f8db65694299d61ddf029f466df68549fc2f2c5f5c"
22
+ "url": "https://github.com/tribgames/mixdog/releases/download/spawn-v0.1.12/mixdog-spawn-win32-arm64.exe",
23
+ "sha256": "33872113fc0ca3cd2a2f6cf36c33430431388a5e36f254c8a0c6a44932057009"
24
24
  },
25
25
  "win32-x64": {
26
- "url": "https://github.com/tribgames/mixdog/releases/download/spawn-v0.1.11/mixdog-spawn-win32-x64.exe",
27
- "sha256": "50531a9f033dc22eff3ec5b2a9d10441ffdfac0e50d9acf9047e4f302e5d70d9"
26
+ "url": "https://github.com/tribgames/mixdog/releases/download/spawn-v0.1.12/mixdog-spawn-win32-x64.exe",
27
+ "sha256": "0070823fa03a50819077d60a407f4f8e26becafe3541ecdfabe522fc5e05a8cb"
28
28
  }
29
29
  }
30
30
  }