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.
- package/README.md +3 -1
- package/lib/release-schema.json +2 -2
- package/lib/runtime-closure-policy.json +8 -7
- package/package.json +1 -1
- package/template/coding-task-manager/api-prompts.js +45 -7
- package/template/coding-task-manager/bin/claude-hook.js +55 -1
- package/template/coding-task-manager/bin/ctm-relay-client.js +35 -12
- package/template/coding-task-manager/bin/land.js +2069 -0
- package/template/coding-task-manager/bin/memory-census.js +4 -2
- package/template/coding-task-manager/bin/memory-sync-verify.js +151 -0
- package/template/coding-task-manager/bin/probe-refusal-scope.js +530 -0
- package/template/coding-task-manager/db.js +156 -32
- package/template/coding-task-manager/lib/acp/acp-client.js +383 -0
- package/template/coding-task-manager/lib/acp/dsh-context-truth.js +176 -0
- package/template/coding-task-manager/lib/acp/dsh-launch-model.js +302 -0
- package/template/coding-task-manager/lib/acp/dsh-mcp-servers.js +194 -0
- package/template/coding-task-manager/lib/acp/dsh-prompt-lane.js +616 -0
- package/template/coding-task-manager/lib/acp/dsh-route-patch.js +308 -0
- package/template/coding-task-manager/lib/acp/dsh-session-runtime.js +522 -0
- package/template/coding-task-manager/lib/acp/dsh-transcript.js +395 -0
- package/template/coding-task-manager/lib/acp/dsh-usage-log.js +162 -0
- package/template/coding-task-manager/lib/acp/ndjson-stdio-transport.js +225 -0
- package/template/coding-task-manager/lib/adopted-host-redial.js +47 -11
- package/template/coding-task-manager/lib/adopted-launch-environment.js +123 -0
- package/template/coding-task-manager/lib/agent-auto-update-runtime.js +0 -3
- package/template/coding-task-manager/lib/agent-auto-update.js +26 -10
- package/template/coding-task-manager/lib/agent-bundle/build.js +657 -0
- package/template/coding-task-manager/lib/agent-bundle/codex-install.js +558 -0
- package/template/coding-task-manager/lib/agent-bundle/drift.js +120 -0
- package/template/coding-task-manager/lib/agent-bundle/gc.js +522 -0
- package/template/coding-task-manager/lib/agent-bundle/inventory.js +346 -0
- package/template/coding-task-manager/lib/agent-bundle/push.js +717 -0
- package/template/coding-task-manager/lib/agent-bundle/skill-transfer.js +724 -0
- package/template/coding-task-manager/lib/agent-bundle/spawn.js +119 -0
- package/template/coding-task-manager/lib/agent-bundle/store.js +568 -0
- package/template/coding-task-manager/lib/agent-capabilities.js +142 -12
- package/template/coding-task-manager/lib/agent-cli-cache.js +30 -5
- package/template/coding-task-manager/lib/agent-definitions.js +19 -0
- package/template/coding-task-manager/lib/agent-hooks-codex-toml.js +28 -16
- package/template/coding-task-manager/lib/agent-hooks-codex-workspace.js +9 -1
- package/template/coding-task-manager/lib/agent-hooks-installer.js +23 -8
- package/template/coding-task-manager/lib/agent-hooks-prune.js +37 -11
- package/template/coding-task-manager/lib/agent-hooks-shim.js +105 -0
- package/template/coding-task-manager/lib/agent-presets.js +84 -1
- package/template/coding-task-manager/lib/agent-upgrade-jobs.js +12 -1
- package/template/coding-task-manager/lib/agent-version-service.js +74 -11
- package/template/coding-task-manager/lib/all-sessions-query.js +6 -2
- package/template/coding-task-manager/lib/api/agent-bundles.js +436 -0
- package/template/coding-task-manager/lib/api/backup-retention.js +83 -0
- package/template/coding-task-manager/lib/api/conversation-sync-routes.js +124 -0
- package/template/coding-task-manager/lib/api/diagnostics.js +549 -69
- package/template/coding-task-manager/lib/api/fleet.js +118 -0
- package/template/coding-task-manager/lib/api/hub-bridge.js +243 -0
- package/template/coding-task-manager/lib/api/ingress.js +9 -1
- package/template/coding-task-manager/lib/api/landing.js +403 -0
- package/template/coding-task-manager/lib/api/machines.js +691 -2
- package/template/coding-task-manager/lib/api/memory-sync-routes.js +335 -0
- package/template/coding-task-manager/lib/api/misc-ops.js +8 -3
- package/template/coding-task-manager/lib/api/remote-sessions.js +1326 -0
- package/template/coding-task-manager/lib/api/session-attach.js +623 -0
- package/template/coding-task-manager/lib/api/session-messages.js +1384 -0
- package/template/coding-task-manager/lib/api/sessions.js +77 -51
- package/template/coding-task-manager/lib/api/setup-network.js +544 -16
- package/template/coding-task-manager/lib/api/setup-wizard-handlers.js +884 -0
- package/template/coding-task-manager/lib/api/setup.js +829 -10
- package/template/coding-task-manager/lib/api/walle-bridge.js +9 -1
- package/template/coding-task-manager/lib/attachment-reconciler-wiring.js +5 -2
- package/template/coding-task-manager/lib/auth-rate-limit.js +23 -0
- package/template/coding-task-manager/lib/auth-rules.js +292 -21
- package/template/coding-task-manager/lib/background-stop.js +369 -0
- package/template/coding-task-manager/lib/best-of-attempts.js +4 -1
- package/template/coding-task-manager/lib/best-of-launch-plan.js +52 -0
- package/template/coding-task-manager/lib/box-keep-awake.js +89 -0
- package/template/coding-task-manager/lib/broadcast-reason-stats.js +152 -0
- package/template/coding-task-manager/lib/broadcast-serialize.js +5 -0
- package/template/coding-task-manager/lib/build-info.js +4 -2
- package/template/coding-task-manager/lib/claude-model-truth.js +13 -2
- package/template/coding-task-manager/lib/claude-transcript-terminal-renderer.js +17 -13
- package/template/coding-task-manager/lib/client-diag-handler.js +64 -2
- package/template/coding-task-manager/lib/codex-app-server-client.js +14 -16
- package/template/coding-task-manager/lib/codex-configuration-history.js +55 -0
- package/template/coding-task-manager/lib/codex-harness-qualification.js +43 -20
- package/template/coding-task-manager/lib/codex-rollout-history.js +743 -0
- package/template/coding-task-manager/lib/coding-agent-models.js +27 -12
- package/template/coding-task-manager/lib/coding-cli-skill-installer.js +44 -14
- package/template/coding-task-manager/lib/coding-session-model-adapters.js +43 -2
- package/template/coding-task-manager/lib/coding-session-model-owner.js +1 -1
- package/template/coding-task-manager/lib/coding-session-model-public.js +7 -1
- package/template/coding-task-manager/lib/coding-session-model-runtime.js +23 -4
- package/template/coding-task-manager/lib/coding-session-model-scan.js +31 -0
- package/template/coding-task-manager/lib/coding-session-model-state.js +7 -0
- package/template/coding-task-manager/lib/conversation-freshness-diagnostics.js +280 -0
- package/template/coding-task-manager/lib/conversation-item-clock.js +261 -0
- package/template/coding-task-manager/lib/conversation-source-registry.js +5 -0
- package/template/coding-task-manager/lib/conversation-store-v2.js +44 -0
- package/template/coding-task-manager/lib/conversation-sync.js +355 -0
- package/template/coding-task-manager/lib/create/resolve-launch-params.js +96 -12
- package/template/coding-task-manager/lib/create/worker-role-profile.js +4 -4
- package/template/coding-task-manager/lib/create-intent-lease.js +51 -0
- package/template/coding-task-manager/lib/ctm-session-context-api.js +97 -6
- package/template/coding-task-manager/lib/db-domain-tables.js +1 -0
- package/template/coding-task-manager/lib/db-domains.js +1 -0
- package/template/coding-task-manager/lib/db-owner-dispatch-probe.js +32 -0
- package/template/coding-task-manager/lib/db-owner-request-counts.js +178 -0
- package/template/coding-task-manager/lib/db-owner-worker-client.js +68 -14
- package/template/coding-task-manager/lib/db-schema-source-files.js +5 -0
- package/template/coding-task-manager/lib/deferred-restore-attempt.js +6 -2
- package/template/coding-task-manager/lib/feature-manifest.js +4 -0
- package/template/coding-task-manager/lib/fleet-conversation-route.js +97 -0
- package/template/coding-task-manager/lib/fleet-fanout.js +78 -0
- package/template/coding-task-manager/lib/fleet-hub.js +762 -0
- package/template/coding-task-manager/lib/fleet-origin-follower.js +252 -0
- package/template/coding-task-manager/lib/fleet-resolver-effects.js +147 -0
- package/template/coding-task-manager/lib/fleet-resolver-tick.js +148 -0
- package/template/coding-task-manager/lib/fleet-resolver.js +454 -0
- package/template/coding-task-manager/lib/fleet-session-list-routes.js +114 -0
- package/template/coding-task-manager/lib/fleet-session-lists.js +672 -0
- package/template/coding-task-manager/lib/fleet-update.js +798 -0
- package/template/coding-task-manager/lib/fs-watcher-root-watch.js +141 -0
- package/template/coding-task-manager/lib/fs-watcher.js +58 -42
- package/template/coding-task-manager/lib/harness-history-transition.js +50 -9
- package/template/coding-task-manager/lib/headless-term-service.js +84 -8
- package/template/coding-task-manager/lib/hermetic-agent-launch-guard.js +10 -0
- package/template/coding-task-manager/lib/history-transition-read-op.js +175 -8
- package/template/coding-task-manager/lib/host-cpu-categories.js +310 -0
- package/template/coding-task-manager/lib/host-fseventsd-sampler.js +399 -0
- package/template/coding-task-manager/lib/host-handoff.js +16 -19
- package/template/coding-task-manager/lib/host-stats.js +150 -3
- package/template/coding-task-manager/lib/hosted-sync-worker.js +43 -320
- package/template/coding-task-manager/lib/hub-bridge-access.js +798 -0
- package/template/coding-task-manager/lib/hub-bridge-box.js +409 -0
- package/template/coding-task-manager/lib/hub-bridge-mcp.js +380 -0
- package/template/coding-task-manager/lib/hub-bridge-mesh.js +313 -0
- package/template/coding-task-manager/lib/hub-bridge-ops.js +721 -0
- package/template/coding-task-manager/lib/hub-bridge.js +398 -0
- package/template/coding-task-manager/lib/idle-notify.js +491 -0
- package/template/coding-task-manager/lib/instance-id-ownership-manifest.js +8 -0
- package/template/coding-task-manager/lib/instance-manifest.js +5 -1
- package/template/coding-task-manager/lib/instance-sync-status.js +18 -8
- package/template/coding-task-manager/lib/landing/agent-rule.txt +1 -0
- package/template/coding-task-manager/lib/landing/batch-runner.js +366 -0
- package/template/coding-task-manager/lib/landing/config.js +536 -0
- package/template/coding-task-manager/lib/landing/ctm-view.js +256 -0
- package/template/coding-task-manager/lib/landing/gate.js +360 -0
- package/template/coding-task-manager/lib/landing/git.js +344 -0
- package/template/coding-task-manager/lib/landing/lease.js +247 -0
- package/template/coding-task-manager/lib/landing/merge-delta.js +380 -0
- package/template/coding-task-manager/lib/landing/proof.js +223 -0
- package/template/coding-task-manager/lib/landing/queue.js +998 -0
- package/template/coding-task-manager/lib/landing/radar.js +489 -0
- package/template/coding-task-manager/lib/landing/resolve-cli.js +424 -0
- package/template/coding-task-manager/lib/landing/state.js +263 -0
- package/template/coding-task-manager/lib/landing/stop.js +49 -0
- package/template/coding-task-manager/lib/landing/sync-cli.js +475 -0
- package/template/coding-task-manager/lib/landing/sync.js +456 -0
- package/template/coding-task-manager/lib/landing/train.js +885 -0
- package/template/coding-task-manager/lib/landing/window.js +57 -0
- package/template/coding-task-manager/lib/launch-environment.js +121 -0
- package/template/coding-task-manager/lib/launch-model-override.js +36 -10
- package/template/coding-task-manager/lib/launch-plan-pool.js +2 -1
- package/template/coding-task-manager/lib/launch-plan-warm-cache.js +6 -0
- package/template/coding-task-manager/lib/lead-model-observer.js +57 -1
- package/template/coding-task-manager/lib/lead-model-options.js +160 -3
- package/template/coding-task-manager/lib/lead-model-restore-reconciliation.js +106 -24
- package/template/coding-task-manager/lib/lead-model-switch-runtime.js +352 -68
- package/template/coding-task-manager/lib/lead-model-switch.js +114 -10
- package/template/coding-task-manager/lib/lead-model-transition-runtime.js +14 -2
- package/template/coding-task-manager/lib/lead-model-transition-store.js +24 -6
- package/template/coding-task-manager/lib/lead-route-preflight.js +179 -0
- package/template/coding-task-manager/lib/lead-switch-history-mode.js +110 -0
- package/template/coding-task-manager/lib/lead-wire-agent.js +7 -0
- package/template/coding-task-manager/lib/lifecycle-wake-experiment.js +9 -2
- package/template/coding-task-manager/lib/live-defer-announce.js +45 -0
- package/template/coding-task-manager/lib/machine-registry.js +750 -5
- package/template/coding-task-manager/lib/macos-capabilities.js +32 -15
- package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +433 -53
- package/template/coding-task-manager/lib/mainloop-observability.js +14 -9
- package/template/coding-task-manager/lib/media-draft-owner.js +16 -3
- package/template/coding-task-manager/lib/memory-join.js +337 -0
- package/template/coding-task-manager/lib/memory-sync-verify.js +393 -0
- package/template/coding-task-manager/lib/memory-sync.js +565 -0
- package/template/coding-task-manager/lib/migrations/v57-delegate-pref-mirror-opencode.js +68 -0
- package/template/coding-task-manager/lib/migrations/v58-subagent-launch-native-subagents.js +42 -0
- package/template/coding-task-manager/lib/migrations/v59-coding-session-model-state-dsh.js +73 -0
- package/template/coding-task-manager/lib/migrations/v60-delegate-pref-mirror-dsh.js +78 -0
- package/template/coding-task-manager/lib/migrations/v61-conversation-item-clock.js +71 -0
- package/template/coding-task-manager/lib/model-availability.js +727 -0
- package/template/coding-task-manager/lib/model-execution-recipe-metadata.js +4 -6
- package/template/coding-task-manager/lib/model-host-receipt.js +6 -3
- package/template/coding-task-manager/lib/model-id-rule.js +19 -0
- package/template/coding-task-manager/lib/model-proxy/anthropic-face.js +4 -0
- package/template/coding-task-manager/lib/model-proxy/auto-mode-review.js +110 -23
- package/template/coding-task-manager/lib/model-proxy/chat-response-translators.js +3 -0
- package/template/coding-task-manager/lib/model-proxy/generation-probe.js +60 -17
- package/template/coding-task-manager/lib/model-proxy/historical-tool-declarations.js +37 -0
- package/template/coding-task-manager/lib/model-proxy/lead-request-validation.js +1 -0
- package/template/coding-task-manager/lib/model-proxy/lead-token-admin.js +130 -0
- package/template/coding-task-manager/lib/model-proxy/refusal-scope.js +151 -0
- package/template/coding-task-manager/lib/model-proxy/request-budget-store.js +11 -2
- package/template/coding-task-manager/lib/model-proxy/tokens.js +69 -5
- package/template/coding-task-manager/lib/model-proxy/upstream-outcome.js +9 -0
- package/template/coding-task-manager/lib/native-agent-model-args.js +25 -22
- package/template/coding-task-manager/lib/native-history-control.js +6 -4
- package/template/coding-task-manager/lib/native-lead-settings-client.js +44 -4
- package/template/coding-task-manager/lib/oauth-proxy-supervisor.js +173 -36
- package/template/coding-task-manager/lib/online-backup-worker-client.js +2 -0
- package/template/coding-task-manager/lib/opencode-cli.js +480 -0
- package/template/coding-task-manager/lib/opencode-launch-model.js +471 -0
- package/template/coding-task-manager/lib/opencode-v1-plugin/model-carrier.mjs +35 -0
- package/template/coding-task-manager/lib/opencode-v2-plugin/server.js +118 -0
- package/template/coding-task-manager/lib/owner-action-manifest-data.json +1 -1
- package/template/coding-task-manager/lib/park-composer-draft.js +54 -0
- package/template/coding-task-manager/lib/park-fresh-relaunch.js +41 -10
- package/template/coding-task-manager/lib/perf-phase-trace.js +72 -0
- package/template/coding-task-manager/lib/perf-summary.js +7 -0
- package/template/coding-task-manager/lib/perf-tracker.js +63 -2
- package/template/coding-task-manager/lib/project-config.js +7 -0
- package/template/coding-task-manager/lib/prompt-purge-v2.js +52 -1
- package/template/coding-task-manager/lib/provider-login-broker.js +72 -15
- package/template/coding-task-manager/lib/provider-login-tab.js +10 -2
- package/template/coding-task-manager/lib/read-pool-client.js +89 -2
- package/template/coding-task-manager/lib/read-pool-worker-handle.js +29 -4
- package/template/coding-task-manager/lib/recent-catalog-query.js +15 -0
- package/template/coding-task-manager/lib/recent-session-catalog-response.js +67 -10
- package/template/coding-task-manager/lib/recovery-evidence.js +26 -11
- package/template/coding-task-manager/lib/relay-client.js +57 -3
- package/template/coding-task-manager/lib/relink-audit.js +34 -5
- package/template/coding-task-manager/lib/remote-machine-catalog.js +321 -26
- package/template/coding-task-manager/lib/remote-machine-create.js +576 -36
- package/template/coding-task-manager/lib/remote-machine-link.js +546 -59
- package/template/coding-task-manager/lib/remote-machine-proxy.js +143 -9
- package/template/coding-task-manager/lib/remote-session-forward.js +65 -7
- package/template/coding-task-manager/lib/remote-terminal-target.js +9 -2
- package/template/coding-task-manager/lib/restart-guard.js +18 -6
- package/template/coding-task-manager/lib/restore-seed-modes.js +87 -0
- package/template/coding-task-manager/lib/restored-scrollback-hydration-runtime.js +22 -2
- package/template/coding-task-manager/lib/resume-admission.js +17 -3
- package/template/coding-task-manager/lib/review-repo-registry.js +58 -19
- package/template/coding-task-manager/lib/root-delivery-runtime.js +2 -2
- package/template/coding-task-manager/lib/root-prompt-flow.js +1 -1
- package/template/coding-task-manager/lib/runtime-contract.js +3 -1
- package/template/coding-task-manager/lib/schema-indexes.js +1 -0
- package/template/coding-task-manager/lib/session-context-locator.js +742 -0
- package/template/coding-task-manager/lib/session-create/attachment.js +2 -2
- package/template/coding-task-manager/lib/session-create/create-phases.js +118 -0
- package/template/coding-task-manager/lib/session-create/delegate-worker.js +86 -16
- package/template/coding-task-manager/lib/session-create/dsh-session.js +930 -0
- package/template/coding-task-manager/lib/session-create/lead-route-launch.js +186 -13
- package/template/coding-task-manager/lib/session-create/native-lead-lifetime.js +55 -3
- package/template/coding-task-manager/lib/session-create/opencode-lead-provider.js +50 -0
- package/template/coding-task-manager/lib/session-create/prepared.js +162 -38
- package/template/coding-task-manager/lib/session-create/provider-process.js +160 -28
- package/template/coding-task-manager/lib/session-create/response.js +7 -7
- package/template/coding-task-manager/lib/session-create/structured-session.js +27 -0
- package/template/coding-task-manager/lib/session-create/subagent-pick-table.js +3 -1
- package/template/coding-task-manager/lib/session-create/subagent-pick.js +458 -9
- package/template/coding-task-manager/lib/session-create/transcript.js +1 -1
- package/template/coding-task-manager/lib/session-create/worker-budget-setup.js +21 -2
- package/template/coding-task-manager/lib/session-create/worker-continuation.js +7 -1
- package/template/coding-task-manager/lib/session-create/worker-launch-sandbox.js +39 -10
- package/template/coding-task-manager/lib/session-create/worker-observability.js +2 -2
- package/template/coding-task-manager/lib/session-empty.js +13 -10
- package/template/coding-task-manager/lib/session-host-manager.js +60 -0
- package/template/coding-task-manager/lib/session-jobs.js +3 -1
- package/template/coding-task-manager/lib/session-lifecycle.js +10 -4
- package/template/coding-task-manager/lib/session-model-action-preconditions.js +6 -2
- package/template/coding-task-manager/lib/session-model-intent-contract.js +11 -8
- package/template/coding-task-manager/lib/session-park.js +738 -0
- package/template/coding-task-manager/lib/session-path-grant.js +13 -4
- package/template/coding-task-manager/lib/session-status-ledger.js +106 -4
- package/template/coding-task-manager/lib/session-transport.js +17 -0
- package/template/coding-task-manager/lib/session-workspace-projection.js +54 -5
- package/template/coding-task-manager/lib/session-workspace-tracking-runtime.js +213 -1
- package/template/coding-task-manager/lib/skill-autocomplete-query.js +120 -3
- package/template/coding-task-manager/lib/skill-autocomplete.js +15 -1
- package/template/coding-task-manager/lib/sqlite-file-set.js +39 -0
- package/template/coding-task-manager/lib/standup-search-adapter.js +16 -2
- package/template/coding-task-manager/lib/state-sync/client-ledger.js +12 -0
- package/template/coding-task-manager/lib/state-sync/frame-emitter.js +319 -100
- package/template/coding-task-manager/lib/storage/coding-session-model-store.js +4 -1
- package/template/coding-task-manager/lib/storage/delegate-pref-mirror-store.js +32 -9
- package/template/coding-task-manager/lib/structured-session-boot-sweep.js +235 -0
- package/template/coding-task-manager/lib/submission-ack.js +90 -2
- package/template/coding-task-manager/lib/systemd-scope.js +115 -0
- package/template/coding-task-manager/lib/telemetry-receiver.js +39 -0
- package/template/coding-task-manager/lib/terminal-attach-geometry.js +985 -0
- package/template/coding-task-manager/lib/terminal-bootstrap.js +30 -0
- package/template/coding-task-manager/lib/terminal-draft-operation.js +357 -0
- package/template/coding-task-manager/lib/terminal-input-admission.js +17 -1
- package/template/coding-task-manager/lib/terminal-input-prepared.js +563 -0
- package/template/coding-task-manager/lib/transcript-cache-temps.js +171 -0
- package/template/coding-task-manager/lib/transcript-frame-cache.js +8 -3
- package/template/coding-task-manager/lib/transcript-source.js +36 -4
- package/template/coding-task-manager/lib/transcript-store.js +9 -0
- package/template/coding-task-manager/lib/typing-interaction-trace.js +106 -3
- package/template/coding-task-manager/lib/usage-ledger/backfill.js +40 -5
- package/template/coding-task-manager/lib/usage-ledger/client.js +4 -1
- package/template/coding-task-manager/lib/usage-ledger/dsh-parse.js +152 -0
- package/template/coding-task-manager/lib/usage-ledger/dsh-read.js +73 -0
- package/template/coding-task-manager/lib/usage-ledger/ingest.js +34 -6
- package/template/coding-task-manager/lib/usage-ledger/live.js +35 -8
- package/template/coding-task-manager/lib/usage-ledger/read.js +29 -20
- package/template/coding-task-manager/lib/usage-ledger/schema.js +61 -1
- package/template/coding-task-manager/lib/usage-ledger/task-coordinator.js +4 -2
- package/template/coding-task-manager/lib/wake-skip-reasons.js +1 -0
- package/template/coding-task-manager/lib/walle/turn.js +1 -1
- package/template/coding-task-manager/lib/walle-history-serve.js +838 -0
- package/template/coding-task-manager/lib/walle-kill-authority.js +2 -2
- package/template/coding-task-manager/lib/walle-log-rotation.js +164 -1
- package/template/coding-task-manager/lib/walle-mcp-auto-config.js +56 -2
- package/template/coding-task-manager/lib/walle-row-history.js +495 -0
- package/template/coding-task-manager/lib/walle-supervisor.js +33 -14
- package/template/coding-task-manager/lib/watch-worker-client.js +203 -0
- package/template/coding-task-manager/lib/worker-terminal-output.js +5 -2
- package/template/coding-task-manager/lib/ws/hub-bridge.js +35 -0
- package/template/coding-task-manager/lib/ws/lifecycle.js +6 -11
- package/template/coding-task-manager/lib/ws/misc.js +1 -1
- package/template/coding-task-manager/lib/ws/state-sync.js +106 -4
- package/template/coding-task-manager/lib/ws/terminal-attach.js +91 -0
- package/template/coding-task-manager/oauth-proxy.js +112 -115
- package/template/coding-task-manager/package-lock.json +10 -94
- package/template/coding-task-manager/package.json +4 -3
- package/template/coding-task-manager/protocol/message-table.json +10 -0
- package/template/coding-task-manager/protocol/messages.js +10 -0
- package/template/coding-task-manager/protocol/ws-schema.json +20 -0
- package/template/coding-task-manager/protocol/ws-schema.md +2 -0
- package/template/coding-task-manager/public/css/app.css +170 -0
- package/template/coding-task-manager/public/css/walle-session.css +4 -0
- package/template/coding-task-manager/public/index.html +473 -243
- package/template/coding-task-manager/public/js/agent-command.js +86 -0
- package/template/coding-task-manager/public/js/app/backups-panel.js +111 -5
- package/template/coding-task-manager/public/js/app/coding-agent-upgrade.js +103 -4
- package/template/coding-task-manager/public/js/app/machines-page.js +1328 -8
- package/template/coding-task-manager/public/js/app/models-tab.js +113 -38
- package/template/coding-task-manager/public/js/app/restart-banner.js +53 -0
- package/template/coding-task-manager/public/js/app/worktrees-panel.js +184 -2
- package/template/coding-task-manager/public/js/background-work.js +196 -27
- package/template/coding-task-manager/public/js/coding-session-model-presentation.js +138 -10
- package/template/coding-task-manager/public/js/core/active-session-groups.js +179 -0
- package/template/coding-task-manager/public/js/core/pending-create.js +238 -0
- package/template/coding-task-manager/public/js/core/recent-machine-source.js +228 -0
- package/template/coding-task-manager/public/js/core/session-commands.js +180 -14
- package/template/coding-task-manager/public/js/dashboard-client-function-order.json +8 -0
- package/template/coding-task-manager/public/js/document-review-links.js +9 -1
- package/template/coding-task-manager/public/js/file-resolution-client.js +52 -1
- package/template/coding-task-manager/public/js/history-app-seam.js +22 -2
- package/template/coding-task-manager/public/js/machine-pool-shared.js +558 -3
- package/template/coding-task-manager/public/js/message-renderer.js +4 -3
- package/template/coding-task-manager/public/js/model-picker.js +3 -1
- package/template/coding-task-manager/public/js/native-subagent-review.js +15 -2
- package/template/coding-task-manager/public/js/new-session-agent-grid.js +346 -0
- package/template/coding-task-manager/public/js/pasted-content.js +236 -0
- package/template/coding-task-manager/public/js/pe-private-content-lifetime.js +39 -7
- package/template/coding-task-manager/public/js/private-model-lifetime.js +6 -2
- package/template/coding-task-manager/public/js/prompts.js +11 -10
- package/template/coding-task-manager/public/js/protocol.js +10 -0
- package/template/coding-task-manager/public/js/restart-banner-policy.js +29 -1
- package/template/coding-task-manager/public/js/session-model-picker.js +447 -57
- package/template/coding-task-manager/public/js/session-search-utils.js +18 -2
- package/template/coding-task-manager/public/js/session-usage-view.js +2 -1
- package/template/coding-task-manager/public/js/session-workspace-tracking.js +51 -1
- package/template/coding-task-manager/public/js/state-sync-client.js +10 -1
- package/template/coding-task-manager/public/js/stream-view.js +6 -5
- package/template/coding-task-manager/public/js/terminal/conv-only-session.js +287 -0
- package/template/coding-task-manager/public/js/terminal/runtime-09.js +2 -0
- package/template/coding-task-manager/public/js/terminal/runtime-10.js +5 -5
- package/template/coding-task-manager/public/js/terminal/runtime-12.js +8 -1
- package/template/coding-task-manager/public/js/terminal/runtime-16.js +4 -0
- package/template/coding-task-manager/public/js/terminal/runtime-20.js +1 -1
- package/template/coding-task-manager/public/js/terminal-attachment-draft.js +67 -13
- package/template/coding-task-manager/public/js/terminal-image-orchestration.js +52 -6
- package/template/coding-task-manager/public/js/terminal-wheel.js +23 -3
- package/template/coding-task-manager/public/js/walle/chat-conversation.js +4 -4
- package/template/coding-task-manager/public/js/walle.js +6 -0
- package/template/coding-task-manager/public/js/ws/live-dispatch.js +10 -0
- package/template/coding-task-manager/public/m/app.css +140 -0
- package/template/coding-task-manager/public/m/app.js +507 -68
- package/template/coding-task-manager/public/m/index.html +17 -0
- package/template/coding-task-manager/public/m/machines.js +838 -4
- package/template/coding-task-manager/public/m/private-api.js +31 -1
- package/template/coding-task-manager/public/usage.html +1 -0
- package/template/coding-task-manager/scripts/dev-route-fixture/synthetic-route-upstream.js +104 -0
- package/template/coding-task-manager/scripts/dev-route-fixture.js +339 -0
- package/template/coding-task-manager/scripts/opencode-lead-route-probe.js +195 -0
- package/template/coding-task-manager/scripts/opencode-v2-contract-probe/fake-anthropic.js +168 -0
- package/template/coding-task-manager/scripts/opencode-v2-contract-probe/probe-plugin/server.js +95 -0
- package/template/coding-task-manager/scripts/opencode-v2-contract-probe.js +673 -0
- package/template/coding-task-manager/server.js +1950 -11679
- package/template/coding-task-manager/session-integrity.js +64 -4
- package/template/coding-task-manager/workers/db-owner-worker.js +19 -2
- package/template/coding-task-manager/workers/online-backup-worker.js +1 -0
- package/template/coding-task-manager/workers/read-pool-model-collector.js +51 -3
- package/template/coding-task-manager/workers/read-pool-worker.js +76 -6
- package/template/coding-task-manager/workers/session-host-pool-process.js +6 -6
- package/template/coding-task-manager/workers/session-host-process.js +5 -5
- package/template/coding-task-manager/workers/watch-worker.js +105 -0
- package/template/coding-task-manager/workers/worktree-worker.js +168 -33
- package/template/ctm-relay/lib/protocol.js +14 -1
- package/template/package-lock.json +2 -2
- package/template/package.json +1 -1
- package/template/shared/backup-retention.js +82 -0
- package/template/shared/conversation-admission.js +2 -0
- package/template/shared/conversation-provenance.js +3 -7
- package/template/shared/conversation-storage-v2-contract.js +5 -0
- package/template/shared/linked-worktree.js +51 -0
- package/template/shared/macos-fda-target.js +57 -0
- package/template/shared/orphan-parents.js +55 -0
- package/template/shared/release-schema.json +2 -2
- package/template/shared/storage/contract.js +3 -0
- package/template/wall-e/adapters/imessage.js +12 -0
- package/template/wall-e/agent.js +71 -4
- package/template/wall-e/api-walle.js +21 -1
- package/template/wall-e/bin/train-gemma-e4b-tooluse.js +2 -2
- package/template/wall-e/bin/walle-mcp-stdio.js +5 -5
- package/template/wall-e/brain/brain-join.js +859 -0
- package/template/wall-e/brain/brain-sync-membership.js +20 -0
- package/template/wall-e/brain/brain-sync-store.js +131 -77
- package/template/wall-e/brain/brain-sync-v2-schema.js +203 -0
- package/template/wall-e/brain/brain-sync-v2.js +578 -0
- package/template/wall-e/brain/delegate-model-prefs-store.js +13 -4
- package/template/wall-e/brain/deterministic-id.js +51 -0
- package/template/wall-e/brain/memories.js +2 -2
- package/template/wall-e/brain/migrations.js +40 -6
- package/template/wall-e/brain/model-identity-migration.js +1 -1
- package/template/wall-e/brain/owner-call-whitelist.js +23 -7
- package/template/wall-e/brain/schema.js +45 -85
- package/template/wall-e/brain.js +171 -35
- package/template/wall-e/chat.js +3 -3
- package/template/wall-e/coding/agent-shell-env.js +7 -2
- package/template/wall-e/coding/prompt-bundle.js +66 -21
- package/template/wall-e/config/main-loop-exemptions.json +2 -2
- package/template/wall-e/http/admin-api.js +37 -0
- package/template/wall-e/http/gws-api.js +4 -0
- package/template/wall-e/http/helper-rules-api.js +7 -1
- package/template/wall-e/http/model-admin.js +8 -1
- package/template/wall-e/http/model-delegate-pref-admin.js +3 -3
- package/template/wall-e/http/model-proxy-providers.js +174 -1
- package/template/wall-e/http/model-route-health.js +157 -0
- package/template/wall-e/http/sync-api.js +278 -464
- package/template/wall-e/jobs/job-runner.js +28 -1
- package/template/wall-e/lib/brain-owner-op-priority.js +9 -1
- package/template/wall-e/lib/ctm-data-dir.js +26 -0
- package/template/wall-e/lib/derivation-home.js +147 -0
- package/template/wall-e/lib/helper-rules/evaluate.js +50 -9
- package/template/wall-e/lib/helper-rules/grade.js +51 -8
- package/template/wall-e/lib/helper-rules/lead-call-hook.js +38 -5
- package/template/wall-e/lib/helper-rules/store.js +4 -2
- package/template/wall-e/lib/landing-mcp-tools.js +170 -0
- package/template/wall-e/lib/mcp-client-config.js +133 -14
- package/template/wall-e/lib/mcp-gateway-migrate.js +28 -64
- package/template/wall-e/lib/mcp-integration.js +58 -13
- package/template/wall-e/lib/parent-death.js +7 -1
- package/template/wall-e/lib/scheduler-worker-jobs.js +6 -4
- package/template/wall-e/llm/delegate-lead-prefs.js +16 -1
- package/template/wall-e/llm/delegate-vendor-default.js +12 -5
- package/template/wall-e/llm/model-roles.js +1 -1
- package/template/wall-e/llm/native-delegate-selection.js +56 -8
- package/template/wall-e/llm/worker-delegate-args.js +72 -7
- package/template/wall-e/llm/worker-mcp-routing.js +66 -17
- package/template/wall-e/loops/brain-custom-writes.js +19 -0
- package/template/wall-e/loops/ingest.js +17 -1
- package/template/wall-e/loops/think.js +19 -4
- package/template/wall-e/mcp-contract.js +222 -6
- package/template/wall-e/mcp-protocol-contract.js +40 -5
- package/template/wall-e/mcp-server.js +128 -29
- package/template/wall-e/mcp-worker-schemas.js +5 -5
- package/template/wall-e/memory/ctm-context-client.js +85 -0
- package/template/wall-e/memory/ctm-session-context.js +9 -3
- package/template/wall-e/memory/hub-bridge-client.js +388 -0
- package/template/wall-e/memory/replica-origin.js +56 -0
- package/template/wall-e/memory/session-pool.js +135 -0
- package/template/wall-e/runtime/live-capabilities.js +8 -3
- package/template/wall-e/server.js +4 -2
- package/template/wall-e/shared/release-schema.json +2 -2
- package/template/wall-e/shared/sqlite-file-set.js +39 -0
- package/template/wall-e/shared/storage/contract.js +3 -0
- package/template/wall-e/skills/_templates/delegate-worker/VERSION +1 -1
- package/template/wall-e/skills/_templates/delegate-worker/released-sha256.json +7 -1
- package/template/wall-e/skills/_templates/delegate-worker/templates/claude/SKILL.md +4 -4
- package/template/wall-e/skills/_templates/delegate-worker/templates/codex/SKILL.md +6 -6
- package/template/wall-e/skills/_templates/fleet-sync/VERSION +1 -0
- package/template/wall-e/skills/_templates/fleet-sync/released-sha256.json +16 -0
- package/template/wall-e/skills/_templates/fleet-sync/templates/claude/SKILL.md +64 -0
- package/template/wall-e/skills/_templates/fleet-sync/templates/codex/SKILL.md +64 -0
- package/template/wall-e/tools/chrome-automation.js +6 -0
- package/template/wall-e/training/setup-cuda-train-env.sh +2 -2
- package/template/wall-e/workers/brain-owner-worker.js +18 -4
- 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
|
+
};
|