create-walle 0.10.1 → 0.10.2

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 (341) hide show
  1. package/README.md +4 -0
  2. package/lib/owner-write-safety.js +135 -0
  3. package/lib/public-runtime.js +13 -5
  4. package/lib/release-schema.json +3 -2
  5. package/lib/runtime-closure-policy.json +6 -4
  6. package/package.json +1 -1
  7. package/template/bin/deps-doctor.js +1 -1
  8. package/template/bin/node-bin.sh +11 -0
  9. package/template/coding-task-manager/api-prompts.js +10 -22
  10. package/template/coding-task-manager/bin/claude-hook.js +7 -3
  11. package/template/coding-task-manager/bin/claude-table-parity.js +307 -0
  12. package/template/coding-task-manager/bin/conversation-storage-auto-migration-lib.js +16 -1
  13. package/template/coding-task-manager/bin/conversation-storage-generation-evidence.js +9 -2
  14. package/template/coding-task-manager/bin/conversation-storage-service-entry-lib.js +16 -1
  15. package/template/coding-task-manager/bin/conversation-storage-walle-owner.js +247 -0
  16. package/template/coding-task-manager/bin/ctm-machine-pair.js +160 -0
  17. package/template/coding-task-manager/bin/lead-wait-census.js +1348 -0
  18. package/template/coding-task-manager/bin/repair-foreign-clone.js +564 -0
  19. package/template/coding-task-manager/bin/repair-resume-echo-titles.js +248 -0
  20. package/template/coding-task-manager/bin/write-lock-bench.js +111 -0
  21. package/template/coding-task-manager/db.js +705 -286
  22. package/template/coding-task-manager/git-utils.js +44 -0
  23. package/template/coding-task-manager/lib/adopted-host-redial.js +4 -0
  24. package/template/coding-task-manager/lib/agent-presets.js +15 -0
  25. package/template/coding-task-manager/lib/agent-title-apply.js +88 -0
  26. package/template/coding-task-manager/lib/agent-title-backfill-read.js +92 -0
  27. package/template/coding-task-manager/lib/api/host-stats.js +98 -0
  28. package/template/coding-task-manager/lib/api/ingress.js +35 -12
  29. package/template/coding-task-manager/lib/api/machine-pairing.js +285 -0
  30. package/template/coding-task-manager/lib/api/machine-pool.js +307 -0
  31. package/template/coding-task-manager/lib/api/machines.js +461 -0
  32. package/template/coding-task-manager/lib/api/misc-ops.js +20 -5
  33. package/template/coding-task-manager/lib/api/providers.js +53 -6
  34. package/template/coding-task-manager/lib/api/resume-from-store.js +25 -0
  35. package/template/coding-task-manager/lib/api/sessions.js +160 -8
  36. package/template/coding-task-manager/lib/api/transcript-history.js +17 -4
  37. package/template/coding-task-manager/lib/api/worktrees.js +38 -37
  38. package/template/coding-task-manager/lib/approval-drift.js +16 -1
  39. package/template/coding-task-manager/lib/approval-logical-frame.js +5 -1
  40. package/template/coding-task-manager/lib/approval-scope.js +68 -4
  41. package/template/coding-task-manager/lib/auth-context.js +32 -2
  42. package/template/coding-task-manager/lib/auth-rate-limit.js +41 -1
  43. package/template/coding-task-manager/lib/auth-rules.js +186 -45
  44. package/template/coding-task-manager/lib/broadcast-serialize.js +2 -1
  45. package/template/coding-task-manager/lib/browser-session-broker.js +9 -1
  46. package/template/coding-task-manager/lib/browser-use-adapter.js +33 -2
  47. package/template/coding-task-manager/lib/claude-markdown-ansi.js +27 -85
  48. package/template/coding-task-manager/lib/claude-session-title.js +6 -30
  49. package/template/coding-task-manager/lib/claude-table-layout.js +322 -0
  50. package/template/coding-task-manager/lib/client-diag-handler.js +5 -0
  51. package/template/coding-task-manager/lib/codex-config-guard.js +33 -3
  52. package/template/coding-task-manager/lib/codex-harness-qualification.js +12 -0
  53. package/template/coding-task-manager/lib/codex-launch-health.js +29 -2
  54. package/template/coding-task-manager/lib/codex-link-preparation.js +9 -8
  55. package/template/coding-task-manager/lib/codex-paths.js +2 -1
  56. package/template/coding-task-manager/lib/coding-agent-models.js +21 -30
  57. package/template/coding-task-manager/lib/coding-session-model-adapters.js +27 -2
  58. package/template/coding-task-manager/lib/coding-session-model-projection.js +5 -0
  59. package/template/coding-task-manager/lib/coding-session-model-runtime.js +1 -2
  60. package/template/coding-task-manager/lib/coding-session-model-state.js +10 -0
  61. package/template/coding-task-manager/lib/coding-session-model-sync-state.js +0 -1
  62. package/template/coding-task-manager/lib/conversation-import/cursor-guard-owner.js +10 -0
  63. package/template/coding-task-manager/lib/create/resolve-launch-params.js +8 -1
  64. package/template/coding-task-manager/lib/ctm-boot-lifecycle.js +17 -1
  65. package/template/coding-task-manager/lib/db-domain-tables.js +7 -0
  66. package/template/coding-task-manager/lib/db-domains.js +21 -2
  67. package/template/coding-task-manager/lib/db-owner-op-registry.js +7 -0
  68. package/template/coding-task-manager/lib/db-schema-source-files.js +1 -0
  69. package/template/coding-task-manager/lib/feature-manifest.js +1 -1
  70. package/template/coding-task-manager/lib/fs-watcher.js +3 -2
  71. package/template/coding-task-manager/lib/harness-history-transition.js +3 -3
  72. package/template/coding-task-manager/lib/headless-inflight-ops.js +203 -0
  73. package/template/coding-task-manager/lib/headless-missing-reply.js +42 -0
  74. package/template/coding-task-manager/lib/headless-term-service.js +55 -17
  75. package/template/coding-task-manager/lib/host-control-dial.js +167 -0
  76. package/template/coding-task-manager/lib/host-handoff-orchestrator.js +16 -2
  77. package/template/coding-task-manager/lib/host-handoff.js +51 -13
  78. package/template/coding-task-manager/lib/host-health-ledger.js +164 -12
  79. package/template/coding-task-manager/lib/host-self-truth-watchdog.js +4 -0
  80. package/template/coding-task-manager/lib/host-stats.js +328 -0
  81. package/template/coding-task-manager/lib/host-threadpool-evacuation.js +160 -0
  82. package/template/coding-task-manager/lib/host-threadpool-monitor.js +14 -1
  83. package/template/coding-task-manager/lib/hosted-input-dispatch.js +203 -0
  84. package/template/coding-task-manager/lib/hosted-sync-worker.js +113 -5
  85. package/template/coding-task-manager/lib/instance-id-ownership-manifest.js +4 -0
  86. package/template/coding-task-manager/lib/instance-manifest-runtime.js +3 -0
  87. package/template/coding-task-manager/lib/instance-manifest.js +13 -1
  88. package/template/coding-task-manager/lib/instance-sync-status.js +15 -0
  89. package/template/coding-task-manager/lib/launch-model-override.js +160 -1
  90. package/template/coding-task-manager/lib/launch-plan-worker.js +59 -2
  91. package/template/coding-task-manager/lib/lead-model-switch-runtime.js +57 -4
  92. package/template/coding-task-manager/lib/lead-model-switch.js +3 -2
  93. package/template/coding-task-manager/lib/lead-model-transition-store.js +61 -3
  94. package/template/coding-task-manager/lib/legacy-draft-cancellation-recovery.js +111 -0
  95. package/template/coding-task-manager/lib/login-tripwire.js +1 -0
  96. package/template/coding-task-manager/lib/machine-pairing.js +238 -0
  97. package/template/coding-task-manager/lib/machine-pool.js +409 -0
  98. package/template/coding-task-manager/lib/machine-registry.js +749 -0
  99. package/template/coding-task-manager/lib/machine-self.js +66 -0
  100. package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +52 -4
  101. package/template/coding-task-manager/lib/media-journal-control-reader.js +473 -0
  102. package/template/coding-task-manager/lib/media-journal.js +84 -27
  103. package/template/coding-task-manager/lib/media-store-options.js +43 -15
  104. package/template/coding-task-manager/lib/migrations/v56-host-samples-and-machine-pool.js +76 -0
  105. package/template/coding-task-manager/lib/mirror-feed-sanitize.js +12 -3
  106. package/template/coding-task-manager/lib/model-proxy/anthropic-face.js +4 -0
  107. package/template/coding-task-manager/lib/model-proxy/chat-response-translators.js +3 -0
  108. package/template/coding-task-manager/lib/model-proxy/handoff.js +494 -0
  109. package/template/coding-task-manager/lib/model-proxy/lead-worker-picks.js +25 -0
  110. package/template/coding-task-manager/lib/model-proxy/native-lead-listener.js +61 -7
  111. package/template/coding-task-manager/lib/model-proxy/proxy-cli.js +200 -0
  112. package/template/coding-task-manager/lib/model-proxy/proxy-instance-record.js +257 -0
  113. package/template/coding-task-manager/lib/model-proxy/stream-accounting.js +205 -0
  114. package/template/coding-task-manager/lib/model-proxy/tokens.js +78 -0
  115. package/template/coding-task-manager/lib/model-proxy/upstream-outcome.js +9 -0
  116. package/template/coding-task-manager/lib/native-history-control.js +2 -2
  117. package/template/coding-task-manager/lib/native-lead-settings-client.js +2 -1
  118. package/template/coding-task-manager/lib/native-lead-tls.js +18 -4
  119. package/template/coding-task-manager/lib/oauth-proxy-supervisor.js +628 -64
  120. package/template/coding-task-manager/lib/owner-action-manifest-data.json +14 -0
  121. package/template/coding-task-manager/lib/painted-row-recorder.js +9 -1
  122. package/template/coding-task-manager/lib/prompt-submission-contract.js +21 -1
  123. package/template/coding-task-manager/lib/provider-login-broker.js +192 -9
  124. package/template/coding-task-manager/lib/provider-login-capture.js +153 -0
  125. package/template/coding-task-manager/lib/queue-draft-status-overlay.js +36 -0
  126. package/template/coding-task-manager/lib/raw-codex-history-window.js +5 -3
  127. package/template/coding-task-manager/lib/read-pool-client.js +5 -2
  128. package/template/coding-task-manager/lib/read-pool-op-registry.js +10 -1
  129. package/template/coding-task-manager/lib/recent-catalog-query.js +26 -0
  130. package/template/coding-task-manager/lib/recent-session-catalog-response.js +9 -0
  131. package/template/coding-task-manager/lib/remote-command-context.js +12 -0
  132. package/template/coding-task-manager/lib/remote-command-runtime.js +13 -1
  133. package/template/coding-task-manager/lib/remote-machine-catalog.js +1062 -0
  134. package/template/coding-task-manager/lib/remote-machine-create.js +693 -0
  135. package/template/coding-task-manager/lib/remote-machine-link.js +939 -0
  136. package/template/coding-task-manager/lib/remote-machine-proxy.js +520 -0
  137. package/template/coding-task-manager/lib/remote-session-forward.js +306 -0
  138. package/template/coding-task-manager/lib/remote-submission-status.js +6 -0
  139. package/template/coding-task-manager/lib/restart-outcome-truth.js +20 -3
  140. package/template/coding-task-manager/lib/restart-preflight.js +9 -2
  141. package/template/coding-task-manager/lib/restore-backoff-gate.js +10 -2
  142. package/template/coding-task-manager/lib/restore-prepare-retry.js +12 -5
  143. package/template/coding-task-manager/lib/restore-resume-batch.js +22 -1
  144. package/template/coding-task-manager/lib/resume-blocked-wire.js +62 -1
  145. package/template/coding-task-manager/lib/resume-cwd.js +13 -0
  146. package/template/coding-task-manager/lib/resume-from-store.js +12 -1
  147. package/template/coding-task-manager/lib/resume-origin-facts.js +38 -1
  148. package/template/coding-task-manager/lib/resume-preflight.js +21 -16
  149. package/template/coding-task-manager/lib/review-repo-admission.js +39 -6
  150. package/template/coding-task-manager/lib/review-repo-registry.js +17 -0
  151. package/template/coding-task-manager/lib/root-delivery-runtime.js +72 -15
  152. package/template/coding-task-manager/lib/root-delivery-selection.js +37 -1
  153. package/template/coding-task-manager/lib/root-prompt-admission.js +33 -2
  154. package/template/coding-task-manager/lib/root-prompt-flow.js +167 -33
  155. package/template/coding-task-manager/lib/scrollback-archive-store.js +7 -4
  156. package/template/coding-task-manager/lib/session-analysis-memory.js +2 -0
  157. package/template/coding-task-manager/lib/session-create/codex-native-provider-gate.js +218 -0
  158. package/template/coding-task-manager/lib/session-create/codex-resume-exit-classify.js +52 -0
  159. package/template/coding-task-manager/lib/session-create/delegate-worker.js +95 -6
  160. package/template/coding-task-manager/lib/session-create/identity.js +20 -1
  161. package/template/coding-task-manager/lib/session-create/prepared.js +69 -1
  162. package/template/coding-task-manager/lib/session-create/provider-launch-context.js +77 -0
  163. package/template/coding-task-manager/lib/session-create/provider-process.js +34 -37
  164. package/template/coding-task-manager/lib/session-create/response.js +30 -12
  165. package/template/coding-task-manager/lib/session-create/transcript.js +13 -4
  166. package/template/coding-task-manager/lib/session-create/worker-continuation.js +43 -1
  167. package/template/coding-task-manager/lib/session-create/worker-notices.js +94 -5
  168. package/template/coding-task-manager/lib/session-create/worker-observability.js +99 -7
  169. package/template/coding-task-manager/lib/session-create/worker-stop.js +11 -3
  170. package/template/coding-task-manager/lib/session-create/worker-wait-watcher.js +315 -0
  171. package/template/coding-task-manager/lib/session-geometry-authority-handler.js +10 -1
  172. package/template/coding-task-manager/lib/session-host-manager.js +152 -206
  173. package/template/coding-task-manager/lib/session-jobs.js +17 -9
  174. package/template/coding-task-manager/lib/session-lifecycle.js +18 -0
  175. package/template/coding-task-manager/lib/session-model-action-preconditions.js +9 -0
  176. package/template/coding-task-manager/lib/session-resource-sampler.js +19 -9
  177. package/template/coding-task-manager/lib/session-standup.js +67 -4
  178. package/template/coding-task-manager/lib/session-status-ledger.js +42 -3
  179. package/template/coding-task-manager/lib/session-status-runtime.js +45 -1
  180. package/template/coding-task-manager/lib/session-title-mirror.js +25 -57
  181. package/template/coding-task-manager/lib/session-transcript-trash.js +8 -1
  182. package/template/coding-task-manager/lib/startup-pointer-write.js +57 -0
  183. package/template/coding-task-manager/lib/state-sync/painted-frame-field.js +11 -0
  184. package/template/coding-task-manager/lib/storage/coding-session-model-store.js +21 -22
  185. package/template/coding-task-manager/lib/surviving-owner-bootstrap.js +7 -2
  186. package/template/coding-task-manager/lib/surviving-owner-recovery.js +2 -1
  187. package/template/coding-task-manager/lib/terminal-draft-http-status.js +25 -0
  188. package/template/coding-task-manager/lib/transcript-archiver.js +229 -229
  189. package/template/coding-task-manager/lib/transcript-frame-cache.js +12 -2
  190. package/template/coding-task-manager/lib/transcript-guard-job.js +55 -4
  191. package/template/coding-task-manager/lib/transcript-link.js +77 -0
  192. package/template/coding-task-manager/lib/transcript-manifest-store.js +8 -4
  193. package/template/coding-task-manager/lib/transcript-source.js +8 -3
  194. package/template/coding-task-manager/lib/transcript-store.js +40 -0
  195. package/template/coding-task-manager/lib/tunnel-lane-governor.js +6 -3
  196. package/template/coding-task-manager/lib/typing-interaction-trace.js +92 -0
  197. package/template/coding-task-manager/lib/typing-worker-stages.js +60 -0
  198. package/template/coding-task-manager/lib/visible-revision-watchdog.js +8 -12
  199. package/template/coding-task-manager/lib/wake-skip-reasons.js +46 -0
  200. package/template/coding-task-manager/lib/walle-supervisor.js +98 -14
  201. package/template/coding-task-manager/lib/worker-failure-classify.js +13 -1
  202. package/template/coding-task-manager/lib/ws/state-sync.js +19 -2
  203. package/template/coding-task-manager/lib/ws/terminal-attach.js +32 -1
  204. package/template/coding-task-manager/lib/ws-router.js +68 -1
  205. package/template/coding-task-manager/lib/ws-send-admission.js +22 -4
  206. package/template/coding-task-manager/oauth-proxy.js +99 -55
  207. package/template/coding-task-manager/package-lock.json +24 -0
  208. package/template/coding-task-manager/package.json +3 -0
  209. package/template/coding-task-manager/providers/codex-mcp-extract.js +121 -19
  210. package/template/coding-task-manager/providers/codex-mcp.js +14 -5
  211. package/template/coding-task-manager/public/css/app.css +279 -1
  212. package/template/coding-task-manager/public/css/reviews.css +4 -0
  213. package/template/coding-task-manager/public/index.html +294 -223
  214. package/template/coding-task-manager/public/js/app/machines-page.js +1252 -0
  215. package/template/coding-task-manager/public/js/app/models-tab.js +546 -36
  216. package/template/coding-task-manager/public/js/app/worktrees-panel.js +7 -5
  217. package/template/coding-task-manager/public/js/background-work.js +13 -7
  218. package/template/coding-task-manager/public/js/coding-session-model-presentation.js +44 -4
  219. package/template/coding-task-manager/public/js/core/session-commands.js +676 -0
  220. package/template/coding-task-manager/public/js/core/session-selectors.js +5 -1
  221. package/template/coding-task-manager/public/js/history-app-seam.js +26 -2
  222. package/template/coding-task-manager/public/js/history-painted-rows.js +17 -2
  223. package/template/coding-task-manager/public/js/history-reader-park.js +13 -1
  224. package/template/coding-task-manager/public/js/history-scroller.js +187 -9
  225. package/template/coding-task-manager/public/js/history-transcript-pages.js +5 -5
  226. package/template/coding-task-manager/public/js/machine-pool-shared.js +436 -0
  227. package/template/coding-task-manager/public/js/private-state-scope.js +31 -3
  228. package/template/coding-task-manager/public/js/prompt-submission-client.js +173 -8
  229. package/template/coding-task-manager/public/js/prompt-submission-tray.js +281 -0
  230. package/template/coding-task-manager/public/js/queue-panel-submission-reconcile.js +174 -1
  231. package/template/coding-task-manager/public/js/reviews.js +7 -3
  232. package/template/coding-task-manager/public/js/root-draft-lifetime.js +139 -2
  233. package/template/coding-task-manager/public/js/root-queue-builder-lifetime.js +16 -0
  234. package/template/coding-task-manager/public/js/row-join.js +4 -2
  235. package/template/coding-task-manager/public/js/session-model-picker.js +1 -1
  236. package/template/coding-task-manager/public/js/session-search-utils.js +46 -19
  237. package/template/coding-task-manager/public/js/state-sync-client.js +4 -0
  238. package/template/coding-task-manager/public/js/terminal/runtime-02.js +16 -7
  239. package/template/coding-task-manager/public/js/terminal/runtime-03.js +41 -0
  240. package/template/coding-task-manager/public/js/terminal/runtime-04.js +41 -4
  241. package/template/coding-task-manager/public/js/terminal/runtime-12.js +52 -2
  242. package/template/coding-task-manager/public/js/terminal/runtime-16.js +12 -12
  243. package/template/coding-task-manager/public/js/terminal/runtime-18.js +14 -0
  244. package/template/coding-task-manager/public/js/terminal/runtime-19.js +8 -4
  245. package/template/coding-task-manager/public/js/terminal/runtime-20.js +210 -73
  246. package/template/coding-task-manager/public/js/terminal/runtime-21.js +19 -6
  247. package/template/coding-task-manager/public/js/terminal/runtime-22.js +86 -0
  248. package/template/coding-task-manager/public/js/terminal-history-viewport.js +1 -1
  249. package/template/coding-task-manager/public/js/terminal-wheel.js +39 -2
  250. package/template/coding-task-manager/public/js/walle/chat-conversation.js +11 -3
  251. package/template/coding-task-manager/public/js/walle-session.js +30 -7
  252. package/template/coding-task-manager/public/m/app.css +226 -8
  253. package/template/coding-task-manager/public/m/app.js +502 -9
  254. package/template/coding-task-manager/public/m/index.html +38 -1
  255. package/template/coding-task-manager/public/m/machines.js +1313 -0
  256. package/template/coding-task-manager/public/m/pool-failover.js +391 -0
  257. package/template/coding-task-manager/public/m/private-queue-actions.js +26 -7
  258. package/template/coding-task-manager/public/m/sw.js +5 -0
  259. package/template/coding-task-manager/queue-engine.js +13 -0
  260. package/template/coding-task-manager/server/approvals.js +5 -6
  261. package/template/coding-task-manager/server/boot.js +136 -17
  262. package/template/coding-task-manager/server.js +902 -239
  263. package/template/coding-task-manager/session-integrity.js +7 -9
  264. package/template/coding-task-manager/workers/db-owner-worker.js +53 -7
  265. package/template/coding-task-manager/workers/read-pool-worker.js +17 -6
  266. package/template/coding-task-manager/workers/session-host-pool-process.js +18 -4
  267. package/template/coding-task-manager/workers/session-host-process.js +14 -3
  268. package/template/coding-task-manager/workers/transcript-archive-worker.js +1 -1
  269. package/template/coding-task-manager/workers/worktree-worker.js +34 -0
  270. package/template/package-lock.json +2 -2
  271. package/template/package.json +1 -1
  272. package/template/scripts/conversation-storage-v2-migration.js +78 -11
  273. package/template/shared/conversation-storage-v2-contract.js +42 -6
  274. package/template/shared/fixtures/worker-status-payloads.json +392 -0
  275. package/template/shared/git-remote-key.js +83 -0
  276. package/template/shared/host-identity.js +98 -2
  277. package/template/shared/owner-write-safety.js +135 -0
  278. package/template/shared/public-runtime.js +13 -5
  279. package/template/shared/release-schema.json +3 -2
  280. package/template/shared/report-citations.js +14 -0
  281. package/template/shared/sqlite-write-lock.js +361 -223
  282. package/template/shared/storage/contract.js +35 -3
  283. package/template/wall-e/agent.js +21 -0
  284. package/template/wall-e/api-walle.js +5 -0
  285. package/template/wall-e/bin/brain-purge-legacy-ctm.js +3 -2
  286. package/template/wall-e/bin/walle-mcp-stdio.js +12 -6
  287. package/template/wall-e/brain/helper-rules-schema.js +129 -0
  288. package/template/wall-e/brain/migrations.js +51 -22
  289. package/template/wall-e/brain/model-deployment-facts.js +31 -1
  290. package/template/wall-e/brain/owner-call-whitelist.js +16 -0
  291. package/template/wall-e/brain/schema-ensures.js +22 -0
  292. package/template/wall-e/brain.js +1 -1
  293. package/template/wall-e/coding/journal-dispatcher.js +51 -0
  294. package/template/wall-e/coding/session-journal.js +28 -40
  295. package/template/wall-e/coding-orchestrator.js +23 -26
  296. package/template/wall-e/http/auth.js +18 -1
  297. package/template/wall-e/http/blog-views-api.js +318 -0
  298. package/template/wall-e/http/helper-rules-api.js +321 -0
  299. package/template/wall-e/http/model-proxy-providers.js +3 -1
  300. package/template/wall-e/http/telemetry-api.js +12 -0
  301. package/template/wall-e/lib/blog-view-policy.js +88 -0
  302. package/template/wall-e/lib/blog-view-store.js +169 -0
  303. package/template/wall-e/lib/client-config-lock.js +2 -1
  304. package/template/wall-e/lib/coding-journal-writer-child-client.js +67 -18
  305. package/template/wall-e/lib/error-kinds.js +10 -1
  306. package/template/wall-e/lib/feature-manifest.js +1 -1
  307. package/template/wall-e/lib/helper-rules/cell-state.js +123 -0
  308. package/template/wall-e/lib/helper-rules/evaluate.js +357 -0
  309. package/template/wall-e/lib/helper-rules/explore-rubric.md +12 -0
  310. package/template/wall-e/lib/helper-rules/grade.js +277 -0
  311. package/template/wall-e/lib/helper-rules/label.js +247 -0
  312. package/template/wall-e/lib/helper-rules/lead-call-hook.js +522 -0
  313. package/template/wall-e/lib/helper-rules/store.js +667 -0
  314. package/template/wall-e/lib/mcp-integration.js +5 -6
  315. package/template/wall-e/lib/runtime-worker-pool.js +1 -0
  316. package/template/wall-e/lib/scheduler-worker-jobs.js +6 -0
  317. package/template/wall-e/lib/telemetry-db-schema.js +37 -0
  318. package/template/wall-e/lib/telemetry-process-client.js +76 -1
  319. package/template/wall-e/llm/default-fallback.js +4 -0
  320. package/template/wall-e/llm/model-catalog.js +13 -4
  321. package/template/wall-e/llm/model-roles.js +6 -0
  322. package/template/wall-e/llm/portkey-http.js +150 -0
  323. package/template/wall-e/llm/portkey-sync.js +15 -145
  324. package/template/wall-e/llm/worker-mcp-routing.js +61 -0
  325. package/template/wall-e/llm/worker-route.js +43 -1
  326. package/template/wall-e/loops/brain-custom-writes.js +45 -0
  327. package/template/wall-e/mcp-protocol-contract.js +41 -1
  328. package/template/wall-e/mcp-server.js +13 -2
  329. package/template/wall-e/mcp-worker-schemas.js +20 -2
  330. package/template/wall-e/memory/ctm-context-client.js +25 -5
  331. package/template/wall-e/shared/release-schema.json +3 -2
  332. package/template/wall-e/shared/sqlite-write-lock.js +361 -223
  333. package/template/wall-e/shared/storage/contract.js +35 -3
  334. package/template/wall-e/skills/_templates/delegate-worker/VERSION +1 -1
  335. package/template/wall-e/skills/_templates/delegate-worker/released-sha256.json +6 -0
  336. package/template/wall-e/skills/_templates/delegate-worker/templates/claude/SKILL.md +7 -2
  337. package/template/wall-e/skills/_templates/delegate-worker/templates/codex/SKILL.md +7 -2
  338. package/template/wall-e/tools/local-tools.js +6 -4
  339. package/template/wall-e/workers/brain-owner-worker.js +9 -0
  340. package/template/wall-e/workers/coding-journal-writer-child.js +8 -1
  341. package/template/wall-e/workers/telemetry-worker.js +8 -0
