create-walle 0.10.2 → 0.10.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 (488) hide show
  1. package/README.md +3 -1
  2. package/lib/release-schema.json +2 -2
  3. package/lib/runtime-closure-policy.json +8 -7
  4. package/package.json +1 -1
  5. package/template/coding-task-manager/api-prompts.js +45 -7
  6. package/template/coding-task-manager/bin/claude-hook.js +55 -1
  7. package/template/coding-task-manager/bin/ctm-relay-client.js +35 -12
  8. package/template/coding-task-manager/bin/land.js +2069 -0
  9. package/template/coding-task-manager/bin/memory-census.js +4 -2
  10. package/template/coding-task-manager/bin/memory-sync-verify.js +151 -0
  11. package/template/coding-task-manager/bin/probe-refusal-scope.js +530 -0
  12. package/template/coding-task-manager/db.js +156 -32
  13. package/template/coding-task-manager/lib/acp/acp-client.js +383 -0
  14. package/template/coding-task-manager/lib/acp/dsh-context-truth.js +176 -0
  15. package/template/coding-task-manager/lib/acp/dsh-launch-model.js +302 -0
  16. package/template/coding-task-manager/lib/acp/dsh-mcp-servers.js +194 -0
  17. package/template/coding-task-manager/lib/acp/dsh-prompt-lane.js +616 -0
  18. package/template/coding-task-manager/lib/acp/dsh-route-patch.js +308 -0
  19. package/template/coding-task-manager/lib/acp/dsh-session-runtime.js +522 -0
  20. package/template/coding-task-manager/lib/acp/dsh-transcript.js +395 -0
  21. package/template/coding-task-manager/lib/acp/dsh-usage-log.js +162 -0
  22. package/template/coding-task-manager/lib/acp/ndjson-stdio-transport.js +225 -0
  23. package/template/coding-task-manager/lib/adopted-host-redial.js +47 -11
  24. package/template/coding-task-manager/lib/adopted-launch-environment.js +123 -0
  25. package/template/coding-task-manager/lib/agent-auto-update-runtime.js +0 -3
  26. package/template/coding-task-manager/lib/agent-auto-update.js +26 -10
  27. package/template/coding-task-manager/lib/agent-bundle/build.js +657 -0
  28. package/template/coding-task-manager/lib/agent-bundle/codex-install.js +558 -0
  29. package/template/coding-task-manager/lib/agent-bundle/drift.js +120 -0
  30. package/template/coding-task-manager/lib/agent-bundle/gc.js +522 -0
  31. package/template/coding-task-manager/lib/agent-bundle/inventory.js +346 -0
  32. package/template/coding-task-manager/lib/agent-bundle/push.js +717 -0
  33. package/template/coding-task-manager/lib/agent-bundle/skill-transfer.js +724 -0
  34. package/template/coding-task-manager/lib/agent-bundle/spawn.js +119 -0
  35. package/template/coding-task-manager/lib/agent-bundle/store.js +568 -0
  36. package/template/coding-task-manager/lib/agent-capabilities.js +142 -12
  37. package/template/coding-task-manager/lib/agent-cli-cache.js +30 -5
  38. package/template/coding-task-manager/lib/agent-definitions.js +19 -0
  39. package/template/coding-task-manager/lib/agent-hooks-codex-toml.js +28 -16
  40. package/template/coding-task-manager/lib/agent-hooks-codex-workspace.js +9 -1
  41. package/template/coding-task-manager/lib/agent-hooks-installer.js +23 -8
  42. package/template/coding-task-manager/lib/agent-hooks-prune.js +37 -11
  43. package/template/coding-task-manager/lib/agent-hooks-shim.js +105 -0
  44. package/template/coding-task-manager/lib/agent-presets.js +84 -1
  45. package/template/coding-task-manager/lib/agent-upgrade-jobs.js +12 -1
  46. package/template/coding-task-manager/lib/agent-version-service.js +74 -11
  47. package/template/coding-task-manager/lib/all-sessions-query.js +6 -2
  48. package/template/coding-task-manager/lib/api/agent-bundles.js +436 -0
  49. package/template/coding-task-manager/lib/api/backup-retention.js +83 -0
  50. package/template/coding-task-manager/lib/api/conversation-sync-routes.js +124 -0
  51. package/template/coding-task-manager/lib/api/diagnostics.js +549 -69
  52. package/template/coding-task-manager/lib/api/fleet.js +118 -0
  53. package/template/coding-task-manager/lib/api/hub-bridge.js +243 -0
  54. package/template/coding-task-manager/lib/api/ingress.js +9 -1
  55. package/template/coding-task-manager/lib/api/landing.js +403 -0
  56. package/template/coding-task-manager/lib/api/machines.js +691 -2
  57. package/template/coding-task-manager/lib/api/memory-sync-routes.js +335 -0
  58. package/template/coding-task-manager/lib/api/misc-ops.js +8 -3
  59. package/template/coding-task-manager/lib/api/remote-sessions.js +1326 -0
  60. package/template/coding-task-manager/lib/api/session-attach.js +623 -0
  61. package/template/coding-task-manager/lib/api/session-messages.js +1384 -0
  62. package/template/coding-task-manager/lib/api/sessions.js +77 -51
  63. package/template/coding-task-manager/lib/api/setup-network.js +544 -16
  64. package/template/coding-task-manager/lib/api/setup-wizard-handlers.js +884 -0
  65. package/template/coding-task-manager/lib/api/setup.js +829 -10
  66. package/template/coding-task-manager/lib/api/walle-bridge.js +9 -1
  67. package/template/coding-task-manager/lib/attachment-reconciler-wiring.js +5 -2
  68. package/template/coding-task-manager/lib/auth-rate-limit.js +23 -0
  69. package/template/coding-task-manager/lib/auth-rules.js +292 -21
  70. package/template/coding-task-manager/lib/background-stop.js +369 -0
  71. package/template/coding-task-manager/lib/best-of-attempts.js +4 -1
  72. package/template/coding-task-manager/lib/best-of-launch-plan.js +52 -0
  73. package/template/coding-task-manager/lib/box-keep-awake.js +89 -0
  74. package/template/coding-task-manager/lib/broadcast-reason-stats.js +152 -0
  75. package/template/coding-task-manager/lib/broadcast-serialize.js +5 -0
  76. package/template/coding-task-manager/lib/build-info.js +4 -2
  77. package/template/coding-task-manager/lib/claude-model-truth.js +13 -2
  78. package/template/coding-task-manager/lib/claude-transcript-terminal-renderer.js +17 -13
  79. package/template/coding-task-manager/lib/client-diag-handler.js +64 -2
  80. package/template/coding-task-manager/lib/codex-app-server-client.js +14 -16
  81. package/template/coding-task-manager/lib/codex-configuration-history.js +55 -0
  82. package/template/coding-task-manager/lib/codex-harness-qualification.js +43 -20
  83. package/template/coding-task-manager/lib/codex-rollout-history.js +743 -0
  84. package/template/coding-task-manager/lib/coding-agent-models.js +27 -12
  85. package/template/coding-task-manager/lib/coding-cli-skill-installer.js +44 -14
  86. package/template/coding-task-manager/lib/coding-session-model-adapters.js +43 -2
  87. package/template/coding-task-manager/lib/coding-session-model-owner.js +1 -1
  88. package/template/coding-task-manager/lib/coding-session-model-public.js +7 -1
  89. package/template/coding-task-manager/lib/coding-session-model-runtime.js +23 -4
  90. package/template/coding-task-manager/lib/coding-session-model-scan.js +31 -0
  91. package/template/coding-task-manager/lib/coding-session-model-state.js +7 -0
  92. package/template/coding-task-manager/lib/conversation-freshness-diagnostics.js +280 -0
  93. package/template/coding-task-manager/lib/conversation-item-clock.js +261 -0
  94. package/template/coding-task-manager/lib/conversation-source-registry.js +5 -0
  95. package/template/coding-task-manager/lib/conversation-store-v2.js +44 -0
  96. package/template/coding-task-manager/lib/conversation-sync.js +355 -0
  97. package/template/coding-task-manager/lib/create/resolve-launch-params.js +96 -12
  98. package/template/coding-task-manager/lib/create/worker-role-profile.js +4 -4
  99. package/template/coding-task-manager/lib/create-intent-lease.js +51 -0
  100. package/template/coding-task-manager/lib/ctm-session-context-api.js +97 -6
  101. package/template/coding-task-manager/lib/db-domain-tables.js +1 -0
  102. package/template/coding-task-manager/lib/db-domains.js +1 -0
  103. package/template/coding-task-manager/lib/db-owner-dispatch-probe.js +32 -0
  104. package/template/coding-task-manager/lib/db-owner-request-counts.js +178 -0
  105. package/template/coding-task-manager/lib/db-owner-worker-client.js +68 -14
  106. package/template/coding-task-manager/lib/db-schema-source-files.js +5 -0
  107. package/template/coding-task-manager/lib/deferred-restore-attempt.js +6 -2
  108. package/template/coding-task-manager/lib/feature-manifest.js +4 -0
  109. package/template/coding-task-manager/lib/fleet-conversation-route.js +97 -0
  110. package/template/coding-task-manager/lib/fleet-fanout.js +78 -0
  111. package/template/coding-task-manager/lib/fleet-hub.js +762 -0
  112. package/template/coding-task-manager/lib/fleet-origin-follower.js +252 -0
  113. package/template/coding-task-manager/lib/fleet-resolver-effects.js +147 -0
  114. package/template/coding-task-manager/lib/fleet-resolver-tick.js +148 -0
  115. package/template/coding-task-manager/lib/fleet-resolver.js +454 -0
  116. package/template/coding-task-manager/lib/fleet-session-list-routes.js +114 -0
  117. package/template/coding-task-manager/lib/fleet-session-lists.js +672 -0
  118. package/template/coding-task-manager/lib/fleet-update.js +798 -0
  119. package/template/coding-task-manager/lib/fs-watcher-root-watch.js +141 -0
  120. package/template/coding-task-manager/lib/fs-watcher.js +58 -42
  121. package/template/coding-task-manager/lib/harness-history-transition.js +50 -9
  122. package/template/coding-task-manager/lib/headless-term-service.js +84 -8
  123. package/template/coding-task-manager/lib/hermetic-agent-launch-guard.js +10 -0
  124. package/template/coding-task-manager/lib/history-transition-read-op.js +175 -8
  125. package/template/coding-task-manager/lib/host-cpu-categories.js +310 -0
  126. package/template/coding-task-manager/lib/host-fseventsd-sampler.js +399 -0
  127. package/template/coding-task-manager/lib/host-handoff.js +16 -19
  128. package/template/coding-task-manager/lib/host-stats.js +150 -3
  129. package/template/coding-task-manager/lib/hosted-sync-worker.js +43 -320
  130. package/template/coding-task-manager/lib/hub-bridge-access.js +798 -0
  131. package/template/coding-task-manager/lib/hub-bridge-box.js +409 -0
  132. package/template/coding-task-manager/lib/hub-bridge-mcp.js +380 -0
  133. package/template/coding-task-manager/lib/hub-bridge-mesh.js +313 -0
  134. package/template/coding-task-manager/lib/hub-bridge-ops.js +721 -0
  135. package/template/coding-task-manager/lib/hub-bridge.js +398 -0
  136. package/template/coding-task-manager/lib/idle-notify.js +491 -0
  137. package/template/coding-task-manager/lib/instance-id-ownership-manifest.js +8 -0
  138. package/template/coding-task-manager/lib/instance-manifest.js +5 -1
  139. package/template/coding-task-manager/lib/instance-sync-status.js +18 -8
  140. package/template/coding-task-manager/lib/landing/agent-rule.txt +1 -0
  141. package/template/coding-task-manager/lib/landing/batch-runner.js +366 -0
  142. package/template/coding-task-manager/lib/landing/config.js +536 -0
  143. package/template/coding-task-manager/lib/landing/ctm-view.js +256 -0
  144. package/template/coding-task-manager/lib/landing/gate.js +360 -0
  145. package/template/coding-task-manager/lib/landing/git.js +344 -0
  146. package/template/coding-task-manager/lib/landing/lease.js +247 -0
  147. package/template/coding-task-manager/lib/landing/merge-delta.js +380 -0
  148. package/template/coding-task-manager/lib/landing/proof.js +223 -0
  149. package/template/coding-task-manager/lib/landing/queue.js +998 -0
  150. package/template/coding-task-manager/lib/landing/radar.js +489 -0
  151. package/template/coding-task-manager/lib/landing/resolve-cli.js +424 -0
  152. package/template/coding-task-manager/lib/landing/state.js +263 -0
  153. package/template/coding-task-manager/lib/landing/stop.js +49 -0
  154. package/template/coding-task-manager/lib/landing/sync-cli.js +475 -0
  155. package/template/coding-task-manager/lib/landing/sync.js +456 -0
  156. package/template/coding-task-manager/lib/landing/train.js +885 -0
  157. package/template/coding-task-manager/lib/landing/window.js +57 -0
  158. package/template/coding-task-manager/lib/launch-environment.js +121 -0
  159. package/template/coding-task-manager/lib/launch-model-override.js +36 -10
  160. package/template/coding-task-manager/lib/launch-plan-pool.js +2 -1
  161. package/template/coding-task-manager/lib/launch-plan-warm-cache.js +6 -0
  162. package/template/coding-task-manager/lib/lead-model-observer.js +57 -1
  163. package/template/coding-task-manager/lib/lead-model-options.js +160 -3
  164. package/template/coding-task-manager/lib/lead-model-restore-reconciliation.js +106 -24
  165. package/template/coding-task-manager/lib/lead-model-switch-runtime.js +352 -68
  166. package/template/coding-task-manager/lib/lead-model-switch.js +114 -10
  167. package/template/coding-task-manager/lib/lead-model-transition-runtime.js +14 -2
  168. package/template/coding-task-manager/lib/lead-model-transition-store.js +24 -6
  169. package/template/coding-task-manager/lib/lead-route-preflight.js +179 -0
  170. package/template/coding-task-manager/lib/lead-switch-history-mode.js +110 -0
  171. package/template/coding-task-manager/lib/lead-wire-agent.js +7 -0
  172. package/template/coding-task-manager/lib/lifecycle-wake-experiment.js +9 -2
  173. package/template/coding-task-manager/lib/live-defer-announce.js +45 -0
  174. package/template/coding-task-manager/lib/machine-registry.js +750 -5
  175. package/template/coding-task-manager/lib/macos-capabilities.js +32 -15
  176. package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +433 -53
  177. package/template/coding-task-manager/lib/mainloop-observability.js +14 -9
  178. package/template/coding-task-manager/lib/media-draft-owner.js +16 -3
  179. package/template/coding-task-manager/lib/memory-join.js +337 -0
  180. package/template/coding-task-manager/lib/memory-sync-verify.js +393 -0
  181. package/template/coding-task-manager/lib/memory-sync.js +565 -0
  182. package/template/coding-task-manager/lib/migrations/v57-delegate-pref-mirror-opencode.js +68 -0
  183. package/template/coding-task-manager/lib/migrations/v58-subagent-launch-native-subagents.js +42 -0
  184. package/template/coding-task-manager/lib/migrations/v59-coding-session-model-state-dsh.js +73 -0
  185. package/template/coding-task-manager/lib/migrations/v60-delegate-pref-mirror-dsh.js +78 -0
  186. package/template/coding-task-manager/lib/migrations/v61-conversation-item-clock.js +71 -0
  187. package/template/coding-task-manager/lib/model-availability.js +727 -0
  188. package/template/coding-task-manager/lib/model-execution-recipe-metadata.js +4 -6
  189. package/template/coding-task-manager/lib/model-host-receipt.js +6 -3
  190. package/template/coding-task-manager/lib/model-id-rule.js +19 -0
  191. package/template/coding-task-manager/lib/model-proxy/anthropic-face.js +4 -0
  192. package/template/coding-task-manager/lib/model-proxy/auto-mode-review.js +110 -23
  193. package/template/coding-task-manager/lib/model-proxy/chat-response-translators.js +3 -0
  194. package/template/coding-task-manager/lib/model-proxy/generation-probe.js +60 -17
  195. package/template/coding-task-manager/lib/model-proxy/historical-tool-declarations.js +37 -0
  196. package/template/coding-task-manager/lib/model-proxy/lead-request-validation.js +1 -0
  197. package/template/coding-task-manager/lib/model-proxy/lead-token-admin.js +130 -0
  198. package/template/coding-task-manager/lib/model-proxy/refusal-scope.js +151 -0
  199. package/template/coding-task-manager/lib/model-proxy/request-budget-store.js +11 -2
  200. package/template/coding-task-manager/lib/model-proxy/tokens.js +69 -5
  201. package/template/coding-task-manager/lib/model-proxy/upstream-outcome.js +9 -0
  202. package/template/coding-task-manager/lib/native-agent-model-args.js +25 -22
  203. package/template/coding-task-manager/lib/native-history-control.js +6 -4
  204. package/template/coding-task-manager/lib/native-lead-settings-client.js +44 -4
  205. package/template/coding-task-manager/lib/oauth-proxy-supervisor.js +173 -36
  206. package/template/coding-task-manager/lib/online-backup-worker-client.js +2 -0
  207. package/template/coding-task-manager/lib/opencode-cli.js +480 -0
  208. package/template/coding-task-manager/lib/opencode-launch-model.js +471 -0
  209. package/template/coding-task-manager/lib/opencode-v1-plugin/model-carrier.mjs +35 -0
  210. package/template/coding-task-manager/lib/opencode-v2-plugin/server.js +118 -0
  211. package/template/coding-task-manager/lib/owner-action-manifest-data.json +1 -1
  212. package/template/coding-task-manager/lib/park-composer-draft.js +54 -0
  213. package/template/coding-task-manager/lib/park-fresh-relaunch.js +41 -10
  214. package/template/coding-task-manager/lib/perf-phase-trace.js +72 -0
  215. package/template/coding-task-manager/lib/perf-summary.js +7 -0
  216. package/template/coding-task-manager/lib/perf-tracker.js +63 -2
  217. package/template/coding-task-manager/lib/project-config.js +7 -0
  218. package/template/coding-task-manager/lib/prompt-purge-v2.js +52 -1
  219. package/template/coding-task-manager/lib/provider-login-broker.js +72 -15
  220. package/template/coding-task-manager/lib/provider-login-tab.js +10 -2
  221. package/template/coding-task-manager/lib/read-pool-client.js +89 -2
  222. package/template/coding-task-manager/lib/read-pool-worker-handle.js +29 -4
  223. package/template/coding-task-manager/lib/recent-catalog-query.js +15 -0
  224. package/template/coding-task-manager/lib/recent-session-catalog-response.js +67 -10
  225. package/template/coding-task-manager/lib/recovery-evidence.js +26 -11
  226. package/template/coding-task-manager/lib/relay-client.js +57 -3
  227. package/template/coding-task-manager/lib/relink-audit.js +34 -5
  228. package/template/coding-task-manager/lib/remote-machine-catalog.js +321 -26
  229. package/template/coding-task-manager/lib/remote-machine-create.js +576 -36
  230. package/template/coding-task-manager/lib/remote-machine-link.js +546 -59
  231. package/template/coding-task-manager/lib/remote-machine-proxy.js +143 -9
  232. package/template/coding-task-manager/lib/remote-session-forward.js +65 -7
  233. package/template/coding-task-manager/lib/remote-terminal-target.js +9 -2
  234. package/template/coding-task-manager/lib/restart-guard.js +18 -6
  235. package/template/coding-task-manager/lib/restore-seed-modes.js +87 -0
  236. package/template/coding-task-manager/lib/restored-scrollback-hydration-runtime.js +22 -2
  237. package/template/coding-task-manager/lib/resume-admission.js +17 -3
  238. package/template/coding-task-manager/lib/review-repo-registry.js +58 -19
  239. package/template/coding-task-manager/lib/root-delivery-runtime.js +2 -2
  240. package/template/coding-task-manager/lib/root-prompt-flow.js +1 -1
  241. package/template/coding-task-manager/lib/runtime-contract.js +3 -1
  242. package/template/coding-task-manager/lib/schema-indexes.js +1 -0
  243. package/template/coding-task-manager/lib/session-context-locator.js +742 -0
  244. package/template/coding-task-manager/lib/session-create/attachment.js +2 -2
  245. package/template/coding-task-manager/lib/session-create/create-phases.js +118 -0
  246. package/template/coding-task-manager/lib/session-create/delegate-worker.js +86 -16
  247. package/template/coding-task-manager/lib/session-create/dsh-session.js +930 -0
  248. package/template/coding-task-manager/lib/session-create/lead-route-launch.js +186 -13
  249. package/template/coding-task-manager/lib/session-create/native-lead-lifetime.js +55 -3
  250. package/template/coding-task-manager/lib/session-create/opencode-lead-provider.js +50 -0
  251. package/template/coding-task-manager/lib/session-create/prepared.js +162 -38
  252. package/template/coding-task-manager/lib/session-create/provider-process.js +160 -28
  253. package/template/coding-task-manager/lib/session-create/response.js +7 -7
  254. package/template/coding-task-manager/lib/session-create/structured-session.js +27 -0
  255. package/template/coding-task-manager/lib/session-create/subagent-pick-table.js +3 -1
  256. package/template/coding-task-manager/lib/session-create/subagent-pick.js +458 -9
  257. package/template/coding-task-manager/lib/session-create/transcript.js +1 -1
  258. package/template/coding-task-manager/lib/session-create/worker-budget-setup.js +21 -2
  259. package/template/coding-task-manager/lib/session-create/worker-continuation.js +7 -1
  260. package/template/coding-task-manager/lib/session-create/worker-launch-sandbox.js +39 -10
  261. package/template/coding-task-manager/lib/session-create/worker-observability.js +2 -2
  262. package/template/coding-task-manager/lib/session-empty.js +13 -10
  263. package/template/coding-task-manager/lib/session-host-manager.js +60 -0
  264. package/template/coding-task-manager/lib/session-jobs.js +3 -1
  265. package/template/coding-task-manager/lib/session-lifecycle.js +10 -4
  266. package/template/coding-task-manager/lib/session-model-action-preconditions.js +6 -2
  267. package/template/coding-task-manager/lib/session-model-intent-contract.js +11 -8
  268. package/template/coding-task-manager/lib/session-park.js +738 -0
  269. package/template/coding-task-manager/lib/session-path-grant.js +13 -4
  270. package/template/coding-task-manager/lib/session-status-ledger.js +106 -4
  271. package/template/coding-task-manager/lib/session-transport.js +17 -0
  272. package/template/coding-task-manager/lib/session-workspace-projection.js +54 -5
  273. package/template/coding-task-manager/lib/session-workspace-tracking-runtime.js +213 -1
  274. package/template/coding-task-manager/lib/skill-autocomplete-query.js +120 -3
  275. package/template/coding-task-manager/lib/skill-autocomplete.js +15 -1
  276. package/template/coding-task-manager/lib/sqlite-file-set.js +39 -0
  277. package/template/coding-task-manager/lib/standup-search-adapter.js +16 -2
  278. package/template/coding-task-manager/lib/state-sync/client-ledger.js +12 -0
  279. package/template/coding-task-manager/lib/state-sync/frame-emitter.js +319 -100
  280. package/template/coding-task-manager/lib/storage/coding-session-model-store.js +4 -1
  281. package/template/coding-task-manager/lib/storage/delegate-pref-mirror-store.js +32 -9
  282. package/template/coding-task-manager/lib/structured-session-boot-sweep.js +235 -0
  283. package/template/coding-task-manager/lib/submission-ack.js +90 -2
  284. package/template/coding-task-manager/lib/systemd-scope.js +115 -0
  285. package/template/coding-task-manager/lib/telemetry-receiver.js +39 -0
  286. package/template/coding-task-manager/lib/terminal-attach-geometry.js +985 -0
  287. package/template/coding-task-manager/lib/terminal-bootstrap.js +30 -0
  288. package/template/coding-task-manager/lib/terminal-draft-operation.js +357 -0
  289. package/template/coding-task-manager/lib/terminal-input-admission.js +17 -1
  290. package/template/coding-task-manager/lib/terminal-input-prepared.js +563 -0
  291. package/template/coding-task-manager/lib/transcript-cache-temps.js +171 -0
  292. package/template/coding-task-manager/lib/transcript-frame-cache.js +8 -3
  293. package/template/coding-task-manager/lib/transcript-source.js +36 -4
  294. package/template/coding-task-manager/lib/transcript-store.js +9 -0
  295. package/template/coding-task-manager/lib/typing-interaction-trace.js +106 -3
  296. package/template/coding-task-manager/lib/usage-ledger/backfill.js +40 -5
  297. package/template/coding-task-manager/lib/usage-ledger/client.js +4 -1
  298. package/template/coding-task-manager/lib/usage-ledger/dsh-parse.js +152 -0
  299. package/template/coding-task-manager/lib/usage-ledger/dsh-read.js +73 -0
  300. package/template/coding-task-manager/lib/usage-ledger/ingest.js +34 -6
  301. package/template/coding-task-manager/lib/usage-ledger/live.js +35 -8
  302. package/template/coding-task-manager/lib/usage-ledger/read.js +29 -20
  303. package/template/coding-task-manager/lib/usage-ledger/schema.js +61 -1
  304. package/template/coding-task-manager/lib/usage-ledger/task-coordinator.js +4 -2
  305. package/template/coding-task-manager/lib/wake-skip-reasons.js +1 -0
  306. package/template/coding-task-manager/lib/walle/turn.js +1 -1
  307. package/template/coding-task-manager/lib/walle-history-serve.js +838 -0
  308. package/template/coding-task-manager/lib/walle-kill-authority.js +2 -2
  309. package/template/coding-task-manager/lib/walle-log-rotation.js +164 -1
  310. package/template/coding-task-manager/lib/walle-mcp-auto-config.js +56 -2
  311. package/template/coding-task-manager/lib/walle-row-history.js +495 -0
  312. package/template/coding-task-manager/lib/walle-supervisor.js +33 -14
  313. package/template/coding-task-manager/lib/watch-worker-client.js +203 -0
  314. package/template/coding-task-manager/lib/worker-terminal-output.js +5 -2
  315. package/template/coding-task-manager/lib/ws/hub-bridge.js +35 -0
  316. package/template/coding-task-manager/lib/ws/lifecycle.js +6 -11
  317. package/template/coding-task-manager/lib/ws/misc.js +1 -1
  318. package/template/coding-task-manager/lib/ws/state-sync.js +106 -4
  319. package/template/coding-task-manager/lib/ws/terminal-attach.js +91 -0
  320. package/template/coding-task-manager/oauth-proxy.js +112 -115
  321. package/template/coding-task-manager/package-lock.json +10 -94
  322. package/template/coding-task-manager/package.json +4 -3
  323. package/template/coding-task-manager/protocol/message-table.json +10 -0
  324. package/template/coding-task-manager/protocol/messages.js +10 -0
  325. package/template/coding-task-manager/protocol/ws-schema.json +20 -0
  326. package/template/coding-task-manager/protocol/ws-schema.md +2 -0
  327. package/template/coding-task-manager/public/css/app.css +170 -0
  328. package/template/coding-task-manager/public/css/walle-session.css +4 -0
  329. package/template/coding-task-manager/public/index.html +473 -243
  330. package/template/coding-task-manager/public/js/agent-command.js +86 -0
  331. package/template/coding-task-manager/public/js/app/backups-panel.js +111 -5
  332. package/template/coding-task-manager/public/js/app/coding-agent-upgrade.js +103 -4
  333. package/template/coding-task-manager/public/js/app/machines-page.js +1328 -8
  334. package/template/coding-task-manager/public/js/app/models-tab.js +113 -38
  335. package/template/coding-task-manager/public/js/app/restart-banner.js +53 -0
  336. package/template/coding-task-manager/public/js/app/worktrees-panel.js +184 -2
  337. package/template/coding-task-manager/public/js/background-work.js +196 -27
  338. package/template/coding-task-manager/public/js/coding-session-model-presentation.js +138 -10
  339. package/template/coding-task-manager/public/js/core/active-session-groups.js +179 -0
  340. package/template/coding-task-manager/public/js/core/pending-create.js +238 -0
  341. package/template/coding-task-manager/public/js/core/recent-machine-source.js +228 -0
  342. package/template/coding-task-manager/public/js/core/session-commands.js +180 -14
  343. package/template/coding-task-manager/public/js/dashboard-client-function-order.json +8 -0
  344. package/template/coding-task-manager/public/js/document-review-links.js +9 -1
  345. package/template/coding-task-manager/public/js/file-resolution-client.js +52 -1
  346. package/template/coding-task-manager/public/js/history-app-seam.js +22 -2
  347. package/template/coding-task-manager/public/js/machine-pool-shared.js +558 -3
  348. package/template/coding-task-manager/public/js/message-renderer.js +4 -3
  349. package/template/coding-task-manager/public/js/model-picker.js +3 -1
  350. package/template/coding-task-manager/public/js/native-subagent-review.js +15 -2
  351. package/template/coding-task-manager/public/js/new-session-agent-grid.js +346 -0
  352. package/template/coding-task-manager/public/js/pasted-content.js +236 -0
  353. package/template/coding-task-manager/public/js/pe-private-content-lifetime.js +39 -7
  354. package/template/coding-task-manager/public/js/private-model-lifetime.js +6 -2
  355. package/template/coding-task-manager/public/js/prompts.js +11 -10
  356. package/template/coding-task-manager/public/js/protocol.js +10 -0
  357. package/template/coding-task-manager/public/js/restart-banner-policy.js +29 -1
  358. package/template/coding-task-manager/public/js/session-model-picker.js +447 -57
  359. package/template/coding-task-manager/public/js/session-search-utils.js +18 -2
  360. package/template/coding-task-manager/public/js/session-usage-view.js +2 -1
  361. package/template/coding-task-manager/public/js/session-workspace-tracking.js +51 -1
  362. package/template/coding-task-manager/public/js/state-sync-client.js +10 -1
  363. package/template/coding-task-manager/public/js/stream-view.js +6 -5
  364. package/template/coding-task-manager/public/js/terminal/conv-only-session.js +287 -0
  365. package/template/coding-task-manager/public/js/terminal/runtime-09.js +2 -0
  366. package/template/coding-task-manager/public/js/terminal/runtime-10.js +5 -5
  367. package/template/coding-task-manager/public/js/terminal/runtime-12.js +8 -1
  368. package/template/coding-task-manager/public/js/terminal/runtime-16.js +4 -0
  369. package/template/coding-task-manager/public/js/terminal/runtime-20.js +1 -1
  370. package/template/coding-task-manager/public/js/terminal-attachment-draft.js +67 -13
  371. package/template/coding-task-manager/public/js/terminal-image-orchestration.js +52 -6
  372. package/template/coding-task-manager/public/js/terminal-wheel.js +23 -3
  373. package/template/coding-task-manager/public/js/walle/chat-conversation.js +4 -4
  374. package/template/coding-task-manager/public/js/walle.js +6 -0
  375. package/template/coding-task-manager/public/js/ws/live-dispatch.js +10 -0
  376. package/template/coding-task-manager/public/m/app.css +140 -0
  377. package/template/coding-task-manager/public/m/app.js +507 -68
  378. package/template/coding-task-manager/public/m/index.html +17 -0
  379. package/template/coding-task-manager/public/m/machines.js +838 -4
  380. package/template/coding-task-manager/public/m/private-api.js +31 -1
  381. package/template/coding-task-manager/public/usage.html +1 -0
  382. package/template/coding-task-manager/scripts/dev-route-fixture/synthetic-route-upstream.js +104 -0
  383. package/template/coding-task-manager/scripts/dev-route-fixture.js +339 -0
  384. package/template/coding-task-manager/scripts/opencode-lead-route-probe.js +195 -0
  385. package/template/coding-task-manager/scripts/opencode-v2-contract-probe/fake-anthropic.js +168 -0
  386. package/template/coding-task-manager/scripts/opencode-v2-contract-probe/probe-plugin/server.js +95 -0
  387. package/template/coding-task-manager/scripts/opencode-v2-contract-probe.js +673 -0
  388. package/template/coding-task-manager/server.js +1950 -11679
  389. package/template/coding-task-manager/session-integrity.js +64 -4
  390. package/template/coding-task-manager/workers/db-owner-worker.js +19 -2
  391. package/template/coding-task-manager/workers/online-backup-worker.js +1 -0
  392. package/template/coding-task-manager/workers/read-pool-model-collector.js +51 -3
  393. package/template/coding-task-manager/workers/read-pool-worker.js +76 -6
  394. package/template/coding-task-manager/workers/session-host-pool-process.js +6 -6
  395. package/template/coding-task-manager/workers/session-host-process.js +5 -5
  396. package/template/coding-task-manager/workers/watch-worker.js +105 -0
  397. package/template/coding-task-manager/workers/worktree-worker.js +168 -33
  398. package/template/ctm-relay/lib/protocol.js +14 -1
  399. package/template/package-lock.json +2 -2
  400. package/template/package.json +1 -1
  401. package/template/shared/backup-retention.js +82 -0
  402. package/template/shared/conversation-admission.js +2 -0
  403. package/template/shared/conversation-provenance.js +3 -7
  404. package/template/shared/conversation-storage-v2-contract.js +5 -0
  405. package/template/shared/linked-worktree.js +51 -0
  406. package/template/shared/macos-fda-target.js +57 -0
  407. package/template/shared/orphan-parents.js +55 -0
  408. package/template/shared/release-schema.json +2 -2
  409. package/template/shared/storage/contract.js +3 -0
  410. package/template/wall-e/adapters/imessage.js +12 -0
  411. package/template/wall-e/agent.js +71 -4
  412. package/template/wall-e/api-walle.js +21 -1
  413. package/template/wall-e/bin/train-gemma-e4b-tooluse.js +2 -2
  414. package/template/wall-e/bin/walle-mcp-stdio.js +5 -5
  415. package/template/wall-e/brain/brain-join.js +859 -0
  416. package/template/wall-e/brain/brain-sync-membership.js +20 -0
  417. package/template/wall-e/brain/brain-sync-store.js +131 -77
  418. package/template/wall-e/brain/brain-sync-v2-schema.js +203 -0
  419. package/template/wall-e/brain/brain-sync-v2.js +578 -0
  420. package/template/wall-e/brain/delegate-model-prefs-store.js +13 -4
  421. package/template/wall-e/brain/deterministic-id.js +51 -0
  422. package/template/wall-e/brain/memories.js +2 -2
  423. package/template/wall-e/brain/migrations.js +40 -6
  424. package/template/wall-e/brain/model-identity-migration.js +1 -1
  425. package/template/wall-e/brain/owner-call-whitelist.js +23 -7
  426. package/template/wall-e/brain/schema.js +45 -85
  427. package/template/wall-e/brain.js +171 -35
  428. package/template/wall-e/chat.js +3 -3
  429. package/template/wall-e/coding/agent-shell-env.js +7 -2
  430. package/template/wall-e/coding/prompt-bundle.js +66 -21
  431. package/template/wall-e/config/main-loop-exemptions.json +2 -2
  432. package/template/wall-e/http/admin-api.js +37 -0
  433. package/template/wall-e/http/gws-api.js +4 -0
  434. package/template/wall-e/http/helper-rules-api.js +7 -1
  435. package/template/wall-e/http/model-admin.js +8 -1
  436. package/template/wall-e/http/model-delegate-pref-admin.js +3 -3
  437. package/template/wall-e/http/model-proxy-providers.js +174 -1
  438. package/template/wall-e/http/model-route-health.js +157 -0
  439. package/template/wall-e/http/sync-api.js +278 -464
  440. package/template/wall-e/jobs/job-runner.js +28 -1
  441. package/template/wall-e/lib/brain-owner-op-priority.js +9 -1
  442. package/template/wall-e/lib/ctm-data-dir.js +26 -0
  443. package/template/wall-e/lib/derivation-home.js +147 -0
  444. package/template/wall-e/lib/helper-rules/evaluate.js +50 -9
  445. package/template/wall-e/lib/helper-rules/grade.js +51 -8
  446. package/template/wall-e/lib/helper-rules/lead-call-hook.js +38 -5
  447. package/template/wall-e/lib/helper-rules/store.js +4 -2
  448. package/template/wall-e/lib/landing-mcp-tools.js +170 -0
  449. package/template/wall-e/lib/mcp-client-config.js +133 -14
  450. package/template/wall-e/lib/mcp-gateway-migrate.js +28 -64
  451. package/template/wall-e/lib/mcp-integration.js +58 -13
  452. package/template/wall-e/lib/parent-death.js +7 -1
  453. package/template/wall-e/lib/scheduler-worker-jobs.js +6 -4
  454. package/template/wall-e/llm/delegate-lead-prefs.js +16 -1
  455. package/template/wall-e/llm/delegate-vendor-default.js +12 -5
  456. package/template/wall-e/llm/model-roles.js +1 -1
  457. package/template/wall-e/llm/native-delegate-selection.js +56 -8
  458. package/template/wall-e/llm/worker-delegate-args.js +72 -7
  459. package/template/wall-e/llm/worker-mcp-routing.js +66 -17
  460. package/template/wall-e/loops/brain-custom-writes.js +19 -0
  461. package/template/wall-e/loops/ingest.js +17 -1
  462. package/template/wall-e/loops/think.js +19 -4
  463. package/template/wall-e/mcp-contract.js +222 -6
  464. package/template/wall-e/mcp-protocol-contract.js +40 -5
  465. package/template/wall-e/mcp-server.js +128 -29
  466. package/template/wall-e/mcp-worker-schemas.js +5 -5
  467. package/template/wall-e/memory/ctm-context-client.js +85 -0
  468. package/template/wall-e/memory/ctm-session-context.js +9 -3
  469. package/template/wall-e/memory/hub-bridge-client.js +388 -0
  470. package/template/wall-e/memory/replica-origin.js +56 -0
  471. package/template/wall-e/memory/session-pool.js +135 -0
  472. package/template/wall-e/runtime/live-capabilities.js +8 -3
  473. package/template/wall-e/server.js +4 -2
  474. package/template/wall-e/shared/release-schema.json +2 -2
  475. package/template/wall-e/shared/sqlite-file-set.js +39 -0
  476. package/template/wall-e/shared/storage/contract.js +3 -0
  477. package/template/wall-e/skills/_templates/delegate-worker/VERSION +1 -1
  478. package/template/wall-e/skills/_templates/delegate-worker/released-sha256.json +7 -1
  479. package/template/wall-e/skills/_templates/delegate-worker/templates/claude/SKILL.md +4 -4
  480. package/template/wall-e/skills/_templates/delegate-worker/templates/codex/SKILL.md +6 -6
  481. package/template/wall-e/skills/_templates/fleet-sync/VERSION +1 -0
  482. package/template/wall-e/skills/_templates/fleet-sync/released-sha256.json +16 -0
  483. package/template/wall-e/skills/_templates/fleet-sync/templates/claude/SKILL.md +64 -0
  484. package/template/wall-e/skills/_templates/fleet-sync/templates/codex/SKILL.md +64 -0
  485. package/template/wall-e/tools/chrome-automation.js +6 -0
  486. package/template/wall-e/training/setup-cuda-train-env.sh +2 -2
  487. package/template/wall-e/workers/brain-owner-worker.js +18 -4
  488. package/template/wall-e/workers/mcp-gateway-process.js +5 -5
