create-walle 0.10.2 → 0.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (488) hide show
  1. package/README.md +3 -1
  2. package/lib/release-schema.json +2 -2
  3. package/lib/runtime-closure-policy.json +8 -7
  4. package/package.json +1 -1
  5. package/template/coding-task-manager/api-prompts.js +45 -7
  6. package/template/coding-task-manager/bin/claude-hook.js +55 -1
  7. package/template/coding-task-manager/bin/ctm-relay-client.js +35 -12
  8. package/template/coding-task-manager/bin/land.js +2069 -0
  9. package/template/coding-task-manager/bin/memory-census.js +4 -2
  10. package/template/coding-task-manager/bin/memory-sync-verify.js +151 -0
  11. package/template/coding-task-manager/bin/probe-refusal-scope.js +530 -0
  12. package/template/coding-task-manager/db.js +156 -32
  13. package/template/coding-task-manager/lib/acp/acp-client.js +383 -0
  14. package/template/coding-task-manager/lib/acp/dsh-context-truth.js +176 -0
  15. package/template/coding-task-manager/lib/acp/dsh-launch-model.js +302 -0
  16. package/template/coding-task-manager/lib/acp/dsh-mcp-servers.js +194 -0
  17. package/template/coding-task-manager/lib/acp/dsh-prompt-lane.js +616 -0
  18. package/template/coding-task-manager/lib/acp/dsh-route-patch.js +308 -0
  19. package/template/coding-task-manager/lib/acp/dsh-session-runtime.js +522 -0
  20. package/template/coding-task-manager/lib/acp/dsh-transcript.js +395 -0
  21. package/template/coding-task-manager/lib/acp/dsh-usage-log.js +162 -0
  22. package/template/coding-task-manager/lib/acp/ndjson-stdio-transport.js +225 -0
  23. package/template/coding-task-manager/lib/adopted-host-redial.js +47 -11
  24. package/template/coding-task-manager/lib/adopted-launch-environment.js +123 -0
  25. package/template/coding-task-manager/lib/agent-auto-update-runtime.js +0 -3
  26. package/template/coding-task-manager/lib/agent-auto-update.js +26 -10
  27. package/template/coding-task-manager/lib/agent-bundle/build.js +657 -0
  28. package/template/coding-task-manager/lib/agent-bundle/codex-install.js +558 -0
  29. package/template/coding-task-manager/lib/agent-bundle/drift.js +120 -0
  30. package/template/coding-task-manager/lib/agent-bundle/gc.js +522 -0
  31. package/template/coding-task-manager/lib/agent-bundle/inventory.js +346 -0
  32. package/template/coding-task-manager/lib/agent-bundle/push.js +717 -0
  33. package/template/coding-task-manager/lib/agent-bundle/skill-transfer.js +724 -0
  34. package/template/coding-task-manager/lib/agent-bundle/spawn.js +119 -0
  35. package/template/coding-task-manager/lib/agent-bundle/store.js +568 -0
  36. package/template/coding-task-manager/lib/agent-capabilities.js +142 -12
  37. package/template/coding-task-manager/lib/agent-cli-cache.js +30 -5
  38. package/template/coding-task-manager/lib/agent-definitions.js +19 -0
  39. package/template/coding-task-manager/lib/agent-hooks-codex-toml.js +28 -16
  40. package/template/coding-task-manager/lib/agent-hooks-codex-workspace.js +9 -1
  41. package/template/coding-task-manager/lib/agent-hooks-installer.js +23 -8
  42. package/template/coding-task-manager/lib/agent-hooks-prune.js +37 -11
  43. package/template/coding-task-manager/lib/agent-hooks-shim.js +105 -0
  44. package/template/coding-task-manager/lib/agent-presets.js +84 -1
  45. package/template/coding-task-manager/lib/agent-upgrade-jobs.js +12 -1
  46. package/template/coding-task-manager/lib/agent-version-service.js +74 -11
  47. package/template/coding-task-manager/lib/all-sessions-query.js +6 -2
  48. package/template/coding-task-manager/lib/api/agent-bundles.js +436 -0
  49. package/template/coding-task-manager/lib/api/backup-retention.js +83 -0
  50. package/template/coding-task-manager/lib/api/conversation-sync-routes.js +124 -0
  51. package/template/coding-task-manager/lib/api/diagnostics.js +549 -69
  52. package/template/coding-task-manager/lib/api/fleet.js +118 -0
  53. package/template/coding-task-manager/lib/api/hub-bridge.js +243 -0
  54. package/template/coding-task-manager/lib/api/ingress.js +9 -1
  55. package/template/coding-task-manager/lib/api/landing.js +403 -0
  56. package/template/coding-task-manager/lib/api/machines.js +691 -2
  57. package/template/coding-task-manager/lib/api/memory-sync-routes.js +335 -0
  58. package/template/coding-task-manager/lib/api/misc-ops.js +8 -3
  59. package/template/coding-task-manager/lib/api/remote-sessions.js +1326 -0
  60. package/template/coding-task-manager/lib/api/session-attach.js +623 -0
  61. package/template/coding-task-manager/lib/api/session-messages.js +1384 -0
  62. package/template/coding-task-manager/lib/api/sessions.js +77 -51
  63. package/template/coding-task-manager/lib/api/setup-network.js +544 -16
  64. package/template/coding-task-manager/lib/api/setup-wizard-handlers.js +884 -0
  65. package/template/coding-task-manager/lib/api/setup.js +829 -10
  66. package/template/coding-task-manager/lib/api/walle-bridge.js +9 -1
  67. package/template/coding-task-manager/lib/attachment-reconciler-wiring.js +5 -2
  68. package/template/coding-task-manager/lib/auth-rate-limit.js +23 -0
  69. package/template/coding-task-manager/lib/auth-rules.js +292 -21
  70. package/template/coding-task-manager/lib/background-stop.js +369 -0
  71. package/template/coding-task-manager/lib/best-of-attempts.js +4 -1
  72. package/template/coding-task-manager/lib/best-of-launch-plan.js +52 -0
  73. package/template/coding-task-manager/lib/box-keep-awake.js +89 -0
  74. package/template/coding-task-manager/lib/broadcast-reason-stats.js +152 -0
  75. package/template/coding-task-manager/lib/broadcast-serialize.js +5 -0
  76. package/template/coding-task-manager/lib/build-info.js +4 -2
  77. package/template/coding-task-manager/lib/claude-model-truth.js +13 -2
  78. package/template/coding-task-manager/lib/claude-transcript-terminal-renderer.js +17 -13
  79. package/template/coding-task-manager/lib/client-diag-handler.js +64 -2
  80. package/template/coding-task-manager/lib/codex-app-server-client.js +14 -16
  81. package/template/coding-task-manager/lib/codex-configuration-history.js +55 -0
  82. package/template/coding-task-manager/lib/codex-harness-qualification.js +43 -20
  83. package/template/coding-task-manager/lib/codex-rollout-history.js +743 -0
  84. package/template/coding-task-manager/lib/coding-agent-models.js +27 -12
  85. package/template/coding-task-manager/lib/coding-cli-skill-installer.js +44 -14
  86. package/template/coding-task-manager/lib/coding-session-model-adapters.js +43 -2
  87. package/template/coding-task-manager/lib/coding-session-model-owner.js +1 -1
  88. package/template/coding-task-manager/lib/coding-session-model-public.js +7 -1
  89. package/template/coding-task-manager/lib/coding-session-model-runtime.js +23 -4
  90. package/template/coding-task-manager/lib/coding-session-model-scan.js +31 -0
  91. package/template/coding-task-manager/lib/coding-session-model-state.js +7 -0
  92. package/template/coding-task-manager/lib/conversation-freshness-diagnostics.js +280 -0
  93. package/template/coding-task-manager/lib/conversation-item-clock.js +261 -0
  94. package/template/coding-task-manager/lib/conversation-source-registry.js +5 -0
  95. package/template/coding-task-manager/lib/conversation-store-v2.js +44 -0
  96. package/template/coding-task-manager/lib/conversation-sync.js +355 -0
  97. package/template/coding-task-manager/lib/create/resolve-launch-params.js +96 -12
  98. package/template/coding-task-manager/lib/create/worker-role-profile.js +4 -4
  99. package/template/coding-task-manager/lib/create-intent-lease.js +51 -0
  100. package/template/coding-task-manager/lib/ctm-session-context-api.js +97 -6
  101. package/template/coding-task-manager/lib/db-domain-tables.js +1 -0
  102. package/template/coding-task-manager/lib/db-domains.js +1 -0
  103. package/template/coding-task-manager/lib/db-owner-dispatch-probe.js +32 -0
  104. package/template/coding-task-manager/lib/db-owner-request-counts.js +178 -0
  105. package/template/coding-task-manager/lib/db-owner-worker-client.js +68 -14
  106. package/template/coding-task-manager/lib/db-schema-source-files.js +5 -0
  107. package/template/coding-task-manager/lib/deferred-restore-attempt.js +6 -2
  108. package/template/coding-task-manager/lib/feature-manifest.js +4 -0
  109. package/template/coding-task-manager/lib/fleet-conversation-route.js +97 -0
  110. package/template/coding-task-manager/lib/fleet-fanout.js +78 -0
  111. package/template/coding-task-manager/lib/fleet-hub.js +762 -0
  112. package/template/coding-task-manager/lib/fleet-origin-follower.js +252 -0
  113. package/template/coding-task-manager/lib/fleet-resolver-effects.js +147 -0
  114. package/template/coding-task-manager/lib/fleet-resolver-tick.js +148 -0
  115. package/template/coding-task-manager/lib/fleet-resolver.js +454 -0
  116. package/template/coding-task-manager/lib/fleet-session-list-routes.js +114 -0
  117. package/template/coding-task-manager/lib/fleet-session-lists.js +672 -0
  118. package/template/coding-task-manager/lib/fleet-update.js +798 -0
  119. package/template/coding-task-manager/lib/fs-watcher-root-watch.js +141 -0
  120. package/template/coding-task-manager/lib/fs-watcher.js +58 -42
  121. package/template/coding-task-manager/lib/harness-history-transition.js +50 -9
  122. package/template/coding-task-manager/lib/headless-term-service.js +84 -8
  123. package/template/coding-task-manager/lib/hermetic-agent-launch-guard.js +10 -0
  124. package/template/coding-task-manager/lib/history-transition-read-op.js +175 -8
  125. package/template/coding-task-manager/lib/host-cpu-categories.js +310 -0
  126. package/template/coding-task-manager/lib/host-fseventsd-sampler.js +399 -0
  127. package/template/coding-task-manager/lib/host-handoff.js +16 -19
  128. package/template/coding-task-manager/lib/host-stats.js +150 -3
  129. package/template/coding-task-manager/lib/hosted-sync-worker.js +43 -320
  130. package/template/coding-task-manager/lib/hub-bridge-access.js +798 -0
  131. package/template/coding-task-manager/lib/hub-bridge-box.js +409 -0
  132. package/template/coding-task-manager/lib/hub-bridge-mcp.js +380 -0
  133. package/template/coding-task-manager/lib/hub-bridge-mesh.js +313 -0
  134. package/template/coding-task-manager/lib/hub-bridge-ops.js +721 -0
  135. package/template/coding-task-manager/lib/hub-bridge.js +398 -0
  136. package/template/coding-task-manager/lib/idle-notify.js +491 -0
  137. package/template/coding-task-manager/lib/instance-id-ownership-manifest.js +8 -0
  138. package/template/coding-task-manager/lib/instance-manifest.js +5 -1
  139. package/template/coding-task-manager/lib/instance-sync-status.js +18 -8
  140. package/template/coding-task-manager/lib/landing/agent-rule.txt +1 -0
  141. package/template/coding-task-manager/lib/landing/batch-runner.js +366 -0
  142. package/template/coding-task-manager/lib/landing/config.js +536 -0
  143. package/template/coding-task-manager/lib/landing/ctm-view.js +256 -0
  144. package/template/coding-task-manager/lib/landing/gate.js +360 -0
  145. package/template/coding-task-manager/lib/landing/git.js +344 -0
  146. package/template/coding-task-manager/lib/landing/lease.js +247 -0
  147. package/template/coding-task-manager/lib/landing/merge-delta.js +380 -0
  148. package/template/coding-task-manager/lib/landing/proof.js +223 -0
  149. package/template/coding-task-manager/lib/landing/queue.js +998 -0
  150. package/template/coding-task-manager/lib/landing/radar.js +489 -0
  151. package/template/coding-task-manager/lib/landing/resolve-cli.js +424 -0
  152. package/template/coding-task-manager/lib/landing/state.js +263 -0
  153. package/template/coding-task-manager/lib/landing/stop.js +49 -0
  154. package/template/coding-task-manager/lib/landing/sync-cli.js +475 -0
  155. package/template/coding-task-manager/lib/landing/sync.js +456 -0
  156. package/template/coding-task-manager/lib/landing/train.js +885 -0
  157. package/template/coding-task-manager/lib/landing/window.js +57 -0
  158. package/template/coding-task-manager/lib/launch-environment.js +121 -0
  159. package/template/coding-task-manager/lib/launch-model-override.js +36 -10
  160. package/template/coding-task-manager/lib/launch-plan-pool.js +2 -1
  161. package/template/coding-task-manager/lib/launch-plan-warm-cache.js +6 -0
  162. package/template/coding-task-manager/lib/lead-model-observer.js +57 -1
  163. package/template/coding-task-manager/lib/lead-model-options.js +160 -3
  164. package/template/coding-task-manager/lib/lead-model-restore-reconciliation.js +106 -24
  165. package/template/coding-task-manager/lib/lead-model-switch-runtime.js +352 -68
  166. package/template/coding-task-manager/lib/lead-model-switch.js +114 -10
  167. package/template/coding-task-manager/lib/lead-model-transition-runtime.js +14 -2
  168. package/template/coding-task-manager/lib/lead-model-transition-store.js +24 -6
  169. package/template/coding-task-manager/lib/lead-route-preflight.js +179 -0
  170. package/template/coding-task-manager/lib/lead-switch-history-mode.js +110 -0
  171. package/template/coding-task-manager/lib/lead-wire-agent.js +7 -0
  172. package/template/coding-task-manager/lib/lifecycle-wake-experiment.js +9 -2
  173. package/template/coding-task-manager/lib/live-defer-announce.js +45 -0
  174. package/template/coding-task-manager/lib/machine-registry.js +750 -5
  175. package/template/coding-task-manager/lib/macos-capabilities.js +32 -15
  176. package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +433 -53
  177. package/template/coding-task-manager/lib/mainloop-observability.js +14 -9
  178. package/template/coding-task-manager/lib/media-draft-owner.js +16 -3
  179. package/template/coding-task-manager/lib/memory-join.js +337 -0
  180. package/template/coding-task-manager/lib/memory-sync-verify.js +393 -0
  181. package/template/coding-task-manager/lib/memory-sync.js +565 -0
  182. package/template/coding-task-manager/lib/migrations/v57-delegate-pref-mirror-opencode.js +68 -0
  183. package/template/coding-task-manager/lib/migrations/v58-subagent-launch-native-subagents.js +42 -0
  184. package/template/coding-task-manager/lib/migrations/v59-coding-session-model-state-dsh.js +73 -0
  185. package/template/coding-task-manager/lib/migrations/v60-delegate-pref-mirror-dsh.js +78 -0
  186. package/template/coding-task-manager/lib/migrations/v61-conversation-item-clock.js +71 -0
  187. package/template/coding-task-manager/lib/model-availability.js +727 -0
  188. package/template/coding-task-manager/lib/model-execution-recipe-metadata.js +4 -6
  189. package/template/coding-task-manager/lib/model-host-receipt.js +6 -3
  190. package/template/coding-task-manager/lib/model-id-rule.js +19 -0
  191. package/template/coding-task-manager/lib/model-proxy/anthropic-face.js +4 -0
  192. package/template/coding-task-manager/lib/model-proxy/auto-mode-review.js +110 -23
  193. package/template/coding-task-manager/lib/model-proxy/chat-response-translators.js +3 -0
  194. package/template/coding-task-manager/lib/model-proxy/generation-probe.js +60 -17
  195. package/template/coding-task-manager/lib/model-proxy/historical-tool-declarations.js +37 -0
  196. package/template/coding-task-manager/lib/model-proxy/lead-request-validation.js +1 -0
  197. package/template/coding-task-manager/lib/model-proxy/lead-token-admin.js +130 -0
  198. package/template/coding-task-manager/lib/model-proxy/refusal-scope.js +151 -0
  199. package/template/coding-task-manager/lib/model-proxy/request-budget-store.js +11 -2
  200. package/template/coding-task-manager/lib/model-proxy/tokens.js +69 -5
  201. package/template/coding-task-manager/lib/model-proxy/upstream-outcome.js +9 -0
  202. package/template/coding-task-manager/lib/native-agent-model-args.js +25 -22
  203. package/template/coding-task-manager/lib/native-history-control.js +6 -4
  204. package/template/coding-task-manager/lib/native-lead-settings-client.js +44 -4
  205. package/template/coding-task-manager/lib/oauth-proxy-supervisor.js +173 -36
  206. package/template/coding-task-manager/lib/online-backup-worker-client.js +2 -0
  207. package/template/coding-task-manager/lib/opencode-cli.js +480 -0
  208. package/template/coding-task-manager/lib/opencode-launch-model.js +471 -0
  209. package/template/coding-task-manager/lib/opencode-v1-plugin/model-carrier.mjs +35 -0
  210. package/template/coding-task-manager/lib/opencode-v2-plugin/server.js +118 -0
  211. package/template/coding-task-manager/lib/owner-action-manifest-data.json +1 -1
  212. package/template/coding-task-manager/lib/park-composer-draft.js +54 -0
  213. package/template/coding-task-manager/lib/park-fresh-relaunch.js +41 -10
  214. package/template/coding-task-manager/lib/perf-phase-trace.js +72 -0
  215. package/template/coding-task-manager/lib/perf-summary.js +7 -0
  216. package/template/coding-task-manager/lib/perf-tracker.js +63 -2
  217. package/template/coding-task-manager/lib/project-config.js +7 -0
  218. package/template/coding-task-manager/lib/prompt-purge-v2.js +52 -1
  219. package/template/coding-task-manager/lib/provider-login-broker.js +72 -15
  220. package/template/coding-task-manager/lib/provider-login-tab.js +10 -2
  221. package/template/coding-task-manager/lib/read-pool-client.js +89 -2
  222. package/template/coding-task-manager/lib/read-pool-worker-handle.js +29 -4
  223. package/template/coding-task-manager/lib/recent-catalog-query.js +15 -0
  224. package/template/coding-task-manager/lib/recent-session-catalog-response.js +67 -10
  225. package/template/coding-task-manager/lib/recovery-evidence.js +26 -11
  226. package/template/coding-task-manager/lib/relay-client.js +57 -3
  227. package/template/coding-task-manager/lib/relink-audit.js +34 -5
  228. package/template/coding-task-manager/lib/remote-machine-catalog.js +321 -26
  229. package/template/coding-task-manager/lib/remote-machine-create.js +576 -36
  230. package/template/coding-task-manager/lib/remote-machine-link.js +546 -59
  231. package/template/coding-task-manager/lib/remote-machine-proxy.js +143 -9
  232. package/template/coding-task-manager/lib/remote-session-forward.js +65 -7
  233. package/template/coding-task-manager/lib/remote-terminal-target.js +9 -2
  234. package/template/coding-task-manager/lib/restart-guard.js +18 -6
  235. package/template/coding-task-manager/lib/restore-seed-modes.js +87 -0
  236. package/template/coding-task-manager/lib/restored-scrollback-hydration-runtime.js +22 -2
  237. package/template/coding-task-manager/lib/resume-admission.js +17 -3
  238. package/template/coding-task-manager/lib/review-repo-registry.js +58 -19
  239. package/template/coding-task-manager/lib/root-delivery-runtime.js +2 -2
  240. package/template/coding-task-manager/lib/root-prompt-flow.js +1 -1
  241. package/template/coding-task-manager/lib/runtime-contract.js +3 -1
  242. package/template/coding-task-manager/lib/schema-indexes.js +1 -0
  243. package/template/coding-task-manager/lib/session-context-locator.js +742 -0
  244. package/template/coding-task-manager/lib/session-create/attachment.js +2 -2
  245. package/template/coding-task-manager/lib/session-create/create-phases.js +118 -0
  246. package/template/coding-task-manager/lib/session-create/delegate-worker.js +86 -16
  247. package/template/coding-task-manager/lib/session-create/dsh-session.js +930 -0
  248. package/template/coding-task-manager/lib/session-create/lead-route-launch.js +186 -13
  249. package/template/coding-task-manager/lib/session-create/native-lead-lifetime.js +55 -3
  250. package/template/coding-task-manager/lib/session-create/opencode-lead-provider.js +50 -0
  251. package/template/coding-task-manager/lib/session-create/prepared.js +162 -38
  252. package/template/coding-task-manager/lib/session-create/provider-process.js +160 -28
  253. package/template/coding-task-manager/lib/session-create/response.js +7 -7
  254. package/template/coding-task-manager/lib/session-create/structured-session.js +27 -0
  255. package/template/coding-task-manager/lib/session-create/subagent-pick-table.js +3 -1
  256. package/template/coding-task-manager/lib/session-create/subagent-pick.js +458 -9
  257. package/template/coding-task-manager/lib/session-create/transcript.js +1 -1
  258. package/template/coding-task-manager/lib/session-create/worker-budget-setup.js +21 -2
  259. package/template/coding-task-manager/lib/session-create/worker-continuation.js +7 -1
  260. package/template/coding-task-manager/lib/session-create/worker-launch-sandbox.js +39 -10
  261. package/template/coding-task-manager/lib/session-create/worker-observability.js +2 -2
  262. package/template/coding-task-manager/lib/session-empty.js +13 -10
  263. package/template/coding-task-manager/lib/session-host-manager.js +60 -0
  264. package/template/coding-task-manager/lib/session-jobs.js +3 -1
  265. package/template/coding-task-manager/lib/session-lifecycle.js +10 -4
  266. package/template/coding-task-manager/lib/session-model-action-preconditions.js +6 -2
  267. package/template/coding-task-manager/lib/session-model-intent-contract.js +11 -8
  268. package/template/coding-task-manager/lib/session-park.js +738 -0
  269. package/template/coding-task-manager/lib/session-path-grant.js +13 -4
  270. package/template/coding-task-manager/lib/session-status-ledger.js +106 -4
  271. package/template/coding-task-manager/lib/session-transport.js +17 -0
  272. package/template/coding-task-manager/lib/session-workspace-projection.js +54 -5
  273. package/template/coding-task-manager/lib/session-workspace-tracking-runtime.js +213 -1
  274. package/template/coding-task-manager/lib/skill-autocomplete-query.js +120 -3
  275. package/template/coding-task-manager/lib/skill-autocomplete.js +15 -1
  276. package/template/coding-task-manager/lib/sqlite-file-set.js +39 -0
  277. package/template/coding-task-manager/lib/standup-search-adapter.js +16 -2
  278. package/template/coding-task-manager/lib/state-sync/client-ledger.js +12 -0
  279. package/template/coding-task-manager/lib/state-sync/frame-emitter.js +319 -100
  280. package/template/coding-task-manager/lib/storage/coding-session-model-store.js +4 -1
  281. package/template/coding-task-manager/lib/storage/delegate-pref-mirror-store.js +32 -9
  282. package/template/coding-task-manager/lib/structured-session-boot-sweep.js +235 -0
  283. package/template/coding-task-manager/lib/submission-ack.js +90 -2
  284. package/template/coding-task-manager/lib/systemd-scope.js +115 -0
  285. package/template/coding-task-manager/lib/telemetry-receiver.js +39 -0
  286. package/template/coding-task-manager/lib/terminal-attach-geometry.js +985 -0
  287. package/template/coding-task-manager/lib/terminal-bootstrap.js +30 -0
  288. package/template/coding-task-manager/lib/terminal-draft-operation.js +357 -0
  289. package/template/coding-task-manager/lib/terminal-input-admission.js +17 -1
  290. package/template/coding-task-manager/lib/terminal-input-prepared.js +563 -0
  291. package/template/coding-task-manager/lib/transcript-cache-temps.js +171 -0
  292. package/template/coding-task-manager/lib/transcript-frame-cache.js +8 -3
  293. package/template/coding-task-manager/lib/transcript-source.js +36 -4
  294. package/template/coding-task-manager/lib/transcript-store.js +9 -0
  295. package/template/coding-task-manager/lib/typing-interaction-trace.js +106 -3
  296. package/template/coding-task-manager/lib/usage-ledger/backfill.js +40 -5
  297. package/template/coding-task-manager/lib/usage-ledger/client.js +4 -1
  298. package/template/coding-task-manager/lib/usage-ledger/dsh-parse.js +152 -0
  299. package/template/coding-task-manager/lib/usage-ledger/dsh-read.js +73 -0
  300. package/template/coding-task-manager/lib/usage-ledger/ingest.js +34 -6
  301. package/template/coding-task-manager/lib/usage-ledger/live.js +35 -8
  302. package/template/coding-task-manager/lib/usage-ledger/read.js +29 -20
  303. package/template/coding-task-manager/lib/usage-ledger/schema.js +61 -1
  304. package/template/coding-task-manager/lib/usage-ledger/task-coordinator.js +4 -2
  305. package/template/coding-task-manager/lib/wake-skip-reasons.js +1 -0
  306. package/template/coding-task-manager/lib/walle/turn.js +1 -1
  307. package/template/coding-task-manager/lib/walle-history-serve.js +838 -0
  308. package/template/coding-task-manager/lib/walle-kill-authority.js +2 -2
  309. package/template/coding-task-manager/lib/walle-log-rotation.js +164 -1
  310. package/template/coding-task-manager/lib/walle-mcp-auto-config.js +56 -2
  311. package/template/coding-task-manager/lib/walle-row-history.js +495 -0
  312. package/template/coding-task-manager/lib/walle-supervisor.js +33 -14
  313. package/template/coding-task-manager/lib/watch-worker-client.js +203 -0
  314. package/template/coding-task-manager/lib/worker-terminal-output.js +5 -2
  315. package/template/coding-task-manager/lib/ws/hub-bridge.js +35 -0
  316. package/template/coding-task-manager/lib/ws/lifecycle.js +6 -11
  317. package/template/coding-task-manager/lib/ws/misc.js +1 -1
  318. package/template/coding-task-manager/lib/ws/state-sync.js +106 -4
  319. package/template/coding-task-manager/lib/ws/terminal-attach.js +91 -0
  320. package/template/coding-task-manager/oauth-proxy.js +112 -115
  321. package/template/coding-task-manager/package-lock.json +10 -94
  322. package/template/coding-task-manager/package.json +4 -3
  323. package/template/coding-task-manager/protocol/message-table.json +10 -0
  324. package/template/coding-task-manager/protocol/messages.js +10 -0
  325. package/template/coding-task-manager/protocol/ws-schema.json +20 -0
  326. package/template/coding-task-manager/protocol/ws-schema.md +2 -0
  327. package/template/coding-task-manager/public/css/app.css +170 -0
  328. package/template/coding-task-manager/public/css/walle-session.css +4 -0
  329. package/template/coding-task-manager/public/index.html +473 -243
  330. package/template/coding-task-manager/public/js/agent-command.js +86 -0
  331. package/template/coding-task-manager/public/js/app/backups-panel.js +111 -5
  332. package/template/coding-task-manager/public/js/app/coding-agent-upgrade.js +103 -4
  333. package/template/coding-task-manager/public/js/app/machines-page.js +1328 -8
  334. package/template/coding-task-manager/public/js/app/models-tab.js +113 -38
  335. package/template/coding-task-manager/public/js/app/restart-banner.js +53 -0
  336. package/template/coding-task-manager/public/js/app/worktrees-panel.js +184 -2
  337. package/template/coding-task-manager/public/js/background-work.js +196 -27
  338. package/template/coding-task-manager/public/js/coding-session-model-presentation.js +138 -10
  339. package/template/coding-task-manager/public/js/core/active-session-groups.js +179 -0
  340. package/template/coding-task-manager/public/js/core/pending-create.js +238 -0
  341. package/template/coding-task-manager/public/js/core/recent-machine-source.js +228 -0
  342. package/template/coding-task-manager/public/js/core/session-commands.js +180 -14
  343. package/template/coding-task-manager/public/js/dashboard-client-function-order.json +8 -0
  344. package/template/coding-task-manager/public/js/document-review-links.js +9 -1
  345. package/template/coding-task-manager/public/js/file-resolution-client.js +52 -1
  346. package/template/coding-task-manager/public/js/history-app-seam.js +22 -2
  347. package/template/coding-task-manager/public/js/machine-pool-shared.js +558 -3
  348. package/template/coding-task-manager/public/js/message-renderer.js +4 -3
  349. package/template/coding-task-manager/public/js/model-picker.js +3 -1
  350. package/template/coding-task-manager/public/js/native-subagent-review.js +15 -2
  351. package/template/coding-task-manager/public/js/new-session-agent-grid.js +346 -0
  352. package/template/coding-task-manager/public/js/pasted-content.js +236 -0
  353. package/template/coding-task-manager/public/js/pe-private-content-lifetime.js +39 -7
  354. package/template/coding-task-manager/public/js/private-model-lifetime.js +6 -2
  355. package/template/coding-task-manager/public/js/prompts.js +11 -10
  356. package/template/coding-task-manager/public/js/protocol.js +10 -0
  357. package/template/coding-task-manager/public/js/restart-banner-policy.js +29 -1
  358. package/template/coding-task-manager/public/js/session-model-picker.js +447 -57
  359. package/template/coding-task-manager/public/js/session-search-utils.js +18 -2
  360. package/template/coding-task-manager/public/js/session-usage-view.js +2 -1
  361. package/template/coding-task-manager/public/js/session-workspace-tracking.js +51 -1
  362. package/template/coding-task-manager/public/js/state-sync-client.js +10 -1
  363. package/template/coding-task-manager/public/js/stream-view.js +6 -5
  364. package/template/coding-task-manager/public/js/terminal/conv-only-session.js +287 -0
  365. package/template/coding-task-manager/public/js/terminal/runtime-09.js +2 -0
  366. package/template/coding-task-manager/public/js/terminal/runtime-10.js +5 -5
  367. package/template/coding-task-manager/public/js/terminal/runtime-12.js +8 -1
  368. package/template/coding-task-manager/public/js/terminal/runtime-16.js +4 -0
  369. package/template/coding-task-manager/public/js/terminal/runtime-20.js +1 -1
  370. package/template/coding-task-manager/public/js/terminal-attachment-draft.js +67 -13
  371. package/template/coding-task-manager/public/js/terminal-image-orchestration.js +52 -6
  372. package/template/coding-task-manager/public/js/terminal-wheel.js +23 -3
  373. package/template/coding-task-manager/public/js/walle/chat-conversation.js +4 -4
  374. package/template/coding-task-manager/public/js/walle.js +6 -0
  375. package/template/coding-task-manager/public/js/ws/live-dispatch.js +10 -0
  376. package/template/coding-task-manager/public/m/app.css +140 -0
  377. package/template/coding-task-manager/public/m/app.js +507 -68
  378. package/template/coding-task-manager/public/m/index.html +17 -0
  379. package/template/coding-task-manager/public/m/machines.js +838 -4
  380. package/template/coding-task-manager/public/m/private-api.js +31 -1
  381. package/template/coding-task-manager/public/usage.html +1 -0
  382. package/template/coding-task-manager/scripts/dev-route-fixture/synthetic-route-upstream.js +104 -0
  383. package/template/coding-task-manager/scripts/dev-route-fixture.js +339 -0
  384. package/template/coding-task-manager/scripts/opencode-lead-route-probe.js +195 -0
  385. package/template/coding-task-manager/scripts/opencode-v2-contract-probe/fake-anthropic.js +168 -0
  386. package/template/coding-task-manager/scripts/opencode-v2-contract-probe/probe-plugin/server.js +95 -0
  387. package/template/coding-task-manager/scripts/opencode-v2-contract-probe.js +673 -0
  388. package/template/coding-task-manager/server.js +1950 -11679
  389. package/template/coding-task-manager/session-integrity.js +64 -4
  390. package/template/coding-task-manager/workers/db-owner-worker.js +19 -2
  391. package/template/coding-task-manager/workers/online-backup-worker.js +1 -0
  392. package/template/coding-task-manager/workers/read-pool-model-collector.js +51 -3
  393. package/template/coding-task-manager/workers/read-pool-worker.js +76 -6
  394. package/template/coding-task-manager/workers/session-host-pool-process.js +6 -6
  395. package/template/coding-task-manager/workers/session-host-process.js +5 -5
  396. package/template/coding-task-manager/workers/watch-worker.js +105 -0
  397. package/template/coding-task-manager/workers/worktree-worker.js +168 -33
  398. package/template/ctm-relay/lib/protocol.js +14 -1
  399. package/template/package-lock.json +2 -2
  400. package/template/package.json +1 -1
  401. package/template/shared/backup-retention.js +82 -0
  402. package/template/shared/conversation-admission.js +2 -0
  403. package/template/shared/conversation-provenance.js +3 -7
  404. package/template/shared/conversation-storage-v2-contract.js +5 -0
  405. package/template/shared/linked-worktree.js +51 -0
  406. package/template/shared/macos-fda-target.js +57 -0
  407. package/template/shared/orphan-parents.js +55 -0
  408. package/template/shared/release-schema.json +2 -2
  409. package/template/shared/storage/contract.js +3 -0
  410. package/template/wall-e/adapters/imessage.js +12 -0
  411. package/template/wall-e/agent.js +71 -4
  412. package/template/wall-e/api-walle.js +21 -1
  413. package/template/wall-e/bin/train-gemma-e4b-tooluse.js +2 -2
  414. package/template/wall-e/bin/walle-mcp-stdio.js +5 -5
  415. package/template/wall-e/brain/brain-join.js +859 -0
  416. package/template/wall-e/brain/brain-sync-membership.js +20 -0
  417. package/template/wall-e/brain/brain-sync-store.js +131 -77
  418. package/template/wall-e/brain/brain-sync-v2-schema.js +203 -0
  419. package/template/wall-e/brain/brain-sync-v2.js +578 -0
  420. package/template/wall-e/brain/delegate-model-prefs-store.js +13 -4
  421. package/template/wall-e/brain/deterministic-id.js +51 -0
  422. package/template/wall-e/brain/memories.js +2 -2
  423. package/template/wall-e/brain/migrations.js +40 -6
  424. package/template/wall-e/brain/model-identity-migration.js +1 -1
  425. package/template/wall-e/brain/owner-call-whitelist.js +23 -7
  426. package/template/wall-e/brain/schema.js +45 -85
  427. package/template/wall-e/brain.js +171 -35
  428. package/template/wall-e/chat.js +3 -3
  429. package/template/wall-e/coding/agent-shell-env.js +7 -2
  430. package/template/wall-e/coding/prompt-bundle.js +66 -21
  431. package/template/wall-e/config/main-loop-exemptions.json +2 -2
  432. package/template/wall-e/http/admin-api.js +37 -0
  433. package/template/wall-e/http/gws-api.js +4 -0
  434. package/template/wall-e/http/helper-rules-api.js +7 -1
  435. package/template/wall-e/http/model-admin.js +8 -1
  436. package/template/wall-e/http/model-delegate-pref-admin.js +3 -3
  437. package/template/wall-e/http/model-proxy-providers.js +174 -1
  438. package/template/wall-e/http/model-route-health.js +157 -0
  439. package/template/wall-e/http/sync-api.js +278 -464
  440. package/template/wall-e/jobs/job-runner.js +28 -1
  441. package/template/wall-e/lib/brain-owner-op-priority.js +9 -1
  442. package/template/wall-e/lib/ctm-data-dir.js +26 -0
  443. package/template/wall-e/lib/derivation-home.js +147 -0
  444. package/template/wall-e/lib/helper-rules/evaluate.js +50 -9
  445. package/template/wall-e/lib/helper-rules/grade.js +51 -8
  446. package/template/wall-e/lib/helper-rules/lead-call-hook.js +38 -5
  447. package/template/wall-e/lib/helper-rules/store.js +4 -2
  448. package/template/wall-e/lib/landing-mcp-tools.js +170 -0
  449. package/template/wall-e/lib/mcp-client-config.js +133 -14
  450. package/template/wall-e/lib/mcp-gateway-migrate.js +28 -64
  451. package/template/wall-e/lib/mcp-integration.js +58 -13
  452. package/template/wall-e/lib/parent-death.js +7 -1
  453. package/template/wall-e/lib/scheduler-worker-jobs.js +6 -4
  454. package/template/wall-e/llm/delegate-lead-prefs.js +16 -1
  455. package/template/wall-e/llm/delegate-vendor-default.js +12 -5
  456. package/template/wall-e/llm/model-roles.js +1 -1
  457. package/template/wall-e/llm/native-delegate-selection.js +56 -8
  458. package/template/wall-e/llm/worker-delegate-args.js +72 -7
  459. package/template/wall-e/llm/worker-mcp-routing.js +66 -17
  460. package/template/wall-e/loops/brain-custom-writes.js +19 -0
  461. package/template/wall-e/loops/ingest.js +17 -1
  462. package/template/wall-e/loops/think.js +19 -4
  463. package/template/wall-e/mcp-contract.js +222 -6
  464. package/template/wall-e/mcp-protocol-contract.js +40 -5
  465. package/template/wall-e/mcp-server.js +128 -29
  466. package/template/wall-e/mcp-worker-schemas.js +5 -5
  467. package/template/wall-e/memory/ctm-context-client.js +85 -0
  468. package/template/wall-e/memory/ctm-session-context.js +9 -3
  469. package/template/wall-e/memory/hub-bridge-client.js +388 -0
  470. package/template/wall-e/memory/replica-origin.js +56 -0
  471. package/template/wall-e/memory/session-pool.js +135 -0
  472. package/template/wall-e/runtime/live-capabilities.js +8 -3
  473. package/template/wall-e/server.js +4 -2
  474. package/template/wall-e/shared/release-schema.json +2 -2
  475. package/template/wall-e/shared/sqlite-file-set.js +39 -0
  476. package/template/wall-e/shared/storage/contract.js +3 -0
  477. package/template/wall-e/skills/_templates/delegate-worker/VERSION +1 -1
  478. package/template/wall-e/skills/_templates/delegate-worker/released-sha256.json +7 -1
  479. package/template/wall-e/skills/_templates/delegate-worker/templates/claude/SKILL.md +4 -4
  480. package/template/wall-e/skills/_templates/delegate-worker/templates/codex/SKILL.md +6 -6
  481. package/template/wall-e/skills/_templates/fleet-sync/VERSION +1 -0
  482. package/template/wall-e/skills/_templates/fleet-sync/released-sha256.json +16 -0
  483. package/template/wall-e/skills/_templates/fleet-sync/templates/claude/SKILL.md +64 -0
  484. package/template/wall-e/skills/_templates/fleet-sync/templates/codex/SKILL.md +64 -0
  485. package/template/wall-e/tools/chrome-automation.js +6 -0
  486. package/template/wall-e/training/setup-cuda-train-env.sh +2 -2
  487. package/template/wall-e/workers/brain-owner-worker.js +18 -4
  488. package/template/wall-e/workers/mcp-gateway-process.js +5 -5
