mixdog 0.9.147 → 0.9.149

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 (265) hide show
  1. package/package.json +23 -3
  2. package/scripts/build-voice-runtime-unix.sh +101 -0
  3. package/scripts/embedding-quality-probe.mjs +96 -0
  4. package/scripts/generate-voice-runtime-manifest.mjs +300 -0
  5. package/scripts/generate-voice-runtime-manifest.test.mjs +135 -0
  6. package/scripts/native-binary-arch.mjs +30 -13
  7. package/scripts/native-binary-arch.test.mjs +40 -4
  8. package/scripts/plan-voice-runtime-release.mjs +73 -0
  9. package/scripts/prepare-native-assets.mjs +50 -9
  10. package/scripts/prune-embedding-runtime.mjs +3 -3
  11. package/scripts/sync-voice-runtime-manifest.mjs +46 -0
  12. package/scripts/verify-release-assets.mjs +106 -6
  13. package/scripts/verify-voice-runtime-release.mjs +89 -0
  14. package/scripts/voice-runtime-config.json +52 -0
  15. package/src/app.mjs +42 -5
  16. package/src/headless-exec.mjs +58 -32
  17. package/src/headless-exec.test.mjs +74 -13
  18. package/src/rules/lead/01-general.md +2 -0
  19. package/src/rules/shared/10-tool-workflow.md +7 -4
  20. package/src/rules/shared/30-exploration.md +5 -1
  21. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +28 -9
  22. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +6 -1
  23. package/src/runtime/agent/orchestrator/config.mjs +53 -18
  24. package/src/runtime/agent/orchestrator/mcp/cancellation.test.mjs +139 -0
  25. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +34 -5
  26. package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -10
  27. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +5 -0
  28. package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +98 -0
  29. package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.test.mjs +90 -0
  30. package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +67 -16
  31. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +4 -82
  32. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +0 -5
  33. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +62 -121
  34. package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.mjs +48 -0
  35. package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.test.mjs +215 -0
  36. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +44 -48
  37. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +54 -31
  38. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +56 -2
  39. package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +34 -2
  40. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +61 -1
  41. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +6 -1
  42. package/src/runtime/agent/orchestrator/providers/gemini.mjs +61 -15
  43. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +120 -1
  44. package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +9 -0
  45. package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +0 -3
  46. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +187 -84
  47. package/src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs +32 -9
  48. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +78 -33
  49. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +21 -0
  50. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +36 -36
  51. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +16 -6
  52. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +50 -213
  53. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +135 -126
  54. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +20 -11
  55. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +112 -36
  56. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +220 -50
  57. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +112 -9
  58. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +11 -0
  59. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +130 -60
  60. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -2
  61. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +28 -10
  62. package/src/runtime/agent/orchestrator/providers/registry.mjs +122 -95
  63. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +69 -3
  64. package/src/runtime/agent/orchestrator/providers/tool-stream-state.mjs +4 -22
  65. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +7 -14
  66. package/src/runtime/agent/orchestrator/session/compact-policy.test.mjs +128 -0
  67. package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +5 -0
  68. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +27 -48
  69. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +19 -14
  70. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -43
  71. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +5 -1
  72. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +43 -5
  73. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +15 -2
  74. package/src/runtime/agent/orchestrator/session/manager/pending-messages-persistence.test.mjs +325 -0
  75. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +599 -195
  76. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +22 -0
  77. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +9 -2
  78. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +2 -0
  79. package/src/runtime/agent/orchestrator/session/manager.mjs +3 -0
  80. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +26 -16
  81. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +23 -11
  82. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +118 -3
  83. package/src/runtime/agent/orchestrator/session/store-summary-reader.test.mjs +332 -0
  84. package/src/runtime/agent/orchestrator/session/store.mjs +38 -1
  85. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +27 -6
  86. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -20
  87. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +291 -128
  88. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +46 -9
  89. package/src/runtime/agent/orchestrator/tools/builtin/edit-encoding-eol.test.mjs +224 -0
  90. package/src/runtime/agent/orchestrator/tools/builtin/edit-sequential-occupation.test.mjs +53 -3
  91. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +255 -25
  92. package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +87 -0
  93. package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +1 -1
  94. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +132 -9
  95. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +10 -0
  96. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-pattern-fanout.mjs +29 -3
  97. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +1 -1
  98. package/src/runtime/agent/orchestrator/tools/builtin/list-tool-integrity.test.mjs +14 -0
  99. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +180 -31
  100. package/src/runtime/agent/orchestrator/tools/builtin/local-search-telemetry.mjs +4 -1
  101. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +32 -5
  102. package/src/runtime/agent/orchestrator/tools/builtin/native-search-health.test.mjs +5 -2
  103. package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +10 -1
  104. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +38 -10
  105. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +12 -0
  106. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +31 -1
  107. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +11 -1
  108. package/src/runtime/agent/orchestrator/tools/builtin/read-windows.mjs +22 -4
  109. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +6 -8
  110. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +25 -24
  111. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +152 -133
  112. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +114 -42
  113. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +129 -15
  114. package/src/runtime/agent/orchestrator/tools/code-graph/loose-file-target.test.mjs +72 -0
  115. package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +7 -8
  116. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +4 -4
  117. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +201 -19
  118. package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +26 -3
  119. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +10 -1
  120. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-search-scope.test.mjs +55 -0
  121. package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +115 -25
  122. package/src/runtime/agent/orchestrator/tools/code-graph/text-span-mask.test.mjs +268 -0
  123. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -3
  124. package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +296 -46
  125. package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.test.mjs +104 -0
  126. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +136 -0
  127. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +127 -38
  128. package/src/runtime/agent/orchestrator/tools/patch/engine-contract-gate.test.mjs +661 -0
  129. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +350 -9
  130. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +418 -44
  131. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +27 -3
  132. package/src/runtime/agent/orchestrator/tools/patch/patch-codec-eol.test.mjs +552 -0
  133. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +65 -15
  134. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  135. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +284 -109
  136. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +51 -21
  137. package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +44 -27
  138. package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +6 -1
  139. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
  140. package/src/runtime/attachments/store.mjs +11 -3
  141. package/src/runtime/channels/data/voice-runtime-manifest.json +46 -70
  142. package/src/runtime/channels/lib/event-queue.mjs +51 -41
  143. package/src/runtime/channels/lib/scheduler.mjs +23 -28
  144. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -17
  145. package/src/runtime/channels/lib/voice-runtime-fetcher.test.mjs +17 -0
  146. package/src/runtime/channels/lib/webhook.mjs +39 -20
  147. package/src/runtime/channels/lib/whisper-server.mjs +46 -1
  148. package/src/runtime/channels/lib/worker-main.mjs +3 -1
  149. package/src/runtime/media/adapters/gemini-video.mjs +20 -4
  150. package/src/runtime/media/jobs.mjs +13 -0
  151. package/src/runtime/media/renditions.mjs +7 -1
  152. package/src/runtime/media/store.mjs +42 -22
  153. package/src/runtime/memory/index.mjs +4 -9
  154. package/src/runtime/memory/lib/agent-ipc.mjs +1 -6
  155. package/src/runtime/memory/lib/archive-security.test.mjs +21 -1
  156. package/src/runtime/memory/lib/core-memory-store.mjs +28 -50
  157. package/src/runtime/memory/lib/embedding-model-config.mjs +37 -1
  158. package/src/runtime/memory/lib/embedding-model-config.test.mjs +33 -0
  159. package/src/runtime/memory/lib/embedding-provider.mjs +34 -12
  160. package/src/runtime/memory/lib/embedding-worker.mjs +37 -5
  161. package/src/runtime/memory/lib/ko-morph.mjs +46 -15
  162. package/src/runtime/memory/lib/memory-action-handlers.mjs +18 -1
  163. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -18
  164. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +1 -18
  165. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +98 -45
  166. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +71 -0
  167. package/src/runtime/memory/lib/memory-cycle2-shared.test.mjs +78 -0
  168. package/src/runtime/memory/lib/memory-cycle2.mjs +37 -3
  169. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
  170. package/src/runtime/memory/lib/memory-embedding-migration.test.mjs +50 -0
  171. package/src/runtime/memory/lib/memory-recall-store.mjs +9 -24
  172. package/src/runtime/memory/lib/memory.mjs +50 -12
  173. package/src/runtime/memory/lib/pg/process.mjs +1 -10
  174. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -9
  175. package/src/runtime/memory/lib/query-handlers.mjs +2 -4
  176. package/src/runtime/memory/lib/session-ingest-runtime.mjs +2 -2
  177. package/src/runtime/shared/background-tasks.mjs +15 -0
  178. package/src/runtime/shared/background-tasks.test.mjs +78 -0
  179. package/src/runtime/shared/child-guardian.mjs +2 -10
  180. package/src/runtime/shared/pid-liveness.mjs +27 -0
  181. package/src/runtime/shared/pristine-execution-contract.json +0 -1
  182. package/src/runtime/shared/runtime-root.mjs +1 -11
  183. package/src/runtime/shared/service-discovery.mjs +1 -17
  184. package/src/runtime/shared/singleton-owner.mjs +1 -11
  185. package/src/runtime/shared/staged-update.mjs +2 -12
  186. package/src/runtime/shared/tool-status.mjs +5 -1
  187. package/src/runtime/shared/tool-surface.mjs +15 -7
  188. package/src/runtime/shared/turn-snapshot-store.mjs +168 -0
  189. package/src/runtime/shared/turn-snapshot.mjs +137 -13
  190. package/src/runtime/shared/turn-worktree-snapshot.mjs +110 -18
  191. package/src/runtime/shared/webhook-session-run.mjs +16 -2
  192. package/src/runtime/web-search/lib/http-fetch.mjs +34 -4
  193. package/src/runtime/web-search/lib/state.mjs +15 -15
  194. package/src/runtime/web-search/lib/web-tools.mjs +46 -5
  195. package/src/session-runtime/context-status.mjs +4 -12
  196. package/src/session-runtime/runtime-core.mjs +20 -1
  197. package/src/session-runtime/session-lifecycle.mjs +9 -0
  198. package/src/standalone/agent-dispatch-broker.mjs +33 -2
  199. package/src/standalone/agent-tool/job-task-reconcile.mjs +78 -0
  200. package/src/standalone/agent-tool/notify.mjs +3 -1
  201. package/src/standalone/agent-tool/spawn-flow.mjs +52 -70
  202. package/src/standalone/agent-tool/tag-registry.mjs +116 -33
  203. package/src/standalone/agent-tool/tool-def.mjs +2 -2
  204. package/src/standalone/agent-tool/worker-index.mjs +8 -11
  205. package/src/standalone/agent-tool/worker-rows.mjs +4 -1
  206. package/src/standalone/agent-tool.mjs +20 -55
  207. package/src/standalone/agent-watchdog-registry.mjs +28 -4
  208. package/src/standalone/channel-client.mjs +47 -15
  209. package/src/standalone/channel-transport.mjs +44 -25
  210. package/src/standalone/daemon.mjs +31 -2
  211. package/src/standalone/hook-bus/config.mjs +25 -9
  212. package/src/standalone/hook-bus/handlers.mjs +16 -52
  213. package/src/standalone/hook-bus.mjs +29 -23
  214. package/src/standalone/memory-runtime-proxy.mjs +1 -16
  215. package/src/standalone/session-client.mjs +86 -23
  216. package/src/standalone/session-protocol.mjs +0 -2
  217. package/src/standalone/session-runtime-host-health.test.mjs +5 -4
  218. package/src/standalone/session-runtime-host.mjs +101 -6
  219. package/src/standalone/session-runtime-record.mjs +1 -5
  220. package/src/standalone/session-runtime-worker.mjs +52 -57
  221. package/src/standalone/session-service.mjs +4 -1
  222. package/src/standalone/session-transport.mjs +56 -25
  223. package/src/tui/App.jsx +40 -49
  224. package/src/tui/app/app-view.jsx +14 -21
  225. package/src/tui/app/core-memory-picker.mjs +29 -16
  226. package/src/tui/app/create-app-pickers.mjs +15 -36
  227. package/src/tui/app/extension-pickers.mjs +60 -49
  228. package/src/tui/app/maintenance-pickers.mjs +100 -57
  229. package/src/tui/app/message-selector.mjs +6 -5
  230. package/src/tui/app/model-picker.mjs +30 -13
  231. package/src/tui/app/onboarding-steps.mjs +21 -12
  232. package/src/tui/app/panel-epoch.mjs +56 -0
  233. package/src/tui/app/panel-epoch.test.mjs +1009 -0
  234. package/src/tui/app/panel-surface.mjs +98 -0
  235. package/src/tui/app/project-picker.mjs +30 -29
  236. package/src/tui/app/prompt-submit.mjs +192 -97
  237. package/src/tui/app/prompt-submit.test.mjs +206 -0
  238. package/src/tui/app/provider-setup-picker.mjs +74 -43
  239. package/src/tui/app/resume-picker.mjs +11 -8
  240. package/src/tui/app/route-pickers.mjs +45 -28
  241. package/src/tui/app/settings-picker.mjs +69 -18
  242. package/src/tui/app/shell-layout.mjs +1 -3
  243. package/src/tui/app/slash-commands.mjs +0 -2
  244. package/src/tui/app/slash-dispatch.mjs +4 -2
  245. package/src/tui/app/text-entry-policy.mjs +25 -0
  246. package/src/tui/app/theme-effort-pickers.mjs +11 -16
  247. package/src/tui/app/usage-context-panels.mjs +21 -25
  248. package/src/tui/app/use-global-key-input.mjs +3 -2
  249. package/src/tui/app/use-prompt-draft-flow.mjs +6 -18
  250. package/src/tui/app/use-prompt-handlers.mjs +3 -2
  251. package/src/tui/app/use-welcome-prompt-hint.mjs +17 -3
  252. package/src/tui/components/Picker.jsx +1 -1
  253. package/src/tui/components/TextEntryPanel.jsx +7 -2
  254. package/src/tui/dist/index.mjs +652 -561
  255. package/src/tui/lib/voice-setup.mjs +1 -2
  256. package/src/tui/session/agent-job-feed.mjs +73 -11
  257. package/src/tui/session/session-api-ext.mjs +5 -6
  258. package/src/tui/session/session-api.mjs +1 -0
  259. package/src/tui/session/tool-approval.mjs +15 -5
  260. package/src/tui/session/tui-steering-persist.mjs +33 -19
  261. package/src/tui/session/tui-steering-persist.test.mjs +104 -0
  262. package/src/tui/session-local.mjs +0 -22
  263. package/scripts/.tmp-coverage-report.mjs +0 -96
  264. package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.mjs +0 -57
  265. package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.test.mjs +0 -57