@@ -0,0 +1,151 @@
1
+ 'use strict';
2
+
3
+ // One entry per (client, feature): the refusal wording whose MEASURED fallback scope is the
4
+ // narrowest the client offers. Evidence and re-measure procedure:
5
+ // docs/auto-mode-native-lead-subagent-latch-2026-10-30.html §1.2 / §7.
6
+ //
7
+ // Measured on Claude Code 2.1.314 (§1.2 P-2 vs P-3/P-4): a 400 whose message names any of the
8
+ // tokens in `forbiddenTokens` (case-insensitive) makes the client turn server-side review off
9
+ // for its WHOLE SESSION -- the host latch `thirdPartyServerClassifierRefused()` fires and the
10
+ // lead process keeps classifying locally after a single subagent was refused. The wording below
11
+ // names none of them, so the client falls back for that ONE CONVERSATION and the session-scoped
12
+ // latch is never set. Re-measure with the `reprobe` command after each Claude Code update; on
13
+ // PASS, bump `measuredOn` in an isolated commit that cites the probe output.
14
+ //
15
+ // `version` is only ever the `\d+\.\d+\.\d+` capture of `entry.userAgent`, never raw header text,
16
+ // so nothing but a version number can reach a log line.
17
+
18
+ const REFUSAL_SCOPE_UNVERIFIED_PREFIX = '[model-proxy] refusal_scope_unverified';
19
+
20
+ const REVIEW_UNAVAILABLE_MESSAGE =
21
+ 'This translated route cannot run server-side action review for this request '
22
+ + '(ctm: review_unavailable_on_route). Send it without that review.';
23
+
24
+ const REFUSAL_SCOPE_ENTRIES = Object.freeze([
25
+ Object.freeze({
26
+ id: 'claude-code/server-review',
27
+ client: 'claude-code',
28
+ feature: 'server-review',
29
+ userAgent: /^claude-cli\/(\d+\.\d+\.\d+)\b/,
30
+ measuredOn: '2.1.314',
31
+ scope: 'conversation',
32
+ status: 400,
33
+ errorType: 'invalid_request_error',
34
+ message: REVIEW_UNAVAILABLE_MESSAGE,
35
+ forbiddenTokens: Object.freeze(['safeguard', 'dangerous-tool-use', 'dangerous_tool_use', 'afk-mode']),
36
+ reprobe: 'node coding-task-manager/bin/probe-refusal-scope.js',
37
+ }),
38
+ ]);
39
+
40
+ const SCOPE_VERIFIED = 'verified';
41
+ const SCOPE_UNVERIFIED = 'unverified';
42
+ const SCOPE_UNKNOWN = 'unknown';
43
+
44
+ // Bound on the once-per-(entry.id, version) warn set: after this many distinct versions the
45
+ // module stops recording AND stops warning, so an oscillating client cannot grow the set or
46
+ // flood the log without limit.
47
+ const MAX_TRACKED_VERSIONS = 64;
48
+
49
+ // The versions already warned about, keyed `${entry.id}\u0000${version}`. Process-global by
50
+ // design: "once per process" is the contract.
51
+ const warnedVersions = new Set();
52
+
53
+ function _isPlainObject(value) {
54
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
55
+ }
56
+
57
+ // One header value by case-insensitive name, or `undefined`. Node lower-cases incoming header
58
+ // names, but a caller-built map may spell them any way, so the lookup never relies on that.
59
+ function _headerValue(headers, name) {
60
+ if (!_isPlainObject(headers)) return undefined;
61
+ for (const [key, value] of Object.entries(headers)) {
62
+ if (typeof key === 'string' && key.toLowerCase() === name) return value;
63
+ }
64
+ return undefined;
65
+ }
66
+
67
+ /**
68
+ * The registered entry for a (client, feature) pair.
69
+ * @param {string} client
70
+ * @param {string} feature
71
+ * @returns {object|null} the frozen entry, or null when none is registered
72
+ */
73
+ function refusalEntry(client, feature) {
74
+ for (const entry of REFUSAL_SCOPE_ENTRIES) {
75
+ if (entry.client === client && entry.feature === feature) return entry;
76
+ }
77
+ return null;
78
+ }
79
+
80
+ /**
81
+ * The client version `X.Y.Z`, parsed with `entry.userAgent` out of the request's `user-agent`
82
+ * header (any case). Only the regex capture is ever returned, so a header that carries more
83
+ * than a version (e.g. `claude-cli/2.1.314<script>`) yields exactly `2.1.314`.
84
+ * @param {object|null} entry
85
+ * @param {unknown} headers
86
+ * @returns {string|null}
87
+ */
88
+ function clientVersion(entry, headers) {
89
+ if (!entry || !(entry.userAgent instanceof RegExp)) return null;
90
+ const raw = _headerValue(headers, 'user-agent');
91
+ const value = Array.isArray(raw) ? raw.find((part) => typeof part === 'string') : raw;
92
+ if (typeof value !== 'string') return null;
93
+ const match = entry.userAgent.exec(value);
94
+ return match && typeof match[1] === 'string' ? match[1] : null;
95
+ }
96
+
97
+ /**
98
+ * Whether the client's measured fallback scope for this entry still holds for the version seen.
99
+ * @param {object|null} entry
100
+ * @param {string|null|undefined} version
101
+ * @returns {'verified'|'unverified'|'unknown'} 'unknown' when no version could be read
102
+ */
103
+ function scopeVerification(entry, version) {
104
+ if (!entry) return SCOPE_UNKNOWN;
105
+ if (typeof version !== 'string' || version === '') return SCOPE_UNKNOWN;
106
+ return version === entry.measuredOn ? SCOPE_VERIFIED : SCOPE_UNVERIFIED;
107
+ }
108
+
109
+ /**
110
+ * The refusal response for an entry, as fresh objects on every call (a caller may mutate them).
111
+ * @param {object|null} entry
112
+ * @returns {{status:number, body:{type:string, error:{type:string, message:string}}}|null}
113
+ */
114
+ function refusalResponse(entry) {
115
+ if (!entry) return null;
116
+ return {
117
+ status: entry.status,
118
+ body: {
119
+ type: 'error',
120
+ error: { type: entry.errorType, message: entry.message },
121
+ },
122
+ };
123
+ }
124
+
125
+ /**
126
+ * Warn ONCE per (entry.id, version) per process when the installed client version is not the one
127
+ * the entry was measured on. A no-op for 'verified'/'unknown' and for a missing entry.
128
+ * @param {object|null} entry
129
+ * @param {string|null|undefined} version
130
+ */
131
+ function warnIfUnverified(entry, version) {
132
+ if (scopeVerification(entry, version) !== SCOPE_UNVERIFIED) return;
133
+ const key = `${entry.id}\u0000${version}`;
134
+ if (warnedVersions.has(key)) return;
135
+ if (warnedVersions.size >= MAX_TRACKED_VERSIONS) return; // bounded: stop recording and warning
136
+ warnedVersions.add(key);
137
+ console.warn(
138
+ `${REFUSAL_SCOPE_UNVERIFIED_PREFIX} entry=${entry.id} client_version=${version}`
139
+ + ` measured_on=${entry.measuredOn} reprobe="${entry.reprobe}"`,
140
+ );
141
+ }
142
+
143
+ module.exports = {
144
+ REVIEW_UNAVAILABLE_MESSAGE,
145
+ REFUSAL_SCOPE_ENTRIES,
146
+ refusalEntry,
147
+ clientVersion,
148
+ scopeVerification,
149
+ refusalResponse,
150
+ warnIfUnverified,
151
+ };
@@ -391,10 +391,19 @@ async function readTaskLedger({ ctmSessionId, dataDir = getDataDir(), fsImpl = f
391
391
  const terminalRequests = terminal && Number.isSafeInteger(terminal.providerRequests) ? terminal.providerRequests : null;
392
392
  const terminalOutput = terminal && Number.isSafeInteger(terminal.outputTokens) ? terminal.outputTokens : null;
393
393
  const priced = completes.filter((record) => typeof record.costUsd === 'number' && Number.isFinite(record.costUsd));
394
+ // C7-A: the journal's own admits/completions are the floor, never a ceiling. CTM's terminal
395
+ // report is derived runtime telemetry: it can be stale (a runtime restart or a report written
396
+ // before the proxy actor's own appends land) and it can legitimately lag a completion that the
397
+ // writer journals after the terminal for an already-admitted id. The admit and known-completion
398
+ // records are append-only observations made by this store itself, so a smaller terminal snapshot
399
+ // cannot erase them. Math.max is a lower bound over two views of the SAME work, never an
400
+ // addition -- a larger legacy terminal snapshot stays the bound -- and a null terminal count is
401
+ // no bound at all, not a measured 0: usage no record observed stays unmeasured.
402
+ const knownCompletionOutputTokens = completes.reduce((sum, record) => sum + (record.outputTokens || 0), 0);
394
403
  rows.push({
395
404
  generation,
396
- requests: terminalRequests !== null ? terminalRequests : admits.length,
397
- outputTokens: terminalOutput !== null ? terminalOutput : completes.reduce((sum, record) => sum + (record.outputTokens || 0), 0),
405
+ requests: Math.max(terminalRequests ?? 0, admits.length),
406
+ outputTokens: Math.max(terminalOutput ?? 0, knownCompletionOutputTokens),
398
407
  // WP-2: a v1 admit has no limits.startLease, so the fallback defaults to 1
399
408
  // (a v1 journal's endLease is always a number).
400
409
  endLease: leases.length ? Math.max(...leases.map((record) => record.lease)) : (admits.length ? admits[0].limits.startLease ?? 1 : 1),
@@ -19,6 +19,11 @@ const LEAD_TOKEN_RE = /^ctml_[0-9a-f]{32}$/;
19
19
  const LEAD_HARNESSES = new Set(['codex-cli', 'claude-code']);
20
20
  const SAFE_DEPLOYMENT_ID = /^[@A-Za-z0-9][A-Za-z0-9._:/@+-]{0,159}$/;
21
21
  const LEAD_PURPOSES = new Set(['routed-primary', 'native-claude-passthrough']);
22
+ // WP-9 §9b L-2 (docs/deepseek-harness-acp-agent-2026-10-28.html D-19): an OPTIONAL mark on a lead
23
+ // token saying its requests must also be written to a per-request usage file. Only 'dsh' exists — the
24
+ // DeepSeek Harness lane, whose ACP wire carries no token counts — and a token without the field is
25
+ // byte-for-byte the binding it has always been (the proxy writes nothing for it).
26
+ const LEAD_USAGE_LOGS = new Set(['dsh']);
22
27
  const NATIVE_CLAUDE_ALIAS = 'anthropic-oauth';
23
28
  const SAFE_OWNER_ID = /^[A-Za-z0-9][A-Za-z0-9._:@/+\-]{0,255}$/;
24
29
  // WP-0A (docs/claude-native-auth-mtls-and-routed-subagents-2026-10-15.html §6.2, D-2/D-7): the
@@ -170,6 +175,10 @@ function _sameLeadRegistration(entry, next) {
170
175
  && entry.harness === next.harness
171
176
  && entry.providerThreadId === next.providerThreadId
172
177
  && entry.transitionId === next.transitionId
178
+ // D-19: the usage mark is immutable identity like every other field here — an absent value reads as
179
+ // null, so a replay of the exact same registration stays idempotent while turning the mark on (or
180
+ // off) for a live token is a rebind, never an upgrade.
181
+ && (entry.usageLog || null) === (next.usageLog || null)
173
182
  // Carrier and fingerprint comparison uses the RESOLVED values: an owner registered before the
174
183
  // fields existed (or one whose caller omits them) is on 'legacy-bearer' with no fingerprint, so
175
184
  // replaying it as an explicit 'legacy-bearer' stays idempotent while a switch to 'mtls-v1' — or a
@@ -214,8 +223,14 @@ function _liveLeadEntry(token) {
214
223
 
215
224
  function _normalizeLeadRegistration({ ctmSessionId, aliases, bindings, processGeneration, transitionId,
216
225
  purpose = 'routed-primary', delegateScope = false, harness, providerThreadId, nativeTransport,
217
- nativeCertFingerprint, nativeSettingsRef } = {}) {
226
+ nativeCertFingerprint, nativeSettingsRef, usageLog } = {}) {
218
227
  if (!LEAD_PURPOSES.has(purpose)) throw new TypeError('registerLeadToken: unknown purpose');
228
+ // D-19/L-2: `usageLog` is an optional mark, and 'dsh' is the only value it has. `typeof`-first for
229
+ // the same reason the native fields are: an array- or object-wrapped value must be refused rather
230
+ // than coerced and then stored as something no downstream comparison could ever match.
231
+ if (usageLog != null && (typeof usageLog !== 'string' || !LEAD_USAGE_LOGS.has(usageLog))) {
232
+ throw new TypeError('registerLeadToken: unknown usageLog');
233
+ }
219
234
  if (typeof delegateScope !== 'boolean') throw new TypeError('registerLeadToken: delegateScope must be boolean');
220
235
  if (typeof ctmSessionId !== 'string' || !SAFE_OWNER_ID.test(ctmSessionId)) {
221
236
  throw new TypeError('registerLeadToken: ctmSessionId required');
@@ -282,7 +297,8 @@ function _normalizeLeadRegistration({ ctmSessionId, aliases, bindings, processGe
282
297
  // a settings locator is ever manufactured for it.
283
298
  return { ctmSessionId, aliases: new Set(aliases), bindings: [], processGeneration,
284
299
  purpose, delegateScope, harness, providerThreadId,
285
- ...(nativeTransport === NATIVE_TRANSPORT_MTLS ? { nativeTransport, nativeCertFingerprint, nativeSettingsRef } : {}) };
300
+ ...(nativeTransport === NATIVE_TRANSPORT_MTLS ? { nativeTransport, nativeCertFingerprint, nativeSettingsRef } : {}),
301
+ ...(usageLog ? { usageLog } : {}) };
286
302
  }
287
303
  if (providerThreadId != null && (typeof providerThreadId !== 'string' || !SAFE_OWNER_ID.test(providerThreadId))) {
288
304
  throw new TypeError('registerLeadToken: providerThreadId must be a bounded identifier');
@@ -299,7 +315,8 @@ function _normalizeLeadRegistration({ ctmSessionId, aliases, bindings, processGe
299
315
  const coherentHarness = harness || derivedHarness;
300
316
  return { ctmSessionId, aliases: new Set(aliases), bindings: normalizedBindings,
301
317
  processGeneration, purpose, delegateScope, ...(coherentHarness ? { harness: coherentHarness } : {}),
302
- ...(providerThreadId ? { providerThreadId } : {}), ...(transitionId != null ? { transitionId } : {}) };
318
+ ...(providerThreadId ? { providerThreadId } : {}), ...(transitionId != null ? { transitionId } : {}),
319
+ ...(usageLog ? { usageLog } : {}) };
303
320
  }
304
321
 
305
322
  function registerLeadToken(registration = {}) {
@@ -310,6 +327,11 @@ function registerLeadToken(registration = {}) {
310
327
  return _commitLeadRegistration(validated.token, validated.entry);
311
328
  }
312
329
 
330
+ // WP-1 (§5 D-4, §9 S5): the bounded code a REFUSED revocation replay carries. A static string -- never a
331
+ // token, a fingerprint or any other caller value -- so the native admin path can map it to a 409 without
332
+ // ever echoing input.
333
+ const LEAD_TOKEN_REVOKED_CODE = 'lead_token_revoked';
334
+
313
335
  /**
314
336
  * The PURE half of lead-token registration: the field allowlist, the token grammar, and every
315
337
  * shape/normalization rule, including the immutable carrier/fingerprint/settings-ref pairing
@@ -329,7 +351,7 @@ function registerLeadToken(registration = {}) {
329
351
  function validateLeadTokenRegistration(registration = {}) {
330
352
  const allowedFields = new Set(['token', 'ctmSessionId', 'aliases', 'bindings', 'processGeneration',
331
353
  'transitionId', 'purpose', 'delegateScope', 'harness', 'providerThreadId', 'nativeTransport',
332
- 'nativeCertFingerprint', 'nativeSettingsRef']);
354
+ 'nativeCertFingerprint', 'nativeSettingsRef', 'usageLog']);
333
355
  if (!registration || typeof registration !== 'object' || Array.isArray(registration)
334
356
  || Object.keys(registration).some((key) => !allowedFields.has(key))) {
335
357
  throw new TypeError('registerLeadToken: unknown registration field');
@@ -348,6 +370,13 @@ function validateLeadTokenRegistration(registration = {}) {
348
370
  function _commitLeadRegistration(token, next) {
349
371
  const conflict = _leadRegistrationConflictMessage(token, next);
350
372
  if (conflict) throw new TypeError(conflict);
373
+ // WP-1 (§5 D-4, §9 S5): a REVOKED owner is a tombstone, not an upgrade. Only an EXACT replay of a
374
+ // revoked registration reaches here (a differing one is already the rebind conflict above), and it is
375
+ // refused with the bounded code BEFORE the idempotent-replay short-circuit below: acknowledging it as
376
+ // a successful grant is exactly the lie this fix removes, because the revoke released this owner's
377
+ // certificate index row too, so the "granted" token resolved to no live owner at all.
378
+ const revokedRefusal = _revokedLeadRegistrationRefusal(token, next);
379
+ if (revokedRefusal) throw revokedRefusal;
351
380
  // The idempotent-replay case writes the same entry again; that is what makes a replay a no-op.
352
381
  const existing = _tokens.get(token);
353
382
  if (existing && _sameLeadRegistration(existing, next)) return { token };
@@ -357,6 +386,28 @@ function _commitLeadRegistration(token, next) {
357
386
  return { token };
358
387
  }
359
388
 
389
+ /**
390
+ * The revocation half of the LIVE registration rules (§5 D-4): an EXACT replay of a revoked lead
391
+ * registration is refused, with a bounded code and no write. It reads and writes nothing else, so asking
392
+ * it before a ticket exists and again inside the commit is the same answer.
393
+ *
394
+ * A DIFFERING registration of a revoked token is NOT answered here -- the rebind conflict above owns it,
395
+ * so every immutable-field rule and the duplicate-certificate rule keep their existing refusal. And since
396
+ * a revoke releases the fingerprint in the same step, a different NEW token may still take that released
397
+ * certificate over (D-2): this refusal is about the revoked IDENTITY, never about the certificate.
398
+ *
399
+ * @returns {Error|null} the coded TypeError the caller must throw, or null when this is not a replay of a
400
+ * revoked registration
401
+ */
402
+ function _revokedLeadRegistrationRefusal(token, next) {
403
+ const existing = _tokens.get(token);
404
+ if (!existing || existing.kind !== 'lead' || !existing.revoked) return null;
405
+ if (!_sameLeadRegistration(existing, next)) return null;
406
+ const error = new TypeError('registerLeadToken: a revoked token cannot be re-registered');
407
+ error.code = LEAD_TOKEN_REVOKED_CODE;
408
+ return error;
409
+ }
410
+
360
411
  /**
361
412
  * The read-only LIVE half of lead-token registration: everything that depends on the CURRENT registry.
362
413
  *
@@ -454,6 +505,11 @@ function beginLeadTokenRegistration(registration = {}) {
454
505
  // there — this early call is what keeps a doomed registration from ever loading a CA or binding a port.
455
506
  const conflict = _leadRegistrationConflictMessage(validated.token, validated.entry);
456
507
  if (conflict) throw new TypeError(conflict);
508
+ // WP-1 (§5 D-4): a revoked owner is refused HERE, before the ticket exists, so a doomed replay never
509
+ // loads a CA and never binds a port. The commit below asks the same question again, so a revocation
510
+ // that lands while the caller is awaiting is refused there too.
511
+ const revokedRefusal = _revokedLeadRegistrationRefusal(validated.token, validated.entry);
512
+ if (revokedRefusal) throw revokedRefusal;
457
513
  if (_pendingLeadRegistrations.size >= MAX_PENDING_LEAD_REGISTRATIONS) {
458
514
  throw new TypeError('beginLeadTokenRegistration: too many pending registrations');
459
515
  }
@@ -474,10 +530,13 @@ function beginLeadTokenRegistration(registration = {}) {
474
530
  const result = _commitLeadRegistration(ticket.token, ticket.entry);
475
531
  ticket.state = 'committed';
476
532
  return { ok: true, token: result.token };
477
- } catch {
533
+ } catch (err) {
478
534
  // The live checks refused (a concurrent owner took this certificate, or the token was rebound):
479
535
  // the bounded code is all the caller learns — never the underlying message, which is internal.
480
536
  ticket.state = 'refused';
537
+ // ...and a revocation refusal keeps its OWN bounded code, so the native admin path answers 409
538
+ // `lead_token_revoked` at commit time exactly as it does on the synchronous path (§9 S6).
539
+ if (err && err.code === LEAD_TOKEN_REVOKED_CODE) return { ok: false, code: LEAD_TOKEN_REVOKED_CODE };
481
540
  return { ok: false, code: LEAD_REGISTRATION_TICKET_CODES.REFUSED };
482
541
  }
483
542
  },
@@ -552,6 +611,10 @@ function leadTokenBinding(token) {
552
611
  ...(entry.nativeTransport ? { nativeTransport: entry.nativeTransport } : {}),
553
612
  ...(entry.nativeCertFingerprint ? { nativeCertFingerprint: entry.nativeCertFingerprint } : {}),
554
613
  ...(entry.nativeSettingsRef ? { nativeSettingsRef: entry.nativeSettingsRef } : {}),
614
+ // D-19/L-2: the usage mark rides the binding projection exactly as supplied, so the proxy's
615
+ // completion path can tell a dsh lane from every other lead without a second registry. Absent on
616
+ // every registration that never set it, so those projections stay byte-for-byte unchanged.
617
+ ...(entry.usageLog ? { usageLog: entry.usageLog } : {}),
555
618
  };
556
619
  }
557
620
 
@@ -869,6 +932,7 @@ module.exports = {
869
932
  NATIVE_CERT_FINGERPRINT_RE,
870
933
  NATIVE_SETTINGS_REF_RE,
871
934
  LEAD_REGISTRATION_TICKET_CODES,
935
+ LEAD_TOKEN_REVOKED_CODE,
872
936
  MAX_PENDING_LEAD_REGISTRATIONS,
873
937
  registerLeadToken,
874
938
  validateLeadTokenRegistration,
@@ -11,6 +11,7 @@ const SAFE_MESSAGES = Object.freeze({
11
11
  server_error: 'The provider could not complete the request.',
12
12
  model_not_invocable: 'This route cannot invoke this model id; it needs the model’s inference-profile id.',
13
13
  model_access_denied: 'The route’s provider account denies access to this model.',
14
+ model_not_found: 'This route does not serve this model id.',
14
15
  });
15
16
 
16
17
  const SAFE_CODE = /^[A-Za-z0-9_.-]{1,64}$/;
@@ -45,6 +46,14 @@ function _classify(code, status, message) {
45
46
  // rate_limit_exceeded) and before the generic invalid/4xx line. The provider's own prose is never copied.
46
47
  if (status === 401 || status === 403 || code === '401' || code === '403') return 'model_access_denied';
47
48
  if (/on-demand throughput|inference profile/.test(folded) && (status === 400 || status === 422 || code === '400' || code === '422')) return 'model_not_invocable';
49
+ // WP-1b (§12a rule 2): a 404, or a 400/422 whose prose says the model ID ITSELF is unknown or
50
+ // unreachable from this account, is the same verdict the document above describes — the route does
51
+ // not serve this model id — and the switch has to name it truthfully instead of "the request is
52
+ // invalid". Both run after the four message-driven classes and the two route/model-id lines, and
53
+ // before the generic invalid/4xx line (which would otherwise swallow a 404 as invalid_prompt).
54
+ if (status === 404 || code === '404') return 'model_not_found';
55
+ if ((status === 400 || status === 422 || code === '400' || code === '422')
56
+ && /model identifier is invalid|model[_ -]?not[_ -]?found|not found or your project does not have access/.test(folded)) return 'model_not_found';
48
57
  if (/invalid|model[_ -]?not[_ -]?found|history/.test(folded) || ['400', '404', '422'].includes(code) || [400, 404, 422].includes(status)) return 'invalid_prompt';
49
58
  return 'server_error';
50
59
  }
@@ -16,11 +16,6 @@ const NATIVE_AGENT_MODEL_FLAG = Object.freeze({
16
16
  opencode: '--model',
17
17
  });
18
18
 
19
- // OpenCode model IDs include the provider ID (`provider/model`). Keep CTM's
20
- // opinionated default here instead of mutating ~/.config/opencode so terminal
21
- // launches outside CTM keep the user's own OpenCode defaults.
22
- const DEFAULT_OPENCODE_MODEL = 'zai/glm-5.2';
23
-
24
19
  function _normalizeProvider(value) {
25
20
  const text = String(value || '').trim().toLowerCase().replace(/_/g, '-');
26
21
  if (text === 'claude' || text === 'claude-code') return 'anthropic';
@@ -43,14 +38,6 @@ function _inferProviderFromModel(modelId) {
43
38
  return '';
44
39
  }
45
40
 
46
- function _opencodeDefaultModel(env = process.env) {
47
- return String(
48
- env?.CTM_OPENCODE_DEFAULT_MODEL
49
- || env?.OPENCODE_DEFAULT_MODEL
50
- || DEFAULT_OPENCODE_MODEL
51
- ).trim();
52
- }
53
-
54
41
  function nativeProviderForAgent(agentType) {
55
42
  return NATIVE_AGENT_PROVIDER[normalizeAgentType(agentType)] || '';
56
43
  }
@@ -85,18 +72,18 @@ function selectNativeAgentLaunchModel({
85
72
  explicitModelId,
86
73
  explicitModelProvider,
87
74
  defaultSelection,
88
- env = process.env,
89
75
  } = {}) {
90
76
  const normalizedAgent = normalizeAgentType(agentType);
77
+ // OpenCode model ids are `provider/model`. Same rule as Claude/Codex: an explicit pick becomes
78
+ // `--model`; with no pick CTM passes none and the CLI's own default decides (observed on the
79
+ // first `opencode:message`). CTM never invents an OpenCode default (OpenCode round 2 §5 R-5).
91
80
  if (normalizedAgent === 'opencode') {
92
81
  const explicitModel = String(explicitModelId || '').trim();
93
- const model = explicitModel || _opencodeDefaultModel(env);
94
- if (!model) return null;
95
- const modelProvider = _normalizeProvider(explicitModelProvider) || _inferProviderFromModel(model) || 'opencode';
82
+ if (!explicitModel) return null;
96
83
  return {
97
- model_id: model,
98
- model_provider: modelProvider,
99
- source: explicitModel ? 'explicit' : 'opencode-default',
84
+ model_id: explicitModel,
85
+ model_provider: _normalizeProvider(explicitModelProvider) || _inferProviderFromModel(explicitModel) || 'opencode',
86
+ source: 'explicit',
100
87
  };
101
88
  }
102
89
 
@@ -129,18 +116,34 @@ function selectNativeAgentLaunchModel({
129
116
  return null;
130
117
  }
131
118
 
132
- function injectNativeAgentModelArgs({ agentType, args, modelId } = {}) {
119
+ // OpenCode 2's TUI rejects `--model` (it prints usage and exits); OpenCode 1.x
120
+ // accepts it. The launch path is synchronous, so the line comes from the last
121
+ // detection this process recorded (opencode-cli.peekOpencodeLine — sync, no fs, no
122
+ // spawn). R-6 (opencode-v2-model-switch §5.1): that detection is PRIMED by an
123
+ // `await primeOpencodeLine()` at the top of server.js's handleCreate, before any
124
+ // OpenCode argv is built, so an OpenCode launch never decides on an unknown line.
125
+ // The old background opencodeLine() warm-up is gone. Only OpenCode 2's line is
126
+ // treated specially: every other value (including a `null` we could not determine)
127
+ // keeps the pre-existing behaviour exactly.
128
+ function injectNativeAgentModelArgs({ agentType, args, modelId, env, lineOf } = {}) {
133
129
  const normalizedAgent = normalizeAgentType(agentType);
134
130
  const flag = NATIVE_AGENT_MODEL_FLAG[normalizedAgent];
135
131
  const model = String(modelId || '').trim();
136
132
  const list = Array.isArray(args) ? args.slice() : [];
137
133
  if (!flag || !model) return { args: list, injected: false, reason: 'unsupported' };
134
+ if (normalizedAgent === 'opencode') {
135
+ const cli = require('./opencode-cli');
136
+ const resolveLine = lineOf || (() => cli.peekOpencodeLine());
137
+ const line = resolveLine(env || process.env);
138
+ if (line === 'v2') {
139
+ return { args: list, injected: false, reason: 'opencode-v2-no-tui-model-flag' };
140
+ }
141
+ }
138
142
  if (hasExplicitNativeModelArg(list)) return { args: list, injected: false, reason: 'explicit-model-arg' };
139
143
  return { args: [flag, model, ...list], injected: true, reason: 'injected' };
140
144
  }
141
145
 
142
146
  module.exports = {
143
- DEFAULT_OPENCODE_MODEL,
144
147
  NATIVE_AGENT_PROVIDER,
145
148
  hasExplicitNativeModelArg,
146
149
  injectNativeAgentModelArgs,
@@ -4,8 +4,10 @@ const { spawn } = require('node:child_process');
4
4
  const { StringDecoder } = require('node:string_decoder');
5
5
  const path = require('node:path');
6
6
  const { codexProxyProviderArgs } = require('./codex-launch-health');
7
- // S-5: the adapter id and the qualified-version membership test come from the one table.
8
- const { CODEX_CONTROL_ADAPTER, isQualifiedCodexVersion, isNonElevatedCyberAccessProgram } = require('./codex-harness-qualification');
7
+ // S-5: the adapter id and the non-elevated cyber program set come from the one table. The Codex
8
+ // version is recorded provenance only (WP-0): the two validators below refuse a missing/blank
9
+ // version string but never compare it against a qualified-version table, range or floor.
10
+ const { CODEX_CONTROL_ADAPTER, isNonElevatedCyberAccessProgram } = require('./codex-harness-qualification');
9
11
 
10
12
  const QUALIFIED_ADAPTER = CODEX_CONTROL_ADAPTER;
11
13
  const MAX_FRAME_BYTES = 1024 * 1024;
@@ -52,7 +54,7 @@ function createCodexNativeHistoryControl({ sourceRecipe, targetRecipe } = {}) {
52
54
 
53
55
  function validateSource(recipe) {
54
56
  if (!isRecord(recipe) || recipe.controlAdapterVersion !== QUALIFIED_ADAPTER
55
- || !isQualifiedCodexVersion(recipe.programVersion) || !absolute(recipe.executablePath)
57
+ || !nonempty(recipe.programVersion) || !absolute(recipe.executablePath)
56
58
  || !absolute(recipe.codexHome) || !absolute(recipe.homeDir) || !absolute(recipe.workingDirectory)
57
59
  || (recipe.tmpDir != null && !absolute(recipe.tmpDir))
58
60
  || !validCyberAccessProgram(recipe.cyberAccessProgram)
@@ -67,7 +69,7 @@ function validateSource(recipe) {
67
69
  }
68
70
 
69
71
  function validateTarget(recipe) {
70
- if (!isRecord(recipe) || recipe.harness !== 'codex-cli' || !isQualifiedCodexVersion(recipe.harnessVersion)
72
+ if (!isRecord(recipe) || recipe.harness !== 'codex-cli' || !nonempty(recipe.harnessVersion)
71
73
  || !nonempty(recipe.providerId) || !nonempty(recipe.providerName) || !nonempty(recipe.modelId)
72
74
  || !Number.isSafeInteger(recipe.proxyPort) || recipe.proxyPort < 1 || recipe.proxyPort > 65535
73
75
  || !validAlias(recipe.alias) || !validEnvKey(recipe.authEnvKey)
@@ -219,17 +219,34 @@ async function _ensureGeneratedDirAsync(dirPath, dataDir) {
219
219
  // Exclusive creation, so a ref collision is a refusal rather than an adoption of someone else's
220
220
  // container. The captured identity is what the later guarded cleanup and the worker's descriptor
221
221
  // check both use.
222
- async function _reserveEmptyDirAsync(dirPath) {
222
+ //
223
+ // S-30: the reserving parent then holds a read-only directory FileHandle (stored in `sink[key]`) for
224
+ // as long as the identity is relied on. dev+ino alone is unsound on ext4 (a removed directory's inode
225
+ // number is reused by the next mkdir), but an inode with an open fd cannot be freed.
226
+ async function _reserveEmptyDirAsync(dirPath, sink, key) {
223
227
  try {
224
228
  await fs.promises.mkdir(dirPath, { mode: 0o700 });
225
229
  } catch {
226
230
  // EEXIST (or anything else): never adopt, never delete, never retry with a different name.
227
231
  _fail(NATIVE_LEAD_SETTINGS_CLIENT_CODES.RESERVE_FAILED);
228
232
  }
233
+ let handle = null;
234
+ try {
235
+ handle = await fs.promises.open(
236
+ dirPath,
237
+ fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | (fs.constants.O_NOFOLLOW || 0),
238
+ );
239
+ } catch {
240
+ _fail(NATIVE_LEAD_SETTINGS_CLIENT_CODES.RESERVE_FAILED);
241
+ }
242
+ sink[key] = handle; // owned by the reservation from here, so every unwind path closes it
229
243
  const detailed = await _lstatDetailed(dirPath);
244
+ const opened = await handle.stat();
230
245
  if (detailed.status !== 'ok'
231
246
  || detailed.stat.isSymbolicLink()
232
247
  || !detailed.stat.isDirectory()
248
+ || Number(opened.dev) !== Number(detailed.stat.dev)
249
+ || Number(opened.ino) !== Number(detailed.stat.ino)
233
250
  || (typeof process.getuid === 'function' && Number(detailed.stat.uid) !== process.getuid())
234
251
  || (Number(detailed.stat.mode) & 0o077) !== 0) {
235
252
  _fail(NATIVE_LEAD_SETTINGS_CLIENT_CODES.RESERVE_FAILED);
@@ -746,6 +763,7 @@ function createNativeLeadSettingsClient(options = {}) {
746
763
  const tracked = new Set(); // every in-flight preparation, from its reservation onward
747
764
  const adoptions = new Set(); // every in-flight adoption, joined by close() too (D-31)
748
765
  const obligations = new Map(); // retained cleanup obligations, keyed for stable reporting
766
+ const reservations = new Set(); // every reservation holding directory handles, until closed
749
767
  const obligationKeys = new Map(); // reservation -> Set<key>, so a LATER verified cleanup clears them
750
768
  let obligationSequence = 0;
751
769
  let closed = false;
@@ -794,12 +812,14 @@ function createNativeLeadSettingsClient(options = {}) {
794
812
  attemptDir,
795
813
  authorityCandidateDir,
796
814
  descriptor: { attempt: null, authorityCandidate: null },
815
+ handles: { attempt: null, authorityCandidate: null }, // parent-side only, never serialized
797
816
  };
817
+ reservations.add(reservation);
798
818
  try {
799
819
  await _ensureGeneratedDirAsync(path.dirname(attemptDir), dataDir);
800
820
  await _ensureGeneratedDirAsync(nativeLeadTlsDir({ dataDir }), dataDir);
801
- reservation.descriptor.attempt = await _reserveEmptyDirAsync(attemptDir);
802
- reservation.descriptor.authorityCandidate = await _reserveEmptyDirAsync(authorityCandidateDir);
821
+ reservation.descriptor.attempt = await _reserveEmptyDirAsync(attemptDir, reservation.handles, 'attempt');
822
+ reservation.descriptor.authorityCandidate = await _reserveEmptyDirAsync(authorityCandidateDir, reservation.handles, 'authorityCandidate');
803
823
  // The <= 4 KiB owner/ref stamp is written BEFORE dispatch, so a lost reply or a partial worker
804
824
  // write still leaves a directory whose identity this client can clean by exact match.
805
825
  const stamp = `${JSON.stringify({ version: 1, owner: { ...owner }, settingsRef, fingerprint: null })}\n`;
@@ -833,6 +853,17 @@ function createNativeLeadSettingsClient(options = {}) {
833
853
  }
834
854
  }
835
855
 
856
+ // Idempotent: each handle is detached before it is closed, so it is closed exactly once.
857
+ async function _closeReservationHandles(reservation) {
858
+ const handles = reservation && reservation.handles;
859
+ if (!handles) return;
860
+ for (const name of Object.keys(handles)) {
861
+ const handle = handles[name];
862
+ handles[name] = null;
863
+ if (handle) await handle.close().catch(() => { /* expected: a directory fd close cannot lose data; the handle is detached either way */ });
864
+ }
865
+ }
866
+
836
867
  // The guarded cleanup of one reservation. The authority CANDIDATE directory is normally already
837
868
  // gone (the worker removes its own link on success), so an absent candidate is not an obligation.
838
869
  async function _cleanupReservation(reservation) {
@@ -843,7 +874,12 @@ function createNativeLeadSettingsClient(options = {}) {
843
874
  const candidate = descriptor.authorityCandidate
844
875
  ? await _removeReservedDirGuarded(descriptor.authorityCandidate, NATIVE_LEAD_CANDIDATE_REMOVABLE)
845
876
  : { removed: true, alreadyGone: true };
846
- if (attempt.removed && candidate.removed) return { cleaned: true };
877
+ if (attempt.removed && candidate.removed) {
878
+ // Closed only AFTER the verified removal: the open fd is what kept the guarded check honest.
879
+ await _closeReservationHandles(reservation);
880
+ reservations.delete(reservation);
881
+ return { cleaned: true };
882
+ }
847
883
  return {
848
884
  cleaned: false,
849
885
  code: NATIVE_LEAD_SETTINGS_CLIENT_CODES.CLEANUP_DEFERRED,
@@ -1456,6 +1492,10 @@ function createNativeLeadSettingsClient(options = {}) {
1456
1492
  // below — a single pass is therefore complete, and a committed host is never touched.
1457
1493
  const uncommitted = Array.from(leases).filter((lease) => lease.kind === 'native-lead-settings' && !lease.isCommitted());
1458
1494
  await Promise.all(uncommitted.map((lease) => lease.release().catch(() => undefined)));
1495
+ // Handles of reservations with no outstanding obligation are closed; retained obligations keep theirs.
1496
+ for (const reservation of reservations) {
1497
+ if (!obligationKeys.has(reservation)) await _closeReservationHandles(reservation);
1498
+ }
1459
1499
  return { ok: obligations.size === 0, obligations: Array.from(obligations.values()) };
1460
1500
  })();
1461
1501
  return closePromise;