@@ -224,10 +224,10 @@
224
224
  <div id="nav-more-wrap">
225
225
  <button class="nav-pill" id="nav-more-button" onclick="toggleNavMore()" title="More pages" aria-haspopup="menu" aria-expanded="false">More <span style="font-size:10px;">&#9662;</span></button>
226
226
  <div id="nav-more-dropdown" role="menu" aria-label="More pages">
227
+ <button class="nav-pill" data-nav="machines" onclick="navTo('machines');closeNavMore()" title="Machines — how this Mac is used and by which sessions" role="menuitem">Machines</button>
227
228
  <button class="nav-pill" data-nav="insights" onclick="navTo('insights');closeNavMore()" role="menuitem">Insights</button>
228
229
  <button class="nav-pill" data-nav="permissions" onclick="navTo('permissions');closeNavMore()" role="menuitem">Permissions</button>
229
230
  <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>
231
231
  </div>
232
232
  </div>
233
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>
@@ -310,6 +310,10 @@
310
310
  </span>
311
311
  <button id="restart-banner-reload" type="button" onclick="reloadForInstalledUpdate('banner')" hidden>Reload</button>
312
312
  <button id="restart-banner-dismiss" type="button" onclick="dismissRestartBanner()" title="Dismiss" aria-label="Dismiss restart notice" hidden>&times;</button>
