mixdog 1.2.2 → 1.2.3

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 (415) hide show
  1. package/README.md +18 -0
  2. package/package.json +2 -1
  3. package/scripts/provider-pingpong-live.mjs +509 -0
  4. package/src/agents/advisor/AGENT.md +1 -1
  5. package/src/agents/advisor/agent.json +1 -1
  6. package/src/defaults/shipped-history.json +5 -0
  7. package/src/defaults/skills/setup/SKILL.md +6 -1
  8. package/src/defaults/skills/setup/references/actions.md +44 -22
  9. package/src/defaults/skills/setup/references/surfaces.md +10 -8
  10. package/src/headless-exec.mjs +15 -8
  11. package/src/lib/keychain-cjs.cjs +7 -4
  12. package/src/output-styles/common.md +11 -10
  13. package/src/rules/agent/AGENT.md +5 -5
  14. package/src/rules/lead/LEAD.md +5 -5
  15. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +92 -61
  16. package/src/runtime/agent/orchestrator/cache-mtime.mjs +10 -1
  17. package/src/runtime/agent/orchestrator/config-providers-merge.mjs +7 -0
  18. package/src/runtime/agent/orchestrator/config.mjs +5 -0
  19. package/src/runtime/agent/orchestrator/context/collect.mjs +1 -0
  20. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +26 -7
  21. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +112 -7
  22. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +48 -20
  23. package/src/runtime/agent/orchestrator/providers/anthropic-error-contract.mjs +2 -0
  24. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +24 -17
  25. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +7 -6
  26. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  27. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +29 -10
  28. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-blocks.mjs +20 -1
  29. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events/content-block-delta.mjs +15 -2
  30. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events/content-block-start.mjs +11 -4
  31. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events/message-events.mjs +3 -0
  32. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events.mjs +12 -5
  33. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-outcome.mjs +1 -0
  34. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-state.mjs +14 -0
  35. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +2 -0
  36. package/src/runtime/agent/orchestrator/providers/anthropic-turn-result.mjs +9 -0
  37. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +6 -0
  38. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +58 -7
  39. package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +29 -10
  40. package/src/runtime/agent/orchestrator/providers/antigravity-retry-policy.mjs +16 -3
  41. package/src/runtime/agent/orchestrator/providers/antigravity-stream.mjs +10 -61
  42. package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +12 -6
  43. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
  44. package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +114 -72
  45. package/src/runtime/agent/orchestrator/providers/compat-request-policy.mjs +78 -2
  46. package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +37 -5
  47. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  48. package/src/runtime/agent/orchestrator/providers/cursor-wire-stream-bridge.mjs +1 -0
  49. package/src/runtime/agent/orchestrator/providers/cursor-wire-stream-messages.mjs +43 -2
  50. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +3 -0
  51. package/src/runtime/agent/orchestrator/providers/cursor.mjs +9 -2
  52. package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +64 -1
  53. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +70 -6
  54. package/src/runtime/agent/orchestrator/providers/custom-reasoning.mjs +34 -7
  55. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +32 -6
  56. package/src/runtime/agent/orchestrator/providers/deepseek.mjs +67 -0
  57. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +3 -1
  58. package/src/runtime/agent/orchestrator/providers/gemini-cache-policy.mjs +11 -4
  59. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +25 -8
  60. package/src/runtime/agent/orchestrator/providers/gemini-error-contract.mjs +87 -5
  61. package/src/runtime/agent/orchestrator/providers/gemini-request-body.mjs +41 -5
  62. package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +13 -13
  63. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +355 -37
  64. package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +1 -2
  65. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +93 -47
  66. package/src/runtime/agent/orchestrator/providers/gemini-thinking.mjs +55 -10
  67. package/src/runtime/agent/orchestrator/providers/gemini.mjs +26 -8
  68. package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
  69. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +1 -0
  70. package/src/runtime/agent/orchestrator/providers/lib/action-input-contract.mjs +60 -0
  71. package/src/runtime/agent/orchestrator/providers/lib/anthropic-native-blocks.mjs +1 -0
  72. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +246 -42
  73. package/src/runtime/agent/orchestrator/providers/lib/anthropic-server-tool-usage.mjs +18 -0
  74. package/src/runtime/agent/orchestrator/providers/lib/anthropic-system-blocks.mjs +27 -6
  75. package/src/runtime/agent/orchestrator/providers/lib/claude-bridge-schema.mjs +388 -0
  76. package/src/runtime/agent/orchestrator/providers/lib/inclusive-usage.mjs +54 -0
  77. package/src/runtime/agent/orchestrator/providers/lib/origin-bound-fetch.mjs +16 -0
  78. package/src/runtime/agent/orchestrator/providers/lib/responses-terminal-fields.mjs +11 -0
  79. package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +8 -0
  80. package/src/runtime/agent/orchestrator/providers/lib/wire-pairing.mjs +48 -22
  81. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +119 -18
  82. package/src/runtime/agent/orchestrator/providers/mimo-oauth-profile.mjs +13 -15
  83. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +1 -1
  84. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +1 -0
  85. package/src/runtime/agent/orchestrator/providers/model-proxy-provider.mjs +33 -1
  86. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +2 -0
  87. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +2 -0
  88. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +121 -61
  89. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/chunk-events.mjs +35 -14
  90. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-result.mjs +5 -2
  91. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +37 -7
  92. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/tool-call-acc.mjs +2 -9
  93. package/src/runtime/agent/orchestrator/providers/openai-compat-error-contract.mjs +1 -1
  94. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +47 -2
  95. package/src/runtime/agent/orchestrator/providers/openai-compat-options.mjs +47 -25
  96. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +5 -1
  97. package/src/runtime/agent/orchestrator/providers/openai-compat-reasoning-field.mjs +151 -0
  98. package/src/runtime/agent/orchestrator/providers/openai-compat-response-normalization.mjs +42 -24
  99. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +63 -25
  100. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-head-parity.json +2901 -0
  101. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-parity.fixtures.mjs +172 -0
  102. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +53 -5
  103. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +7 -1
  104. package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +2 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +103 -26
  106. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +23 -4
  107. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +11 -6
  108. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +6 -1
  109. package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +15 -23
  110. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events/output-items.mjs +23 -10
  111. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events/terminal-frames.mjs +25 -4
  112. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events.mjs +10 -0
  113. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-items.mjs +18 -7
  114. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-outcome.mjs +4 -0
  115. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-state.mjs +5 -0
  116. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/text-relay.mjs +9 -1
  117. package/src/runtime/agent/orchestrator/providers/openai-model-capabilities.mjs +80 -0
  118. package/src/runtime/agent/orchestrator/providers/openai-oauth-dispatch.mjs +3 -1
  119. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +7 -0
  120. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +7 -1
  121. package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +71 -24
  122. package/src/runtime/agent/orchestrator/providers/openai-responses-payload-parity.fixture.json +323 -0
  123. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +44 -16
  124. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +7 -16
  125. package/src/runtime/agent/orchestrator/providers/openai-ws-response-state/refusals.mjs +60 -0
  126. package/src/runtime/agent/orchestrator/providers/openai-ws-response-state/tool-calls.mjs +51 -35
  127. package/src/runtime/agent/orchestrator/providers/openai-ws-response-state.mjs +30 -0
  128. package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +12 -0
  129. package/src/runtime/agent/orchestrator/providers/openai-ws-stream/terminal-frames.mjs +25 -6
  130. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +11 -3
  131. package/src/runtime/agent/orchestrator/providers/opencode-go-gemini.mjs +170 -0
  132. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +75 -70
  133. package/src/runtime/agent/orchestrator/providers/openrouter.mjs +75 -0
  134. package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +147 -8
  135. package/src/runtime/agent/orchestrator/providers/registry.mjs +4 -1
  136. package/src/runtime/agent/orchestrator/providers/retry-classification.mjs +5 -0
  137. package/src/runtime/agent/orchestrator/providers/transport-scope.d.mts +6 -0
  138. package/src/runtime/agent/orchestrator/providers/transport-scope.mjs +28 -0
  139. package/src/runtime/agent/orchestrator/providers/wire-gateway.mjs +157 -0
  140. package/src/runtime/agent/orchestrator/providers/wire-route.mjs +512 -0
  141. package/src/runtime/agent/orchestrator/providers/xai-tool-search-head-parity.json +1131 -0
  142. package/src/runtime/agent/orchestrator/providers/xai-tool-search.fixtures.mjs +183 -0
  143. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +100 -0
  144. package/src/runtime/agent/orchestrator/runtime-core/deferred-tool-availability.mjs +21 -0
  145. package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +15 -0
  146. package/src/runtime/agent/orchestrator/runtime-core/provider-request-snapshot.mjs +7 -3
  147. package/src/runtime/agent/orchestrator/runtime-core/session-title.mjs +21 -1
  148. package/src/runtime/agent/orchestrator/runtime-core/skill-tool-loading.mjs +14 -3
  149. package/src/runtime/agent/orchestrator/runtime-core/tool-bridge.mjs +526 -0
  150. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
  151. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-schema.mjs +10 -7
  152. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +176 -71
  153. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +62 -10
  154. package/src/runtime/agent/orchestrator/runtime-core/tool-exposure.mjs +96 -0
  155. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +3 -1
  156. package/src/runtime/agent/orchestrator/session/compact/constants.mjs +3 -2
  157. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +14 -93
  158. package/src/runtime/agent/orchestrator/session/compact/summary-anchor.mjs +5 -0
  159. package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +120 -0
  160. package/src/runtime/agent/orchestrator/session/context-tool-schema.mjs +17 -2
  161. package/src/runtime/agent/orchestrator/session/context-utils.mjs +8 -4
  162. package/src/runtime/agent/orchestrator/session/eager-dispatch/admission.mjs +2 -1
  163. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +6 -1
  164. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +91 -1
  165. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +1 -1
  166. package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +56 -0
  167. package/src/runtime/agent/orchestrator/session/loop/deferred-call-through.mjs +18 -8
  168. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +1 -0
  169. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +21 -10
  170. package/src/runtime/agent/orchestrator/session/loop/provider-tool-exclusions.mjs +132 -0
  171. package/src/runtime/agent/orchestrator/session/loop/request-boundary.mjs +30 -0
  172. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +63 -20
  173. package/src/runtime/agent/orchestrator/session/loop/step-auto-effort.mjs +2 -1
  174. package/src/runtime/agent/orchestrator/session/loop/tool-call-resolution.mjs +95 -0
  175. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +30 -3
  176. package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +15 -4
  177. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +15 -1
  178. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +150 -0
  179. package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +4 -1
  180. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +2 -1
  181. package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +34 -12
  182. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +16 -0
  183. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +57 -25
  184. package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +6 -0
  185. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +4 -0
  186. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +8 -0
  187. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +208 -14
  188. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +15 -3
  189. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +25 -3
  190. package/src/runtime/agent/orchestrator/session/manager/session-fallback-title.mjs +82 -0
  191. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +19 -2
  192. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +3 -0
  193. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +4 -1
  194. package/src/runtime/agent/orchestrator/session/manager.mjs +2 -1
  195. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +6 -0
  196. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +13 -0
  197. package/src/runtime/agent/orchestrator/session/side-question-store.mjs +96 -0
  198. package/src/runtime/agent/orchestrator/session/side-question.mjs +192 -0
  199. package/src/runtime/agent/orchestrator/session/store/lifecycle-barriers.mjs +17 -3
  200. package/src/runtime/agent/orchestrator/session/store/live-state.mjs +23 -0
  201. package/src/runtime/agent/orchestrator/session/store/paths-heartbeat.mjs +58 -2
  202. package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +41 -28
  203. package/src/runtime/agent/orchestrator/session/store/session-observers.mjs +4 -2
  204. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-finish.mjs +12 -1
  205. package/src/runtime/agent/orchestrator/session/store/write-admission.mjs +3 -1
  206. package/src/runtime/agent/orchestrator/session/store-summary-archived-agent.mjs +34 -15
  207. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +4 -0
  208. package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +8 -0
  209. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +10 -1
  210. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +10 -2
  211. package/src/runtime/agent/orchestrator/session/store.mjs +19 -3
  212. package/src/runtime/agent/orchestrator/session/tool-batch/execute-call.mjs +1 -0
  213. package/src/runtime/agent/orchestrator/session/tool-batch/finalize.mjs +1 -1
  214. package/src/runtime/agent/orchestrator/session/tool-batch/record-outcome.mjs +1 -1
  215. package/src/runtime/agent/orchestrator/session/tool-batch/state.mjs +7 -1
  216. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +4 -0
  217. package/src/runtime/agent/orchestrator/session/tool-schema-rejection.mjs +77 -0
  218. package/src/runtime/agent/orchestrator/tools/bash-policy-scan.mjs +8 -2
  219. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/command-preflight.mjs +3 -3
  220. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +4 -1
  221. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +14 -10
  222. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +49 -34
  223. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +23 -1
  224. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/root-resolution.mjs +1 -1
  225. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1 -1
  226. package/src/runtime/agent/orchestrator/tools/patch/ui-diff.mjs +73 -39
  227. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +18 -1
  228. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +7 -4
  229. package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +88 -12
  230. package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +2 -0
  231. package/src/runtime/computer-bridge/action-schema.mjs +44 -21
  232. package/src/runtime/github/tool.mjs +92 -2
  233. package/src/runtime/memory/index.mjs +1 -0
  234. package/src/runtime/memory/lib/http-router/ingest-routes.mjs +22 -1
  235. package/src/runtime/memory/lib/ingest/transcript-offsets.mjs +2 -0
  236. package/src/runtime/memory/lib/pg/adapter.mjs +98 -1
  237. package/src/runtime/memory/lib/pg/process.mjs +31 -26
  238. package/src/runtime/memory/lib/pg/run-child.mjs +49 -0
  239. package/src/runtime/memory/lib/pg/supervisor.mjs +108 -3
  240. package/src/runtime/memory/lib/runtime-fetcher.mjs +12 -13
  241. package/src/runtime/memory/lib/transcript-ingest.mjs +14 -0
  242. package/src/runtime/office/README.md +1 -1
  243. package/src/runtime/office/authoring/html-document-author.mjs +2 -0
  244. package/src/runtime/office/authoring/pptx-contact-sheet.mjs +2 -0
  245. package/src/runtime/office/authoring/pptx-html-measure.mjs +2 -0
  246. package/src/runtime/office/authoring/pptx-html-runner.mjs +2 -0
  247. package/src/runtime/office/authoring/xlsx-html-runner.mjs +2 -0
  248. package/src/runtime/office/core/office-actions-read.mjs +3 -0
  249. package/src/runtime/office/core/office-render-preview.mjs +2 -0
  250. package/src/runtime/office/core/office-session-dispatch.mjs +7 -0
  251. package/src/runtime/office/core/office-sessionless-actions.mjs +8 -0
  252. package/src/runtime/office/core/office-sessions.mjs +5 -0
  253. package/src/runtime/office/core/office-transactions.mjs +3 -0
  254. package/src/runtime/office/core/office-write-ledger.mjs +98 -0
  255. package/src/runtime/office/design/library/templates/mixdog-executive.mixdog-edit.pptx +0 -0
  256. package/src/runtime/office/index.mjs +30 -0
  257. package/src/runtime/office/pdf/pdf-edit-document.mjs +2 -0
  258. package/src/runtime/office/pdf/pdf-render.mjs +23 -7
  259. package/src/runtime/office/pdf/pdf-security.mjs +2 -0
  260. package/src/runtime/office/portable/portable-opc.mjs +4 -1
  261. package/src/runtime/office/quality/visual-diff.mjs +2 -0
  262. package/src/runtime/shared/bounded-file-read.mjs +13 -0
  263. package/src/runtime/shared/bounded-line-diff.mjs +182 -0
  264. package/src/runtime/shared/buffered-appender.mjs +18 -0
  265. package/src/runtime/shared/config.mjs +78 -18
  266. package/src/runtime/shared/err-text.mjs +6 -0
  267. package/src/runtime/shared/file-permissions.d.mts +6 -1
  268. package/src/runtime/shared/file-permissions.mjs +54 -6
  269. package/src/runtime/shared/llm/anthropic-effort.mjs +6 -0
  270. package/src/runtime/shared/llm/usage-accounting.mjs +5 -2
  271. package/src/runtime/shared/llm/usage-ledger-rollup.mjs +3 -0
  272. package/src/runtime/shared/loopback-listener.mjs +9 -0
  273. package/src/runtime/shared/provider-api-key.mjs +2 -0
  274. package/src/runtime/shared/review-diff.mjs +52 -0
  275. package/src/runtime/shared/session-sidebar-host.d.mts +4 -0
  276. package/src/runtime/shared/session-sidebar-host.mjs +38 -0
  277. package/src/runtime/shared/slow-span.mjs +114 -0
  278. package/src/runtime/shared/statusline/statusline-segments.mjs +28 -4
  279. package/src/runtime/shared/tool-card-model/card-header.mjs +6 -1
  280. package/src/runtime/shared/tool-card-model/generic-detail.mjs +39 -5
  281. package/src/runtime/shared/tool-card-model.mjs +5 -2
  282. package/src/runtime/shared/tool-output-limit.mjs +4 -0
  283. package/src/runtime/shared/tool-primitives.mjs +13 -0
  284. package/src/runtime/shared/tool-result-summary.mjs +8 -1
  285. package/src/runtime/shared/tool-surface.mjs +18 -3
  286. package/src/runtime/shared/tool-work-units.mjs +40 -4
  287. package/src/runtime/shared/transcript-writer/purge.mjs +74 -0
  288. package/src/runtime/shared/transcript-writer.mjs +10 -2
  289. package/src/runtime/shared/turn-changes.mjs +268 -100
  290. package/src/runtime/shared/turn-review-reply.mjs +8 -39
  291. package/src/runtime/shared/turn-snapshot-store.mjs +19 -108
  292. package/src/runtime/shared/turn-snapshot.mjs +857 -683
  293. package/src/runtime/shared/turn-worktree-snapshot.mjs +183 -255
  294. package/src/runtime/shared/user-data-guard.mjs +262 -49
  295. package/src/runtime/shared/worktrees/carry.mjs +531 -0
  296. package/src/runtime/shared/worktrees/contract.mjs +34 -0
  297. package/src/runtime/shared/worktrees/file-recovery.mjs +68 -0
  298. package/src/runtime/shared/worktrees/git-head.mjs +205 -0
  299. package/src/runtime/shared/worktrees/git.mjs +8 -3
  300. package/src/runtime/shared/worktrees/maintenance.mjs +1 -1
  301. package/src/runtime/shared/worktrees/manager.mjs +156 -8
  302. package/src/runtime/shared/worktrees/merge.mjs +31 -21
  303. package/src/runtime/shared/worktrees/paths.d.mts +6 -0
  304. package/src/runtime/terminal-bridge/client.mjs +44 -11
  305. package/src/runtime/terminal-bridge/tool-defs.mjs +13 -6
  306. package/src/runtime/tidy/apply.mjs +47 -2
  307. package/src/runtime/tidy/run-engines.mjs +11 -5
  308. package/src/session-runtime/agent-tool-routing.mjs +19 -13
  309. package/src/session-runtime/boot/apis.mjs +13 -5
  310. package/src/session-runtime/boot/facade.mjs +1 -0
  311. package/src/session-runtime/boot/infrastructure.mjs +6 -1
  312. package/src/session-runtime/boot/session.mjs +0 -1
  313. package/src/session-runtime/boot/tools.mjs +4 -1
  314. package/src/session-runtime/cwd-plugins/cwd-apply.mjs +12 -5
  315. package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +65 -0
  316. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +3 -1
  317. package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +11 -4
  318. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +115 -18
  319. package/src/session-runtime/internal-tool-executor.mjs +6 -2
  320. package/src/session-runtime/lifecycle/cold-heir.mjs +188 -0
  321. package/src/session-runtime/lifecycle/inheritance.mjs +71 -27
  322. package/src/session-runtime/lifecycle/session-catalog.mjs +12 -3
  323. package/src/session-runtime/lifecycle/session-switch/resume.mjs +6 -0
  324. package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +29 -6
  325. package/src/session-runtime/lifecycle/teardown.mjs +72 -7
  326. package/src/session-runtime/lifecycle-api.mjs +1 -0
  327. package/src/session-runtime/mcp-glue/connect.mjs +17 -5
  328. package/src/session-runtime/model-route/set-route.mjs +2 -2
  329. package/src/session-runtime/native-web-search-usage.mjs +51 -0
  330. package/src/session-runtime/native-web-search.mjs +10 -1
  331. package/src/session-runtime/new-session-config.mjs +4 -2
  332. package/src/session-runtime/provider-auth/login.mjs +4 -0
  333. package/src/session-runtime/resource-scope-decorate.mjs +10 -3
  334. package/src/session-runtime/runtime-review-api.mjs +8 -2
  335. package/src/session-runtime/services/agent-tool/index-write-queue.mjs +81 -20
  336. package/src/session-runtime/services/agent-tool/lead-worker-index/forgotten-leads.mjs +29 -0
  337. package/src/session-runtime/services/agent-tool/lead-worker-index/index-file.mjs +1 -1
  338. package/src/session-runtime/services/agent-tool/lead-worker-index.mjs +73 -7
  339. package/src/session-runtime/services/agent-tool/tag-registry.mjs +15 -1
  340. package/src/session-runtime/services/agent-tool/worker-index/row-store.mjs +10 -1
  341. package/src/session-runtime/services/agent-tool/worker-index.mjs +46 -7
  342. package/src/session-runtime/services/agent-tool.mjs +3 -0
  343. package/src/session-runtime/services/channel-admin.mjs +19 -3
  344. package/src/session-runtime/services/memory-runtime-proxy/calls.mjs +4 -0
  345. package/src/session-runtime/services/memory-runtime-proxy.mjs +1 -0
  346. package/src/session-runtime/services/provider-admin.mjs +81 -15
  347. package/src/session-runtime/session-transcript-purge.mjs +32 -0
  348. package/src/session-runtime/session-turn-api.mjs +14 -5
  349. package/src/session-runtime/settings-builtin-tools-api.mjs +9 -0
  350. package/src/session-runtime/setup-tool/executor.mjs +36 -6
  351. package/src/session-runtime/setup-tool/extended-actions.mjs +86 -6
  352. package/src/session-runtime/setup-tool/settings-contract.mjs +13 -3
  353. package/src/session-runtime/setup-tool/tool-defs.mjs +13 -4
  354. package/src/session-runtime/skills-api.mjs +3 -5
  355. package/src/session-runtime/tool-surface.mjs +5 -1
  356. package/src/session-runtime/turn/compaction-gate.mjs +25 -0
  357. package/src/session-runtime/turn/session-ops.mjs +7 -5
  358. package/src/session-runtime/turn/side-question-ops.mjs +22 -0
  359. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +5 -1
  360. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +8 -3
  361. package/src/session-runtime/turn/turn-run/turn-settle.mjs +10 -0
  362. package/src/session-runtime/turn/turn-run/turn-timing.mjs +4 -3
  363. package/src/session-runtime/turn/turn-run.mjs +25 -9
  364. package/src/session-runtime/usage-stats-api.mjs +43 -11
  365. package/src/session-runtime/workspace/project-branches.mjs +7 -5
  366. package/src/session-runtime/workspace/transition-steps.mjs +129 -31
  367. package/src/session-runtime/workspace/transitions.mjs +190 -15
  368. package/src/standalone/daemon-agent-control.mjs +9 -1
  369. package/src/standalone/daemon-local-session-bridge.mjs +14 -1
  370. package/src/standalone/daemon-owner.mjs +17 -4
  371. package/src/standalone/daemon-pg-stop.mjs +51 -0
  372. package/src/standalone/daemon-stop-budget.d.mts +2 -0
  373. package/src/standalone/daemon-stop-budget.mjs +18 -0
  374. package/src/standalone/daemon-stored-session-views.mjs +2 -0
  375. package/src/standalone/daemon-telemetry.mjs +19 -0
  376. package/src/standalone/daemon.mjs +61 -1
  377. package/src/standalone/desktop-service-registry.mjs +5 -0
  378. package/src/standalone/local-session-runtime.mjs +70 -19
  379. package/src/standalone/session-client.mjs +42 -0
  380. package/src/standalone/session-protocol.mjs +7 -0
  381. package/src/standalone/session-proxy/projection.mjs +41 -4
  382. package/src/standalone/session-runtime-inline-host/boot-loaders.mjs +15 -1
  383. package/src/standalone/session-runtime-inline-host.mjs +1 -0
  384. package/src/standalone/session-service/session-calls/session-action.mjs +18 -3
  385. package/src/standalone/session-service-api.mjs +6 -1
  386. package/src/standalone/session-state-patch.d.mts +2 -1
  387. package/src/standalone/session-state-patch.mjs +41 -1
  388. package/src/tui/App.jsx +25 -0
  389. package/src/tui/app/app-view/floating-panel.jsx +26 -1
  390. package/src/tui/app/btw-controller.mjs +118 -0
  391. package/src/tui/app/create-app-pickers.mjs +2 -0
  392. package/src/tui/app/shell-layout/row-budgets.mjs +11 -1
  393. package/src/tui/app/shell-layout/surfaces.mjs +3 -1
  394. package/src/tui/app/slash-commands.mjs +13 -3
  395. package/src/tui/app/slash-dispatch/session-commands.mjs +19 -6
  396. package/src/tui/app/use-global-key-input/approval-shortcut.mjs +12 -0
  397. package/src/tui/app/use-global-key-input.mjs +5 -12
  398. package/src/tui/app/use-prompt-handlers.mjs +11 -0
  399. package/src/tui/components/BtwPanel.jsx +53 -0
  400. package/src/tui/components/QuestionPanel.jsx +275 -0
  401. package/src/tui/components/question-panel-model.mjs +156 -0
  402. package/src/tui/dist/index.mjs +2413 -1465
  403. package/src/tui/session/context-state.mjs +4 -2
  404. package/src/tui/session/session-api/commands.mjs +10 -0
  405. package/src/tui/session/session-api/controls.mjs +2 -2
  406. package/src/tui/session/session-api/integrations.mjs +9 -2
  407. package/src/tui/session/session-api/lifecycle/dispose.mjs +1 -0
  408. package/src/tui/session/session-api/lifecycle.mjs +2 -1
  409. package/src/tui/session/session-api/settings.mjs +6 -0
  410. package/src/tui/session/session-api.mjs +1 -0
  411. package/src/tui/session/tool-approval.mjs +10 -7
  412. package/src/tui/session/transcript-spill.mjs +14 -7
  413. package/src/tui/session/turn-stream.mjs +174 -31
  414. package/src/tui/session/turn.mjs +12 -5
  415. package/src/workflows/default/WORKFLOW.md +3 -1