@@ -0,0 +1,136 @@
1
+ 'use strict';
2
+ // Post-exit descendant observation for the shell runner.
3
+ //
4
+ // The question this module answers is "did the command leave anything
5
+ // RUNNING?", and it answers it by looking at PROCESSES — never at the command
6
+ // text. Nothing here parses, classifies or rewrites a single byte of what the
7
+ // caller typed; the shell receives the command exactly as written and the
8
+ // decision is taken afterwards from what the operating system reports.
9
+ //
10
+ // Evidence per platform, both produced by the existing spawn infrastructure:
11
+ // POSIX — native/mixdog-spawn spawns every shell with `process_group(0)`,
12
+ // so the command owns a process group whose id is the shell pid.
13
+ // Once the shell has exited and been reaped, ANY remaining member
14
+ // of that group is a descendant it left behind, and one signal to
15
+ // the negative pgid reaches all of them.
16
+ // win32 — there is no process group to signal, but the shell's stdout and
17
+ // stderr pipes are INHERITED by its descendants: if they are still
18
+ // held when the shell process itself has exited, something of this
19
+ // command is alive. That observation gates a single batched
20
+ // process-table read (shared with mcp/child-tree.mjs) which names
21
+ // the survivors that are still linked to the shell pid.
22
+ import { collectDescendantProcesses, killProcessTrees } from '../../mcp/child-tree.mjs';
23
+
24
+ const isWin = process.platform === 'win32';
25
+
26
+ // The spawn server waits up to 2 s for the shell's stdio pumps to drain after
27
+ // the root process exits and then reports the exit regardless (main.rs
28
+ // run_spawn). A close that lands at/after this threshold therefore means the
29
+ // pipes were STILL held by another process at that deadline, not that draining
30
+ // was slow — a normal command closes within milliseconds of its exit.
31
+ export const STDIO_HELD_AFTER_EXIT_MS = 1_500;
32
+
33
+ // Windows attaches a console host to the hidden console the shell runs in. It
34
+ // is a real child of the shell and it lives exactly as long as console clients
35
+ // remain, which makes it a liveness WITNESS for descendants that pid-level
36
+ // tracking can no longer reach — but it is never a kill target: terminating it
37
+ // leaves the actual worker running (verified) and would only destroy the
38
+ // witness.
39
+ const CONSOLE_HOST = /^conhost\.exe$/i;
40
+
41
+ export function pidAlive(pid) {
42
+ if (!Number.isInteger(pid) || pid <= 0) return false;
43
+ try {
44
+ process.kill(pid, 0);
45
+ return true;
46
+ } catch (error) {
47
+ // EPERM: exists but is not ours to signal. ESRCH: gone.
48
+ return error?.code === 'EPERM';
49
+ }
50
+ }
51
+
52
+ function groupAlive(pgid) {
53
+ if (!Number.isInteger(pgid) || pgid <= 0) return false;
54
+ try {
55
+ process.kill(-pgid, 0);
56
+ return true;
57
+ } catch (error) {
58
+ return error?.code === 'EPERM';
59
+ }
60
+ }
61
+
62
+ function delay(ms) {
63
+ return new Promise((resolve) => {
64
+ const timer = setTimeout(resolve, ms);
65
+ timer.unref?.();
66
+ });
67
+ }
68
+
69
+ /** Observe what the finished command left running.
70
+ *
71
+ * Returns null when nothing is left (the normal case — the caller then
72
+ * reports a plain completion), or a handle describing the survivors:
73
+ * groupPid POSIX process group that still has members
74
+ * killPids pids a cancellation can signal directly
75
+ * watchPids pids whose liveness answers "is this task still running?"
76
+ * reachable false when survivors are observable but no longer addressable
77
+ * by pid (Windows re-parents nothing: a process whose parent
78
+ * exited keeps a dangling parent id, so an intermediate that
79
+ * already exited breaks the link). */
80
+ export async function probeShellDescendants({ pid, stdioHeld = false } = {}) {
81
+ const root = Number(pid) || 0;
82
+ if (root <= 0) return null;
83
+ if (!isWin) {
84
+ if (!groupAlive(root)) return null;
85
+ return { groupPid: root, killPids: [], watchPids: [], reachable: true };
86
+ }
87
+ if (!stdioHeld) return null;
88
+ const found = await collectDescendantProcesses(root).catch(() => []);
89
+ const live = found.filter((entry) => pidAlive(entry.pid));
90
+ if (live.length === 0) return null;
91
+ const killPids = live.filter((entry) => !CONSOLE_HOST.test(entry.name)).map((entry) => entry.pid);
92
+ return {
93
+ groupPid: null,
94
+ killPids,
95
+ watchPids: live.map((entry) => entry.pid),
96
+ reachable: killPids.length > 0,
97
+ };
98
+ }
99
+
100
+ /** True while the observed survivors are still running. */
101
+ export function descendantsAlive(handle) {
102
+ if (!handle) return false;
103
+ if (handle.groupPid) return groupAlive(handle.groupPid);
104
+ return handle.watchPids.some((pid) => pidAlive(pid));
105
+ }
106
+
107
+ /** Resolve once every observed survivor has exited. Poll-based on purpose:
108
+ * these processes are not our children any more, so there is no exit event to
109
+ * subscribe to — only their liveness can be observed. */
110
+ export async function waitForShellDescendants(handle, { pollMs = 1_000, signal = null } = {}) {
111
+ while (descendantsAlive(handle)) {
112
+ if (signal?.aborted) return false;
113
+ await delay(pollMs);
114
+ }
115
+ return true;
116
+ }
117
+
118
+ /** Terminate the survivors. POSIX signals the whole process group, so the kill
119
+ * is complete by construction; Windows kills every reachable tree and then
120
+ * RE-OBSERVES, so the caller reports what actually died instead of what was
121
+ * signalled. */
122
+ export async function killShellDescendants(handle) {
123
+ if (!handle) return { terminated: true, survivors: [] };
124
+ if (handle.groupPid) {
125
+ try { process.kill(-handle.groupPid, 'SIGTERM'); } catch { /* already gone */ }
126
+ await delay(200);
127
+ if (groupAlive(handle.groupPid)) {
128
+ try { process.kill(-handle.groupPid, 'SIGKILL'); } catch { /* already gone */ }
129
+ await delay(100);
130
+ }
131
+ return { terminated: !groupAlive(handle.groupPid), survivors: [] };
132
+ }
133
+ await killProcessTrees(handle.killPids).catch(() => {});
134
+ const survivors = handle.watchPids.filter((pid) => pidAlive(pid));
135
+ return { terminated: survivors.length === 0, survivors };
136
+ }
@@ -14,6 +14,9 @@ import {
14
14
  } from '../builtin.mjs';