313
+ <!-- WP-2 (docs/external-causality-kill-session-loss-2026-10-27.html §5 D-5, §6, §8 WP-2, AC-2a):
314
+ the dead adopted host's ONE action. Sits after the restart/reload pair so the existing
315
+ restart-group geometry is untouched; visibility + copy come from RestartBannerPolicy. -->
316
+ <button id="restart-banner-resume" type="button" onclick="resumeHostDeadSessions()" hidden>Resume</button>
313
317
  </span>
314
318
  </div>
315
319
  <div class="modal-overlay update-wizard-overlay hidden" id="update-wizard" role="dialog" aria-modal="true" aria-labelledby="update-wizard-heading">
@@ -346,6 +350,7 @@
346
350
  <div class="sidebar-section">
347
351
  <div style="display:flex;align-items:center;justify-content:space-between;gap:6px;">
348
352
  <h3 style="margin-bottom:0">Active Sessions</h3>
353
+ <span id="active-computer-filter-chip" style="margin-left:auto"></span>
349
354
  <select id="active-sort" onchange="setActiveSort(this.value)" title="Sort active sessions" style="background:var(--bg);color:var(--fg-dim);border:1px solid var(--border);padding:2px 4px;border-radius:3px;font-size:10px;cursor:pointer;">
350
355
  <option value="by_activity">By activity</option>
351
356
  <option value="tab_order">Tab order</option>
@@ -359,7 +364,7 @@
359
364
  </div>
360
365
  <div class="sidebar-section" style="border-top:1px solid var(--border)">
361
366
  <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;">
362
- <h3 style="margin-bottom:0;display:flex;align-items:center;gap:6px;">Recent Sessions <span id="recent-refreshing-chip" class="recent-refreshing-chip" hidden>Refreshing</span></h3>
367
+ <h3 style="margin-bottom:0;display:flex;align-items:center;gap:6px;">Recent Sessions <span id="recent-refreshing-chip" class="recent-refreshing-chip" hidden>Refreshing</span> <span id="recent-computer-filter-chip"></span></h3>
363
368
  <button class="mini-btn delete" onclick="cleanEmptySessions()" title="Delete all empty sessions" style="background:var(--bg);border:1px solid var(--border);color:var(--red);padding:2px 6px;border-radius:3px;font-size:9px;cursor:pointer;">Clean Empty</button>
364
369
  </div>
365
370
  <div id="prefs-load-error" role="status">
@@ -2274,7 +2279,7 @@ url = "http://localhost:<span id="setup-mcp-port-display-toml">3457</span>/mcp"<
2274
2279
  <button class="btn primary small" onclick="PE.createManualBackup()">Backup Now</button>
2275
2280
  </div>
2276
2281
  <div style="padding:12px;font-size:12px;color:var(--fg-dim);border-bottom:1px solid var(--border);background:var(--bg-light);">
2277
- Daily auto-backups are created automatically. Backups older than 30 days are cleaned up (minimum 5 kept).
2282
+ Daily auto-backups are created automatically. Old backups are removed after each backup — set how many days to keep in Setup → Backups (default 3).
2278
2283
  </div>
2279
2284
  <div id="pe-backup-list" style="flex:1;overflow-y:auto;padding:10px;"></div>
2280
2285
  </div>
@@ -2778,7 +2783,7 @@ url = "http://localhost:<span id="setup-mcp-port-display-toml">3457</span>/mcp"<
2778
2783
  <script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js" crossorigin="anonymous"></script>
2779
2784
  <script src="js/session-surface-runtime.js?v=__CTM_ASSET_EPOCH__"></script>
2780
2785
  <script src="js/presentation-arbiter.js?v=__CTM_ASSET_EPOCH__"></script>
2781
- <script src="js/lifecycle-cover-render.js?v=__CTM_ASSET_EPOCH__"></script><script src="js/porter-stemmer.js?v=__CTM_ASSET_EPOCH__"></script><script src="js/session-search-utils.js?v=__CTM_ASSET_EPOCH__"></script><script src="js/recent-active-identity-index.js?v=__CTM_ASSET_EPOCH__"></script>
2786
+ <script src="js/lifecycle-cover-render.js?v=__CTM_ASSET_EPOCH__"></script><script src="js/porter-stemmer.js?v=__CTM_ASSET_EPOCH__"></script><script src="js/agent-command.js?v=__CTM_ASSET_EPOCH__"></script><script src="js/session-search-utils.js?v=__CTM_ASSET_EPOCH__"></script><script src="js/recent-active-identity-index.js?v=__CTM_ASSET_EPOCH__"></script>
2782
2787
  <script src="js/session-activity-utils.js?v=__CTM_ASSET_EPOCH__"></script>
2783
2788
  <script src="js/session-status-precedence.js?v=__CTM_ASSET_EPOCH__"></script>
2784
2789
  <script src="js/session-phase.js?v=__CTM_ASSET_EPOCH__"></script>
@@ -2846,6 +2851,7 @@ url = "http://localhost:<span id="setup-mcp-port-display-toml">3457</span>/mcp"<
2846
2851
  <script src="js/walle-session-runtime-helpers.js?v=__CTM_ASSET_EPOCH__"></script>
2847
2852
  <script src="js/core/01-util.js?v=__CTM_ASSET_EPOCH__"></script>
2848
2853
  <script src="js/core/02-toast.js?v=__CTM_ASSET_EPOCH__"></script>
2854
+ <script src="js/core/pending-create.js?v=__CTM_ASSET_EPOCH__"></script>
2849
2855
  <script src="js/model-picker.js?v=__CTM_ASSET_EPOCH__"></script>
2850
2856
  <script src="js/private-model-lifetime.js?v=__CTM_ASSET_EPOCH__"></script>
2851
2857
  <script src="js/session-model-picker.js?v=__CTM_ASSET_EPOCH__"></script>
@@ -2857,6 +2863,7 @@ url = "http://localhost:<span id="setup-mcp-port-display-toml">3457</span>/mcp"<
2857
2863
  <script src="js/terminal-reference-adapter.js?v=__CTM_ASSET_EPOCH__"></script><script src="js/document-review-links.js?v=__CTM_ASSET_EPOCH__"></script>
2858
2864
  <script src="js/conversation-turn-cards.js?v=__CTM_ASSET_EPOCH__"></script>
2859
2865
  <script src="js/root-review-actions.js?v=__CTM_ASSET_EPOCH__"></script>
2866
+ <script src="js/pasted-content.js?v=__CTM_ASSET_EPOCH__"></script>
2860
2867
  <script src="js/message-renderer.js?v=__CTM_ASSET_EPOCH__"></script>
2861
2868
  <script src="js/native-subagent-review.js?v=__CTM_ASSET_EPOCH__"></script>
2862
2869
  <script src="js/main-thread-budget.js?v=__CTM_ASSET_EPOCH__"></script>
@@ -2927,7 +2934,10 @@ url = "http://localhost:<span id="setup-mcp-port-display-toml">3457</span>/mcp"<
2927
2934
  <script src="js/background-work.js?v=__CTM_ASSET_EPOCH__"></script>
2928
2935
  <script src="js/core/session-selectors.js?v=__CTM_ASSET_EPOCH__"></script>
2929
2936
  <script src="js/core/session-commands.js?v=__CTM_ASSET_EPOCH__"></script>
2937
+ <script src="js/core/active-session-groups.js?v=__CTM_ASSET_EPOCH__"></script><script src="js/core/recent-machine-source.js?v=__CTM_ASSET_EPOCH__"></script>
2930
2938
  <script src="js/core/worker-tree.js?v=__CTM_ASSET_EPOCH__"></script>
2939
+ <!-- S-43 (WP-7): the New Session agent grid + the dsh card's Model select. -->
2940
+ <script src="js/new-session-agent-grid.js?v=__CTM_ASSET_EPOCH__"></script>
2931
2941
  <script src="js/terminal-input-policy.js?v=__CTM_ASSET_EPOCH__"></script>
2932
2942
  <script src="js/session-transport-state.js?v=__CTM_ASSET_EPOCH__"></script>
2933
2943
  <script src="js/terminal-surface-resume-policy.js?v=__CTM_ASSET_EPOCH__"></script>
@@ -2955,6 +2965,7 @@ url = "http://localhost:<span id="setup-mcp-port-display-toml">3457</span>/mcp"<
2955
2965
  <script src="js/terminal/runtime-18.js?v=__CTM_ASSET_EPOCH__"></script>
2956
2966
  <script src="js/terminal/runtime-19.js?v=__CTM_ASSET_EPOCH__"></script>
2957
2967
  <script src="js/terminal/runtime-20.js?v=__CTM_ASSET_EPOCH__"></script>
2968
+ <script src="js/terminal/conv-only-session.js?v=__CTM_ASSET_EPOCH__"></script>
2958
2969
  <script src="js/sidebar-active-reveal.js?v=__CTM_ASSET_EPOCH__"></script>
2959
2970
  <script src="js/terminal/runtime-21.js?v=__CTM_ASSET_EPOCH__"></script>
2960
2971
  <script src="js/terminal/runtime-23.js?v=__CTM_ASSET_EPOCH__"></script>
@@ -3677,7 +3688,7 @@ const state = window._ctmState = {
3677
3688
  minClientBuildStale: false,
3678
3689
  clientUpdateDiskEpoch: '',
3679
3690
  },
3680
- restartBanner: { walle: null, ctm: { reloadRequired: false, backendRestartRequired: false, protocolIncompatible: false, loadedBuildId: '', latestBuildId: '' } },
3691
+ restartBanner: { walle: null, hostDead: null, ctm: { reloadRequired: false, backendRestartRequired: false, protocolIncompatible: false, loadedBuildId: '', latestBuildId: '' } },
3681
3692
  splitRoot: null, // Binary tree of panes (null = single-pane mode)
3682
3693
  savedSplitLayout: null, // Suspended split tree (saved when switching to non-split tab)
3683
3694
  focusedPaneId: null, // ID of focused leaf pane
