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
@@ -22,7 +22,7 @@ import {
22
22
  waitNativeTask,
23
23
  } from '../lib/native-spawn-client.mjs';
24
24
  import { attachJobInsights, shellJobPublicTaskResult } from './lib/shell-job-insights.mjs';
25
- import { tryAdmitWorkspaceActivity } from '../../../../shared/worktrees/activity-gate.mjs';
25
+ import { tryAdmitWorkspaceActivityAsync } from '../../../../shared/worktrees/activity-gate.mjs';
26
26
  import { worktreeError } from '../../../../shared/worktrees/contract.mjs';
27
27
  import {
28
28
  adoptShellWorkspaceAdmission,
@@ -30,6 +30,7 @@ import {
30
30
  completeShellJobRecord,
31
31
  listShellJobRecords,
32
32
  markShellJobTerminationUnconfirmed,
33
+ pendingWorkspaceAdmission,
33
34
  publishShellJobRecord,
34
35
  recordShellJobHop,
35
36
  registerTerminationHold,
@@ -37,9 +38,12 @@ import {
37
38
  takeReleaseRequest,
38
39
  } from './lib/shell-job-records.mjs';
39
40
  import {
41
+ createContainedTreeTracker,
40
42
  createProcessTreeTracker,
43
+ endWatchWhenUnconfirmable,
41
44
  killAfterSnapshot,
42
45
  PROCESS_TABLE_READ_BOUND_MS,
46
+ sweepLiveIdentities,
43
47
  watchTermination,
44
48
  withinBound,
45
49
  } from '../lib/shell-descendants.mjs';
@@ -61,13 +65,33 @@ const PRE_DEADLINE_CAPTURE_MS = PROCESS_TABLE_READ_BOUND_MS + 1_000;
61
65
  // the promotion baseline predates whatever the job started since (a server,
62
66
  // an npm child); the baseline still stands when the fresh read fails. Marks
63
67
  // the job killed, so its settle relies on this snapshot instead of an
64
- // after-exit observation.
68
+ // after-exit observation. A job the spawn server contains needs none (true at
69
+ // once): its kill is verified by the server's empty-job report.
65
70
  function prepareTrackedKill(entry) {
66
71
  entry.killed = true;
72
+ if (entry.contained) return true;
67
73
  return (entry.baseline || Promise.resolve(false)).then(() =>
68
74
  entry.tracker.capture().catch(() => entry.tracker.complete)
69
75
  );
70
76
  }
77
+
78
+ const FINAL_TREE_OUTCOMES = new Set(['empty', 'unverified', 'released']);
79
+
80
+ /** The tracker of a contained job's killed or emptied tree, driven by the
81
+ * server's outcome (a final one latched: the task entry may expire).
82
+ * Survivors the server kept in the job are cancelled through it. */
83
+ function containedJobTracker(jobId) {
84
+ let final = null;
85
+ return createContainedTreeTracker({
86
+ outcome: () => {
87
+ if (final) return final;
88
+ const outcome = getNativeTask(jobId)?.treeOutcome ?? null;
89
+ if (FINAL_TREE_OUTCOMES.has(outcome)) final = outcome;
90
+ return outcome;
91
+ },
92
+ cancel: () => cancelNativeTask(jobId),
93
+ });
94
+ }
71
95
  import { isPidAlive } from '../../../../shared/pid-liveness.mjs';
72
96
  import { hopOf } from '../../../../shared/bot-hops.mjs';
73
97
  import {
@@ -110,22 +134,30 @@ function demoteBackgroundShellPriority(pid) {
110
134
  // The workspace admission a job record holds is released only once the job's
111
135
  // processes are confirmed gone — the same confirmation path a foreground run
112
136
  // uses, not the native terminal status alone.
113
- function trackShellJobRecord(task, options) {
137
+ // Resolves once both workspace leases are written; the promoted run's own
138
+ // admission is released only after that (handOverLease).
139
+ async function trackShellJobRecord(task, options) {
114
140
  const jobId = String(task?.jobId || '').trim();
115
141
  if (!jobId) return;
116
- publishShellJobRecord(task, options);
142
+ const published = publishShellJobRecord(task, options);
117
143
  if (task?.cwd) {
118
144
  // The record's own reader is filed under the job's root pid, which the
119
145
  // gate drops the moment that pid dies. Survivors must keep the folder
120
146
  // reserved, so this runtime holds it under its own live pid as well.
121
- const hold = tryAdmitWorkspaceActivity(options?.ownerSessionId || jobId, task.cwd, { kind: 'command' });
122
- if (!hold) throw worktreeError('WORKTREE_BUSY_JOBS', 'The command folder is being managed.');
123
- adoptShellWorkspaceAdmission(jobId, hold);
147
+ await published;
148
+ const holding = tryAdmitWorkspaceActivityAsync(options?.ownerSessionId || jobId, task.cwd, { kind: 'command' });
149
+ adoptShellWorkspaceAdmission(jobId, pendingWorkspaceAdmission(holding));
150
+ if (!(await holding)) throw worktreeError('WORKTREE_BUSY_JOBS', 'The command folder is being managed.');
124
151
  }
125
152
  const tracker = task?.cwd ? createProcessTreeTracker({ pid: task.pid }) : null;
153
+ // Killed, or emptied on its own, a contained job is confirmed by the server
154
+ // (containedJobTracker); the process tracker still observes the survivors a
155
+ // root that exited on its own leaves.
156
+ const contained = task?.contained === true;
126
157
  let resolveTerminated = null;
127
158
  const entry = {
128
159
  tracker,
160
+ contained,
129
161
  killed: false,
130
162
  baseline: null,
131
163
  // Settles once every process of the job is confirmed gone (or the user
@@ -150,7 +182,7 @@ function trackShellJobRecord(task, options) {
150
182
  // identities of what the job started since its promotion.
151
183
  const deadlineMs = Math.floor(Number(options?.timeoutMs) || 0);
152
184
  const deadlineCapture =
153
- tracker && deadlineMs > 0
185
+ tracker && !contained && deadlineMs > 0
154
186
  ? setTimeout(
155
187
  () => {
156
188
  if (!settled) void tracker.capture().catch(() => false);
@@ -177,31 +209,45 @@ function trackShellJobRecord(task, options) {
177
209
  // settle does. A killed one relies on the snapshot taken before the kill.
178
210
  const killed = entry.killed || detail.killed || detail.timedOut || String(detail.status) === 'cancelled';
179
211
  if (!killed) tracker.acceptExitObservation();
212
+ const watched =
213
+ contained && (killed || detail.treeOutcome === 'empty') ? containedJobTracker(jobId) : tracker;
180
214
  // The completion notice is already on its way (watchBackgroundShellJob);
181
215
  // the record — and with it the workspace admission — completes only once
182
216
  // every process is confirmed gone, or the user releases it explicitly.
217
+ // A force-release only drops the workspace hold (the record completes);
218
+ // the job stays tracked and observed until its processes are confirmed
219
+ // gone, and only that confirmation reports it terminated.
220
+ let released = null;
221
+ const releaseHold = () => {
222
+ clearTerminationHold(jobId);
223
+ released ??= completeShellJobRecord(jobId, normalized);
224
+ return released;
225
+ };
183
226
  let finished = false;
184
227
  const finish = () => {
185
228
  if (finished) return;
186
229
  finished = true;
187
230
  stop();
188
- clearTerminationHold(jobId);
189
231
  if (backgroundTrackers.get(jobId) === entry) backgroundTrackers.delete(jobId);
190
232
  resolveTerminated(true);
191
- return completeShellJobRecord(jobId, normalized);
233
+ return releaseHold();
192
234
  };
193
235
  const stop = watchTermination({
194
- tracker,
236
+ tracker: endWatchWhenUnconfirmable(watched, { released: () => released !== null, stop: () => stop() }),
195
237
  // A killed job is not over while processes it started still run.
196
238
  sweep: killed,
197
239
  onTerminated: finish,
198
- releaseRequested: () => takeReleaseRequest(jobId),
240
+ // A release requested from another process is the same force-release.
241
+ releaseRequested: () => {
242
+ if (takeReleaseRequest(jobId)) void releaseHold();
243
+ return false;
244
+ },
199
245
  onUnconfirmed: () => {
200
246
  void markShellJobTerminationUnconfirmed(jobId);
201
247
  registerTerminationHold(jobId, {
202
248
  ownerSessionId: options?.ownerSessionId || null,
203
249
  cwd: task.cwd,
204
- release: finish,
250
+ release: releaseHold,
205
251
  });
206
252
  },
207
253
  });
@@ -415,7 +461,27 @@ export function killShellJob(jobId) {
415
461
  // complete, a fresh read). The kill waits for it at most the read bound
416
462
  // and is sent regardless; without a snapshot the outcome stays unknown and
417
463
  // the folder stays held under the observation loop.
418
- void killAfterSnapshot(prepareTrackedKill(tracked), () => cancelNativeTask(jobId));
464
+ // The kill never outruns the promotion baseline itself: a read still in
465
+ // flight takes its table after a kill sent beside it, finds the root gone,
466
+ // and the job could never be confirmed terminated. That read ends by its
467
+ // own enumeration deadline. A contained job is cancelled at once.
468
+ const snapshot = prepareTrackedKill(tracked);
469
+ tracked.killSent =
470
+ snapshot === true
471
+ ? new Promise((resolve) => resolve(cancelNativeTask(jobId)))
472
+ : tracked.baseline.then(() => killAfterSnapshot(snapshot, () => cancelNativeTask(jobId)));
473
+ return { ...attachJobInsights(normalizeShellJobDetail(current)), killed: true };
474
+ }
475
+ if (tracked) {
476
+ // The root already exited but its tracked descendants run on: the stop
477
+ // reaches them through the tracker alone, whether or not the native task
478
+ // is still cached; the termination watch then releases the folder.
479
+ void tracked.tracker
480
+ .observe({ rootExited: true })
481
+ .then(() => sweepLiveIdentities(tracked.tracker))
482
+ .catch(() => {});
483
+ // An expired native entry says nothing about the root, so none is claimed.
484
+ if (!current) return { jobId, killed: true };
419
485
  return { ...attachJobInsights(normalizeShellJobDetail(current)), killed: true };
420
486
  }
421
487
  const detail = cancelNativeTask(jobId);
@@ -424,6 +490,12 @@ export function killShellJob(jobId) {
424
490
  return { ...attachJobInsights(normalizeShellJobDetail(detail)), killed: wasRunning };
425
491
  }
426
492
 
493
+ /** Settles once the kill a stop of the tracked job queued has been sent (at
494
+ * once when none is pending). */
495
+ export function shellJobKillSent(jobId) {
496
+ return Promise.resolve(backgroundTrackers.get(String(jobId || ''))?.killSent).catch(() => {});
497
+ }
498
+
427
499
  /** True once every process of a tracked background job is confirmed gone,
428
500
  * false while any is still alive when `timeoutMs` passes. A job without a
429
501
  * process tracker (or one already finished) has nothing left to confirm. */
@@ -572,7 +644,7 @@ export async function promoteForegroundShellJob({
572
644
  }
573
645
  // Added before tracking: the record's settlement removes it again.
574
646
  promotedJobIds.add(native.jobId);
575
- trackShellJobRecord(native, { ownerSessionId, clientHostPid, startedAtMs: started, timeoutMs });
647
+ await trackShellJobRecord(native, { ownerSessionId, clientHostPid, startedAtMs: started, timeoutMs });
576
648
  return native;
577
649
  }
578
650
 
@@ -606,8 +678,8 @@ export function publishForegroundShellRecord({
606
678
  clientHostPid = null,
607
679
  terminationUnconfirmed = false,
608
680
  }) {
609
- if (!jobId || !Number.isFinite(Number(pid)) || Number(pid) <= 0) return;
610
- publishShellJobRecord(
681
+ if (!jobId || !Number.isFinite(Number(pid)) || Number(pid) <= 0) return Promise.resolve();
682
+ return publishShellJobRecord(
611
683
  { jobId, command, cwd, pid: Number(pid), shellType },
612
684
  {
613
685
  ownerSessionId,
@@ -696,8 +768,9 @@ export async function shutdownShellJobs(_reason = 'runtime-close', { scope = nul
696
768
  let cancelledWatchers = 0;
697
769
  // Identities first, as every other kill does: once a root dies, what it
698
770
  // started outside its process group is reparented and can no longer be
699
- // found. The cancel is sent at the snapshot bound regardless.
700
- await killAfterSnapshot(Promise.all(prepareOwnedShellTreeKills(scoped ? ownerSessionId : null)), () => {
771
+ // found. The cancel is sent at the snapshot bound regardless — and at once
772
+ // when no snapshot is pending (every tree contained, or already captured).
773
+ const cancelAll = () => {
701
774
  for (const task of jobs) {
702
775
  if (cancelBackgroundShellJobWatch(task.jobId)) cancelledWatchers += 1;
703
776
  try {
@@ -705,7 +778,10 @@ export async function shutdownShellJobs(_reason = 'runtime-close', { scope = nul
705
778
  } catch {}
706
779
  jobNotifyCtxByJobId.delete(task.jobId);
707
780
  }
708
- });
781
+ };
782
+ const snapshots = prepareOwnedShellTreeKills(scoped ? ownerSessionId : null);
783
+ if (snapshots.length === 0) cancelAll();
784
+ else await killAfterSnapshot(Promise.all(snapshots), cancelAll);
709
785
  const settled = await Promise.all(jobs.map((task) => waitNativeTask(task.jobId, 1_200).catch(() => null)));
710
786
  // A process still running after the wait is not forgotten: it keeps being
711
787
  // stopped until its exit is confirmed.
@@ -1,11 +1,13 @@
1
1
  // Generic background-task tool (status/wait/cancel).
2
2
  import {
3
3
  buildJobNotFoundMessage,
4
+ isShellJobRunning,
4
5
  peekShellJob,
5
6
  killShellJob,
6
7
  otherSessionBackgroundShellJobCount,
7
8
  cancelBackgroundShellJobWatch,
8
9
  clearShellJobNotifyCtx,
10
+ shellJobKillSent,
9
11
  shellJobPublicTaskResult,
10
12
  shellJobTaskStatus,
11
13
  waitForShellJob,
@@ -21,6 +23,7 @@ import {
21
23
  cancelBackgroundTask,
22
24
  completeBackgroundTask,
23
25
  getBackgroundTask,
26
+ listBackgroundTasks,
24
27
  registerBackgroundTask,
25
28
  renderBackgroundTask,
26
29
  renderBackgroundTaskList,
@@ -227,12 +230,16 @@ async function cancelShellTaskOutcome(task, taskId, options) {
227
230
  // their shell). Remember that it WAS live so the outcome is
228
231
  // reported as a cancellation rather than "task not found".
229
232
  const wasRunning = task.status === 'running';
233
+ // A tracked tree outlives the native task's cache entry: its own
234
+ // termination watch, not the native task, confirms the stop.
235
+ const trackedTree = isShellJobRunning(taskId);
230
236
  const job = killShellJob(taskId);
231
237
  if (job) {
232
238
  // Confirm the process actually terminated before reporting a
233
239
  // terminal status. Removing the watcher on the strength of a
234
240
  // delivered signal left a live, unmonitored process behind a
235
- // false 'cancelled'.
241
+ // false 'cancelled'. The window opens once the kill is actually sent.
242
+ await shellJobKillSent(taskId);
236
243
  const confirmed = await waitForShellJob(taskId, { timeoutMs: TASK_CANCEL_CONFIRM_MS });
237
244
  if (confirmed && confirmed.status === 'running') {
238
245
  // Watcher and notify context stay armed: the eventual exit
@@ -248,10 +255,10 @@ async function cancelShellTaskOutcome(task, taskId, options) {
248
255
  }
249
256
  // The root exited; what it started outside its process group is swept
250
257
  // by the job's termination watch. Cancelled means all of it is gone.
251
- if (confirmed && !(await waitShellJobTerminated(taskId, TASK_CANCEL_TREE_CONFIRM_MS))) {
258
+ if ((confirmed || trackedTree) && !(await waitShellJobTerminated(taskId, TASK_CANCEL_TREE_CONFIRM_MS))) {
252
259
  return { outcome: 'unconfirmed', text: cancelPendingText(taskId) };
253
260
  }
254
- if (!confirmed) {
261
+ if (!confirmed && !trackedTree) {
255
262
  // No terminal event and no observable task: absence of
256
263
  // state is not proof of termination. Stop tracking it so it
257
264
  // cannot hang as 'running' forever, but never claim it died.
@@ -273,7 +280,7 @@ async function cancelShellTaskOutcome(task, taskId, options) {
273
280
  cancelBackgroundTask(taskId, 'cancelled by task control');
274
281
  // A task whose own cancel hook kills (what a finished command left running)
275
282
  // reports what that kill confirmed.
276
- const hookKill = job ? null : await task.cancelOutcome;
283
+ const hookKill = await task.cancelOutcome;
277
284
  if (hookKill && hookKill.terminated !== true) return { outcome: 'unconfirmed', text: cancelPendingText(taskId) };
278
285
  return job || wasRunning
279
286
  ? {
@@ -298,22 +305,64 @@ export async function stopBackgroundShellJob(taskId, options = {}) {
298
305
  const id = typeof taskId === 'string' ? taskId.trim() : '';
299
306
  if (!id) return { stopped: false, error: 'task_id is required' };
300
307
  const task = getBackgroundTask(id, { context: options });
301
- if (!task || task.surface !== 'shell') return { stopped: false, error: `task not found: ${id}` };
308
+ if (task?.surface !== 'shell') return { stopped: false, error: `task not found: ${id}` };
302
309
  const job = peekShellJob(id);
303
310
  const status = job ? job.status : task.status;
304
- if (status !== 'running') return { stopped: false, error: `task already ${status}: ${id}` };
311
+ // A root that exited while tracked descendants run is still stoppable, and
312
+ // so is the survivors task of a finished command (`detached`): its own
313
+ // registry status, not the finished command's cached native entry, says
314
+ // whether anything still runs.
315
+ const survivors = task.detached === true && task.status === 'running';
316
+ if (status !== 'running' && !isShellJobRunning(id) && !survivors)
317
+ return { stopped: false, error: `task already ${status}: ${id}` };
305
318
  try {
306
319
  const { outcome, text } = await cancelShellTaskOutcome(task, id, options);
307
320
  if (outcome === 'cancelled') return { stopped: true };
308
- return {
309
- stopped: false,
310
- error: outcome === 'unconfirmed' ? `cancellation not confirmed: ${id}` : text.replace(/^Error:\s*/, ''),
311
- };
321
+ if (outcome === 'unconfirmed')
322
+ return { stopped: false, unconfirmed: true, error: `cancellation not confirmed: ${id}` };
323
+ return { stopped: false, error: text.replace(/^Error:\s*/, '') };
312
324
  } catch (error) {
313
325
  return { stopped: false, error: error instanceof Error ? error.message : String(error) };
314
326
  }
315
327
  }
316
328
 
329
+ // The running shell tasks the caller's session owns (promoted jobs and the
330
+ // survivors a finished command left), including a task whose command already
331
+ // exited while its tracked descendants still run (what the island lists as
332
+ // termination unconfirmed). A foreground command holds no background task, so
333
+ // it never appears here.
334
+ function sessionShellTasks(options) {
335
+ const sessionId = String(options.callerSessionId || '').trim();
336
+ if (!sessionId) return [];
337
+ return listBackgroundTasks({ ...options, surface: 'shell' })
338
+ .filter((row) => row.status === 'running' || isShellJobRunning(row.task_id))
339
+ .map((row) => getBackgroundTask(row.task_id))
340
+ .filter((task) => task?.ownerSessionId === sessionId);
341
+ }
342
+
343
+ /** What `/stop` would stop: this session's running background shells. */
344
+ export function listStoppableShellJobs(options = {}) {
345
+ return sessionShellTasks(options).map((task) => ({
346
+ taskId: task.taskId,
347
+ command: String(task.input?.command || task.label || ''),
348
+ }));
349
+ }
350
+
351
+ /**
352
+ * Stop every background shell of the caller's session, each through the same
353
+ * confirmed cancel as a single stop. `stopping` counts the ones whose
354
+ * processes are not yet confirmed gone (they keep being terminated).
355
+ */
356
+ export async function stopBackgroundShellJobs(options = {}) {
357
+ const results = await Promise.all(
358
+ sessionShellTasks(options).map((task) => stopBackgroundShellJob(task.taskId, options))
359
+ );
360
+ return {
361
+ stopped: results.filter((result) => result.stopped).length,
362
+ stopping: results.filter((result) => result.unconfirmed).length,
363
+ };
364
+ }
365
+
317
366
  export async function executeTaskTool(args, options = {}) {
318
367
  const action = typeof args.action === 'string' ? args.action.toLowerCase() : '';
319
368
  if (!action) return 'Error: task action is required';
@@ -1,30 +1,30 @@
1
1
  {
2
- "version": "0.1.34",
2
+ "version": "0.1.35",
3
3
  "_comment": "Synced from immutable graph-v release assets.",
4
4
  "assets": {
5
5
  "darwin-arm64": {
6
- "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.34/mixdog-graph-darwin-arm64",
7
- "sha256": "5bcf8c79135224d2e77eec70a57debd06d462bab01418ce1a6c1656a858eedad"
6
+ "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.35/mixdog-graph-darwin-arm64",
7
+ "sha256": "f7a6a0bf10e4a541e9f7c529388fc14401b5f1727a5e8629fbeaf17c094300b2"
8
8
  },
9
9
  "darwin-x64": {
10
- "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.34/mixdog-graph-darwin-x64",
11
- "sha256": "2845ea2afa2802841589bd74c8785670cfe8c6b58b2ad5b258ac928afccbaa94"
10
+ "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.35/mixdog-graph-darwin-x64",
11
+ "sha256": "a0d2bc124e264f7c5699af71adae3f047ee07591ddff415274bea23935bdbb75"
12
12
  },
13
13
  "linux-arm64": {
14
- "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.34/mixdog-graph-linux-arm64",
15
- "sha256": "0364bc1c3741683fbdb45ed755f2baabb50a0556e2354c13a05f3d816bc1c461"
14
+ "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.35/mixdog-graph-linux-arm64",
15
+ "sha256": "ed5d75b8cd3a4de5366c233cfd55b07931b56fac7dd82fadad6a7b172ced97ed"
16
16
  },
17
17
  "linux-x64": {
18
- "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.34/mixdog-graph-linux-x64",
19
- "sha256": "4f9091b2c5a89631efade89aae17e5411dda8bfec99e15ff64948906f9fa20d0"
18
+ "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.35/mixdog-graph-linux-x64",
19
+ "sha256": "b9b54529264d93648c16142ad05ba706c2757304688ccbf7d89e1a46762f4c13"
20
20
  },
21
21
  "win32-arm64": {
22
- "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.34/mixdog-graph-win32-arm64.exe",
23
- "sha256": "03abef6f09b095b86c5cc32c6bead3f5d82f77961220c31221ab0b3f60974110"
22
+ "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.35/mixdog-graph-win32-arm64.exe",
23
+ "sha256": "dd9fb667ddeb366dbfeabf58b3023e8b0f40b09edccc50dc5902b33de97de233"
24
24
  },
25
25
  "win32-x64": {
26
- "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.34/mixdog-graph-win32-x64.exe",
27
- "sha256": "650cffe82ccbe9898e7ac8fc9b025c92098a810ad3ca95276c0bb8c19a328993"
26
+ "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.35/mixdog-graph-win32-x64.exe",
27
+ "sha256": "be2abdb0b1c28647332531ac149a4ebba17e8cfb7093b7d5694a4be8bd21e369"
28
28
  }
29
29
  }
30
30
  }
@@ -13,6 +13,51 @@ const RESTART_BACKOFF_MS = 30_000;
13
13
  const SERVER_READY_TIMEOUT_MS = 5_000;
14
14
  const REQUIRED_LIFECYCLE_CAPS = Object.freeze(['trackedForeground', 'promoteTask', 'cancelOwner']);
15
15
 
16
+ // Windows containment. A spawn that asks for it (`spawnOptions.contain`, the
17
+ // shell runner's commands) runs in a job object it cannot break away from, and
18
+ // the server reports what became of that job's tree (`treeOutcome`, below):
19
+ // verified empty, the whole tree is gone, so a kill needs no process-table
20
+ // snapshot first. A program that must start children with
21
+ // CREATE_BREAKAWAY_FROM_JOB (some installers, launchers) fails to under it.
22
+ // `MIXDOG_SHELL_ALLOW_BREAKAWAY=1` in a command's OWN environment
23
+ // (`spawnOptions.env`) opts that command out; in Mixdog's environment it is
24
+ // the emergency switch for every command. Either way the command keeps the
25
+ // conservative process-table tracking. A failed command is never re-run
26
+ // without containment: it may already have had side effects.
27
+ // Servers without the `verifiedContainment` capability are never asked.
28
+ export const ALLOW_BREAKAWAY_ENV = 'MIXDOG_SHELL_ALLOW_BREAKAWAY';
29
+
30
+ /** Whether a spawn with `spawnOptions` asks the server `caps` for containment. */
31
+ export function nativeSpawnContainmentRequested(
32
+ caps,
33
+ spawnOptions = {},
34
+ { platform = process.platform, env = process.env } = {}
35
+ ) {
36
+ return (
37
+ spawnOptions?.contain === true &&
38
+ platform === 'win32' &&
39
+ caps?.verifiedContainment === true &&
40
+ env[ALLOW_BREAKAWAY_ENV] !== '1' &&
41
+ spawnOptions.env?.[ALLOW_BREAKAWAY_ENV] !== '1'
42
+ );
43
+ }
44
+
45
+ // treeOutcome of a contained spawn (null until known):
46
+ // 'empty' verified: every process of the tree has exited (final)
47
+ // 'pending' killed, the server is still verifying
48
+ // 'preserved' the root exited on its own before any kill reached it; the
49
+ // server keeps its survivors in their job for a while, where a
50
+ // targeted cancel (`cancel` by request id, `cancelTask`) still
51
+ // kills exactly them and is verified like any kill
52
+ // 'unverified' killed, but the job was never verified empty (final)
53
+ // 'released' the kept survivors were let go unobserved (final). With
54
+ // `breakawayRestored: false` (on `tree_released`, `exit`, task
55
+ // snapshots) they were not even let go: they stay in a job they
56
+ // cannot leave. Either way unknown for good, never gone.
57
+ const FINAL_TREE_OUTCOMES = new Set(['empty', 'unverified', 'released']);
58
+ const treeOutcomeOf = (value) =>
59
+ value === 'pending' || value === 'preserved' || FINAL_TREE_OUTCOMES.has(value) ? value : null;
60
+
16
61
  let _server = null;
17
62
  let _binaryPath;
18
63
  let _lastFailureAt = 0;
@@ -205,12 +250,19 @@ function normalizeTask(task) {
205
250
  stdoutPath: override?.stdoutPath || null,
206
251
  stderrPath: override?.stderrPath || null,
207
252
  mergeStderr: false,
253
+ contained: task.contained === true,
254
+ treeOutcome: treeOutcomeOf(task.treeOutcome),
208
255
  };
209
256
  }
210
257
 
211
258
  function acceptTask(message) {
212
259
  const task = normalizeTask(message?.task);
213
260
  if (!task) return null;
261
+ const known = _tasks.get(task.jobId);
262
+ // A final outcome stands: a snapshot taken before its report is late.
263
+ if (FINAL_TREE_OUTCOMES.has(known?.treeOutcome) && known.requestId === task.requestId) {
264
+ task.treeOutcome = known.treeOutcome;
265
+ }
214
266
  _tasks.set(task.jobId, task);
215
267
  _markTerminalTask(task.jobId, task.status);
216
268
  _taskEvents.emit(task.jobId, task);
@@ -260,6 +312,13 @@ class NativeSpawnChild extends EventEmitter {
260
312
  this._cancel = cancel;
261
313
  this._send = send;
262
314
  this._closed = false;
315
+ // Windows kernel creation time (FILETIME ticks, decimal) from the server's
316
+ // 'spawned' event; null from servers that do not report it.
317
+ this.creationTime = null;
318
+ // Spawned into a job it cannot leave, and what the server reported about
319
+ // that job's tree (treeOutcomeOf).
320
+ this.contained = false;
321
+ this.treeOutcome = null;
263
322
  }
264
323
 
265
324
  // stdinPipe children only (warm shell standby): forward script text to the
@@ -394,6 +453,21 @@ function _teardown(error, { kill = true } = {}) {
394
453
  }
395
454
  }
396
455
 
456
+ // The server's outcome for the tree of request `id`. Only a request of THIS
457
+ // server still awaiting a final outcome takes it: a lost server's trees stay
458
+ // unconfirmed, whatever a new server later says about a reused request id.
459
+ function _settleTree(server, id, outcome) {
460
+ const entry = server.trees.get(id);
461
+ if (!entry || FINAL_TREE_OUTCOMES.has(entry.child.treeOutcome)) return;
462
+ entry.child.treeOutcome = outcome;
463
+ if (FINAL_TREE_OUTCOMES.has(outcome)) server.trees.delete(id);
464
+ const task = entry.jobId ? _tasks.get(entry.jobId) : null;
465
+ if (task && task.requestId === id && !FINAL_TREE_OUTCOMES.has(task.treeOutcome)) {
466
+ task.treeOutcome = outcome;
467
+ _taskEvents.emit(entry.jobId, { ...task });
468
+ }
469
+ }
470
+
397
471
  export async function shutdownNativeSpawnServer(reason = 'process-exit', timeoutMs = 1_000) {
398
472
  const server = _server;
399
473
  if (!server) return true;
@@ -442,7 +516,9 @@ function _ensureServer() {
442
516
  _lastFailureAt = Date.now();
443
517
  return null;
444
518
  }
445
- const server = { child, pending: new Map(), sequence: 0, ready: false, caps: {} };
519
+ // trees: request id -> { child, jobId } of contained spawns awaiting a final
520
+ // tree outcome (it may follow their exit).
521
+ const server = { child, pending: new Map(), trees: new Map(), sequence: 0, ready: false, caps: {} };
446
522
  let resolveReady;
447
523
  server.readyPromise = new Promise((resolve) => {
448
524
  resolveReady = resolve;
@@ -472,7 +548,16 @@ function _ensureServer() {
472
548
  const jobId = String(message?.jobId || '');
473
549
  if (jobId) _dropTaskEntry(jobId, { notifyServer: false });
474
550
  } else if (event.startsWith('task_')) {
475
- acceptTask(message);
551
+ const task = acceptTask(message);
552
+ // A job id assigned by the server (background) or later (track).
553
+ const tree = task ? server.trees.get(task.requestId) : null;
554
+ if (tree) tree.jobId = task.jobId;
555
+ } else if (event === 'tree_empty') {
556
+ _settleTree(server, Number(message?.id), 'empty');
557
+ } else if (event === 'tree_unverified') {
558
+ _settleTree(server, Number(message?.id), 'unverified');
559
+ } else if (event === 'tree_released') {
560
+ _settleTree(server, Number(message?.id), 'released');
476
561
  }
477
562
  const pending = server.pending.get(Number(message?.id));
478
563
  if (!pending) return;
@@ -570,6 +655,7 @@ export function tryNativeSpawn({ shell, argv, spawnOptions = {}, cwd } = {}) {
570
655
  // fields and keep piping, so the capability gate decides rather than hope.
571
656
  const _fileCapture =
572
657
  server.caps?.fileCapture === true && Boolean(spawnOptions.stdoutPath) && Boolean(spawnOptions.stderrPath);
658
+ const contain = nativeSpawnContainmentRequested(server.caps, spawnOptions);
573
659
  const request = {
574
660
  id,
575
661
  program: String(shell || ''),
@@ -589,6 +675,7 @@ export function tryNativeSpawn({ shell, argv, spawnOptions = {}, cwd } = {}) {
589
675
  }
590
676
  : {}),
591
677
  ...(spawnOptions.stdinPipe === true ? { stdinPipe: true } : {}),
678
+ ...(contain ? { contain: true } : {}),
592
679
  command: spawnOptions.command || undefined,
593
680
  shellType: spawnOptions.shellType || undefined,
594
681
  ownerSessionId: spawnOptions.ownerSessionId || undefined,
@@ -611,6 +698,12 @@ export function tryNativeSpawn({ shell, argv, spawnOptions = {}, cwd } = {}) {
611
698
  const event = String(message?.event || '');
612
699
  if (event === 'spawned') {
613
700
  fake.pid = Number(message.pid) || undefined;
701
+ if (/^\d+$/.test(String(message.creationTime ?? ''))) fake.creationTime = String(message.creationTime);
702
+ // Only a containment the request asked for counts.
703
+ if (contain && message.contained === true) {
704
+ fake.contained = true;
705
+ server.trees.set(id, { child: fake, jobId: request.jobId || null });
706
+ }
614
707
  fake.emit('spawn');
615
708
  return;
616
709
  }
@@ -628,6 +721,10 @@ export function tryNativeSpawn({ shell, argv, spawnOptions = {}, cwd } = {}) {
628
721
  server.pending.delete(id);
629
722
  fake.exitCode = message.code == null ? null : Number(message.code);
630
723
  fake.signalCode = message.signal || null;
724
+ // The outcome when the server finished the request; a pending one is
725
+ // made final by a later report. A contained exit without one (a
726
+ // malformed or partial report) proves nothing.
727
+ if (fake.contained) _settleTree(server, id, treeOutcomeOf(message.treeOutcome) ?? 'unverified');
631
728
  fake._closed = true;
632
729
  fake.emit('close', fake.exitCode, fake.signalCode);
633
730
  _recomputeServerRef(server);
@@ -5,6 +5,8 @@
5
5
  // PROCESS_TABLE_READ_BOUND_MS, and then sends the kill whether or not they
6
6
  // arrived. A missing snapshot leaves the outcome unknown, so the workspace
7
7
  // stays held under the observation loop instead of being released blind.
8
+ // A tree the spawn server contains (Windows job) needs no snapshot: its
9
+ // prepareKill answers true, so a cancel of only such trees is sent at once.
8
10
  import { cancelNativeTasks } from './native-spawn-client.mjs';
9
11
  import { killAfterSnapshot } from './shell-descendants.mjs';
10
12