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,471 @@
1
+ 'use strict';
2
+
3
+ // The never-prompted OpenCode 1.x lead-model switch (docs/opencode-lead-and-delegate-model-
4
+ // selection-2026-10-28.html §5 R-2/R-4/R-5/R-7/R-8/R-9, §6.3, §8 WP-1).
5
+ //
6
+ // Why this is a SEPARATE controller and not a branch of lib/lead-model-switch.js: the journaled
7
+ // Claude/Codex machine is built around an owner tuple, a provider thread and a history handoff.
8
+ // An OpenCode 1.x tab that has never been prompted has NONE of those -- its session does not exist
9
+ // yet (probe P-9), the owner is null and the resume id is empty -- so it is a distinct request kind,
10
+ // `launch_model`, guarded by the process generation instead of `expected_owner` (R-3). The switch is
11
+ // park -> fresh relaunch with `--model provider/model` (R-2, probe P-8), and it settles only on an
12
+ // `opencode:message` observation carrying the requested model (R-7) -- never on argv, never on
13
+ // `session.model` alone.
14
+ //
15
+ // What it deliberately does NOT do:
16
+ // - a v2 binary keeps its `--model`-free argv (R-8): the 2.x TUI rejects the flag (probe P-12), so
17
+ // its target rides the inline config + the CTM 2.x plugin's `switch_model` instead (WP-2/WP-4).
18
+ // - effort (R-9): OpenCode 1.x has no `--variant` on the TUI argv.
19
+ //
20
+ // Revision 2 (opencode-v2-model-switch-standalone-plugin-2026-10-30.html §5.1 R-9, §6.7): a tab WITH a
21
+ // resume id is NO LONGER refused. TUI 1.x still keeps the model its last user message recorded and
22
+ // still ignores `--model` on a resume (probe P-7, upstream #53806), but a server plugin's
23
+ // `chat.message` hook CAN rewrite `output.message.model` (P-V2/P-V4). So a prompted v1 tab switches by
24
+ // the SAME park + resume relaunch as Claude/Codex, with `lib/opencode-v1-plugin/model-carrier.mjs`
25
+ // added to that one relaunch's inline config and the target riding the spawn env (subagent-pick.js's
26
+ // opencodeV1CarrierEnv). Settlement is unchanged (R-5).
27
+ //
28
+ // Revision 3 (opencode-v2-model-switch-standalone-plugin-2026-10-30.html §5.1 R-4/R-5, §8 WP-4): the
29
+ // SAME park + resume relaunch now serves an OpenCode 2.x tab. Its target rides the launch's inline
30
+ // config `model` and the CTM 2.x plugin's `switch_model` (subagent-pick.js's opencodeV2PluginEnv) --
31
+ // never a flag, since the 2.x TUI rejects `--model`. A v2 tab with no resume id is a fresh relaunch.
32
+ //
33
+ // It reuses the SAME `park` and `restore` primitives the journaled machine uses (the runtime passes
34
+ // the same two lambdas), so a relaunch is one park and one restore, exactly as Claude's is.
35
+
36
+ const crypto = require('node:crypto');
37
+ const { publicSessionModelRequest } = require('./session-model-action-preconditions');
38
+ const { leadWireAgent } = require('./lead-wire-agent');
39
+ const { OPENCODE_LEAD_MODEL_REF } = require('./session-create/opencode-lead-provider');
40
+
41
+ const SESSION_NOT_FOUND = 'session_not_found';
42
+ const FLIGHT_KEY = 'relaunching';
43
+ const MAX_MODEL_ID_LENGTH = 200;
44
+ const MAX_ROUTE_ID_LENGTH = 200;
45
+ const MAX_CLIENT_REQUEST_ID_LENGTH = 128;
46
+
47
+ // The sentence each refusal carries, so every client shows a reason instead of a bare HTTP status
48
+ // (the shared presenter prefers its own sentence for a code it knows, then this `message`).
49
+ const REFUSAL_MESSAGES = Object.freeze({
50
+ invalid_request_kind: 'This OpenCode tab can only switch at launch; reopen the model picker and try again.',
51
+ invalid_route_id: 'The gateway route this pick carried was not usable. Reopen the model picker and try again.',
52
+ lead_route_invalid: 'That gateway route is not available for this lead right now.',
53
+ model_owner_changed: 'This OpenCode tab restarted since the picker opened. Reopen the picker and try again.',
54
+ lead_switch_busy: 'OpenCode is busy right now, so nothing was restarted. Try again in a moment.',
55
+ lead_switch_in_flight: 'A model switch for this tab is already running.',
56
+ opencode_catalog_unavailable: 'OpenCode could not list its models, so nothing was restarted.',
57
+ lead_switch_park_failed: 'CTM could not stop this OpenCode tab to restart it, so it keeps its current model.',
58
+ lead_switch_restore_failed: 'OpenCode did not restart on the new model.',
59
+ lead_switch_not_cancellable: 'There is no pending model switch to cancel.',
60
+ });
61
+ // Park refusals that only mean "not right now" (the journaled machine requeues these, lib/lead-model-switch.js
62
+ // REQUEUE_PARK_CODES): a busy/protected tab or the post-boot restore window. A pinned tab, a draft or a
63
+ // non-resumable tab is a real refusal and is reported as such.
64
+ const TRANSIENT_PARK_CODES = new Set(['session_protected', 'park_race_protected']);
65
+ const NON_TRANSIENT_PROTECTIONS = new Set(['pinned', 'draft', 'not-resumable']);
66
+
67
+ function refusal(status, code, extra = {}) {
68
+ const message = REFUSAL_MESSAGES[code];
69
+ return { ok: false, status, code, ...(message ? { message } : {}), ...extra };
70
+ }
71
+
72
+ // The session's observed model in the SAME `provider/model` spelling a catalog row carries.
73
+ // `session.model_id` may already be qualified (`moonshotai/kimi-k3`) or bare (`kimi-k3`) with the
74
+ // provider kept separately; only the bare form is qualified here.
75
+ function observedModelValue(session) {
76
+ const raw = String((session && session.model_id) || '').trim();
77
+ if (!raw) return '';
78
+ if (raw.includes('/')) return raw;
79
+ const provider = String((session && session.model_provider) || '').trim();
80
+ return provider ? `${provider}/${raw}` : raw;
81
+ }
82
+
83
+ /**
84
+ * @param {object} deps
85
+ * @param {(id: string) => ({ agent: string|null, resumeId?: string, live?: boolean, processGeneration?: number }|null)} deps.getSessionInfo
86
+ * @param {(id: string) => (object|null)} deps.getSession the live session object (model_id/model_provider/model_source/model_observed_at)
87
+ * @param {(opts: { bin: string, env: object }) => Promise<object|null>} deps.opencodeVersion
88
+ * @param {(opts: { bin: string, env: object }) => Promise<object[]>} deps.listModels `opencodeCatalog`
89
+ * @param {(id: string, opts: object) => Promise<object>} deps.park the runtime's parkSession lambda
90
+ * @param {(id: string, opts: object) => Promise<boolean>} deps.restore the runtime's restore lambda
91
+ * @param {(id: string) => Promise<boolean>} deps.isIdle
92
+ * @param {(frame: object) => void} deps.broadcast
93
+ * @param {(agent: string, routeId: string, modelId: string, sessionId: string) => Promise<object>} [deps.resolveLeadRoute]
94
+ * the runtime's shared routed-target resolver (the SAME lambda the journaled machine calls): it logs
95
+ * refusals and returns `{ ok:true, display, contextWindow, deployment, profileFingerprint, … }` or
96
+ * `{ ok:false, reason, message }`.
97
+ * @param {(sessionId: string) => (object|null)} [deps.getLeadRoute] the session's stored lead-route row, or null.
98
+ * @param {(sessionId: string, agent: string, route: object|null) => Promise<void>} [deps.persistLeadRoute]
99
+ * writes (route) or clears (null) the session's lead-route row — the journaled machine's own lambda.
100
+ * @param {(id: string, kind: string, detail: object) => void} [deps.diagnostic]
101
+ * @param {() => number} [deps.now]
102
+ * @param {() => string} [deps.newId]
103
+ */
104
+ function createOpencodeLaunchModel({
105
+ getSessionInfo, getSession, opencodeVersion, listModels,
106
+ park, restore, isIdle, broadcast, diagnostic = null,
107
+ resolveLeadRoute = null, getLeadRoute = null, persistLeadRoute = null,
108
+ now = Date.now, newId = () => crypto.randomUUID(),
109
+ } = {}) {
110
+ // One entry per session: a later request for the same tab REPLACES the previous one (terminal or
111
+ // not), so the map is bounded by the number of live OpenCode tabs.
112
+ const entries = new Map();
113
+
114
+ // The session's launch env, when the observation pipeline captured one. The version probe must ask
115
+ // the SAME `opencode` the tab launched -- a PATH-only lookup could answer for a different install.
116
+ function launchEnvFor(sessionId) {
117
+ const session = getSession(String(sessionId || ''));
118
+ const overrides = session && session._launchEnvOverrides;
119
+ if (overrides && typeof overrides === 'object' && !Array.isArray(overrides)) {
120
+ return { ...process.env, ...overrides };
121
+ }
122
+ return process.env;
123
+ }
124
+
125
+ async function readVersion(sessionId) {
126
+ try { return await opencodeVersion({ bin: 'opencode', env: launchEnvFor(sessionId) }); }
127
+ catch (error) {
128
+ void error; /* REPORT: an unusable probe is "unknown version", which is read-only (R-1) */
129
+ return null;
130
+ }
131
+ }
132
+
133
+ // The session's stored lead-route row, or null. Optional dep: a harness without it simply never has a
134
+ // route (the pre-WP-3 behaviour), and a throwing reader is treated as "no route" rather than a crash.
135
+ function readLeadRoute(sessionId) {
136
+ if (typeof getLeadRoute !== 'function') return null;
137
+ try { return getLeadRoute(String(sessionId || '')) || null; }
138
+ catch (error) { void error; return null; }
139
+ }
140
+
141
+ /**
142
+ * How this tab may switch, decided per launch from the installed binary (R-1), never guessed.
143
+ * @returns {Promise<{ line: 'v1'|'v2'|null, switch_mode: 'fresh_relaunch'|'resume_relaunch'|'read_only', reason: string, process_generation?: number }>}
144
+ */
145
+ async function sessionMode(sessionId) {
146
+ const info = getSessionInfo(String(sessionId || ''));
147
+ if (!info || info.agent !== 'opencode') return { line: null, switch_mode: 'read_only', reason: 'opencode_version_unknown' };
148
+ const version = await readVersion(sessionId);
149
+ if (!version || (version.line !== 'v1' && version.line !== 'v2')) {
150
+ return { line: null, switch_mode: 'read_only', reason: 'opencode_version_unknown' };
151
+ }
152
+ // `process_generation` is the CAS token the client echoes as `expected_process_generation` (R-3).
153
+ const processGeneration = Number(info.processGeneration || 0);
154
+ // R-4/R-5: a live 2.x tab switches by the SAME park + resume relaunch Claude/Codex and 1.x use. The
155
+ // target rides the launch's inline config `model` + plugin `switch_model` (WP-2/WP-4), never a flag:
156
+ // the 2.x TUI rejects `--model`, so nothing may reach a v2 argv. Without a resume id there is no
157
+ // conversation to continue, so the relaunch is fresh (the config model seeds it).
158
+ if (version.line === 'v2') {
159
+ return { line: 'v2', switch_mode: info.resumeId ? 'resume_relaunch' : 'fresh_relaunch', reason: '', process_generation: processGeneration };
160
+ }
161
+ // R-9: a prompted v1 tab switches by park + resume relaunch, with the model carrier riding that one
162
+ // relaunch (the TUI keeps sending its recorded model, and the carrier rewrites it). A native pick
163
+ // rides `launchModelOverride`; a routed pick rides `launchLeadRoute` (the fixed ref ctm-lead/lead).
164
+ if (info.resumeId) return { line: 'v1', switch_mode: 'resume_relaunch', reason: '', process_generation: processGeneration };
165
+ return { line: 'v1', switch_mode: 'fresh_relaunch', reason: '', process_generation: processGeneration };
166
+ }
167
+
168
+ function frame(entry) {
169
+ return { type: 'lead-model-request', request: publicSessionModelRequest(entry.req) };
170
+ }
171
+
172
+ function report(entry) {
173
+ if (typeof diagnostic !== 'function') return;
174
+ try {
175
+ diagnostic(entry.req.ctm_session_id, `opencode-launch-model-${entry.req.state}`, {
176
+ client_request_id: entry.req.client_request_id, model_id: entry.req.model_id,
177
+ reason: entry.req.reason, detail: entry.req.detail,
178
+ });
179
+ } catch { /* expected: a diagnostic sink failure must not change the switch verdict */ }
180
+ }
181
+
182
+ // A refusal that PRE-DATES the park: nothing was started, so the placeholder entry must not
183
+ // survive as a request the client would later read back as its own.
184
+ function withdraw(entry) {
185
+ if (entry && entry.controller) entry.controller.abort();
186
+ if (entry && entries.get(entry.req.ctm_session_id) === entry) entries.delete(entry.req.ctm_session_id);
187
+ }
188
+
189
+ function failEntry(entry, reason, detail) {
190
+ entry.req.state = 'failed';
191
+ entry.req.phase = 'failed';
192
+ entry.req.reason = reason;
193
+ entry.req.detail = String(detail || '').slice(0, 200);
194
+ entry.req.cancellable = false;
195
+ entry.req.settled_at = now();
196
+ report(entry);
197
+ broadcast(frame(entry));
198
+ }
199
+
200
+ /**
201
+ * Settle an `awaiting_turn` request from the session's own observation fields (R-7). Only an
202
+ * `opencode:message` observation NEWER than the relaunch counts; anything else leaves it waiting.
203
+ */
204
+ function settle(entry) {
205
+ if (!entry || entry.req.state !== 'awaiting_turn') return;
206
+ const session = getSession(entry.req.ctm_session_id);
207
+ if (!session) return;
208
+ if (String(session.model_source || '') !== 'opencode:message') return;
209
+ const at = Number(session.model_observed_at || 0);
210
+ if (!Number.isFinite(at) || at <= Number(entry.req.relaunched_at || 0)) return;
211
+ const observed = observedModelValue(session);
212
+ entry.req.observed_model = observed || null;
213
+ entry.req.observed_source = 'opencode:message';
214
+ entry.req.cancellable = false;
215
+ entry.req.settled_at = now();
216
+ // A routed target launches the fixed ref `ctm-lead/lead` (the gateway provider's alias), NOT the
217
+ // route's model id, so a routed request settles on the ref. Every other target settles on its own id.
218
+ const expected = entry.expectedObserved || entry.req.model_id;
219
+ if (observed && observed === expected) {
220
+ entry.req.state = 'succeeded';
221
+ entry.req.phase = 'succeeded';
222
+ entry.req.reason = null;
223
+ entry.req.detail = '';
224
+ } else {
225
+ entry.req.state = 'failed';
226
+ entry.req.phase = 'failed';
227
+ entry.req.reason = 'observed_model_mismatch';
228
+ entry.req.detail = '';
229
+ }
230
+ report(entry);
231
+ broadcast(frame(entry));
232
+ }
233
+
234
+ /**
235
+ * A never-prompted OpenCode 1.x switch request. Validations run in the design's order, and the
236
+ * entry is installed SYNCHRONOUSLY once the cheap refusals are out of the way, so two concurrent
237
+ * requests can never both reach `park` (exactly one park for N concurrent requests).
238
+ * @returns {Promise<{ ok:true, request: object }|{ ok:false, status:number, code:string }>}
239
+ */
240
+ async function request(sessionId, body) {
241
+ const id = String(sessionId || '');
242
+ const info = getSessionInfo(id);
243
+ // 1. This controller is only ever reached for an OpenCode tab; anything else is the runtime's
244
+ // routing bug, and the honest answer is the same 404 the router would have produced.
245
+ if (!info || info.agent !== 'opencode') return refusal(404, SESSION_NOT_FOUND);
246
+ const input = body && typeof body === 'object' && !Array.isArray(body) ? body : {};
247
+ // 2. A default-scope write is not what this kind offers; a missing/unknown scope is invalid.
248
+ if (input.scope === 'session+default') return refusal(400, 'lead_default_unsupported', { agent: 'opencode' });
249
+ if (input.scope !== 'session') return refusal(400, 'invalid_scope');
250
+ // 3. The distinct request kind (R-3): a launch-time model, never an in-place switch.
251
+ if (input.request_kind !== 'launch_model') return refusal(400, 'invalid_request_kind');
252
+ // 4. Bounded target + idempotency key.
253
+ const modelId = input.model_id;
254
+ if (typeof modelId !== 'string' || !modelId.length || modelId.length > MAX_MODEL_ID_LENGTH) {
255
+ return refusal(400, 'invalid_model_id');
256
+ }
257
+ // 4b. An OPTIONAL gateway route id (WP-3). Present = a ROUTED target: the pick names one of Wall-E's
258
+ // shared routed rows, proven by the same resolver the journaled machine uses, never the
259
+ // `opencode models` catalog (a gateway model is not in the CLI's own list).
260
+ const routeId = input.route_id;
261
+ if (routeId != null && (typeof routeId !== 'string' || !routeId.length || routeId.length > MAX_ROUTE_ID_LENGTH)) {
262
+ return refusal(400, 'invalid_route_id');
263
+ }
264
+ const routed = typeof routeId === 'string' && routeId.length > 0;
265
+ const clientRequestId = input.client_request_id;
266
+ if (typeof clientRequestId !== 'string' || !clientRequestId.length
267
+ || clientRequestId.length > MAX_CLIENT_REQUEST_ID_LENGTH) {
268
+ return refusal(400, 'invalid_request');
269
+ }
270
+ // 5. Capability of the INSTALLED binary (R-1, R-4, R-5, R-8, R-9). A prompted v1 (R-9) or v2 (R-4)
271
+ // tab switches by the SAME park + resume relaunch: its model rides the launch's carrier — the 1.x
272
+ // plugin env, injected by the spawn path from `launchModelOverride` / `launchLeadRoute` — or the
273
+ // 2.x inline config + plugin `switch_model`, native and routed alike. Only an unreadable binary
274
+ // has nothing to offer.
275
+ const storedRoute = readLeadRoute(id);
276
+ const mode = await sessionMode(id);
277
+ if (mode.switch_mode === 'read_only') return refusal(409, 'lead_switch_unsupported', { reason: mode.reason });
278
+ // 6. A parked/deferred tab has no process to relaunch.
279
+ if (!info.live) return refusal(409, 'lead_switch_unsupported', { reason: 'not_live' });
280
+ // 7. The process-generation CAS (R-3): the client's generation must still be the running one.
281
+ if (String(input.expected_process_generation) !== String(info.processGeneration)) {
282
+ return refusal(409, 'model_owner_changed');
283
+ }
284
+ // 8. Idempotent replay of the SAME request, or a switch already in flight.
285
+ const existing = entries.get(id);
286
+ if (existing && existing.req.client_request_id === clientRequestId) {
287
+ return { ok: true, request: publicSessionModelRequest(existing.req) };
288
+ }
289
+ if (existing && existing.req.state === FLIGHT_KEY) return refusal(409, 'lead_switch_in_flight');
290
+
291
+ const entry = {
292
+ req: {
293
+ id: newId(), ctm_session_id: id, client_request_id: clientRequestId, agent: 'opencode',
294
+ model_id: modelId, effort: null, effort_only: false, scope: 'session',
295
+ ...(routed ? { route_id: routeId } : {}),
296
+ state: FLIGHT_KEY, phase: FLIGHT_KEY, reason: null, detail: '', cancellable: false,
297
+ requested_at: now(), relaunched_at: null, settled_at: null,
298
+ observed_model: null, observed_source: null,
299
+ },
300
+ // A routed launch runs the fixed ref `ctm-lead/lead`, so its observation settles on the REF, not
301
+ // the route's model id (R-7). Private to the entry — never in the public request shape.
302
+ expectedObserved: routed ? OPENCODE_LEAD_MODEL_REF : modelId,
303
+ // The shared routed resolver requires an AbortSignal (its profile step throws "AbortSignal
304
+ // required" without one -- observed live, opencode-followups WP-6); a pre-park withdraw aborts it.
305
+ controller: new AbortController(),
306
+ };
307
+ // Installed SYNCHRONOUSLY, BEFORE the catalog/idle awaits below: a second request arriving while
308
+ // this one is between validation and park must see it (and be told `lead_switch_in_flight`).
309
+ // It is NOT broadcast yet: a refusal below withdraws it silently, so no viewer may have seen it.
310
+ entries.set(id, entry);
311
+
312
+ // 9. Target proof. A ROUTED target is proven by the SHARED resolver (existence / enabled / routable /
313
+ // servable-for-this-agent + the model in Wall-E's coding catalog) — never the CLI's own list. A
314
+ // NATIVE target is proven by the `opencode models` catalog (R-5): the CLI stores anything, so the
315
+ // pick is what proves the id is real. An unreadable catalog is its own typed refusal, not an empty 400.
316
+ let resolved = null;
317
+ if (routed) {
318
+ if (typeof resolveLeadRoute !== 'function') {
319
+ withdraw(entry);
320
+ return refusal(400, 'lead_route_invalid', { reason: 'route_invalid', message: '' });
321
+ }
322
+ let v;
323
+ try { v = await resolveLeadRoute('opencode', routeId, modelId, id, { signal: entry.controller.signal }); }
324
+ catch (error) { v = { ok: false, reason: 'route_invalid', message: String(error && error.message || error).slice(0, 200) }; }
325
+ if (entries.get(id) !== entry) return refusal(409, 'lead_switch_in_flight');
326
+ if (!v || !v.ok) {
327
+ withdraw(entry);
328
+ return refusal(400, 'lead_route_invalid', { reason: (v && v.reason) || 'route_invalid', message: (v && v.message) || '' });
329
+ }
330
+ resolved = v;
331
+ } else {
332
+ let rows;
333
+ try {
334
+ rows = await listModels({ bin: 'opencode', env: launchEnvFor(id) });
335
+ } catch (error) {
336
+ void error; /* REPORT: the refusal below names the code; the cause never reaches the client */
337
+ withdraw(entry);
338
+ return refusal(503, 'opencode_catalog_unavailable');
339
+ }
340
+ if (entries.get(id) !== entry) return refusal(409, 'lead_switch_in_flight');
341
+ if (!Array.isArray(rows) || !rows.some((row) => String(row && row.value) === modelId)) {
342
+ withdraw(entry);
343
+ return refusal(400, 'lead_model_not_switchable', { model_id: modelId });
344
+ }
345
+ }
346
+ // 10. Relaunching WOULD interrupt a busy tab, so the same idle verdict Claude/Codex use applies.
347
+ const idle = await isIdle(id);
348
+ if (entries.get(id) !== entry) return refusal(409, 'lead_switch_in_flight');
349
+ if (idle !== true) {
350
+ withdraw(entry);
351
+ return refusal(409, 'lead_switch_busy');
352
+ }
353
+
354
+ // R-2/R-6: park, then relaunch with the model on the ONE launch's argv. No user config is written.
355
+ // A THROWN park/restore is a refusal too: leaving the entry in `relaunching` would block this tab
356
+ // forever (the in-flight guard), so every exit below is a terminal state.
357
+ let parked;
358
+ try { parked = await park(id, { manual: true, reason: 'model' }); }
359
+ catch (error) {
360
+ const code = String((error && error.message) || error || 'park_failed').slice(0, 200);
361
+ failEntry(entry, 'park_refused', code);
362
+ return refusal(409, 'lead_switch_park_failed', { park_code: 'park_threw' });
363
+ }
364
+ if (!parked || parked.ok === false) {
365
+ const code = String((parked && (parked.code || parked.reason)) || 'park_failed');
366
+ const protections = Array.isArray(parked && parked.protections) ? parked.protections : [];
367
+ if (TRANSIENT_PARK_CODES.has(code) && !protections.some((p) => NON_TRANSIENT_PROTECTIONS.has(p))) {
368
+ // Nothing was stopped: the tab is busy or inside CTM's post-boot restore window. Withdraw the
369
+ // never-broadcast entry and answer "busy", with the time the window ends when park knows it.
370
+ withdraw(entry);
371
+ const retryAt = Number(parked && parked.retryAt);
372
+ return refusal(409, 'lead_switch_busy', { park_code: code, protections,
373
+ ...(Number.isFinite(retryAt) && retryAt > 0 ? { retry_at: retryAt } : {}) });
374
+ }
375
+ failEntry(entry, 'park_refused', code);
376
+ // `code` is the refusal's own identity; the park's reason rides `park_code` (the same field
377
+ // name the journaled machine records), so neither can overwrite the other.
378
+ return refusal(409, 'lead_switch_park_failed', { park_code: code });
379
+ }
380
+ if (routed) entry.req.route_display = resolved.display || '';
381
+ let started;
382
+ // A NATIVE pick relaunches with `--model <id>`; a ROUTED pick hands the spawn path a `launchLeadRoute`
383
+ // object (gateway ref + env + configPatch), exactly as a routed Claude/Codex lead does. A native pick
384
+ // on a tab that HAS a stored route passes an EXPLICIT `launchLeadRoute: null`, so the spawn path clears
385
+ // the row instead of re-applying the route the user just left.
386
+ const restoreOpts = routed
387
+ ? { launchLeadRoute: {
388
+ routeId, model: modelId,
389
+ display: resolved.display || modelId,
390
+ contextWindow: resolved.contextWindow != null ? resolved.contextWindow : (resolved.deployment?.contextTokens ?? null),
391
+ } }
392
+ : (storedRoute ? { launchModelOverride: modelId, launchLeadRoute: null } : { launchModelOverride: modelId });
393
+ try { started = await restore(id, restoreOpts); }
394
+ catch (error) {
395
+ void error; /* REPORT: the failure path below records the terminal state */
396
+ started = false;
397
+ }
398
+ // The tab is parked: from here on every exit is a broadcast state, so the first frame viewers see
399
+ // is one that will be followed by its outcome (a refusal before this point broadcasts nothing).
400
+ broadcast(frame(entry));
401
+ if (started !== true) {
402
+ failEntry(entry, 'restore_failed', 'restore_refused');
403
+ return refusal(409, 'lead_switch_restore_failed');
404
+ }
405
+ entry.req.state = 'awaiting_turn';
406
+ entry.req.phase = 'awaiting_turn';
407
+ entry.req.relaunched_at = now();
408
+ entry.req.cancellable = true;
409
+ // The session's lead-route row follows the switch (WP-3), so the NEXT resume re-applies it: a routed
410
+ // target writes the row (the same field set the journaled machine persists) and a native target on a
411
+ // routed tab clears it. A write failure is REPORTED via `diagnostic` and never fails the switch — the
412
+ // relaunched process already carries the target.
413
+ if ((routed || storedRoute) && typeof persistLeadRoute === 'function') {
414
+ try {
415
+ if (routed) {
416
+ await persistLeadRoute(id, leadWireAgent('opencode'), {
417
+ routeId, model: modelId, display: resolved.display || modelId,
418
+ contextWindow: resolved.contextWindow != null ? resolved.contextWindow : (resolved.deployment?.contextTokens ?? null),
419
+ profileFingerprint: resolved.profileFingerprint || null,
420
+ factsRevision: resolved.deployment?.factsRevision || null,
421
+ processGeneration: resolved.processGeneration || null,
422
+ transitionId: null,
423
+ });
424
+ } else {
425
+ await persistLeadRoute(id, leadWireAgent('opencode'), null);
426
+ }
427
+ } catch (error) {
428
+ if (typeof diagnostic === 'function') {
429
+ try { diagnostic(id, 'opencode-launch-model-persist-route-failed', { message: String((error && error.message) || error).slice(0, 200) }); }
430
+ catch { /* expected: a diagnostic sink failure must not change the switch verdict */ }
431
+ }
432
+ }
433
+ }
434
+ broadcast(frame(entry));
435
+ return { ok: true, request: publicSessionModelRequest(entry.req) };
436
+ }
437
+
438
+ /** The public request for this tab, settled lazily (the poll path), or null. */
439
+ function get(sessionId) {
440
+ const entry = entries.get(String(sessionId || ''));
441
+ if (!entry) return null;
442
+ if (entry.req.state === 'awaiting_turn') settle(entry);
443
+ return publicSessionModelRequest(entry.req);
444
+ }
445
+
446
+ /** Cancelling an unconfirmed switch abandons the REQUEST; the relaunched process keeps running. */
447
+ function cancel(sessionId) {
448
+ const entry = entries.get(String(sessionId || ''));
449
+ if (!entry || entry.req.state !== 'awaiting_turn') {
450
+ return { ok: false, status: 409, code: 'lead_switch_not_cancellable' };
451
+ }
452
+ entry.req.state = 'cancelled';
453
+ entry.req.phase = 'cancelled';
454
+ entry.req.cancellable = false;
455
+ entry.req.settled_at = now();
456
+ broadcast(frame(entry));
457
+ return { ok: true, request: publicSessionModelRequest(entry.req) };
458
+ }
459
+
460
+ /**
461
+ * A model observation for this tab arrived (the runtime calls this on a status edge). Settles an
462
+ * `awaiting_turn` request from the session's own fields, so a switch completes without a poll.
463
+ */
464
+ function notifyObservation(sessionId) {
465
+ settle(entries.get(String(sessionId || '')));
466
+ }
467
+
468
+ return { request, get, cancel, sessionMode, notifyObservation };
469
+ }
470
+
471
+ module.exports = { createOpencodeLaunchModel, observedModelValue };
@@ -0,0 +1,35 @@
1
+ // WP-5 (docs/opencode-v2-model-switch-standalone-plugin-2026-10-30.html §3.5 P-V2..P-V5, §5.1 R-9,
2
+ // §5.2 D-9/D-10/D-11, §6.7): the OpenCode 1.x model carrier.
3
+ //
4
+ // A resumed 1.x TUI re-sends the model its conversation recorded on every prompt (the TUI syncs its
5
+ // model from the last user message once per session, prompt/index.tsx:311-331), so `--model` on a
6
+ // resume is ignored (round-1 P-7, upstream #53806). A SERVER plugin's `chat.message` hook can rewrite
7
+ // `output.message.model` before the request is made, so this file is what carries a prompted tab's
8
+ // switch: CTM adds this one file to the launch's inline `plugin` array and sets the target ref and the
9
+ // resumed session id in the spawn env (`CTM_OPENCODE_SWITCH_MODEL`, `CTM_OPENCODE_SWITCH_SESSION`).
10
+ //
11
+ // The rewrite is NOT unconditional (P-V3/P-V4): the first model seen on the target session is `from`,
12
+ // and only a prompt still arriving on `from` is rewritten. Any other model is a pick the user made in
13
+ // the TUI itself, which wins — the carrier disarms for good rather than fighting it.
14
+ //
15
+ // Loaded by OpenCode 1.x under Bun: ESM, no imports. It NEVER throws — a carrier failure leaves the
16
+ // prompt on its own model (the chip then never settles, R-5), which is strictly better than failing a
17
+ // user's turn.
18
+ export const CtmModelCarrier = async () => {
19
+ const target = process.env.CTM_OPENCODE_SWITCH_MODEL || ''; // 'provider/model' (first '/' splits)
20
+ const sessionID = process.env.CTM_OPENCODE_SWITCH_SESSION || '';
21
+ const cut = target.indexOf('/');
22
+ if (!sessionID || cut <= 0 || cut === target.length - 1) return {};
23
+ const providerID = target.slice(0, cut), modelID = target.slice(cut + 1);
24
+ let from = null, off = false;
25
+ return { 'chat.message': async (input, output) => {
26
+ try {
27
+ const m = output && output.message && output.message.model;
28
+ if (off || !m || !input || input.sessionID !== sessionID) return;
29
+ const key = m.providerID + '/' + m.modelID;
30
+ if (from === null) from = key;
31
+ if (key !== from) { off = true; return; } // the user picked in the TUI (P-V4)
32
+ output.message.model = { ...m, providerID, modelID };
33
+ } catch { /* expected: a carrier failure leaves the prompt on its own model; the chip never settles (R-5) */ }
34
+ } };
35
+ };
@@ -0,0 +1,118 @@
1
+ 'use strict';
2
+ // WP-2 (docs/opencode-v2-model-switch-standalone-plugin-2026-10-30.html §3.2 P-S7/P-S8, §5.1 R-3/R-4,
3
+ // §5.2 D-12/D-13, §6.3 revision 3): the CTM OpenCode 2.x plugin. OpenCode 2.x loads a `plugins` entry
4
+ // by DIRECTORY (a single file path is skipped, P-S7) and requires this module's `server.js`; the entry
5
+ // shape that works is CommonJS (`module.exports` + `.default`, P-S3b), so this file is CommonJS even
6
+ // though 1.x loads its plugin as ESM.
7
+ //
8
+ // It runs INSIDE OpenCode's private per-tab server (R-1), so it never requires a CTM module and never
9
+ // throws: no imports, never awaits a post, aborts a request after 1500 ms, and is inert when the spawn
10
+ // path did not pass a port or a session id (the 1.x plugin rules, lib/agent-hooks-opencode-plugin.js:6-8,
11
+ // 30-40). Identity comes from ctx.options, filled by the spawn path from the SAME spawn env values the
12
+ // 1.x plugin reads (CTM_PORT / CTM_SESSION_ID / CTM_MODEL_PROCESS_GENERATION).
13
+ //
14
+ // Three jobs (R-3):
15
+ // 1. switch_model, once, in setup, BEFORE subscribing (D-12): ctx.session.switchModel({ sessionID,
16
+ // model: { providerID, id } }) applies the target to a session that already exists (P-S8c). On a
17
+ // never-prompted tab the call rejects Session.NotFoundError (P-S8a) -- the inline config model
18
+ // applies instead, so this is the expected fresh-tab case and stays silent. Any OTHER rejection is
19
+ // one line to stderr. The outcome is never posted: the next completed turn's model is the truth.
20
+ // 2. turn state (D-13): only events whose sessionID is THIS tab's session id, one post per transition
21
+ // -- session.execution.started -> 'start' (busy once), any other session.execution.* -> 'stop'
22
+ // (the first end after a start). A child's execution events are ignored by construction (the
23
+ // session id check below), so a subagent's turn never posts start/stop for the tab.
24
+ // Routes /hook/opencode/start and /hook/opencode/stop are the 1.x ingress routes, unchanged.
25
+ // 3. permission.asked -> 'waiting', permission.replied -> 'resumed' (OQ-2, cited @opencode/schema 2.2.5;
26
+ // WP-3b, OQ-5): 2.x has no permission.updated (D-6). Waiting is a status report only -- it does not
27
+ // touch the busy flag, so the turn-state transitions above are unchanged. Two pieces of state make
28
+ // the report exact:
29
+ // - children: a subagent (task tool) runs in its OWN session, and P-S11 observed on 2.2.4 that its
30
+ // ask carries the CHILD session id while its session.created carries data.parentID = the parent's
31
+ // id (both at the top level of data). So session.created builds a Set of descendant session ids
32
+ // (parentID is the tab's own id OR an id already in the set -- grandchildren included) BEFORE the
33
+ // own-session filter would drop them, and a permission event is accepted when its sessionID is
34
+ // the tab's own id or in that set. Without this a blocked subagent never shows "Needs you".
35
+ // - pending: the set of open ask ids (permission.asked.data.id). 'waiting' posts only on the
36
+ // empty -> non-empty edge and 'resumed' only when the set is empty again after removing
37
+ // permission.replied.data.requestID, so overlapping asks from the tab and its children yield one
38
+ // waiting/resumed pair; a requestID that was never seen removes nothing and the empty check
39
+ // still holds (the WP-3 reply-without-ask case).
40
+ // Routes /hook/opencode/waiting and /hook/opencode/resumed are the 1.x ingress routes
41
+ // (lib/agent-hooks-opencode-plugin.js:56-57), unchanged.
42
+
43
+ // post(route): best effort, never awaited, never throws (a slow CTM must not stall the agent).
44
+ function makePost(o, ctx) {
45
+ return (route) => {
46
+ try {
47
+ fetch('http://127.0.0.1:' + o.ctm_port + '/hook/opencode/' + route, {
48
+ method: 'POST',
49
+ headers: { 'content-type': 'application/json' },
50
+ body: JSON.stringify({
51
+ ctm_id: o.ctm_session_id,
52
+ process_generation: o.process_generation || undefined,
53
+ cwd: ctx.location && ctx.location.directory,
54
+ session_id: o.session_id,
55
+ }),
56
+ signal: AbortSignal.timeout(1500),
57
+ }).catch(() => { /* expected: a status report is best effort; a slow CTM must not stall the agent */ });
58
+ } catch (e) { /* expected: a status report is best effort; a slow CTM must not stall the agent */ }
59
+ };
60
+ }
61
+
62
+ async function setup(ctx) {
63
+ const o = (ctx && ctx.options) || {};
64
+ // 1. switch_model, once, before subscribing (P-S8c).
65
+ const sw = o.switch_model;
66
+ if (sw && typeof sw === 'object') {
67
+ try {
68
+ await ctx.session.switchModel(sw);
69
+ } catch (e) {
70
+ // P-S8a: a never-prompted tab rejects Session.NotFoundError and the config model applies instead.
71
+ if (!e || e._tag !== 'Session.NotFoundError') {
72
+ process.stderr.write('[ctm.status] switchModel failed: ' + String((e && e.message) || e) + '\n');
73
+ }
74
+ }
75
+ }
76
+ // 2. status: started by setup, never awaited by it; a missing port/id leaves the plugin inert.
77
+ if (!o.ctm_port || !o.ctm_session_id) return;
78
+ const post = makePost(o, ctx);
79
+ void (async () => {
80
+ try {
81
+ let busy = false;
82
+ const children = new Set(); // child (subagent) session ids under this tab, grandchildren included
83
+ const pending = new Set(); // open permission-ask ids (the tab's own and its children's)
84
+ for await (const ev of ctx.event.subscribe()) {
85
+ const data = ev && ev.data;
86
+ if (!data) continue;
87
+ const type = ev.type;
88
+ const sid = data.sessionID;
89
+ if (type === 'session.created') {
90
+ // P-S11 observed on 2.2.4: a child ask carries the child session id, and the child's
91
+ // session.created carries data.parentID = the parent's id. Track descendants BEFORE the
92
+ // own-session filter, which would otherwise drop the event (it is not the tab's own id).
93
+ if (data.parentID && sid && (data.parentID === o.session_id || children.has(data.parentID))) children.add(sid);
94
+ continue;
95
+ }
96
+ if (sid !== o.session_id && !children.has(sid)) continue;
97
+ if (type === 'session.execution.started') {
98
+ // Turn state is the tab's OWN session only: a child's execution events never post start/stop.
99
+ if (sid === o.session_id && !busy) { busy = true; post('start'); }
100
+ } else if (type === 'permission.asked') {
101
+ if (pending.size === 0) post('waiting'); // D-6: 2.x has no permission.updated; one pair per overlap
102
+ pending.add(data.id);
103
+ } else if (type === 'permission.replied') {
104
+ pending.delete(data.requestID); // unknown id: nothing to remove, the empty check below still holds
105
+ if (pending.size === 0) post('resumed');
106
+ } else if (typeof type === 'string' && type.startsWith('session.execution.')) {
107
+ // A turn end closes any ask left open (a reject/abort path that emits no permission.replied is
108
+ // unmeasured), so the NEXT turn's first ask posts 'waiting' again.
109
+ if (sid === o.session_id && busy) { busy = false; pending.clear(); post('stop'); }
110
+ }
111
+ }
112
+ } catch (e) { /* expected: the status loop is best effort; a broken stream never breaks the agent */ }
113
+ })();
114
+ }
115
+
116
+ const def = { id: 'ctm.status', setup };
117
+ module.exports = def;
118
+ module.exports.default = def;
@@ -3464,7 +3464,7 @@
3464
3464
  ],
3465
3465
  [
3466
3466
  "POST",
3467
- "/api/session/image-refs",
3467
+ "/api/sessions/:p1/image-refs",
3468
3468
  "respond",
3469
3469
  "human",
3470
3470
  false