@@ -8,15 +8,25 @@
8
8
  import { createHash } from 'node:crypto';
9
9
  import { lstat, open, realpath, stat } from 'node:fs/promises';
10
10
  import { dirname, isAbsolute, relative, resolve } from 'node:path';
11
- import { structuredPatch } from 'diff';
11
+ import { deflateSync } from 'node:zlib';
12
12
  import { cleanString as clean } from './clean.mjs';
13
13
  import { runGitOffThread } from './git-runner.mjs';
14
+ import { createLineDiffBudget, lineDiffHunks } from './bounded-line-diff.mjs';
15
+ import {
16
+ createReviewPatchBudget,
17
+ filePatchReply,
18
+ PATCH_TOO_LARGE,
19
+ patchOmittedFile,
20
+ REVIEW_FILE_PATCH_MAX_BYTES,
21
+ REVIEW_FILE_PATCH_SHOW_MAX_BYTES,
22
+ REVIEW_PATCH_MAX_BYTES,
23
+ reviewDiffTooLarge,
24
+ } from './review-diff.mjs';
14
25
 
15
26
  /** Largest single-file patch a record keeps replayable. */
16
27
  export const TURN_CHANGE_FILE_PATCH_MAX_BYTES = 8 * 1024 * 1024;
17
28
  const APPLY_TIMEOUT_MS = 60_000;
