create-walle 0.10.2 → 0.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (488) hide show
  1. package/README.md +3 -1
  2. package/lib/release-schema.json +2 -2
  3. package/lib/runtime-closure-policy.json +8 -7
  4. package/package.json +1 -1
  5. package/template/coding-task-manager/api-prompts.js +45 -7
  6. package/template/coding-task-manager/bin/claude-hook.js +55 -1
  7. package/template/coding-task-manager/bin/ctm-relay-client.js +35 -12
  8. package/template/coding-task-manager/bin/land.js +2069 -0
  9. package/template/coding-task-manager/bin/memory-census.js +4 -2
  10. package/template/coding-task-manager/bin/memory-sync-verify.js +151 -0
  11. package/template/coding-task-manager/bin/probe-refusal-scope.js +530 -0
  12. package/template/coding-task-manager/db.js +156 -32
  13. package/template/coding-task-manager/lib/acp/acp-client.js +383 -0
  14. package/template/coding-task-manager/lib/acp/dsh-context-truth.js +176 -0
  15. package/template/coding-task-manager/lib/acp/dsh-launch-model.js +302 -0
  16. package/template/coding-task-manager/lib/acp/dsh-mcp-servers.js +194 -0
  17. package/template/coding-task-manager/lib/acp/dsh-prompt-lane.js +616 -0
  18. package/template/coding-task-manager/lib/acp/dsh-route-patch.js +308 -0
  19. package/template/coding-task-manager/lib/acp/dsh-session-runtime.js +522 -0
  20. package/template/coding-task-manager/lib/acp/dsh-transcript.js +395 -0
  21. package/template/coding-task-manager/lib/acp/dsh-usage-log.js +162 -0
  22. package/template/coding-task-manager/lib/acp/ndjson-stdio-transport.js +225 -0
  23. package/template/coding-task-manager/lib/adopted-host-redial.js +47 -11
  24. package/template/coding-task-manager/lib/adopted-launch-environment.js +123 -0
  25. package/template/coding-task-manager/lib/agent-auto-update-runtime.js +0 -3
  26. package/template/coding-task-manager/lib/agent-auto-update.js +26 -10
  27. package/template/coding-task-manager/lib/agent-bundle/build.js +657 -0
  28. package/template/coding-task-manager/lib/agent-bundle/codex-install.js +558 -0
  29. package/template/coding-task-manager/lib/agent-bundle/drift.js +120 -0
  30. package/template/coding-task-manager/lib/agent-bundle/gc.js +522 -0
  31. package/template/coding-task-manager/lib/agent-bundle/inventory.js +346 -0
  32. package/template/coding-task-manager/lib/agent-bundle/push.js +717 -0
  33. package/template/coding-task-manager/lib/agent-bundle/skill-transfer.js +724 -0
  34. package/template/coding-task-manager/lib/agent-bundle/spawn.js +119 -0
  35. package/template/coding-task-manager/lib/agent-bundle/store.js +568 -0
  36. package/template/coding-task-manager/lib/agent-capabilities.js +142 -12
  37. package/template/coding-task-manager/lib/agent-cli-cache.js +30 -5
  38. package/template/coding-task-manager/lib/agent-definitions.js +19 -0
  39. package/template/coding-task-manager/lib/agent-hooks-codex-toml.js +28 -16
  40. package/template/coding-task-manager/lib/agent-hooks-codex-workspace.js +9 -1
  41. package/template/coding-task-manager/lib/agent-hooks-installer.js +23 -8
  42. package/template/coding-task-manager/lib/agent-hooks-prune.js +37 -11
  43. package/template/coding-task-manager/lib/agent-hooks-shim.js +105 -0
  44. package/template/coding-task-manager/lib/agent-presets.js +84 -1
  45. package/template/coding-task-manager/lib/agent-upgrade-jobs.js +12 -1
  46. package/template/coding-task-manager/lib/agent-version-service.js +74 -11
  47. package/template/coding-task-manager/lib/all-sessions-query.js +6 -2
  48. package/template/coding-task-manager/lib/api/agent-bundles.js +436 -0
  49. package/template/coding-task-manager/lib/api/backup-retention.js +83 -0
  50. package/template/coding-task-manager/lib/api/conversation-sync-routes.js +124 -0
  51. package/template/coding-task-manager/lib/api/diagnostics.js +549 -69
  52. package/template/coding-task-manager/lib/api/fleet.js +118 -0
  53. package/template/coding-task-manager/lib/api/hub-bridge.js +243 -0
  54. package/template/coding-task-manager/lib/api/ingress.js +9 -1
  55. package/template/coding-task-manager/lib/api/landing.js +403 -0
  56. package/template/coding-task-manager/lib/api/machines.js +691 -2
  57. package/template/coding-task-manager/lib/api/memory-sync-routes.js +335 -0
  58. package/template/coding-task-manager/lib/api/misc-ops.js +8 -3
  59. package/template/coding-task-manager/lib/api/remote-sessions.js +1326 -0
  60. package/template/coding-task-manager/lib/api/session-attach.js +623 -0
  61. package/template/coding-task-manager/lib/api/session-messages.js +1384 -0
  62. package/template/coding-task-manager/lib/api/sessions.js +77 -51
  63. package/template/coding-task-manager/lib/api/setup-network.js +544 -16
  64. package/template/coding-task-manager/lib/api/setup-wizard-handlers.js +884 -0
  65. package/template/coding-task-manager/lib/api/setup.js +829 -10
  66. package/template/coding-task-manager/lib/api/walle-bridge.js +9 -1
  67. package/template/coding-task-manager/lib/attachment-reconciler-wiring.js +5 -2
  68. package/template/coding-task-manager/lib/auth-rate-limit.js +23 -0
  69. package/template/coding-task-manager/lib/auth-rules.js +292 -21
  70. package/template/coding-task-manager/lib/background-stop.js +369 -0
  71. package/template/coding-task-manager/lib/best-of-attempts.js +4 -1
  72. package/template/coding-task-manager/lib/best-of-launch-plan.js +52 -0
  73. package/template/coding-task-manager/lib/box-keep-awake.js +89 -0
  74. package/template/coding-task-manager/lib/broadcast-reason-stats.js +152 -0
  75. package/template/coding-task-manager/lib/broadcast-serialize.js +5 -0
  76. package/template/coding-task-manager/lib/build-info.js +4 -2
  77. package/template/coding-task-manager/lib/claude-model-truth.js +13 -2
  78. package/template/coding-task-manager/lib/claude-transcript-terminal-renderer.js +17 -13
  79. package/template/coding-task-manager/lib/client-diag-handler.js +64 -2
  80. package/template/coding-task-manager/lib/codex-app-server-client.js +14 -16
  81. package/template/coding-task-manager/lib/codex-configuration-history.js +55 -0
  82. package/template/coding-task-manager/lib/codex-harness-qualification.js +43 -20
  83. package/template/coding-task-manager/lib/codex-rollout-history.js +743 -0
  84. package/template/coding-task-manager/lib/coding-agent-models.js +27 -12
  85. package/template/coding-task-manager/lib/coding-cli-skill-installer.js +44 -14
  86. package/template/coding-task-manager/lib/coding-session-model-adapters.js +43 -2
  87. package/template/coding-task-manager/lib/coding-session-model-owner.js +1 -1
  88. package/template/coding-task-manager/lib/coding-session-model-public.js +7 -1
  89. package/template/coding-task-manager/lib/coding-session-model-runtime.js +23 -4
  90. package/template/coding-task-manager/lib/coding-session-model-scan.js +31 -0
  91. package/template/coding-task-manager/lib/coding-session-model-state.js +7 -0
  92. package/template/coding-task-manager/lib/conversation-freshness-diagnostics.js +280 -0
  93. package/template/coding-task-manager/lib/conversation-item-clock.js +261 -0
  94. package/template/coding-task-manager/lib/conversation-source-registry.js +5 -0
  95. package/template/coding-task-manager/lib/conversation-store-v2.js +44 -0
  96. package/template/coding-task-manager/lib/conversation-sync.js +355 -0
  97. package/template/coding-task-manager/lib/create/resolve-launch-params.js +96 -12
  98. package/template/coding-task-manager/lib/create/worker-role-profile.js +4 -4
  99. package/template/coding-task-manager/lib/create-intent-lease.js +51 -0
  100. package/template/coding-task-manager/lib/ctm-session-context-api.js +97 -6
  101. package/template/coding-task-manager/lib/db-domain-tables.js +1 -0
  102. package/template/coding-task-manager/lib/db-domains.js +1 -0
  103. package/template/coding-task-manager/lib/db-owner-dispatch-probe.js +32 -0
  104. package/template/coding-task-manager/lib/db-owner-request-counts.js +178 -0
  105. package/template/coding-task-manager/lib/db-owner-worker-client.js +68 -14
  106. package/template/coding-task-manager/lib/db-schema-source-files.js +5 -0
  107. package/template/coding-task-manager/lib/deferred-restore-attempt.js +6 -2
  108. package/template/coding-task-manager/lib/feature-manifest.js +4 -0
  109. package/template/coding-task-manager/lib/fleet-conversation-route.js +97 -0
  110. package/template/coding-task-manager/lib/fleet-fanout.js +78 -0
  111. package/template/coding-task-manager/lib/fleet-hub.js +762 -0
  112. package/template/coding-task-manager/lib/fleet-origin-follower.js +252 -0
  113. package/template/coding-task-manager/lib/fleet-resolver-effects.js +147 -0
  114. package/template/coding-task-manager/lib/fleet-resolver-tick.js +148 -0
  115. package/template/coding-task-manager/lib/fleet-resolver.js +454 -0
  116. package/template/coding-task-manager/lib/fleet-session-list-routes.js +114 -0
  117. package/template/coding-task-manager/lib/fleet-session-lists.js +672 -0
  118. package/template/coding-task-manager/lib/fleet-update.js +798 -0
  119. package/template/coding-task-manager/lib/fs-watcher-root-watch.js +141 -0
  120. package/template/coding-task-manager/lib/fs-watcher.js +58 -42
  121. package/template/coding-task-manager/lib/harness-history-transition.js +50 -9
  122. package/template/coding-task-manager/lib/headless-term-service.js +84 -8
  123. package/template/coding-task-manager/lib/hermetic-agent-launch-guard.js +10 -0
  124. package/template/coding-task-manager/lib/history-transition-read-op.js +175 -8
  125. package/template/coding-task-manager/lib/host-cpu-categories.js +310 -0
  126. package/template/coding-task-manager/lib/host-fseventsd-sampler.js +399 -0
  127. package/template/coding-task-manager/lib/host-handoff.js +16 -19
  128. package/template/coding-task-manager/lib/host-stats.js +150 -3
  129. package/template/coding-task-manager/lib/hosted-sync-worker.js +43 -320
  130. package/template/coding-task-manager/lib/hub-bridge-access.js +798 -0
  131. package/template/coding-task-manager/lib/hub-bridge-box.js +409 -0
  132. package/template/coding-task-manager/lib/hub-bridge-mcp.js +380 -0
  133. package/template/coding-task-manager/lib/hub-bridge-mesh.js +313 -0
  134. package/template/coding-task-manager/lib/hub-bridge-ops.js +721 -0
  135. package/template/coding-task-manager/lib/hub-bridge.js +398 -0
  136. package/template/coding-task-manager/lib/idle-notify.js +491 -0
  137. package/template/coding-task-manager/lib/instance-id-ownership-manifest.js +8 -0
  138. package/template/coding-task-manager/lib/instance-manifest.js +5 -1
  139. package/template/coding-task-manager/lib/instance-sync-status.js +18 -8
  140. package/template/coding-task-manager/lib/landing/agent-rule.txt +1 -0
  141. package/template/coding-task-manager/lib/landing/batch-runner.js +366 -0
  142. package/template/coding-task-manager/lib/landing/config.js +536 -0
  143. package/template/coding-task-manager/lib/landing/ctm-view.js +256 -0
  144. package/template/coding-task-manager/lib/landing/gate.js +360 -0
  145. package/template/coding-task-manager/lib/landing/git.js +344 -0
  146. package/template/coding-task-manager/lib/landing/lease.js +247 -0
  147. package/template/coding-task-manager/lib/landing/merge-delta.js +380 -0
  148. package/template/coding-task-manager/lib/landing/proof.js +223 -0
  149. package/template/coding-task-manager/lib/landing/queue.js +998 -0
  150. package/template/coding-task-manager/lib/landing/radar.js +489 -0
  151. package/template/coding-task-manager/lib/landing/resolve-cli.js +424 -0
  152. package/template/coding-task-manager/lib/landing/state.js +263 -0
  153. package/template/coding-task-manager/lib/landing/stop.js +49 -0
  154. package/template/coding-task-manager/lib/landing/sync-cli.js +475 -0
  155. package/template/coding-task-manager/lib/landing/sync.js +456 -0
  156. package/template/coding-task-manager/lib/landing/train.js +885 -0
  157. package/template/coding-task-manager/lib/landing/window.js +57 -0
  158. package/template/coding-task-manager/lib/launch-environment.js +121 -0
  159. package/template/coding-task-manager/lib/launch-model-override.js +36 -10
  160. package/template/coding-task-manager/lib/launch-plan-pool.js +2 -1
  161. package/template/coding-task-manager/lib/launch-plan-warm-cache.js +6 -0
  162. package/template/coding-task-manager/lib/lead-model-observer.js +57 -1
  163. package/template/coding-task-manager/lib/lead-model-options.js +160 -3
  164. package/template/coding-task-manager/lib/lead-model-restore-reconciliation.js +106 -24
  165. package/template/coding-task-manager/lib/lead-model-switch-runtime.js +352 -68
  166. package/template/coding-task-manager/lib/lead-model-switch.js +114 -10
  167. package/template/coding-task-manager/lib/lead-model-transition-runtime.js +14 -2
  168. package/template/coding-task-manager/lib/lead-model-transition-store.js +24 -6
  169. package/template/coding-task-manager/lib/lead-route-preflight.js +179 -0
  170. package/template/coding-task-manager/lib/lead-switch-history-mode.js +110 -0
  171. package/template/coding-task-manager/lib/lead-wire-agent.js +7 -0
  172. package/template/coding-task-manager/lib/lifecycle-wake-experiment.js +9 -2
  173. package/template/coding-task-manager/lib/live-defer-announce.js +45 -0
  174. package/template/coding-task-manager/lib/machine-registry.js +750 -5
  175. package/template/coding-task-manager/lib/macos-capabilities.js +32 -15
  176. package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +433 -53
  177. package/template/coding-task-manager/lib/mainloop-observability.js +14 -9
  178. package/template/coding-task-manager/lib/media-draft-owner.js +16 -3
  179. package/template/coding-task-manager/lib/memory-join.js +337 -0
  180. package/template/coding-task-manager/lib/memory-sync-verify.js +393 -0
  181. package/template/coding-task-manager/lib/memory-sync.js +565 -0
  182. package/template/coding-task-manager/lib/migrations/v57-delegate-pref-mirror-opencode.js +68 -0
  183. package/template/coding-task-manager/lib/migrations/v58-subagent-launch-native-subagents.js +42 -0
  184. package/template/coding-task-manager/lib/migrations/v59-coding-session-model-state-dsh.js +73 -0
  185. package/template/coding-task-manager/lib/migrations/v60-delegate-pref-mirror-dsh.js +78 -0
  186. package/template/coding-task-manager/lib/migrations/v61-conversation-item-clock.js +71 -0
  187. package/template/coding-task-manager/lib/model-availability.js +727 -0
  188. package/template/coding-task-manager/lib/model-execution-recipe-metadata.js +4 -6
  189. package/template/coding-task-manager/lib/model-host-receipt.js +6 -3
  190. package/template/coding-task-manager/lib/model-id-rule.js +19 -0
  191. package/template/coding-task-manager/lib/model-proxy/anthropic-face.js +4 -0
  192. package/template/coding-task-manager/lib/model-proxy/auto-mode-review.js +110 -23
  193. package/template/coding-task-manager/lib/model-proxy/chat-response-translators.js +3 -0
  194. package/template/coding-task-manager/lib/model-proxy/generation-probe.js +60 -17
  195. package/template/coding-task-manager/lib/model-proxy/historical-tool-declarations.js +37 -0
  196. package/template/coding-task-manager/lib/model-proxy/lead-request-validation.js +1 -0
  197. package/template/coding-task-manager/lib/model-proxy/lead-token-admin.js +130 -0
  198. package/template/coding-task-manager/lib/model-proxy/refusal-scope.js +151 -0
  199. package/template/coding-task-manager/lib/model-proxy/request-budget-store.js +11 -2
  200. package/template/coding-task-manager/lib/model-proxy/tokens.js +69 -5
  201. package/template/coding-task-manager/lib/model-proxy/upstream-outcome.js +9 -0
  202. package/template/coding-task-manager/lib/native-agent-model-args.js +25 -22
  203. package/template/coding-task-manager/lib/native-history-control.js +6 -4
  204. package/template/coding-task-manager/lib/native-lead-settings-client.js +44 -4
  205. package/template/coding-task-manager/lib/oauth-proxy-supervisor.js +173 -36
  206. package/template/coding-task-manager/lib/online-backup-worker-client.js +2 -0
  207. package/template/coding-task-manager/lib/opencode-cli.js +480 -0
  208. package/template/coding-task-manager/lib/opencode-launch-model.js +471 -0
  209. package/template/coding-task-manager/lib/opencode-v1-plugin/model-carrier.mjs +35 -0
  210. package/template/coding-task-manager/lib/opencode-v2-plugin/server.js +118 -0
  211. package/template/coding-task-manager/lib/owner-action-manifest-data.json +1 -1
  212. package/template/coding-task-manager/lib/park-composer-draft.js +54 -0
  213. package/template/coding-task-manager/lib/park-fresh-relaunch.js +41 -10
  214. package/template/coding-task-manager/lib/perf-phase-trace.js +72 -0
  215. package/template/coding-task-manager/lib/perf-summary.js +7 -0
  216. package/template/coding-task-manager/lib/perf-tracker.js +63 -2
  217. package/template/coding-task-manager/lib/project-config.js +7 -0
  218. package/template/coding-task-manager/lib/prompt-purge-v2.js +52 -1
  219. package/template/coding-task-manager/lib/provider-login-broker.js +72 -15
  220. package/template/coding-task-manager/lib/provider-login-tab.js +10 -2
  221. package/template/coding-task-manager/lib/read-pool-client.js +89 -2
  222. package/template/coding-task-manager/lib/read-pool-worker-handle.js +29 -4
  223. package/template/coding-task-manager/lib/recent-catalog-query.js +15 -0
  224. package/template/coding-task-manager/lib/recent-session-catalog-response.js +67 -10
  225. package/template/coding-task-manager/lib/recovery-evidence.js +26 -11
  226. package/template/coding-task-manager/lib/relay-client.js +57 -3
  227. package/template/coding-task-manager/lib/relink-audit.js +34 -5
  228. package/template/coding-task-manager/lib/remote-machine-catalog.js +321 -26
  229. package/template/coding-task-manager/lib/remote-machine-create.js +576 -36
  230. package/template/coding-task-manager/lib/remote-machine-link.js +546 -59
  231. package/template/coding-task-manager/lib/remote-machine-proxy.js +143 -9
  232. package/template/coding-task-manager/lib/remote-session-forward.js +65 -7
  233. package/template/coding-task-manager/lib/remote-terminal-target.js +9 -2
  234. package/template/coding-task-manager/lib/restart-guard.js +18 -6
  235. package/template/coding-task-manager/lib/restore-seed-modes.js +87 -0
  236. package/template/coding-task-manager/lib/restored-scrollback-hydration-runtime.js +22 -2
  237. package/template/coding-task-manager/lib/resume-admission.js +17 -3
  238. package/template/coding-task-manager/lib/review-repo-registry.js +58 -19
  239. package/template/coding-task-manager/lib/root-delivery-runtime.js +2 -2
  240. package/template/coding-task-manager/lib/root-prompt-flow.js +1 -1
  241. package/template/coding-task-manager/lib/runtime-contract.js +3 -1
  242. package/template/coding-task-manager/lib/schema-indexes.js +1 -0
  243. package/template/coding-task-manager/lib/session-context-locator.js +742 -0
  244. package/template/coding-task-manager/lib/session-create/attachment.js +2 -2
  245. package/template/coding-task-manager/lib/session-create/create-phases.js +118 -0
  246. package/template/coding-task-manager/lib/session-create/delegate-worker.js +86 -16
  247. package/template/coding-task-manager/lib/session-create/dsh-session.js +930 -0
  248. package/template/coding-task-manager/lib/session-create/lead-route-launch.js +186 -13
  249. package/template/coding-task-manager/lib/session-create/native-lead-lifetime.js +55 -3
  250. package/template/coding-task-manager/lib/session-create/opencode-lead-provider.js +50 -0
  251. package/template/coding-task-manager/lib/session-create/prepared.js +162 -38
  252. package/template/coding-task-manager/lib/session-create/provider-process.js +160 -28
  253. package/template/coding-task-manager/lib/session-create/response.js +7 -7
  254. package/template/coding-task-manager/lib/session-create/structured-session.js +27 -0
  255. package/template/coding-task-manager/lib/session-create/subagent-pick-table.js +3 -1
  256. package/template/coding-task-manager/lib/session-create/subagent-pick.js +458 -9
  257. package/template/coding-task-manager/lib/session-create/transcript.js +1 -1
  258. package/template/coding-task-manager/lib/session-create/worker-budget-setup.js +21 -2
  259. package/template/coding-task-manager/lib/session-create/worker-continuation.js +7 -1
  260. package/template/coding-task-manager/lib/session-create/worker-launch-sandbox.js +39 -10
  261. package/template/coding-task-manager/lib/session-create/worker-observability.js +2 -2
  262. package/template/coding-task-manager/lib/session-empty.js +13 -10
  263. package/template/coding-task-manager/lib/session-host-manager.js +60 -0
  264. package/template/coding-task-manager/lib/session-jobs.js +3 -1
  265. package/template/coding-task-manager/lib/session-lifecycle.js +10 -4
  266. package/template/coding-task-manager/lib/session-model-action-preconditions.js +6 -2
  267. package/template/coding-task-manager/lib/session-model-intent-contract.js +11 -8
  268. package/template/coding-task-manager/lib/session-park.js +738 -0
  269. package/template/coding-task-manager/lib/session-path-grant.js +13 -4
  270. package/template/coding-task-manager/lib/session-status-ledger.js +106 -4
  271. package/template/coding-task-manager/lib/session-transport.js +17 -0
  272. package/template/coding-task-manager/lib/session-workspace-projection.js +54 -5
  273. package/template/coding-task-manager/lib/session-workspace-tracking-runtime.js +213 -1
  274. package/template/coding-task-manager/lib/skill-autocomplete-query.js +120 -3
  275. package/template/coding-task-manager/lib/skill-autocomplete.js +15 -1
  276. package/template/coding-task-manager/lib/sqlite-file-set.js +39 -0
  277. package/template/coding-task-manager/lib/standup-search-adapter.js +16 -2
  278. package/template/coding-task-manager/lib/state-sync/client-ledger.js +12 -0
  279. package/template/coding-task-manager/lib/state-sync/frame-emitter.js +319 -100
  280. package/template/coding-task-manager/lib/storage/coding-session-model-store.js +4 -1
  281. package/template/coding-task-manager/lib/storage/delegate-pref-mirror-store.js +32 -9
  282. package/template/coding-task-manager/lib/structured-session-boot-sweep.js +235 -0
  283. package/template/coding-task-manager/lib/submission-ack.js +90 -2
  284. package/template/coding-task-manager/lib/systemd-scope.js +115 -0
  285. package/template/coding-task-manager/lib/telemetry-receiver.js +39 -0
  286. package/template/coding-task-manager/lib/terminal-attach-geometry.js +985 -0
  287. package/template/coding-task-manager/lib/terminal-bootstrap.js +30 -0
  288. package/template/coding-task-manager/lib/terminal-draft-operation.js +357 -0
  289. package/template/coding-task-manager/lib/terminal-input-admission.js +17 -1
  290. package/template/coding-task-manager/lib/terminal-input-prepared.js +563 -0
  291. package/template/coding-task-manager/lib/transcript-cache-temps.js +171 -0
  292. package/template/coding-task-manager/lib/transcript-frame-cache.js +8 -3
  293. package/template/coding-task-manager/lib/transcript-source.js +36 -4
  294. package/template/coding-task-manager/lib/transcript-store.js +9 -0
  295. package/template/coding-task-manager/lib/typing-interaction-trace.js +106 -3
  296. package/template/coding-task-manager/lib/usage-ledger/backfill.js +40 -5
  297. package/template/coding-task-manager/lib/usage-ledger/client.js +4 -1
  298. package/template/coding-task-manager/lib/usage-ledger/dsh-parse.js +152 -0
  299. package/template/coding-task-manager/lib/usage-ledger/dsh-read.js +73 -0
  300. package/template/coding-task-manager/lib/usage-ledger/ingest.js +34 -6
  301. package/template/coding-task-manager/lib/usage-ledger/live.js +35 -8
  302. package/template/coding-task-manager/lib/usage-ledger/read.js +29 -20
  303. package/template/coding-task-manager/lib/usage-ledger/schema.js +61 -1
  304. package/template/coding-task-manager/lib/usage-ledger/task-coordinator.js +4 -2
  305. package/template/coding-task-manager/lib/wake-skip-reasons.js +1 -0
  306. package/template/coding-task-manager/lib/walle/turn.js +1 -1
  307. package/template/coding-task-manager/lib/walle-history-serve.js +838 -0
  308. package/template/coding-task-manager/lib/walle-kill-authority.js +2 -2
  309. package/template/coding-task-manager/lib/walle-log-rotation.js +164 -1
  310. package/template/coding-task-manager/lib/walle-mcp-auto-config.js +56 -2
  311. package/template/coding-task-manager/lib/walle-row-history.js +495 -0
  312. package/template/coding-task-manager/lib/walle-supervisor.js +33 -14
  313. package/template/coding-task-manager/lib/watch-worker-client.js +203 -0
  314. package/template/coding-task-manager/lib/worker-terminal-output.js +5 -2
  315. package/template/coding-task-manager/lib/ws/hub-bridge.js +35 -0
  316. package/template/coding-task-manager/lib/ws/lifecycle.js +6 -11
  317. package/template/coding-task-manager/lib/ws/misc.js +1 -1
  318. package/template/coding-task-manager/lib/ws/state-sync.js +106 -4
  319. package/template/coding-task-manager/lib/ws/terminal-attach.js +91 -0
  320. package/template/coding-task-manager/oauth-proxy.js +112 -115
  321. package/template/coding-task-manager/package-lock.json +10 -94
  322. package/template/coding-task-manager/package.json +4 -3
  323. package/template/coding-task-manager/protocol/message-table.json +10 -0
  324. package/template/coding-task-manager/protocol/messages.js +10 -0
  325. package/template/coding-task-manager/protocol/ws-schema.json +20 -0
  326. package/template/coding-task-manager/protocol/ws-schema.md +2 -0
  327. package/template/coding-task-manager/public/css/app.css +170 -0
  328. package/template/coding-task-manager/public/css/walle-session.css +4 -0
  329. package/template/coding-task-manager/public/index.html +473 -243
  330. package/template/coding-task-manager/public/js/agent-command.js +86 -0
  331. package/template/coding-task-manager/public/js/app/backups-panel.js +111 -5
  332. package/template/coding-task-manager/public/js/app/coding-agent-upgrade.js +103 -4
  333. package/template/coding-task-manager/public/js/app/machines-page.js +1328 -8
  334. package/template/coding-task-manager/public/js/app/models-tab.js +113 -38
  335. package/template/coding-task-manager/public/js/app/restart-banner.js +53 -0
  336. package/template/coding-task-manager/public/js/app/worktrees-panel.js +184 -2
  337. package/template/coding-task-manager/public/js/background-work.js +196 -27
  338. package/template/coding-task-manager/public/js/coding-session-model-presentation.js +138 -10
  339. package/template/coding-task-manager/public/js/core/active-session-groups.js +179 -0
  340. package/template/coding-task-manager/public/js/core/pending-create.js +238 -0
  341. package/template/coding-task-manager/public/js/core/recent-machine-source.js +228 -0
  342. package/template/coding-task-manager/public/js/core/session-commands.js +180 -14
  343. package/template/coding-task-manager/public/js/dashboard-client-function-order.json +8 -0
  344. package/template/coding-task-manager/public/js/document-review-links.js +9 -1
  345. package/template/coding-task-manager/public/js/file-resolution-client.js +52 -1
  346. package/template/coding-task-manager/public/js/history-app-seam.js +22 -2
  347. package/template/coding-task-manager/public/js/machine-pool-shared.js +558 -3
  348. package/template/coding-task-manager/public/js/message-renderer.js +4 -3
  349. package/template/coding-task-manager/public/js/model-picker.js +3 -1
  350. package/template/coding-task-manager/public/js/native-subagent-review.js +15 -2
  351. package/template/coding-task-manager/public/js/new-session-agent-grid.js +346 -0
  352. package/template/coding-task-manager/public/js/pasted-content.js +236 -0
  353. package/template/coding-task-manager/public/js/pe-private-content-lifetime.js +39 -7
  354. package/template/coding-task-manager/public/js/private-model-lifetime.js +6 -2
  355. package/template/coding-task-manager/public/js/prompts.js +11 -10
  356. package/template/coding-task-manager/public/js/protocol.js +10 -0
  357. package/template/coding-task-manager/public/js/restart-banner-policy.js +29 -1
  358. package/template/coding-task-manager/public/js/session-model-picker.js +447 -57
  359. package/template/coding-task-manager/public/js/session-search-utils.js +18 -2
  360. package/template/coding-task-manager/public/js/session-usage-view.js +2 -1
  361. package/template/coding-task-manager/public/js/session-workspace-tracking.js +51 -1
  362. package/template/coding-task-manager/public/js/state-sync-client.js +10 -1
  363. package/template/coding-task-manager/public/js/stream-view.js +6 -5
  364. package/template/coding-task-manager/public/js/terminal/conv-only-session.js +287 -0
  365. package/template/coding-task-manager/public/js/terminal/runtime-09.js +2 -0
  366. package/template/coding-task-manager/public/js/terminal/runtime-10.js +5 -5
  367. package/template/coding-task-manager/public/js/terminal/runtime-12.js +8 -1
  368. package/template/coding-task-manager/public/js/terminal/runtime-16.js +4 -0
  369. package/template/coding-task-manager/public/js/terminal/runtime-20.js +1 -1
  370. package/template/coding-task-manager/public/js/terminal-attachment-draft.js +67 -13
  371. package/template/coding-task-manager/public/js/terminal-image-orchestration.js +52 -6
  372. package/template/coding-task-manager/public/js/terminal-wheel.js +23 -3
  373. package/template/coding-task-manager/public/js/walle/chat-conversation.js +4 -4
  374. package/template/coding-task-manager/public/js/walle.js +6 -0
  375. package/template/coding-task-manager/public/js/ws/live-dispatch.js +10 -0
  376. package/template/coding-task-manager/public/m/app.css +140 -0
  377. package/template/coding-task-manager/public/m/app.js +507 -68
  378. package/template/coding-task-manager/public/m/index.html +17 -0
  379. package/template/coding-task-manager/public/m/machines.js +838 -4
  380. package/template/coding-task-manager/public/m/private-api.js +31 -1
  381. package/template/coding-task-manager/public/usage.html +1 -0
  382. package/template/coding-task-manager/scripts/dev-route-fixture/synthetic-route-upstream.js +104 -0
  383. package/template/coding-task-manager/scripts/dev-route-fixture.js +339 -0
  384. package/template/coding-task-manager/scripts/opencode-lead-route-probe.js +195 -0
  385. package/template/coding-task-manager/scripts/opencode-v2-contract-probe/fake-anthropic.js +168 -0
  386. package/template/coding-task-manager/scripts/opencode-v2-contract-probe/probe-plugin/server.js +95 -0
  387. package/template/coding-task-manager/scripts/opencode-v2-contract-probe.js +673 -0
  388. package/template/coding-task-manager/server.js +1950 -11679
  389. package/template/coding-task-manager/session-integrity.js +64 -4
  390. package/template/coding-task-manager/workers/db-owner-worker.js +19 -2
  391. package/template/coding-task-manager/workers/online-backup-worker.js +1 -0
  392. package/template/coding-task-manager/workers/read-pool-model-collector.js +51 -3
  393. package/template/coding-task-manager/workers/read-pool-worker.js +76 -6
  394. package/template/coding-task-manager/workers/session-host-pool-process.js +6 -6
  395. package/template/coding-task-manager/workers/session-host-process.js +5 -5
  396. package/template/coding-task-manager/workers/watch-worker.js +105 -0
  397. package/template/coding-task-manager/workers/worktree-worker.js +168 -33
  398. package/template/ctm-relay/lib/protocol.js +14 -1
  399. package/template/package-lock.json +2 -2
  400. package/template/package.json +1 -1
  401. package/template/shared/backup-retention.js +82 -0
  402. package/template/shared/conversation-admission.js +2 -0
  403. package/template/shared/conversation-provenance.js +3 -7
  404. package/template/shared/conversation-storage-v2-contract.js +5 -0
  405. package/template/shared/linked-worktree.js +51 -0
  406. package/template/shared/macos-fda-target.js +57 -0
  407. package/template/shared/orphan-parents.js +55 -0
  408. package/template/shared/release-schema.json +2 -2
  409. package/template/shared/storage/contract.js +3 -0
  410. package/template/wall-e/adapters/imessage.js +12 -0
  411. package/template/wall-e/agent.js +71 -4
  412. package/template/wall-e/api-walle.js +21 -1
  413. package/template/wall-e/bin/train-gemma-e4b-tooluse.js +2 -2
  414. package/template/wall-e/bin/walle-mcp-stdio.js +5 -5
  415. package/template/wall-e/brain/brain-join.js +859 -0
  416. package/template/wall-e/brain/brain-sync-membership.js +20 -0
  417. package/template/wall-e/brain/brain-sync-store.js +131 -77
  418. package/template/wall-e/brain/brain-sync-v2-schema.js +203 -0
  419. package/template/wall-e/brain/brain-sync-v2.js +578 -0
  420. package/template/wall-e/brain/delegate-model-prefs-store.js +13 -4
  421. package/template/wall-e/brain/deterministic-id.js +51 -0
  422. package/template/wall-e/brain/memories.js +2 -2
  423. package/template/wall-e/brain/migrations.js +40 -6
  424. package/template/wall-e/brain/model-identity-migration.js +1 -1
  425. package/template/wall-e/brain/owner-call-whitelist.js +23 -7
  426. package/template/wall-e/brain/schema.js +45 -85
  427. package/template/wall-e/brain.js +171 -35
  428. package/template/wall-e/chat.js +3 -3
  429. package/template/wall-e/coding/agent-shell-env.js +7 -2
  430. package/template/wall-e/coding/prompt-bundle.js +66 -21
  431. package/template/wall-e/config/main-loop-exemptions.json +2 -2
  432. package/template/wall-e/http/admin-api.js +37 -0
  433. package/template/wall-e/http/gws-api.js +4 -0
  434. package/template/wall-e/http/helper-rules-api.js +7 -1
  435. package/template/wall-e/http/model-admin.js +8 -1
  436. package/template/wall-e/http/model-delegate-pref-admin.js +3 -3
  437. package/template/wall-e/http/model-proxy-providers.js +174 -1
  438. package/template/wall-e/http/model-route-health.js +157 -0
  439. package/template/wall-e/http/sync-api.js +278 -464
  440. package/template/wall-e/jobs/job-runner.js +28 -1
  441. package/template/wall-e/lib/brain-owner-op-priority.js +9 -1
  442. package/template/wall-e/lib/ctm-data-dir.js +26 -0
  443. package/template/wall-e/lib/derivation-home.js +147 -0
  444. package/template/wall-e/lib/helper-rules/evaluate.js +50 -9
  445. package/template/wall-e/lib/helper-rules/grade.js +51 -8
  446. package/template/wall-e/lib/helper-rules/lead-call-hook.js +38 -5
  447. package/template/wall-e/lib/helper-rules/store.js +4 -2
  448. package/template/wall-e/lib/landing-mcp-tools.js +170 -0
  449. package/template/wall-e/lib/mcp-client-config.js +133 -14
  450. package/template/wall-e/lib/mcp-gateway-migrate.js +28 -64
  451. package/template/wall-e/lib/mcp-integration.js +58 -13
  452. package/template/wall-e/lib/parent-death.js +7 -1
  453. package/template/wall-e/lib/scheduler-worker-jobs.js +6 -4
  454. package/template/wall-e/llm/delegate-lead-prefs.js +16 -1
  455. package/template/wall-e/llm/delegate-vendor-default.js +12 -5
  456. package/template/wall-e/llm/model-roles.js +1 -1
  457. package/template/wall-e/llm/native-delegate-selection.js +56 -8
  458. package/template/wall-e/llm/worker-delegate-args.js +72 -7
  459. package/template/wall-e/llm/worker-mcp-routing.js +66 -17
  460. package/template/wall-e/loops/brain-custom-writes.js +19 -0
  461. package/template/wall-e/loops/ingest.js +17 -1
  462. package/template/wall-e/loops/think.js +19 -4
  463. package/template/wall-e/mcp-contract.js +222 -6
  464. package/template/wall-e/mcp-protocol-contract.js +40 -5
  465. package/template/wall-e/mcp-server.js +128 -29
  466. package/template/wall-e/mcp-worker-schemas.js +5 -5
  467. package/template/wall-e/memory/ctm-context-client.js +85 -0
  468. package/template/wall-e/memory/ctm-session-context.js +9 -3
  469. package/template/wall-e/memory/hub-bridge-client.js +388 -0
  470. package/template/wall-e/memory/replica-origin.js +56 -0
  471. package/template/wall-e/memory/session-pool.js +135 -0
  472. package/template/wall-e/runtime/live-capabilities.js +8 -3
  473. package/template/wall-e/server.js +4 -2
  474. package/template/wall-e/shared/release-schema.json +2 -2
  475. package/template/wall-e/shared/sqlite-file-set.js +39 -0
  476. package/template/wall-e/shared/storage/contract.js +3 -0
  477. package/template/wall-e/skills/_templates/delegate-worker/VERSION +1 -1
  478. package/template/wall-e/skills/_templates/delegate-worker/released-sha256.json +7 -1
  479. package/template/wall-e/skills/_templates/delegate-worker/templates/claude/SKILL.md +4 -4
  480. package/template/wall-e/skills/_templates/delegate-worker/templates/codex/SKILL.md +6 -6
  481. package/template/wall-e/skills/_templates/fleet-sync/VERSION +1 -0
  482. package/template/wall-e/skills/_templates/fleet-sync/released-sha256.json +16 -0
  483. package/template/wall-e/skills/_templates/fleet-sync/templates/claude/SKILL.md +64 -0
  484. package/template/wall-e/skills/_templates/fleet-sync/templates/codex/SKILL.md +64 -0
  485. package/template/wall-e/tools/chrome-automation.js +6 -0
  486. package/template/wall-e/training/setup-cuda-train-env.sh +2 -2
  487. package/template/wall-e/workers/brain-owner-worker.js +18 -4
  488. package/template/wall-e/workers/mcp-gateway-process.js +5 -5
