mixdog 1.1.2 → 1.2.1

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 (424) hide show
  1. package/README.md +8 -5
  2. package/package.json +2 -1
  3. package/scripts/lib/run-node-tests.mjs +9 -0
  4. package/scripts/prune-desktop-runtime.mjs +3 -0
  5. package/scripts/test-direct.mjs +4 -2
  6. package/src/defaults/shipped-history.json +10 -0
  7. package/src/defaults/skills/pptx/SKILL.md +6 -6
  8. package/src/defaults/skills/pptx/references/review.md +6 -4
  9. package/src/defaults/skills/pptx/scripts/source-extract.mjs +1 -1
  10. package/src/defaults/skills/setup/references/actions.md +18 -5
  11. package/src/defaults/skills/visualize/SKILL.md +160 -0
  12. package/src/defaults/skills/visualize/references/craft.md +88 -0
  13. package/src/headless-command.mjs +56 -3
  14. package/src/headless-exec.mjs +17 -1
  15. package/src/help.mjs +2 -0
  16. package/src/rules/shared/60-verification.md +3 -1
  17. package/src/rules/shared/70-delivery.md +4 -2
  18. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +1 -0
  19. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +1 -0
  20. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -0
  21. package/src/runtime/agent/orchestrator/cache-mtime.mjs +26 -5
  22. package/src/runtime/agent/orchestrator/config-presets.mjs +2 -0
  23. package/src/runtime/agent/orchestrator/config-providers-merge.mjs +17 -11
  24. package/src/runtime/agent/orchestrator/config-storage.mjs +1 -0
  25. package/src/runtime/agent/orchestrator/config.mjs +19 -5
  26. package/src/runtime/agent/orchestrator/context/collect.mjs +2 -1
  27. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +11 -5
  28. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +74 -21
  29. package/src/runtime/agent/orchestrator/mcp/client-config.mjs +133 -15
  30. package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +55 -8
  31. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +86 -8
  32. package/src/runtime/agent/orchestrator/mcp/client.mjs +474 -164
  33. package/src/runtime/agent/orchestrator/mcp/lazy-output-validator.mjs +25 -0
  34. package/src/runtime/agent/orchestrator/mcp/plugin-placeholders.mjs +54 -0
  35. package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +4 -0
  36. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +108 -17
  37. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  38. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +3 -0
  39. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -1
  40. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -12
  41. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +2 -0
  42. package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +9 -0
  43. package/src/runtime/agent/orchestrator/providers/command-code-oauth-login.mjs +337 -0
  44. package/src/runtime/agent/orchestrator/providers/command-code-oauth-tokens.mjs +159 -0
  45. package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +444 -0
  46. package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +2 -0
  47. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  48. package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +2 -1
  49. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +7 -4
  50. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +21 -6
  51. package/src/runtime/agent/orchestrator/providers/effort-rejection.mjs +244 -0
  52. package/src/runtime/agent/orchestrator/providers/gemini.mjs +41 -28
  53. package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
  54. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +3 -0
  55. package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +7 -1
  56. package/src/runtime/agent/orchestrator/providers/mimo-oauth-profile.mjs +8 -40
  57. package/src/runtime/agent/orchestrator/providers/mimo-oauth-tokens.mjs +2 -0
  58. package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +51 -12
  59. package/src/runtime/agent/orchestrator/providers/model-cache.mjs +37 -2
  60. package/src/runtime/agent/orchestrator/providers/model-proxy-bridge.mjs +1080 -0
  61. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +82 -0
  62. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +556 -0
  63. package/src/runtime/agent/orchestrator/providers/model-proxy-provider.mjs +251 -0
  64. package/src/runtime/agent/orchestrator/providers/muse-code-oauth-tokens.mjs +2 -0
  65. package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +69 -102
  66. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +15 -0
  67. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +6 -5
  68. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -1
  69. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +6 -2
  70. package/src/runtime/agent/orchestrator/providers/openai-compat-options.mjs +11 -0
  71. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +2 -1
  72. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +15 -4
  73. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog-store.mjs +1 -1
  74. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +5 -2
  75. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +3 -0
  76. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +4 -4
  77. package/src/runtime/agent/orchestrator/providers/registry.mjs +47 -7
  78. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +10 -0
  79. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +36 -3
  80. package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +15 -11
  81. package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +15 -55
  82. package/src/runtime/agent/orchestrator/runtime-core/goal-control.mjs +15 -2
  83. package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +16 -1
  84. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +278 -84
  85. package/src/runtime/agent/orchestrator/runtime-core/skill-tool-loading.mjs +4 -1
  86. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-schema.mjs +21 -0
  87. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +54 -3
  88. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +10 -3
  89. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +10 -0
  90. package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +9 -5
  91. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +18 -8
  92. package/src/runtime/agent/orchestrator/session/catalog-db/index.mjs +284 -0
  93. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +268 -0
  94. package/src/runtime/agent/orchestrator/session/catalog-db/sqlite-gate.mjs +27 -0
  95. package/src/runtime/agent/orchestrator/session/catalog-db/worker.mjs +628 -0
  96. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +10 -3
  97. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +24 -13
  98. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +13 -5
  99. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +362 -0
  100. package/src/runtime/agent/orchestrator/session/compaction-archive.mjs +253 -0
  101. package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +184 -0
  102. package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +136 -0
  103. package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +128 -0
  104. package/src/runtime/agent/orchestrator/session/conversation-import/imported-session.mjs +127 -0
  105. package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +101 -0
  106. package/src/runtime/agent/orchestrator/session/explicit-skills.mjs +83 -5
  107. package/src/runtime/agent/orchestrator/session/imported-message.mjs +11 -0
  108. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +2 -1
  109. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +1 -1
  110. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/continuation.mjs +1 -1
  111. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/diagnostic.mjs +1 -7
  112. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/refusal.mjs +1 -1
  113. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +3 -2
  114. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +5 -3
  115. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +7 -4
  116. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +21 -10
  117. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +3 -0
  118. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +3 -0
  119. package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +12 -3
  120. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +15 -3
  121. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +22 -12
  122. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +9 -2
  123. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +9 -3
  124. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +5 -2
  125. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +37 -16
  126. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +4 -0
  127. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +17 -4
  128. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +90 -0
  129. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +22 -12
  130. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +32 -3
  131. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +27 -0
  132. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -1
  133. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +1 -0
  134. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +2 -0
  135. package/src/runtime/agent/orchestrator/session/manager.mjs +0 -1
  136. package/src/runtime/agent/orchestrator/session/message-ids.mjs +123 -0
  137. package/src/runtime/agent/orchestrator/session/message-ids.test-support.mjs +29 -0
  138. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +33 -22
  139. package/src/runtime/agent/orchestrator/session/store/listing.mjs +6 -2
  140. package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +3 -0
  141. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +6 -0
  142. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-finish.mjs +10 -3
  143. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +11 -0
  144. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +817 -0
  145. package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +36 -0
  146. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +23 -12
  147. package/src/runtime/agent/orchestrator/session/store-summary-reader-catalog-fixture.mjs +158 -0
  148. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +202 -374
  149. package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +20 -3
  150. package/src/runtime/agent/orchestrator/session/synthetic-user-envelope.mjs +3 -1
  151. package/src/runtime/agent/orchestrator/session/tool-batch/pre-dispatch.mjs +1 -1
  152. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +48 -9
  153. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +161 -0
  154. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +17 -1
  155. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +4 -0
  156. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +52 -2
  157. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +32 -22
  158. package/src/runtime/agent/orchestrator/tools/builtin/git-repo-rw-lock.mjs +10 -3
  159. package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +2 -2
  160. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +6 -6
  161. package/src/runtime/agent/orchestrator/tools/builtin/read-cache-policy.mjs +8 -0
  162. package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +203 -33
  163. package/src/runtime/agent/orchestrator/tools/builtin/read-single-fast-paths.mjs +32 -35
  164. package/src/runtime/agent/orchestrator/tools/builtin/read-single-guards.mjs +8 -4
  165. package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +11 -12
  166. package/src/runtime/agent/orchestrator/tools/builtin/read-single-stream.mjs +9 -6
  167. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +83 -68
  168. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +8 -3
  169. package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +20 -3
  170. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +16 -7
  171. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-dispatch.mjs +2 -1
  172. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-publication.mjs +137 -0
  173. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-snapshot.mjs +11 -10
  174. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +21 -11
  175. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +12 -1
  176. package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +13 -10
  177. package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +1 -1
  178. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +17 -12
  179. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +56 -13
  180. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +58 -0
  181. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/root-resolution.mjs +2 -2
  182. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +1 -1
  183. package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +25 -1
  184. package/src/runtime/agent/orchestrator/tools/patch/apply-patch/codex-batch.mjs +4 -4
  185. package/src/runtime/agent/orchestrator/tools/patch/apply-patch/route.mjs +2 -2
  186. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -1
  187. package/src/runtime/agent/orchestrator/tools/patch/read-redirects.mjs +34 -30
  188. package/src/runtime/agent/orchestrator/tools/patch/sequence/units.mjs +7 -7
  189. package/src/runtime/agent/orchestrator/tools/patch/sequence.mjs +1 -1
  190. package/src/runtime/agent/orchestrator/tools/patch/ui-diff.mjs +78 -5
  191. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +2 -0
  192. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -0
  193. package/src/runtime/attachments/store.d.mts +3 -0
  194. package/src/runtime/attachments/store.mjs +38 -0
  195. package/src/runtime/channels/lib/owned-runtime/owned-start.mjs +1 -1
  196. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +16 -4
  197. package/src/runtime/channels/lib/worker-main.mjs +17 -14
  198. package/src/runtime/computer-bridge/linux-readiness.mjs +0 -2
  199. package/src/runtime/effort-judge/judge-client.mjs +56 -18
  200. package/src/runtime/memory/lib/memory.mjs +1 -1
  201. package/src/runtime/memory/lib/session-ingest.mjs +2 -2
  202. package/src/runtime/shared/atomic-file.mjs +33 -6
  203. package/src/runtime/shared/best-effort-stderr.mjs +8 -0
  204. package/src/runtime/shared/code-graph-state.mjs +12 -0
  205. package/src/runtime/shared/defaults-separation.mjs +2 -0
  206. package/src/runtime/shared/extension-scopes.mjs +5 -3
  207. package/src/runtime/shared/file-lock.mjs +37 -11
  208. package/src/runtime/shared/git-runner-worker.mjs +3 -2
  209. package/src/runtime/shared/git-runner.mjs +4 -3
  210. package/src/runtime/shared/llm/anthropic-effort.mjs +14 -0
  211. package/src/runtime/shared/llm/default-context-window.mjs +6 -0
  212. package/src/runtime/shared/llm/model-catalog-projection.mjs +5 -4
  213. package/src/runtime/shared/llm/model-catalog.mjs +240 -130
  214. package/src/runtime/shared/llm/provider-catalog-cache.mjs +36 -28
  215. package/src/runtime/shared/llm/provider-catalog-registry.mjs +162 -0
  216. package/src/runtime/shared/llm/provider-seed-rosters.mjs +130 -0
  217. package/src/runtime/shared/model-proxy-mode.d.mts +3 -0
  218. package/src/runtime/shared/model-proxy-mode.mjs +19 -0
  219. package/src/runtime/shared/plugin-manifest.mjs +18 -9
  220. package/src/runtime/shared/pristine-execution-contract.json +9 -2
  221. package/src/runtime/shared/pristine-execution.mjs +28 -4
  222. package/src/runtime/shared/provider-accounts.mjs +125 -21
  223. package/src/runtime/shared/provider-api-key.mjs +33 -1
  224. package/src/runtime/shared/schedule-model-ref.mjs +11 -2
  225. package/src/runtime/shared/session-usage-totals.mjs +19 -0
  226. package/src/runtime/shared/sharp-electron-linux.mjs +25 -0
  227. package/src/runtime/shared/shell-display.mjs +9 -3
  228. package/src/runtime/shared/singleton-owner.mjs +36 -6
  229. package/src/runtime/shared/skill-mentions.d.mts +10 -0
  230. package/src/runtime/shared/skill-mentions.mjs +133 -0
  231. package/src/runtime/shared/skill-script-runtime.mjs +264 -0
  232. package/src/runtime/shared/statusline/statusline-segments.mjs +68 -14
  233. package/src/runtime/shared/tool-primitives.mjs +35 -2
  234. package/src/runtime/shared/tool-surface.mjs +7 -0
  235. package/src/runtime/shared/tool-work-units.mjs +14 -0
  236. package/src/runtime/shared/transcript-metadata.mjs +17 -1
  237. package/src/runtime/shared/turn-changes.mjs +791 -0
  238. package/src/runtime/shared/turn-snapshot-store.mjs +346 -12
  239. package/src/runtime/shared/turn-snapshot.mjs +690 -81
  240. package/src/runtime/shared/turn-worktree-snapshot.mjs +185 -15
  241. package/src/runtime/shared/worker-requests.mjs +15 -2
  242. package/src/runtime/shared/worktrees/cache-cleanups.mjs +52 -0
  243. package/src/runtime/shared/worktrees/contract.mjs +551 -0
  244. package/src/runtime/shared/worktrees/disk.mjs +57 -0
  245. package/src/runtime/shared/worktrees/git.mjs +247 -0
  246. package/src/runtime/shared/worktrees/include.mjs +480 -0
  247. package/src/runtime/shared/worktrees/lock.mjs +46 -0
  248. package/src/runtime/shared/worktrees/maintenance.mjs +96 -0
  249. package/src/runtime/shared/worktrees/manager.mjs +1215 -0
  250. package/src/runtime/shared/worktrees/merge.mjs +182 -0
  251. package/src/runtime/shared/worktrees/owner.mjs +46 -0
  252. package/src/runtime/shared/worktrees/paths.mjs +62 -0
  253. package/src/runtime/shared/worktrees/registry.mjs +110 -0
  254. package/src/runtime/shared/worktrees/settings.mjs +84 -0
  255. package/src/runtime/shared/worktrees/slug.mjs +62 -0
  256. package/src/runtime/shared/worktrees/test-support.mjs +96 -0
  257. package/src/runtime/visualize-bridge/client.mjs +181 -0
  258. package/src/runtime/visualize-bridge/tool-defs.mjs +97 -0
  259. package/src/session-runtime/automation-agents.mjs +8 -2
  260. package/src/session-runtime/boot/apis.mjs +90 -0
  261. package/src/session-runtime/boot/config.mjs +7 -2
  262. package/src/session-runtime/boot/facade.mjs +21 -0
  263. package/src/session-runtime/boot/infrastructure.mjs +30 -18
  264. package/src/session-runtime/boot/session.mjs +1 -0
  265. package/src/session-runtime/boot/tools.mjs +4 -0
  266. package/src/session-runtime/config-lifecycle.mjs +10 -1
  267. package/src/session-runtime/conversation-import/discovery.mjs +698 -0
  268. package/src/session-runtime/conversation-import/service.mjs +369 -0
  269. package/src/session-runtime/cwd-plugins/cwd-apply.mjs +21 -6
  270. package/src/session-runtime/cwd-plugins/plugin-status.mjs +47 -42
  271. package/src/session-runtime/global-extensions.mjs +2 -2
  272. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +27 -2
  273. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +32 -1
  274. package/src/session-runtime/internal-tool-executor.mjs +9 -1
  275. package/src/session-runtime/lifecycle/session-catalog.mjs +8 -1
  276. package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +5 -0
  277. package/src/session-runtime/lifecycle/session-switch/resume.mjs +21 -3
  278. package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +16 -0
  279. package/src/session-runtime/mcp-glue/connect.mjs +68 -16
  280. package/src/session-runtime/mcp-glue/plugin-servers.mjs +499 -0
  281. package/src/session-runtime/mcp-glue/server-catalog.mjs +122 -33
  282. package/src/session-runtime/mcp-glue/server-input.mjs +29 -0
  283. package/src/session-runtime/mcp-glue.mjs +22 -2
  284. package/src/session-runtime/model-route/route-persist.mjs +3 -0
  285. package/src/session-runtime/model-route/set-route.mjs +15 -2
  286. package/src/session-runtime/new-session-config.mjs +3 -2
  287. package/src/session-runtime/notification-bus.mjs +18 -3
  288. package/src/session-runtime/provider-auth/accounts.mjs +33 -15
  289. package/src/session-runtime/provider-models/row-hydration.mjs +2 -0
  290. package/src/session-runtime/provider-models/web-search-catalog.mjs +5 -1
  291. package/src/session-runtime/provider-readiness.mjs +16 -10
  292. package/src/session-runtime/resource-extension-sync.mjs +45 -7
  293. package/src/session-runtime/resource-mcp-api.mjs +121 -84
  294. package/src/session-runtime/resource-plugins-api.mjs +29 -41
  295. package/src/session-runtime/resource-scope-decorate.mjs +6 -8
  296. package/src/session-runtime/resource-skills-api.mjs +23 -2
  297. package/src/session-runtime/route-resolution.mjs +10 -1
  298. package/src/session-runtime/route-state.mjs +16 -2
  299. package/src/session-runtime/runtime-core.mjs +5 -0
  300. package/src/session-runtime/runtime-facade.mjs +8 -0
  301. package/src/session-runtime/runtime-review-api.mjs +9 -0
  302. package/src/session-runtime/schedule-session-run.mjs +1 -1
  303. package/src/session-runtime/services/agent-tool/helpers.mjs +5 -0
  304. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +3 -0
  305. package/src/session-runtime/services/agent-tool/job-views/job-lookup.mjs +1 -0
  306. package/src/session-runtime/services/agent-tool/job-views/spawn-meta.mjs +1 -0
  307. package/src/session-runtime/services/agent-tool/lead-worker-index.mjs +15 -0
  308. package/src/session-runtime/services/agent-tool/send-flow.mjs +1 -0
  309. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +10 -2
  310. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +19 -7
  311. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +3 -0
  312. package/src/session-runtime/services/agent-tool/tag-registry.mjs +2 -1
  313. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +5 -0
  314. package/src/session-runtime/services/agent-tool/worker-rows.mjs +1 -0
  315. package/src/session-runtime/services/agent-tool.mjs +1 -0
  316. package/src/session-runtime/services/channel-session-router.mjs +4 -1
  317. package/src/session-runtime/services/daemon-candidate.mjs +3 -2
  318. package/src/session-runtime/services/daemon-crash-capture/spawn-capture.mjs +55 -0
  319. package/src/session-runtime/services/hook-bus/command-handler.mjs +4 -1
  320. package/src/session-runtime/services/hook-bus/handlers.mjs +7 -3
  321. package/src/session-runtime/services/provider-admin.mjs +64 -18
  322. package/src/session-runtime/services/usage-dashboard-model.mjs +3 -0
  323. package/src/session-runtime/session-create/session-options.mjs +10 -1
  324. package/src/session-runtime/session-turn-api.mjs +1 -0
  325. package/src/session-runtime/settings-api.mjs +4 -2
  326. package/src/session-runtime/settings-builtin-tools-api.mjs +9 -2
  327. package/src/session-runtime/settings-worktree-api.mjs +17 -0
  328. package/src/session-runtime/setup-tool/executor.mjs +23 -5
  329. package/src/session-runtime/setup-tool/extended-actions.mjs +1 -0
  330. package/src/session-runtime/setup-tool/settings-contract.mjs +4 -1
  331. package/src/session-runtime/setup-tool/tool-defs.mjs +38 -4
  332. package/src/session-runtime/skills-api.mjs +7 -3
  333. package/src/session-runtime/turn/agent-ops.mjs +12 -1
  334. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +14 -3
  335. package/src/session-runtime/turn/turn-run/snapshot-tracker.mjs +20 -15
  336. package/src/session-runtime/turn/turn-run/turn-open.mjs +4 -5
  337. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +29 -1
  338. package/src/session-runtime/turn/turn-run/turn-settle.mjs +6 -1
  339. package/src/session-runtime/turn/turn-run.mjs +6 -7
  340. package/src/session-runtime/webhook-session-run.mjs +16 -4
  341. package/src/session-runtime/workflow-agents-api/agent-route.mjs +15 -3
  342. package/src/session-runtime/workspace/admission-gate.mjs +197 -0
  343. package/src/session-runtime/workspace/project-branches.mjs +136 -0
  344. package/src/session-runtime/workspace/transition-steps.mjs +364 -0
  345. package/src/session-runtime/workspace/transitions.mjs +651 -0
  346. package/src/session-runtime/workspace/workspace-state.mjs +112 -0
  347. package/src/skill-script-entry.mjs +36 -0
  348. package/src/standalone/channel-transport/http-routes.mjs +17 -0
  349. package/src/standalone/channel-transport.mjs +13 -1
  350. package/src/standalone/daemon-desktop-runtime.mjs +1 -0
  351. package/src/standalone/daemon-owner.mjs +372 -0
  352. package/src/standalone/daemon-stored-session-views.mjs +15 -4
  353. package/src/standalone/daemon.mjs +179 -8
  354. package/src/standalone/desktop-service-registry.mjs +20 -0
  355. package/src/standalone/local-session-runtime.mjs +11 -0
  356. package/src/standalone/session-client/attachment-calls.mjs +3 -1
  357. package/src/standalone/session-client/daemon-unavailable.mjs +62 -0
  358. package/src/standalone/session-client/lease-heartbeat.mjs +47 -0
  359. package/src/standalone/session-client.mjs +104 -13
  360. package/src/standalone/session-protocol.mjs +21 -0
  361. package/src/standalone/session-proxy.mjs +3 -0
  362. package/src/standalone/session-service/agent-tree/agent-child.mjs +4 -0
  363. package/src/standalone/session-service/agent-tree/agent-rehydrate.mjs +1 -0
  364. package/src/standalone/session-service/entries/lifecycle.mjs +6 -0
  365. package/src/standalone/session-service/projection/revisions.mjs +2 -0
  366. package/src/standalone/session-service/projection/transcript-window.mjs +149 -19
  367. package/src/standalone/session-service/projection/workspace.mjs +14 -0
  368. package/src/standalone/session-service/session-calls/session-action.mjs +17 -4
  369. package/src/standalone/session-transport/call-route.mjs +13 -0
  370. package/src/standalone/session-transport/client-registry/client-grace.mjs +45 -3
  371. package/src/standalone/session-transport/client-registry/registration-replays.mjs +1 -0
  372. package/src/standalone/session-transport/client-registry.mjs +20 -3
  373. package/src/standalone/session-transport/routes.mjs +79 -2
  374. package/src/standalone/session-transport/stack.mjs +2 -0
  375. package/src/standalone/session-transport.mjs +24 -1
  376. package/src/tui/App.jsx +9 -1
  377. package/src/tui/app/app-view/floating-panel.jsx +9 -1
  378. package/src/tui/app/app-view.jsx +6 -0
  379. package/src/tui/app/extension-pickers/plugins-pickers.mjs +35 -16
  380. package/src/tui/app/message-selector.mjs +2 -1
  381. package/src/tui/app/model-options.mjs +37 -5
  382. package/src/tui/app/model-picker/provider-models-list.mjs +2 -2
  383. package/src/tui/app/model-picker.mjs +4 -2
  384. package/src/tui/app/skill-mention-palette.mjs +88 -0
  385. package/src/tui/app/slash-palette.mjs +23 -7
  386. package/src/tui/app/transcript-row-estimate.mjs +11 -3
  387. package/src/tui/app/use-prompt-draft-flow.mjs +7 -2
  388. package/src/tui/app/use-prompt-state.mjs +5 -0
  389. package/src/tui/components/Message.jsx +22 -1
  390. package/src/tui/components/PromptInput.jsx +21 -3
  391. package/src/tui/components/SlashCommandPalette.jsx +12 -3
  392. package/src/tui/components/TranscriptItem.jsx +9 -1
  393. package/src/tui/components/prompt-input/key-handler.mjs +21 -8
  394. package/src/tui/components/user-message-skills.mjs +38 -0
  395. package/src/tui/dist/index.mjs +839 -207
  396. package/src/tui/index.jsx +10 -2
  397. package/src/tui/session/compaction-view.mjs +147 -0
  398. package/src/tui/session/context-state.mjs +6 -0
  399. package/src/tui/session/initial-state.mjs +3 -0
  400. package/src/tui/session/queue-helpers.mjs +7 -2
  401. package/src/tui/session/quota-resume.mjs +2 -1
  402. package/src/tui/session/session-api/commands.mjs +10 -2
  403. package/src/tui/session/session-api/controls.mjs +4 -0
  404. package/src/tui/session/session-api/extensions.mjs +4 -2
  405. package/src/tui/session/session-api/intake/rewind.mjs +2 -1
  406. package/src/tui/session/session-api/integrations.mjs +17 -0
  407. package/src/tui/session/session-api/lifecycle/dispose.mjs +6 -1
  408. package/src/tui/session/session-api/lifecycle/inheritance.mjs +2 -0
  409. package/src/tui/session/session-api/lifecycle/resume.mjs +3 -0
  410. package/src/tui/session/session-api/routes.mjs +10 -1
  411. package/src/tui/session/session-api/settings.mjs +9 -0
  412. package/src/tui/session/session-api/worktree.mjs +60 -0
  413. package/src/tui/session/session-api.mjs +2 -0
  414. package/src/tui/session/session-flow/auto-clear.mjs +31 -12
  415. package/src/tui/session/session-flow/drain.mjs +27 -3
  416. package/src/tui/session/session-flow/session-reset.mjs +4 -3
  417. package/src/tui/session/session-flow.mjs +2 -0
  418. package/src/tui/session/transcript-intake.mjs +14 -5
  419. package/src/tui/session/tui-steering-persist.mjs +25 -9
  420. package/src/tui/session/turn-stream.mjs +32 -4
  421. package/src/tui/session/turn-tool-cards.mjs +10 -2
  422. package/src/tui/session/turn.mjs +56 -10
  423. package/src/runtime/effort-judge/previous-human-request.mjs +0 -162
  424. package/src/runtime/effort-judge/verified-context-input.mjs +0 -10