@@ -227,6 +227,7 @@
227
227
  <button class="nav-pill" data-nav="insights" onclick="navTo('insights');closeNavMore()" role="menuitem">Insights</button>
228
228
  <button class="nav-pill" data-nav="permissions" onclick="navTo('permissions');closeNavMore()" role="menuitem">Permissions</button>
229
229
  <button class="nav-pill" data-nav="worktrees" onclick="navTo('worktrees');closeNavMore()" role="menuitem">Worktrees</button>
230
+ <button class="nav-pill" data-nav="machines" onclick="navTo('machines');closeNavMore()" role="menuitem">Machines</button>
230
231
  </div>
231
232
  </div>
232
233
  <button class="nav-pill" data-nav="setup" onclick="navTo('setup')" title="Setup & Settings" style="margin-left:auto;font-size:16px;padding:4px 8px;">⚙</button>
@@ -593,6 +594,8 @@
593
594
  <div id="worktrees-filters" style="padding:10px 20px 0;border-bottom:1px solid var(--border);display:none;flex-wrap:wrap;gap:6px;background:var(--bg);"></div>
594
595
  <div id="worktrees-body" style="flex:1;overflow-y:auto;padding:16px 20px;"></div>
595
596
  </div>
597
+ <!-- Machines (WP-5 / D-14): rendered by js/app/machines-page.js; replaces the Settings tab. -->
598
+ <div id="machines-panel" style="display:none;flex-direction:column;height:100%;overflow:hidden;"></div>
596
599
  <div id="setup-panel">