15
15
  import { atomicWrite } from '../builtin/atomic-write.mjs';
16
16
  import { isSpecialFileStat } from '../builtin/device-paths.mjs';
17
+ import { getReadSnapshot } from '../builtin/read-snapshot-runtime.mjs';
18
+ import { snapshotCoversFullFile } from '../builtin/snapshot-helpers.mjs';
19
+ import { hashText } from '../builtin/hash-utils.mjs';
17
20
  import { markCodeGraphDirtyPaths } from '../code-graph-state.mjs';
18
21
  import {
19
22
  classifyEntry,
@@ -37,6 +40,9 @@ import {
37
40
  findFirstFailingUnifiedHunk,
38
41
  findUnifiedHunkMatch,
39
42
  collectUnifiedNewLines,
43
+ collectUnifiedOpEntries,
44
+ terminatorsForUnifiedOps,
45
+ localTerminatorForWindow,
40
46
  assertSafeReplacementPlan,
41
47
  firstFailingUnifiedHunkLineDetail,
42
48
  firstMeaningfulUnifiedHunkLine,
@@ -47,6 +53,14 @@ import {
47
53
  normalizeTypographic,
48
54
  splitBufferLinesForPatch,
49
55
  splitTextLinesForPatch,
56
+ cloneTextLinesForPatch,
57
+ spliceTextLinesForPatch,
58
+ joinTextLinesForPatch,
59
+ setFinalNewlineForPatch,
60
+ patchTargetEncodingError,
61
+ detectDominantEol,
62
+ decodePatchTargetBuffer,
63
+ encodePatchTargetContent,
50
64
  } from './matcher.mjs';
51
65
 
52
66
  function formatNativeFailureContext(parsed, basePath, failedPath = '', options = {}) {
@@ -98,6 +112,50 @@ function formatNativeFailureContext(parsed, basePath, failedPath = '', options =
98
112
  // caller MUST surface throws as `Error: ...` strings.
99
113
  export async function dispatchNativePatch({ entries, basePath, nativePatchStr, fuzz, rejectPartial, dryRun, readStateScope, signal, parsed }) {
100
114
  const nativeStart = performance.now();
115
+ // Pre-write identity of every target. The read-snapshot recorder needs it to
116
+ // decide whether the session's earlier full read still described this file
117
+ // when the patch landed — otherwise an external change made before the patch
118
+ // would stay hidden behind "[file unchanged]".
119
+ const preMutationStats = new Map();
120
+ // Encoding gate, same rule as the JS writer: the native engine rewrites files
121
+ // as UTF-8, so a target that is not valid UTF-8 must be refused rather than
122
+ // silently transcoded. (UTF-16 targets are routed away before this point.)
123
+ for (const entry of entries || []) {
124
+ if (entry.kind === 'create' || entry.kind === 'delete') continue;
125
+ const encodingError = patchTargetEncodingError(entry.fullPath, entry.displayPath);
126
+ if (encodingError) return `Error: ${encodingError}`;
127
+ }
128
+ if (!dryRun) {
129
+ for (const entry of entries || []) {
130
+ try { preMutationStats.set(entry.fullPath, lstatSync(entry.fullPath)); } catch { /* absent target */ }
131
+ }
132
+ }
133
+ // Body-knowledge proof for the "[file unchanged]" fast path. A stat captured
134
+ // before the engine runs cannot vouch for the bytes it actually patched, so
135
+ // predict the post-apply content from bytes we verify against the session's
136
+ // read snapshot; after the apply, the engine's own content hash must equal
137
+ // that prediction. Only computed where the fast path could be inherited.
138
+ const bodyProofs = new Map();
139
+ if (!dryRun && readStateScope) {
140
+ for (const entry of entries || []) {
141
+ if (entry.kind === 'create' || entry.kind === 'delete') continue;
142
+ const prior = getReadSnapshot(entry.fullPath, readStateScope);
143
+ if (!prior?.contentHash) continue;
144
+ if (prior.bodyDelivered !== true && !snapshotCoversFullFile(prior)) continue;
145
+ try {
146
+ const { text } = decodePatchTargetBuffer(readFileSync(entry.fullPath), entry.displayPath);
147
+ if (hashText(text) !== prior.contentHash) continue; // already stale — claim nothing
148
+ const parsedEntry = findParsedForRow(entry, parsed, basePath);
149
+ if (!parsedEntry) continue;
150
+ const updated = applyUnifiedHunksToLines(splitTextLinesForPatch(text), parsedEntry.hunks || [], {
151
+ fuzz: Number.isFinite(fuzz) ? fuzz : 0,
152
+ displayPath: entry.displayPath,
153
+ eol: detectDominantEol(text),
154
+ });
155
+ bodyProofs.set(entry.fullPath, hashText(joinTextLinesForPatch(updated)));
156
+ } catch { /* no proof — the fast path stays off for this file */ }
157
+ }
158
+ }
101
159
  let stats;
102
160
  try {
103
161
  stats = await runServerApply(basePath, nativePatchStr, { fuzz, rejectPartial, dryRun, signal });
@@ -147,11 +205,33 @@ export async function dispatchNativePatch({ entries, basePath, nativePatchStr, f
147
205
  if (entry.kind === 'delete') {
148
206
  clearReadSnapshotForPath(entry.fullPath, readStateScope);
149
207
  } else {
208
+ const contentHash = stats.contentHashes?.[i] || null;
209
+ // Only a matching prediction proves the engine patched the bytes this
210
+ // session had read; otherwise the pre-mutation stat is not evidence.
211
+ const proof = bodyProofs.get(entry.fullPath);
212
+ const provenSameBytes = !!proof && !!contentHash && proof === contentHash;
213
+ // ONE consistent observation of the result, stat FIRST: a write that
214
+ // lands after the stat breaks the hash (claim dropped); one that lands
215
+ // after the read leaves the recorded stat older than the file, so the
216
+ // fast path fails closed at read time. Without that pairing an external
217
+ // write between the apply and this record could inherit bodyDelivered.
218
+ let observedStat = null;
219
+ let observationConsistent = false;
220
+ if (provenSameBytes) {
221
+ try {
222
+ observedStat = lstatSync(entry.fullPath);
223
+ const observed = decodePatchTargetBuffer(readFileSync(entry.fullPath), entry.displayPath);
224
+ observationConsistent = hashText(observed.text) === contentHash;
225
+ } catch {
226
+ observationConsistent = false;
227
+ }
228
+ }
150
229
  const snapshotMeta = {
151
230
  source: 'apply_patch_native',
152
231
  isPartialView: false,
232
+ preMutationStat: observationConsistent ? (preMutationStats.get(entry.fullPath) || null) : null,
153
233
  };
154
- const contentHash = stats.contentHashes?.[i] || null;
234
+ if (observationConsistent && observedStat) snapshotMeta.st = observedStat;
155
235
  if (contentHash) snapshotMeta.contentHash = contentHash;
156
236
  recordReadSnapshotForPath(entry.fullPath, readStateScope, snapshotMeta);
157
237
  }
@@ -211,34 +291,6 @@ function entryPathKey(fullPath) {
211
291
  return process.platform === 'win32' ? String(fullPath || '').toLowerCase() : String(fullPath || '');
212
292
  }
213
293
 
214
- function joinTextLinesForPatch(lines) {
215
- const eol = lines?.eol || '\n';
216
- const body = (lines || []).join(eol);
217
- return lines?.hasFinalNewline !== false ? `${body}${eol}` : body;
218
- }
219
-
220
- // The file's dominant line terminator. splitTextLinesForPatch() folds CRLF into
221
- // LF, so without this an out-of-base CRLF file would be rewritten as LF.
222
- function detectDominantEol(text) {
223
- const body = String(text ?? '');
224
- if (body.includes('\r') && !body.includes('\n')) return '\r';
225
- let lf = 0;
226
- let crlf = 0;
227
- for (let i = 0; i < body.length; i++) {
228
- if (body[i] !== '\n') continue;
229
- lf++;
230
- if (i > 0 && body[i - 1] === '\r') crlf++;
231
- }
232
- return lf > 0 && crlf * 2 >= lf ? '\r\n' : '\n';
233
- }
234
-
235
- function cloneTextLinesForPatch(sourceLines, eol) {
236
- const lines = [...(sourceLines || [])];
237
- lines.hasFinalNewline = sourceLines?.hasFinalNewline !== false;
238
- lines.eol = eol || sourceLines?.eol || '\n';
239
- return lines;
240
- }
241
-
242
294
  // Which side of the hunk, if any, carries an explicit
243
295
  // `` marker. Both transitions must be honoured:
244
296
  // a marker on the NEW side drops the terminator, a marker on the OLD side
@@ -300,6 +352,10 @@ function applyUnifiedHunksToLines(sourceLines, hunks, { fuzz, displayPath, eol }
300
352
  start: match.start,
301
353
  oldLen: match.end - match.start,
302
354
  newLines,
355
+ // Op sequence of THIS hunk: the only sound source for terminator
356
+ // inheritance (context keeps its own, an add takes the delete it
357
+ // replaces). Content alignment cannot express interior context or moves.
358
+ ops: collectUnifiedOpEntries(hunk),
303
359
  intent,
304
360
  atEof: match.end >= lines.length,
305
361
  });
@@ -309,12 +365,20 @@ function applyUnifiedHunksToLines(sourceLines, hunks, { fuzz, displayPath, eol }
309
365
  assertSafeReplacementPlan(replacements, `apply_patch: ${displayPath}`);
310
366
  for (let i = replacements.length - 1; i >= 0; i--) {
311
367
  const rep = replacements[i];
312
- lines.splice(rep.start, rep.oldLen, ...rep.newLines);
368
+ const oldTerms = Array.isArray(lines.terminators)
369
+ ? lines.terminators.slice(rep.start, rep.start + rep.oldLen)
370
+ : [];
371
+ const newTerms = terminatorsForUnifiedOps(
372
+ rep.ops,
373
+ oldTerms,
374
+ localTerminatorForWindow(lines, rep.start, rep.oldLen),
375
+ );
376
+ spliceTextLinesForPatch(lines, rep.start, rep.oldLen, rep.newLines, newTerms);
313
377
  // Explicit markers win in BOTH directions; a hunk that is silent about the
314
378
  // terminator leaves the file's existing state untouched (native parity).
315
379
  if (!rep.atEof) continue;
316
- if (rep.intent.newNoNewline) lines.hasFinalNewline = false;
317
- else if (rep.intent.oldNoNewline) lines.hasFinalNewline = true;
380
+ if (rep.intent.newNoNewline) setFinalNewlineForPatch(lines, false);
381
+ else if (rep.intent.oldNoNewline) setFinalNewlineForPatch(lines, true);
318
382
  }
319
383
  return lines;
320
384
  }
@@ -401,21 +465,46 @@ async function applyJsParsedEntry(entry, basePath, { dryRun, fuzzy, readStateSco
401
465
  return { kind, fullPath, displayPath, added, removed };
402
466
  }
403
467
 
404
- lstatRegularPatchFile(fullPath, displayPath);
405
- const raw = readFileSync(fullPath);
406
- const rawText = raw.toString('utf8');
468
+ const preMutationStat = lstatRegularPatchFile(fullPath, displayPath);
469
+ // Rewrite path: decode by BOM, re-encode into the same codec, and keep every
470
+ // untouched line's own terminator (spliceTextLinesForPatch).
471
+ // `toString('utf8')` here transcoded non-UTF-8 files and mangled UTF-16.
472
+ const { text: rawText, enc: rawEnc } = decodePatchTargetBuffer(readFileSync(fullPath), displayPath);
407
473
  const sourceLines = splitTextLinesForPatch(rawText);
408
474
  const updatedLines = applyUnifiedHunksToLines(sourceLines, entry.hunks || [], {
409
475
  fuzz: fuzzy ? 2 : 0,
410
476
  displayPath,
411
477
  eol: detectDominantEol(rawText),
412
478
  });
413
- const content = joinTextLinesForPatch(updatedLines);
479
+ const content = encodePatchTargetContent(joinTextLinesForPatch(updatedLines), rawEnc);
414
480
  if (!dryRun) {
415
- await atomicWrite(fullPath, content, { sessionId: readStateScope });
481
+ // The expected-target snapshot closes the read→write window: if anything
482
+ // rewrote this file after we read it, the write fails instead of silently
483
+ // clobbering it (and instead of inheriting a stale "body delivered" claim).
484
+ try {
485
+ await atomicWrite(fullPath, content, {
486
+ sessionId: readStateScope,
487
+ expectedTargetSnapshot: preMutationStat ? {
488
+ exists: true,
489
+ size: preMutationStat.size,
490
+ mtimeMs: preMutationStat.mtimeMs,
491
+ ctimeMs: preMutationStat.ctimeMs,
492
+ ino: preMutationStat.ino,
493
+ } : undefined,
494
+ });
495
+ } catch (err) {
496
+ if (err?.code === 'ESTALE_TARGET') {
497
+ throw new Error(`apply_patch: ${normalizeOutputPath(displayPath)} changed on disk during the patch; re-read it and retry`);
498
+ }
499
+ throw err;
500
+ }
416
501
  invalidateBuiltinResultCache([fullPath]);
417
502
  markCodeGraphDirtyPaths([fullPath]);
418
- recordReadSnapshotForPath(fullPath, readStateScope, { source: 'apply_patch_js', isPartialView: false });
503
+ recordReadSnapshotForPath(fullPath, readStateScope, {
504
+ source: 'apply_patch_js',
505
+ isPartialView: false,
506
+ preMutationStat,
507
+ });
419
508
  }
420
509
  const { added, removed } = countHunkChanges(entry.hunks);
421
510
  return { kind, fullPath, displayPath, added, removed };