18
29
  const APPLY_OUTPUT_MAX_BYTES = 4 * 1024 * 1024;
19
- const TRACKED_DIFF_TIMEOUT_MS = 2_000;
20
30
  const STATUSES = new Set(['A', 'M', 'D', 'R']);
21
31
  // Characters a patch header cannot carry unquoted; such a path is recorded but
22
32
  // never replayed.
@@ -157,13 +167,15 @@ export function splitPatchSections(buffer) {
157
167
  });
158
168
  }
159
169
 
160
- // ── Exact tracker (non-Git) patches ─────────────────────────────────────────
170
+ // ── Exact tracker patches (the session's own tool bytes) ────────────────────
161
171
 
162
172
  const utf8Decoder = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
163
- function decodeText(content) {
173
+ /** Text of one change side (bytes or a string): '' when absent, null for
174
+ * binary content (not UTF-8, or holding a NUL byte). */
175
+ export function decodeText(content) {
164
176
  if (content == null) return '';
165
177
  try {
166
- const text = utf8Decoder.decode(content);
178
+ const text = typeof content === 'string' ? content : utf8Decoder.decode(content);
167
179
  // A NUL byte marks binary content: it gets no text diff.
168
180
  return text.includes('\0') ? null : text;
169
181
  } catch {
@@ -171,13 +183,79 @@ function decodeText(content) {
171
183
  }
172
184
  }
173
185
 
186
+ /** True for bytes that get a binary patch instead of a text diff (not UTF-8,
187
+ * or holding a NUL byte). */
188
+ export function isBinaryChangeContent(content) {
189
+ return content != null && decodeText(content) === null;
190
+ }
191
+
174
192
  const headerTab = (path) => (path.includes(' ') ? '\t' : '');
175
193
 
194
+ /** The object format (`sha1`/`sha256`) `git apply` hashes blobs with in `root`:
195
+ * the repository's own, or sha1 outside any repository. */
196
+ export async function repositoryObjectFormat(root) {
197
+ const result = await runGitOffThread(['rev-parse', '--show-object-format'], {
198
+ cwd: root,
199
+ timeoutMs: APPLY_TIMEOUT_MS,
200
+ maxBytes: APPLY_OUTPUT_MAX_BYTES,
201
+ }).catch(() => null);
202
+ return result?.code === 0 && clean(result.stdout) === 'sha256' ? 'sha256' : 'sha1';
203
+ }
204
+
205
+ const BASE85 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~';
206
+
207
+ /** One line of a git binary hunk: a length letter, then 4-byte groups as base85. */
208
+ function base85Line(chunk) {
209
+ let line = String.fromCharCode(chunk.length <= 26 ? 64 + chunk.length : 70 + chunk.length);
210
+ for (let offset = 0; offset < chunk.length; offset += 4) {
211
+ let value = 0;
212
+ for (let index = 0; index < 4; index += 1) value = value * 256 + (chunk[offset + index] ?? 0);
213
+ const group = new Array(5);
214
+ for (let index = 4; index >= 0; index -= 1) {
215
+ group[index] = BASE85[value % 85];
216
+ value = Math.floor(value / 85);
217
+ }
218
+ line += group.join('');
219
+ }
220
+ return line;
221
+ }
222
+
223
+ function binaryLiteral(bytes) {
224
+ const deflated = deflateSync(bytes);
225
+ let text = `literal ${bytes.length}\n`;
226
+ for (let offset = 0; offset < deflated.length; offset += 52) {
227
+ text += `${base85Line(deflated.subarray(offset, offset + 52))}\n`;
228
+ }
229
+ return `${text}\n`;
230
+ }
231
+
232
+ /** The `index` line and `GIT binary patch` body of a byte change. Full blob
233
+ * ids make `git apply` verify the file still holds the recorded bytes on
234
+ * either side before it writes, so a binary file changed since is refused. */
235
+ function binaryPatchBody(before, after, status, objectFormat) {
236
+ const empty = Buffer.alloc(0);
237
+ const id = (bytes) => createHash(objectFormat).update(`blob ${bytes.length}\0`).update(bytes).digest('hex');
238
+ const zero = '0'.repeat(objectFormat === 'sha256' ? 64 : 40);
239
+ const oldId = before === null ? zero : id(before);
240
+ const newId = after === null ? zero : id(after);
241
+ return `index ${oldId}..${newId}${status === 'M' ? ' 100644' : ''}\nGIT binary patch\n${binaryLiteral(after ?? empty)}${binaryLiteral(before ?? empty)}`;
242
+ }
243
+
176
244
  /**
177
245
  * One git-format patch from exact before/after bytes (null = absent). Paths
178
- * are root-relative. Returns the record file without its state.
246
+ * are root-relative. Text diffs as hunks within `budget` (see
247
+ * bounded-line-diff.mjs); binary content (isBinaryChangeContent) as a
248
+ * full-index binary patch hashed in `objectFormat`. Returns the record file
249
+ * without its state.
179
250
  */
180
- export function trackedChangeFile({ oldPath, newPath, before, after }) {
251
+ export function trackedChangeFile({
252
+ oldPath,
253
+ newPath,
254
+ before,
255
+ after,
256
+ objectFormat = 'sha1',
257
+ budget = createLineDiffBudget(),
258
+ }) {
181
259
  let status = 'M';
182
260
  if (before === null) status = 'A';
183
261
  else if (after === null) status = 'D';
@@ -190,22 +268,16 @@ export function trackedChangeFile({ oldPath, newPath, before, after }) {
190
268
  const oldText = decodeText(before);
191
269
  const newText = decodeText(after);
192
270
  if (oldText === null || newText === null) {
193
- return {
194
- ...base,
195
- binary: true,
196
- revertable: false,
197
- reason: 'binary content was not recorded as a replayable patch',
198
- };
199
- }
200
- let hunks = [];
201
- if (oldText !== newText) {
202
- const result = structuredPatch(`a/${oldPath}`, `b/${newPath}`, oldText, newText, '', '', {
203
- context: 3,
204
- timeout: TRACKED_DIFF_TIMEOUT_MS,
205
- });
206
- if (!result) return { ...base, revertable: false, reason: 'the diff took too long to compute' };
207
- hunks = result.hunks;
271
+ let patch = `diff --git a/${status === 'A' ? newPath : oldPath} b/${status === 'D' ? oldPath : newPath}\n`;
272
+ if (status === 'A') patch += 'new file mode 100644\n';
273
+ if (status === 'D') patch += 'deleted file mode 100644\n';
274
+ if (status === 'R') patch += `rename from ${oldPath}\nrename to ${newPath}\n`;
275
+ // A pure rename carries the full-index body too: `git apply` then moves
276
+ // the file only while it still holds the recorded bytes.
277
+ patch += binaryPatchBody(before, after, status, objectFormat);
278
+ return { ...base, binary: true, revertable: true, patch: Buffer.from(patch, 'utf8') };
208
279
  }
280
+ const { hunks } = lineDiffHunks(oldText, newText, budget, [before, after]);
209
281
  const oldName = status === 'A' ? newPath : oldPath;
210
282
  const newName = status === 'D' ? oldPath : newPath;
211
283
  let patch = `diff --git a/${oldName} b/${newName}\n`;
@@ -349,16 +421,17 @@ export function turnChangesReply({ status, checkpointId, record = null, patch =
349
421
 
350
422
  export const NET_TEXT_MAX_BYTES = 1024 * 1024;
351
423
  /** Budget of the merged session patch, in UTF-8 bytes. */
352
- export const MERGED_PATCH_MAX_BYTES = 2 * 1024 * 1024;
424
+ export const MERGED_PATCH_MAX_BYTES = REVIEW_PATCH_MAX_BYTES;
353
425
  const VIEW_PATCH_MAX_BYTES = 256 * 1024;
354
426
 
355
427
  /** Counts and a placeholder-named patch of exact before/after bytes. */
356
- function measureChange(before, after, renamed) {
428
+ function measureChange(before, after, renamed, budget) {
357
429
  const measured = trackedChangeFile({
358
430
  oldPath: renamed ? 'mx-old-file' : 'mx-file',
359
431
  newPath: renamed ? 'mx-new-file' : 'mx-file',
360
432
  before,
361
433
  after,
434
+ budget,
362
435
  });
363
436
  return {
364
437
  status: measured.status,
@@ -384,9 +457,16 @@ function nameMeasuredPatch(text, oldName, newName) {
384
457
 
385
458
  /** A git-format patch of exact before/after text under the names a viewer
386
459
  * shows (absolute for a file outside the worktree). `before`/`after` are
387
- * Buffers or null (absent). */
388
- export function namedChangePatch({ before, after, oldName, newName, maxBytes = VIEW_PATCH_MAX_BYTES }) {
389
- const { text, ...measured } = measureChange(before, after, oldName !== newName);
460
+ * Buffers or null (absent); `budget` the operation's line-diff budget. */
461
+ export function namedChangePatch({
462
+ before,
463
+ after,
464
+ oldName,
465
+ newName,
466
+ maxBytes = VIEW_PATCH_MAX_BYTES,
467
+ budget = createLineDiffBudget(),
468
+ }) {
469
+ const { text, ...measured } = measureChange(before, after, oldName !== newName, budget);
390
470
  const patch = nameMeasuredPatch(text, oldName, newName);
391
471
  return { ...measured, patch: Buffer.byteLength(patch, 'utf8') > maxBytes ? '' : patch };
392
472
  }
@@ -397,21 +477,39 @@ const absolutePath = (root, path) => (isOutsidePath(path) ? resolve(path) : reso
397
477
  const patchOf = (file) =>
398
478
  changePatchBuffer(file)?.toString('utf8') || (typeof file?.viewPatch === 'string' ? file.viewPatch : '');
399
479
 
480
+ /** A record file's patch as a review shows it: a binary body becomes the
481
+ * `Binary files … differ` line a diff viewer renders. */
482
+ export function changeDisplayPatch(file) {
483
+ const buffer =
484
+ changePatchBuffer(file) || (typeof file?.viewPatch === 'string' ? Buffer.from(file.viewPatch, 'utf8') : null);
485
+ return splitPatchSections(buffer)
486
+ .map(({ oldPath, newPath, patch }) => {
487
+ const text = patch.toString('utf8');
488
+ const body = text.indexOf('\nGIT binary patch\n');
489
+ if (body < 0) return text;
490
+ const head = text.slice(0, body + 1).replace(/^index .*\n/m, '');
491
+ return `${head}Binary files ${oldPath === null ? '/dev/null' : `a/${oldPath}`} and ${newPath === null ? '/dev/null' : `b/${newPath}`} differ\n`;
492
+ })
493
+ .join('');
494
+ }
495
+
400
496
  const insideDirectory = (parent, child) => {
401
497
  const rel = relative(parent, child);
402
498
  return rel === '' || (!isAbsolute(rel) && rel.split(/[\\/]+/)[0] !== '..');
403
499
  };
404
500
 
405
- /** At most NET_TEXT_MAX_BYTES of a regular file, read through one handle:
406
- * undefined when it is larger (checked by size first, never read whole). */
407
- async function readBounded(file) {
501
+ /** At most `maxBytes` of a regular file, read through one handle: undefined
502
+ * when it is larger (checked by size first, never read whole). */
503
+ async function readBounded(file, maxBytes = NET_TEXT_MAX_BYTES) {
408
504
  const handle = await open(file, 'r');
409
505
  try {
410
506
  const info = await handle.stat();
411
- if (!info.isFile() || info.size > NET_TEXT_MAX_BYTES) return undefined;
412
- const buffer = Buffer.alloc(NET_TEXT_MAX_BYTES + 1);
507
+ if (!info.isFile() || info.size > maxBytes) return undefined;
508
+ // Sized by the file, one byte over: a file that grew since its stat is
509
+ // not read part-way.
510
+ const buffer = Buffer.alloc(info.size + 1);
413
511
  const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
414
- return bytesRead > NET_TEXT_MAX_BYTES ? undefined : buffer.subarray(0, bytesRead);
512
+ return bytesRead > info.size ? undefined : buffer.subarray(0, bytesRead);
415
513
  } finally {
416
514
  await handle.close();
417
515
  }
@@ -471,13 +569,20 @@ function isUnmeasuredFile(file) {
471
569
 
472
570
  /** A legacy record kept no counts or diff for a new file it could not replay
473
571
  * (outside the root): measure the file as it is on disk now. Returns the
474
- * file with its counts and display patch, or the file unchanged. */
475
- export async function measureLegacyChangeFile(file, root) {
572
+ * file with its counts and display patch, or the file unchanged. `budget`:
573
+ * the line-diff budget of the read it is part of. */
574
+ export async function measureLegacyChangeFile(file, root, budget = createLineDiffBudget()) {
476
575
  if (!isUnmeasuredFile(file)) return file;
477
576
  const text = await currentText(root, file.path);
478
577
  if (typeof text !== 'string' || !text) return file;
479
578
  const name = slashPath(file.path);
480
- const named = namedChangePatch({ before: null, after: Buffer.from(text, 'utf8'), oldName: name, newName: name });
579
+ const named = namedChangePatch({
580
+ before: null,
581
+ after: Buffer.from(text, 'utf8'),
582
+ oldName: name,
583
+ newName: name,
584
+ budget,
585
+ });
481
586
  if (!named.patch) return file;
482
587
  return { ...file, additions: named.additions, deletions: named.deletions, viewPatch: named.patch };
483
588
  }
@@ -487,7 +592,10 @@ const DIGEST_MAX_BYTES = 64 * 1024 * 1024;
487
592
  // Largest measured patch a review keeps cached between polls.
488
593
  const CACHED_PATCH_MAX_BYTES = 256 * 1024;
489
594
  const READ_CONCURRENCY = 16;
490
- const TOO_LARGE_REASON = 'file is too large to diff';
595
+ const CURRENT_READ_MAX_BYTES = REVIEW_FILE_PATCH_SHOW_MAX_BYTES;
596
+ /** A file whose current content is past CURRENT_READ_MAX_BYTES (or cannot be
597
+ * read whole): listed without a patch, explicitly too large. */
598
+ const tooLargeChange = (status) => ({ status, additions: 0, deletions: 0, text: '', patchOmitted: true });
491
599
  const UNREADABLE_REASON = 'the file cannot be read safely now';
492
600
  const SEVERAL_TURNS_REASON = 'edited in several turns before its session baseline was recorded';
493
601
  /** Why a listed file has no exact diff against the session's first state. */
@@ -534,12 +642,14 @@ async function currentFile({ root, path }) {
534
642
 
535
643
  const currentSig = (current) => (current?.state === 'present' ? current.sig : current?.state || '');
536
644
 
537
- /** Bytes of a present file up to NET_TEXT_MAX_BYTES; undefined past it or
538
- * when it cannot be read. */
645
+ /** Bytes of a present file up to CURRENT_READ_MAX_BYTES — what one file's
646
+ * own patch read may show, so a file past the reply's per-file cap still has
647
+ * its counts and its patch on request; undefined past it or when it cannot
648
+ * be read. */
539
649
  async function currentBytes(current) {
540
- if (current.size > NET_TEXT_MAX_BYTES) return undefined;
650
+ if (current.size > CURRENT_READ_MAX_BYTES) return undefined;
541
651
  try {
542
- return await readBounded(current.file);
652
+ return await readBounded(current.file, CURRENT_READ_MAX_BYTES);
543
653
  } catch {
544
654
  return undefined;
545
655
  }
@@ -600,7 +710,7 @@ const UNVOUCHED_UNCHANGED = Object.freeze({ hidden: true, partial: true });
600
710
  /** One file (or rename) measured from its first-seen state `entry` to the
601
711
  * disk now (`current`); null when the session left it as it found it,
602
712
  * UNVOUCHED_UNCHANGED when only a hash says so. */
603
- async function measureBaseline(entry, current, renamed, readBlob) {
713
+ async function measureBaseline(entry, current, renamed, readBlob, budget) {
604
714
  const changed = renamed ? 'R' : 'M';
605
715
  if (current.state === 'unsafe') {
606
716
  return partialChange(entry.state === 'absent' ? 'A' : changed, UNREADABLE_REASON);
@@ -609,15 +719,15 @@ async function measureBaseline(entry, current, renamed, readBlob) {
609
719
  const after = present ? await currentBytes(current) : null;
610
720
  if (entry.state === 'absent') {
611
721
  if (!present) return null;
612
- if (after === undefined) return { status: 'A', additions: 0, deletions: 0, text: '', reason: TOO_LARGE_REASON };
613
- return measureChange(null, after, false);
722
+ if (after === undefined) return tooLargeChange('A');
723
+ return measureChange(null, after, false, budget);
614
724
  }
615
725
  if (present) {
616
726
  const same = await sameAsBaseline(entry, current, after);
617
727
  const exact = entry.status === 'captured';
618
728
  if (same === true && !renamed) return exact ? null : UNVOUCHED_UNCHANGED;
619
729
  if (same === true) {
620
- const moved = await measureChange(Buffer.alloc(0), Buffer.alloc(0), true);
730
+ const moved = measureChange(Buffer.alloc(0), Buffer.alloc(0), true, budget);
621
731
  return exact
622
732
  ? moved
623
733
  : { ...moved, partial: true, baseline: entry.status, reason: BASELINE_REASONS[entry.status] };
@@ -627,14 +737,24 @@ async function measureBaseline(entry, current, renamed, readBlob) {
627
737
  const before = entry.status === 'captured' ? await readBlob(entry) : undefined;
628
738
  if (!before)
629
739
  return baselineGap(entry.status === 'captured' ? { ...entry, status: 'evicted' } : entry, present ? changed : 'D');
630
- if (after === undefined) return { status: changed, additions: 0, deletions: 0, text: '', reason: TOO_LARGE_REASON };
631
- return measureChange(before, after, renamed);
740
+ if (after === undefined) return tooLargeChange(changed);
741
+ return measureChange(before, after, renamed, budget);
632
742
  }
633
743
 
634
- /** Every baseline not `covered` by another source, measured against the disk.
635
- * A path holding a renamed file pairs with the path it came from when that
636
- * one is gone. Measurements are reused while the files' stats are unchanged. */
637
- async function baselineChanges(baselines, covered, unvouched) {
744
+ /** A list measurement as a reply keeps it: text past the per-file inline cap
745
+ * is dropped at once, its counts kept and the file marked too large. */
746
+ function inlineChange(change) {
747
+ if (Buffer.byteLength(change?.text || '', 'utf8') <= REVIEW_FILE_PATCH_MAX_BYTES) return change;
748
+ return { ...change, text: '', patchOmitted: true };
749
+ }
750
+
751
+ /** Every baseline not `covered` by another source, measured against the disk
752
+ * within the review's line-diff `budget`. A path holding a renamed file pairs
753
+ * with the path it came from when that one is gone. Measurements are reused
754
+ * while the files' stats are unchanged. With `target` (an entry predicate)
755
+ * only the units it matches are measured, with their whole text; every other
756
+ * unit is returned `nameOnly`, for naming. */
757
+ async function baselineChanges(baselines, covered, unvouched, budget, { target = null } = {}) {
638
758
  const entries = new Map();
639
759
  for (const entry of baselines.files) if (!covered(entry.path)) entries.set(entry.key, entry);
640
760
  const list = [...entries.values()];
@@ -655,6 +775,12 @@ async function baselineChanges(baselines, covered, unvouched) {
655
775
  for (const entry of list) if (!paired.has(entry.key)) units.push({ key: entry.key, before: entry, after: entry });
656
776
  const measured = await mapLimit(units, READ_CONCURRENCY, async (unit) => {
657
777
  const renamed = unit.before !== unit.after;
778
+ const named = {
779
+ key: unit.after.key,
780
+ abs: unit.after.path,
781
+ root: unit.after.root,
782
+ ...(renamed ? { oldAbs: unit.before.path, oldRoot: unit.before.root } : {}),
783
+ };
658
784
  const now = current.get(unit.after.key);
659
785
  const sig = [
660
786
  unit.before.state,
@@ -667,22 +793,27 @@ async function baselineChanges(baselines, covered, unvouched) {
667
793
  currentSig(current.get(unit.before.key)),
668
794
  ].join('|');
669
795
  const cached = baselines.cache?.get(unit.key);
670
- let change = cached?.sig === sig ? cached.change : undefined;
796
+ if (target && !target(unit.after) && !target(unit.before)) {
797
+ // Not measured: it names a file unless the cache knows it is unchanged.
798
+ const unchanged = cached?.sig === sig && (!cached.change || cached.change.hidden);
799
+ return unchanged ? null : { ...named, nameOnly: true };
800
+ }
801
+ // A file's own patch read measures again what was cached without its text.
802
+ let change = cached?.sig === sig && !(target && cached.stripped) ? cached.change : undefined;
671
803
  if (change === undefined) {
672
- change = await measureBaseline(unit.before, now, renamed, baselines.readBlob);
673
- if (Buffer.byteLength(change?.text || '', 'utf8') <= CACHED_PATCH_MAX_BYTES) {
674
- baselines.cache?.set(unit.key, { sig, change });
804
+ const whole = await measureBaseline(unit.before, now, renamed, baselines.readBlob, budget);
805
+ const inline = inlineChange(whole);
806
+ // Once the budget is spent a diff may have been cut short to the
807
+ // replacement hunk: such a measurement is not kept, a later review
808
+ // measures it again.
809
+ if (budget.remainingMs > 0 && Buffer.byteLength(inline?.text || '', 'utf8') <= CACHED_PATCH_MAX_BYTES) {
810
+ baselines.cache?.set(unit.key, { sig, change: inline, ...(inline !== whole ? { stripped: true } : {}) });
675
811
  }
812
+ change = target ? whole : inline;
676
813
  }
677
814
  if (change?.hidden) unvouched.add(unit.after.key);
678
815
  if (!change || change.hidden) return null;
679
- return {
680
- key: unit.after.key,
681
- abs: unit.after.path,
682
- root: unit.after.root,
683
- ...(renamed ? { oldAbs: unit.before.path, oldRoot: unit.before.root } : {}),
684
- change,
685
- };
816
+ return { ...named, change };
686
817
  });
687
818
  return measured.filter(Boolean);
688
819
  }
@@ -694,7 +825,7 @@ async function baselineChanges(baselines, covered, unvouched) {
694
825
  * Also returns the baseline keys such older records touched: those baselines
695
826
  * were first seen after an unrecorded edit.
696
827
  */
697
- async function legacyChanges(records, { kinds, listed, covered, since }) {
828
+ async function legacyChanges(records, { kinds, listed, covered, since, budget }) {
698
829
  const groups = new Map();
699
830
  const late = new Set();
700
831
  for (const record of records) {
@@ -747,7 +878,7 @@ async function legacyChanges(records, { kinds, listed, covered, since }) {
747
878
  });
748
879
  continue;
749
880
  }
750
- const measured = await measureLegacyChangeFile(file, group.root);
881
+ const measured = await measureLegacyChangeFile(file, group.root, budget);
751
882
  const recordPatch = patchOf(measured);
752
883
  const counts = (Number(measured.additions) || 0) + (Number(measured.deletions) || 0);
753
884
  changes.push({
@@ -802,10 +933,14 @@ async function missingChanges(refs, { listed, covered }) {
802
933
  * first state the session itself recorded; otherwise the result is `partial`.
803
934
  * A file the user undid is back at its first state and is not listed. Each
804
935
  * listed file carries an `id` (its absolute path); two roots sharing a
805
- * relative name both show their absolute path.
936
+ * relative name both show their absolute path. A file's patch past the
937
+ * per-file cap, or past what the reply's total leaves, is left out
938
+ * (`patchOmitted`, reason PATCH_TOO_LARGE); `diffTooLarge` once nothing fit.
939
+ * `file` (an `id` or listed path) answers that one file's patch instead, up
940
+ * to the file-patch read's cap.
806
941
  */
807
- export async function sessionChangeReview(base, { baselines = null, records = [] } = {}) {
808
- const { source = '', reason: unavailableReason, owned, ...kept } = base || {};
942
+ export async function sessionChangeReview(base, { baselines = null, records = [], file: target = '' } = {}) {
943
+ const { source = '', reason: unavailableReason, owned, diffTooLarge: _baseTooLarge, ...kept } = base || {};
809
944
  const gitBacked = source === 'git';
810
945
  const baseRoot = clean(base?.root);
811
946
  const covered = (abs) => gitBacked && Boolean(baseRoot) && insideDirectory(baseRoot, abs);
@@ -815,12 +950,27 @@ export async function sessionChangeReview(base, { baselines = null, records = []
815
950
  else if (source === 'absent' || source === 'unavailable') kinds = new Set(['git', 'tracked']);
816
951
  // Baselines nothing is listed for, yet whose emptiness cannot be vouched for.
817
952
  const unvouched = new Set();
818
- const measured = baselines ? await baselineChanges(baselines, covered, unvouched) : [];
953
+ // One line-diff budget for the whole review pass.
954
+ const diffBudget = createLineDiffBudget();
955
+ // A file's own patch read measures only the baselines of that file (by
956
+ // `id` or by its name under its root).
957
+ const targetKey = clean(target) ? changePathKey(target) : '';
958
+ const targeted = (entry) =>
959
+ changePathKey(entry.path) === targetKey ||
960
+ (Boolean(entry.root) &&
961
+ insideDirectory(entry.root, entry.path) &&
962
+ changePathKey(relative(entry.root, entry.path)) === targetKey);
963
+ const baselineUnits = baselines
964
+ ? await baselineChanges(baselines, covered, unvouched, diffBudget, { target: targetKey ? targeted : null })
965
+ : [];
966
+ // Units named but not measured still claim their names (see `show`).
967
+ const measured = baselineUnits.filter((unit) => !unit.nameOnly);
819
968
  const legacy = await legacyChanges(Array.isArray(records) ? records : [], {
820
969
  kinds,
821
970
  listed,
822
971
  covered,
823
972
  since: Number(baselines?.createdAt) || Number.POSITIVE_INFINITY,
973
+ budget: diffBudget,
824
974
  });
825
975
  for (const change of legacy.changes) listed.add(change.key);
826
976
  // Owned files of the non-Git root, and paths whose first state a clear in
@@ -846,7 +996,7 @@ export async function sessionChangeReview(base, { baselines = null, records = []
846
996
  owners.set(key, new Set([...(owners.get(key) || []), changePathKey(abs)]));
847
997
  };
848
998
  for (const file of baseFiles) if (file?.path) claim(file.path, resolve(baseRoot, file.path));
849
- for (const change of changes) {
999
+ for (const change of [...changes, ...baselineUnits.filter((unit) => unit.nameOnly)]) {
850
1000
  claim(nameOf(change.abs, change.root), change.abs);
851
1001
  if (change.oldAbs) claim(nameOf(change.oldAbs, change.oldRoot), change.oldAbs);
852
1002
  }
@@ -861,11 +1011,6 @@ export async function sessionChangeReview(base, { baselines = null, records = []
861
1011
  let patch = '';
862
1012
  if (change.text) patch = nameMeasuredPatch(change.text, oldName, path);
863
1013
  else if (change.recordPatch && change.recordName === path) patch = change.recordPatch;
864
- let patchOmitted = change.patchOmitted === true;
865
- if (Buffer.byteLength(patch, 'utf8') > MERGED_PATCH_MAX_BYTES) {
866
- patch = '';
867
- patchOmitted = true;
868
- }
869
1014
  return {
870
1015
  path,
871
1016
  id: slashPath(abs),
@@ -878,7 +1023,8 @@ export async function sessionChangeReview(base, { baselines = null, records = []
878
1023
  ...(change.reason ? { reason: change.reason } : {}),
879
1024
  ...(change.baseline ? { baseline: change.baseline } : {}),
880
1025
  ...(change.unmeasured ? { unmeasured: true } : {}),
881
- ...(patchOmitted ? { patchOmitted: true } : {}),
1026
+ // Its patch is past what can be kept or read: listed too large.
1027
+ ...(change.patchOmitted === true && !patch ? { patchOmitted: true, reason: PATCH_TOO_LARGE } : {}),
882
1028
  patch,
883
1029
  };
884
1030
  })
@@ -886,21 +1032,20 @@ export async function sessionChangeReview(base, { baselines = null, records = []
886
1032
  if (left.path === right.path) return 0;
887
1033
  return left.path < right.path ? -1 : 1;
888
1034
  });
889
- // The merged response stays bounded: past the budget a file is listed
890
- // without its diff.
891
- let budget = MERGED_PATCH_MAX_BYTES - Buffer.byteLength(basePatch, 'utf8');
892
- let truncated = false;
1035
+ if (clean(target)) {
1036
+ const key = changePathKey(target);
1037
+ const row = rows.find((entry) => changePathKey(entry.id) === key || changePathKey(entry.path) === key);
1038
+ if (row?.patchOmitted === true && !row.patch) return { ...filePatchReply(row.path, ''), ...patchOmittedFile({}) };
1039
+ return filePatchReply(row?.path || slashPath(target), row?.patch || '');
1040
+ }
1041
+ // The merged response stays bounded: a file past the per-file cap or the
1042
+ // total left is listed with its counts but without its diff.
1043
+ const budget = createReviewPatchBudget(MERGED_PATCH_MAX_BYTES - Buffer.byteLength(basePatch, 'utf8'));
893
1044
  const files = [];
894
1045
  const patches = [];
895
1046
  for (const { patch, ...file } of rows) {
896
- const size = Buffer.byteLength(patch, 'utf8');
897
- if (patch && size <= budget) {
898
- budget -= size;
899
- patches.push(patch);
900
- } else if (patch) {
901
- truncated = true;
902
- file.patchOmitted = true;
903
- }
1047
+ if (patch && budget.admit(Buffer.byteLength(patch, 'utf8'))) patches.push(patch);
1048
+ else if (patch) Object.assign(file, patchOmittedFile(file));
904
1049
  files.push(file);
905
1050
  }
906
1051
  // A baseline first seen after an older recorded edit (`late`) was not the
@@ -917,6 +1062,7 @@ export async function sessionChangeReview(base, { baselines = null, records = []
917
1062
  const all = [...baseFiles, ...files];
918
1063
  // An unavailable Git baseline with nothing else to list stays unavailable.
919
1064
  if (all.length === 0 && !vouched && kept.supported === false) return { ...kept, reason: unavailableReason };
1065
+ const patch = [basePatch, ...patches].filter(Boolean).join(basePatch.endsWith('\n') || !basePatch ? '' : '\n');
920
1066
  return {
921
1067
  ...kept,
922
1068
  supported: true,
@@ -926,18 +1072,33 @@ export async function sessionChangeReview(base, { baselines = null, records = []
926
1072
  ...(partial ? { partial: true } : {}),
927
1073
  ...(baselines?.unsaved === true ? { baselinesUnsaved: true } : {}),
928
1074
  files: all,
929
- patch: [basePatch, ...patches].filter(Boolean).join(basePatch.endsWith('\n') || !basePatch ? '' : '\n'),
930
- patchTruncated: (gitBacked && kept.patchTruncated === true) || truncated,
1075
+ patch,
1076
+ patchTruncated: gitBacked && kept.patchTruncated === true,
1077
+ ...(reviewDiffTooLarge(all, patch) ? { diffTooLarge: true } : {}),
931
1078
  snapshotKind: kept.snapshotKind || 'session',
932
1079
  };
933
1080
  }
934
1081
 
935
1082
  // ── Apply ───────────────────────────────────────────────────────────────────
936
1083
 
937
- /** Every path a stored patch touches must be exactly the recorded file's. */
1084
+ /** Every path a stored patch touches must be exactly the recorded file's.
1085
+ * Several sections are the file's own segments in write order: each starts
1086
+ * where the one before ended (a foreign edit in between stays out of both),
1087
+ * from the recorded old path to its current one. Resolves to the sections. */
938
1088
  function assertPatchMatchesFile(file, buffer) {
939
1089
  const sections = splitPatchSections(buffer);
940
- if (sections.length !== 1) throw new Error(`${file.path} has a malformed recorded patch`);
1090
+ if (sections.length === 0) throw new Error(`${file.path} has a malformed recorded patch`);
1091
+ const mismatch = () => new Error(`${file.path} has a recorded patch for a different path`);
1092
+ if (sections.length > 1) {
1093
+ let at = file.oldPath || file.path;
1094
+ for (const { oldPath, newPath } of sections) {
1095
+ if ([oldPath, newPath].some((path) => path !== null && !safeChangePath(path))) throw mismatch();
1096
+ if ((oldPath ?? newPath) !== at) throw mismatch();
1097
+ at = newPath ?? oldPath;
1098
+ }
1099
+ if (at !== file.path) throw mismatch();
1100
+ return sections;
1101
+ }
941
1102
  const { oldPath, newPath } = sections[0];
942
1103
  const expectedOld = file.status === 'A' ? null : file.oldPath || file.path;
943
1104
  const expectedNew = file.status === 'D' ? null : file.path;
@@ -945,10 +1106,9 @@ function assertPatchMatchesFile(file, buffer) {
945
1106
  [oldPath, expectedOld],
946
1107
  [newPath, expectedNew],
947
1108
  ]) {
948
- if (expected === null ? actual !== null : actual !== expected || !safeChangePath(actual)) {
949
- throw new Error(`${file.path} has a recorded patch for a different path`);
950
- }
1109
+ if (expected === null ? actual !== null : actual !== expected || !safeChangePath(actual)) throw mismatch();
951
1110
  }
1111
+ return sections;
952
1112
  }
953
1113
 
954
1114
  /** The recorded root must still be the directory (and, for a Git record, the
@@ -989,13 +1149,21 @@ export async function applyTurnChangeFiles(record, files, { reverse }) {
989
1149
  }
990
1150
  const buffer = changePatchBuffer(file);
991
1151
  if (!buffer?.length) throw new Error(`${file.path} has no recorded patch`);
992
- assertPatchMatchesFile(file, buffer);
993
- return buffer.at(-1) === 0x0a ? buffer : Buffer.concat([buffer, Buffer.from('\n')]);
1152
+ const sections = assertPatchMatchesFile(file, buffer);
1153
+ // `git apply` takes a file's sections in input order, each on the result
1154
+ // of the one before: oldest first forward, newest first for an Undo.
1155
+ return Buffer.concat(
1156
+ (reverse ? [...sections].reverse() : sections).map(({ patch }) =>
1157
+ patch.at(-1) === 0x0a ? patch : Buffer.concat([patch, Buffer.from('\n')])
1158
+ )
1159
+ );
994
1160
  });
995
- // A Git record replays with the repository's own line-ending conversion
996
- // (the semantics the shadow snapshot hashed with). An exact tracker patch
997
- // holds raw bytes, so nothing may convert them.
998
- const conversion = record.kind === 'git' ? [] : ['-c', 'core.autocrlf=false', '-c', 'core.eol=lf'];
1161
+ // An older Git record (worktree diff) replays with the repository's own
1162
+ // line-ending conversion, the semantics the shadow snapshot hashed with. A
1163
+ // record of the session's own tool bytes (`counts: 'own'`, any kind) holds
1164
+ // raw bytes, so nothing may convert them.
1165
+ const rawBytes = record.kind !== 'git' || record.counts === 'own';
1166
+ const conversion = rawBytes ? ['-c', 'core.autocrlf=false', '-c', 'core.eol=lf'] : [];
999
1167
  const args = [
1000
1168
  ...conversion,
1001
1169
  '-c',