597
600
  <div class="setup-header">
598
601
  <div style="display:flex;justify-content:space-between;align-items:baseline;">
@@ -1765,6 +1768,7 @@ url = "http://localhost:<span id="setup-mcp-port-display-toml">3457</span>/mcp"<
1765
1768
  </div>
1766
1769
  </div>
1767
1770
  </div>
1771
+
1768
1772
  <!-- TAB: Backups -->
1769
1773
  <div class="setup-section" id="setup-section-backups" style="display:none;">
1770
1774
  <div class="card" id="setup-backups">
@@ -2637,6 +2641,14 @@ url = "http://localhost:<span id="setup-mcp-port-display-toml">3457</span>/mcp"<
2637
2641
  <!-- Populated dynamically by showNewSessionModal() -->
2638
2642
  </div>
2639
2643
  <input type="hidden" id="ns-agent" value="claude:" />
2644
+ <!-- WP-5 (S-11/AC-5a): machine cards, the agent grid's twin. Hidden with the hidden
2645
+ input emptied when the hub has no machines, so the modal renders exactly as before. -->
2646
+ <label id="ns-machine-label" style="display:none">Choose Machine</label>
2647
+ <div id="ns-machine-grid" class="ns-agent-grid ns-machine-grid" style="display:none">
2648
+ <!-- Populated dynamically by loadNewSessionMachines() -->
2649
+ </div>
2650
+ <input type="hidden" id="ns-machine" value="" />
2651
+ <div id="ns-machine-hint" class="ns-machine-hint" style="display:none"></div>
2640
2652
  <label id="ns-cwd-label">Project Folder</label>
2641
2653
  <div class="ns-path-picker" id="ns-path-picker">
2642
2654
  <div class="ns-path-input-row">
@@ -2887,6 +2899,7 @@ url = "http://localhost:<span id="setup-mcp-port-display-toml">3457</span>/mcp"<
2887
2899
  <script src="js/terminal-queue-plan.js?v=__CTM_ASSET_EPOCH__"></script>
2888
2900
  <script src="js/prompt-submission-client.js?v=__CTM_ASSET_EPOCH__"></script>
2889
2901
  <script src="js/queue-panel-submission-reconcile.js?v=__CTM_ASSET_EPOCH__"></script>
2902
+ <script src="js/prompt-submission-tray.js?v=__CTM_ASSET_EPOCH__"></script>
2890
2903
  <script src="js/screenshot-router.js?v=__CTM_ASSET_EPOCH__"></script>
2891
2904
  <script src="js/upload-banner-state.js?v=__CTM_ASSET_EPOCH__"></script>
2892
2905
  <script src="js/standup-stale-followup.js?v=__CTM_ASSET_EPOCH__"></script>
@@ -2904,6 +2917,11 @@ url = "http://localhost:<span id="setup-mcp-port-display-toml">3457</span>/mcp"<
2904
2917
  <script src="js/app/models-provider-tls.js?v=__CTM_ASSET_EPOCH__"></script>
2905
2918
  <script src="js/app/backups-panel.js?v=__CTM_ASSET_EPOCH__"></script>
2906
2919
  <script src="js/app/worktrees-panel.js?v=__CTM_ASSET_EPOCH__"></script>
2920
+ <!-- WP-6: the ONE DOM-free machine-pool presenter copy (order, search text, groups, load line,
2921
+ sparkline, pairing container). Loaded before the picker (js/core/session-commands.js) and the
2922
+ Machines page (js/app/machines-page.js), both of which call into it and no longer define it. -->
2923
+ <script src="js/machine-pool-shared.js?v=__CTM_ASSET_EPOCH__"></script>
2924
+ <script src="js/app/machines-page.js?v=__CTM_ASSET_EPOCH__"></script>
2907
2925
  <script src="js/app/approvals-ui.js?v=__CTM_ASSET_EPOCH__"></script>
2908
2926
  <script src="js/app/restart-banner.js?v=__CTM_ASSET_EPOCH__"></script>
2909
2927
  <script src="js/background-work.js?v=__CTM_ASSET_EPOCH__"></script>