@@ -6,7 +6,7 @@ import { tokenizeDirectArgv } from './shell-direct-exe.mjs';
6
6
  import { GIT_DIFF_EXIT_SIGNAL_FLAGS } from './bash-tool/benign-exit.mjs';
7
7
  import { withBuiltinPathLocks } from './path-locks.mjs';
8
8
  import { withAdvisoryLocks } from './advisory-lock.mjs';
9
- import { withGitRepoReadLock, withGitRepoWriteLock } from './git-repo-rw-lock.mjs';
9
+ import { gitRepoLockKey, withGitRepoReadLock, withGitRepoWriteLock } from './git-repo-rw-lock.mjs';
10
10
  import { invalidateBuiltinResultCache } from './cache-layers.mjs';
11
11
  import { drainCodeGraphCache } from '../code-graph-state.mjs';
12
12
  import { ensureNativeSpawnServer, tryNativeSpawn } from '../lib/native-spawn-client.mjs';
@@ -460,22 +460,22 @@ function nearestGitDir(startDir) {
460
460
 
461
461
  function cachedRepoRoot(key, plan) {
462
462
  if (!key) return null;
463
- const root = repoRootCache.get(key);
464
- if (!root) return null;
463
+ const hit = repoRootCache.get(key);
464
+ if (!hit) return null;
465
465
  const nearest = nearestGitDir(plan.cwd);
466
- if (!nearest || resolve(nearest) !== resolve(root)) {
466
+ if (!nearest || resolve(nearest) !== resolve(hit.root)) {
467
467
  repoRootCache.delete(key);
468
468
  return null;
469
469
  }
470
470
  repoRootCache.delete(key);
471
- repoRootCache.set(key, root);
472
- return root;
471
+ repoRootCache.set(key, hit);
472
+ return hit;
473
473
  }
474
474
 
475
- function rememberRepoRoot(key, root) {
476
- if (!key || !root) return;
475
+ function rememberRepoRoot(key, hit) {
476
+ if (!key || !hit?.root) return;
477
477
  repoRootCache.delete(key);
478
- repoRootCache.set(key, root);
478
+ repoRootCache.set(key, hit);
479
479
  while (repoRootCache.size > REPO_ROOT_CACHE_MAX) {
480
480
  const oldest = repoRootCache.keys().next().value;
481
481
  if (oldest === undefined) break;
@@ -483,15 +483,22 @@ function rememberRepoRoot(key, root) {
483
483
  }
484
484
  }
485
485
 
486
+ // `lockKey` is the repository's canonical common git dir: the main checkout
487
+ // and its linked worktrees share one object store and ref namespace, so their
488
+ // git calls serialize on one repository lock (managed worktree mutations take
489
+ // the same key).
486
490
  async function resolveRepo(plan, signal) {
487
491
  const key = repoRootCacheKey(plan);
488
492
  const hit = cachedRepoRoot(key, plan);
489
- if (hit) return { root: hit, probe: null };
490
- const result = await runGit(plan, ['rev-parse', '--show-toplevel'], { signal });
493
+ if (hit) return { root: hit.root, lockKey: hit.lockKey, probe: null };
494
+ const result = await runGit(plan, ['rev-parse', '--show-toplevel', '--path-format=absolute', '--git-common-dir'], {
495
+ signal,
496
+ });
491
497
  if (succeeded(result)) {
492
- const root = cleanText(result.stdout);
493
- rememberRepoRoot(key, root);
494
- return { root, probe: result };
498
+ const [root, commonDir] = cleanText(result.stdout).split('\n');
499
+ const resolved = { root, lockKey: gitRepoLockKey(commonDir || root) };
500
+ rememberRepoRoot(key, resolved);
501
+ return { ...resolved, probe: result };
495
502
  }
496
503
  // A bare repository (mirror clone, server-side repo) has no work tree, so
497
504
  // `--show-toplevel` refuses; that is still a repository. Answering
@@ -499,12 +506,13 @@ async function resolveRepo(plan, signal) {
499
506
  // to inspect objects it already had.
500
507
  if (/must be run in a work tree/i.test(String(result.stderr || ''))) {
501
508
  const bare = await runGit(plan, ['rev-parse', '--absolute-git-dir'], { signal });
502
- if (!succeeded(bare)) return { root: null, probe: bare };
509
+ if (!succeeded(bare)) return { root: null, lockKey: null, probe: bare };
503
510
  const root = cleanText(bare.stdout);
504
- rememberRepoRoot(key, root);
505
- return { root, probe: bare };
511
+ const resolved = { root, lockKey: gitRepoLockKey(root) };
512
+ rememberRepoRoot(key, resolved);
513
+ return { ...resolved, probe: bare };
506
514
  }
507
- return { root: null, probe: result };
515
+ return { root: null, lockKey: null, probe: result };
508
516
  }
509
517
 
510
518
  function localizeConfigPlan(plan) {
@@ -650,8 +658,9 @@ async function executeGitStage(input, workDir, options = {}) {
650
658
  const limit = outputLimit(input?.output_limit, 50);
651
659
  const signal = abortSignalOf(options);
652
660
  const repo = snapshot.repo;
661
+ const { lockKey } = await resolveRepo({ ...snapshot.plan, cwd: repo }, signal);
653
662
  return withGitRepoWriteLock(
654
- repo,
663
+ lockKey || repo,
655
664
  () =>
656
665
  withGitMutationLocks(repo, async () => {
657
666
  const current = await runStageableDiff(snapshot.plan, snapshot.argv, repo, signal);
@@ -722,7 +731,7 @@ async function executeSingleGitTool(input, workDir, options = {}) {
722
731
  // ("git rev-parse exited 128") for a `git status` in a plain directory hid
723
732
  // git's own "fatal: not a git repository" line behind a command the model
724
733
  // never issued — and re-ran the probe just to build that message.
725
- const { root: repo, probe } = await resolveRepo(plan, signal);
734
+ const { root: repo, lockKey, probe } = await resolveRepo(plan, signal);
726
735
  if (!repo) {
727
736
  const missing = probe ?? (await runGit(plan, ['rev-parse', '--show-toplevel'], { signal }));
728
737
  return commandResult(plan, missing, limit);
@@ -732,7 +741,7 @@ async function executeSingleGitTool(input, workDir, options = {}) {
732
741
  if (stageableRequest) prepared.argv.splice(1, 0, '--no-textconv', '--src-prefix=a/', '--dst-prefix=b/');
733
742
  if (isReadOnly(plan)) {
734
743
  return withGitRepoReadLock(
735
- repo,
744
+ lockKey,
736
745
  async () => {
737
746
  const result = stageableRequest
738
747
  ? await runStageableDiff(plan, prepared.argv, repo, signal)
@@ -754,7 +763,7 @@ async function executeSingleGitTool(input, workDir, options = {}) {
754
763
  );
755
764
  }
756
765
  return withGitRepoWriteLock(
757
- repo,
766
+ lockKey,
758
767
  () =>
759
768
  withGitMutationLocks(repo, async () => {
760
769
  const result = await runGit(plan, prepared.argv, { signal });
@@ -896,4 +905,5 @@ export const _gitCommandInternals = {
896
905
  localizeConfigPlan,
897
906
  parseCommand,
898
907
  prepare,
908
+ resolveRepo,
899
909
  };
@@ -1,10 +1,17 @@
1
- import { resolve } from 'node:path';
1
+ import { pathKey } from '../../../../shared/worktrees/paths.mjs';
2
2
 
3
3
  const states = new Map();
4
4
 
5
+ // Callers key by the canonical `git rev-parse --git-common-dir`, so the main
6
+ // checkout and every linked worktree of one repository share one lock. The key
7
+ // is the real path (8.3 names, symlinked parents) and case-insensitive on
8
+ // Windows, so two spellings of one directory never hold separate locks.
9
+ export function gitRepoLockKey(repo) {
10
+ return pathKey(String(repo || '.'));
11
+ }
12
+
5
13
  function keyFor(repo) {
6
- const key = resolve(String(repo || '.'));
7
- return process.platform === 'win32' ? key.toLowerCase() : key;
14
+ return gitRepoLockKey(repo);
8
15
  }
9
16
 
10
17
  function stateFor(key) {
@@ -8,7 +8,7 @@ import { normalizeOutputPath, resolveAgainstCwd } from '../path-utils.mjs';
8
8
  /** Report the requested path's result; suggestions never retarget the operation. */
9
9
  export async function readFamilyPathEnoentOrError(workDir, fullPath, inputPath, err) {
10
10
  const safeMsg = normalizeErrorMessage(err instanceof Error ? err.message : String(err));
11
- const hint = buildNotFoundHint(workDir, fullPath, 'List', err?.code);
11
+ const hint = await buildNotFoundHint(workDir, fullPath, 'List', err?.code);
12
12
  const tail = finalizeReadFamilyEnoentTail(hint, inputPath, err?.code);
13
13
  // Conclusive absence is the list's ANSWER, not a tool failure (matches
14
14
  // read's `[path absent]` and git's `repo:false` policy). Genuine
@@ -78,4 +78,4 @@ export async function countTextFileLines(fullPath, size) {
78
78
  } finally {
79
79
  await handle?.close().catch(() => {});
80
80
  }
81
- }
81
+ }
@@ -97,7 +97,7 @@ const BASENAME_SCAN_SKIP_DIRS = new Set([
97
97
  // directories scanned and wall-clock. `visitEntry` decides what counts as a
98
98
  // match and which directories to queue, and returns true to stop reading the
99
99
  // current directory (its own limit break).
100
- function scanByBasename(searchRoot, { limit, maxDirs, deadlineMs }, visitEntry) {
100
+ async function scanByBasename(searchRoot, { limit, maxDirs, deadlineMs }, visitEntry) {
101
101
  const matches = [];
102
102
  const queue = [searchRoot];
103
103
  const stopAt = Date.now() + Math.max(1, deadlineMs);
@@ -112,7 +112,7 @@ function scanByBasename(searchRoot, { limit, maxDirs, deadlineMs }, visitEntry)
112
112
  scanned++;
113
113
  let entries;
114
114
  try {
115
- entries = readdirSync(dir, { withFileTypes: true });
115
+ entries = await readdir(dir, { withFileTypes: true });
116
116
  } catch {
117
117
  continue;
118
118
  }
@@ -127,7 +127,7 @@ function scanByBasename(searchRoot, { limit, maxDirs, deadlineMs }, visitEntry)
127
127
  return matches;
128
128
  }
129
129
 
130
- export function findFileByBasename(
130
+ export async function findFileByBasename(
131
131
  searchRoot,
132
132
  fullPath,
133
133
  { limit = 3, maxDirs = 6000, deadlineMs = BASENAME_SCAN_DEADLINE_MS } = {}
@@ -136,7 +136,7 @@ export function findFileByBasename(
136
136
  if (typeof searchRoot !== 'string' || !searchRoot) return [];
137
137
  const target = basename(fullPath).toLowerCase();
138
138
  if (!target) return [];
139
- return scanByBasename(searchRoot, { limit, maxDirs, deadlineMs }, (dir, ent, matches, queue) => {
139
+ return await scanByBasename(searchRoot, { limit, maxDirs, deadlineMs }, (dir, ent, matches, queue) => {
140
140
  const name = ent.name;
141
141
  if (ent.isDirectory()) {
142
142
  if (name.startsWith('.') || BASENAME_SCAN_SKIP_DIRS.has(name)) return false;
@@ -205,7 +205,7 @@ export function findBySuffixStrip(searchRoot, fullPath, { maxIterations = 12 } =
205
205
 
206
206
  // Same as findFileByBasename but for directories sharing the missing path's
207
207
  // final segment name (e.g. guessed `src/shared` → the real `lib/shared`).
208
- export function findDirectoryByBasename(
208
+ export async function findDirectoryByBasename(
209
209
  searchRoot,
210
210
  fullPath,
211
211
  { limit = 3, maxDirs = 6000, deadlineMs = BASENAME_SCAN_DEADLINE_MS } = {}
@@ -214,7 +214,7 @@ export function findDirectoryByBasename(
214
214
  if (typeof searchRoot !== 'string' || !searchRoot) return [];
215
215
  const target = basename(fullPath).toLowerCase();
216
216
  if (!target) return [];
217
- return scanByBasename(searchRoot, { limit, maxDirs, deadlineMs }, (dir, ent, matches, queue) => {
217
+ return await scanByBasename(searchRoot, { limit, maxDirs, deadlineMs }, (dir, ent, matches, queue) => {
218
218
  const name = ent.name;
219
219
  if (!ent.isDirectory()) return false;
220
220
  if (name.toLowerCase() === target) {
@@ -0,0 +1,8 @@
1
+ // The bounded cache validator can certify the whole file only within this
2
+ // byte budget. Larger reads must not reuse metadata-only or prefix-only
3
+ // content/line-position evidence: equal stats can hide edits past the prefix.
4
+ export const READ_PREFIX_HASH_BYTES = 65536;
5
+
6
+ export function canCacheReadFile(stat) {
7
+ return Number.isFinite(stat?.size) && stat.size >= 0 && stat.size <= READ_PREFIX_HASH_BYTES;
8
+ }
@@ -1,6 +1,7 @@
1
1
  import { mkdir, open, readFile, readdir, stat, unlink } from 'node:fs/promises';
2
2
  import { statSync } from 'node:fs';
3
3
  import { fileVersion, sameFileVersion } from './file-version.mjs';
4
+ import { canCacheReadFile, READ_PREFIX_HASH_BYTES } from './read-cache-policy.mjs';
4
5
  import { hashText as sha256Hex } from './hash-utils.mjs';
5
6
  import { join, normalize, resolve } from 'node:path';
6
7
  import { getPluginData } from '../../config.mjs';
@@ -18,6 +19,15 @@ const READ_RANGE_INDEX_SWEEP_CONCURRENCY = 8;
18
19
  const READ_RANGE_INDEX_CACHE = new Map();
19
20
  const READ_RANGE_INDEX_DRAFTS = new WeakMap();
20
21
  const READ_RANGE_INDEX_PERSIST_PENDING = new Map();
22
+ const READ_RANGE_INDEX_INFLIGHT = new Map();
23
+ const READ_RANGE_INDEX_LAST_PERSISTED = new Map();
24
+ // key -> outstanding deletes whose unlink / cancelled write has not settled.
25
+ const READ_RANGE_INDEX_DELETING = new Map();
26
+ let readRangeIndexDeleteSeq = 0;
27
+ const READ_RANGE_INDEX_LAST_PERSISTED_MAX = 256;
28
+ const READ_RANGE_INDEX_WRITE_CONCURRENCY = 2;
29
+ const READ_RANGE_INDEX_WRITE_WAITERS = [];
30
+ let readRangeIndexWritesActive = 0;
21
31
  let readRangeIndexDiskSwept = false;
22
32
 
23
33
  let traceReadRangeIndex = () => {};
@@ -83,7 +93,7 @@ function readRangeIndexMatches(row, fullPath, version) {
83
93
  // `prefixHash` they persist into the anchor index.
84
94
  async function computePrefixHashForIndex(fullPath, st, handle = null, prefixBuffer = null) {
85
95
  try {
86
- const cap = Math.min(Number(st?.size) || 0, 65536);
96
+ const cap = Math.min(Number(st?.size) || 0, READ_PREFIX_HASH_BYTES);
87
97
  if (cap <= 0) return '';
88
98
  if (Buffer.isBuffer(prefixBuffer) && prefixBuffer.length === cap) {
89
99
  return sha256Hex(prefixBuffer);
@@ -98,7 +108,9 @@ async function computePrefixHashForIndex(fullPath, st, handle = null, prefixBuff
98
108
  if (!handle) {
99
109
  try {
100
110
  await fh.close();
101
- } catch {}
111
+ } catch {
112
+ /* close failure after a completed read is irrelevant */
113
+ }
102
114
  }
103
115
  }
104
116
  } catch {
@@ -122,14 +134,21 @@ async function loadReadRangeIndexFromDisk(fullPath, st, handle, prefixBuffer) {
122
134
  // No existsSync preflight: a missing file surfaces as an ENOENT from
123
135
  // readFile below, caught by the same try/catch — one FS pass, not two.
124
136
  if (!file || !st) return null;
137
+ // A delete wins over any disk content: while a cancelled write may still be
138
+ // landing the file is invisible, and a read that straddled a delete is void.
139
+ const key = canonicalCachePath(fullPath);
140
+ const deleteSeq = readRangeIndexDeleteSeq;
141
+ const deleted = () => READ_RANGE_INDEX_DELETING.has(key) || readRangeIndexDeleteSeq !== deleteSeq;
142
+ if (deleted()) return null;
125
143
  try {
126
144
  const row = JSON.parse(await readFile(file, 'utf-8'));
127
- if (!readRangeIndexMatches(row, fullPath, st)) return null;
145
+ if (deleted() || !readRangeIndexMatches(row, fullPath, st)) return null;
128
146
  // The prefix is an extra check, not proof that later byte offsets survived.
129
147
  if (typeof row.prefixHash === 'string' && row.prefixHash) {
130
148
  const cur = await computePrefixHashForIndex(fullPath, st, handle, prefixBuffer);
131
149
  if (!cur || cur !== row.prefixHash) return null;
132
150
  }
151
+ if (deleted()) return null;
133
152
  const anchors = new Map([[0, 0]]);
134
153
  for (const entry of row.anchors) {
135
154
  if (!Array.isArray(entry) || entry.length < 2) continue;
@@ -160,35 +179,133 @@ function currentReadRangeIndex(index) {
160
179
  return !!index?.published && READ_RANGE_INDEX_CACHE.get(canonicalCachePath(index.fullPath)) === index;
161
180
  }
162
181
 
163
- function persistReadRangeIndexSync(index) {
182
+ function rememberPersisted(key, text) {
183
+ READ_RANGE_INDEX_LAST_PERSISTED.delete(key);
184
+ READ_RANGE_INDEX_LAST_PERSISTED.set(key, text);
185
+ if (READ_RANGE_INDEX_LAST_PERSISTED.size > READ_RANGE_INDEX_LAST_PERSISTED_MAX) {
186
+ READ_RANGE_INDEX_LAST_PERSISTED.delete(READ_RANGE_INDEX_LAST_PERSISTED.keys().next().value);
187
+ }
188
+ }
189
+
190
+ // job = { index, row }: row is a pre-serialised evicted row (index no longer cached).
191
+ // Returns false only when a write was needed but failed (e.g. lock contended).
192
+ function persistReadRangeIndexSync(job) {
193
+ const { index, row: evictedRow } = job;
164
194
  const file = readRangeIndexFilePath(index?.fullPath);
165
- if (!file || !currentReadRangeIndex(index)) return;
195
+ if (!file || (!evictedRow && !currentReadRangeIndex(index))) return true;
166
196
  try {
167
- if (!sameFileVersion(index.fileVersion, statSync(index.fullPath))) return;
168
- const row = serialiseReadRangeIndex(index);
169
- if (row) writeJsonAtomicSync(file, row, { compact: true, lock: true, fsync: false });
170
- } catch {}
197
+ if (!sameFileVersion(evictedRow?.fileVersion || index.fileVersion, statSync(index.fullPath))) return true;
198
+ const row = evictedRow || serialiseReadRangeIndex(index);
199
+ if (!row) return true;
200
+ const key = canonicalCachePath(index.fullPath);
201
+ const text = JSON.stringify(row);
202
+ if (READ_RANGE_INDEX_LAST_PERSISTED.get(key) === text) return true;
203
+ writeJsonAtomicSync(file, row, { compact: true, lock: true, fsync: false });
204
+ rememberPersisted(key, text);
205
+ return true;
206
+ } catch {
207
+ return false;
208
+ }
209
+ }
210
+
211
+ function acquireReadRangeIndexWriteSlot() {
212
+ if (readRangeIndexWritesActive < READ_RANGE_INDEX_WRITE_CONCURRENCY) {
213
+ readRangeIndexWritesActive += 1;
214
+ return Promise.resolve();
215
+ }
216
+ return new Promise((resolve) => READ_RANGE_INDEX_WRITE_WAITERS.push(resolve));
217
+ }
218
+
219
+ function releaseReadRangeIndexWriteSlot() {
220
+ const next = READ_RANGE_INDEX_WRITE_WAITERS.shift();
221
+ if (next) next();
222
+ else readRangeIndexWritesActive -= 1;
171
223
  }
172
224
 
173
- async function persistReadRangeIndex(index, evictedRow = null) {
225
+ // One write attempt. `state.epoch` changes when the path is deleted: a write
226
+ // that straddles a delete removes what it just wrote instead of resurrecting it.
227
+ async function persistReadRangeIndexOnce(key, job, state) {
228
+ const { index, row: evictedRow } = job;
174
229
  const file = readRangeIndexFilePath(index?.fullPath);
175
230
  if (!file || (!evictedRow && !currentReadRangeIndex(index))) return;
231
+ const epoch = state.epoch;
232
+ // Admission precedes every stat/lock so waiting writers hold no file lock.
233
+ await acquireReadRangeIndexWriteSlot();
176
234
  try {
235
+ if (state.epoch !== epoch || (!evictedRow && !currentReadRangeIndex(index))) return;
177
236
  if (!sameFileVersion(evictedRow?.fileVersion || index.fileVersion, await stat(index.fullPath))) return;
178
237
  await mkdir(READ_RANGE_INDEX_DISK_DIR, { recursive: true });
179
- if (!evictedRow && !currentReadRangeIndex(index)) return;
238
+ if (state.epoch !== epoch || (!evictedRow && !currentReadRangeIndex(index))) return;
180
239
  const row = evictedRow || serialiseReadRangeIndex(index);
181
- if (row) await writeJsonAtomicAsync(file, row, { compact: true, lock: true, fsync: false });
182
- } catch {}
240
+ if (!row) return;
241
+ const text = JSON.stringify(row);
242
+ if (READ_RANGE_INDEX_LAST_PERSISTED.get(key) === text) return;
243
+ readRangeIndexPersistStats.concurrent += 1;
244
+ readRangeIndexPersistStats.maxConcurrent = Math.max(
245
+ readRangeIndexPersistStats.maxConcurrent,
246
+ readRangeIndexPersistStats.concurrent
247
+ );
248
+ try {
249
+ await writeJsonAtomicAsync(file, row, { compact: true, lock: true, fsync: false });
250
+ readRangeIndexPersistStats.writes += 1;
251
+ } finally {
252
+ readRangeIndexPersistStats.concurrent -= 1;
253
+ }
254
+ if (state.epoch !== epoch) {
255
+ await unlink(file).catch(() => {});
256
+ return;
257
+ }
258
+ rememberPersisted(key, text);
259
+ } catch {
260
+ /* persisting the index is best-effort */
261
+ } finally {
262
+ releaseReadRangeIndexWriteSlot();
263
+ }
264
+ }
265
+
266
+ // Single-flight per cache file: a request during an in-flight write only
267
+ // replaces the pending job; one follow-up write persists the latest state.
268
+ function requestReadRangeIndexPersist(key, job) {
269
+ const state = READ_RANGE_INDEX_INFLIGHT.get(key);
270
+ if (state) {
271
+ state.next = job;
272
+ return;
273
+ }
274
+ const fresh = { epoch: 0, next: null, cancelWaiters: [] };
275
+ READ_RANGE_INDEX_INFLIGHT.set(key, fresh);
276
+ const settleCancelled = () => {
277
+ for (const resolve of fresh.cancelWaiters.splice(0)) resolve();
278
+ };
279
+ void (async () => {
280
+ try {
281
+ let cur = job;
282
+ while (cur) {
283
+ await persistReadRangeIndexOnce(key, cur, fresh);
284
+ // A cancelled write has finished (and cleaned up) once Once returns.
285
+ settleCancelled();
286
+ cur = fresh.next;
287
+ fresh.next = null;
288
+ }
289
+ } finally {
290
+ settleCancelled();
291
+ READ_RANGE_INDEX_INFLIGHT.delete(key);
292
+ }
293
+ })();
183
294
  }
184
295
 
185
296
  export function scheduleReadRangeIndexPersist(index) {
186
297
  if (!READ_RANGE_INDEX_DISK_DIR || !currentReadRangeIndex(index)) return;
187
298
  const key = canonicalCachePath(index.fullPath);
299
+ const inflight = READ_RANGE_INDEX_INFLIGHT.get(key);
300
+ if (inflight) {
301
+ inflight.next = { index, row: null };
302
+ return;
303
+ }
188
304
  if (READ_RANGE_INDEX_PERSIST_PENDING.has(key)) return;
189
305
  const t = setTimeout(() => {
190
306
  READ_RANGE_INDEX_PERSIST_PENDING.delete(key);
191
- void persistReadRangeIndex(index);
307
+ const latest = READ_RANGE_INDEX_CACHE.get(key);
308
+ if (latest) requestReadRangeIndexPersist(key, { index: latest, row: null });
192
309
  }, READ_RANGE_INDEX_PERSIST_DEBOUNCE_MS);
193
310
  t.unref();
194
311
  READ_RANGE_INDEX_PERSIST_PENDING.set(key, t);
@@ -198,10 +315,19 @@ export function flushReadRangeIndexesSync() {
198
315
  for (const [key, timer] of READ_RANGE_INDEX_PERSIST_PENDING) {
199
316
  try {
200
317
  clearTimeout(timer);
201
- } catch {}
318
+ } catch {
319
+ /* a timer that cannot be cleared must not block the flush */
320
+ }
202
321
  READ_RANGE_INDEX_PERSIST_PENDING.delete(key);
203
322
  const index = READ_RANGE_INDEX_CACHE.get(key);
204
- if (index) persistReadRangeIndexSync(index);
323
+ if (index) persistReadRangeIndexSync({ index, row: null });
324
+ }
325
+ // Writes queued behind an in-flight one are still pending state at exit.
326
+ for (const state of READ_RANGE_INDEX_INFLIGHT.values()) {
327
+ // Keep the job unless it was durably handled: when the async writer holds
328
+ // the lock, its follow-up still persists it if the process keeps running.
329
+ const job = state.next;
330
+ if (job && persistReadRangeIndexSync(job) && state.next === job) state.next = null;
205
331
  }
206
332
  }
207
333
 
@@ -212,11 +338,32 @@ function dropReadRangeIndexSlot(key) {
212
338
  }
213
339
 
214
340
  export function deleteReadRangeIndexForPath(fullPath) {
215
- dropReadRangeIndexSlot(canonicalCachePath(fullPath));
341
+ const key = canonicalCachePath(fullPath);
342
+ dropReadRangeIndexSlot(key);
343
+ READ_RANGE_INDEX_LAST_PERSISTED.delete(key);
344
+ readRangeIndexDeleteSeq += 1;
345
+ // Disk loads for this path are void until the unlink and any cancelled
346
+ // in-flight write (which removes its own output) have settled.
347
+ READ_RANGE_INDEX_DELETING.set(key, (READ_RANGE_INDEX_DELETING.get(key) || 0) + 1);
348
+ const inflight = READ_RANGE_INDEX_INFLIGHT.get(key);
349
+ let cancelled = Promise.resolve();
350
+ if (inflight) {
351
+ inflight.epoch += 1;
352
+ inflight.next = null;
353
+ cancelled = new Promise((resolve) => inflight.cancelWaiters.push(resolve));
354
+ }
216
355
  const file = readRangeIndexFilePath(fullPath);
217
- if (file) void unlink(file).catch(() => {});
356
+ const unlinked = file ? unlink(file).catch(() => {}) : Promise.resolve();
357
+ void Promise.all([unlinked, cancelled]).then(() => {
358
+ const left = (READ_RANGE_INDEX_DELETING.get(key) || 1) - 1;
359
+ if (left > 0) READ_RANGE_INDEX_DELETING.set(key, left);
360
+ else READ_RANGE_INDEX_DELETING.delete(key);
361
+ });
218
362
  }
219
363
 
364
+ // Test observability for write coalescing and the concurrency cap.
365
+ export const readRangeIndexPersistStats = { writes: 0, concurrent: 0, maxConcurrent: 0 };
366
+
220
367
  export async function sweepStaleReadRangeIndexes(directory = READ_RANGE_INDEX_DISK_DIR) {
221
368
  if (!directory) return;
222
369
  let entries;
@@ -234,8 +381,14 @@ export async function sweepStaleReadRangeIndexes(directory = READ_RANGE_INDEX_DI
234
381
  const full = join(directory, name);
235
382
  try {
236
383
  const st = await stat(full);
237
- if (now - st.mtimeMs > READ_RANGE_INDEX_DISK_STALE_MS) await unlink(full);
238
- } catch {}
384
+ if (now - st.mtimeMs > READ_RANGE_INDEX_DISK_STALE_MS) {
385
+ await unlink(full);
386
+ // A removed file invalidates the unchanged-content skip.
387
+ READ_RANGE_INDEX_LAST_PERSISTED.clear();
388
+ }
389
+ } catch {
390
+ /* best-effort sweep: a file that vanished or cannot be removed is skipped */
391
+ }
239
392
  }
240
393
  }
241
394
  await Promise.all(Array.from({ length: Math.min(READ_RANGE_INDEX_SWEEP_CONCURRENCY, entries.length) }, worker));
@@ -254,7 +407,7 @@ function refreshReadRangeIndexSlot(key, index) {
254
407
  // their slot immediately. Disk IO must not delay the foreground lookup.
255
408
  const row = serialiseReadRangeIndex(evicted);
256
409
  dropReadRangeIndexSlot(oldest);
257
- if (row) void persistReadRangeIndex(evicted, row);
410
+ if (row) requestReadRangeIndexPersist(oldest, { index: evicted, row });
258
411
  }
259
412
  return index;
260
413
  }
@@ -269,6 +422,8 @@ function freshReadRangeIndex(fullPath, version = null) {
269
422
  export async function getReadRangeIndex(fullPath, st, handle = null, prefixBuffer = null) {
270
423
  const key = canonicalCachePath(fullPath);
271
424
  let slot = READ_RANGE_INDEX_CACHE.get(key);
425
+ // Reserve ownership before awaiting the real stat even when the hint is
426
+ // large: a now-small file must not renew a lookup invalidated during IO.
272
427
  if (!slot) slot = refreshReadRangeIndexSlot(key, freshReadRangeIndex(fullPath));
273
428
  const hint = fileVersion(st);
274
429
  const readStat = () => (handle ? handle.stat() : stat(fullPath));
@@ -280,8 +435,18 @@ export async function getReadRangeIndex(fullPath, st, handle = null, prefixBuffe
280
435
  } catch {
281
436
  return null;
282
437
  }
283
- if (!version || READ_RANGE_INDEX_CACHE.get(key) !== slot) return null;
438
+ if (!version) return null;
284
439
  version = Object.freeze(version);
440
+ // Metadata and a bounded prefix cannot certify suffix line positions.
441
+ // Large scans own only this call's handle identity and byte-zero draft:
442
+ // never load, hash-validate, or publish cross-read anchors/counts.
443
+ if (!canCacheReadFile(version)) {
444
+ if (slot && READ_RANGE_INDEX_CACHE.get(key) === slot) dropReadRangeIndexSlot(key);
445
+ const draft = freshReadRangeIndex(fullPath, version);
446
+ READ_RANGE_INDEX_DRAFTS.set(draft, { slot: null, version, fullPath });
447
+ return draft;
448
+ }
449
+ if (READ_RANGE_INDEX_CACHE.get(key) !== slot) return null;
285
450
  if (!slot.fileVersion) slot.fileVersion = version;
286
451
  if (!sameFileVersion(slot.fileVersion, version)) {
287
452
  dropReadRangeIndexSlot(key);
@@ -314,27 +479,32 @@ export async function getReadRangeIndex(fullPath, st, handle = null, prefixBuffe
314
479
  anchors: new Map(source.anchors),
315
480
  ...(Number.isFinite(source.totalLines) ? { totalLines: source.totalLines } : {}),
316
481
  };
317
- READ_RANGE_INDEX_DRAFTS.set(draft, { slot, version });
482
+ READ_RANGE_INDEX_DRAFTS.set(draft, { slot, version, fullPath });
318
483
  return draft;
319
484
  }
320
485
 
321
- // Successful readers publish once, before closing their handle. A failed,
322
- // cancelled or detached scan keeps its observation private. Post-stat failures
323
- // are cache misses, not reasons to retry the operation or relabel its anchors.
324
- export async function publishReadRangeIndex(index, handle, signal = null, beforePublish = null) {
486
+ // Successful small-file readers publish once, before closing their handle.
487
+ // Large or detached scans remain private, but terminal validation can still
488
+ // certify their producer identity via onValidated. Post-stat failures never
489
+ // authorize relabelling bytes with a later version.
490
+ export async function publishReadRangeIndex(index, handle, signal = null, beforePublish = null, onValidated = null) {
325
491
  const owned = READ_RANGE_INDEX_DRAFTS.get(index);
326
492
  if (!owned) return false;
327
493
  READ_RANGE_INDEX_DRAFTS.delete(index);
328
- const { slot, version } = owned;
329
- const key = canonicalCachePath(slot.fullPath);
330
- if (signal?.aborted || READ_RANGE_INDEX_CACHE.get(key) !== slot) return false;
494
+ const { slot, version, fullPath } = owned;
495
+ const key = canonicalCachePath(fullPath);
496
+ if (signal?.aborted) return false;
497
+ // Detached cache-only publishers must not wait on validation IO. A streamed
498
+ // result requesting producer certification still owes its terminal stats.
499
+ if (slot && READ_RANGE_INDEX_CACHE.get(key) !== slot && !onValidated) return false;
331
500
  try {
332
- if (!sameFileVersion(version, await handle.stat()) || !sameFileVersion(version, await stat(slot.fullPath)))
333
- return false;
501
+ if (!sameFileVersion(version, await handle.stat()) || !sameFileVersion(version, await stat(fullPath))) return false;
334
502
  } catch {
335
503
  return false;
336
504
  }
337
- if (signal?.aborted || READ_RANGE_INDEX_CACHE.get(key) !== slot) return false;
505
+ if (signal?.aborted) return false;
506
+ onValidated?.(fileVersion(version));
507
+ if (!slot || READ_RANGE_INDEX_CACHE.get(key) !== slot) return false;
338
508
  if (slot.prefixHash && index.prefixHash && slot.prefixHash !== index.prefixHash) return false;
339
509
  // The reader's synchronous deadline/cancellation decision belongs after all
340
510
  // awaited validation and outside its best-effort stat catch. Once committed,