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
@@ -3,8 +3,10 @@
3
3
  // patch.mjs; matching/fuzz semantics mirror the native engine and are
4
4
  // unchanged.
5
5
 
6
+ import { closeSync, openSync, readFileSync, readSync, statSync } from 'node:fs';
6
7
  import { classifyEntry, stripDiffPrefix } from './paths.mjs';
7
8
  import { normalizeOutputPath } from '../builtin.mjs';
9
+ import { detectReadEncodingFromBuffer } from '../builtin/snapshot-helpers.mjs';
8
10
 
9
11
  function collectUnifiedOldLines(hunk) {
10
12
  const oldLines = [];
@@ -39,6 +41,19 @@ export function collectUnifiedOps(hunk) {
39
41
  return ops;
40
42
  }
41
43
 
44
+ /** collectUnifiedOps plus each op's line text (terminator inheritance needs it). */
45
+ export function collectUnifiedOpEntries(hunk) {
46
+ const ops = [];
47
+ for (const raw of hunk?.lines || []) {
48
+ if (typeof raw !== 'string' || raw.length === 0) continue;
49
+ const tag = raw[0];
50
+ if (tag === ' ') ops.push({ op: 'context', line: raw.slice(1) });
51
+ else if (tag === '-') ops.push({ op: 'delete', line: raw.slice(1) });
52
+ else if (tag === '+') ops.push({ op: 'add', line: raw.slice(1) });
53
+ }
54
+ return ops;
55
+ }
56
+
42
57
  // New-side lines of a unified hunk (context + additions), plus whether the
43
58
  // hunk marks the NEW content as lacking a terminating newline
44
59
  // (`` directly after a ' ' / '+' line).
@@ -186,6 +201,145 @@ export function decodeValidUtf8OrNull(buf) {
186
201
  }
187
202
  }
188
203
 