@@ -3600,6 +3618,7 @@ const state = window._ctmState = {
3600
3618
  ws: null,
3601
3619
  token: getCookie('ctm_token'),
3602
3620
  sessions: new Map(), // id -> { term, fitAddon, container }
3621
+ pendingCloseIds: new Map(), // id -> addedAtMs; cleared only by evidence (unlisted, intentional exit, kill error), no timer
3603
3622
  sessionScrollbackRows: {},
3604
3623
  activeTab: null, // session id or 'rules'
3605
3624
  // Lifecycle WP-6 (delegated-worker-lifecycle-identity-observability-2026-09-27.html §5 D-3):
@@ -4441,7 +4460,25 @@ function _dispatchWebSocketMessage(msg) {
4441
4460
  case 'resume-prompt-result': onResumePromptResult(msg); break;
4442
4461
  case 'resume-blocked': onSessionResumeBlocked(msg); break;
4443
4462
  case 'resume-retry-noop': onResumeRetryNoop(msg); break;
4444
- case 'error': console.error('Server error:', msg.message); break;
4463
+ case 'error': {
4464
+ // WP-5 (§6.3/AC-5a): a machine-scoped failure (machine_unreachable, or the box's own code)
4465
+ // is SHOWN — the user picks another machine or fixes the box. Never retried here, and every
4466
+ // other error frame keeps its existing console-only path.
4467
+ var _machineErr = /^machine_/.test(String(msg.code || '')) || !!msg.machine;
4468
+ if (_machineErr && typeof toast === 'function') {
4469
+ // One toast per machine id: a storm of the same box's failures never stacks.
4470
+ toast(msg.message || msg.error || String(msg.code || 'Machine error'),
4471
+ { type: 'error', title: 'Machine error', key: 'machine-error:' + String(msg.machine || msg.code || '') });
4472
+ }
4473
+ // D-7: the server refused THIS kill (e.g. owner_recovery_pending). Say so and drop the
4474
+ // pending entry, so the next list brings the tab back truthfully instead of hiding it.
4475
+ if (msg.id && state.pendingCloseIds.has(msg.id)) {
4476
+ state.pendingCloseIds.delete(msg.id);
4477
+ toast(msg.message || 'Could not close the session.', { type: 'error', key: 'kill-failed:' + msg.id });
4478
+ }
4479
+ console.error('Server error:', msg.message);
4480
+ break;
4481
+ }
4445
4482
  case 'analysis-progress': onAnalysisProgress(msg); break;
4446
4483
  case 'analysis-done': onAnalysisDone(msg); break;
4447
4484
  case 'queue-state': onQueueState(msg); break;
@@ -5600,90 +5637,63 @@ function _promptTrayClearanceBottomPx(viewportHeight, inputStackTop) {
5600
5637
  // ".walle-input-stack" today (no MutationObserver wired to it), so a TTL is
5601
5638
  // the fallback staleness bound; forceRecompute lets a known-authoritative
5602
5639
  // caller (resize) always get a fresh read.
5603
- const _promptTrayAnchorCache = { atMs: 0, inputStackTop: 0 };
5640
+ const _promptTrayAnchorCache = { atMs: 0, inputStackTop: 0, queuePanelOpen: null };
5604
5641
  const _promptTrayAnchorCacheTtlMs = 2000;
5605
5642
 
5606
5643
  function _positionPromptSubmissionTray(tray, opts) {
5607
5644
  const now = Date.now();
5645
+ const queuePanelOpen = window._ctmState?.queuePanelOpen === true;
5608
5646
  const stale = !_promptTrayAnchorCache.atMs || (now - _promptTrayAnchorCache.atMs) > _promptTrayAnchorCacheTtlMs;
5609
- if ((opts && opts.forceRecompute) || stale) {
5647
+ if ((opts && opts.forceRecompute) || stale || _promptTrayAnchorCache.queuePanelOpen !== queuePanelOpen) {
5610
5648
  let inputStackTop = 0;
5611
- for (const stack of document.querySelectorAll('.walle-input-stack')) {
5649
+ for (const stack of document.querySelectorAll('.walle-input-stack, #qp-bottom')) {
5612
5650
  if (!stack.offsetParent) continue;
5613
5651
  const rect = stack.getBoundingClientRect();
5614
5652
  if (rect.height > 0 && (!inputStackTop || rect.top < inputStackTop)) inputStackTop = rect.top;
5615
5653
  }
5616
- _promptTrayAnchorCache.inputStackTop = inputStackTop;
5617
- _promptTrayAnchorCache.atMs = now;
5654
+ Object.assign(_promptTrayAnchorCache, { inputStackTop, atMs: now, queuePanelOpen });
5618
5655
  }
5619
5656
  tray.style.bottom = _promptTrayClearanceBottomPx(window.innerHeight, _promptTrayAnchorCache.inputStackTop) + 'px';
5620
5657
  }
5621
5658
 
5659
+ // WP-1 (C2, design §6 + docs/queue-delivery-recovery-evidence-2026-10-23.html §6 "WP-1 boundary
5660
+ // update"): the tray renderer itself now lives in public/js/prompt-submission-tray.js — index.html
5661
+ // sits at its file-size ceiling, so only the renderer moved. The three dependencies that can only
5662
+ // exist here (the tray element, its composer clearance, and the owner-fenced recovery route) are
5663
+ // passed in explicitly; the module owns no globals and no queue state of its own.
5622
5664
  function _renderPromptSubmissionStatus(record) {
5623
- const tray = _ensurePromptSubmissionTray();
5624
- _positionPromptSubmissionTray(tray);
5625
- let row = document.getElementById('prompt-submission-' + record.clientMessageId);
5626
- if (!row) {
5627
- row = document.createElement('div');
5628
- row.id = 'prompt-submission-' + record.clientMessageId;
5629
- row.style.cssText = 'display:flex;align-items:center;gap:8px;padding:9px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-secondary);box-shadow:0 6px 20px rgba(0,0,0,.28);font-size:12px;color:var(--text-primary)';
5630
- tray.appendChild(row);
5631
- }
5632
- // Quiet happy path (user feedback 2026-08-20: stacked "Sending prompt..." rows sat over the
5633
- // composer for a whole 21-min turn saying nothing). The tray's real job is the FAILURE lane —
5634
- // Retry/Restore when a prompt would otherwise be silently lost — so pending/queued rows stay
5635
- // hidden unless a prompt is still unacknowledged after 5s, a 'sent' that was never shown
5636
- // dismisses without the "Prompt sent" flash, and a user-hidden quiet row stays hidden until
5637
- // its status escalates to a failure (loss protection always wins over the quieting).
5638
- const quietStatus = record.status === 'pending' || record.status === 'queued';
5639
- if (!quietStatus && row.dataset.hiddenByUser === '1') delete row.dataset.hiddenByUser;
5640
- if (quietStatus) {
5641
- if (!row.dataset.quietRevealAt) {
5642
- row.dataset.quietRevealAt = String(Date.now() + 5000);
5643
- row.style.display = 'none';
5644
- setTimeout(() => {
5645
- const live = document.getElementById('prompt-submission-' + record.clientMessageId);
5646
- if (live && live.dataset.quietRevealAt && live.dataset.hiddenByUser !== '1') live.style.display = 'flex';
5647
- }, 5000);
5648
- } else if (row.dataset.hiddenByUser === '1' || Date.now() < Number(row.dataset.quietRevealAt)) {
5649
- row.style.display = 'none';
5650
- } else {
5651
- row.style.display = 'flex';
5652
- }
5653
- } else if (record.status === 'sent' && row.style.display === 'none') {
5654
- row.remove();
5655
- setTimeout(() => window.CTMPromptSubmission.dismiss(record.clientMessageId), 0);
5656
- return;
5657
- } else {
5658
- delete row.dataset.quietRevealAt;
5659
- row.style.display = 'flex';
5660
- }
5661
- row.textContent = '';
5662
- const label = document.createElement('span');
5663
- label.style.flex = '1';
5664
- label.textContent = record.status === 'pending' ? 'Sending prompt...' : record.status === 'sent' ? 'Prompt sent' : (record.message || 'Prompt needs attention');
5665
- row.appendChild(label);
5666
- if (record.status === 'needs_attention' || record.status === 'blocked' || record.status === 'failed') {
5667
- const retry = document.createElement('button');
5668
- retry.className = 'btn small'; retry.textContent = 'Retry';
5669
- retry.onclick = () => window.CTMPromptSubmission.retry(record.clientMessageId);
5670
- row.appendChild(retry);
5671
- }
5672
- if (record.status !== 'pending' && record.status !== 'queued') {
5673
- const dismiss = document.createElement('button');
5674
- dismiss.className = 'btn small'; dismiss.textContent = 'Dismiss';
5675
- dismiss.onclick = () => window.CTMPromptSubmission.dismiss(record.clientMessageId);
5676
- row.appendChild(dismiss);
5677
- if (record.status === 'sent') setTimeout(() => window.CTMPromptSubmission.dismiss(record.clientMessageId), 2200);
5678
- } else {
5679
- // Quiet rows get a HIDE-ONLY dismiss — never CTMPromptOutbox.dismiss here, that would drop
5680
- // the durable retry record while the prompt is still in flight. The row (and its tracking)
5681
- // stays; only the pixels go, and a failure escalation clears hiddenByUser above.
5682
- const hide = document.createElement('button');
5683
- hide.className = 'btn small'; hide.textContent = 'Dismiss';
5684
- hide.onclick = () => { row.dataset.hiddenByUser = '1'; row.style.display = 'none'; };
5685
- row.appendChild(hide);
5686
- }
5665
+ if (!window.PromptSubmissionTray) return null;
5666
+ return window.PromptSubmissionTray.renderStatus(record, {
5667
+ document,
5668
+ ensureTray: _ensurePromptSubmissionTray,
5669
+ positionTray: _positionPromptSubmissionTray,
5670
+ getManager: () => window.CTMPromptSubmission,
5671
+ onRecovery: _recoverPromptSubmission,
5672
+ setTimer: (callback, delayMs) => setTimeout(callback, delayMs),
5673
+ now: () => Date.now(),
5674
+ });
5675
+ }
5676
+
5677
+ // C2/C3 (R6, AC-1d/AC-1e, AC-1f): the tray's recovery gesture is routed by the queue module, which
5678
+ // owns the exact-item matching and the one-gesture-at-a-time rule; the page supplies its own state
5679
+ // and the owner-fenced resendQpItem, and never mints an identity here.
5680
+ function _recoverPromptSubmission(action, record, options = {}) {
5681
+ return window.QueuePanelSubmissionReconcile.routeTrayRecovery(action, record, options, {
5682
+ getSessionId: () => qpCurrentSession, getItems: () => qpItems, resendItem: resendQpItem,
5683
+ getManager: () => window.CTMPromptSubmission,
5684
+ });
5685
+ }
5686
+
5687
+ // WP-1 (C4, R7/AC-1e): the review surface for a queue item whose last outcome is uncertain. The
5688
+ // record and its lifetime guard live in the reconcile module and the row itself in the tray module;
5689
+ // the page supplies its own state plus the owner-fenced resend route behind the confirmation, and
5690
+ // never mints an identity here.
5691
+ function _reviewQpItems(items, canNotify) {
5692
+ return window.QueuePanelSubmissionReconcile.openItemReview(items, {
5693
+ getItems: () => qpItems, getSessionId: () => qpCurrentSession, getManager: () => window.CTMPromptSubmission,
5694
+ notify: message => canNotify && toast('Delivery unconfirmed — review this item before sending again. ' + message, { type: 'warning', key: 'qp-review:' + qpCurrentSession }),
5695
+ renderReview: (record, isCurrent) => window.PromptSubmissionTray.renderItemReview(record, { document, ensureTray: _ensurePromptSubmissionTray, positionTray: _positionPromptSubmissionTray, getManager: () => window.CTMPromptSubmission, isCurrent, onRecovery: _recoverPromptSubmission }),
5696
+ });
5687
5697
  }
5688
5698
 
5689
5699
  // S-16/S-17/D-12/D-16: the tray is no longer a status ghost for every outbox row of every
@@ -12072,34 +12082,22 @@ function createTerminal(id, opts) {
12072
12082
  return intent;
12073
12083
  }
12074
12084
 
12075
- async function _terminalDeleteServerDraft(id, attachment, options = {}) {
12076
- if (attachment && !attachment.attachmentId && options.cancelRequest && attachment.clientId) {
12077
- // Upload cancelled before the 201 arrived: cancel the server-side create by its request id.
12078
- const cancel = await _terminalFetchWithRetry('/api/sessions/' + encodeURIComponent(id) + '/image-drafts?request_id=' +
12079
- encodeURIComponent(attachment.clientId) + '&purge=1&token=' + encodeURIComponent(state.token || ''), { method: 'DELETE' }, { attempts: 3 });
12080
- if (!cancel.ok && cancel.status !== 404) throw new Error('HTTP ' + cancel.status);
12081
- return true;
12082
- }
12083
- if (!attachment || !attachment.attachmentId) return true;
12084
- const headers = attachment.etag ? { 'If-Match': attachment.etag } : {};
12085
- const response = await _terminalFetchWithRetry('/api/sessions/' + encodeURIComponent(id) + '/image-drafts/' +
12086
- encodeURIComponent(attachment.attachmentId) + '?token=' + encodeURIComponent(state.token || '') +
12087
- (options.purge === true ? '&purge=1' : ''), {
12088
- method: 'DELETE', headers,
12089
- }, { attempts: 3 });
12090
- if (!response.ok && response.status !== 404) throw new Error('HTTP ' + response.status);
12091
- return true;
12092
- }
12093
-
12094
12085
  async function _terminalRemoveDraftAttachment(id, attachment, metricEvent, options = {}) {
12095
12086
  const Draft = window.TerminalAttachmentDraft;
12096
12087
  const draft = _terminalAttachmentDraftState(id);
12097
12088
  if (!Draft || !draft || !attachment || !draft.attachments.includes(attachment)) return false;
12089
+ const owned = _terminalDraftUploadOwnership(id);
12090
+ const current = () => Boolean(owned.lifetime && owned.lifetime === _terminalRootDraftLifetime
12091
+ && !owned.reason && owned.context && owned.context.draft === draft
12092
+ && owned.lifetime.attachmentCurrent(owned.context, id, draft, attachment));
12093
+ if (!current()) return false;
12098
12094
  clearTimeout(attachment._unconfirmedReconcileTimer);
12099
12095
  attachment._unconfirmedReconcileTimer = null;
12100
12096
  try {
12101
- await _terminalDeleteServerDraft(id, attachment, options);
12097
+ if (!await _terminalDeleteServerDraft(id, attachment, options,
12098
+ { context: owned.context, isCurrent: current }) || !current()) return false;
12102
12099
  } catch (error) {
12100
+ if (!current()) return false;
12103
12101
  console.warn('[terminal-image] draft cleanup failed:', error && error.message ? error.message : error);
12104
12102
  _terminalAttachmentMetric(id, 'provider_delete_cleanup_failed', { attachmentCount: draft.attachments.length });
12105
12103
  _terminalRenderAttachmentDraft(id);
@@ -12119,10 +12117,13 @@ function createTerminal(id, opts) {
12119
12117
  if (action === 'disabled' || !action) return false;
12120
12118
  if (action === 'cancel') return _terminalCancelAttachmentUpload(id, attachment);
12121
12119
  const metric = action === 'detach' ? 'attached_detached_by_user' : 'chip_dismissed_by_user';
12120
+ const owned = _terminalDraftUploadOwnership(id);
12122
12121
  const removed = _terminalRemoveDraftAttachment(id, attachment, metric);
12123
12122
  if (action === 'detach') {
12124
12123
  void Promise.resolve(removed).then(didRemove => {
12125
12124
  if (!didRemove || typeof toast !== 'function') return;
12125
+ if (!owned.context || owned.lifetime !== _terminalRootDraftLifetime || owned.context.draft !== draft
12126
+ || owned.lifetime.current(owned.context) !== true) return;
12126
12127
  toast((attachment.label || 'Image token') + ' text remains in the composer - delete it there too.', {
12127
12128
  type: 'info', duration: 4200,
12128
12129
  });
@@ -12141,8 +12142,9 @@ function createTerminal(id, opts) {
12141
12142
  return target ? String(target.dataset.attachmentId || '') : '';
12142
12143
  }
12143
12144
 
12144
- function _terminalFocusAfterAttachmentDismiss(id, adjacentClientId, dismissed) {
12145
+ function _terminalFocusAfterAttachmentDismiss(id, adjacentClientId, dismissed, context) {
12145
12146
  if (!dismissed || !adjacentClientId) return;
12147
+ if (!context || context.id !== id || _terminalRootDraftLifetime?.capture(id, context.session, context.draft) !== context) return;
12146
12148
  const region = document.getElementById('tray-chip-region');
12147
12149
  if (!region || region.dataset.sessionId !== id) return;
12148
12150
  const target = Array.from(region.querySelectorAll('.terminal-attachment-chip'))
@@ -12377,8 +12379,7 @@ function createTerminal(id, opts) {
12377
12379
  function _terminalCancelAttachmentUpload(id, attachment) {
12378
12380
  if (!attachment || (attachment.status !== 'uploading' && attachment.status !== 'materializing')) return false;
12379
12381
  try { attachment._uploadAbortController?.abort(); } catch {}
12380
- void _terminalRemoveDraftAttachment(id, attachment, 'upload_cancelled', { purge: true, cancelRequest: true });
12381
- return true;
12382
+ return _terminalRemoveDraftAttachment(id, attachment, 'upload_cancelled', { purge: true, cancelRequest: true });
12382
12383
  }
12383
12384
 
12384
12385
  // image-attach-latency-redesign F2: with the server reconciler on, retrying a failed draft
@@ -12560,6 +12561,8 @@ function createTerminal(id, opts) {
12560
12561
  return;
12561
12562
  }
12562
12563
  region.replaceChildren();
12564
+ const focusOwned = _terminalDraftUploadOwnership(id);
12565
+ const focusContext = !focusOwned.reason && focusOwned.context?.draft === draft ? focusOwned.context : null;
12563
12566
  for (const attachment of draft.attachments) {
12564
12567
  const chip = document.createElement('div');
12565
12568
  chip.className = 'terminal-attachment-chip tray-attachment-chip';
@@ -12634,7 +12637,7 @@ function createTerminal(id, opts) {
12634
12637
  const adjacentClientId = _terminalAdjacentAttachmentClientId(id, attachment);
12635
12638
  const dismissed = _terminalDismissAttachment(id, attachment);
12636
12639
  void Promise.resolve(dismissed).then(result => {
12637
- _terminalFocusAfterAttachmentDismiss(id, adjacentClientId, result);
12640
+ _terminalFocusAfterAttachmentDismiss(id, adjacentClientId, result, focusContext);
12638
12641
  });
12639
12642
  }
12640
12643
  };
@@ -13323,16 +13326,22 @@ function createTerminal(id, opts) {
13323
13326
  const failed = draft.attachments.filter(attachment => attachment && attachment.status === 'failed');
13324
13327
  if (!failed.length) return [];
13325
13328
 
13329
+ const owned = _terminalDraftUploadOwnership(id);
13330
+ const current = () => Boolean(owned.lifetime && owned.lifetime === _terminalRootDraftLifetime
13331
+ && !owned.reason && owned.context?.draft === draft && owned.lifetime.current(owned.context));
13326
13332
  const removed = [];
13327
13333
  for (const attachment of failed) {
13334
+ if (!current()) break;
13328
13335
  if (!Draft.removeAttachment(draft, attachment.clientId)) continue;
13329
13336
  removed.push(attachment);
13330
13337
  _terminalReleaseAttachmentUploadSource(attachment);
13331
13338
  await _terminalDeleteServerDraft(id, attachment).catch(error => {
13339
+ if (!current()) return;
13332
13340
  console.warn('[terminal-image] failed draft cleanup after send failed:', error && error.message ? error.message : error);
13333
13341
  _terminalAttachmentMetric(id, 'failed_draft_cleanup_failed', { attachmentCount: removed.length });
13334
13342
  });
13335
13343
  }
13344
+ if (!current()) return removed;
13336
13345
  _terminalRenderAttachmentDraft(id);
13337
13346
  _terminalAttachmentMetric(id, 'failed_drafts_discarded_on_send', { attachmentCount: removed.length });
13338
13347
  if (options.userAuthorized === true) {
@@ -13865,12 +13874,13 @@ function createTerminal(id, opts) {
13865
13874
  const providerOrderBase = draft.attachments.reduce((max, item) => (
13866
13875
  Math.max(max, Number(item._providerOrdinal || item.providerOrder || 0))
13867
13876
  ), 0);
13877
+ const stagedOwnerHostId = _terminalAttachmentOwnerHostId(state.sessions.get(id)); // R-1: the id the server stamps
13868
13878
  const staged = files.map((file, index) => {
13869
13879
  const providerOrder = providerOrderBase + index + 1;
13870
13880
  const attachment = Draft.addUploading(draft, {
13871
13881
  clientId: _terminalAttachmentClientId(),
13872
13882
  filename: file.name || ('clipboard-image-' + (index + 1) + '.png'),
13873
- ownerHostId: _terminalAttachmentCurrentHostId(),
13883
+ ownerHostId: stagedOwnerHostId,
13874
13884
  });
13875
13885
  attachment._promptOffset = promptOffset;
13876
13886
  attachment._promptOrder = index;
@@ -13930,7 +13940,7 @@ function createTerminal(id, opts) {
13930
13940
  // is adopted here instead of being rendered as an intermediate 'ready' first. Only a
13931
13941
  // row with no reservation (or no retained publication) takes the legacy transition.
13932
13942
  const outcome = _terminalFinishAttachmentUpload(id, attachment, uploaded); // WP-3 §6: adopt or legacy ready
13933
- if (outcome.outcome === 'rejected') return;
13943
+ if (outcome.outcome === 'rejected') return void _terminalRejectedUploadOutcome(id, attachment, outcome, { metric: _terminalAttachmentMetric, postDiagnostic: _terminalPostAttachmentDiagnostic }); // R-4: fails visibly, never silently
13934
13944
  // image-attach-latency-redesign WP-1: with the server reconciler on, do not race a
13935
13945
  // second PTY paste against it here -- the reconciler already got a nudge from this
13936
13946
  // draft's server-side 'create' op and owns delivering it. readyCount here only
@@ -14387,12 +14397,13 @@ function createTerminal(id, opts) {
14387
14397
  // the CONTINUOUS reader on THIS tick -- no mouse report is built/sent (AC-8d).
14388
14398
  // WP-2 (D-4): px = |deltaY| in px per deltaMode -- the live terminal lands that far below the pane top.
14389
14399
  if (decision.action === 'reader-enter') {
14390
- const px = _wheelDeltaPx(e, term); wheelRecorder.noteEvent({ deltaPx: px, deltaMode: e.deltaMode, steps: 0 });
14400
+ if (writer._wheelSender && writer._wheelSender.cancel) writer._wheelSender.cancel(); const px = _wheelDeltaPx(e, term); wheelRecorder.noteEvent({ deltaPx: px, deltaMode: e.deltaMode, steps: 0 });
14391
14401
  _enterContinuousForSession(id, px);
14392
14402
  e.preventDefault();
14393
14403
  e.stopPropagation();
14394
14404
  return;
14395
14405
  }
14406
+ if (decision.action === 'swallow') { e.preventDefault(); e.stopPropagation(); return; } // D-4: a Codex app view's wheel-down is CTM's, never the PTY's
14396
14407
  if (decision.shouldForward) {
14397
14408
  let cw = 0, ch = 0;
14398
14409
  try { const d = term._core._renderService.dimensions.css.cell; cw = d.width; ch = d.height; } catch {}
@@ -14428,7 +14439,7 @@ function createTerminal(id, opts) {
14428
14439
  e.stopPropagation();
14429
14440
  return;
14430
14441
  }
14431
- } catch (error) {
14442
+ } catch (error) { e.preventDefault(); e.stopPropagation();
14432
14443
  send({
14433
14444
  type: 'client-diag', id, event: 'terminal-wheel-forward-error',
14434
14445
  details: { message: String(error && error.message || error).slice(0, 160) },
@@ -14441,7 +14452,7 @@ function createTerminal(id, opts) {
14441
14452
  if (wheelDecisionForDiag) {
14442
14453
  if (wheelDecisionForDiag.action === 'reader') wheelRecorder.noteEvent({ deltaPx: _wheelDeltaPx(e, term), deltaMode: e.deltaMode, steps: 0, route: 'reader' }); // D-15: reader-open events
14443
14454
  wheelRecorder.record(wheelDecisionForDiag.route, {
14444
- bufferType: buf.type, baseY: buf.baseY,
14455
+ bufferType: buf.type, baseY: buf.baseY, deltaY: e.deltaY, action: wheelDecisionForDiag.action,
14445
14456
  appMouse: wheelDecisionForDiag.mouseActive, isAlt: wheelDecisionForDiag.isAlt,
14446
14457
  mouseSource: wheelDecisionForDiag.mouseState && wheelDecisionForDiag.mouseState.source, frameTracking: wheelDecisionForDiag.frameTracking,
14447
14458
  });
@@ -17024,14 +17035,15 @@ function _showWarmSessionShell(id) {
17024
17035
  for (const panelId of [
17025
17036
  'rules-panel', 'review-panel', 'codereview-panel', 'insights-panel',
17026
17037
  'permissions-panel', 'prompts-panel', 'walle-panel', 'models-panel',
17027
- 'backups-panel', 'worktrees-panel', 'setup-panel',
17038
+ 'backups-panel', 'worktrees-panel', 'machines-panel', 'setup-panel',
17028
17039
  ]) {
17029
17040
  const panel = document.getElementById(panelId);
17030
17041
  panel?.classList?.remove('active');
17031
- if (panel && ['models-panel', 'backups-panel', 'worktrees-panel', 'setup-panel'].includes(panelId)) {
17042
+ if (panel && ['models-panel', 'backups-panel', 'worktrees-panel', 'machines-panel', 'setup-panel'].includes(panelId)) {
17032
17043
  panel.style.display = 'none';
17033
17044
  }
17034
17045
  }
17046
+ if (typeof machinesPanelHidden === 'function') machinesPanelHidden();
17035
17047
  const sidebar = document.getElementById('sidebar');
17036
17048
  const sidebarResize = document.getElementById('sidebar-resize');
17037
17049
  if (!state.sidebarManuallyHidden) {
@@ -17143,7 +17155,7 @@ function _prepareSurfaceResume(id) {
17143
17155
  const previousId = state.activeTab;
17144
17156
  const switchingTabs = previousId !== id;
17145
17157
  const session = state.sessions.get(id);
17146
- const isPanel = ['rules', 'insights', 'permissions', 'prompts', 'codereview', 'walle', 'models', 'worktrees', 'setup'].includes(id);
17158
+ const isPanel = ['rules', 'insights', 'permissions', 'prompts', 'codereview', 'walle', 'models', 'worktrees', 'machines', 'setup'].includes(id);
17147
17159
  const wakeDirtyEvent = session?._wakeStreamDirty;
17148
17160
  if (wakeDirtyEvent) {
17149
17161
  session._wakeStreamDirty = false;
@@ -17355,6 +17367,7 @@ function _commitSurfaceVisibility(id, isPanel, wasSubscribedBeforeActivation) {
17355
17367
  if (bkp) { bkp.classList.remove('active'); bkp.style.display = 'none'; }
17356
17368
  var wtp = document.getElementById('worktrees-panel');
17357
17369
  if (wtp) { wtp.classList.remove('active'); wtp.style.display = 'none'; }
17370
+ var mcp = document.getElementById('machines-panel'); if (mcp) { mcp.classList.remove('active'); mcp.style.display = 'none'; } if (typeof machinesPanelHidden === 'function') machinesPanelHidden();
17358
17371
  var stp = document.getElementById('setup-panel');
17359
17372
  if (stp) { stp.classList.remove('active'); stp.style.display = 'none'; }
17360
17373
  state.activeTab = id;
@@ -17467,6 +17480,10 @@ if (id === 'rules') {
17467
17480
  wtp.style.display = 'flex';
17468
17481
  wtp.classList.add('active');
17469
17482
  loadWorktreesPanel();
17483
+ } else if (id === 'machines') {
17484
+ var mcp = document.getElementById('machines-panel');
17485
+ mcp.style.display = 'flex'; mcp.classList.add('active');
17486
+ loadMachinesPanel();
17470
17487
  } else if (id === 'setup') {
17471
17488
  var sp = document.getElementById('setup-panel');
17472
17489
  sp.style.display = 'flex';
@@ -17725,7 +17742,7 @@ function activateTab(id) {
17725
17742
 
17726
17743
  function syncNavPills(activeId) {
17727
17744
  const navPills = document.querySelectorAll('#topbar-nav .nav-pill');
17728
- const specialPanels = ['rules', 'insights', 'permissions', 'prompts', 'codereview', 'walle', 'models', 'backups', 'worktrees', 'setup'];
17745
+ const specialPanels = ['rules', 'insights', 'permissions', 'prompts', 'codereview', 'walle', 'models', 'backups', 'worktrees', 'machines', 'setup'];
17729
17746
  const navId = specialPanels.includes(activeId) ? activeId : 'sessions';
17730
17747
  navPills.forEach(pill => {
17731
17748
  pill.classList.toggle('active', pill.dataset.nav === navId);
@@ -18421,10 +18438,10 @@ function showStandupDashboard(opts) {
18421
18438
  const el = document.getElementById(id);
18422
18439
  if (el) el.classList.remove('active');
18423
18440
  });
18424
- ['models-panel', 'backups-panel', 'worktrees-panel', 'setup-panel'].forEach(id => {
18441
+ ['models-panel', 'backups-panel', 'worktrees-panel', 'machines-panel', 'setup-panel'].forEach(id => {
18425
18442
  const el = document.getElementById(id);
18426
18443
  if (el) { el.classList.remove('active'); el.style.display = 'none'; }
18427
- });
18444
+ }); if (typeof machinesPanelHidden === 'function') machinesPanelHidden();
18428
18445
 
18429
18446
  state.activeTab = SESSIONS_OVERVIEW_TAB_ID;
18430
18447
  _projectSessionListSelection(SESSIONS_OVERVIEW_TAB_ID);
@@ -18828,6 +18845,9 @@ function renderStandupCard(card, opts) {
18828
18845
  opts = opts || {};
18829
18846
  const tokens = opts.tokens || [];
18830
18847
  const providerBadge = renderStandupProviderBadge(card);
18848
+ // WP-5 (§6.6/S-14): the standup card carries the same chip. '' for a local session (and for a
18849
+ // catalog that predates the hub — the presenter tolerates the missing fields).
18850
+ const machineChip = machineChipHtml(card);
18831
18851
  const subtitle = [card.agent, card.model, card.branch].filter(Boolean).join(' / ');
18832
18852
  const goal = standupCleanGoalText(card.goal || card.intent || '');
18833
18853
  const progress = card.progressSummary || card.progress || '';
@@ -18847,6 +18867,7 @@ function renderStandupCard(card, opts) {
18847
18867
  <div class="standup-card-title">${titleHtml}</div>
18848
18868
  <div class="standup-card-meta" title="${standupEsc(subtitle || card.cwd || '')}">
18849
18869
  ${providerBadge}
18870
+ ${machineChip}
18850
18871
  <div class="standup-card-subtitle">${standupEsc(subtitle || card.cwd || '')}</div>
18851
18872
  </div>
18852
18873
  </div>
@@ -19043,6 +19064,8 @@ function navTo(target, opts) {
19043
19064
  showSetupPanel();
19044
19065
  } else if (target === 'worktrees') {
19045
19066
  showWorktreesPanel();
19067
+ } else if (target === 'machines') {
19068
+ showMachinesPanel(opts || {});
19046
19069
  }
19047
19070
  }
19048
19071
 
@@ -22778,19 +22801,19 @@ function onSnapshot(msg) {
22778
22801
  const dimsMismatch = gridMismatchDecision.mismatch;
22779
22802
  if (dimsMismatch) {
22780
22803
  const manualReflowSnapshot = restoreSource === 'reflow';
22781
- if (gridMismatchDecision.action === 'apply-authoritative') {
22782
- // Serialized xterm snapshots are grid-relative. Final-exit and manual
22783
- // reflow snapshots are user-authoritative recovery frames, so replay them
22784
- // at the captured PTY grid instead of throwing them away after a local
22785
- // fit drift. Activation/cache snapshots can still be discarded and
22786
- // re-requested at local dimensions.
22787
- try {
22788
- if (ptyCols > 0 && ptyRows > 0 && (s.term.cols !== ptyCols || s.term.rows !== ptyRows)) {
22789
- _runTerminalMutation(s, 'authoritative-snapshot-resize', () => s.term.resize(ptyCols, ptyRows));
22790
- if (finalExitSnapshot) s._lastFinalExitSnapshotResizeAt = Date.now();
22791
- if (manualReflowSnapshot) s._lastManualReflowSnapshotResizeAt = Date.now();
22792
- }
22793
- } catch {}
22804
+ // Serialized xterm snapshots are grid-relative. Final-exit and manual reflow snapshots are
22805
+ // user-authoritative recovery frames, so replay them at the captured PTY grid instead of
22806
+ // throwing them away after a local fit drift. Activation/cache snapshots can still be
22807
+ // discarded and re-requested at local dimensions.
22808
+ // WP-1 (D-1): adopting that grid is all-or-nothing. A grid the cols-pin chokepoint refused
22809
+ // is NOT replayed at the landed hybrid -- it takes the mismatch branch below (resync the
22810
+ // server to our dims, request a fresh frame), which is the recovery this path already had.
22811
+ const adoptAuthoritative = ptyCols > 0 && ptyRows > 0
22812
+ && gridMismatchDecision.action === 'apply-authoritative'
22813
+ && _adoptTerminalGrid(s, ptyCols, ptyRows, 'authoritative-snapshot-resize');
22814
+ if (adoptAuthoritative) {
22815
+ if (finalExitSnapshot) s._lastFinalExitSnapshotResizeAt = Date.now();
22816
+ if (manualReflowSnapshot) s._lastManualReflowSnapshotResizeAt = Date.now();
22794
22817
  } else {
22795
22818
  // Non-interim, non-authoritative snapshot at a foreign grid: the client
22796
22819
  // resized after requesting. Do NOT replay foreign-width data. Resync the
@@ -23280,6 +23303,13 @@ function _showLoadingOverlay(s, opts) {
23280
23303
  }
23281
23304
  const DISMISS_AUTHORITIES = new Set(['activation-ready', 'degraded', 'user', 'reconcile']); // R5 §6.4: replaces {authoritative:true}; only 'reconcile' is vetoable (I-9: veto is a no-op, NEVER _showLoadingOverlay)
23282
23305
  function _loadingOverlayDismissAuthority(opts) { return (opts && DISMISS_AUTHORITIES.has(opts.authority)) ? opts.authority : (opts && opts.authoritative === true ? 'activation-ready' : 'reconcile'); }
23306
+ // WP-4a (§5 OQ-4, §6): record which exit ran and the store lifecycle it saw, on the session, so the
23307
+ // next stranded cover can be attributed (H-A store missing/active while mounted, H-B resume-blocked
23308
+ // held, H-C no dismissal ran) without a per-call beacon. Instrumentation only: no exit changes.
23309
+ function _recordOverlayDismiss(s, authority, exit) {
23310
+ if (!s) return;
23311
+ s._lastOverlayDismiss = { authority, exit, lifecycle: _presentationStore.getSessionState(_resumeClientSessionId(s))?.lifecycle ?? null, at: Date.now() };
23312
+ }
23283
23313
  function _dismissLoadingOverlay(s, opts) {
23284
23314
  opts = opts || {}; const authority = _loadingOverlayDismissAuthority(opts);
23285
23315
  // WI-1: keep the corner chip in sync on every dismiss checkpoint (this function is called from
@@ -23294,16 +23324,19 @@ function _dismissLoadingOverlay(s, opts) {
23294
23324
  // "session-restore-started" means the PTY exists, not that the visible page is ready.
23295
23325
  if (s && authority === 'reconcile') {
23296
23326
  const rs = _resumeStateForSession(s);
23297
- if (rs && rs.dismissBlocked) return { vetoed: true }; // I-9: never _showLoadingOverlay here.
23327
+ if (rs && rs.dismissBlocked) { _recordOverlayDismiss(s, authority, 'vetoed-dismiss-blocked'); return { vetoed: true }; } // I-9: never _showLoadingOverlay here.
23298
23328
  }
23299
23329
  // A resume-blocked card is a terminal recovery state, not a loading cover. Readiness and
23300
23330
  // lifecycle handlers share this DOM shell, but they do not own the card and must not erase its
23301
23331
  // diagnosis/actions. Retry and the server's explicit blocked=false projection clear it.
23302
- if (_presentationStore.getSessionState(_resumeClientSessionId(s))?.lifecycle === 'resume-blocked'
23303
- && opts.clearResumeBlocked !== true) return;
23332
+ const lifecycle = _presentationStore.getSessionState(_resumeClientSessionId(s))?.lifecycle ?? null;
23333
+ if (lifecycle === 'resume-blocked' && opts.clearResumeBlocked !== true) { _recordOverlayDismiss(s, authority, 'resume-blocked-held'); return; }
23304
23334
  if (s._loadingOverlayTimer) { clearTimeout(s._loadingOverlayTimer); s._loadingOverlayTimer = null; }
23305
23335
  s._resumeLoadStartedAt = 0;
23306
- if (!s.container) return;
23336
+ if (!s.container) { _recordOverlayDismiss(s, authority, 'no-container'); return; }
23337
+ // The store keeps ONE lifecycle per session, and _clearSessionLifecycle only writes the family it
23338
+ // owns; naming that mismatch here is what separates a silent no-op clear from a real one.
23339
+ _recordOverlayDismiss(s, authority, LIFECYCLE_COVER_FAMILY[lifecycle] === 'restore' ? 'cleared' : 'not-restore-family');
23307
23340
  _clearSessionLifecycle(s, 'restore');
23308
23341
  s._loadingOverlayShownAt = 0;
23309
23342
  }
@@ -23311,6 +23344,9 @@ function _dismissLoadingOverlay(s, opts) {
23311
23344
 
23312
23345
 
23313
23346
  function onExit(msg) {
23347
+ // D-7: an intentional exit is the server confirming the close. It must clear the pending entry
23348
+ // BEFORE the early return below — killSession already removed the tab, so there is no `s`.
23349
+ if (msg.intentional === true) state.pendingCloseIds.delete(msg.id);
23314
23350
  const s = state.sessions.get(msg.id);
23315
23351
  if (!s) return;
23316
23352
  // S-19 (terminal-sync-adopted-restart-race-2026-09-07.html §9 S-19): an exited session
@@ -24007,72 +24043,6 @@ function onSessionRestoreFailed(msg) {
24007
24043
  );
24008
24044
  }
24009
24045
 
24010
- // WI-5: cause-specific, non-alarming copy for the enumerated deterministic resume_blocked reason
24011
- // codes the server can send (see server.js markSessionResumeBlocked / _notifyResumePreflightFailure).
24012
- // Branch ONLY on `msg.code` here — never on message text — so this stays correct even if the
24013
- // human-readable message wording changes server-side. An unrecognized code (contract drift, or a
24014
- // code we haven't special-cased yet) falls back to the same honest red failure banner
24015
- // onSessionRestoreFailed already renders, rather than inventing false calm.
24016
- const RESUME_BLOCKED_COPY = {
24017
- missing_claude_transcript: {
24018
- tone: 'info',
24019
- title: "This session's transcript is gone from this machine.",
24020
- body: 'The file was removed before CTM stored any of its prompts (Claude Code deletes transcripts older than '
24021
- + 'its retention period, 30 days by default). There is nothing to resume; start a fresh session in the same folder.',
24022
- },
24023
- missing_codex_rollout: {
24024
- tone: 'info',
24025
- title: "This session's transcript is gone from this machine.",
24026
- body: 'The file was removed before CTM stored any of its prompts. There is nothing to resume; '
24027
- + 'start a fresh session in the same folder.',
24028
- },
24029
- agent_cli_not_found: {
24030
- tone: 'warn',
24031
- title: 'The agent CLI isn’t available on this host.',
24032
- body: 'CTM could not find the CLI needed to resume this session on this machine. '
24033
- + 'Install it (or fix PATH), then reload — or start fresh here.',
24034
- },
24035
- resume_exit_loop: {
24036
- tone: 'warn',
24037
- title: 'This session could not finish resuming.',
24038
- body: '',
24039
- },
24040
- resume_no_frame: {
24041
- tone: 'warn',
24042
- title: 'The session never painted a usable screen.',
24043
- body: '',
24044
- },
24045
- resume_no_output: {
24046
- tone: 'warn',
24047
- title: 'The agent did not respond while resuming.',
24048
- body: 'It may be waiting on authentication, blocked by configuration, or unavailable on this host.',
24049
- },
24050
- // Mixed-version compatibility: current servers normalize this internal
24051
- // lifecycle reason to resume_no_output before it reaches the browser.
24052
- no_provider_output: {
24053
- tone: 'warn',
24054
- title: 'The agent did not respond while resuming.',
24055
- body: 'It may be waiting on authentication, blocked by configuration, or unavailable on this host.',
24056
- },
24057
- // Compatibility for a server from before the resume lifecycle became provider-neutral.
24058
- codex_no_output: {
24059
- tone: 'warn',
24060
- title: 'Codex did not respond while resuming.',
24061
- body: 'It may be waiting on authentication, blocked by configuration, or unavailable on this host.',
24062
- },
24063
- restore_retries_exhausted: {
24064
- tone: 'warn',
24065
- title: 'CTM could not prepare this session after several tries.',
24066
- body: 'The launch planner kept timing out. Retry now, or start fresh here.',
24067
- },
24068
- };
24069
-
24070
-
24071
-
24072
-
24073
-
24074
-
24075
-
24076
24046
  // Render (or reuse) the same DOM overlay shell used for the codex resume-loading spinner, but as
24077
24047
  // a STATIC informational/warning card — no spinner, no progress bar, no red "failed" styling
24078
24048
  // (WI-5: the cross-host case is expected, not an error). Returns false for an unrecognized code
@@ -24426,6 +24396,9 @@ async function onSessionsList(msg, options = {}) {
24426
24396
 
24427
24397
  // Add tabs for sessions we don't have terminals for (reconnect scenario)
24428
24398
  for (const sess of msg.sessions) {
24399
+ // D-7: a close we already sent wins over any list that still carries the id — otherwise this
24400
+ // loop re-creates the stub and re-pushes tabOrder, and the tab the user just closed returns.
24401
+ if (state.pendingCloseIds.has(sess.id)) continue;
24429
24402
  const incomingLiveStatus = sess && sess.liveStatus;
24430
24403
  const hasIncomingLiveStatus = incomingLiveStatus !== undefined && incomingLiveStatus !== null && incomingLiveStatus !== '';
24431
24404
  const existingBeforeShape = state.sessions.get(sess.id);
@@ -24601,6 +24574,9 @@ async function onSessionsList(msg, options = {}) {
24601
24574
  lastSeenAt: sess.resumeBlockedLastSeenAt || '',
24602
24575
  trashPath: sess.resumeBlockedTrashPath || '',
24603
24576
  originalPath: sess.resumeBlockedOriginalPath || '',
24577
+ // WP-3 (D-13): why the copy is in CTM's trash and when — the card's "why" sentence.
24578
+ trashReason: sess.resumeBlockedTrashReason || '',
24579
+ trashedAt: sess.resumeBlockedTrashedAt || '',
24604
24580
  // WP-6: the resume ladder's answer for the missing-transcript card.
24605
24581
  resumeRung: sess.resumeBlockedRung || '',
24606
24582
  coverage: sess.resumeBlockedCoverage || null,
@@ -24650,15 +24626,23 @@ async function onSessionsList(msg, options = {}) {
24650
24626
  deterministic: false,
24651
24627
  });
24652
24628
  }
24653
- } else if (existing._resumeBlocked && existing._resumeCheckPending !== true) {
24629
+ } else if (existing._resumeCheckPending !== true) {
24654
24630
  // A pending Check again is not a clear: its answer (resume-blocked / resume-retry-noop / restore-started) arrives on its own.
24655
- existing._resumeBlocked = false;
24656
- existing._resumeBlockedRung = '';
24657
- existing._resumeBlockedCode = '';
24658
- existing._resumeBlockedMessage = '';
24659
- existing._resumeBlockedDetail = '';
24660
- existing._resumeBlockedAttempts = 0;
24661
- // Leave only a resume-blocked lifecycle: a loading cover (a Retry in flight) is not this tick's to clear.
24631
+ if (existing._resumeBlocked) {
24632
+ existing._resumeBlocked = false;
24633
+ existing._resumeBlockedRung = '';
24634
+ existing._resumeBlockedCode = '';
24635
+ existing._resumeBlockedMessage = '';
24636
+ existing._resumeBlockedDetail = '';
24637
+ existing._resumeBlockedAttempts = 0;
24638
+ }
24639
+ // WP-4c (session-exit-close-truth-2026-10-24.html §5 OQ-4 = H-B, §8 WP-4c): the server's
24640
+ // resumeBlocked:false is the authority for a SERVER-owned block, and it must clear a
24641
+ // resume-blocked store lifecycle even when THIS client object never saw the flag -- a
24642
+ // `created` replacement rebuilds the object (`onCreated` -> createTerminal) while
24643
+ // _presentationStore (keyed by session id) keeps the card, so a flag-only test strands the
24644
+ // card's shell (and the loading cover that shell hosts) forever. Leave any other lifecycle:
24645
+ // a loading cover (a Retry in flight) is not this tick's to clear.
24662
24646
  if (existing.container && _presentationStore.getSessionState(_resumeClientSessionId(existing))?.lifecycle === 'resume-blocked') _clearSessionLifecycle(existing, 'restore');
24663
24647
  }
24664
24648
  }
@@ -24719,6 +24703,11 @@ async function onSessionsList(msg, options = {}) {
24719
24703
  _maybeRearmTerminalBootstrapFromInventory(existing, sess, { restoring });
24720
24704
  }
24721
24705
  }
24706
+ // D-7: a FULL list that omits the id is the server's own evidence that the close landed — only
24707
+ // then is the pending entry dropped (no timer). A restoring list is partial by design, so it is no evidence.
24708
+ if (!restoring) for (const pendingId of Array.from(state.pendingCloseIds.keys())) {
24709
+ if (!serverIds.has(pendingId)) state.pendingCloseIds.delete(pendingId);
24710
+ }
24722
24711
 
24723
24712
  if (!options.skipStreamStatusRefresh && !restoring && typeof refreshStreamStatuses === 'function') {
24724
24713
  await refreshStreamStatuses({ force: true, render: false, timeoutMs: 3000,
@@ -24751,6 +24740,10 @@ async function onSessionsList(msg, options = {}) {
24751
24740
  }
24752
24741
 
24753
24742
  if (!ownsRoot()) return false;
24743
+ // WP-5 (S-15): repaint machine chips that are already mounted from THIS frame's facts (the
24744
+ // render paths build chips from the row text, but a tab chip reparented into the toolbar is not
24745
+ // rebuilt by a skipped/deferred render). A no-op for every local session.
24746
+ if (typeof refreshSessionMachineChips === 'function') refreshSessionMachineChips(msg.sessions);
24754
24747
  if (!options.deferRender) _renderSessionsListProjection();
24755
24748
 
24756
24749
  // Auto-activate explicit hash targets as soon as they appear. Saved-session
@@ -25318,9 +25311,12 @@ function populateModelSwitcher(sessionId, opts) {
25318
25311
  // The chip label is the lead model + tags + token badge only; effort and the delegate model ride the
25319
25312
  // tooltip / aria-label that syncChip paints. This writes only .model-chip-lead.
25320
25313
  var leadHost = _chipLeadHost(chip);
25321
- leadHost.textContent = '';
25314
+ var labelHost = leadHost.querySelector ? leadHost.querySelector('.model-chip-label') : null;
25315
+ if (!labelHost) { labelHost = document.createElement('span'); labelHost.className = 'model-chip-label'; }
25316
+ labelHost.textContent = chipView.text;
25317
+ while (leadHost.firstChild) leadHost.removeChild(leadHost.firstChild);
25318
+ leadHost.appendChild(labelHost);
25322
25319
  chip.classList.toggle('model-stale', chipView.stale);
25323
- leadHost.appendChild(document.createTextNode(chipView.text));
25324
25320
  chipView.tags.forEach(function(tag) {
25325
25321
  var tagEl = document.createElement('span');
25326
25322
  tagEl.className = 'model-chip-tag';
@@ -27098,7 +27094,7 @@ function renderSessionList(force) {
27098
27094
  }).map(p =>
27099
27095
  `<span class="prompt-badge" onclick="event.stopPropagation();openPromptInEditor(${p.prompt_id})" title="${escHtml(p.title || 'Prompt')}">${escHtml((p.title || 'Prompt').slice(0, 20))}</span>`
27100
27096
  ).join('');
27101
- const branchBadge = activeSessionBranchBadge(s);
27097
+ const branchBadge = activeSessionBranchBadge(s) + machineChipHtml(s); // + WP-5 machine chip ('' local)
27102
27098
  const forkBadge = activeSessionForkBadge(s);
27103
27099
  const worktreeBadge = worktreeAttentionBadge(s);
27104
27100
  const ownerChip = sessionOwnerChipHtml(s);
@@ -28253,9 +28249,12 @@ function _renderTabsLegacy(force) {
28253
28249
  ? SessionWorkspaceTracking.tabBranchNode(SessionWorkspaceTracking.trackingForSession(s), tabBranch, '\u25ED', branchBadgeHtml)
28254
28250
  : null;
28255
28251
  const lifecycleChip = _sessionLifecycleChipNode(s);
28252
+ // WP-5 (\u00a76.6/S-14): the same presenter as the stream-chrome tab (session-commands.js), so both
28253
+ // tab render modes show an identical chip. null for a local session.
28254
+ const tabMachineChip = machineSessionChipNode(s, id);
28256
28255
  const tabClose = document.createElement('span'); tabClose.className = 'close-tab'; tabClose.textContent = '\u00d7';
28257
28256
  tabClose.onclick = function(e) { e.stopPropagation(); killSession(id); };
28258
- tab.appendChild(tabIcon); tab.appendChild(tabLabel); if (tabBranchEl) tab.appendChild(tabBranchEl); if (lifecycleChip) tab.appendChild(lifecycleChip); tab.appendChild(tabClose);
28257
+ tab.appendChild(tabIcon); tab.appendChild(tabLabel); if (tabBranchEl) tab.appendChild(tabBranchEl); if (tabMachineChip) tab.appendChild(tabMachineChip); if (lifecycleChip) tab.appendChild(lifecycleChip); tab.appendChild(tabClose);
28259
28258
  tab.onpointerdown = function(e) {
28260
28259
  if (e.target?.closest?.('.close-tab')) return;
28261
28260
  if (e.button === undefined || e.button === 0) {
@@ -28362,6 +28361,9 @@ function _streamChromeTabUnitForId(id) {
28362
28361
  branchInfo.displayNamespace || '',
28363
28362
  workspaceTracking ? JSON.stringify(workspaceTracking) : '',
28364
28363
  lifecycleHtml,
28364
+ // WP-5 (S-15): the machine chip's label/title depend on the machine's health, so a
28365
+ // reachable -> unreachable flip must rebuild the tab instead of leaving a stale chip.
28366
+ JSON.stringify(sessionMachineFacts(session)),
28365
28367
  ]),
28366
28368
  visualHash: JSON.stringify([
28367
28369
  state.activeTab === id,
@@ -28869,9 +28871,12 @@ function _nsPathRefreshHostStatus() {
28869
28871
  attempt: bounded ? _nsPathPickerState.hostPathAttempts : 0,
28870
28872
  count: candidates.length,
28871
28873
  });
28874
+ // WP-5 (§6.5): the probe is answered BY the selected machine via the X-CTM-Machine header (a
28875
+ // local selection sends none), and the seq guard above is what keeps a box answer from ever
28876
+ // landing in a local path field.
28872
28877
  fetch('/api/path-status', {
28873
28878
  method: 'POST',
28874
- headers: { 'Content-Type': 'application/json' },
28879
+ headers: machineScopedHeaders({ 'Content-Type': 'application/json' }),
28875
28880
  body: JSON.stringify({ paths: candidates }),
28876
28881
  })
28877
28882
  .then(function(r) {
@@ -28958,7 +28963,10 @@ function _nsPathRefreshHostStatus() {
28958
28963
  function _nsPathLoadProjects() {
28959
28964
  if (_nsPathPickerState.projectsLoaded || _nsPathPickerState.projectsLoading) return;
28960
28965
  _nsPathPickerState.projectsLoading = true;
28961
- fetch('/api/projects?token=' + encodeURIComponent(state.token || ''))
28966
+ // WP-5 (§6.5): the selected machine answers this list ('' ⇒ this Mac, URL unchanged). These rows
28967
+ // are the only place the same-repo prefill looks — plain gitRemoteKey equality inside
28968
+ // nsMachineAfterProjectsLoaded(), against the keys of the machine we are pointed at.
28969
+ fetch('/api/projects?token=' + encodeURIComponent(state.token || '') + machineQueryParam())
28962
28970
  .then(function(r) { return r.ok ? r.json() : []; })
28963
28971
  .then(function(projects) {
28964
28972
  _nsPathPickerState.projects = Array.isArray(projects)
@@ -28967,6 +28975,7 @@ function _nsPathLoadProjects() {
28967
28975
  _nsPathPickerState.projectsLoaded = true;
28968
28976
  _nsPathPickerState.projectsLoading = false;
28969
28977
  _nsPathRefreshHostStatus();
28978
+ if (typeof nsMachineAfterProjectsLoaded === 'function') nsMachineAfterProjectsLoaded(_nsPathPickerState);
28970
28979
  if (_nsPathPickerState.open) _nsPathRenderSuggestions();
28971
28980
  _nsPathUpdateValidation();
28972
28981
  if (typeof _storagePathRepaintOpen === 'function') _storagePathRepaintOpen();
@@ -29179,7 +29188,9 @@ function _nsFetchFsSuggestions(value) {
29179
29188
  clearTimeout(_nsPathPickerState.fsTimer);
29180
29189
  _nsPathPickerState.fsTimer = setTimeout(function() {
29181
29190
  var seq = ++_nsPathPickerState.fsSeq;
29182
- fetch('/api/path-suggest?q=' + encodeURIComponent(q), { cache: 'no-store' })
29191
+ // WP-5 (§6.5): suggestions come FROM the selected machine; the fsSeq guard above already drops
29192
+ // a stale answer, including one that arrives after a machine switch.
29193
+ fetch('/api/path-suggest?q=' + encodeURIComponent(q) + machineQueryParam(), { cache: 'no-store' })
29183
29194
  .then(function(r) { return r.ok ? r.json() : null; })
29184
29195
  .then(function(d) {
29185
29196
  if (!d || seq !== _nsPathPickerState.fsSeq) return; // a newer keystroke won
@@ -29923,6 +29934,9 @@ async function _maybeRecommendWorktreeForNewSession(force) {
29923
29934
 
29924
29935
  function showNewSessionModal() {
29925
29936
  document.getElementById('new-session-modal').classList.remove('hidden');
29937
+ // WP-5: the machine list is fetched per open; with zero machines the grid stays unrendered
29938
+ // and this modal is pixel-identical to its pre-hub self (AC-5a).
29939
+ if (typeof loadNewSessionMachines === 'function') loadNewSessionMachines();
29926
29940
  _nsPathReset(getLastSessionCwd());
29927
29941
  document.getElementById('ns-label').value = '';
29928
29942
  _nsWorktreeTouched = false;
@@ -30108,6 +30122,8 @@ function killSession(id, opts) {
30108
30122
  const s = state.sessions.get(id);
30109
30123
  const label = s?.meta?.label || id.slice(0, 8);
30110
30124
  if (!opts.skipConfirm && !confirm(`Close session "${label}"?`)) return;
30125
+ // D-7: our close wins over any list that was already in flight when it was sent.
30126
+ state.pendingCloseIds.set(id, Date.now());
30111
30127
  send({ type: 'kill', id });
30112
30128
  // Clean up locally
30113
30129
  if (s) {
@@ -30172,6 +30188,8 @@ function _killTabSilent(id) {
30172
30188
  if (window.CTMNativeSubagentReview?.hasTab(id)) { closeSpecialTab(id); return; }
30173
30189
  const s = state.sessions.get(id);
30174
30190
  if (!s) return;
30191
+ // D-7: see killSession — the pending entry bridges until the list proves the close landed.
30192
+ state.pendingCloseIds.set(id, Date.now());
30175
30193
  send({ type: 'kill', id });
30176
30194
  _clearTerminalRecoveryProtocolResources(s);
30177
30195
  if (s._outputIdleTimer) clearTimeout(s._outputIdleTimer);
@@ -31060,6 +31078,8 @@ document.addEventListener('keydown', (e) => {
31060
31078
  }
31061
31079
  // Cmd+Shift+V / Ctrl+Shift+V: cycle the available surfaces in product order.
31062
31080
  if ((e.metaKey || e.ctrlKey) && e.shiftKey && (e.key === 'V' || e.key === 'v')) {
31081
+ // One keystroke, one action: the terminal already consumed this chord as a path paste.
31082
+ if (e.defaultPrevented) return;
31063
31083
  const id = state.activeTab;
31064
31084
  if (id && state.sessions && state.sessions.has(id) && typeof setSessionView === 'function') {
31065
31085
  e.preventDefault();
@@ -32060,12 +32080,13 @@ async function loadRecentSessions(options = {}) {
32060
32080
  if (!state.tabOrder.includes('review')) state.tabOrder.push('review');
32061
32081
  // If not post-restart (e.g. normal page load with saved review), restore active tab now
32062
32082
  if (!state._postRestart) {
32063
- const _hashNav = location.hash.slice(1).split('&')[0];
32083
+ // AC-5e: resolve the alias before classifying, so boot never flashes the old #setup tab.
32084
+ const _hashNav = (typeof _machinesHashAliasTarget === 'function' ? _machinesHashAliasTarget(location.hash) : '') || location.hash.slice(1).split('&')[0];
32064
32085
  // Map hash aliases (e.g. #review → codereview) to the internal nav target so a
32065
32086
  // panel-hash load isn't mistaken for "no panel" and overridden by the saved
32066
32087
  // session below — the bug that made #review steal focus to the last session.
32067
32088
  const _hashTarget = NAV_TARGET_ALIASES[_hashNav] || _hashNav;
32068
- const _isHashPanel = ['command', 'rules', 'insights', 'permissions', 'prompts', 'review', 'codereview', 'walle', 'models', 'backups', 'worktrees', 'setup'].includes(_hashNav);
32089
+ const _isHashPanel = ['command', 'rules', 'insights', 'permissions', 'prompts', 'review', 'codereview', 'walle', 'models', 'backups', 'worktrees', 'machines', 'setup'].includes(_hashNav);
32069
32090
  if (_isHashPanel) {
32070
32091
  const _alreadyOnHashPanel = state.activeTab === _hashTarget || (_hashNav === 'command' && state.activeTab === 'overview');
32071
32092
  if (!_alreadyOnHashPanel) navTo(_hashTarget, { skipHash: true });
@@ -32645,6 +32666,9 @@ function sessionItemHtml(s, opts = {}) {
32645
32666
  const deviceTag = (!thisDevice && s.hostname) ? `<span style="color:var(--accent);font-size:9px;margin-left:4px">${escHtml(s.hostname)}</span>` : '';
32646
32667
  const msgCount = s.userMsgCount > 0 ? `<span>${s.userMsgCount} msgs</span>` : '';
32647
32668
  const providerBadge = recentSessionAgentBadgeHtml(s);
32669
+ // WP-5 (§6.6/S-14): '' for a local session, so local rows keep their exact markup. NOTE: this
32670
+ // row template is duplicated in renderServerSearchResults() below — both must stay in step.
32671
+ const machineChip = machineChipHtml(s);
32648
32672
  const activeInfo = recentSessionActiveInfo(s, opts.activeIndex || null);
32649
32673
  const activeBadge = recentSessionActiveBadgeHtml(activeInfo);
32650
32674
  const activeCls = activeInfo ? ' active-session' : '';
@@ -32659,6 +32683,7 @@ function sessionItemHtml(s, opts = {}) {
32659
32683
  <span class="project">${escHtml(project)}</span>
32660
32684
  ${activeBadge}
32661
32685
  ${providerBadge}
32686
+ ${machineChip}
32662
32687
  ${branch}
32663
32688
  ${msgCount}
32664
32689
  <span>${ago}</span>
@@ -32842,7 +32867,7 @@ function continueInShell(sessionId, cwd) {
32842
32867
  send({
32843
32868
  type: 'create',
32844
32869
  cwd: cwd || undefined,
32845
- label: `Shell: ${cwd}`,
32870
+ label: `Shell: ${cwd}`, titleIntent: 'default', // D-13: CTM-composed name, never tier 1
32846
32871
  });
32847
32872
  }
32848
32873
 
@@ -33350,7 +33375,7 @@ function openFolder(folderPath) {
33350
33375
  cmd: 'open',
33351
33376
  args: [folderPath],
33352
33377
  cwd: folderPath,
33353
- label: `Folder: ${folderPath.replace(/^\/Users\/[^/]+\//, '~/')}`,
33378
+ label: `Folder: ${folderPath.replace(/^\/Users\/[^/]+\//, '~/')}`, titleIntent: 'default', // D-13: CTM-composed name
33354
33379
  });
33355
33380
  }
33356
33381
 
@@ -34932,6 +34957,8 @@ function renderServerSearchResults(results, total, query, degraded) {
34932
34957
  const branch = s.gitBranch ? branchMetaHtml(s.gitBranch) : '';
34933
34958
  const msgCount = s.userMsgCount > 0 ? `<span>${s.userMsgCount} msgs</span>` : '';
34934
34959
  const providerBadge = recentSessionAgentBadgeHtml(s);
34960
+ // WP-5 (§6.6/S-14): the SECOND copy of the recent-item row template (see sessionItemHtml).
34961
+ const machineChip = machineChipHtml(s);
34935
34962
  const activeInfo = recentSessionActiveInfo(s, _activeIndex);
34936
34963
  const activeBadge = recentSessionActiveBadgeHtml(activeInfo);
34937
34964
  const activeCls = activeInfo ? ' active-session' : '';
@@ -34960,7 +34987,7 @@ function renderServerSearchResults(results, total, query, degraded) {
34960
34987
  + '<div class="recent-msg"><span class="pin-icon" style="' + (isPinned ? 'cursor:grab;' : '') + '">&#9733;</span><span class="recent-msg-text">' + highlighted + '</span></div>'
34961
34988
  + matchCtx
34962
34989
  + msgSnippet
34963
- + '<div class="recent-meta"><span class="project">' + escHtml(project) + '</span>' + activeBadge + providerBadge + branch + msgCount + '<span>' + ago + '</span></div>'
34990
+ + '<div class="recent-meta"><span class="project">' + escHtml(project) + '</span>' + activeBadge + providerBadge + machineChip + branch + msgCount + '<span>' + ago + '</span></div>'
34964
34991
  + '</div>';
34965
34992
  }).join('');
34966
34993
 
@@ -35441,11 +35468,11 @@ function onDataChanged(msg) {
35441
35468
  refreshQpPromptsIfLoaded();
35442
35469
  }
35443
35470
  if (r === 'queue-draft') {
35444
- const sid = getQpSessionId();
35445
- const changedSessionId = String(msg.id || '');
35446
- if (sid && (!changedSessionId || changedSessionId === sid)) {
35447
- qpScheduleDraftRevalidate(sid, 'data-changed');
35448
- }
35471
+ // clock-free r4 R-5: invalidate the session the echo names, not the panel's
35472
+ // session dropdown — an echo that lands while the select is empty (getQpSessionId() ===
35473
+ // null) used to be dropped, so the stale draft was never re-checked.
35474
+ const target = String(msg.id || '') || getQpSessionId() || qpCurrentSession;
35475
+ if (target) qpScheduleDraftRevalidate(target, 'data-changed');
35449
35476
  }
35450
35477
  if (r === 'auto-approvals' || r === 'approval-rules' || r === 'approval-decisions') {
35451
35478
  if (state.activeTab === 'permissions') loadAutoApprovals();
@@ -36006,10 +36033,14 @@ window.addEventListener('message', (e) => {
36006
36033
  // 'review' is the canonical code-review route; 'codereview' is kept as a legacy alias.
36007
36034
  // Both classify as nav panels (so initial-load session-restore suppression treats them
36008
36035
  // like any other panel), but they dispatch to the same internal target ('codereview').
36009
- const NAV_TARGETS = ['sessions', 'command', 'prompts', 'rules', 'insights', 'permissions', 'review', 'codereview', 'walle', 'models', 'backups', 'worktrees', 'setup'];
36036
+ const NAV_TARGETS = ['sessions', 'command', 'prompts', 'rules', 'insights', 'permissions', 'review', 'codereview', 'walle', 'models', 'backups', 'worktrees', 'machines', 'setup'];
36010
36037
  // Hash nav targets that are aliases for a different internal navTo() id.
36011
36038
  const NAV_TARGET_ALIASES = { review: 'codereview' };
36012
36039
 
36040
+ // AC-5e: the old Settings → Machines deep link resolves to the first-class `machines` page (WP-5/D-14).
36041
+ const _MACHINES_HASH_ALIAS = 'setup&tab=machines';
36042
+ function _machinesHashAliasTarget(hash) { return String(hash || '').replace(/^#/, '') === _MACHINES_HASH_ALIAS ? 'machines' : ''; }
36043
+
36013
36044
  function _parseHashRoute() {
36014
36045
  const hash = location.hash.slice(1);
36015
36046
  const parts = hash ? hash.split('&') : [];
@@ -36065,8 +36096,12 @@ function handleHashRoute() {
36065
36096
  const hash = route.hash;
36066
36097
  const routeLease = _rootPrivateRuntime?.capture?.();
36067
36098
  if (!_isRootPrivateLeaseCurrent(routeLease)) {
36068
- const publicNav = route.isNav && route.firstPart && ['setup', 'models', 'backups'].includes(route.firstPart);
36069
- if (publicNav) navTo(NAV_TARGET_ALIASES[route.firstPart] || route.firstPart, { skipHash: true });
36099
+ // Machines is the pool-admin page that replaced a Settings tab, so its deep link (AC-5e alias
36100
+ // included) resolves here alongside setup/models/backups.
36101
+ // typeof-guarded: this router is extracted and run on its own by the hash-route harnesses.
36102
+ const _publicAlias = route.isNav && typeof _machinesHashAliasTarget === 'function' ? _machinesHashAliasTarget(route.hash) : '';
36103
+ const publicNav = route.isNav && route.firstPart && ['setup', 'models', 'backups', 'machines'].includes(route.firstPart);
36104
+ if (publicNav) navTo(_publicAlias || NAV_TARGET_ALIASES[route.firstPart] || route.firstPart, { skipHash: true });
36070
36105
  return;
36071
36106
  }
36072
36107
  const deferPromptRoute = (delay, action) => {
@@ -36128,6 +36163,9 @@ function handleHashRoute() {
36128
36163
  return;
36129
36164
  }
36130
36165
 
36166
+ // #setup&tab=machines (AC-5e) — the Settings tab is gone, so the alias rewrites onto #machines.
36167
+ if (isNav && typeof _machinesHashAliasTarget === 'function' && _machinesHashAliasTarget(hash) === 'machines') { navTo('machines'); return; }
36168
+
36131
36169
  // #setup&tab=access — open setup and let SETUP.init pick the inner tab.
36132
36170
  if (isNav && firstPart === 'setup') {
36133
36171
  navTo('setup', { skipHash: true });
@@ -36959,6 +36997,11 @@ function qpReconcileSubmissionMessage(message, options = {}) {
36959
36997
  status: message.status,
36960
36998
  retryable: message.retryable === true,
36961
36999
  message: message.message,
37000
+ // WP-1 (C3, R3/R7): the broadcast frame carries the same typed recovery verdict as the
37001
+ // direct reply — pass it through so lastAttempt is never quietly downgraded to a guess.
37002
+ recoveryAction: message.recoveryAction,
37003
+ reason: message.reason,
37004
+ code: message.code,
36962
37005
  observedAt: message.observedAt || message.updated_at || message.updatedAt || Date.now(),
36963
37006
  });
36964
37007
  if (options.owner && !qpDraftLifetime.current(options.owner)) return false;
@@ -37506,6 +37549,9 @@ async function loadQpDraftForSession(sessionId) {
37506
37549
  }
37507
37550
  const loadState = qpDraftStateFor(sessionId, owner);
37508
37551
  loadState.phase = 'loading';
37552
+ // clock-free r4 R-5b: this load may only clear the invalidations it covered — an
37553
+ // echo that lands in the awaits below cannot have been seen by this fetch.
37554
+ const invalidationAtStart = loadState.invalidationSeq || 0;
37509
37555
  try {
37510
37556
  const data = (await qpFetchDraftWithRetry(sessionId, owner)) || {};
37511
37557
  if (!isQpDraftLoadCurrent(sessionId, seq, owner)) return;
@@ -37523,7 +37569,9 @@ async function loadQpDraftForSession(sessionId) {
37523
37569
  loadState.failCount = 0;
37524
37570
  loadState.lastError = '';
37525
37571
  loadState.nextRetryAt = 0;
37526
- loadState.stale = false;
37572
+ // clock-free r4 R-5b: keep stale set when an echo invalidated this session after
37573
+ // the fetch above started; only the invalidations this load covered are cleared.
37574
+ loadState.stale = (loadState.invalidationSeq || 0) !== invalidationAtStart;
37527
37575
  qpDraftFailureEpisodeActive = false;
37528
37576
  clearDegradedIndicator('queue-draft');
37529
37577
  clearToastByKey('queue-draft-load');
@@ -37546,10 +37594,11 @@ async function loadQpDraftForSession(sessionId) {
37546
37594
  await _handleQpDraftLoadFailure(sessionId, seq, error, owner);
37547
37595
  } finally {
37548
37596
  // Safety net (WP-2a): guarantee the panel never strands on "Loading draft..."
37549
- // even if something above throws unexpectedly. Only clear/repaint when this
37550
- // load still owns the panel — a stale/superseded load must never clobber a
37551
- // newer load's state.
37552
- if (await qpDraftLifetime.verify(owner) && isQpDraftLoadCurrent(sessionId, seq, owner) && qpDraftLoading) {
37597
+ // even if something above throws unexpectedly. The clear never awaits: the
37598
+ // currency check below is synchronous, so a private-scope verify that is slow
37599
+ // or fails can no longer hold the flag. A stale/superseded load must never
37600
+ // clobber a newer load's state, which isQpDraftLoadCurrent already encodes.
37601
+ if (isQpDraftLoadCurrent(sessionId, seq, owner) && qpDraftLoading) {
37553
37602
  qpDraftLoading = false;
37554
37603
  renderQpItems();
37555
37604
  updateQpModeBtn();
@@ -37573,6 +37622,9 @@ async function qpScheduleDraftRevalidate(sessionId, reason = 'invalidation', ori
37573
37622
  const loadState = qpDraftStateFor(sessionId, timerOwner);
37574
37623
  if (!qpDraftLifetime.current(timerOwner)) return false;
37575
37624
  loadState.stale = true;
37625
+ // clock-free r4 R-5b: the counter lets an in-flight load tell which invalidations
37626
+ // it predates, so completing it cannot clear an echo it never saw.
37627
+ loadState.invalidationSeq = (loadState.invalidationSeq || 0) + 1;
37576
37628
  if (sessionId !== qpCurrentSession || !qpQueuePanelVisible()) return false;
37577
37629
  if (qpEditingIdx >= 0 || qpDraftSavePending(sessionId) || qpDraftLoading) {
37578
37630
  qpDraftLifetime.defer(timerOwner, qpDeferredDraftReloads);
@@ -37623,7 +37675,6 @@ async function syncQpDraftWithServerQueue(sessionId, seq = qpDraftLoadSeq, owner
37623
37675
  try {
37624
37676
  const res = await qpDraftLifetime.request(owner, fetch, `/api/queues/${encodeURIComponent(sessionId)}`, {
37625
37677
  headers: { 'Authorization': 'Bearer ' + owner?.token },
37626
- signal: AbortSignal.timeout(4000),
37627
37678
  });
37628
37679
  if (!res || !isQpDraftLoadCurrent(sessionId, seq, owner)) return false;
37629
37680
  if (res.status === 204) {
@@ -39047,6 +39098,9 @@ async function sendQueueFromPanel(actionOwner, viewOwner) {
39047
39098
  if (ownsContext) { qpDraftLifetime.finish(actionOwner); qpDraftLifetime.finish(viewOwner); }
39048
39099
  return;
39049
39100
  }
39101
+ // WP-1 (C4, AC-1e): review-blocking happens BEFORE any dispatch, single item or auto batch, so
39102
+ // an uncertain outcome can never be sent by switching mode.
39103
+ if (_reviewQpItems(pendingItems, canPublish())) { finishOwnedAction(); return; }
39050
39104
  if (pendingItems.length === 1) {
39051
39105
  try {
39052
39106
  if (await qpMaybeRouteWorkflow(sessionId, pendingItems[0])) {
@@ -39154,6 +39208,13 @@ async function sendQueueFromPanel(actionOwner, viewOwner) {
39154
39208
  try {
39155
39209
  const session = actionOwner.session;
39156
39210
  if (!session) throw new Error('Session not connected');
39211
+ // R7 (AC-1e): Send Next invokes the recovery policy published with this item's last failure
39212
+ // instead of blindly repeating an identity the server already refused. A `review` verdict was
39213
+ // already handled by the review gate above -- it is never dispatched from here.
39214
+ const itemPolicy = window.QueuePanelSubmissionReconcile.itemRecoveryPolicy(item);
39215
+ // The certified no-effect recovery is the one place an item may drop the dead identity it
39216
+ // was admitted with -- and only because the user just asked for a fresh attempt.
39217
+ if (itemPolicy === 'resend') { delete item._submissionClientMessageId; delete item.lastAttempt; item._forceResend = true; }
39157
39218
  const displayText = qpBuildDisplayText(item);
39158
39219
  const images = (item && item.images) || [];
39159
39220
  const wantsImagePaste = images.length > 0 && window.TerminalImageTransport.providerAcceptsImagePathPaste(
@@ -39189,9 +39250,15 @@ async function sendQueueFromPanel(actionOwner, viewOwner) {
39189
39250
  clientMessageId: submissionId,
39190
39251
  status: result?.status || 'failed',
39191
39252
  retryable: result?.retryable === true,
39253
+ recoveryAction: result?.recoveryAction,
39254
+ reason: result?.reason,
39255
+ code: result?.code,
39192
39256
  message: result?.message || '',
39193
39257
  observedAt: Date.now(),
39194
39258
  }, { itemId, owner: actionOwner });
39259
+ // The explicit fresh attempt has been dispatched: this transient click intent must not
39260
+ // outlive it, or the next Send Next would mint yet another identity from the same gesture.
39261
+ delete item._forceResend;
39195
39262
  if (!result || result.status !== 'sent') {
39196
39263
  manualSendSent = false;
39197
39264
  const liveItem = canPublish() ? qpItems.find(candidate => candidate?.id === itemId
@@ -39203,12 +39270,16 @@ async function sendQueueFromPanel(actionOwner, viewOwner) {
39203
39270
  outcome: result?.status || 'unresolved',
39204
39271
  message: result?.message || '',
39205
39272
  retryable: result?.retryable === true,
39273
+ // Same typed verdict the direct reply carried, so the next Send Next follows
39274
+ // the server's policy for this failure rather than guessing one.
39275
+ recoveryAction: result?.recoveryAction,
39276
+ ...(result?.reason || result?.code ? { reason: result?.reason || result?.code } : {}),
39206
39277
  };
39207
39278
  }
39208
39279
  }
39209
39280
  const msg = result?.message || 'CTM could not verify that the prompt was accepted';
39210
39281
  if (canPublish()) toast(
39211
- result?.retryable ? 'Not sent yet — ' + msg + ' It stays queued.' : 'Not sent — ' + msg,
39282
+ result?.retryable ? 'Not sent yet — ' + msg + ' It stays queued.' : 'Prompt needs attention — ' + msg,
39212
39283
  { type: result?.retryable ? 'warning' : 'error', key: 'qp-send-failed:' + sessionId },
39213
39284
  );
39214
39285
  saveQpDraft(actionOwner);