@@ -0,0 +1,985 @@
1
+ 'use strict';
2
+
3
+ // Landing-throughput WP-5 shard S6 (design doc §5 D-10 "move-only", §6 "Shard
4
+ // module seam", §8 WP-6, §9 S-21..S-27): the WebSocket attach / geometry-handler
5
+ // cluster (handleAttach, handleTerminalBootstrap, handleResize, handleReflow) moves
6
+ // out of server.js VERBATIM into this module behind the same configure(deps) seam as
7
+ // lib/api/session-messages.js (S3) and lib/api/diagnostics.js (S4). This module
8
+ // registers NO ws handlers -- dispatch stays in lib/ws/terminal-attach.js, whose
9
+ // configure() call in server.js now receives attachGeometry.handleAttach /
10
+ // attachGeometry.handleTerminalBootstrap / attachGeometry.handleResize /
11
+ // attachGeometry.handleReflow. Each moved body is identical to server.js's
12
+ // declaration except for the `_deps.` prefix on every identifier that used to resolve
13
+ // to a server.js top-level binding (D-10 rule 1), proven by
14
+ // `node bin/move-only-check.js`.
15
+ //
16
+ // Injected deps, per function:
17
+ // handleAttach: CTM_FLOW_CONTROL_ENABLED, CTM_INTERACTIVE_SERIALIZE_TIMEOUT_MS,
18
+ // MIRROR_BACKGROUND_ROWS, _applyActivationArchiveTopup, _cacheSerializedSnapshot,
19
+ // _coalesceSnapshotSynchronizedFrames, _codexRolloutHistoryActive, _composeCodexRolloutResp,
20
+ // _ensureScrollbackTailCache, _flowPruneStalePausers, _hydrateRuntimeGoalProjectionForSocket,
21
+ // _lifecycleClientKind, _markCtmUiRefreshActivitySuppression, _maybeAutoStartDeferredRestore,
22
+ // _noteInteractiveSerializeDone, _noteInteractiveSerializeStart,
23
+ // _prefetchActivationArchiveTopup, _recordClientAttachDiagnostic, _replayRestoreWakeVerdict,
24
+ // _reportWorkerIpc, _resolveRestoredHeadlessRequestGrid, _restoreFailureWirePayload,
25
+ // _restoreMirrorToPtyGridAfterSerialize, _resumeLifecycleWireState,
26
+ // _sendRuntimeWorkStateToSocket, _serveCachedSnapshotOnSerializeTimeout, _snapshotOutputMarker,
27
+ // _startDeferredRestoreSession, _subscribeStateSyncClient, _wakeBlockedNoticeOwnsUi,
28
+ // _wsClientAsset, _wsSend, cachedSnapshotMatchesAuthority, headlessWorker,
29
+ // normalizeSnapshotScrollbackRows, recordSessionDiagnostic, sendCachedSnapshot,
30
+ // sendDeferredRestoreStatus, sessionEvents, sessions
31
+ // handleTerminalBootstrap: TERMINAL_BOOTSTRAP_ABSOLUTE_LIFETIME_MS,
32
+ // _armTerminalBootstrapGateDeadline, _clearTerminalBootstrapGate,
33
+ // _detachTerminalBootstrapSocket, _impliedView, _maybeAutoStartDeferredRestore,
34
+ // _postWorkerClientDetached, _recordClientAttachDiagnostic, _replayRestoreWakeVerdict,
35
+ // _restoreWindowPending, _retainRestoreAwaitingSocket, _sendTerminalBootstrapFailed,
36
+ // _sessionHasViewer, _subscribeStateSyncClient, _terminalBootstrapGatesFor,
37
+ // _terminalProcessGeneration, _wsSend, composeTerminalBootstrapResult, headlessWorker,
38
+ // isSubscribed, isWalleLikeSession, normalizeSnapshotScrollbackRows,
39
+ // parseTerminalBootstrapRequest, sendDeferredRestoreStatus, sessions, truncateBootstrapAnsi
40
+ // handleResize: CTM_FRESH_PROCESS_ADOPT_ENABLED, CTM_FRESH_PROCESS_ADOPT_MS, CTM_GRID_PROVENANCE,
41
+ // GeometryClaimValidationError, _applyPrimaryPtyResize, _broadcastViewerRoles,
42
+ // _claimGeometryFocus, _clearRowsFlapDebounce, _deferPrimaryPtyResizeIfStreaming,
43
+ // _geometryLease, _markCtmUiRefreshActivitySuppression, _maybeDebounceRowsFlapResize,
44
+ // _providerIdFromCmd, _pushSecondarySnapshots, _recordGridProvenanceDiagnostic,
45
+ // _recordViewerDims, _resolveRestoredHeadlessRequestGrid, _sendTerminalGeometryFenceAck,
46
+ // _viewerArbitration, classifyInboundGrid, looksLikeXtermDefault, mayDrivePtyResize,
47
+ // recordSessionDiagnostic, sanitizeGeometryResize, sessions, validateGeometryFence
48
+ // handleReflow: CTM_INTERACTIVE_SERIALIZE_TIMEOUT_MS, RENDER_CHURN_FLASH_THRESHOLD,
49
+ // RENDER_CHURN_WINDOW_MS, _cacheSerializedSnapshot, _coalesceSnapshotSynchronizedFrames,
50
+ // _codexRolloutHistoryActive, _composeCodexRolloutResp, _deferPrimaryPtyResizeIfStreaming,
51
+ // _markCtmUiRefreshActivitySuppression, _maybeAutoStartDeferredRestore,
52
+ // _noteInteractiveSerializeDone, _noteInteractiveSerializeStart, _recordRenderChurn,
53
+ // _resolveRestoredHeadlessRequestGrid, _serveCachedSnapshotOnSerializeTimeout,
54
+ // _snapshotOutputMarker, _wsClientAsset, _wsSend, buildPtyResizeAppliedDetail, headlessWorker,
55
+ // normalizeSnapshotScrollbackRows, recordSessionDiagnostic, sendDeferredRestoreStatus,
56
+ // sendTerminalSessionUnavailableSnapshot, sessions
57
+ //
58
+ // None of these bindings is reassigned in server.js, so all are passed by value (no
59
+ // getters needed) and no moved body assigns server.js state.
60
+
61
+ let _deps = null;
62
+
63
+ function configure(deps = {}) {
64
+ _deps = deps;
65
+ }
66
+
67
+ async function handleAttach(ws, msg) {
68
+ let session = _deps.sessions.get(msg.id);
69
+ if (!session) {
70
+ _deps._wsSend(ws, JSON.stringify({ type: 'error', message: 'Session not found', id: msg.id }));
71
+ return;
72
+ }
73
+ const _attachScrollbackRows = _deps.normalizeSnapshotScrollbackRows(msg.scrollback);
74
+ session._snapshotScrollbackRows = _attachScrollbackRows;
75
+ _deps._markCtmUiRefreshActivitySuppression(session, 'attach');
76
+ if (!session.clients.includes(ws)) {
77
+ session.clients.push(ws);
78
+ }
79
+ _deps._recordClientAttachDiagnostic(session, msg.id, ws);
80
+ // A client attaching after the app already entered its TUI modes must learn them now (the enable
81
+ // sequences were stripped from the stream and only broadcast on change), so wheel→PTY forwarding works.
82
+ if (session._tuiModes && (session._tuiModes.altScreen || session._tuiModes.appMouse)) {
83
+ try {
84
+ _deps._wsSend(ws, JSON.stringify({
85
+ type: 'tui-mode', id: msg.id,
86
+ altScreen: session._tuiModes.altScreen, appMouse: session._tuiModes.appMouse, mouseEnc: session._tuiModes.mouseEnc,
87
+ }));
88
+ } catch {}
89
+ }
90
+ if (_deps.CTM_FLOW_CONTROL_ENABLED && session._flowPaused) _deps._flowPruneStalePausers(session, msg.id);
91
+ // Auto-attach on attach: a client explicitly asking to
92
+ // attach is asking to receive this session output immediately, without a separate
93
+ // subscribe round-trip — see tests/create-subscribe-race.test.js.
94
+ _deps._subscribeStateSyncClient(ws, msg.id, 'attach');
95
+ // Resume transitions can race ahead of a newly-created browser session and are intentionally
96
+ // multicast only to attached viewers. Re-publish the current generation after subscription so
97
+ // attach is a state barrier: a prompt/ready transition that happened between `created` and this
98
+ // request cannot remain invisible until the next session-list refresh.
99
+ const attachedResume = _deps._resumeLifecycleWireState(session);
100
+ if (attachedResume && ws.readyState === 1) {
101
+ _deps._wsSend(ws, JSON.stringify({ type: 'resume-state', id: msg.id, resume: attachedResume, transition: session._resumeLastTransition || null }));
102
+ }
103
+ _deps._replayRestoreWakeVerdict(session, msg.id, ws); // D-B2: a late-attaching tab still gets the revived verdict
104
+
105
+ if (session._restoreDeferred && (session._restoreTask?.type === 'walle' || session._restoreTask?.type === 'dsh')) { // WP-5 (S-40): attaching a restored DeepSeek Harness tab starts it through the same wake path.
106
+ // _startDeferredRestoreSession is async (Fix B Stage 4). It still rejects a
107
+ // failed spawn synchronously (returns false before any yield), so awaiting
108
+ // the promise resolves to that same boolean — we just notify the client
109
+ // when it settles instead of synchronously.
110
+ _deps._startDeferredRestoreSession(session, {
111
+ cols: msg.cols || session._ptyCols || 120,
112
+ rows: msg.rows || session._ptyRows || 30,
113
+ terminalDimsMeasured: msg.terminalDimsMeasured === true,
114
+ scrollbackRows: _attachScrollbackRows,
115
+ clientRestoreSeq: msg.clientRestoreSeq,
116
+ source: 'attach',
117
+ clientKind: _deps._lifecycleClientKind(ws, msg),
118
+ }).then((started) => {
119
+ if (started || ws.readyState !== 1) return;
120
+ // FINAL REVIEW S-2 (Important): `started === false` here is NOT one fact. Several bails
121
+ // inside _startDeferredRestoreSession(Attempt) return false with no failure emitted and
122
+ // NO server retry queued behind them (wake-capacity refusal, rollout-compaction bail,
123
+ // restore-backoff skip). Two rulings, both about not lying to the client:
124
+ // (a) the wake-capacity refusal already owns the UI via its own `session-wake-blocked`
125
+ // notice -- emit nothing at all, or the user gets two contradictory surfaces;
126
+ // (b) every other bail has no retry behind it, so the payload must say retryable:false.
127
+ // (The old code let the wire contract compute retryable from the backoff ledger,
128
+ // which reports "sure, retry later" for a loop nobody is going to run.)
129
+ if (_deps._wakeBlockedNoticeOwnsUi(session)) return;
130
+ _deps._wsSend(ws, JSON.stringify(_deps._restoreFailureWirePayload(msg.id, {
131
+ code: 'walle_restore_not_started',
132
+ message: 'Wall-E session failed to resume.',
133
+ retryable: false,
134
+ })));
135
+ }).catch((e) => console.error('[ctm] attach-triggered walle restore error:', e.message));
136
+ _deps.sessionEvents.emit('session:attached', { id: msg.id, clientCount: session.clients.length, deferred: true, walle: true });
137
+ return;
138
+ }
139
+
140
+ if (session._restoreDeferred) {
141
+ const starting = _deps._maybeAutoStartDeferredRestore(session, msg, 'attach', ws);
142
+ _deps.sendDeferredRestoreStatus(ws, session, msg.id, msg.cols, msg.rows, 'attach', {
143
+ scrollbackRows: _attachScrollbackRows,
144
+ clientRestoreSeq: msg.clientRestoreSeq,
145
+ restoreSource: 'attach-deferred-cache',
146
+ starting,
147
+ });
148
+ _deps.sessionEvents.emit('session:attached', { id: msg.id, clientCount: session.clients.length, deferred: true });
149
+ return;
150
+ }
151
+
152
+ // Wall-E sessions: replay chat history instead of PTY scrollback
153
+ if (session.type === 'walle') {
154
+ _deps._sendRuntimeWorkStateToSocket(ws, msg.id, 'attach');
155
+ _deps._hydrateRuntimeGoalProjectionForSocket(ws, msg.id, 'attach-goal-hydrated');
156
+ return;
157
+ }
158
+
159
+ // Fast path: send cached snapshot immediately if available (pre-cached on
160
+ // unsubscribe). Do NOT consume the cache — a concurrent-or-subsequent
161
+ // retry would otherwise hit the same slow worker and leave the tab blank.
162
+ // The fresh serialize below refreshes the cache when it eventually arrives.
163
+ _deps._ensureScrollbackTailCache(session);
164
+ const _attachGrid = _deps._resolveRestoredHeadlessRequestGrid(session, msg, 'attach');
165
+ const _cacheServed = _deps.sendCachedSnapshot(ws, session, msg.id, _attachGrid.cols, _attachGrid.rows, 'attach', {
166
+ allowMismatched: !!msg.allowMismatchedCache,
167
+ allowDirty: !!session._cachedSnapshotInterim,
168
+ scrollbackRows: _attachScrollbackRows,
169
+ clientRestoreSeq: msg.clientRestoreSeq,
170
+ });
171
+ const _cacheMatchesAttachDims = _cacheServed
172
+ && _deps.cachedSnapshotMatchesAuthority(session, _attachGrid.cols, _attachGrid.rows);
173
+
174
+ // Open a 2s output-budget grace window: tab-switch usually triggers a PTY
175
+ // resize → SIGWINCH → TUI redraw burst (Claude Code / Codex easily emit
176
+ // 60-100KB on a single redraw). Without grace, the burst trips the 64KB
177
+ // budget cap and triggers a budget-recovery snapshot ~2s later, which the
178
+ // user sees as a delayed re-render. Letting raw bytes through during the
179
+ // first 2s after attach means the redraw paints incrementally rather than
180
+ // being suppressed-then-snapshotted.
181
+ if (_cacheServed) session._budgetGraceUntil = Date.now() + 2000;
182
+
183
+ // Skip the fresh-serialize round-trip only when the cache is pristine and
184
+ // dimension-compatible with this attach request. A mismatched cache is sent
185
+ // as an interim restore; the client then expects a full snapshot at the
186
+ // requested dimensions to complete loading without tab-switch retries.
187
+ // The dim-matched fast path avoids the "Codex tab renders, then re-renders
188
+ // a few seconds later" complaint from redundant cache+fresh frames.
189
+ if (_cacheServed && _cacheMatchesAttachDims && session._cachedSnapshotDirty === false && session._cachedSnapshotInterim === false) {
190
+ _deps.sessionEvents.emit('session:attached', { id: msg.id, clientCount: session.clients.length });
191
+ return;
192
+ }
193
+
194
+ // Phase 3D: send snapshot from headless terminal instead of in-memory scrollback.
195
+ // The headless terminal has the authoritative state (text, colors, cursor).
196
+ // If the client sends cols/rows, resize headless terminal to match before serializing.
197
+ // Otherwise, sync headless terminal to the PTY's actual dimensions.
198
+ if (typeof session._flushHeadlessFeed === 'function') session._flushHeadlessFeed();
199
+ if (_attachGrid.cols && _attachGrid.rows) {
200
+ _deps.headlessWorker.postMessage({ type: 'resize', sessionId: msg.id, cols: _attachGrid.cols, rows: _attachGrid.rows });
201
+ } else if (session.ptyProcess) {
202
+ // Use PTY's actual cols/rows as fallback
203
+ try {
204
+ const ptyCols = session.ptyProcess.cols || 120;
205
+ const ptyRows = session.ptyProcess.rows || 30;
206
+ _deps.headlessWorker.postMessage({ type: 'resize', sessionId: msg.id, cols: ptyCols, rows: ptyRows });
207
+ } catch {}
208
+ }
209
+ const _attachWs = ws;
210
+ const _attachId = msg.id;
211
+ const _attachUseRolloutHistory = _deps._codexRolloutHistoryActive(session, _attachId);
212
+ // WP-3 fix round 1: Phase A -- bulk prefetch BEFORE the marker/serialize, OUTSIDE the stale-window.
213
+ const _topupPrefetch = (!_attachUseRolloutHistory && _attachScrollbackRows > _deps.MIRROR_BACKGROUND_ROWS)
214
+ ? await _deps._prefetchActivationArchiveTopup(_attachId, _attachScrollbackRows) : null;
215
+ const _snapReqId = `attach-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
216
+ const _attachOutputMarker = _deps._snapshotOutputMarker(session);
217
+ const _attachTimer = setTimeout(() => {
218
+ _deps.headlessWorker.off('message', _onSnap);
219
+ _deps._noteInteractiveSerializeDone(_snapReqId);
220
+ console.warn(`[attach] Headless worker timed out for session ${_attachId.slice(0, 8)}`);
221
+ _deps.recordSessionDiagnostic(_attachId, 'attach-snapshot-timeout', { requestId: _snapReqId, cols: _attachGrid.cols || 0, rows: _attachGrid.rows || 0 });
222
+ // Prefer the last good cached frame over a blank/data:null + client retry that would
223
+ // re-contend the busy worker (same fallback as the reflow path).
224
+ const _attachSess = _deps.sessions.get(_attachId);
225
+ if (_deps._serveCachedSnapshotOnSerializeTimeout(_attachWs, _attachSess, _attachId, _attachGrid.cols, _attachGrid.rows, 'attach', msg.clientRestoreSeq)) return;
226
+ // Notify client so it can trigger recovery instead of staying blank
227
+ if (_attachWs.readyState === 1) _deps._wsSend(_attachWs, JSON.stringify({ type: 'snapshot', id: _attachId, data: null, timedOut: true, clientRestoreSeq: msg.clientRestoreSeq, restoreSource: 'attach' }));
228
+ }, _deps.CTM_INTERACTIVE_SERIALIZE_TIMEOUT_MS);
229
+ const _onSnap = (resp) => { _onSnapAsync(resp).catch((error) => _deps._reportWorkerIpc('attach-snapshot', error, _attachId)); }; // WP-3: async body (Phase B); a rejection must never reach unhandledRejection (_fatalShutdown)
230
+ const _onSnapAsync = async (resp) => {
231
+ if (resp.type !== 'serialized' || resp.requestId !== _snapReqId) return;
232
+ clearTimeout(_attachTimer);
233
+ _deps._noteInteractiveSerializeDone(_snapReqId);
234
+ _deps.headlessWorker.off('message', _onSnap);
235
+ // Update cache for next time
236
+ const s = _deps.sessions.get(_attachId);
237
+ if (_attachUseRolloutHistory && s && resp.data) {
238
+ resp = _deps._composeCodexRolloutResp(s, _attachId, resp);
239
+ } else if (s && resp.data) {
240
+ // WP-3 fix round 1: Phase B (bulk read already ran as `_topupPrefetch` above).
241
+ resp = await _deps._applyActivationArchiveTopup(_attachId, _attachScrollbackRows, resp, _topupPrefetch);
242
+ }
243
+ let staleSnapshot = false;
244
+ let staleMarker; let staleCurrent;
245
+ if (s) {
246
+ const stored = _deps._cacheSerializedSnapshot(s, resp, _attachOutputMarker);
247
+ staleSnapshot = !!stored.stale;
248
+ if (staleSnapshot) {
249
+ staleMarker = stored.marker; staleCurrent = stored.current;
250
+ _deps.recordSessionDiagnostic(_attachId, 'attach-stale-output-after-request', {
251
+ requestId: _snapReqId,
252
+ marker: stored.marker,
253
+ current: stored.current,
254
+ });
255
+ }
256
+ }
257
+ // WP-4f round 3: a stale marker used to skip the only snapshot send, stranding a
258
+ // cold attach forever — still send it, flagged stale/allowStaleSnapshotData (client honors this).
259
+ if (_attachWs.readyState === 1) {
260
+ const snapshotData = _deps._coalesceSnapshotSynchronizedFrames(resp.data);
261
+ _deps._wsSend(_attachWs, JSON.stringify({
262
+ type: 'snapshot', id: _attachId, data: snapshotData,
263
+ ptyCols: resp.cols, ptyRows: resp.rows,
264
+ scrollback: resp.scrollback,
265
+ clientRestoreSeq: msg.clientRestoreSeq,
266
+ restoreSource: 'attach',
267
+ codexRolloutComposed: resp.codexRolloutComposed || undefined,
268
+ frameOwnedViewport: resp.frameOwnedViewport || undefined,
269
+ terminalDisplayMode: resp.terminalDisplayMode || undefined,
270
+ renderProjection: resp.renderProjection || undefined, archiveRow0: Number.isFinite(resp.archiveRow0) ? resp.archiveRow0 : null, // D-4: client identity-seam anchor
271
+ stale: staleSnapshot || undefined, allowStaleSnapshotData: staleSnapshot || undefined,
272
+ }));
273
+ }
274
+ const _snapshotSendDiagFields = {
275
+ requestId: _snapReqId,
276
+ clientAsset: _deps._wsClientAsset(_attachWs),
277
+ cols: resp.cols || 0,
278
+ rows: resp.rows || 0,
279
+ bytes: String(resp.data || '').length,
280
+ };
281
+ if (staleSnapshot) {
282
+ _deps.recordSessionDiagnostic(_attachId, 'attach-stale-snapshot-send', { ..._snapshotSendDiagFields, marker: staleMarker, current: staleCurrent });
283
+ } else {
284
+ _deps.recordSessionDiagnostic(_attachId, 'attach-fresh-snapshot-send', { ..._snapshotSendDiagFields, stale: false });
285
+ }
286
+ };
287
+ _deps.headlessWorker.on('message', _onSnap);
288
+ _deps._noteInteractiveSerializeStart(_snapReqId); // Part 1: prewarm defers while this is in flight
289
+ _deps.headlessWorker.postMessage({ type: 'serialize', sessionId: _attachId, requestId: _snapReqId, scrollback: _attachUseRolloutHistory ? 0 : _attachScrollbackRows });
290
+ _deps._restoreMirrorToPtyGridAfterSerialize(session, _attachId, _attachGrid, 'attach');
291
+ _deps.sessionEvents.emit('session:attached', { id: msg.id, clientCount: session.clients.length });
292
+ }
293
+
294
+ function handleTerminalBootstrap(ws, msg) {
295
+ let request;
296
+ try { request = _deps.parseTerminalBootstrapRequest(msg); } catch { return; }
297
+ const session = _deps.sessions.get(request.id);
298
+ if (!session) {
299
+ // WP-1 (§5 D-1): an id the restore loop has not reached YET is a non-terminal
300
+ // "restore-starting" (like a 503), not the terminal "session-not-found" a deleted/
301
+ // unknown id gets -- the live incident this fixes is exactly this misclassification: a
302
+ // reconnect ~4s ahead of the loop's insertion for this id was told the session is gone
303
+ // and never retried when it came back 3s later. Retain the socket so D-2's announce can
304
+ // reach it the moment the loop inserts this session; an id outside the startup set falls
305
+ // through unchanged (AC-1d).
306
+ if (_deps._restoreWindowPending(request.id)) {
307
+ _deps._retainRestoreAwaitingSocket(request.id, ws);
308
+ return _deps._sendTerminalBootstrapFailed(ws, request.id, 'restore-starting', true);
309
+ }
310
+ return _deps._sendTerminalBootstrapFailed(ws, request.id, 'session-not-found');
311
+ }
312
+ if (session.type === 'walle') return _deps._sendTerminalBootstrapFailed(ws, request.id, 'session-not-live', false);
313
+ // F5 (WP-2 review round 1): Wall-E-like sessions (chatSessionId set, agentType 'walle', etc.
314
+ // -- see isWalleLikeSession) are not terminal frames; D-4's revive is scoped to Codex/Claude
315
+ // PTY sessions only. shouldAutoStartDeferredRestore keeps requiring 'attach' for these.
316
+ if (_deps.isWalleLikeSession(session)) return _deps._sendTerminalBootstrapFailed(ws, request.id, 'session-not-live', false);
317
+ if (session._exited) return _deps._sendTerminalBootstrapFailed(ws, request.id, 'session-exited');
318
+ if (!session.ptyProcess) {
319
+ // D-4 (session-runtime-reset-regression-triage-2026-09-06.html §5): a deferred
320
+ // (parked-PTY) session used to answer 'session-not-live' here and never start the
321
+ // restore, stranding the client after its one retry. Kick the deferred restore the
322
+ // same way `attach`/`snapshot`/`reflow` already do and tell the client to wait for
323
+ // liveness instead of giving up -- but ONLY for a focused/visible request. F2 (WP-2
324
+ // review round 1): handleSessionInventory fans this out to up to 128 ids on every
325
+ // reconnect; without this gate every deferred BACKGROUND session would force-start a
326
+ // restore. A background id stays parked; selecting it later re-requests bootstrap
327
+ // through WP-1's afterPaint path, which IS focused.
328
+ // `request.focused` is the client's in-band statement that this is its active tab or a
329
+ // split-visible pane (public/index.html _terminalBootstrapRequestIsFocused). Needed because a
330
+ // cold selection's request arrives BEFORE the visibility/subscription traffic that feeds
331
+ // _sessionHasViewer -- live on primary the gate answered session-not-live at +2 ms.
332
+ const focused = request.focused === true
333
+ || !!(ws?.viewing?.has(request.id)) || _deps._sessionHasViewer(request.id);
334
+ if (focused && (session._restoreDeferred || session._restoreStarting)) {
335
+ // F1 (WP-2 review round 1): retain this socket as a lifecycle listener BEFORE replying
336
+ // restore-starting, or the session-wake-ready signal this reply promises
337
+ // (broadcastToSession only iterates session.clients) never reaches it. This is NOT a
338
+ // state-sync/input route; the later live bootstrap installs that through
339
+ // _subscribeStateSyncClient before returning terminal-bootstrap-result.
340
+ if (!session.clients.includes(ws)) {
341
+ session.clients.push(ws);
342
+ _deps._recordClientAttachDiagnostic(session, request.id, ws);
343
+ }
344
+ // D-8 (WP-3): the placeholder already holds the persisted last screen in
345
+ // _cachedSnapshot; serve it now instead of leaving the cold click on a blank
346
+ // pixel-less reply while the restore loop spins up. Called UNCONDITIONALLY (fix-round-2:
347
+ // a boot-restore deferred placeholder has no _cachedSnapshot yet -- sendDeferredRestoreStatus
348
+ // itself calls _ensureScrollbackTailCache first, which is what BUILDS the interim tail;
349
+ // gating this call on session._cachedSnapshot already being set, like the legacy attach
350
+ // path never did, would skip that build entirely). `terminal-bootstrap` carries no
351
+ // cols/rows/scrollback/clientRestoreSeq of its own (parseTerminalBootstrapRequest
352
+ // allowlists only type/id/lastAppliedSeq/knownProcessGeneration/focused) -- serve the
353
+ // placeholder's OWN geometry rather than reading undefined request fields, or
354
+ // cachedSnapshotMatchesScrollback's gate (server.js sendCachedSnapshot, not covered
355
+ // by allowMismatched) would silently refuse whenever the placeholder's stored
356
+ // scrollback differs from the library default.
357
+ // Final-review finding #3: compute `starting` FIRST (same order handleAttach already
358
+ // uses, ~line 41433) — the deferred-serve frame below must report the REAL outcome of
359
+ // the auto-start attempt, not a hardcoded `true` followed moments later by a
360
+ // 'restore-blocked' failed frame that contradicts it (that ordering told a blocked
361
+ // client "Starting terminal process..." right before "restore-blocked"). Calling
362
+ // _maybeAutoStartDeferredRestore here (once) also means the later check below never
363
+ // needs to call it again.
364
+ let starting = true;
365
+ if (session._restoreDeferred && !session._restoreStarting) {
366
+ starting = _deps._maybeAutoStartDeferredRestore(session,
367
+ { type: 'terminal-bootstrap', autoStartRestore: true, focused: true }, 'terminal-bootstrap', ws);
368
+ }
369
+ // Final-review finding #5: a Wall-E session never legitimately reaches this deferred
370
+ // branch (handled by the earlier session.type/isWalleLikeSession guards above), but the
371
+ // deferred-serve call itself carried no matching guard, unlike the removed
372
+ // attach-resuming block and handleAttach's cache serve — add it defensively so the two
373
+ // sites stay consistent; the failed reply below is unchanged for walle.
374
+ if (session.type !== 'walle') {
375
+ _deps.sendDeferredRestoreStatus(ws, session, request.id, session._cachedSnapshotCols, session._cachedSnapshotRows, 'terminal-bootstrap', {
376
+ scrollbackRows: session._cachedSnapshotScrollbackRows != null
377
+ ? session._cachedSnapshotScrollbackRows
378
+ : _deps.normalizeSnapshotScrollbackRows(undefined),
379
+ restoreSource: 'terminal-bootstrap-deferred-cache',
380
+ starting: starting === true,
381
+ });
382
+ }
383
+ if (session._restoreDeferred && !session._restoreStarting) {
384
+ return _deps._sendTerminalBootstrapFailed(ws, request.id, starting ? 'restore-starting' : 'restore-blocked', starting === true);
385
+ }
386
+ return _deps._sendTerminalBootstrapFailed(ws, request.id, 'restore-starting', true);
387
+ }
388
+ return _deps._sendTerminalBootstrapFailed(ws, request.id, 'session-not-live', false);
389
+ }
390
+ const gates = _deps._terminalBootstrapGatesFor(ws);
391
+ const existingGate = gates.get(request.id);
392
+ if (existingGate?.phase === 'serializing') return;
393
+ if (existingGate?.phase === 'await-ack' && existingGate.result) {
394
+ _deps._wsSend(ws, existingGate.result, { class: 'interactive', channel: 'terminal-bootstrap', sessionId: request.id });
395
+ return;
396
+ }
397
+ if (existingGate) return;
398
+ const wasAttached = session.clients.includes(ws) && _deps.isSubscribed(ws, request.id);
399
+ if (wasAttached) {
400
+ // A bootstrap from a socket that already holds a live route on this session (session-inventory
401
+ // re-send, manual retry, deferred revive) is a ROUTE RESET, not a departure. Only the worker-side
402
+ // route is reset; attachment authority (viewer-connected), geometry lease / primary role /
403
+ // measured dims and session.clients membership stay intact. Live 2026-09-07 19:53 (4dd72d9f):
404
+ // the departure path here dispatched viewer-disconnected, clearOwnerForClient wiped the tab's
405
+ // lease + primary + dims with no diagnostic, and the next role broadcast locked its input.
406
+ _deps._postWorkerClientDetached(request.id, ws);
407
+ } else {
408
+ _deps._detachTerminalBootstrapSocket(session, request.id, ws);
409
+ }
410
+ const requestId = `terminal-bootstrap-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
411
+ const gate = {
412
+ phase: 'serializing', requestId, timer: null, onSerialized: null, baseSeq: 0, result: null,
413
+ provisionalAttached: false,
414
+ processGeneration: _deps._terminalProcessGeneration(session),
415
+ absoluteDeadlineAt: Date.now() + _deps.TERMINAL_BOOTSTRAP_ABSOLUTE_LIFETIME_MS,
416
+ };
417
+ gates.set(request.id, gate);
418
+ if (!session.clients.includes(ws)) {
419
+ session.clients.push(ws);
420
+ _deps._recordClientAttachDiagnostic(session, request.id, ws);
421
+ }
422
+ if (!_deps._subscribeStateSyncClient(ws, request.id, 'terminal-bootstrap-route', { skipWorkerAttach: true })) {
423
+ session.clients = session.clients.filter(client => client !== ws);
424
+ gates.delete(request.id);
425
+ return _deps._sendTerminalBootstrapFailed(ws, request.id, 'session-not-live');
426
+ }
427
+ _deps._replayRestoreWakeVerdict(session, request.id, ws); // D-B2
428
+ // D-3: a terminal-bootstrap request IS a request to view this session's live frames -- a
429
+ // socket driving bootstrap through a route that predates (or never sends) an explicit 'view'
430
+ // declaration must still get frames once its base ACK installs, or the catch-up this gate
431
+ // exists to deliver would never leave the worker (see _impliedView).
432
+ _deps._impliedView(ws, request.id, 'terminal-bootstrap', { skipWorkerAttach: true }); // NEW-3: same skip as its own subscribe above
433
+ // Input routing is live before visual serialization. The worker bootstrap reservation still
434
+ // holds ordinary frames until the base ACK, so early keystrokes cannot race stale projection
435
+ // bytes even though they reach the live PTY immediately. "provisional" means THIS bootstrap
436
+ // attached the socket -- a failed re-bootstrap of an already-live socket must not detach it.
437
+ gate.provisionalAttached = !wasAttached;
438
+ const onSerialized = (serialized) => {
439
+ if (serialized.type !== 'serialized' || serialized.requestId !== requestId) return;
440
+ if (gates.get(request.id) !== gate || gate.phase !== 'serializing') return;
441
+ clearTimeout(gate.timer);
442
+ _deps.headlessWorker.off('message', onSerialized);
443
+ gate.timer = null;
444
+ gate.onSerialized = null;
445
+ const currentSession = _deps.sessions.get(request.id);
446
+ if (!currentSession || currentSession._exited || !currentSession.ptyProcess
447
+ || _deps._terminalProcessGeneration(currentSession) !== gate.processGeneration) {
448
+ return _deps._clearTerminalBootstrapGate(ws, request.id, {
449
+ reason: 'process-replaced', notify: true,
450
+ });
451
+ }
452
+ const cols = Number(serialized.cols || 0);
453
+ const rows = Number(serialized.rows || 0);
454
+ const baseSeq = Math.max(0, Number(serialized.bootstrap?.baseSeq || 0) || 0);
455
+ if (!serialized.bootstrap || !Number.isSafeInteger(cols) || cols <= 0 || !Number.isSafeInteger(rows) || rows <= 0) {
456
+ return _deps._clearTerminalBootstrapGate(ws, request.id, {
457
+ reason: 'serialize-invalid', notify: true,
458
+ });
459
+ }
460
+ // D-5/WP-3 fix2: mirror the worker's own base-vs-keyframe decision (from `clientBufferLength`).
461
+ const bootstrapMode = serialized.bootstrapMode === 'keyframe' ? 'keyframe' : 'base';
462
+ // WP-3 (D-9/S-48): the reservation is the ONLY source of gridRevision/surfaceEpoch for the
463
+ // wire result -- carrying them lets the browser commit a real baseline tuple from the
464
+ // bootstrap seed instead of forcing a keyframe on the first post-activation frame.
465
+ const gridRevision = Math.max(0, Number(serialized.bootstrap?.gridRevision || 0) || 0);
466
+ const surfaceEpoch = Math.max(0, Number(serialized.bootstrap?.surfaceEpoch || 0) || 0);
467
+ let result;
468
+ try {
469
+ result = _deps.truncateBootstrapAnsi(serialized.data, (ansi) => _deps.composeTerminalBootstrapResult({
470
+ id: request.id, baseSeq, cols, rows, ansi, mode: bootstrapMode, gridRevision, surfaceEpoch, ...(bootstrapMode === 'base' ? { archiveRow0: Number.isFinite(serialized.archiveRow0) ? serialized.archiveRow0 : null } : {}), // D-4 (WP-4 fix round 4): base-only identity-seam anchor, worker-computed
471
+ ...(serialized.mouse ? { mouse: serialized.mouse } : {}), // altscreen-wheel-mouse-report-truth-2026-09-12 gap fix: same-instant mirror mouse state
472
+ }));
473
+ } catch (error) {
474
+ // Fixed metadata is bounded; this is a defensive truthful completion if an
475
+ // unexpected serializer value still cannot compose a truncated base. Never
476
+ // send unchanged here: a cold client has no prior base to acknowledge.
477
+ return _deps._clearTerminalBootstrapGate(ws, request.id, {
478
+ reason: 'serialize-invalid', notify: true,
479
+ });
480
+ }
481
+ if (_deps.sessions.get(request.id) !== currentSession
482
+ || _deps._terminalProcessGeneration(currentSession) !== gate.processGeneration) {
483
+ return _deps._clearTerminalBootstrapGate(ws, request.id, {
484
+ reason: 'process-replaced', notify: true,
485
+ });
486
+ }
487
+ // S-31 (terminal-baseline-single-owner-2026-09-14 WP-4): deliberately NO gridRevision check
488
+ // here. Under D-2 a seq is never reused within a worker lifetime, and `processGeneration`
489
+ // already covers a PTY restart (the one case a seq could repeat) -- `lastAppliedSeq ===
490
+ // baseSeq` at an unchanged `processGeneration` already uniquely identifies the same delivery
491
+ // state the client already has. Adding a gridRevision comparison would only add a field this
492
+ // decision does not need to disambiguate anything it doesn't already disambiguate.
493
+ const unchanged = request.knownProcessGeneration > 0
494
+ && request.knownProcessGeneration === gate.processGeneration
495
+ && request.lastAppliedSeq === baseSeq;
496
+ _deps._armTerminalBootstrapGateDeadline(ws, request.id, gate, 'await-ack');
497
+ gate.baseSeq = baseSeq;
498
+ gate.bootstrapMode = bootstrapMode; gate.clientBufferLength = Number(request.clientBufferLength || 0); gate.rows = rows;
499
+ gate.result = unchanged // altscreen-wheel-mouse-report-truth-2026-09-12 gap fix: mouse rides unchanged too
500
+ ? _deps.composeTerminalBootstrapResult({ id: request.id, baseSeq, unchanged: true, gridRevision, surfaceEpoch, ...(serialized.mouse ? { mouse: serialized.mouse } : {}) })
501
+ : result;
502
+ _deps._wsSend(ws, gate.result, { class: 'interactive', channel: 'terminal-bootstrap', sessionId: request.id });
503
+ };
504
+ gate.onSerialized = onSerialized;
505
+ _deps._armTerminalBootstrapGateDeadline(ws, request.id, gate, 'serializing');
506
+ _deps.headlessWorker.on('message', onSerialized);
507
+ _deps.headlessWorker.postMessage({
508
+ // WP-6 (reload regression): was a hardcoded scrollback: 0 -- post the same normalized cap handleAttach uses.
509
+ type: 'serialize-bootstrap', sessionId: request.id, requestId, scrollback: _deps.normalizeSnapshotScrollbackRows(session._snapshotScrollbackRows),
510
+ clientId: String(ws.clientId || ''), socketGeneration: Number(ws.socketGeneration || 0),
511
+ clientBufferLength: request.clientBufferLength,
512
+ });
513
+ }
514
+
515
+ function handleResize(ws, msg) {
516
+ let request = msg;
517
+ if (msg?.geometryProtocolVersion === 2) {
518
+ try {
519
+ const sanitized = _deps.sanitizeGeometryResize(msg, Number(ws?.socketGeneration || 0));
520
+ request = sanitized;
521
+ } catch (error) {
522
+ if (!(error instanceof _deps.GeometryClaimValidationError)) throw error;
523
+ const candidateId = typeof msg?.id === 'string' && msg.id.length <= 128 ? msg.id : '';
524
+ if (candidateId && _deps.sessions.has(candidateId)) {
525
+ _deps.recordSessionDiagnostic(candidateId, 'terminal-geometry-fenced', {
526
+ operation: 'resize', reason: String(error.code || 'resize-shape-invalid').slice(0, 40),
527
+ });
528
+ }
529
+ _deps._sendTerminalGeometryFenceAck(ws, {
530
+ id: candidateId, operation: 'resize', reason: String(error.code || 'resize-shape-invalid'),
531
+ });
532
+ return;
533
+ }
534
+ }
535
+ msg = request;
536
+ const session = _deps.sessions.get(msg.id);
537
+ if (session && msg.cols != null && msg.rows != null) {
538
+ if (msg.geometryProtocolVersion !== 2) {
539
+ _deps.recordSessionDiagnostic(msg.id, 'terminal-geometry-fenced', {
540
+ operation: 'resize', reason: 'geometry-claim-required',
541
+ });
542
+ _deps._sendTerminalGeometryFenceAck(ws, {
543
+ id: msg.id, operation: 'resize', reason: 'geometry-claim-required',
544
+ });
545
+ return;
546
+ }
547
+ let geometryFence = null;
548
+ if (msg.geometryProtocolVersion === 2) {
549
+ geometryFence = {
550
+ clientId: String(ws.clientId || ''),
551
+ socketGeneration: msg.socketGeneration,
552
+ ownerGeneration: msg.ownerGeneration,
553
+ measurementId: msg.measurementId,
554
+ layoutEpoch: msg.layoutEpoch,
555
+ cols: msg.cols,
556
+ rows: msg.rows,
557
+ };
558
+ const fence = _deps.validateGeometryFence(_deps._geometryLease(session, msg.id), geometryFence);
559
+ if (!fence.ok) {
560
+ _deps.recordSessionDiagnostic(msg.id, 'terminal-geometry-fenced', {
561
+ operation: 'resize', reason: fence.reason,
562
+ ownerGeneration: msg.ownerGeneration,
563
+ measurementId: msg.measurementId,
564
+ });
565
+ _deps._sendTerminalGeometryFenceAck(ws, {
566
+ id: msg.id, operation: 'resize', reason: fence.reason,
567
+ });
568
+ return;
569
+ }
570
+ }
571
+ if (!_deps.CTM_GRID_PROVENANCE && (!msg.cols || !msg.rows)) return;
572
+ if (!session.ptyProcess) return;
573
+ const activation = null;
574
+ if (false) {
575
+ // This guard exists to stop a resize from moving the PTY out from under an in-flight
576
+ // activation. It must protect the PTY DIMENSIONS, not discard the client's identity.
577
+ // A resize for the dims the activation ALREADY owns conflicts with nothing — there is
578
+ // no PTY change to make — yet the old unconditional return also skipped
579
+ // _recordViewerDims and _claimGeometryFocus below. A client whose fit-resize reliably
580
+ // lands during activation was therefore never registered as a viewer and never
581
+ // promoted to primary geometry owner, so the lease's measurement identity could never
582
+ // reconverge with the client's newer measurement. Every subsequent activation was then
583
+ // refused 'geometry-generation-fenced', the client's bounded redrive budget (2) drained,
584
+ // the activation latched DEGRADED, and projectInputOwnership disabled terminal input
585
+ // with no escape — a session frozen with a perfectly alive PTY.
586
+ // Live evidence (session eed3efb1): 'primary': null with its ONLY viewer classified as a
587
+ // secondary, three activation epochs fenced in ~1s, then input readiness latched false.
588
+ // Register the identity; decline only the PTY drive.
589
+ const requestedCols = Number(msg.cols) || 0;
590
+ const requestedRows = Number(msg.rows) || 0;
591
+ const ownedCols = Number(activation.grid?.cols || 0);
592
+ const ownedRows = Number(activation.grid?.rows || 0);
593
+ const sameGrid = requestedCols > 0 && requestedRows > 0
594
+ && requestedCols === ownedCols && requestedRows === ownedRows;
595
+ _deps.recordSessionDiagnostic(msg.id, 'terminal-activation-resize-rejected', {
596
+ activationEpoch: activation.activationEpoch,
597
+ phase: activation.phase,
598
+ requestedCols,
599
+ requestedRows,
600
+ ownedCols,
601
+ ownedRows,
602
+ disposition: sameGrid ? 'noop-registered' : 'rejected',
603
+ });
604
+ if (sameGrid) {
605
+ _deps._recordViewerDims(ws, msg.id, requestedCols, requestedRows, 'measured');
606
+ _deps._claimGeometryFocus(ws, msg.id, 'resize', { interaction: true });
607
+ return;
608
+ }
609
+ _deps._sendTerminalGeometryFenceAck(ws, {
610
+ id: msg.id, operation: 'resize', reason: 'activation-owns-grid',
611
+ });
612
+ return;
613
+ }
614
+ const verdict = msg.geometryProtocolVersion === 2 ? {
615
+ source: 'measured',
616
+ cols: Number(msg.cols),
617
+ rows: Number(msg.rows),
618
+ valid: true,
619
+ reason: '',
620
+ } : _deps.classifyInboundGrid(msg);
621
+ const gate = _deps.mayDrivePtyResize(verdict);
622
+ const emitSite = String(msg.emitSite || 'unknown').slice(0, 80);
623
+ const clientId = String(ws.clientId || '').slice(0, 12);
624
+ if (_deps.CTM_GRID_PROVENANCE && !gate.allow) {
625
+ const details = {
626
+ requestedCols: verdict.cols,
627
+ requestedRows: verdict.rows,
628
+ source: verdict.source,
629
+ reason: gate.reason,
630
+ emitSite,
631
+ ptyCols: Number(session._ptyCols) || 0,
632
+ ptyRows: Number(session._ptyRows) || 0,
633
+ looksDefault: _deps.looksLikeXtermDefault(verdict.cols, verdict.rows),
634
+ clientId,
635
+ };
636
+ _deps._recordGridProvenanceDiagnostic(session, msg.id, 'unmeasured-resize-rejected', details);
637
+ _deps._recordGridProvenanceDiagnostic(session, msg.id, 'geometry-provenance', {
638
+ ...details,
639
+ outcome: 'rejected',
640
+ });
641
+ if (session._restoredHeadlessFromSnapshotAtSpawn) {
642
+ _deps._recordGridProvenanceDiagnostic(session, msg.id, 'restored-headless-unmeasured-resize-skip', details);
643
+ }
644
+ return;
645
+ }
646
+ if (!_deps.CTM_GRID_PROVENANCE
647
+ && session._restoredHeadlessFromSnapshotAtSpawn
648
+ && msg.geometryProtocolVersion !== 2
649
+ && msg.terminalDimsMeasured !== true) {
650
+ const grid = _deps._resolveRestoredHeadlessRequestGrid(session, msg, 'resize');
651
+ _deps.recordSessionDiagnostic(msg.id, 'restored-headless-unmeasured-resize-skip', {
652
+ requestedCols: msg.cols || 0,
653
+ requestedRows: msg.rows || 0,
654
+ cols: grid.cols || 0,
655
+ rows: grid.rows || 0,
656
+ });
657
+ return;
658
+ }
659
+ const cols = _deps.CTM_GRID_PROVENANCE ? verdict.cols : (Number(msg.cols) || 0);
660
+ const rows = _deps.CTM_GRID_PROVENANCE ? verdict.rows : (Number(msg.rows) || 0);
661
+ const acceptedSource = _deps.CTM_GRID_PROVENANCE ? verdict.source : 'measured';
662
+ _deps._recordViewerDims(ws, msg.id, cols, rows, acceptedSource);
663
+ _deps._claimGeometryFocus(ws, msg.id, 'resize', { interaction: true });
664
+
665
+ // Width arbitration: the shared PTY follows the PRIMARY viewer only. A
666
+ // SECONDARY viewer's resize must NOT move the PTY/headless — that drift is
667
+ // exactly what corrupts the other viewers' scrollback (their width-specific
668
+ // raw stream lands at the wrong width). Secondaries are rendered at their own
669
+ // width via a pushed snapshot instead. Single-viewer sessions: that sole
670
+ // viewer is the primary, so this is identical to the old behavior.
671
+ const arb = _deps._viewerArbitration(session, msg.id);
672
+ const isPrimary = !arb.primaryWs || arb.primaryWs === ws;
673
+ if (_deps.CTM_GRID_PROVENANCE) {
674
+ _deps._recordGridProvenanceDiagnostic(session, msg.id, 'geometry-provenance', {
675
+ requestedCols: cols,
676
+ requestedRows: rows,
677
+ source: acceptedSource,
678
+ reason: gate.reason,
679
+ outcome: 'accepted',
680
+ emitSite,
681
+ ptyCols: Number(session._ptyCols) || 0,
682
+ ptyRows: Number(session._ptyRows) || 0,
683
+ clientId,
684
+ appliedToPty: isPrimary,
685
+ arbitration: isPrimary ? 'primary' : 'secondary',
686
+ });
687
+ }
688
+
689
+ if (isPrimary) {
690
+ _deps._markCtmUiRefreshActivitySuppression(session, 'resize');
691
+ // WI-3 (resume-grid-mismatch-render-churn): a resize arriving within
692
+ // CTM_FRESH_PROCESS_ADOPT_MS of the PTY's own process spawn is nearly free — the TUI
693
+ // hasn't painted a screen the user is reading yet, so applying it now beats deferring 30s
694
+ // (_deferPrimaryPtyResizeIfStreaming's quiet-window hold) and later SIGWINCHing a settled
695
+ // screen into a full reflow. Scoped to agent TUIs (shell PTYs already resize eagerly via
696
+ // the fall-through below) and to a genuine dims change.
697
+ const freshProviderId = session._providerId || _deps._providerIdFromCmd(session.cmd || '');
698
+ const freshAgentTui = !!freshProviderId && freshProviderId !== 'shell';
699
+ const freshPtyCols = Number(session._ptyCols || 0);
700
+ const freshPtyRows = Number(session._ptyRows || 0);
701
+ const freshSpawnedAt = Number(session._ptySpawnedAt || 0);
702
+ const freshAgeMs = freshSpawnedAt > 0 ? Date.now() - freshSpawnedAt : -1;
703
+ const freshDimsDiffer = freshPtyCols > 0 && freshPtyRows > 0 && (cols !== freshPtyCols || rows !== freshPtyRows);
704
+ if (_deps.CTM_FRESH_PROCESS_ADOPT_ENABLED && freshAgentTui && freshDimsDiffer
705
+ && freshAgeMs >= 0 && freshAgeMs < _deps.CTM_FRESH_PROCESS_ADOPT_MS
706
+ && _deps._applyPrimaryPtyResize(session, msg.id, cols, rows, { site: 'resize-fresh-process' })) {
707
+ _deps._clearRowsFlapDebounce(session);
708
+ _deps.recordSessionDiagnostic(msg.id, 'resize-applied-fresh-process', { cols, rows, ageMs: freshAgeMs });
709
+ _deps._broadcastViewerRoles(session, msg.id);
710
+ return;
711
+ }
712
+ if (_deps._deferPrimaryPtyResizeIfStreaming(session, msg.id, cols, rows, 'resize', { geometryFence })) {
713
+ // Output resumed → the stream policy owns convergence now; drop any idle flap
714
+ // debounce so its trailing timer can't apply a stale grid behind the turn.
715
+ _deps._clearRowsFlapDebounce(session);
716
+ _deps._broadcastViewerRoles(session, msg.id);
717
+ return;
718
+ }
719
+ // Not streaming: an idle rows-only micro-flap (the resume-time chrome-height
720
+ // jitter) is trailing-edge debounced instead of applied per event — otherwise
721
+ // each SIGWINCH re-seeds the full composed history and the client re-prints it
722
+ // from the top. Genuine resizes fall through and apply immediately.
723
+ if (_deps._maybeDebounceRowsFlapResize(session, msg.id, cols, rows, { geometryFence })) {
724
+ _deps._broadcastViewerRoles(session, msg.id);
725
+ return;
726
+ }
727
+ if (!_deps._applyPrimaryPtyResize(session, msg.id, cols, rows, { site: 'resize-primary' })) return;
728
+ } else {
729
+ // Secondary viewer: dims recorded above; render it at its own width now
730
+ // without disturbing the PTY/heartbeat dims.
731
+ _deps._pushSecondarySnapshots(session, msg.id, 'secondary-resize').catch(() => {});
732
+ }
733
+ _deps._broadcastViewerRoles(session, msg.id);
734
+ }
735
+ }
736
+
737
+ function handleReflow(ws, msg) {
738
+ const session = _deps.sessions.get(msg.id);
739
+ if (!session) {
740
+ _deps.sendTerminalSessionUnavailableSnapshot(ws, msg.id, 'reflow', msg);
741
+ return;
742
+ }
743
+ _deps._recordRenderChurn(session, 'reflow');
744
+ const reflowGrid = _deps._resolveRestoredHeadlessRequestGrid(session, msg, 'reflow');
745
+ const cols = reflowGrid.cols;
746
+ const rows = reflowGrid.rows;
747
+ _deps.recordSessionDiagnostic(msg.id, 'reflow-request', {
748
+ cols, rows,
749
+ redraw: !!msg.redraw,
750
+ source: msg.restoreSource || null,
751
+ clientRestoreSeq: msg.clientRestoreSeq || null,
752
+ clientAsset: _deps._wsClientAsset(ws),
753
+ });
754
+ // Reflow-storm cost containment: under a churn storm (≥ threshold reflows in the rolling
755
+ // window — the signature of a client stuck re-requesting), cap FRESH full serializes of
756
+ // the giant session to one per window-half and serve the cached snapshot in between.
757
+ // Freshness still converges (a fresh serialize is always allowed once the cap elapses,
758
+ // and the heartbeat/boundary paths keep refreshing the cache); what this kills is the
759
+ // "full multi-MB serialize every 4.5s for an entire turn" failure mode. redraw reflows
760
+ // (stale-width recovery) are never throttled.
761
+ if (!msg.redraw && session._renderChurn && session._renderChurn.reflows.length >= _deps.RENDER_CHURN_FLASH_THRESHOLD
762
+ && Date.now() - (session._lastFreshReflowServeAt || 0) < _deps.RENDER_CHURN_WINDOW_MS / 2
763
+ && _deps._serveCachedSnapshotOnSerializeTimeout(ws, session, msg.id, cols, rows, 'reflow', msg.clientRestoreSeq)) {
764
+ _deps.recordSessionDiagnostic(msg.id, 'reflow-churn-served-cache', {
765
+ cols, rows, reflowsInWindow: session._renderChurn.reflows.length,
766
+ });
767
+ return;
768
+ }
769
+ session._lastFreshReflowServeAt = Date.now();
770
+ const scrollbackRows = _deps.normalizeSnapshotScrollbackRows(msg.scrollback);
771
+ session._snapshotScrollbackRows = scrollbackRows;
772
+ // Codex rollout-backed history: re-render at the REQUESTED cols (clean, width-perfect)
773
+ // instead of reflowing the lossy serialize blob — this is what kills the resize-time
774
+ // in-place corruption (s6ssion / "Working 3 12"). Serialize the native live region only.
775
+ const useRolloutHistory = _deps._codexRolloutHistoryActive(session, msg.id);
776
+ if (session._restoreDeferred) {
777
+ const starting = _deps._maybeAutoStartDeferredRestore(session, msg, 'reflow', ws);
778
+ _deps.sendDeferredRestoreStatus(ws, session, msg.id, cols, rows, 'reflow', {
779
+ scrollbackRows,
780
+ clientRestoreSeq: msg.clientRestoreSeq,
781
+ restoreSource: 'reflow-deferred-cache',
782
+ starting,
783
+ });
784
+ return;
785
+ }
786
+ if (typeof session._flushHeadlessFeed === 'function') session._flushHeadlessFeed();
787
+ const reqId = `reflow-${Date.now()}`;
788
+ let reflowOutputMarker = _deps._snapshotOutputMarker(session);
789
+ const timer = setTimeout(() => {
790
+ _deps.headlessWorker.off('message', onSnap);
791
+ _deps._noteInteractiveSerializeDone(reqId);
792
+ console.warn(`[reflow] Headless worker timed out for session ${msg.id.slice(0, 8)}`);
793
+ // Serve the last good cached frame instead of leaving the client on a half-painted
794
+ // one (and instead of a data:null retry that re-contends the worker). Self-heals via
795
+ // the heartbeat divergence check once the worker frees up.
796
+ const sess = _deps.sessions.get(msg.id);
797
+ if (_deps._serveCachedSnapshotOnSerializeTimeout(ws, sess, msg.id, cols, rows, 'reflow', msg.clientRestoreSeq)) return;
798
+ if (ws.readyState === 1) _deps._wsSend(ws, JSON.stringify({ type: 'snapshot', id: msg.id, data: null, timedOut: true, clientRestoreSeq: msg.clientRestoreSeq, restoreSource: 'reflow' }));
799
+ }, _deps.CTM_INTERACTIVE_SERIALIZE_TIMEOUT_MS);
800
+ const onSnap = (resp) => {
801
+ if (resp.type !== 'serialized' || resp.requestId !== reqId) return;
802
+ clearTimeout(timer);
803
+ _deps._noteInteractiveSerializeDone(reqId);
804
+ _deps.headlessWorker.off('message', onSnap);
805
+ const currentSession = _deps.sessions.get(msg.id);
806
+ const forceMidTurnSnapshot = msg.forceMidTurnSnapshot === true;
807
+ if (useRolloutHistory && currentSession && resp.data) {
808
+ resp = _deps._composeCodexRolloutResp(currentSession, msg.id, resp, {
809
+ deferMidTurn: true,
810
+ forceMidTurnSnapshot,
811
+ reflow: true,
812
+ });
813
+ }
814
+ // Mid-turn suppression: the compose declined to push a destructive snapshot while the
815
+ // codex turn is still streaming (see _shouldSuppressMidTurnRestore). Tell the client to
816
+ // keep its current live render; the settle-time reconcile reapplies the clean snapshot.
817
+ if (resp && resp._codexMidTurnDeferred) {
818
+ _deps.recordSessionDiagnostic(msg.id, 'reflow-deferred-midturn', { requestId: reqId, clientRestoreSeq: msg.clientRestoreSeq || null });
819
+ if (ws.readyState === 1) {
820
+ _deps._wsSend(ws, JSON.stringify({ type: 'snapshot', id: msg.id, data: null, midTurnDeferred: true, clientRestoreSeq: msg.clientRestoreSeq, restoreSource: 'reflow' }));
821
+ }
822
+ return;
823
+ }
824
+ if (resp?.composePending) {
825
+ _deps.recordSessionDiagnostic(msg.id, 'interactive-compose-pending', {
826
+ requestId: reqId,
827
+ retryAfterMs: Number(resp.retryAfterMs) || 1000,
828
+ });
829
+ if (ws.readyState === 1) {
830
+ _deps._wsSend(ws, JSON.stringify({
831
+ type: 'snapshot',
832
+ id: msg.id,
833
+ data: null,
834
+ composePending: true,
835
+ retryAfterMs: Number(resp.retryAfterMs) || 1000,
836
+ clientRestoreSeq: msg.clientRestoreSeq,
837
+ restoreSource: 'reflow',
838
+ }));
839
+ }
840
+ return;
841
+ }
842
+ let staleSnapshot = false;
843
+ if (currentSession && resp.data) {
844
+ const stored = _deps._cacheSerializedSnapshot(currentSession, resp, reflowOutputMarker);
845
+ staleSnapshot = !!stored.stale;
846
+ if (staleSnapshot) {
847
+ _deps.recordSessionDiagnostic(msg.id, 'reflow-stale-output-after-request', {
848
+ requestId: reqId,
849
+ clientRestoreSeq: msg.clientRestoreSeq || null,
850
+ marker: stored.marker,
851
+ current: stored.current,
852
+ });
853
+ }
854
+ }
855
+ if (staleSnapshot) {
856
+ _deps.recordSessionDiagnostic(msg.id, 'reflow-stale-snapshot-skip', {
857
+ requestId: reqId,
858
+ clientRestoreSeq: msg.clientRestoreSeq || null,
859
+ cols: resp.cols || 0,
860
+ rows: resp.rows || 0,
861
+ bytes: String(resp.data || '').length,
862
+ });
863
+ if (ws.readyState === 1) {
864
+ _deps._wsSend(ws, JSON.stringify({
865
+ type: 'snapshot',
866
+ id: msg.id,
867
+ data: null,
868
+ stale: true,
869
+ clientRestoreSeq: msg.clientRestoreSeq,
870
+ restoreSource: 'reflow',
871
+ }));
872
+ }
873
+ return;
874
+ }
875
+ _deps.recordSessionDiagnostic(msg.id, 'reflow-fresh-snapshot-send', {
876
+ requestId: reqId,
877
+ clientRestoreSeq: msg.clientRestoreSeq || null,
878
+ clientAsset: _deps._wsClientAsset(ws),
879
+ cols: resp.cols || 0,
880
+ rows: resp.rows || 0,
881
+ bytes: String(resp.data || '').length,
882
+ stale: false,
883
+ });
884
+ if (ws.readyState === 1) {
885
+ const snapshotData = _deps._coalesceSnapshotSynchronizedFrames(resp.data);
886
+ _deps._wsSend(ws, JSON.stringify({ type: 'snapshot', id: msg.id, data: snapshotData, ptyCols: resp.cols, ptyRows: resp.rows, scrollback: resp.scrollback, clientRestoreSeq: msg.clientRestoreSeq, restoreSource: 'reflow', codexRolloutComposed: resp.codexRolloutComposed || undefined, frameOwnedViewport: resp.frameOwnedViewport || undefined, terminalDisplayMode: resp.terminalDisplayMode || undefined, renderProjection: resp.renderProjection || undefined, fp: resp.fp }));
887
+ }
888
+ };
889
+ _deps.headlessWorker.on('message', onSnap);
890
+ _deps._noteInteractiveSerializeStart(reqId); // Part 1: prewarm defers while this reflow serialize is in flight
891
+
892
+ const serialize = () => {
893
+ if (typeof session._flushHeadlessFeed === 'function') session._flushHeadlessFeed();
894
+ reflowOutputMarker = _deps._snapshotOutputMarker(session);
895
+ // Ensure headless terminal matches client dimensions, then serialize and
896
+ // send snapshot — same path as tab-switch restore.
897
+ _deps.headlessWorker.postMessage({ type: 'resize', sessionId: msg.id, cols, rows });
898
+ _deps.headlessWorker.postMessage({ type: 'serialize', sessionId: msg.id, requestId: reqId, scrollback: useRolloutHistory ? 0 : scrollbackRows });
899
+ };
900
+
901
+ // Width arbitration: only the PRIMARY viewer may SIGWINCH the shared PTY. A
902
+ // redraw-reflow from a secondary would move the PTY to that viewer's width and
903
+ // corrupt everyone else — downgrade it to a plain serialize at the requested
904
+ // dims (no PTY poke), then restore the headless to the primary grid so the
905
+ // heartbeat fingerprint stays put. Scoped strictly to the multi-viewer case:
906
+ // with no secondaries this is ALWAYS true, so single-viewer reflow (the common
907
+ // case) is identical to pre-arbitration behavior.
908
+ const reqMayDrivePty = !session._hasSecondaryViewers || !session._primaryWs || session._primaryWs === ws;
909
+ // Mid-stream guard: a SIGWINCH while output is in flight makes the TUI re-emit its
910
+ // in-flight block -> permanent duplicate scrollback (see _deferPrimaryPtyResizeIfStreaming).
911
+ // While deferred, the PTY/headless grid stays put; we serialize at the REQUESTED dims
912
+ // and restore the headless to the PTY grid afterward (same dance as the secondary
913
+ // path). The destructive redraw nudge is pointless mid-stream anyway — the live
914
+ // output is already repainting the screen.
915
+ const ptyResizeDeferred = reqMayDrivePty && session.ptyProcess
916
+ && _deps._deferPrimaryPtyResizeIfStreaming(session, msg.id, cols, rows, msg.redraw ? 'reflow-redraw' : 'reflow');
917
+ if (ptyResizeDeferred && msg.redraw) {
918
+ _deps.recordSessionDiagnostic(msg.id, 'reflow-redraw-downgraded-streaming', { cols, rows });
919
+ }
920
+ if (msg.redraw && session.ptyProcess && reqMayDrivePty && !ptyResizeDeferred) {
921
+ _deps._markCtmUiRefreshActivitySuppression(session, 'reflow');
922
+ const nudgeCols = cols > 20 ? cols - 1 : cols + 1;
923
+ try {
924
+ // A same-size resize often does not produce SIGWINCH, so tools like
925
+ // Codex/Claude/OpenCode do not repaint their full-screen UI. Nudge one
926
+ // column, then restore the target size and snapshot after the app has a
927
+ // chance to redraw from its own source state.
928
+ session.ptyProcess.resize(nudgeCols, rows);
929
+ session._lastViewerProvokeAt = Date.now(); // SIGWINCH repaint = viewer-provoked output (status D-2)
930
+ _deps.recordSessionDiagnostic(msg.id, 'pty-resize-applied', _deps.buildPtyResizeAppliedDetail({ site: 'reflow-nudge', cols: nudgeCols, rows, intent: 'nudge-enter' }));
931
+ _deps.headlessWorker.postMessage({ type: 'resize', sessionId: msg.id, cols: nudgeCols, rows, nudge: 'enter' });
932
+ } catch {}
933
+ setTimeout(() => {
934
+ try {
935
+ session.ptyProcess.resize(cols, rows);
936
+ session._lastViewerProvokeAt = Date.now(); // SIGWINCH repaint = viewer-provoked output (status D-2)
937
+ _deps.recordSessionDiagnostic(msg.id, 'pty-resize-applied', _deps.buildPtyResizeAppliedDetail({ site: 'reflow-nudge', cols, rows, intent: 'nudge-exit' }));
938
+ } catch {}
939
+ _deps.headlessWorker.postMessage({ type: 'resize', sessionId: msg.id, cols, rows, nudge: 'exit' });
940
+ setTimeout(serialize, 120);
941
+ }, 80);
942
+ } else {
943
+ // Also sync PTY dimensions (in case they drifted) — ONLY when the requester
944
+ // is the primary. A secondary's reflow must NOT move the shared PTY.
945
+ if (session.ptyProcess && reqMayDrivePty && !ptyResizeDeferred) {
946
+ _deps._markCtmUiRefreshActivitySuppression(session, 'reflow');
947
+ try {
948
+ session.ptyProcess.resize(cols, rows);
949
+ session._lastViewerProvokeAt = Date.now(); // SIGWINCH repaint = viewer-provoked output (status D-2)
950
+ _deps.recordSessionDiagnostic(msg.id, 'pty-resize-applied', _deps.buildPtyResizeAppliedDetail({ site: 'reflow-sync', cols, rows }));
951
+ } catch {}
952
+ }
953
+ serialize();
954
+ if ((!reqMayDrivePty || ptyResizeDeferred) && session._ptyCols > 0 && session._ptyRows > 0
955
+ && (session._ptyCols !== cols || session._ptyRows !== rows)) {
956
+ // The serialize ran at the requester's dims; restore the headless to the
957
+ // authoritative PTY grid (FIFO after serialize). The PTY grid is what the TUI
958
+ // BELIEVES the screen is — holding the mirror at ANY other rows makes the TUI's
959
+ // scroll-at-believed-bottom-row miss the mirror's real bottom, so its diff-painter
960
+ // model diverges and every repaint bakes interleaved ghost rows INTO the mirror
961
+ // (live 3457b284/b363b51d: old "Working (Xs)…" rows stranded mid-transcript for the
962
+ // whole rows-only defer — queue-bar row flaps + a chattering background terminal
963
+ // mean the defer can run for many minutes). The previous rows-only "mirror hold"
964
+ // (reflow-storm fix) assumed equal-cols divergence was content-safe; it is NOT —
965
+ // scroll behavior depends on rows. The storm's client half is fixed at the source
966
+ // instead (no adopt-mirror-rows, micro-flap suppression, recovery backoff), so the
967
+ // mirror follows the PTY unconditionally and the client tolerates the shorter
968
+ // frames (clear-orphans) until the deferred resize applies.
969
+ try { _deps.headlessWorker.postMessage({ type: 'resize', sessionId: msg.id, cols: session._ptyCols, rows: session._ptyRows }); } catch {}
970
+ if (ptyResizeDeferred && reqMayDrivePty && session._ptyCols === cols) {
971
+ _deps.recordSessionDiagnostic(msg.id, 'reflow-mirror-restored-rows-only-defer', {
972
+ cols, rows, ptyRows: session._ptyRows,
973
+ });
974
+ }
975
+ }
976
+ }
977
+ }
978
+
979
+ module.exports = {
980
+ configure,
981
+ handleAttach,
982
+ handleTerminalBootstrap,
983
+ handleResize,
984
+ handleReflow,
985
+ };