204
+ // --- Patch-target codec ------------------------------------------------------
205
+ // Every route that rewrites a WHOLE file from decoded text (V4A rename, the JS
206
+ // unified route) must round-trip the file's encoding. `buf.toString('utf8')`
207
+ // replaced each invalid byte with U+FFFD and read UTF-16 as NUL-laden garbage,
208
+ // so the rewrite transcoded the file irreversibly. The BOM decides the codec;
209
+ // anything that is neither valid UTF-8 nor BOM-marked UTF-16 is refused.
210
+ // Codec of a patch target. A BOM is authoritative; a BOM-LESS file is
211
+ // classified by NUL-byte parity, which is what separates UTF-16 text from
212
+ // UTF-8 text. `certain:false` means the bytes cannot be attributed to one
213
+ // codec — the caller must refuse rather than rewrite them. Routing by BOM
214
+ // alone sent BOM-less UTF-16 to the UTF-8 engine, which produced mixed
215
+ // UTF-16/UTF-8 bytes.
216
+ // Minimum evidence for a BOM-less UTF-16 verdict: 16 code units. Below that a
217
+ // NUL is just as likely to be a stray byte inside UTF-8 text (`61 00`), and a
218
+ // wrong verdict DESTROYS the file, so short input is undecidable by
219
+ // definition. `buf` must be the WHOLE file: a bounded prefix cannot rule out a
220
+ // parity flip further in, so a prefix-only caller passes `partial:true` and
221
+ // gets "undecidable" instead of a guess.
222
+ const UTF16_MIN_EVIDENCE_BYTES = 32;
223
+ const UTF16_MIN_NUL_RATIO = 0.5;
224
+
225
+ export function detectPatchTargetCodec(buf, { partial = false } = {}) {
226
+ const bytes = Buffer.isBuffer(buf) ? buf : Buffer.from(buf || []);
227
+ const undecidable = { encoding: null, bomLen: 0, certain: false };
228
+ if (bytes.length >= 2 && bytes[0] === 0xFF && bytes[1] === 0xFE) {
229
+ return { encoding: 'utf16le', bomLen: 2, certain: true };
230
+ }
231
+ if (bytes.length >= 2 && bytes[0] === 0xFE && bytes[1] === 0xFF) {
232
+ return { encoding: 'utf16be', bomLen: 2, certain: true };
233
+ }
234
+ if (bytes.length >= 3 && bytes[0] === 0xEF && bytes[1] === 0xBB && bytes[2] === 0xBF) {
235
+ return { encoding: 'utf8', bomLen: 3, certain: true };
236
+ }
237
+ let nulEven = 0;
238
+ let nulOdd = 0;
239
+ for (let i = 0; i < bytes.length; i++) {
240
+ if (bytes[i] !== 0) continue;
241
+ if (i % 2 === 0) nulEven += 1;
242
+ else nulOdd += 1;
243
+ }
244
+ if (partial) return undecidable;
245
+ if (nulEven === 0 && nulOdd === 0) return { encoding: 'utf8', bomLen: 0, certain: true };
246
+ if (bytes.length < UTF16_MIN_EVIDENCE_BYTES || bytes.length % 2 !== 0) return undecidable;
247
+ const evenSlots = Math.ceil(bytes.length / 2);
248
+ const oddSlots = Math.floor(bytes.length / 2);
249
+ if (nulEven === 0 && nulOdd / oddSlots >= UTF16_MIN_NUL_RATIO) {
250
+ return { encoding: 'utf16le', bomLen: 0, certain: true };
251
+ }
252
+ if (nulOdd === 0 && nulEven / evenSlots >= UTF16_MIN_NUL_RATIO) {
253
+ return { encoding: 'utf16be', bomLen: 0, certain: true };
254
+ }
255
+ return undecidable;
256
+ }
257
+
258
+ export function decodePatchTargetBuffer(buf, displayPath = '') {
259
+ const enc = detectPatchTargetCodec(buf);
260
+ if (!enc.certain) {
261
+ throw new Error(
262
+ `apply_patch: ${normalizeOutputPath(displayPath)} has NUL bytes without a decidable text encoding — `
263
+ + 'refusing to rewrite it, because any codec choice would corrupt the file.',
264
+ );
265
+ }
266
+ let text;
267
+ if (enc.encoding === 'utf16le' || enc.encoding === 'utf16be') {
268
+ const body = buf.subarray(enc.bomLen);
269
+ // An odd trailing byte cannot be decoded and would be DROPPED on re-encode:
270
+ // silent data loss. Refuse the file exactly like invalid UTF-8.
271
+ if (body.length % 2 !== 0) {
272
+ throw new Error(
273
+ `apply_patch: ${normalizeOutputPath(displayPath)} is malformed UTF-16 (odd payload length) — `
274
+ + 'refusing to rewrite it, because the dangling byte cannot be preserved.',
275
+ );
276
+ }
277
+ text = enc.encoding === 'utf16le'
278
+ ? body.toString('utf16le')
279
+ // Node has no 'utf16be': swap pairs into LE and decode.
280
+ : Buffer.from(body).swap16().toString('utf16le');
281
+ } else {
282
+ text = decodeValidUtf8OrNull(buf.subarray(enc.bomLen));
283
+ }
284
+ if (typeof text !== 'string') {
285
+ throw new Error(
286
+ `apply_patch: ${normalizeOutputPath(displayPath)} is neither valid UTF-8 nor BOM-marked UTF-16 — `
287
+ + 'refusing to rewrite it, because the edit would transcode every byte.',
288
+ );
289
+ }
290
+ return { text, enc };
291
+ }
292
+
293
+ // Encoding gate for a target that is about to be rewritten. Returns the
294
+ // refusal message, or null when the bytes round-trip. Both engines use it: the
295
+ // native one speaks UTF-8 only, so invalid UTF-8 must be refused there exactly
296
+ // as the JS writer refuses it, instead of being rewritten byte-lossily.
297
+ export function patchTargetEncodingError(fullPath, displayPath = fullPath) {
298
+ let buf;
299
+ try {
300
+ buf = readFileSync(fullPath);
301
+ } catch {
302
+ return null; // absent/unreadable — the engine reports it in its own words
303
+ }
304
+ try {
305
+ decodePatchTargetBuffer(buf, displayPath);
306
+ return null;
307
+ } catch (err) {
308
+ const message = err?.message || String(err);
309
+ return /^apply_patch:/.test(message) ? message : null;
310
+ }
311
+ }
312
+
313
+ // Routing sniff: ANY UTF-16 target (BOM or not) must take the codec-preserving
314
+ // JS writer instead of the UTF-8 native engine. Undecidable files stay on the
315
+ // native route, where the encoding gate refuses them without writing.
316
+ export function patchTargetUsesUtf16(fullPath) {
317
+ try {
318
+ // WHOLE file: a prefix cannot rule out a parity flip further in, and this
319
+ // decides whether the byte-preserving writer is used.
320
+ const codec = detectPatchTargetCodec(readFileSync(fullPath));
321
+ return codec.certain && codec.encoding !== 'utf8';
322
+ } catch {
323
+ return false;
324
+ }
325
+ }
326
+
327
+ // Re-encode a rewritten file into the codec it was read with (BOM included).
328
+ // Plain UTF-8 stays a string so atomicWrite keeps its existing fast path.
329
+ export function encodePatchTargetContent(text, enc) {
330
+ if (!enc || (enc.encoding === 'utf8' && !enc.bomLen)) return text;
331
+ const body = enc.encoding === 'utf16le'
332
+ ? Buffer.from(text, 'utf16le')
333
+ : (enc.encoding === 'utf16be'
334
+ ? Buffer.from(text, 'utf16le').swap16()
335
+ : Buffer.from(text, 'utf8'));
336
+ if (!enc.bomLen) return body;
337
+ const bom = enc.encoding === 'utf16le'
338
+ ? Buffer.from([0xFF, 0xFE])
339
+ : (enc.encoding === 'utf16be' ? Buffer.from([0xFE, 0xFF]) : Buffer.from([0xEF, 0xBB, 0xBF]));
340
+ return Buffer.concat([bom, body]);
341
+ }
342
+
189
343
  export function unifiedOldLinesMatchAt(sourceLines, oldLines, startIdx, fuzz, band) {
190
344
  if (startIdx < 0 || startIdx + oldLines.length > sourceLines.length) return null;
191
345
  let fuzzUsed = 0;
@@ -344,25 +498,212 @@ export function normalizeTypographic(s) {
344
498
  .replace(/[\u00A0\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000]/g, ' ');
345
499
  }
346
500
 
501
+ // Splits into line CONTENT plus the exact terminator that followed each line
502
+ // (`lines.terminators`). Line content is identical to the historical
503
+ // CRLF-folding split — a lone CR is only a terminator in a CR-dominant file —
504
+ // but recording the real bytes is what lets a rewrite put every untouched
505
+ // line back with its ORIGINAL terminator instead of the file's dominant one.
347
506
  export function splitTextLinesForPatch(text) {
348
507
  const raw = String(text ?? '');
349
508
  const eol = detectDominantEol(raw);
350
- const body = eol === '\r' ? raw.replace(/\r/g, '\n') : raw.replace(/\r\n/g, '\n');
351
- if (body.length === 0) {
352
- const empty = [];
353
- empty.hasFinalNewline = true;
354
- empty.eol = eol;
355
- return empty;
509
+ const splitLoneCr = eol === '\r';
510
+ const lines = [];
511
+ const terminators = [];
512
+ let start = 0;
513
+ let i = 0;
514
+ while (i < raw.length) {
515
+ const ch = raw[i];
516
+ if (ch === '\n') {
517
+ lines.push(raw.slice(start, i));
518
+ terminators.push('\n');
519
+ i += 1;
520
+ start = i;
521
+ continue;
522
+ }
523
+ if (ch === '\r') {
524
+ if (raw[i + 1] === '\n') {
525
+ lines.push(raw.slice(start, i));
526
+ terminators.push('\r\n');
527
+ i += 2;
528
+ start = i;
529
+ continue;
530
+ }
531
+ if (splitLoneCr) {
532
+ lines.push(raw.slice(start, i));
533
+ terminators.push('\r');
534
+ i += 1;
535
+ start = i;
536
+ continue;
537
+ }
538
+ }
539
+ i += 1;
356
540
  }
357
- const lines = body.split('\n');
358
541
  let hasFinalNewline = true;
359
- if (lines[lines.length - 1] === '') lines.pop();
360
- else hasFinalNewline = false;
542
+ if (start < raw.length) {
543
+ lines.push(raw.slice(start));
544
+ terminators.push('');
545
+ hasFinalNewline = false;
546
+ }
361
547
  lines.hasFinalNewline = hasFinalNewline;
362
548
  lines.eol = eol;
549
+ lines.terminators = terminators;
363
550
  return lines;
364
551
  }
365
552
 
553
+ export function cloneTextLinesForPatch(sourceLines, eol) {
554
+ const lines = [...(sourceLines || [])];
555
+ lines.hasFinalNewline = sourceLines?.hasFinalNewline !== false;
556
+ lines.eol = eol || sourceLines?.eol || '\n';
557
+ lines.terminators = Array.isArray(sourceLines?.terminators)
558
+ ? [...sourceLines.terminators]
559
+ : null;
560
+ return lines;
561
+ }
562
+
563
+ // The ONLY mutation entry point for a patch line array. Replaced lines hand
564
+ // their own terminator to the new line at the same offset (a 1:1 replacement
565
+ // is byte-identical outside the changed text), extra inserted lines adopt the
566
+ // local convention, and every line the patch did not touch keeps its bytes.
567
+ // Op-wise terminators for one hunk's output lines. Derived from the hunk's OWN
568
+ // ops — never from content similarity, which cannot describe interior context,
569
+ // several change runs, or a moved line:
570
+ // context → keeps its source line's terminator verbatim
571
+ // delete → contributes its terminator to the current run
572
+ // add → takes the terminator of the delete it replaces inside that run,
573
+ // else the local convention.
574
+ export function terminatorsForUnifiedOps(ops, oldTerminators, fallbackEol = '\n') {
575
+ const olds = Array.isArray(oldTerminators) ? oldTerminators : [];
576
+ const entries = (ops || []).map((entry) => (typeof entry === 'string'
577
+ ? { op: entry, line: undefined }
578
+ : { op: entry?.op, line: entry?.line }));
579
+ // Pre-pass: EVERY delete of the hunk with the terminator of the source line
580
+ // it consumes, tagged with its run. Building the pool as we walk only saw
581
+ // deletes already passed, so a BACKWARD move (the add precedes its delete)
582
+ // could not claim its own line and lost its terminator.
583
+ const pool = [];
584
+ {
585
+ let cursor = 0;
586
+ let run = 0;
587
+ for (const entry of entries) {
588
+ if (entry.op === 'context') { cursor += 1; run += 1; }
589
+ else if (entry.op === 'delete') {
590
+ pool.push({ line: entry.line, terminator: olds[cursor], used: false, run });
591
+ cursor += 1;
592
+ }
593
+ }
594
+ }
595
+ const out = [];
596
+ let oldCursor = 0;
597
+ let currentRun = 0;
598
+ let lastClaimed = -1; // pool index claimed by the previous add
599
+ for (const entry of entries) {
600
+ const { op, line } = entry;
601
+ if (op === 'context') {
602
+ const terminator = olds[oldCursor];
603
+ oldCursor += 1;
604
+ currentRun += 1;
605
+ out.push(terminator !== undefined ? terminator : fallbackEol);
606
+ } else if (op === 'delete') {
607
+ oldCursor += 1;
608
+ } else if (op === 'add') {
609
+ // 1) identity AFTER the previously claimed delete (order-preserving, so
610
+ // duplicates are consumed in sequence instead of first-fit),
611
+ // 2) identity ANYWHERE in the hunk (a line moved forwards or backwards
612
+ // across context keeps its own terminator),
613
+ // 3) the next unclaimed delete OF THIS RUN (the line it replaces).
614
+ let index = -1;
615
+ if (line !== undefined) {
616
+ index = pool.findIndex((slot, i) => !slot.used && slot.line === line && i > lastClaimed);
617
+ if (index < 0) index = pool.findIndex((slot) => !slot.used && slot.line === line);
618
+ }
619
+ if (index < 0) {
620
+ index = pool.findIndex((slot) => !slot.used && slot.run === currentRun);
621
+ }
622
+ let inherited;
623
+ if (index >= 0) {
624
+ pool[index].used = true;
625
+ lastClaimed = index;
626
+ inherited = pool[index].terminator;
627
+ }
628
+ out.push(inherited !== undefined ? inherited : fallbackEol);
629
+ }
630
+ }
631
+ return out;
632
+ }
633
+
634
+ /** Local convention for a replaced window: its own last terminator, else its neighbours'. */
635
+ export function localTerminatorForWindow(lines, start, oldLen) {
636
+ const terms = lines?.terminators;
637
+ if (!Array.isArray(terms)) return lines?.eol || '\n';
638
+ const replaced = terms.slice(start, start + oldLen);
639
+ return (replaced.length ? replaced[replaced.length - 1] : '')
640
+ || terms[start - 1]
641
+ || terms[start]
642
+ || lines.eol
643
+ || '\n';
644
+ }
645
+
646
+ export function spliceTextLinesForPatch(lines, start, oldLen, newLines, newTerminators = null) {
647
+ const terms = lines?.terminators;
648
+ if (Array.isArray(terms)) {
649
+ const replacedTerms = terms.slice(start, start + oldLen);
650
+ const replacedLines = lines.slice(start, start + oldLen);
651
+ const local = localTerminatorForWindow(lines, start, oldLen);
652
+ const explicit = Array.isArray(newTerminators) && newTerminators.length === newLines.length
653
+ ? newTerminators
654
+ : null;
655
+ const nextTerms = newLines.map((line, k) => {
656
+ if (explicit) return explicit[k] !== undefined ? explicit[k] : local;
657
+ // No op information (a caller that cannot describe its edit): keep a
658
+ // terminator ONLY for an output line that is byte-identical to the
659
+ // source line at the same offset; everything else takes the local
660
+ // convention. Never guess across offsets.
661
+ return (replacedLines[k] === line && replacedTerms[k] !== undefined)
662
+ ? replacedTerms[k]
663
+ : local;
664
+ });
665
+ terms.splice(start, oldLen, ...nextTerms);
666
+ }
667
+ lines.splice(start, oldLen, ...newLines);
668
+ }
669
+
670
+ // Explicit end-of-file intent (`` in either
671
+ // direction). The terminator array is the state of record, so the intent is
672
+ // written INTO it; `hasFinalNewline` is kept in sync for matching code.
673
+ export function setFinalNewlineForPatch(lines, hasNewline) {
674
+ if (!lines) return;
675
+ lines.hasFinalNewline = hasNewline;
676
+ const terms = lines.terminators;
677
+ if (!Array.isArray(terms) || terms.length === 0) return;
678
+ const last = terms.length - 1;
679
+ if (!hasNewline) terms[last] = '';
680
+ else if (!terms[last]) terms[last] = terms[last - 1] || lines.eol || '\n';
681
+ }
682
+
683
+ export function joinTextLinesForPatch(lines) {
684
+ const arr = lines || [];
685
+ const eol = arr.eol || '\n';
686
+ const terms = Array.isArray(arr.terminators) && arr.terminators.length === arr.length
687
+ ? arr.terminators
688
+ : null;
689
+ if (!terms) {
690
+ const body = arr.join(eol);
691
+ return arr.hasFinalNewline !== false ? `${body}${eol}` : body;
692
+ }
693
+ // Terminators are authoritative. The file-level `hasFinalNewline` flag must
694
+ // NOT be re-applied here: after a tail deletion the surviving last line is a
695
+ // line the patch never touched, and forcing the old "no final newline" state
696
+ // onto it removed a terminator that belonged to untouched bytes.
697
+ let out = '';
698
+ for (let i = 0; i < arr.length; i++) {
699
+ const isLast = i === arr.length - 1;
700
+ let terminator = terms[i];
701
+ if (!terminator && !isLast) terminator = terms[i - 1] || eol;
702
+ out += `${arr[i]}${terminator}`;
703
+ }
704
+ return out;
705
+ }
706
+
366
707
  export function detectDominantEol(text) {
367
708
  const raw = String(text ?? '');
368
709
  if (raw.includes('\r') && !raw.includes('\n')) return '\r';