@@ -4411,10 +4422,11 @@ function _dispatchWebSocketMessage(msg) {
4411
4422
  case 'ui-prefs-changed': onUiPrefsChanged(msg); break;
4412
4423
  case 'session-surface-preference': onSessionSurfacePreference(msg); break;
4413
4424
  case 'created': onCreated(msg); break;
4425
+ case 'create-accepted': if (typeof PendingCreate !== 'undefined') PendingCreate.accepted(msg); break;
4414
4426
  case 'submit-prompt-status':
4415
4427
  window.CTMPromptSubmission?.handleStatus(msg);
4416
4428
  _handlePromptOutboxStatus(msg);
4417
- qpReconcileSubmissionMessage(msg);
4429
+ qpReconcileSubmissionMessage(msg); if (typeof _convOnlyHandleSubmitStatus === 'function') _convOnlyHandleSubmitStatus(msg);
4418
4430
  break;
4419
4431
  case 'input-dropped': onInputDropped(msg); break;
4420
4432
  case 'output': onOutput(msg); break;
@@ -4461,6 +4473,7 @@ function _dispatchWebSocketMessage(msg) {
4461
4473
  case 'resume-blocked': onSessionResumeBlocked(msg); break;
4462
4474
  case 'resume-retry-noop': onResumeRetryNoop(msg); break;
4463
4475
  case 'error': {
4476
+ if (typeof PendingCreate !== 'undefined' && PendingCreate.settleError(msg)) break;
4464
4477
  // WP-5 (§6.3/AC-5a): a machine-scoped failure (machine_unreachable, or the box's own code)
4465
4478
  // is SHOWN — the user picks another machine or fixes the box. Never retried here, and every
4466
4479
  // other error frame keeps its existing console-only path.
@@ -4556,6 +4569,12 @@ function _dispatchWebSocketMessage(msg) {
4556
4569
  state._restarting = true;
4557
4570
  _transitionSessionTransport({ type: 'server-restarting' });
4558
4571
  break;
4572
+ case 'host-dead':
4573
+ // WP-2 (docs/external-causality-kill-session-loss-2026-10-27.html §5 D-5, §6, §8 WP-2,
4574
+ // AC-2a): an adopted host died with sessions still attached. Same banner element as every
4575
+ // other restart fact — see public/js/app/restart-banner.js's setHostDeadFacts.
4576
+ setHostDeadFacts(msg);
4577
+ break;
4559
4578
  case 'server-ready':
4560
4579
  window.CtmSessionModelPicker?.resetRequestCache?.();
4561
4580
  state._serverReady = true;
@@ -5444,13 +5463,22 @@ function applyTabOrder(saved) {
5444
5463
  const restorableIds = state.tabOrder.filter(id => state.sessions.has(id) || id === 'review');
5445
5464
  const nonRestorableIds = state.tabOrder.filter(id => !state.sessions.has(id) && id !== 'review');
5446
5465
  const ordered = [];
5466
+ // WP-5 (S-12, remote-session-child-transcript-model-truth-and-restart-2026-10-27.html): a saved id
5467
+ // this client has never been told about — a BOX session whose catalog has not landed yet after a hub
5468
+ // restart. It is NOT pushed into state.tabOrder (no renderer or keyboard cycle may see a tab with no
5469
+ // session); it is remembered so the next saveTabOrder() writes it back where it was saved, instead of
5470
+ // losing the pref and letting the session append at the END of the strip when the catalog arrives.
5471
+ const undelivered = new Set();
5447
5472
  for (const id of saved) {
5448
5473
  if (restorableIds.includes(id)) ordered.push(id);
5474
+ else if (!nonRestorableIds.includes(id) && id !== 'review') undelivered.add(id);
5449
5475
  }
5450
5476
  for (const id of restorableIds) {
5451
5477
  if (!ordered.includes(id)) ordered.push(id);
5452
5478
  }
5453
5479
  state.tabOrder = [...nonRestorableIds, ...ordered];
5480
+ state._undeliveredTabIds = undelivered;
5481
+ state._undeliveredTabSaved = saved.slice();
5454
5482
  }
5455
5483
 
5456
5484
  function _sessionOwnershipMeta(session) {
@@ -7071,33 +7099,73 @@ async function loadInstanceSourceLedgers() {
7071
7099
  // to macOS) or NFS mounts, so macOS re-prompts for file access on every restart. Full Disk
7072
7100
  // Access is a superset that, under the stable Dev-ID identity, persists — one grant ends it.
7073
7101
  // We can only detect + guide; Apple forbids granting FDA programmatically.
7102
+ // WP-3 (D-9, docs/signin-flow-restart-settle-and-fda-target-identity-2026-10-26.html): the
7103
+ // banner names the EXACT exec the grant must be charged to (cap.fdaTargets / messages.fdaTarget,
7104
+ // both probed from the running exec path by the server — never a fixed "Coding Task Manager"),
7105
+ // stays up after Grant with a "Check again" button, re-checks once per window focus (no new
7106
+ // timer: release/ownership-registry.json keys client timers by ordinal), and offers
7107
+ // "Restart Wall-E" on the split verdict (CTM's probe says granted, Wall-E's Messages probe
7108
+ // still says denied — a running daemon may need the relaunch).
7074
7109
  let _fdaCapabilityChecked = false;
7110
+ let _fdaFocusHandlerBound = false;
7111
+ let _fdaBannerAfterGrant = false;
7112
+ let _fdaBannerOpts = null;
7075
7113
  const FDA_BANNER_DISMISS_KEY = 'ctm_fda_banner_dismissed_v1';
7076
7114
 
7077
- async function checkFdaCapability() {
7115
+ // One probe, two callers: the boot check and the re-check. Best-effort — a failed probe
7116
+ // returns null and never blocks the UI.
7117
+ async function _fetchFdaState() {
7078
7118
  try {
7079
- if (localStorage.getItem(FDA_BANNER_DISMISS_KEY) === '1') return;
7080
7119
  const r = await fetch(`/api/capabilities?token=${state.token}`);
7081
- if (!r.ok) return;
7120
+ if (!r.ok) return null;
7082
7121
  const cap = await r.json();
7083
7122
  // S-44 (imessage-memory-source WP-4): reuse this banner for the iMessage FDA
7084
7123
  // need too, rather than building a second one — best-effort, never blocks.
7085
- let imessageNeedsFda = false;
7124
+ let messages = null;
7086
7125
  if (cap && cap.platform === 'darwin') {
7087
7126
  try {
7088
7127
  const mr = await fetch('/api/wall-e/gws/accounts?summary=1', { cache: 'no-store' });
7089
7128
  if (mr.ok) {
7090
7129
  const md = await mr.json();
7091
- const messages = md && md.macosNative && md.macosNative.messages;
7092
- if (messages && messages.dbPresent && !messages.fullDiskAccess) imessageNeedsFda = true;
7130
+ messages = (md && md.macosNative && md.macosNative.messages) || null;
7093
7131
  }
7094
- } catch { /* expected: the Wall-E gws/accounts probe is best-effort — a failed fetch just means imessageNeedsFda stays false and the banner falls back to CTM's own needsAttention check */ }
7132
+ } catch { /* expected: the Wall-E gws/accounts probe is best-effort — a failed fetch just leaves messages null and the banner falls back to CTM's own needsAttention check */ }
7133
+ }
7134
+ return { cap, messages, imessageNeedsFda: !!(messages && messages.dbPresent && !messages.fullDiskAccess) };
7135
+ } catch { /* best-effort — never block the UI on this */ return null; }
7136
+ }
7137
+
7138
+ async function checkFdaCapability() {
7139
+ try {
7140
+ if (localStorage.getItem(FDA_BANNER_DISMISS_KEY) === '1') return;
7141
+ const probed = await _fetchFdaState();
7142
+ if (!probed) return;
7143
+ if (probed.cap && (probed.cap.needsAttention || probed.imessageNeedsFda)) {
7144
+ showFdaBanner(probed.cap, { imessageNeedsFda: probed.imessageNeedsFda, messages: probed.messages });
7145
+ } else {
7146
+ hideFdaBanner();
7095
7147
  }
7096
- if (cap && (cap.needsAttention || imessageNeedsFda)) showFdaBanner(cap, { imessageNeedsFda });
7097
- else hideFdaBanner();
7098
7148
  } catch { /* best-effort — never block the UI on this */ }
7099
7149
  }
7100
7150
 
7151
+ // Re-check an EXISTING banner in place (window focus / "Check again"). Never creates one —
7152
+ // checkFdaCapability owns first paint. When nothing is left to fix it hides the banner and,
7153
+ // if the user had already granted, confirms once.
7154
+ async function refreshFdaBanner() {
7155
+ const banner = document.getElementById('fda-capability-banner');
7156
+ if (!banner) return;
7157
+ const probed = await _fetchFdaState();
7158
+ if (!probed) return;
7159
+ if (probed.cap && (probed.cap.needsAttention || probed.imessageNeedsFda)) {
7160
+ _applyFdaBannerState(banner, probed.cap, { imessageNeedsFda: probed.imessageNeedsFda, messages: probed.messages });
7161
+ } else {
7162
+ const wasAfterGrant = _fdaBannerAfterGrant;
7163
+ _fdaBannerAfterGrant = false;
7164
+ hideFdaBanner();
7165
+ if (wasAfterGrant) toast('Full Disk Access granted. Wall-E can read Messages now.', { type: 'success', duration: 7000 });
7166
+ }
7167
+ }
7168
+
7101
7169
  function hideFdaBanner() {
7102
7170
  const b = document.getElementById('fda-capability-banner');
7103
7171
  if (b) b.remove();
@@ -7105,40 +7173,83 @@ function hideFdaBanner() {
7105
7173
 
7106
7174
  function showFdaBanner(cap, opts) {
7107
7175
  opts = opts || {};
7108
- if (document.getElementById('fda-capability-banner')) return;
7109
- const count = Number(cap.networkSessionCount) || 0;
7110
- const denials = Array.isArray(cap.accessDenials) ? cap.accessDenials : [];
7176
+ const existing = document.getElementById('fda-capability-banner');
7177
+ if (existing) { _applyFdaBannerState(existing, cap, opts); return; } // one banner, updated in place
7178
+ const banner = document.createElement('div');
7179
+ banner.id = 'fda-capability-banner';
7180
+ banner.className = 'fda-capability-banner';
7181
+ _applyFdaBannerState(banner, cap, opts);
7182
+ document.body.appendChild(banner);
7183
+ }
7184
+
7185
+ function _escFda(s) {
7186
+ return String(s == null ? '' : s).replace(/[&<>"']/g, (c) => (
7187
+ { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
7188
+ }
7189
+
7190
+ // The exec identities a grant must be charged to: Wall-E's Messages target when the Messages
7191
+ // need is what raised the banner, else every target CTM reported. Empty (an older server that
7192
+ // sends no fdaTargets) keeps the pre-WP-3, path-less sentence.
7193
+ function _fdaBannerTargets(cap, messages, messagesLed) {
7194
+ if (messagesLed && messages && messages.fdaTarget && messages.fdaTarget.path) return [messages.fdaTarget];
7195
+ const out = [];
7196
+ for (const t of (cap && Array.isArray(cap.fdaTargets) ? cap.fdaTargets : [])) if (t && t.path) out.push(t);
7197
+ return out;
7198
+ }
7199
+
7200
+ function _fdaBannerMessage(cap, opts) {
7201
+ const count = Number(cap && cap.networkSessionCount) || 0;
7202
+ const denials = cap && Array.isArray(cap.accessDenials) ? cap.accessDenials : [];
7203
+ const messages = opts.messages || null;
7204
+ const messagesLed = !!opts.imessageNeedsFda && !(cap && cap.needsAttention);
7205
+ const targets = _fdaBannerTargets(cap, messages, messagesLed);
7206
+ const named = targets.length ? ` to <code>${_escFda(targets.map((t) => t.path).join(' and '))}</code>` : '';
7111
7207
  // Prefer the specific, observed cause (e.g. Codex couldn't read ~/.codex) over the generic
7112
7208
  // count — it's actionable and explains why a session just failed to load.
7113
7209
  let message;
7114
7210
  if (denials.length) {
7115
- const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => (
7116
- { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
7117
7211
  const d = denials[0];
7118
7212
  const who = d.provider ? d.provider.charAt(0).toUpperCase() + d.provider.slice(1) : 'An agent';
7119
7213
  const where = (d.path || '').replace(/\/config\.toml$/, '') || d.path || 'a file';
7120
- message = `${esc(who)} couldn't read <code>${esc(where)}</code> (it's on Dropbox/iCloud or a network volume), so the session failed to load. <strong>Grant Full Disk Access once</strong> to fix it.`;
7121
- } else if (opts.imessageNeedsFda && !cap.needsAttention) {
7122
- message = `<strong>Grant Full Disk Access once</strong> so Wall-E can read your Messages history.`;
7214
+ message = `${_escFda(who)} couldn't read <code>${_escFda(where)}</code> (it's on Dropbox/iCloud or a network volume), so the session failed to load. <strong>Grant Full Disk Access once</strong>${named} to fix it.`;
7215
+ } else if (messagesLed) {
7216
+ message = `<strong>Grant Full Disk Access once</strong>${named} so Wall-E can read your Messages history.`;
7123
7217
  } else {
7124
- message = `CTM has ${count} session${count === 1 ? '' : 's'} on a network/Dropbox volume, so macOS keeps asking for file access on every restart. <strong>Grant Full Disk Access once</strong> to stop it.`;
7218
+ message = `CTM has ${count} session${count === 1 ? '' : 's'} on a network/Dropbox volume, so macOS keeps asking for file access on every restart. <strong>Grant Full Disk Access once</strong>${named} to stop it.`;
7125
7219
  }
7126
7220
  // S-44: append the iMessage sentence whenever Wall-E's Messages mirror also needs
7127
7221
  // FDA, even if the banner's primary trigger was CTM's own network-volume/denial check.
7128
- if (opts.imessageNeedsFda && cap.needsAttention) {
7222
+ if (opts.imessageNeedsFda && cap && cap.needsAttention) {
7129
7223
  message += ' iMessage history needs Full Disk Access for Wall-E.';
7130
7224
  }
7131
- const banner = document.createElement('div');
7132
- banner.id = 'fda-capability-banner';
7133
- banner.className = 'fda-capability-banner';
7225
+ // Homebrew's `node` is ad-hoc signed under a versioned Cellar path, so its row can look
7226
+ // granted while the running binary never matches — name the trap only when it applies.
7227
+ if (targets.some((t) => t.displayName === 'node')) {
7228
+ message += ' System Settings lists it as “node”; another “node” row does not count.';
7229
+ }
7230
+ return message;
7231
+ }
7232
+
7233
+ // Write the banner's text + buttons for the current state. Idempotent on an existing node, so a
7234
+ // re-check updates the banner in place instead of stacking a second one (D-9).
7235
+ function _applyFdaBannerState(banner, cap, opts) {
7236
+ opts = opts || {};
7237
+ _fdaBannerOpts = opts;
7238
+ const afterGrant = _fdaBannerAfterGrant;
7239
+ const splitVerdict = !!(cap && cap.fdaGranted === true && opts.messages && opts.messages.fullDiskAccess === false);
7134
7240
  banner.innerHTML = `
7135
7241
  <span class="fda-banner-icon">&#128274;</span>
7136
- <span class="fda-banner-text">${message}</span>
7137
- <button class="fda-banner-grant" type="button">Grant Full Disk Access</button>
7242
+ <span class="fda-banner-text">${_fdaBannerMessage(cap, opts)}</span>
7243
+ <button class="fda-banner-grant" type="button">${afterGrant ? 'Open Settings again' : 'Grant Full Disk Access'}</button>
7244
+ ${afterGrant ? '<button class="fda-banner-check" type="button">Check again</button>' : ''}
7245
+ ${splitVerdict ? '<button class="fda-banner-restart-walle" type="button">Restart Wall-E</button>' : ''}
7138
7246
  <button class="fda-banner-later" type="button">Later</button>
7139
7247
  <button class="fda-banner-never" type="button" title="Don't show again">Don't show again</button>`;
7140
- document.body.appendChild(banner);
7141
7248
  banner.querySelector('.fda-banner-grant').addEventListener('click', () => grantFdaAccess(cap));
7249
+ const check = banner.querySelector('.fda-banner-check');
7250
+ if (check) check.addEventListener('click', refreshFdaBanner);
7251
+ const restart = banner.querySelector('.fda-banner-restart-walle');
7252
+ if (restart) restart.addEventListener('click', () => svcAction('walle', 'restart', { confirmed: true }));
7142
7253
  banner.querySelector('.fda-banner-later').addEventListener('click', hideFdaBanner);
7143
7254
  banner.querySelector('.fda-banner-never').addEventListener('click', () => {
7144
7255
  try { localStorage.setItem(FDA_BANNER_DISMISS_KEY, '1'); } catch {}
@@ -7148,19 +7259,30 @@ function showFdaBanner(cap, opts) {
7148
7259
 
7149
7260
  async function grantFdaAccess(cap) {
7150
7261
  const fallbackHint = (cap && cap.hint) ||
7151
- 'Open System Settings → Privacy & Security → Full Disk Access and add "Coding Task Manager".';
7262
+ 'Open System Settings → Privacy & Security → Full Disk Access and turn on the highlighted file.';
7263
+ // One focus listener per page: the user leaving/returning from System Settings is the
7264
+ // natural re-check trigger, and there is no orderly ordinal left for a new client timer.
7265
+ if (!_fdaFocusHandlerBound) {
7266
+ _fdaFocusHandlerBound = true;
7267
+ window.addEventListener('focus', () => {
7268
+ if (document.getElementById('fda-capability-banner')) refreshFdaBanner();
7269
+ });
7270
+ }
7152
7271
  try {
7153
7272
  const r = await fetch(`/api/capabilities/open-fda?token=${state.token}`, { method: 'POST' });
7154
7273
  const d = await r.json().catch(() => ({}));
7155
7274
  if (d && d.ok) {
7156
- toast('Add "Coding Task Manager" to the opened Full Disk Access list, then restart CTM once — macOS will stop asking.', { type: 'info', title: 'Full Disk Access', duration: 12000 });
7157
- } else {
7158
- toast(fallbackHint, { type: 'warning', title: 'Grant Full Disk Access', duration: 14000 });
7275
+ toast(`Full Disk Access opened on ${d.hostname || 'this Mac'}: add the highlighted file, turn it on, then click Check again.`, { type: 'info', title: 'Full Disk Access', duration: 14000 });
7276
+ // The banner stays up (D-9) in its after-grant state, so the user can confirm or retry.
7277
+ _fdaBannerAfterGrant = true;
7278
+ const banner = document.getElementById('fda-capability-banner');
7279
+ if (banner) _applyFdaBannerState(banner, cap, _fdaBannerOpts || {});
7280
+ return;
7159
7281
  }
7282
+ toast(fallbackHint, { type: 'warning', title: 'Grant Full Disk Access', duration: 14000 });
7160
7283
  } catch {
7161
7284
  toast(fallbackHint, { type: 'warning', title: 'Grant Full Disk Access', duration: 14000 });
7162
7285
  }
7163
- hideFdaBanner();
7164
7286
  }
7165
7287
 
7166
7288
  // --- Global services-health banner (golden-four alarms + actionable Wall-E
@@ -7964,15 +8086,7 @@ window.addEventListener('storage', (event) => {
7964
8086
  // Enabled per-session by agent type. Shell + unknown agents skip overlay
7965
8087
  // (their prompt format is variable; bad overlay positioning is user-visible).
7966
8088
  function _clientDetectAgentType(cmd) {
7967
- if (!cmd) return null;
7968
- const c = String(cmd).toLowerCase();
7969
- if (c.includes('claude')) return 'claude';
7970
- if (c.includes('codex')) return 'codex';
7971
- if (c.includes('gemini')) return 'gemini';
7972
- if (c.includes('opencode') || c.includes('open-code')) return 'opencode';
7973
- if (c.includes('cursor')) return 'cursor';
7974
- if (c.includes('wall-e') || c.includes('walle')) return 'walle';
7975
- return null;
8089
+ return window.CtmAgentCommand.detectAgentType(cmd);
7976
8090
  }
7977
8091
 
7978
8092
  const CTM_AGENT_UI_FLAGS = __CTM_AGENT_UI_FLAGS__;
@@ -7980,14 +8094,17 @@ window.CTM_AGENT_UI_FLAGS = CTM_AGENT_UI_FLAGS;
7980
8094
  const CTM_LIFECYCLE_FLAGS = __CTM_LIFECYCLE_FLAGS__;
7981
8095
  window.CTM_LIFECYCLE_FLAGS = CTM_LIFECYCLE_FLAGS;
7982
8096
  const CLIENT_AGENT_CAPABILITIES = {
7983
- claude: { surfaces:{conv:{available:true,live:true,source:'canonical-rows'},cli:{available:true,source:'pty'}}, structuredTranscript: true, semanticInput: true, semanticQueue: true, semanticApprovals: true, inputFidelity: 'pty', controlFidelity: 'hook', steer:'queue-interrupt',rewind:false,branch:false,modelControl: false, branching: false, export: true, messages: {stream:true,durable:true,branching:false}, input:{send:true,queue:true,interrupt:true,attachments:true}, approvals:{authoritative:true,resolve:true}, work:{phase:true,tasks:true,subagents:true}, controls:{model:false,cwd:false,sandbox:false}, artifacts:{files:true,diffs:true,tests:true}, promptNavigation: 'transcript', review: true, resume: true, runningHoldMs: 15000 },
7984
- 'claude-desktop': { surfaces:{conv:{available:true,live:false,source:'canonical-rows'},cli:{available:false}}, structuredTranscript: true, semanticInput: false, semanticQueue: false, semanticApprovals: false, promptNavigation: 'transcript', review: true, resume: false, convert: true },
7985
- codex: { surfaces:{conv:{available:true,live:true,source:'canonical-rows'},cli:{available:true,source:'pty'}}, structuredTranscript: true, semanticInput: true, semanticQueue: true, semanticApprovals: true, inputFidelity: 'pty', controlFidelity: 'runtime', steer:'queue-interrupt',rewind:false,branch:false,modelControl: false, branching: false, export: true, messages:{stream:true,durable:true,branching:false},input:{send:true,queue:true,interrupt:true,attachments:true},approvals:{authoritative:true,resolve:true},work:{phase:true,tasks:true,subagents:true},controls:{model:false,cwd:false,sandbox:false},artifacts:{files:true,diffs:true,tests:true}, promptNavigation: 'transcript', review: true, resume: true },
7986
- gemini: { surfaces:{conv:{available:false},cli:{available:true,source:'pty'}}, structuredTranscript: false, semanticInput: true, semanticQueue: true, semanticApprovals: false, promptNavigation: 'terminal', review: false, resume: true },
7987
- opencode: { surfaces:{conv:{available:true,live:true,source:'canonical-rows'},cli:{available:true,source:'pty'}}, structuredTranscript: true, semanticInput: true, semanticQueue: true, semanticApprovals: false, inputFidelity:'pty',controlFidelity:'runtime',steer:'queue-interrupt',rewind:false,branch:false,modelControl:false,branching:false,export:true,messages:{stream:true,durable:true,branching:false},input:{send:true,queue:true,interrupt:true,attachments:true},approvals:{authoritative:false,resolve:false},work:{phase:true,tasks:true,subagents:false},controls:{model:false,cwd:false,sandbox:false},artifacts:{files:true,diffs:true,tests:true}, promptNavigation: 'transcript', review: true, resume: true },
7988
- walle: { surfaces:{web:{available:true,interactive:true,source:'walle-native'},conv:{available:true,live:true,source:'canonical-rows'},cli:{available:false,source:'pty'}}, structuredTranscript: true, semanticInput: true, semanticQueue: true, semanticApprovals: true, inputFidelity:'native',controlFidelity:'native',steer:true,rewind:true,branch:true,modelControl:true,branching:true,export:true,messages:{stream:true,durable:true,branching:true},input:{send:true,queue:true,interrupt:true,attachments:true},approvals:{authoritative:true,resolve:true},work:{phase:true,tasks:true,subagents:true},controls:{model:true,cwd:true,sandbox:true},artifacts:{files:true,diffs:true,tests:true}, promptNavigation: 'chat', review: true, resume: false },
7989
- cursor: { surfaces:{conv:{available:false},cli:{available:true,source:'pty'}}, structuredTranscript: false, semanticInput: true, semanticQueue: true, semanticApprovals: false, promptNavigation: 'terminal', review: false, resume: true, runningHoldMs: 10000 },
7990
- shell: { surfaces:{conv:{available:false},cli:{available:true,source:'pty'}}, structuredTranscript: false, semanticInput: false, semanticQueue: false, semanticApprovals: false, promptNavigation: 'terminal', review: false, resume: false },
8097
+ claude: { surfaces:{conv:{available:true,live:true,source:'canonical-rows'},cli:{available:true,source:'pty'}}, structuredTranscript: true, semanticInput: true, semanticQueue: true, semanticApprovals: true, inputFidelity: 'pty', controlFidelity: 'hook', steer:'queue-interrupt',rewind:false,branch:false,modelControl: false, branching: false, export: true, messages: {stream:true,durable:true,branching:false}, input:{send:true,queue:true,interrupt:true,attachments:true}, approvals:{authoritative:true,resolve:true}, work:{phase:true,tasks:true,subagents:true}, controls:{model:false,cwd:false,sandbox:false}, artifacts:{files:true,diffs:true,tests:true}, promptNavigation: 'transcript', historyReader: 'transcript', review: true, resume: true, runningHoldMs: 15000 },
8098
+ 'claude-desktop': { surfaces:{conv:{available:true,live:false,source:'canonical-rows'},cli:{available:false}}, structuredTranscript: true, semanticInput: false, semanticQueue: false, semanticApprovals: false, promptNavigation: 'transcript', historyReader: 'none', review: true, resume: false, convert: true },
8099
+ codex: { surfaces:{conv:{available:true,live:true,source:'canonical-rows'},cli:{available:true,source:'pty'}}, structuredTranscript: true, semanticInput: true, semanticQueue: true, semanticApprovals: true, inputFidelity: 'pty', controlFidelity: 'runtime', steer:'queue-interrupt',rewind:false,branch:false,modelControl: false, branching: false, export: true, messages:{stream:true,durable:true,branching:false},input:{send:true,queue:true,interrupt:true,attachments:true},approvals:{authoritative:true,resolve:true},work:{phase:true,tasks:true,subagents:true},controls:{model:false,cwd:false,sandbox:false},artifacts:{files:true,diffs:true,tests:true}, promptNavigation: 'transcript', historyReader: 'transcript', review: true, resume: true },
8100
+ gemini: { surfaces:{conv:{available:false},cli:{available:true,source:'pty'}}, structuredTranscript: false, semanticInput: true, semanticQueue: true, semanticApprovals: false, promptNavigation: 'terminal', historyReader: 'none', review: false, resume: true },
8101
+ opencode: { surfaces:{conv:{available:true,live:true,source:'canonical-rows'},cli:{available:true,source:'pty'}}, structuredTranscript: true, semanticInput: true, semanticQueue: true, semanticApprovals: false, inputFidelity:'pty',controlFidelity:'runtime',steer:'queue-interrupt',rewind:false,branch:false,modelControl:false,branching:false,export:true,messages:{stream:true,durable:true,branching:false},input:{send:true,queue:true,interrupt:true,attachments:true},approvals:{authoritative:false,resolve:false},work:{phase:true,tasks:true,subagents:false},controls:{model:false,cwd:false,sandbox:false},artifacts:{files:true,diffs:true,tests:true}, promptNavigation: 'transcript', historyReader: 'none', review: true, resume: true },
8102
+ walle: { surfaces:{web:{available:true,interactive:true,source:'walle-native'},conv:{available:true,live:true,source:'canonical-rows'},cli:{available:false,source:'pty'}}, structuredTranscript: true, semanticInput: true, semanticQueue: true, semanticApprovals: true, inputFidelity:'native',controlFidelity:'native',steer:true,rewind:true,branch:true,modelControl:true,branching:true,export:true,messages:{stream:true,durable:true,branching:true},input:{send:true,queue:true,interrupt:true,attachments:true},approvals:{authoritative:true,resolve:true},work:{phase:true,tasks:true,subagents:true},controls:{model:true,cwd:true,sandbox:true},artifacts:{files:true,diffs:true,tests:true}, promptNavigation: 'chat', historyReader: 'none', review: true, resume: false },
8103
+ cursor: { surfaces:{conv:{available:false},cli:{available:true,source:'pty'}}, structuredTranscript: false, semanticInput: true, semanticQueue: true, semanticApprovals: false, promptNavigation: 'terminal', historyReader: 'none', review: false, resume: true, runningHoldMs: 10000 },
8104
+ shell: { surfaces:{conv:{available:false},cli:{available:true,source:'pty'}}, structuredTranscript: false, semanticInput: false, semanticQueue: false, semanticApprovals: false, promptNavigation: 'terminal', historyReader: 'none', review: false, resume: false },
8105
+ // WP-7 (S-44): mirrors lib/agent-capabilities.js's dsh row — conv-only (surfaces.cli.available
8106
+ // false ⇒ no xterm anywhere, AC-7b), ACP input/control fidelity, no attachments until WP-10.
8107
+ dsh: { surfaces:{conv:{available:true,live:true,source:'canonical-rows'},cli:{available:false,source:'pty'}}, structuredTranscript: true, semanticInput: true, semanticQueue: false, semanticApprovals: true, inputFidelity:'acp', controlFidelity:'acp', steer:'queue-interrupt', rewind:false, branch:false, modelControl:false, branching:false, export:true, messages:{stream:true,durable:true,branching:false}, input:{send:true,queue:false,interrupt:true,attachments:false}, approvals:{authoritative:true,resolve:true}, work:{phase:true,tasks:false,subagents:false}, controls:{model:false,cwd:false,sandbox:false}, artifacts:{files:true,diffs:false,tests:false}, historyReader: 'none', promptNavigation: 'transcript', review: false, resume: true },
7991
8108
  };
7992
8109
 
7993
8110
  function _clientNormalizeAgentType(value) {
@@ -7995,6 +8112,7 @@ function _clientNormalizeAgentType(value) {
7995
8112
  const v = String(value).toLowerCase().replace(/_/g, '-');
7996
8113
  if (CLIENT_AGENT_CAPABILITIES[v]) return v;
7997
8114
  if (v === 'claude-code') return 'claude';
8115
+ if (v === 'deepseek-harness' || v === 'deepseek') return 'dsh';
7998
8116
  if (v === 'claude-desktop-session' || v === 'desktop') return 'claude-desktop';
7999
8117
  if (v === 'gemini-cli') return 'gemini';
8000
8118
  if (v === 'open-code' || v === 'opencode-cli') return 'opencode';
@@ -10562,8 +10680,8 @@ function _liveWheelSteps(e, writer, rowHeightPx, rows, wheelRecorder) {
10562
10680
  // (a frozen function-size ceiling, lint/file-size-baseline.json) only pays ONE line per call
10563
10681
  // site for reader-enter/prefetch/frame-note -- the actual logic lives in history-app-seam.js.
10564
10682
  function _continuousController(id) { return window.TerminalHistoryViewport && window.TerminalHistoryViewport.get(id); }
10565
- function _enterContinuousForSession(id, deltaYPx) { const c = _continuousController(id); if (c && window.HistoryAppSeam) window.HistoryAppSeam.enterContinuous(c, 'reader-enter', { deltaYPx }); }
10566
- function _prefetchContinuousForSession(id, reason) { const c = _continuousController(id); if (c && window.HistoryAppSeam) window.HistoryAppSeam.prefetchContinuousPage(c, { reason }); }
10683
+ function _enterContinuousForSession(id, deltaYPx) { const c = _continuousController(id); if (c && window.HistoryAppSeam && window.HistoryAppSeam.historyReader(state.sessions.get(id))) window.HistoryAppSeam.enterContinuous(c, 'reader-enter', { deltaYPx }); }
10684
+ function _prefetchContinuousForSession(id, reason) { const c = _continuousController(id); if (c && window.HistoryAppSeam && window.HistoryAppSeam.historyReader(state.sessions.get(id))) window.HistoryAppSeam.prefetchContinuousPage(c, { reason }); }
10567
10685
  function _wheelDeltaPx(e, term) {
10568
10686
  let ch = 0;
10569
10687
  try { ch = term._core._renderService.dimensions.css.cell.height; } catch {}
@@ -10588,6 +10706,24 @@ function _requestTerminalScrollOverlayRefresh(id, writer, current) {
10588
10706
  _updateTerminalCurrentPromptChip(id);
10589
10707
  });
10590
10708
  }
10709
+ // WP-7 (S-52/S-57, AC-7b): a session whose CLIENT capabilities have no CLI surface (dsh today) is
10710
+ // conv-only — it must never build an xterm, and no bootstrap may wait for a PTY attach that will
10711
+ // never arrive. The rule reads the capability table, never the agent name, so it generalizes: every
10712
+ // existing createTerminal() call site (the `created` ack, the lazy activateTab path, the sessions-list
10713
+ // race path) goes through this wrapper and is answered with the conv-only shape instead.
10714
+ const _createTerminalPtySurface = createTerminal;
10715
+ createTerminal = function createTerminalGated(id, opts) {
10716
+ // The `created` ack calls this BEFORE it writes the session meta, so the frame's own agent identity
10717
+ // (opts) decides first; every later call site already has the meta.
10718
+ const optCaps = (opts && opts.agentCapabilities) || (opts && opts.agentType && typeof _clientAgentCaps === 'function' ? _clientAgentCaps(opts.agentType) : null);
10719
+ const optsConvOnly = !!(optCaps && optCaps.surfaces && optCaps.surfaces.cli && optCaps.surfaces.cli.available === false);
10720
+ if ((optsConvOnly || (typeof _sessionCliUnavailable === 'function' && _sessionCliUnavailable(id)))
10721
+ && typeof _ensureConvOnlySessionClientShape === 'function') {
10722
+ const shaped = _ensureConvOnlySessionClientShape(id, { ...((state.sessions.get(id) || {}).meta || {}), ...(opts && opts.agentType ? { agentType: opts.agentType } : {}), ...(optCaps ? { agentCapabilities: optCaps } : {}) });
10723
+ return shaped || { term: null, fitAddon: null };
10724
+ }
10725
+ return _createTerminalPtySurface(id, opts);
10726
+ };
10591
10727
  function createTerminal(id, opts) {
10592
10728
  opts = opts || {};
10593
10729
  const candidateRecovery = opts.candidateRecovery === true;
@@ -11140,7 +11276,9 @@ function createTerminal(id, opts) {
11140
11276
  }
11141
11277
  if (_terminalShouldHandleCtrlVPasteShortcut(e) && _terminalImagePasteEnabledForSession(id)) {
11142
11278
  e.preventDefault();
11143
- _terminalHandleShortcutPaste({ mode: 'token' });
11279
+ _terminalHandleShortcutPaste(_terminalIsMacPlatform()
11280
+ ? { mode: 'token' }
11281
+ : { mode: 'token', controlVPassthrough: true });
11144
11282
  return false;
11145
11283
  }
11146
11284
  return true;
@@ -12085,7 +12223,7 @@ function createTerminal(id, opts) {
12085
12223
  async function _terminalRemoveDraftAttachment(id, attachment, metricEvent, options = {}) {
12086
12224
  const Draft = window.TerminalAttachmentDraft;
12087
12225
  const draft = _terminalAttachmentDraftState(id);
12088
- if (!Draft || !draft || !attachment || !draft.attachments.includes(attachment)) return false;
12226
+ if (!Draft || !draft || !attachment || !draft.attachments.includes(attachment) || !Draft.canRemoveAttachment(draft, attachment.clientId)) return false;
12089
12227
  const owned = _terminalDraftUploadOwnership(id);
12090
12228
  const current = () => Boolean(owned.lifetime && owned.lifetime === _terminalRootDraftLifetime
12091
12229
  && !owned.reason && owned.context && owned.context.draft === draft
@@ -12115,7 +12253,7 @@ function createTerminal(id, opts) {
12115
12253
  const draft = _terminalAttachmentDraftState(id);
12116
12254
  const action = Draft && Draft.dismissDisposition(draft, attachment);
12117
12255
  if (action === 'disabled' || !action) return false;
12118
- if (action === 'cancel') return _terminalCancelAttachmentUpload(id, attachment);
12256
+ if (action === 'cancel' || action === 'dismiss-submitted') return action === 'cancel' ? _terminalCancelAttachmentUpload(id, attachment) : (Draft.dismissSubmitted(draft) && (_terminalAttachmentMetric(id, 'indeterminate_dismissed_by_user', { attachmentCount: draft.attachments.length }), _terminalRenderAttachmentDraft(id), true)); // R-3b: post-Enter removal is LOCAL only -- no server DELETE, no re-send
12119
12257
  const metric = action === 'detach' ? 'attached_detached_by_user' : 'chip_dismissed_by_user';
12120
12258
  const owned = _terminalDraftUploadOwnership(id);
12121
12259
  const removed = _terminalRemoveDraftAttachment(id, attachment, metric);
@@ -12335,7 +12473,7 @@ function createTerminal(id, opts) {
12335
12473
  if (membershipSnapshot.known) {
12336
12474
  const tokenAttachments = draft.attachments.filter(item => item && item.mode !== 'path');
12337
12475
  const derived = Orchestration.deriveObservedMembership({
12338
- attachments: tokenAttachments,
12476
+ attachments: tokenAttachments, provider: _clientAgentTypeForSession(s),
12339
12477
  observedTokens: membershipSnapshot.tokens,
12340
12478
  scan: {
12341
12479
  known: membershipSnapshot.known === true,
@@ -13145,7 +13283,7 @@ function createTerminal(id, opts) {
13145
13283
  : 'confirmed',
13146
13284
  }));
13147
13285
  if (!list.length) return { ok: true, links: [], skipped: [] };
13148
- const response = await fetch('/api/session/image-refs?token=' + encodeURIComponent(state.token || ''), {
13286
+ const response = await fetch('/api/sessions/' + encodeURIComponent(id) + '/image-refs?token=' + encodeURIComponent(state.token || ''), {
13149
13287
  method: 'POST',
13150
13288
  headers: { 'Content-Type': 'application/json' },
13151
13289
  keepalive: true,
@@ -13433,10 +13571,12 @@ function createTerminal(id, opts) {
13433
13571
  return true;
13434
13572
  } catch (error) {
13435
13573
  if (!current()) return false;
13436
- Draft.markIndeterminate(draft, error && error.message ? error.message : error);
13574
+ const outcome = Draft.failSubmission(draft, error && error.message ? error.message : error);
13437
13575
  _terminalRenderAttachmentDraft(id);
13438
13576
  if (typeof toast === 'function') {
13439
- toast('Image submission is still interrupted. Retry when the connection recovers.', {
13577
+ toast(outcome === 'indeterminate'
13578
+ ? 'Image submission is still interrupted. Retry when the connection recovers.'
13579
+ : 'Image send failed: ' + (error && error.message ? error.message : error), {
13440
13580
  type: 'error', duration: 4000,
13441
13581
  });
13442
13582
  }
@@ -13641,14 +13781,12 @@ function createTerminal(id, opts) {
13641
13781
  // second time: if the deadline branch (or a supersession) already cleared/replaced
13642
13782
  // the submission, this is a no-op.
13643
13783
  if (!ownerCurrent() || !draft.submission || draft.submission.id !== started.submission.id) return false;
13644
- const frozen = frozenRef.count;
13645
- if (frozen > 0) Draft.markIndeterminate(draft, err && err.message ? err.message : err);
13646
- else Draft.abortSubmission(draft, err && err.message ? err.message : err);
13647
- _terminalSettleAttachmentDeferredInput(id, 'submit-catch', frozen > 0 ? 'indeterminate' : 'aborted'); // F-D: before the render, which settles too
13784
+ const outcome = Draft.failSubmission(draft, err && err.message ? err.message : err);
13785
+ _terminalSettleAttachmentDeferredInput(id, 'submit-catch', outcome === 'indeterminate' ? 'indeterminate' : 'aborted'); // F-D: before the render, which settles too
13648
13786
  _terminalRenderAttachmentDraft(id);
13649
- _terminalAttachmentMetric(id, frozen > 0 ? 'freeze_indeterminate' : 'submission_blocked', { attachmentCount: attachments.length });
13787
+ _terminalAttachmentMetric(id, outcome === 'indeterminate' ? 'freeze_indeterminate' : 'submission_blocked', { attachmentCount: attachments.length });
13650
13788
  if (typeof toast === 'function') {
13651
- toast(frozen > 0
13789
+ toast(outcome === 'indeterminate'
13652
13790
  ? 'Image send state is unknown. Review the provider prompt before retrying.'
13653
13791
  : 'Image attachment failed: ' + (err && err.message ? err.message : err), {
13654
13792
  type: 'error',
@@ -13992,9 +14130,16 @@ function createTerminal(id, opts) {
13992
14130
  if (!e || e.type !== 'keydown') return false;
13993
14131
  if (!e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) return false;
13994
14132
  if (String(e.key || '').toLowerCase() !== 'v') return false;
13995
- // On non-macOS platforms Ctrl+V already produces a native paste event with
13996
- // clipboardData. Only bridge macOS Control+V, which otherwise reaches xterm
13997
- // as a control character and never exposes image clipboard data.
14133
+ // xterm.js 6 handles Ctrl+V keydown itself off macOS: it cancels the keydown and sends
14134
+ // \x16 to the PTY, so the browser never fires a `paste` event with clipboardData
14135
+ // (xterm.js #2478). Claim the keydown on every platform and read the clipboard here;
14136
+ // off macOS a non-image clipboard forwards exactly \x16 (controlVPassthrough, see
14137
+ // _terminalHandleShortcutPaste). Design: docs/nightly-linux-host-portability-2026-10-27.html
14138
+ // section 9.4 WP-14 (decision D-13 option d).
14139
+ return true;
14140
+ }
14141
+
14142
+ function _terminalIsMacPlatform() {
13998
14143
  return /Mac|iPhone|iPad|iPod/i.test(String(navigator.platform || ''));
13999
14144
  }
14000
14145
 
@@ -14022,6 +14167,12 @@ function createTerminal(id, opts) {
14022
14167
  await _terminalInsertPastedImageFiles(files, opts);
14023
14168
  return;
14024
14169
  }
14170
+ if (opts.controlVPassthrough) {
14171
+ // Off macOS, Ctrl+V with no image keeps its terminal meaning (^V for Claude Code's own
14172
+ // image paste, vim, shells). Text paste is Ctrl+Shift+V / the native paste event.
14173
+ _terminalForwardObservedInput(id, '\x16', { resolveWaiting: false });
14174
+ return;
14175
+ }
14025
14176
  if (navigator.clipboard && typeof navigator.clipboard.readText === 'function') {
14026
14177
  const text = await navigator.clipboard.readText();
14027
14178
  if (text) {
@@ -14358,7 +14509,7 @@ function createTerminal(id, opts) {
14358
14509
  exited: current._exited,
14359
14510
  agentKind,
14360
14511
  hasScrollback,
14361
- readerOpen: !!(window.TerminalHistoryViewport && window.TerminalHistoryViewport.isActive(id)), // WP-0 §5 D-3
14512
+ readerOpen: !!(window.TerminalHistoryViewport && window.TerminalHistoryViewport.isActive(id)), historyReader: !!(window.HistoryAppSeam && window.HistoryAppSeam.historyReader(current)), // WP-0 §5 D-3; WP-1 D-1/D-3: reader only where a source exists
14362
14513
  }),
14363
14514
  {
14364
14515
  terminalMouseState: window.StateSyncClient?.terminalMouseState,
@@ -14454,7 +14605,7 @@ function createTerminal(id, opts) {
14454
14605
  wheelRecorder.record(wheelDecisionForDiag.route, {
14455
14606
  bufferType: buf.type, baseY: buf.baseY, deltaY: e.deltaY, action: wheelDecisionForDiag.action,
14456
14607
  appMouse: wheelDecisionForDiag.mouseActive, isAlt: wheelDecisionForDiag.isAlt,
14457
- mouseSource: wheelDecisionForDiag.mouseState && wheelDecisionForDiag.mouseState.source, frameTracking: wheelDecisionForDiag.frameTracking,
14608
+ mouseSource: wheelDecisionForDiag.mouseState && wheelDecisionForDiag.mouseState.source, frameTracking: wheelDecisionForDiag.frameTracking, historyReader: wheelDecisionForDiag.historyReader,
14458
14609
  });
14459
14610
  }
14460
14611
  }
@@ -17744,9 +17895,8 @@ function syncNavPills(activeId) {
17744
17895
  const navPills = document.querySelectorAll('#topbar-nav .nav-pill');
17745
17896
  const specialPanels = ['rules', 'insights', 'permissions', 'prompts', 'codereview', 'walle', 'models', 'backups', 'worktrees', 'machines', 'setup'];
17746
17897
  const navId = specialPanels.includes(activeId) ? activeId : 'sessions';
17747
- navPills.forEach(pill => {
17748
- pill.classList.toggle('active', pill.dataset.nav === navId);
17749
- });
17898
+ navPills.forEach(pill => pill.classList.toggle('active', pill.dataset.nav === navId));
17899
+ document.getElementById('nav-more-button')?.classList.toggle('nav-more-has-active', !!document.querySelector('#nav-more-dropdown .nav-pill.active'));
17750
17900
  }
17751
17901
 
17752
17902
  function updateTopbarContext(activeId) {
@@ -18217,7 +18367,7 @@ async function standupPerformDeepSearch(query) {
18217
18367
  standupRerender();
18218
18368
  const liveQuery = () => String(state.standup.searchQuery || '').trim();
18219
18369
  try {
18220
- const url = `/api/sessions/standup/search?q=${encodeURIComponent(query)}&token=${state.token}`;
18370
+ const url = `/api/sessions/standup/search?q=${encodeURIComponent(query)}&token=${state.token}&scope=fleet`;
18221
18371
  const resp = await fetch(url, { cache: 'no-store', signal: controller.signal });
18222
18372
  // Stale-response drop: the user may have kept typing while this was in flight — compare
18223
18373
  // against the LIVE input, not the query we sent (same guard the sidebar's
@@ -18790,7 +18940,7 @@ function renderStandupHistoryCard(result) {
18790
18940
  <div class="standup-card-top">
18791
18941
  <div style="min-width:0;">
18792
18942
  <div class="standup-card-title">${standupEsc(title)}</div>
18793
- <div class="standup-card-subtitle">${standupEsc(meta || result.sessionId || '')}</div>
18943
+ <div class="standup-card-subtitle">${typeof machineChipHtml === 'function' ? machineChipHtml(result) : ''}${standupEsc(meta || result.sessionId || '')}</div>
18794
18944
  </div>
18795
18945
  ${ago ? `<span class="standup-badge">${standupEsc(ago)}</span>` : ''}
18796
18946
  </div>
@@ -19094,6 +19244,7 @@ function showWallePanel() {
19094
19244
  const _pendingResumeProjections = new Map();
19095
19245
 
19096
19246
  function onCreated(msg) {
19247
+ if (typeof PendingCreate !== 'undefined') PendingCreate.settleCreated(msg);
19097
19248
  // A `created` carrying a restore/deferred-start source is a BACKGROUND
19098
19249
  // announcement (e.g. the server auto-resuming a deferred Wall-E session and
19099
19250
  // broadcasting `created` to every connected client), not a session this user
@@ -19163,13 +19314,18 @@ function onCreated(msg) {
19163
19314
  if (priorSession) _clearTerminalBootstrapAttempt(priorSession, id, 'created-replacement');
19164
19315
  // wp1-fix1-brief.md Failure B: a sessions-list race can pre-build this session's surface (createTerminal via _ensureTerminalSessionClientShape, runtime-20.js) before this `created` ack; createTerminal only removes a PRIOR container when it has no `.term`, so a fully-built one was left mounted while this call appended a second `#term-<id>` -- two `.resume-chip`s, strict-mode violation. Retire it with the same full teardown first.
19165
19316
  if (priorSession?.term && priorSession.container) _disposeSessionClientSurface(priorSession);
19166
- const { term, fitAddon } = createTerminal(id, { active: true });
19317
+ const { term, fitAddon } = createTerminal(id, { active: true, agentType: msg.agentType || _clientDetectAgentType(msg.cmd || ''), agentCapabilities: msg.agentCapabilities || null });
19167
19318
 
19168
19319
  // Set initial meta so the session appears in the active sessions list immediately
19169
19320
  const s = state.sessions.get(id);
19170
19321
  if (s) {
19171
19322
  s._exited = false;
19172
19323
  s.needsAttach = true;
19324
+ // AC-7b (WP-7): a conv-only session (client caps have no CLI surface) never waits for a PTY
19325
+ // attach — the server sends no terminal frames for it, so the attach bookkeeping above is
19326
+ // cleared immediately. The literal assignment stays so the source-text wiring assertion in
19327
+ // tests/terminal-bootstrap-client-wiring.test.js:2442 still describes this handler.
19328
+ if (s._convOnly === true) s.needsAttach = false;
19173
19329
  s._terminalBootstrapPending = false;
19174
19330
  s._terminalBootstrapRequestSocketGeneration = 0;
19175
19331
  s._terminalBootstrapRequestProcessGeneration = 0;
@@ -20816,7 +20972,7 @@ function _stateSyncFrameDeps(s, msg) {
20816
20972
  writeFrame: (out, complete) => (pane ? pane.writeFrame(out, complete) : _writeStateSyncFrame(s, out, complete)),
20817
20973
  onStage: (stage, seq) => _noteStateSyncFrameStage(s, msg, stage, seq),
20818
20974
  sendAck: (seq) => send(_stateSyncFrameAckPayload(s, msg, seq)),
20819
- requestKeyframe: (reason) => _requestStateSyncKeyframe(msg.id, { reason }),
20975
+ requestKeyframe: (reason, opts) => _requestStateSyncKeyframe(msg.id, { reason, ...(opts || {}) }),
20820
20976
  cellCodec: window.StateSyncCellCodec,
20821
20977
  send,
20822
20978
  onRendered: pane ? pane.onRendered : _stateSyncFrameOnRendered(s, msg, contentFrameV2),
@@ -23716,6 +23872,10 @@ function onSessionRestoreDeferred(msg) {
23716
23872
  if (!s) return;
23717
23873
  _clearExhaustedResumeBlock(s);
23718
23874
  const starting = !!msg.starting;
23875
+ // WP-2 (live-defer-wake-adopted-host-death-2026-10-28.html R-4): a NON-starting deferred frame means
23876
+ // this session's process is gone and nothing is being resumed -- retire the route (no preserveRoute)
23877
+ // so the next activation of this tab sends a fresh terminal-bootstrap and the server can wake it.
23878
+ if (!starting) _clearTerminalBootstrapAttempt(s, id, 'restore-deferred', { keepCold: true });
23719
23879
  if (!starting && /parked/i.test(String(msg.message || ''))) {
23720
23880
  _applySessionLifecycleState(s, 'parked');
23721
23881
  if (s.meta) s.meta.parkReason = String(msg.reason || '');
@@ -24428,7 +24588,16 @@ async function onSessionsList(msg, options = {}) {
24428
24588
  if (s) s.needsAttach = true;
24429
24589
  }
24430
24590
  if (!state.tabOrder.includes(sess.id)) {
24431
- state.tabOrder.push(sess.id);
24591
+ // WP-5 (AC-5b/5c): a tab the restore deferred because the hub had not delivered its session
24592
+ // yet (a BOX session before the catalog lands) comes back at its SAVED place — right after the
24593
+ // nearest saved predecessor present now — instead of appending at the end of the strip.
24594
+ if (state._undeliveredTabIds && state._undeliveredTabIds.has(sess.id)) {
24595
+ state.tabOrder.splice(
24596
+ _undeliveredTabInsertIndex(sess.id, state._undeliveredTabSaved || [], state.tabOrder), 0, sess.id);
24597
+ state._undeliveredTabIds.delete(sess.id);
24598
+ } else {
24599
+ state.tabOrder.push(sess.id);
24600
+ }
24432
24601
  }
24433
24602
  // Store metadata (preserve type for Wall-E sessions)
24434
24603
  const existing = state.sessions.get(sess.id);
@@ -24703,12 +24872,22 @@ async function onSessionsList(msg, options = {}) {
24703
24872
  _maybeRearmTerminalBootstrapFromInventory(existing, sess, { restoring });
24704
24873
  }
24705
24874
  }
24875
+ // WP-1 (D-7/§6.4): the stubs are in place — adopt a pending New Session that showed up as a row.
24876
+ if (typeof PendingCreate !== 'undefined') PendingCreate.adoptFromList(msg.sessions || []);
24706
24877
  // D-7: a FULL list that omits the id is the server's own evidence that the close landed — only
24707
24878
  // then is the pending entry dropped (no timer). A restoring list is partial by design, so it is no evidence.
24708
24879
  if (!restoring) for (const pendingId of Array.from(state.pendingCloseIds.keys())) {
24709
24880
  if (!serverIds.has(pendingId)) state.pendingCloseIds.delete(pendingId);
24710
24881
  }
24711
24882
 
24883
+ // WP-5 (AC-5d): SETTLE the undelivered set. A non-restoring list from a hub with every machine's
24884
+ // catalog delivered (`machinesPending` absent or 0) classifies every id: present ⇒ it is in
24885
+ // state.sessions (the arrival above already placed it); absent ⇒ the hub is done telling us, so the
24886
+ // id is dropped at the next save instead of being rewritten forever. Settling itself saves nothing.
24887
+ if (!restoring && !(msg.machinesPending > 0) && state._undeliveredTabIds && state._undeliveredTabIds.size) {
24888
+ state._undeliveredTabIds.clear();
24889
+ }
24890
+
24712
24891
  if (!options.skipStreamStatusRefresh && !restoring && typeof refreshStreamStatuses === 'function') {
24713
24892
  await refreshStreamStatuses({ force: true, render: false, timeoutMs: 3000,
24714
24893
  lease: options.lease, socket: options.socket, isCurrent: ownsRoot });
@@ -25324,7 +25503,7 @@ function populateModelSwitcher(sessionId, opts) {
25324
25503
  leadHost.appendChild(tagEl);
25325
25504
  });
25326
25505
  if (chipView.showTokens) _renderModelTokenBadge(leadHost, s.meta?.tokens);
25327
- chip.title = chipView.title;
25506
+ chip.title = chipView.title; if (window.CTMCodingModelPresentation) window.CTMCodingModelPresentation.syncNativeSubagentChip(chip, s, _clientAgentTypeForSession(s)); // WP-3 D-6: passive 'native sub-agents off' chip
25328
25507
 
25329
25508
  // The request decoration + delegate segment (never the lead label: D-3), then the registry names.
25330
25509
  if (window.CtmSessionModelPicker) window.CtmSessionModelPicker.syncChip(sessionId, chip);
@@ -25384,6 +25563,9 @@ let currentActiveSort = 'by_activity';
25384
25563
  let activeListExpanded = false;
25385
25564
  let activeListVisibleLimit = 6;
25386
25565
  let activeProjectCollapsedKeys = new Set();
25566
+ let currentActiveComputerFilter = null; // null = all computers, '' = Local, else an instanceId (pref active_computer_filter)
25567
+ let activeComputerMenuSig = null;
25568
+ let activeComputerSeenLabels = new Map();
25387
25569
  const ACTIVE_LIST_VISIBLE_LIMIT = 6;
25388
25570
  const ACTIVE_LIST_MIN_VISIBLE = 3;
25389
25571
  const ACTIVE_LIST_ROW_STEP_PX = 44;
@@ -25790,6 +25972,35 @@ function toggleActiveProjectGroup(key, event) {
25790
25972
  renderSessionList(true);
25791
25973
  }
25792
25974
 
25975
+ function setActiveComputerFilter(v) {
25976
+ currentActiveComputerFilter = typeof v === 'string' ? v : null;
25977
+ savePref('active_computer_filter', currentActiveComputerFilter);
25978
+ if (recentSourceChanged(currentActiveComputerFilter)) loadRecentSessions().catch(() => {});
25979
+ renderSessionList(true);
25980
+ refreshRecentSearchAfterFilterChange();
25981
+ }
25982
+
25983
+ // G-3: the removable chip in both list headers (empty when the filter is null).
25984
+ function syncActiveComputerFilterChips(label) {
25985
+ const html = currentActiveComputerFilter === null ? '' : activeComputerFilterChipHtml({ label, escHtml });
25986
+ for (const id of ['active-computer-filter-chip', 'recent-computer-filter-chip']) {
25987
+ const el = document.getElementById(id);
25988
+ if (el && el.innerHTML !== html) el.innerHTML = html;
25989
+ }
25990
+ }
25991
+
25992
+ // F-5: rewrite the Computers optgroup only when its signature changed (an open native menu survives re-renders).
25993
+ function syncActiveComputerMenu() {
25994
+ const sel = document.getElementById('active-sort');
25995
+ if (!sel) return;
25996
+ const f = currentActiveComputerFilter;
25997
+ const m = activeComputerMenuModel(Array.from(state.sessions.values()).filter(s => s && s.meta), f, activeComputerSeenLabels);
25998
+ for (const [k, l] of m.labels) activeComputerSeenLabels.set(k, l);
25999
+ sel.classList.toggle('computer-filtered', f !== null);
26000
+ sel.title = f === null ? 'Sort active sessions' : 'Sort active sessions \u00b7 showing ' + m.labels.get(f) + ' only';
26001
+ activeComputerMenuSig = applyActiveComputerOptgroup(sel, m, activeComputerMenuSig);
26002
+ syncActiveComputerFilterChips(f === null ? '' : (m.labels.get(f) || (f === '' ? 'Local' : f)));
26003
+ }
25793
26004
 
25794
26005
  function activeListSessionCount() {
25795
26006
  return Array.from(state.sessions.values()).filter(s => s && s.meta).length;
@@ -25909,18 +26120,68 @@ function workspaceTrackingAttentionStatus(tracking) {
25909
26120
  };
25910
26121
  }
25911
26122
 
26123
+ // WP-3b (docs/local-landing-queue-multi-session-integration-2026-10-26.html §8 WP-3): the landing
26124
+ // queue's chip TEXT. This pair of formatters (here and public/m/app.js) must produce byte-identical
26125
+ // strings for the same input — that is the phone parity rule, so they change together. Nothing here
26126
+ // is HTML: the callers escape, and the parts carry only numbers and closed-set state words.
26127
+ function landingEtaText(etaSeconds) {
26128
+ return typeof etaSeconds === 'number' && Number.isFinite(etaSeconds)
26129
+ ? `${Math.round(etaSeconds / 60)}m` : '';
26130
+ }
26131
+ function landingQueueText(landing) {
26132
+ if (!landing || typeof landing !== 'object') return '';
26133
+ if (typeof landing.position === 'number' && Number.isFinite(landing.position)) {
26134
+ const total = Number.isFinite(Number(landing.total)) ? Number(landing.total) : 0;
26135
+ const eta = landingEtaText(landing.eta_s);
26136
+ return `⇡#${landing.position}/${total}${eta ? ` · ${eta}` : ''}`;
26137
+ }
26138
+ const state = typeof landing.state === 'string' ? landing.state.trim() : '';
26139
+ return state ? `⇡${state}` : '';
26140
+ }
26141
+ // The conflict radar is deliberately NOT on the session row: a pairwise count ("⚠L3 ×14") on every
26142
+ // idle row was mostly ledger-file noise nobody could act on. It stays in the details panel's Radar
26143
+ // row (session-workspace-tracking.js) and the walle_landing_conflicts MCP tool.
26144
+ function landingChipText(landing) {
26145
+ return landingQueueText(landing);
26146
+ }
26147
+ function landingBadgeParts(landing) {
26148
+ const parts = [];
26149
+ const titles = [];
26150
+ const queueText = landingQueueText(landing);
26151
+ if (queueText) {
26152
+ parts.push(`<span class="wt-part wt-landing" aria-hidden="true">${escHtml(queueText)}</span>`);
26153
+ const state = typeof landing.state === 'string' && landing.state.trim() ? ` ${landing.state.trim()}` : '';
26154
+ const eta = landingEtaText(landing.eta_s);
26155
+ titles.push(`landing queue${state}${eta ? `, ETA ${eta}` : ''}`);
26156
+ }
26157
+ return { parts, titles };
26158
+ }
26159
+ // The badge for a session whose ONLY news is the landing queue — same element, same class, no
26160
+ // attention parts. Returns '' when there is nothing to show, which is what keeps a session with
26161
+ // neither attention nor landing byte-identical to before.
26162
+ function landingBadgeHtml(s, parts, titles) {
26163
+ if (!parts.length) return '';
26164
+ const label = [activeSessionBranchName(s), ...titles].filter(Boolean).join(': ');
26165
+ return `<span class="worktree-attn-badge" title="${escHtml(label)}" aria-label="${escHtml(label)}">${parts.join('')}</span>`;
26166
+ }
26167
+
25912
26168
  function worktreeAttentionBadge(s) {
25913
26169
  const tracking = typeof SessionWorkspaceTracking !== 'undefined' ? SessionWorkspaceTracking.trackingForSession(s) : null;
26170
+ // WP-3b: the landing parts are computed BEFORE the attention guard, because a session on a clean
26171
+ // branch can still be #1 in the queue and must show its position (design §8 WP-3, the guard gotcha).
26172
+ const landing = tracking && tracking.landing && typeof tracking.landing === 'object'
26173
+ ? landingBadgeParts(tracking.landing)
26174
+ : { parts: [], titles: [] };
25914
26175
  const wt = tracking ? workspaceTrackingAttentionStatus(tracking) : s?.meta?.worktreeStatus;
25915
- if (!wt || !wt.needsAttention) return '';
26176
+ if (!wt || !wt.needsAttention) return landingBadgeHtml(s, landing.parts, landing.titles);
25916
26177
  const branch = wt.branch || activeSessionBranchName(s);
25917
- if (!branch || branch === 'main' || branch === 'master') return '';
26178
+ if (!branch || branch === 'main' || branch === 'master') return landingBadgeHtml(s, landing.parts, landing.titles);
25918
26179
  const dirtyFiles = Number(wt.dirtyFiles || 0);
25919
26180
  const trackedDirtyFiles = wt.trackedDirtyFiles != null
25920
26181
  ? Number(wt.trackedDirtyFiles || 0)
25921
26182
  : Math.max(0, dirtyFiles - Number(wt.untrackedFiles || 0));
25922
26183
  const unmergedCommits = Number(wt.unmergedCommits || 0);
25923
- if (trackedDirtyFiles <= 0 && unmergedCommits <= 0) return '';
26184
+ if (trackedDirtyFiles <= 0 && unmergedCommits <= 0) return landingBadgeHtml(s, landing.parts, landing.titles);
25924
26185
 
25925
26186
  const parts = [];
25926
26187
  const titleParts = [];
@@ -25944,8 +26205,8 @@ function worktreeAttentionBadge(s) {
25944
26205
  parts.push('<span class="wt-part" aria-hidden="true">&#9670;</span>');
25945
26206
  titleParts.push('merge content differs relative to configured base');
25946
26207
  }
25947
- const title = `${branch}: ${titleParts.join(', ')}`;
25948
- return `<span class="worktree-attn-badge" title="${escHtml(title)}" aria-label="${escHtml(title)}">${parts.join('')}</span>`;
26208
+ const title = `${branch}: ${titleParts.concat(landing.titles).join(', ')}`;
26209
+ return `<span class="worktree-attn-badge" title="${escHtml(title)}" aria-label="${escHtml(title)}">${parts.join('')}${landing.parts.join('')}</span>`;
25949
26210
  }
25950
26211
 
25951
26212
 
@@ -26076,11 +26337,9 @@ function titleMatchesWorktreeLeaf(title, cwd) {
26076
26337
  function activeSessionFallbackLabel(s, id) {
26077
26338
  const meta = s?.meta || {};
26078
26339
  if (meta.type === 'walle') return 'Wall-E session';
26079
- const cmd = String(meta.cmd || '').toLowerCase();
26080
- if (cmd.includes('codex')) return 'Codex session';
26081
- if (cmd.includes('gemini')) return 'Gemini session';
26082
- if (cmd.includes('opencode') || cmd.includes('open-code')) return 'OpenCode session';
26083
- if (cmd.includes('claude')) return 'Claude Code session';
26340
+ // S-11 (opencode-followups D-5): the ONE command-word detector, shared with the server.
26341
+ const label = { dsh: 'DeepSeek Harness session', codex: 'Codex session', gemini: 'Gemini session', opencode: 'OpenCode session', claude: 'Claude Code session' }[window.CtmAgentCommand.detectAgentType(meta.cmd || '')];
26342
+ if (label) return label;
26084
26343
  return id ? `Session ${String(id).slice(0, 8)}` : 'Session';
26085
26344
  }
26086
26345
 
@@ -26732,7 +26991,15 @@ function closeWorkerRow(id) {
26732
26991
  async function retireWorkers(body) {
26733
26992
  try {
26734
26993
  const res = await fetch('/api/sessions/workers/retire', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + state.token }, body: JSON.stringify(body) });
26735
- if (!res.ok) { showToast(`Could not clear workers (${res.status})`); return null; }
26994
+ // WP-1b (§8): a box-owned lead's clear is forwarded by the hub to the box that owns the session, so
26995
+ // a machine_unreachable refusal names that box; any other refusal (and a body that is absent or not
26996
+ // JSON) keeps today's bare-status toast.
26997
+ if (!res.ok) {
26998
+ let refusal = null;
26999
+ try { refusal = await res.json(); } catch { refusal = null; }
27000
+ showToast(refusal?.code === 'machine_unreachable' ? `${refusal.machineName || 'The machine that owns this session'} is unreachable. Retry when ready.` : `Could not clear workers (${res.status})`);
27001
+ return null;
27002
+ }
26736
27003
  return await res.json();
26737
27004
  } catch (err) { showToast(`Could not clear workers: ${err && err.message}`); return null; }
26738
27005
  }
@@ -26751,7 +27018,16 @@ function clearFinishedWorkers(parentId) {
26751
27018
  // items are not CTM sessions, so they cannot ride retireWorkers' worker list; the sidecar
26752
27019
  // endpoint drops them from the lead's meta.native_subagents. Fire-and-forget: the next
26753
27020
  // sessions frame reconciles the rows.
26754
- if ((state.sessions.get(parentId)?.meta?.native_subagents?.items || []).some(i => i.status !== 'running' && i.status !== 'unknown')) fetch('/api/sessions/' + encodeURIComponent(parentId) + '/native-subagents/clear', { method: 'POST', headers: { 'Authorization': 'Bearer ' + state.token } }).then(res => { if (!res.ok) showToast(`Could not clear subagents (${res.status})`); }).catch(err => showToast('Could not clear subagents: ' + (err && err.message)));
27021
+ if ((state.sessions.get(parentId)?.meta?.native_subagents?.items || []).some(i => i.status !== 'running' && i.status !== 'unknown')) {
27022
+ // WP-1b (§8): same refusal copy as retireWorkers above — an unreachable box names itself, and a
27023
+ // body that is absent or not JSON keeps today's bare-status toast.
27024
+ fetch('/api/sessions/' + encodeURIComponent(parentId) + '/native-subagents/clear', { method: 'POST', headers: { 'Authorization': 'Bearer ' + state.token } }).then(async res => {
27025
+ if (res.ok) return;
27026
+ let refusal = null;
27027
+ try { refusal = await res.json(); } catch { refusal = null; }
27028
+ showToast(refusal?.code === 'machine_unreachable' ? `${refusal.machineName || 'The machine that owns this session'} is unreachable. Retry when ready.` : `Could not clear subagents (${res.status})`);
27029
+ }).catch(err => showToast('Could not clear subagents: ' + (err && err.message)));
27030
+ }
26755
27031
  retireWorkers({ lead: parentId });
26756
27032
  }
26757
27033
 
@@ -26884,7 +27160,9 @@ function renderSessionList(force) {
26884
27160
  // always shrink-wraps its actual rows and cannot leave a blank canyon.
26885
27161
  list.style.height = '';
26886
27162
  list.style.flexShrink = '';
27163
+ syncActiveComputerMenu();
26887
27164
  let sessions = Array.from(state.sessions.entries()).filter(([id, s]) => s.meta);
27165
+ if (currentActiveComputerFilter !== null) sessions = sessions.filter(([, s]) => activeComputerFilterPasses(s, currentActiveComputerFilter));
26888
27166
  const statusSnapshot = new Map();
26889
27167
  const statusFor = (id, s) => {
26890
27168
  if (!statusSnapshot.has(id)) statusSnapshot.set(id, getSessionStatus(s));
@@ -26937,39 +27215,16 @@ function renderSessionList(force) {
26937
27215
  });
26938
27216
  }
26939
27217
  }
26940
- let projectLabelByKey = null;
27218
+ let groupInfo = null; // { keyOf, labels, unitPrefix } while a grouped sort mode is active
26941
27219
  if (currentActiveSort === 'tab_order') {
26942
27220
  sessions.sort(tabOrderSortCmp);
26943
- } else if (currentActiveSort === 'by_project') {
26944
- // Group by project directory. Groups are ordered so the project with the most
26945
- // urgent session floats up (best status rank, then most-recent activity);
26946
- // within a group, sessions follow the same status-then-activity order as the
26947
- // activity view.
26948
- const ranks = (typeof SessionActivityUtils !== 'undefined' && SessionActivityUtils.ACTIVE_STATUS_RANK)
26949
- ? SessionActivityUtils.ACTIVE_STATUS_RANK
26950
- : { running: 0, resuming: 1, waiting: 2, idle: 3, exited: 4 };
26951
- const projectAgg = new Map(); // key -> { rank, recent }
26952
- for (const [id, s] of sessions) {
26953
- const k = activeSessionProjectKey(s);
26954
- const rank = ranks[activeStatusBucket(statusFor(id, s).cls)] ?? 4;
26955
- const recent = activeSessionLastActivityMs(s);
26956
- const cur = projectAgg.get(k) || { rank: 99, recent: 0 };
26957
- if (rank < cur.rank) cur.rank = rank;
26958
- if (recent > cur.recent) cur.recent = recent;
26959
- projectAgg.set(k, cur);
26960
- }
26961
- projectLabelByKey = activeSessionProjectLabels(Array.from(projectAgg.keys()));
26962
- sessions.sort((a, b) => {
26963
- const ka = activeSessionProjectKey(a[1]);
26964
- const kb = activeSessionProjectKey(b[1]);
26965
- if (ka !== kb) {
26966
- const ga = projectAgg.get(ka);
26967
- const gb = projectAgg.get(kb);
26968
- if (ga.rank !== gb.rank) return ga.rank - gb.rank;
26969
- if (ga.recent !== gb.recent) return gb.recent - ga.recent;
26970
- return ka < kb ? -1 : 1;
26971
- }
26972
- return compareActiveByStatusThenDisplayOrder(a, b, (entry) => activeStatusBucket(statusFor(entry[0], entry[1]).cls));
27221
+ } else if (activeSortIsGrouped(currentActiveSort)) {
27222
+ groupInfo = sortActiveGroupedSessions(currentActiveSort, sessions, {
27223
+ bucketOf: (id, s) => activeStatusBucket(statusFor(id, s).cls),
27224
+ projectKey: activeSessionProjectKey,
27225
+ projectLabels: activeSessionProjectLabels,
27226
+ lastActivityMs: activeSessionLastActivityMs,
27227
+ compareRows: (a, b) => compareActiveByStatusThenDisplayOrder(a, b, (entry) => activeStatusBucket(statusFor(entry[0], entry[1]).cls)),
26973
27228
  });
26974
27229
  } else {
26975
27230
  sessions.sort((a, b) => compareActiveByStatusThenDisplayOrder(
@@ -27031,27 +27286,15 @@ function renderSessionList(force) {
27031
27286
  return acc;
27032
27287
  }, {})
27033
27288
  : {};
27034
- const projectActiveKeys = currentActiveSort === 'by_project'
27035
- ? sessions.reduce((acc, [id, s]) => {
27036
- if (id === state.activeTab) acc.add(activeSessionProjectKey(s));
27037
- return acc;
27038
- }, new Set())
27039
- : new Set();
27040
- const renderProjectGroupSep = (key, count) => {
27041
- const label = (projectLabelByKey && projectLabelByKey.get(key)) || '(no project)';
27042
- const collapsed = isActiveProjectCollapsed(key);
27043
- const hasActive = projectActiveKeys.has(key);
27044
- const title = key || 'No project directory';
27045
- const verb = collapsed ? 'Expand' : 'Collapse';
27046
- return `<button type="button" class="session-group-sep project-sep${collapsed ? ' collapsed' : ''}${hasActive ? ' has-active' : ''}" data-project-key="${escHtml(key)}" title="${escHtml(title)}" aria-expanded="${collapsed ? 'false' : 'true'}" aria-label="${verb} ${escHtml(label)} project group, ${count} active session${count === 1 ? '' : 's'}" onclick="toggleActiveProjectGroup('${activeSessionJsArg(key)}', event)"><span class="project-sep-chevron" aria-hidden="true">▾</span>${projectGroupFolderIconSvg(13)}<span class="project-sep-label">${escHtml(label)}</span><span class="project-sep-count">${count}</span></button>`;
27047
- };
27048
- const projectCounts = currentActiveSort === 'by_project'
27049
- ? sessions.reduce((acc, [, s]) => {
27050
- const k = activeSessionProjectKey(s);
27051
- acc[k] = (acc[k] || 0) + 1;
27052
- return acc;
27053
- }, {})
27054
- : {};
27289
+ const groupCountByKey = new Map();
27290
+ const groupActiveKeys = new Set();
27291
+ if (groupInfo) {
27292
+ for (const [id, s] of sessions) {
27293
+ const k = groupInfo.keyOf(s);
27294
+ groupCountByKey.set(k, (groupCountByKey.get(k) || 0) + 1);
27295
+ if (id === state.activeTab) groupActiveKeys.add(k);
27296
+ }
27297
+ }
27055
27298
 
27056
27299
  let lastGroup = null;
27057
27300
  const units = [];
@@ -27062,10 +27305,14 @@ function renderSessionList(force) {
27062
27305
  units.push({ key: 'sep:' + cls, html: renderGroupSep(cls, groupCounts[cls] || 0) });
27063
27306
  lastGroup = cls;
27064
27307
  }
27065
- } else if (currentActiveSort === 'by_project') {
27066
- const k = activeSessionProjectKey(s);
27308
+ } else if (groupInfo) {
27309
+ const k = groupInfo.keyOf(s);
27067
27310
  if (k !== lastGroup) {
27068
- units.push({ key: 'psep:' + k, html: renderProjectGroupSep(k, projectCounts[k] || 0) });
27311
+ units.push({ key: groupInfo.unitPrefix + k, html: activeGroupHeaderHtml(currentActiveSort, {
27312
+ key: k, label: groupInfo.labels.get(k), count: groupCountByKey.get(k) || 0,
27313
+ collapsed: isActiveProjectCollapsed(k), hasActive: groupActiveKeys.has(k),
27314
+ iconHtml: projectGroupFolderIconSvg(13), escHtml, jsArg: activeSessionJsArg,
27315
+ }) });
27069
27316
  lastGroup = k;
27070
27317
  }
27071
27318
  if (!isActiveFiltering && isActiveProjectCollapsed(k)) return;
@@ -27220,7 +27467,13 @@ async function dismissSessionAttention(id, event) {
27220
27467
  // Switch the Active Sessions sort mode and persist. Defaults are set at
27221
27468
  // the top of this block; loadPrefs() hydrates from the server-side store.
27222
27469
  function setActiveSort(mode) {
27223
- if (mode !== 'by_activity' && mode !== 'tab_order' && mode !== 'by_project') mode = 'by_activity';
27470
+ const computerFilter = parseActiveComputerOptionValue(mode);
27471
+ if (computerFilter !== undefined) {
27472
+ const csel = document.getElementById('active-sort');
27473
+ if (csel) csel.value = currentActiveSort;
27474
+ return setActiveComputerFilter(computerFilter);
27475
+ }
27476
+ mode = normalizeActiveSort(mode);
27224
27477
  currentActiveSort = mode;
27225
27478
  savePref('active_sort', mode);
27226
27479
  const sel = document.getElementById('active-sort');
@@ -28652,7 +28905,30 @@ function moveTabToStart(id) {
28652
28905
  function saveTabOrder() {
28653
28906
  // Save session IDs and review tab (for position restore after restart)
28654
28907
  const sessionOrder = state.tabOrder.filter(id => state.sessions.has(id) || id === 'review');
28655
- savePref('tab_order', sessionOrder);
28908
+ savePref('tab_order', _withUndeliveredTabIds(sessionOrder));
28909
+ }
28910
+
28911
+ // WP-5 (S-12): an undelivered id is written back where it was saved — right after its nearest
28912
+ // SAVED predecessor that is present now (or at the front) — so a user reorder of the present tabs
28913
+ // is honoured and the box tab still comes back in its place when the catalog lands.
28914
+ function _withUndeliveredTabIds(order) {
28915
+ const pending = state._undeliveredTabIds;
28916
+ if (!pending || pending.size === 0) return order;
28917
+ const saved = state._undeliveredTabSaved || [];
28918
+ const out = order.slice();
28919
+ for (const id of saved) {
28920
+ if (!pending.has(id) || out.includes(id)) continue;
28921
+ out.splice(_undeliveredTabInsertIndex(id, saved, out), 0, id);
28922
+ }
28923
+ return out;
28924
+ }
28925
+
28926
+ function _undeliveredTabInsertIndex(id, saved, out) {
28927
+ for (let k = saved.indexOf(id) - 1; k >= 0; k -= 1) {
28928
+ const at = out.indexOf(saved[k]);
28929
+ if (at !== -1) return at + 1;
28930
+ }
28931
+ return 0;
28656
28932
  }
28657
28933
 
28658
28934
  // --- Actions ---
@@ -28697,7 +28973,7 @@ var _nsPathPickerState = {
28697
28973
  activeIndex: -1,
28698
28974
  items: [],
28699
28975
  projects: [],
28700
- projectsLoaded: false,
28976
+ projectsLoaded: false, projectsError: false, // projectsError (D-6): the list FAILED to read — never an empty list
28701
28977
  projectsLoading: false,
28702
28978
  worktreeTimer: 0,
28703
28979
  expanded: {}, // { projectKey: true } — worktrees the user has revealed
@@ -28967,12 +29243,12 @@ function _nsPathLoadProjects() {
28967
29243
  // are the only place the same-repo prefill looks — plain gitRemoteKey equality inside
28968
29244
  // nsMachineAfterProjectsLoaded(), against the keys of the machine we are pointed at.
28969
29245
  fetch('/api/projects?token=' + encodeURIComponent(state.token || '') + machineQueryParam())
28970
- .then(function(r) { return r.ok ? r.json() : []; })
29246
+ .then(function(r) { if (!r.ok) throw new Error('projects read failed'); return r.json(); })
28971
29247
  .then(function(projects) {
28972
29248
  _nsPathPickerState.projects = Array.isArray(projects)
28973
29249
  ? projects.filter(function(p) { return p && p.path; })
28974
29250
  : [];
28975
- _nsPathPickerState.projectsLoaded = true;
29251
+ _nsPathPickerState.projectsLoaded = true; _nsPathPickerState.projectsError = false;
28976
29252
  _nsPathPickerState.projectsLoading = false;
28977
29253
  _nsPathRefreshHostStatus();
28978
29254
  if (typeof nsMachineAfterProjectsLoaded === 'function') nsMachineAfterProjectsLoaded(_nsPathPickerState);
@@ -28981,7 +29257,8 @@ function _nsPathLoadProjects() {
28981
29257
  if (typeof _storagePathRepaintOpen === 'function') _storagePathRepaintOpen();
28982
29258
  })
28983
29259
  .catch(function() {
28984
- _nsPathPickerState.projectsLoading = false;
29260
+ _nsPathPickerState.projectsLoading = false; _nsPathPickerState.projectsError = true; // D-6: UNKNOWN, not "no projects"; not latched
29261
+ if (typeof nsMachineAfterProjectsLoaded === 'function') nsMachineAfterProjectsLoaded(_nsPathPickerState);
28985
29262
  });
28986
29263
  }
28987
29264
 
@@ -29831,7 +30108,7 @@ function _nsWorktreeNamespaceForAgent(agentType) {
29831
30108
  function _nsAgentUsesCodeWorkspace() {
29832
30109
  var val = document.getElementById('ns-agent').value || 'claude:';
29833
30110
  var agent = val.replace(/:$/, '');
29834
- return ['claude', 'codex', 'gemini', 'walle'].indexOf(agent) !== -1;
30111
+ return ['claude', 'codex', 'gemini', 'walle', 'dsh'].indexOf(agent) !== -1;
29835
30112
  }
29836
30113
 
29837
30114
  function _nsHasActiveSessionInCwd(cwd) {
@@ -29949,73 +30226,13 @@ function showNewSessionModal() {
29949
30226
  var createBtn = document.querySelector('#new-session-modal .btn.primary');
29950
30227
  if (createBtn) { createBtn.disabled = false; createBtn.textContent = 'Create'; }
29951
30228
 
29952
- var grid = document.getElementById('ns-agent-grid');
29953
30229
  document.getElementById('ns-agent').value = 'claude:';
29954
30230
 
29955
- // Agent card definitions. The `agent` key drives providerIconSvg() so the
29956
- // card icon renders the SAME brand mark that appears in the session-list
29957
- // sidebar and tab header — agent identity is one visual vocabulary across
29958
- // the whole UI. Single-letter fallbacks ('$', '>') stay for `shell` and
29959
- // `custom`, which aren't LLM-branded surfaces.
29960
- var agentDefs = [
29961
- { id: 'claude:', name: 'Claude Code', sub: 'Anthropic', color: '#D97757', agent: 'claude', detect: 'claude' },
29962
- { id: 'codex:', name: 'Codex', sub: 'OpenAI', color: '#22c55e', agent: 'codex', detect: 'codex' },
29963
- { id: 'opencode:', name: 'OpenCode', sub: 'GLM 5.2', color: '#e0af68', agent: 'opencode', detect: 'opencode' },
29964
- { id: 'walle:', name: 'Wall-E', sub: 'Smart routing', color: '#f59e0b', agent: 'walle', detect: null },
29965
- { id: 'cursor:', name: 'Cursor', sub: 'Anysphere', color: '#111827', accent: '#7aa2f7', agent: 'cursor', detect: 'cursor-agent' },
29966
- { id: 'gemini:', name: 'Gemini CLI', sub: 'Google', color: '#3b82f6', agent: 'gemini', detect: 'gemini' },
29967
- { id: 'shell:', name: 'Shell', sub: 'Terminal', color: '#6b7280', icon: '$', detect: null },
29968
- ];
29969
-
29970
- function agentAvailability(agents, detectKey) {
29971
- if (!detectKey) return { known: true, available: true, path: null };
29972
- if (!agents) return { known: false, available: true, path: null };
29973
- var detail = agents.agents && agents.agents[detectKey] ? agents.agents[detectKey] : agents[detectKey];
29974
- if (detail && typeof detail === 'object') {
29975
- return { known: true, available: detail.available !== false, path: detail.path || detail.realpath || null };
29976
- }
29977
- if (typeof detail === 'boolean') return { known: true, available: detail, path: null };
29978
- return { known: false, available: true, path: null };
29979
- }
29980
-
29981
- function renderAgentCards(agents) {
29982
- var selected = document.getElementById('ns-agent').value || 'claude:';
29983
- grid.innerHTML = '';
29984
- agentDefs.forEach(function(def) {
29985
- var availability = agentAvailability(agents, def.detect);
29986
- var available = availability.available;
29987
- var card = document.createElement('div');
29988
- card.className = 'ns-agent-card' + (available ? '' : ' not-detected');
29989
- card.style.setProperty('--ns-card-accent', def.accent || def.color);
29990
- card.dataset.value = def.id;
29991
- if (availability.path) card.title = def.name + ': ' + availability.path;
29992
- else if (availability.known && available === false) card.title = def.name + ' CLI not detected on PATH. You can still select it; launch will show the real terminal error if the command is unavailable.';
29993
- // Render a real brand icon when the agent maps to one (claude/codex/
29994
- // gemini/opencode/walle); otherwise the textual fallback ($, >) stays.
29995
- // Inline color forces the brand tint regardless of theme — these
29996
- // tiles always show on the dark agent-picker background, never on
29997
- // an active highlight, so we don't need the dark-on-light invert
29998
- // that the session-list .active rule applies via currentColor.
29999
- var iconHtml = def.agent
30000
- ? '<div class="ns-card-icon" style="background:' + def.color + ';color:#fff;">'
30001
- + providerIconSvg(def.agent, 22) + '</div>'
30002
- : '<div class="ns-card-icon" style="background:' + def.color + '">' + def.icon + '</div>';
30003
- card.innerHTML =
30004
- iconHtml +
30005
- '<div class="ns-card-name">' + def.name + '</div>' +
30006
- '<div class="ns-card-sub">' + def.sub + (available === false ? ' (not found)' : '') + '</div>';
30007
- card.onclick = function() { _selectAgentCard(def.id); };
30008
- grid.appendChild(card);
30009
- });
30010
- _selectAgentCard(selected);
30011
- }
30012
-
30013
- renderAgentCards(null);
30014
-
30015
- fetch('/api/models/available-agents?token=' + encodeURIComponent(state.token))
30016
- .then(function(r) { return r.json(); })
30017
- .then(function(agents) { renderAgentCards(agents); })
30018
- .catch(function() { renderAgentCards(null); });
30231
+ // S-43 (WP-7, docs/deepseek-harness-acp-agent-2026-10-28.html §9): the agent grid — the card
30232
+ // table (with the dsh card between gemini: and shell:), PATH availability, the OQ-4 Model select,
30233
+ // the D-15 remote-machine disable and the D-16 MCP disable — now lives in
30234
+ // public/js/new-session-agent-grid.js; index.html is at its file-size ceiling.
30235
+ NewSessionAgentGrid.open();
30019
30236
 
30020
30237
  document.getElementById('ns-label').focus();
30021
30238
  _maybeRecommendWorktreeForNewSession(true);
@@ -31602,8 +31819,9 @@ async function loadPrefs(opts = {}) {
31602
31819
  }
31603
31820
 
31604
31821
  // Restore Active Sessions sort + expand prefs.
31605
- if (prefs.active_sort === 'tab_order' || prefs.active_sort === 'by_activity' || prefs.active_sort === 'by_project') {
31606
- currentActiveSort = prefs.active_sort;
31822
+ const restoredSort = normalizeActiveSort(prefs.active_sort);
31823
+ if (prefs.active_sort === restoredSort) {
31824
+ currentActiveSort = restoredSort;
31607
31825
  const sel = document.getElementById('active-sort');
31608
31826
  if (sel) sel.value = currentActiveSort;
31609
31827
  }
@@ -31616,6 +31834,7 @@ async function loadPrefs(opts = {}) {
31616
31834
  if (Array.isArray(prefs.active_project_collapsed_keys)) {
31617
31835
  activeProjectCollapsedKeys = new Set(normalizeActiveProjectCollapsedKeys(prefs.active_project_collapsed_keys));
31618
31836
  }
31837
+ currentActiveComputerFilter = typeof prefs.active_computer_filter === 'string' ? prefs.active_computer_filter : null;
31619
31838
  // Lifecycle WP-6 (§5 D-3, §9 S-25): hydrate the per-parent worker-group toggle state.
31620
31839
  // Pruning against state.sessions happens lazily in toggleWorkerGroup (loadPrefs can
31621
31840
  // race the session-list fetch at boot, so pruning here risks discarding a real toggle
@@ -31985,34 +32204,35 @@ async function loadRecentSessions(options = {}) {
31985
32204
  // Keep the Claude Desktop fork map fresh so review actions show Convert vs Resume-fork.
31986
32205
  refreshDesktopForkMap();
31987
32206
  if (!_isRootPrivateLeaseCurrent(lease)) return;
32207
+ const _recent = beginRecentLoad(currentActiveComputerFilter), _live = () => _isRootPrivateLeaseCurrent(lease) && _recent.token === recentMachineLoad.token;
31988
32208
  let res;
31989
32209
  let data;
31990
32210
  try {
31991
- res = await fetch(`/api/recent-sessions?token=${state.token}&limit=200`);
31992
- if (!_isRootPrivateLeaseCurrent(lease)) return;
31993
- data = await res.json();
31994
- if (!_isRootPrivateLeaseCurrent(lease)) return;
31995
- if (!res.ok && res.status !== 202) {
32211
+ res = _recent.source.kind === 'machine' ? null : await fetch(recentRequestUrl(`/api/recent-sessions?token=${state.token}&limit=200`, _recent.source));
32212
+ if (!_live()) return;
32213
+ data = res ? recentNoteFleetBody(await res.json()) : recentNoteFleetBody(await fetchMachineRecentRows(fetch, `/api/recent-sessions?token=${state.token}&limit=200`, _recent, activeComputerSeenLabels));
32214
+ if (!_live()) return;
32215
+ if (res && !res.ok && res.status !== 202) {
31996
32216
  throw new Error(`Recent sessions request failed (${res.status})`);
31997
32217
  }
31998
32218
  } catch (error) {
31999
- if (!_isRootPrivateLeaseCurrent(lease)) return;
32219
+ if (!_live()) return;
32000
32220
  const lastGood = allRecentSessions.length > 0 ? allRecentSessions : _readRecentSessionsLastGood(lease);
32001
- if (!_isRootPrivateLeaseCurrent(lease)) return;
32221
+ if (!_live()) return;
32002
32222
  if (lastGood.length === 0) throw error;
32003
32223
  data = { sessions: lastGood, stale: true, catalogDegraded: true };
32004
32224
  }
32005
- if (!_isRootPrivateLeaseCurrent(lease)) return;
32225
+ if (!_live()) return;
32006
32226
  const degraded = res?.status === 202
32007
32227
  || (!Array.isArray(data) && (data?.stale === true || data?.catalogDegraded === true));
32008
32228
  // Support both old (array) and new (object with sessions + lastActiveProject) response formats
32009
32229
  const incoming = Array.isArray(data) ? data : (Array.isArray(data?.sessions) ? data.sessions : []);
32010
32230
  if (incoming.length > 0) {
32011
32231
  allRecentSessions = incoming;
32012
- if (!degraded) _writeRecentSessionsLastGood(incoming, lease);
32232
+ if (!degraded && res) _writeRecentSessionsLastGood(incoming, lease);
32013
32233
  } else if (degraded) {
32014
32234
  const lastGood = allRecentSessions.length > 0 ? allRecentSessions : _readRecentSessionsLastGood(lease);
32015
- if (!_isRootPrivateLeaseCurrent(lease)) return;
32235
+ if (!_live()) return;
32016
32236
  if (lastGood.length > 0) allRecentSessions = lastGood;
32017
32237
  } else {
32018
32238
  allRecentSessions = incoming;
@@ -32161,6 +32381,7 @@ function _recentSidebarFilterState() {
32161
32381
  emptyMode: showEmptyOnly ? 'only' : 'exclude',
32162
32382
  project: currentProjectFilter,
32163
32383
  agent: currentAgentFilter,
32384
+ machine: currentActiveComputerFilter,
32164
32385
  };
32165
32386
  }
32166
32387
 
@@ -32466,6 +32687,8 @@ function recentSessionAgentProviderFallback(s) {
32466
32687
  if (agentTypes.includes('gemini')) return 'google';
32467
32688
  if (agentTypes.includes('claude') || agentTypes.includes('claude-desktop')) return 'anthropic';
32468
32689
  if (agentTypes.includes('cursor')) return 'cursor';
32690
+ // WP-7 (S-50): dsh chips use the DeepSeek brand colour (recent-model-provider-deepseek).
32691
+ if (agentTypes.includes('dsh')) return 'deepseek';
32469
32692
  return '';
32470
32693
  }
32471
32694
 
@@ -32511,6 +32734,7 @@ function recentSessionAgentBadgeColorKey(agentType) {
32511
32734
  case 'walle': return 'walle';
32512
32735
  case 'cursor': return 'cursor';
32513
32736
  case 'opencode': return 'opencode';
32737
+ case 'dsh': return 'deepseek';
32514
32738
  default: return '';
32515
32739
  }
32516
32740
  }
@@ -32701,9 +32925,10 @@ function recentSearchEmptyHtml(label) {
32701
32925
  }
32702
32926
  if (currentAgentFilter) details.push('agent ' + escHtml(SessionSearchUtils.recentAgentFilterLabel(currentAgentFilter)));
32703
32927
  if (currentProjectFilter) details.push('project ' + escHtml(currentProjectFilter.replace(/^\/Users\/[^/]+/, '~')));
32928
+ if (currentActiveComputerFilter !== null) details.push('computer ' + escHtml(activeComputerSeenLabels.get(currentActiveComputerFilter) || (currentActiveComputerFilter === '' ? 'Local' : currentActiveComputerFilter)));
32704
32929
  details.push(showEmptyOnly ? 'empty only' : 'non-empty');
32705
32930
  if (document.getElementById('this-device')?.checked !== false) details.push('this device');
32706
- const detailHtml = rawQuery && details.length
32931
+ const detailHtml = (rawQuery || currentActiveComputerFilter !== null) && details.length
32707
32932
  ? '<div style="margin-top:6px;color:var(--fg-dim);line-height:1.35">Filters: ' + details.join(', ') + '</div>'
32708
32933
  : '';
32709
32934
  return '<div style="padding:10px;font-size:12px;color:var(--fg-dim)">' + escHtml(label || 'No sessions found') + detailHtml + '</div>';
@@ -32713,7 +32938,7 @@ function renderRecentSessions(sessions) {
32713
32938
  const list = document.getElementById('recent-session-list');
32714
32939
 
32715
32940
  if (sessions.length === 0) {
32716
- list.innerHTML = recentSearchEmptyHtml('No sessions found');
32941
+ list.innerHTML = recentSearchEmptyHtml(recentMachineLoad.unavailable || 'No sessions found');
32717
32942
  return;
32718
32943
  }
32719
32944
 
@@ -33115,7 +33340,7 @@ function _reviewMessagesUrl(sessionId, projectEntry, opts) {
33115
33340
  let url = `/api/session/messages?id=${encodeURIComponent(sessionId)}`
33116
33341
  + `&project=${encodeURIComponent(projectEntry || '')}`
33117
33342
  + `&token=${state.token}`
33118
- + '&conversationContract=4';
33343
+ + '&conversationContract=4' + recentRequestUrl('', recentSourceForFilter(state.reviewMachine));
33119
33344
  if (opts.entries) return url + `&entries=${encodeURIComponent(opts.entries)}`;
33120
33345
  url += `&limitTurns=${REVIEW_TURN_PAGE_SIZE}`;
33121
33346
  if (opts.before) url += `&before=${encodeURIComponent(opts.before)}`;
@@ -34324,7 +34549,7 @@ function toggleEmptyFilter() {
34324
34549
  // element (.session-item or .tab) sets color via CSS so the icon inherits the
34325
34550
  // per-provider hue and inverts cleanly when the row is active (light bg, dark text).
34326
34551
  // Returns an HTML string with title for screen-readers + hover tooltip.
34327
- const AGENT_LABELS = { walle: 'Wall-E', codex: 'Codex', gemini: 'Gemini', opencode: 'OpenCode', cursor: 'Cursor', claude: 'Claude Code', 'claude-desktop': 'Claude Desktop', shell: 'Shell' };
34552
+ const AGENT_LABELS = { walle: 'Wall-E', codex: 'Codex', gemini: 'Gemini', opencode: 'OpenCode', cursor: 'Cursor', claude: 'Claude Code', 'claude-desktop': 'Claude Desktop', dsh: 'DeepSeek Harness', shell: 'Shell' };
34328
34553
  function getAgentType(s) {
34329
34554
  return _clientAgentTypeForSession(s);
34330
34555
  }
@@ -34385,6 +34610,11 @@ function providerIconSvg(agentType, sizePx) {
34385
34610
  + '<path fill="currentColor" fill-opacity="0.55" d="M2.2 4.75 L8 8 L8 14.5 L2.2 11.25 Z"/>'
34386
34611
  + '<path fill="currentColor" fill-opacity="0.3" d="M8 8 L13.8 4.75 L13.8 11.25 L8 14.5 Z"/>';
34387
34612
  break;
34613
+ case 'dsh':
34614
+ // WP-7 (S-48): the DeepSeek Harness mark is the DeepSeek brand whale — the same
34615
+ // simple-icons path the model-provider rows use (brand colour #4D6BFE, currentColor here so
34616
+ // the sidebar/tab .session-item[data-agent="dsh"] rules keep theming control).
34617
+ return brandIconSvg('deepseek', size);
34388
34618
  case 'shell':
34389
34619
  // >_ prompt — universal terminal metaphor.
34390
34620
  inner = '<path fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" d="M2.5 5 L6 8 L2.5 11 M8.5 12 L13.5 12"/>';
@@ -34765,7 +34995,7 @@ function _buildSessionPaletteRow(item, idx) {
34765
34995
  const badge = document.createElement('span');
34766
34996
  badge.style.cssText = 'font-size:9px;padding:1px 6px;border-radius:4px;flex-shrink:0;' + (isActive ? 'background:rgba(0,0,0,0.2);' : 'background:var(--bg);color:var(--fg-dim);');
34767
34997
  badge.textContent = item.kind === 'active' ? 'Tab' : 'View';
34768
- row.appendChild(badge);
34998
+ row.appendChild(badge); const machineChip = typeof machineSessionChipNode === 'function' && machineSessionChipNode(item.kind === 'active' ? (state.sessions.get(item.sessionId) || item) : item, item.sessionId); if (machineChip) row.insertBefore(machineChip, badge); // fleet WP-5
34769
34999
 
34770
35000
  return row;
34771
35001
  }
@@ -34815,12 +35045,12 @@ async function performServerSearch(query) {
34815
35045
  const grammarQuery = (typeof SessionSearchGrammar !== 'undefined')
34816
35046
  ? (SessionSearchGrammar.parseSearchQuery(query).core || query)
34817
35047
  : query;
34818
- const res = await fetch(`/api/sessions/search?q=${encodeURIComponent(grammarQuery)}&limit=50&token=${state.token}`);
34819
- const data = await res.json().catch(() => ({}));
35048
+ const _searchSource = recentSourceForFilter(currentActiveComputerFilter), res = await fetch(recentRequestUrl(`/api/sessions/search?q=${encodeURIComponent(grammarQuery)}&limit=50&token=${state.token}`, _searchSource));
35049
+ const data = stampRecentBodyFromSource(await res.json().catch(() => ({})), _searchSource, activeComputerSeenLabels);
34820
35050
 
34821
- // Check if query is still current (user may have typed more)
35051
+ // Check if query is still current (user may have typed more, or switched the computer filter below)
34822
35052
  const currentQ = document.getElementById('recent-search').value.trim();
34823
- if (currentQ.toLowerCase() !== query.toLowerCase()) return;
35053
+ if (currentQ.toLowerCase() !== query.toLowerCase() || (data._sourceKey || '') !== recentSourceKey(recentSourceForFilter(currentActiveComputerFilter))) return;
34824
35054
  if (!res.ok || data.error || !Array.isArray(data.results)) {
34825
35055
  showSearchDegradedFallback(
34826
35056
  query,
@@ -34993,7 +35223,7 @@ function renderServerSearchResults(results, total, query, degraded) {
34993
35223
 
34994
35224
  // Note: html is safe — all user data is escaped via escHtml(), highlights use <mark> only.
34995
35225
  // DOMPurify was stripping onclick/ondblclick/oncontextmenu handlers, breaking click-to-review.
34996
- list.innerHTML = html || recentSearchEmptyHtml('No results found');
35226
+ list.innerHTML = html || recentSearchEmptyHtml(recentMachineLoad.unavailable || 'No results found');
34997
35227
  }
34998
35228
 
34999
35229
  function updateSearchCount(count, degraded) {