create-walle 0.10.0 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -41
- package/lib/owner-write-safety.js +135 -0
- package/lib/public-runtime.js +13 -5
- package/lib/release-schema.json +3 -2
- package/lib/runtime-closure-policy.json +7 -5
- package/package.json +10 -4
- package/template/bin/deps-doctor.js +1 -1
- package/template/bin/node-bin.sh +11 -0
- package/template/coding-task-manager/api-prompts.js +10 -22
- package/template/coding-task-manager/bin/capture-codex-rollout-records.js +417 -0
- package/template/coding-task-manager/bin/census-codex-lead-history.js +325 -0
- package/template/coding-task-manager/bin/claude-hook.js +7 -3
- package/template/coding-task-manager/bin/claude-table-parity.js +307 -0
- package/template/coding-task-manager/bin/conversation-storage-auto-migration-lib.js +16 -1
- package/template/coding-task-manager/bin/conversation-storage-generation-evidence.js +9 -2
- package/template/coding-task-manager/bin/conversation-storage-service-entry-lib.js +16 -1
- package/template/coding-task-manager/bin/conversation-storage-walle-owner.js +247 -0
- package/template/coding-task-manager/bin/ctm-machine-pair.js +160 -0
- package/template/coding-task-manager/bin/lead-wait-census.js +1348 -0
- package/template/coding-task-manager/bin/repair-foreign-clone.js +564 -0
- package/template/coding-task-manager/bin/repair-resume-echo-titles.js +248 -0
- package/template/coding-task-manager/bin/seam-replay-codex.js +301 -0
- package/template/coding-task-manager/bin/write-lock-bench.js +111 -0
- package/template/coding-task-manager/db.js +705 -286
- package/template/coding-task-manager/git-utils.js +44 -0
- package/template/coding-task-manager/lib/adopted-host-redial.js +4 -0
- package/template/coding-task-manager/lib/agent-presets.js +15 -0
- package/template/coding-task-manager/lib/agent-title-apply.js +88 -0
- package/template/coding-task-manager/lib/agent-title-backfill-read.js +92 -0
- package/template/coding-task-manager/lib/api/host-stats.js +98 -0
- package/template/coding-task-manager/lib/api/ingress.js +35 -12
- package/template/coding-task-manager/lib/api/machine-pairing.js +285 -0
- package/template/coding-task-manager/lib/api/machine-pool.js +307 -0
- package/template/coding-task-manager/lib/api/machines.js +461 -0
- package/template/coding-task-manager/lib/api/misc-ops.js +20 -5
- package/template/coding-task-manager/lib/api/providers.js +53 -6
- package/template/coding-task-manager/lib/api/resume-from-store.js +25 -0
- package/template/coding-task-manager/lib/api/sessions.js +160 -8
- package/template/coding-task-manager/lib/api/transcript-history.js +17 -4
- package/template/coding-task-manager/lib/api/worktrees.js +38 -37
- package/template/coding-task-manager/lib/approval-drift.js +16 -1
- package/template/coding-task-manager/lib/approval-logical-frame.js +5 -1
- package/template/coding-task-manager/lib/approval-scope.js +68 -4
- package/template/coding-task-manager/lib/auth-context.js +32 -2
- package/template/coding-task-manager/lib/auth-rate-limit.js +41 -1
- package/template/coding-task-manager/lib/auth-rules.js +186 -45
- package/template/coding-task-manager/lib/broadcast-serialize.js +2 -1
- package/template/coding-task-manager/lib/browser-session-broker.js +9 -1
- package/template/coding-task-manager/lib/browser-use-adapter.js +33 -2
- package/template/coding-task-manager/lib/claude-markdown-ansi.js +27 -85
- package/template/coding-task-manager/lib/claude-session-title.js +6 -30
- package/template/coding-task-manager/lib/claude-table-layout.js +322 -0
- package/template/coding-task-manager/lib/client-diag-handler.js +25 -0
- package/template/coding-task-manager/lib/codex-config-guard.js +33 -3
- package/template/coding-task-manager/lib/codex-harness-qualification.js +12 -0
- package/template/coding-task-manager/lib/codex-history-terminal-renderer.js +33 -33
- package/template/coding-task-manager/lib/codex-launch-health.js +29 -2
- package/template/coding-task-manager/lib/codex-link-preparation.js +9 -8
- package/template/coding-task-manager/lib/codex-local-links.js +267 -0
- package/template/coding-task-manager/lib/codex-markdown-lists.js +160 -0
- package/template/coding-task-manager/lib/codex-native-history-prefix.js +4 -0
- package/template/coding-task-manager/lib/codex-paths.js +2 -1
- package/template/coding-task-manager/lib/codex-source-handoff.js +186 -0
- package/template/coding-task-manager/lib/coding-agent-models.js +21 -30
- package/template/coding-task-manager/lib/coding-session-model-adapters.js +27 -2
- package/template/coding-task-manager/lib/coding-session-model-projection.js +5 -0
- package/template/coding-task-manager/lib/coding-session-model-runtime.js +1 -2
- package/template/coding-task-manager/lib/coding-session-model-state.js +10 -0
- package/template/coding-task-manager/lib/coding-session-model-sync-state.js +0 -1
- package/template/coding-task-manager/lib/conversation-import/cursor-guard-owner.js +10 -0
- package/template/coding-task-manager/lib/create/resolve-launch-params.js +8 -1
- package/template/coding-task-manager/lib/ctm-boot-lifecycle.js +17 -1
- package/template/coding-task-manager/lib/db-domain-tables.js +7 -0
- package/template/coding-task-manager/lib/db-domains.js +21 -2
- package/template/coding-task-manager/lib/db-owner-op-registry.js +7 -0
- package/template/coding-task-manager/lib/db-owner-worker-client.js +22 -0
- package/template/coding-task-manager/lib/db-schema-source-files.js +1 -0
- package/template/coding-task-manager/lib/feature-manifest.js +1 -3
- package/template/coding-task-manager/lib/fs-watcher.js +3 -2
- package/template/coding-task-manager/lib/harness-history-transition.js +41 -14
- package/template/coding-task-manager/lib/headless-inflight-ops.js +203 -0
- package/template/coding-task-manager/lib/headless-missing-reply.js +42 -0
- package/template/coding-task-manager/lib/headless-term-service.js +60 -23
- package/template/coding-task-manager/lib/history-transition-read-op.js +179 -23
- package/template/coding-task-manager/lib/host-control-dial.js +167 -0
- package/template/coding-task-manager/lib/host-handoff-orchestrator.js +16 -2
- package/template/coding-task-manager/lib/host-handoff.js +51 -13
- package/template/coding-task-manager/lib/host-health-ledger.js +164 -12
- package/template/coding-task-manager/lib/host-self-truth-watchdog.js +4 -0
- package/template/coding-task-manager/lib/host-stats.js +328 -0
- package/template/coding-task-manager/lib/host-threadpool-evacuation.js +160 -0
- package/template/coding-task-manager/lib/host-threadpool-monitor.js +14 -1
- package/template/coding-task-manager/lib/hosted-input-dispatch.js +203 -0
- package/template/coding-task-manager/lib/hosted-sync-worker.js +113 -5
- package/template/coding-task-manager/lib/instance-id-ownership-manifest.js +4 -0
- package/template/coding-task-manager/lib/instance-manifest-runtime.js +3 -0
- package/template/coding-task-manager/lib/instance-manifest.js +13 -1
- package/template/coding-task-manager/lib/instance-sync-status.js +15 -0
- package/template/coding-task-manager/lib/kernel/diag-log.js +12 -0
- package/template/coding-task-manager/lib/launch-model-override.js +160 -1
- package/template/coding-task-manager/lib/launch-plan-worker.js +59 -2
- package/template/coding-task-manager/lib/lead-model-observer.js +17 -2
- package/template/coding-task-manager/lib/lead-model-switch-runtime.js +57 -4
- package/template/coding-task-manager/lib/lead-model-switch.js +11 -2
- package/template/coding-task-manager/lib/lead-model-transition-runtime.js +42 -4
- package/template/coding-task-manager/lib/lead-model-transition-store.js +61 -3
- package/template/coding-task-manager/lib/legacy-draft-cancellation-recovery.js +111 -0
- package/template/coding-task-manager/lib/login-tripwire.js +18 -0
- package/template/coding-task-manager/lib/machine-pairing.js +238 -0
- package/template/coding-task-manager/lib/machine-pool.js +409 -0
- package/template/coding-task-manager/lib/machine-registry.js +749 -0
- package/template/coding-task-manager/lib/machine-self.js +66 -0
- package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +56 -4
- package/template/coding-task-manager/lib/media-journal-control-reader.js +473 -0
- package/template/coding-task-manager/lib/media-journal.js +84 -27
- package/template/coding-task-manager/lib/media-store-options.js +43 -15
- package/template/coding-task-manager/lib/migrations/v56-host-samples-and-machine-pool.js +76 -0
- package/template/coding-task-manager/lib/mirror-feed-sanitize.js +12 -3
- package/template/coding-task-manager/lib/model-execution-recipe-metadata.js +16 -3
- package/template/coding-task-manager/lib/model-proxy/anthropic-face.js +4 -0
- package/template/coding-task-manager/lib/model-proxy/chat-response-translators.js +3 -0
- package/template/coding-task-manager/lib/model-proxy/collaboration-codec.js +94 -0
- package/template/coding-task-manager/lib/model-proxy/handoff.js +494 -0
- package/template/coding-task-manager/lib/model-proxy/lead-worker-picks.js +25 -0
- package/template/coding-task-manager/lib/model-proxy/native-lead-listener.js +61 -7
- package/template/coding-task-manager/lib/model-proxy/proxy-cli.js +200 -0
- package/template/coding-task-manager/lib/model-proxy/proxy-instance-record.js +257 -0
- package/template/coding-task-manager/lib/model-proxy/responses-face.js +24 -2
- package/template/coding-task-manager/lib/model-proxy/stream-accounting.js +205 -0
- package/template/coding-task-manager/lib/model-proxy/tokens.js +78 -0
- package/template/coding-task-manager/lib/model-proxy/upstream-outcome.js +9 -0
- package/template/coding-task-manager/lib/native-history-control.js +62 -14
- package/template/coding-task-manager/lib/native-lead-settings-client.js +2 -1
- package/template/coding-task-manager/lib/native-lead-tls.js +18 -4
- package/template/coding-task-manager/lib/oauth-proxy-supervisor.js +628 -64
- package/template/coding-task-manager/lib/owner-action-manifest-data.json +14 -0
- package/template/coding-task-manager/lib/painted-row-recorder.js +9 -1
- package/template/coding-task-manager/lib/prompt-admission-trace.js +69 -0
- package/template/coding-task-manager/lib/prompt-submission-contract.js +21 -1
- package/template/coding-task-manager/lib/provider-login-broker.js +192 -9
- package/template/coding-task-manager/lib/provider-login-capture.js +153 -0
- package/template/coding-task-manager/lib/queue-draft-status-overlay.js +36 -0
- package/template/coding-task-manager/lib/raw-codex-history-window.js +5 -3
- package/template/coding-task-manager/lib/raw-history-block.js +3 -1
- package/template/coding-task-manager/lib/read-pool-client.js +36 -24
- package/template/coding-task-manager/lib/read-pool-op-registry.js +10 -1
- package/template/coding-task-manager/lib/read-pool-telemetry.js +14 -5
- package/template/coding-task-manager/lib/recent-catalog-query.js +26 -0
- package/template/coding-task-manager/lib/recent-session-catalog-response.js +9 -0
- package/template/coding-task-manager/lib/remote-command-context.js +12 -0
- package/template/coding-task-manager/lib/remote-command-runtime.js +18 -5
- package/template/coding-task-manager/lib/remote-machine-catalog.js +1062 -0
- package/template/coding-task-manager/lib/remote-machine-create.js +693 -0
- package/template/coding-task-manager/lib/remote-machine-link.js +939 -0
- package/template/coding-task-manager/lib/remote-machine-proxy.js +520 -0
- package/template/coding-task-manager/lib/remote-session-forward.js +306 -0
- package/template/coding-task-manager/lib/remote-submission-status.js +6 -0
- package/template/coding-task-manager/lib/restart-outcome-truth.js +20 -3
- package/template/coding-task-manager/lib/restart-preflight.js +9 -2
- package/template/coding-task-manager/lib/restore-backoff-gate.js +10 -2
- package/template/coding-task-manager/lib/restore-prepare-retry.js +12 -5
- package/template/coding-task-manager/lib/restore-resume-batch.js +22 -1
- package/template/coding-task-manager/lib/resume-blocked-wire.js +62 -1
- package/template/coding-task-manager/lib/resume-cwd.js +13 -0
- package/template/coding-task-manager/lib/resume-from-store.js +12 -1
- package/template/coding-task-manager/lib/resume-origin-facts.js +38 -1
- package/template/coding-task-manager/lib/resume-preflight.js +21 -16
- package/template/coding-task-manager/lib/review-repo-admission.js +39 -6
- package/template/coding-task-manager/lib/review-repo-registry.js +17 -0
- package/template/coding-task-manager/lib/root-delivery-runtime.js +72 -15
- package/template/coding-task-manager/lib/root-delivery-selection.js +37 -1
- package/template/coding-task-manager/lib/root-prompt-admission.js +43 -11
- package/template/coding-task-manager/lib/root-prompt-flow.js +175 -38
- package/template/coding-task-manager/lib/scrollback-archive-store.js +7 -4
- package/template/coding-task-manager/lib/server-memory-sampler-runtime.js +10 -0
- package/template/coding-task-manager/lib/server-memory-sampler.js +41 -2
- package/template/coding-task-manager/lib/session-analysis-memory.js +2 -0
- package/template/coding-task-manager/lib/session-create/codex-native-provider-gate.js +218 -0
- package/template/coding-task-manager/lib/session-create/codex-resume-exit-classify.js +52 -0
- package/template/coding-task-manager/lib/session-create/delegate-worker.js +95 -6
- package/template/coding-task-manager/lib/session-create/identity.js +20 -1
- package/template/coding-task-manager/lib/session-create/prepared.js +69 -1
- package/template/coding-task-manager/lib/session-create/provider-launch-context.js +77 -0
- package/template/coding-task-manager/lib/session-create/provider-process.js +38 -39
- package/template/coding-task-manager/lib/session-create/response.js +30 -12
- package/template/coding-task-manager/lib/session-create/transcript.js +13 -4
- package/template/coding-task-manager/lib/session-create/worker-continuation.js +43 -1
- package/template/coding-task-manager/lib/session-create/worker-notices.js +94 -5
- package/template/coding-task-manager/lib/session-create/worker-observability.js +99 -7
- package/template/coding-task-manager/lib/session-create/worker-stop.js +11 -3
- package/template/coding-task-manager/lib/session-create/worker-wait-watcher.js +315 -0
- package/template/coding-task-manager/lib/session-geometry-authority-handler.js +10 -1
- package/template/coding-task-manager/lib/session-host-manager.js +152 -206
- package/template/coding-task-manager/lib/session-jobs.js +17 -9
- package/template/coding-task-manager/lib/session-lifecycle.js +20 -50
- package/template/coding-task-manager/lib/session-model-action-preconditions.js +68 -3
- package/template/coding-task-manager/lib/session-resource-sampler.js +67 -9
- package/template/coding-task-manager/lib/session-standup.js +67 -4
- package/template/coding-task-manager/lib/session-status-ledger.js +42 -3
- package/template/coding-task-manager/lib/session-status-runtime.js +45 -1
- package/template/coding-task-manager/lib/session-status.js +2 -2
- package/template/coding-task-manager/lib/session-title-mirror.js +25 -57
- package/template/coding-task-manager/lib/session-transcript-trash.js +8 -1
- package/template/coding-task-manager/lib/startup-pointer-write.js +57 -0
- package/template/coding-task-manager/lib/state-sync/painted-frame-field.js +11 -0
- package/template/coding-task-manager/lib/storage/coding-session-model-store.js +21 -22
- package/template/coding-task-manager/lib/surviving-owner-bootstrap.js +7 -2
- package/template/coding-task-manager/lib/surviving-owner-recovery.js +2 -1
- package/template/coding-task-manager/lib/terminal-draft-http-status.js +25 -0
- package/template/coding-task-manager/lib/transcript-archiver.js +229 -229
- package/template/coding-task-manager/lib/transcript-frame-cache.js +12 -2
- package/template/coding-task-manager/lib/transcript-guard-job.js +55 -4
- package/template/coding-task-manager/lib/transcript-link.js +77 -0
- package/template/coding-task-manager/lib/transcript-manifest-store.js +8 -4
- package/template/coding-task-manager/lib/transcript-source.js +8 -3
- package/template/coding-task-manager/lib/transcript-store.js +40 -0
- package/template/coding-task-manager/lib/tunnel-lane-governor.js +6 -3
- package/template/coding-task-manager/lib/typing-interaction-trace.js +136 -2
- package/template/coding-task-manager/lib/typing-worker-stages.js +208 -1
- package/template/coding-task-manager/lib/usage-ledger/claude-parse.js +53 -2
- package/template/coding-task-manager/lib/usage-ledger/ingest.js +13 -4
- package/template/coding-task-manager/lib/usage-ledger/read.js +14 -0
- package/template/coding-task-manager/lib/usage-ledger/schema.js +49 -1
- package/template/coding-task-manager/lib/usage-ledger/store.js +337 -6
- package/template/coding-task-manager/lib/usage-ledger/task-read.js +123 -5
- package/template/coding-task-manager/lib/visible-revision-watchdog.js +8 -12
- package/template/coding-task-manager/lib/wake-skip-reasons.js +46 -0
- package/template/coding-task-manager/lib/walle-supervisor.js +98 -14
- package/template/coding-task-manager/lib/worker-failure-classify.js +13 -1
- package/template/coding-task-manager/lib/ws/input.js +1 -0
- package/template/coding-task-manager/lib/ws/state-sync.js +19 -2
- package/template/coding-task-manager/lib/ws/terminal-attach.js +32 -1
- package/template/coding-task-manager/lib/ws-router.js +68 -1
- package/template/coding-task-manager/lib/ws-send-admission.js +22 -4
- package/template/coding-task-manager/oauth-proxy.js +99 -55
- package/template/coding-task-manager/package-lock.json +24 -0
- package/template/coding-task-manager/package.json +3 -0
- package/template/coding-task-manager/protocol/message-table.json +0 -10
- package/template/coding-task-manager/protocol/messages.js +0 -10
- package/template/coding-task-manager/protocol/ws-schema.json +0 -20
- package/template/coding-task-manager/protocol/ws-schema.md +0 -2
- package/template/coding-task-manager/providers/codex-mcp-extract.js +121 -19
- package/template/coding-task-manager/providers/codex-mcp.js +14 -5
- package/template/coding-task-manager/public/css/app.css +279 -2
- package/template/coding-task-manager/public/css/reviews.css +4 -0
- package/template/coding-task-manager/public/index.html +297 -240
- package/template/coding-task-manager/public/js/app/machines-page.js +1252 -0
- package/template/coding-task-manager/public/js/app/models-tab.js +546 -36
- package/template/coding-task-manager/public/js/app/worktrees-panel.js +7 -5
- package/template/coding-task-manager/public/js/background-work.js +13 -7
- package/template/coding-task-manager/public/js/client-health-core.js +81 -0
- package/template/coding-task-manager/public/js/client-health-main.js +61 -0
- package/template/coding-task-manager/public/js/coding-session-model-presentation.js +44 -4
- package/template/coding-task-manager/public/js/core/session-commands.js +676 -0
- package/template/coding-task-manager/public/js/core/session-selectors.js +5 -1
- package/template/coding-task-manager/public/js/history-app-seam.js +26 -2
- package/template/coding-task-manager/public/js/history-painted-rows.js +17 -2
- package/template/coding-task-manager/public/js/history-reader-park.js +13 -1
- package/template/coding-task-manager/public/js/history-scroller.js +187 -9
- package/template/coding-task-manager/public/js/history-transcript-pages.js +5 -5
- package/template/coding-task-manager/public/js/machine-pool-shared.js +436 -0
- package/template/coding-task-manager/public/js/private-state-scope.js +31 -3
- package/template/coding-task-manager/public/js/prompt-submission-client.js +173 -4
- package/template/coding-task-manager/public/js/prompt-submission-tray.js +281 -0
- package/template/coding-task-manager/public/js/protocol.js +0 -10
- package/template/coding-task-manager/public/js/queue-panel-submission-reconcile.js +174 -1
- package/template/coding-task-manager/public/js/reviews.js +7 -3
- package/template/coding-task-manager/public/js/root-draft-lifetime.js +139 -2
- package/template/coding-task-manager/public/js/root-queue-builder-lifetime.js +16 -0
- package/template/coding-task-manager/public/js/row-join.js +18 -3
- package/template/coding-task-manager/public/js/session-model-picker.js +1 -1
- package/template/coding-task-manager/public/js/session-search-utils.js +46 -19
- package/template/coding-task-manager/public/js/session-usage-view.js +1 -0
- package/template/coding-task-manager/public/js/state-sync-client.js +4 -0
- package/template/coding-task-manager/public/js/terminal/runtime-02.js +16 -7
- package/template/coding-task-manager/public/js/terminal/runtime-03.js +41 -0
- package/template/coding-task-manager/public/js/terminal/runtime-04.js +41 -4
- package/template/coding-task-manager/public/js/terminal/runtime-12.js +52 -2
- package/template/coding-task-manager/public/js/terminal/runtime-16.js +12 -12
- package/template/coding-task-manager/public/js/terminal/runtime-18.js +14 -0
- package/template/coding-task-manager/public/js/terminal/runtime-19.js +8 -4
- package/template/coding-task-manager/public/js/terminal/runtime-20.js +210 -73
- package/template/coding-task-manager/public/js/terminal/runtime-21.js +19 -6
- package/template/coding-task-manager/public/js/terminal/runtime-22.js +86 -0
- package/template/coding-task-manager/public/js/terminal-history-viewport.js +1 -3
- package/template/coding-task-manager/public/js/terminal-wheel.js +39 -2
- package/template/coding-task-manager/public/js/walle/chat-conversation.js +11 -3
- package/template/coding-task-manager/public/js/walle-session.js +30 -7
- package/template/coding-task-manager/public/m/app.css +226 -8
- package/template/coding-task-manager/public/m/app.js +502 -9
- package/template/coding-task-manager/public/m/index.html +38 -1
- package/template/coding-task-manager/public/m/machines.js +1313 -0
- package/template/coding-task-manager/public/m/pool-failover.js +391 -0
- package/template/coding-task-manager/public/m/private-queue-actions.js +26 -7
- package/template/coding-task-manager/public/m/sw.js +5 -0
- package/template/coding-task-manager/queue-engine.js +13 -0
- package/template/coding-task-manager/server/approvals.js +5 -6
- package/template/coding-task-manager/server/boot.js +136 -17
- package/template/coding-task-manager/server.js +964 -425
- package/template/coding-task-manager/session-integrity.js +7 -9
- package/template/coding-task-manager/workers/db-owner-worker.js +53 -7
- package/template/coding-task-manager/workers/read-pool-worker.js +17 -6
- package/template/coding-task-manager/workers/session-host-pool-process.js +18 -4
- package/template/coding-task-manager/workers/session-host-process.js +14 -3
- package/template/coding-task-manager/workers/transcript-archive-worker.js +1 -1
- package/template/coding-task-manager/workers/worktree-worker.js +34 -0
- package/template/package-lock.json +2 -2
- package/template/package.json +1 -1
- package/template/scripts/conversation-storage-v2-migration.js +78 -11
- package/template/shared/conversation-storage-v2-contract.js +42 -6
- package/template/shared/fixtures/worker-status-payloads.json +392 -0
- package/template/shared/git-remote-key.js +83 -0
- package/template/shared/host-identity.js +98 -2
- package/template/shared/owner-write-safety.js +135 -0
- package/template/shared/public-runtime.js +13 -5
- package/template/shared/release-schema.json +3 -2
- package/template/shared/report-citations.js +14 -0
- package/template/shared/sqlite-write-lock.js +361 -223
- package/template/shared/storage/contract.js +35 -3
- package/template/wall-e/agent.js +21 -0
- package/template/wall-e/api-walle.js +5 -0
- package/template/wall-e/bin/brain-purge-legacy-ctm.js +3 -2
- package/template/wall-e/bin/walle-mcp-stdio.js +12 -6
- package/template/wall-e/brain/helper-rules-schema.js +129 -0
- package/template/wall-e/brain/migrations.js +51 -22
- package/template/wall-e/brain/model-deployment-facts.js +31 -1
- package/template/wall-e/brain/owner-call-whitelist.js +16 -0
- package/template/wall-e/brain/schema-ensures.js +22 -0
- package/template/wall-e/brain.js +1 -1
- package/template/wall-e/coding/host-capabilities.js +2 -2
- package/template/wall-e/coding/journal-dispatcher.js +51 -0
- package/template/wall-e/coding/session-journal.js +28 -40
- package/template/wall-e/coding-orchestrator.js +23 -26
- package/template/wall-e/fly.toml +1 -1
- package/template/wall-e/http/auth.js +18 -1
- package/template/wall-e/http/blog-views-api.js +318 -0
- package/template/wall-e/http/helper-rules-api.js +321 -0
- package/template/wall-e/http/model-proxy-providers.js +3 -1
- package/template/wall-e/http/telemetry-api.js +12 -0
- package/template/wall-e/lib/blog-view-policy.js +88 -0
- package/template/wall-e/lib/blog-view-store.js +169 -0
- package/template/wall-e/lib/client-config-lock.js +2 -1
- package/template/wall-e/lib/coding-journal-writer-child-client.js +67 -18
- package/template/wall-e/lib/error-kinds.js +10 -1
- package/template/wall-e/lib/feature-manifest.js +1 -1
- package/template/wall-e/lib/helper-rules/cell-state.js +123 -0
- package/template/wall-e/lib/helper-rules/evaluate.js +357 -0
- package/template/wall-e/lib/helper-rules/explore-rubric.md +12 -0
- package/template/wall-e/lib/helper-rules/grade.js +277 -0
- package/template/wall-e/lib/helper-rules/label.js +247 -0
- package/template/wall-e/lib/helper-rules/lead-call-hook.js +522 -0
- package/template/wall-e/lib/helper-rules/store.js +667 -0
- package/template/wall-e/lib/mcp-integration.js +5 -6
- package/template/wall-e/lib/runtime-worker-pool.js +1 -0
- package/template/wall-e/lib/scheduler-worker-jobs.js +6 -0
- package/template/wall-e/lib/telemetry-db-schema.js +37 -0
- package/template/wall-e/lib/telemetry-process-client.js +76 -1
- package/template/wall-e/llm/default-fallback.js +4 -0
- package/template/wall-e/llm/model-catalog.js +13 -4
- package/template/wall-e/llm/model-roles.js +6 -0
- package/template/wall-e/llm/portkey-http.js +150 -0
- package/template/wall-e/llm/portkey-sync.js +15 -145
- package/template/wall-e/llm/worker-mcp-routing.js +61 -0
- package/template/wall-e/llm/worker-route.js +43 -1
- package/template/wall-e/loops/brain-custom-writes.js +45 -0
- package/template/wall-e/mcp-protocol-contract.js +41 -1
- package/template/wall-e/mcp-server.js +13 -2
- package/template/wall-e/mcp-worker-schemas.js +20 -2
- package/template/wall-e/memory/ctm-context-client.js +25 -5
- package/template/wall-e/shared/release-schema.json +3 -2
- package/template/wall-e/shared/sqlite-write-lock.js +361 -223
- package/template/wall-e/shared/storage/contract.js +35 -3
- package/template/wall-e/skills/_templates/delegate-worker/VERSION +1 -1
- package/template/wall-e/skills/_templates/delegate-worker/released-sha256.json +6 -0
- package/template/wall-e/skills/_templates/delegate-worker/templates/claude/SKILL.md +7 -2
- package/template/wall-e/skills/_templates/delegate-worker/templates/codex/SKILL.md +7 -2
- package/template/wall-e/tools/local-tools.js +12 -5
- package/template/wall-e/workers/brain-owner-worker.js +9 -0
- package/template/wall-e/workers/coding-journal-writer-child.js +8 -1
- package/template/wall-e/workers/telemetry-worker.js +8 -0
- package/template/coding-task-manager/lib/giant-freeze-park.js +0 -31
|
@@ -36,7 +36,8 @@ process.env.CTM_PROCESS_ROLE = process.env.CTM_PROCESS_ROLE || 'ctm-server';
|
|
|
36
36
|
// get a second tick to retry (see shared/storage/PERF_SPEC_R6.md V2).
|
|
37
37
|
process.env.CTM_DB_SHADOW_REPLAY = process.env.CTM_DB_SHADOW_REPLAY || '1';
|
|
38
38
|
const serverConfig = require('./lib/server-config');
|
|
39
|
-
const {
|
|
39
|
+
const { prepareMainMediaStore, createMediaJournalProjector } = require('./lib/media-store-options');
|
|
40
|
+
const { createLegacyDraftCancellationRecovery } = require('./lib/legacy-draft-cancellation-recovery');
|
|
40
41
|
const { runLegacyDraftRequestCancellation } = require('./lib/terminal-draft-request-cancellations');
|
|
41
42
|
try { require('./lib/timestamped-output').installTimestampedOutput(); } catch {}
|
|
42
43
|
|
|
@@ -66,7 +67,7 @@ const { DURABLE_KINDS } = require('../shared/conversation-admission');
|
|
|
66
67
|
const { scanTuiInputModes, tuiModesChanged } = require('./lib/tui-input-modes');
|
|
67
68
|
const { resumeFirstFrameWatchdogDecision } = require('./lib/resume-firstframe-watchdog');
|
|
68
69
|
const { waitProjection } = require('./lib/session-wait-projection');
|
|
69
|
-
const { normalizeStoreTurns, resumeBlockedWire, sendResumeBlockedToClients } = require('./lib/resume-blocked-wire');
|
|
70
|
+
const { normalizeStoreTurns, resumeBlockedRungWire, resumeBlockSetDetails, resumeBlockedWire, sendResumeBlockedToClients } = require('./lib/resume-blocked-wire');
|
|
70
71
|
const { persistCodexStartupResumeFromLink } = require('./lib/session-create/codex-startup-pointer');
|
|
71
72
|
const {
|
|
72
73
|
resumeHardCeilingEnabled,
|
|
@@ -102,6 +103,7 @@ const {
|
|
|
102
103
|
stripRepliesStateful,
|
|
103
104
|
writeWithDeviceReplyFirewall,
|
|
104
105
|
} = require('./lib/terminal-query-protocol');
|
|
106
|
+
const { observePtyWrite: _observePtyWrite } = require('./lib/hosted-input-dispatch');
|
|
105
107
|
const {
|
|
106
108
|
classifyPtyChunk,
|
|
107
109
|
ptyRuntimeActivityDecision,
|
|
@@ -197,10 +199,6 @@ const {
|
|
|
197
199
|
} = require('./lib/deferred-input-generation');
|
|
198
200
|
const { createResumeReadyEstimator } = require('./lib/resume-ready-history');
|
|
199
201
|
const { resumeTranscriptPaths } = require('./lib/resume-transcript-stat');
|
|
200
|
-
const {
|
|
201
|
-
DEFAULT_GIANT_ROLLOUT_BYTES,
|
|
202
|
-
shouldFreezeParkGiant,
|
|
203
|
-
} = require('./lib/giant-freeze-park');
|
|
204
202
|
|
|
205
203
|
const http = require('http');
|
|
206
204
|
const https = require('https');
|
|
@@ -228,6 +226,8 @@ function _readPerfCertificationLedger() {
|
|
|
228
226
|
} catch { return {}; } // expected: missing/corrupt ledger surfaces as empty, not a 500
|
|
229
227
|
}
|
|
230
228
|
const { createTypingInteractionTraceStore, noteWorkerStageMessage, pendingInteractionIdsFor } = require('./lib/typing-interaction-trace');
|
|
229
|
+
// WP-3 (§5 D-C2): the single-grapheme input class that can carry an echo prediction -- see lib/typing-worker-stages.js.
|
|
230
|
+
const { isSingleGraphemeEcho } = require('./lib/typing-worker-stages');
|
|
231
231
|
const { getWorkerLoopLag, buildRollupExtras, noteIfWorkerLoopLag } = require('./lib/state-sync/worker-loop-lag-state');
|
|
232
232
|
const { createVisibleRevisionWatchdog } = require('./lib/visible-revision-watchdog');
|
|
233
233
|
const { createClientDiagHandler } = require('./lib/client-diag-handler');
|
|
@@ -455,7 +455,7 @@ if (_shadowBootSafetyMs > 0) {
|
|
|
455
455
|
if (t && typeof t.unref === 'function') t.unref();
|
|
456
456
|
}
|
|
457
457
|
|
|
458
|
-
const { mirrorSessionTitle,
|
|
458
|
+
const { mirrorSessionTitle, applyProviderTitle } = require('./lib/session-title-mirror');
|
|
459
459
|
const streamResizePolicy = require('./public/js/stream-resize-policy');
|
|
460
460
|
const { turnSettleQuietResizeAction, deferredResizeReleaseDecision } = require('./lib/deferred-resize-quiet-policy');
|
|
461
461
|
const { rowsFlapResizeDecision, DEBOUNCE_MS: ROWS_FLAP_DEBOUNCE_MS, MAX_HOLD_MS: ROWS_FLAP_MAX_HOLD_MS } = require('./lib/rows-flap-debounce');
|
|
@@ -651,6 +651,8 @@ const { isCrashCapTripped: _isHostCrashCapTripped } = require('./lib/host-crash-
|
|
|
651
651
|
// _shutdownOrphanHost below).
|
|
652
652
|
const { createHostHealthLedger, readStallFromDataDir } = require('./lib/host-health-ledger');
|
|
653
653
|
const { createReviveUnresponsiveHost } = require('./lib/host-health-revive');
|
|
654
|
+
// WP-3 (stale-pty-host-evacuation design doc, §6): evacuate first, revive second.
|
|
655
|
+
const { createEvacuateStalledHost, evacuateStalledAdoptedHosts } = require('./lib/host-threadpool-evacuation');
|
|
654
656
|
const {
|
|
655
657
|
createPtyStartupReservation,
|
|
656
658
|
ptyStartupCommitComplete,
|
|
@@ -699,6 +701,7 @@ const _typingInteractionTraces = createTypingInteractionTraceStore({
|
|
|
699
701
|
publicLimit: 128,
|
|
700
702
|
ttlMs: 60_000,
|
|
701
703
|
getRollupExtras: () => buildRollupExtras(_loadPressure.getStatus()), // WP-0 D-4/D-5: report-only
|
|
704
|
+
mainSpansForWindow: (startMs, endMs) => _mainSpansForWindow(startMs, endMs), // WP-4 r4 R-8: report-only
|
|
702
705
|
// D-1 (composer-typing-freeze-keystroke-slo-2026-09-11.html §5 D-1, §8 WP-0): persist the
|
|
703
706
|
// keystroke-to-paint distribution past the 60s in-memory ring. _eventLoopIncidents is
|
|
704
707
|
// resolved lazily here (this callback only ever runs later, on the flush timer below) even
|
|
@@ -1507,17 +1510,7 @@ _perfTracker.namedInterval('timer:visible-revision-watchdog', () => {
|
|
|
1507
1510
|
recordDiagnostic: recordSessionDiagnostic,
|
|
1508
1511
|
coalescer: _visibleRevisionMarginalCoalescer,
|
|
1509
1512
|
});
|
|
1510
|
-
|
|
1511
|
-
const target = (session?.clients || []).find((client) => (
|
|
1512
|
-
client.clientId === incident.clientId
|
|
1513
|
-
&& Number(client.socketGeneration || 0) === Number(incident.socketGeneration || 0)
|
|
1514
|
-
));
|
|
1515
|
-
if (target?.readyState !== 1 || incident.recoveryAction === 'report-only') continue;
|
|
1516
|
-
// WP-3b (delivery-path reset): the 'fence-generation' recovery action used to fence the
|
|
1517
|
-
// client here (server-side belief the client's baseline generation is invalid); the client
|
|
1518
|
-
// fence is deleted, so this incident now only ever asks for a keyframe, same as any other
|
|
1519
|
-
// recovery action.
|
|
1520
|
-
_requestKeyframeGrant(target, incident.sessionId, { priority: true });
|
|
1513
|
+
// Report-only (lib/visible-revision-watchdog.js): a late boundary never forces a keyframe.
|
|
1521
1514
|
}
|
|
1522
1515
|
}, 25).unref?.();
|
|
1523
1516
|
// WP-3b (delivery-path reset): the fence-breaker sweep (lib/state-sync/fence-breaker.js) existed
|
|
@@ -1666,13 +1659,11 @@ const {
|
|
|
1666
1659
|
computeParkSweepMaxParks,
|
|
1667
1660
|
classifyWakeOutcome,
|
|
1668
1661
|
describePressureCause,
|
|
1669
|
-
evaluateFreeze,
|
|
1670
1662
|
evaluateManualPark,
|
|
1671
1663
|
evaluatePark,
|
|
1672
1664
|
lifecycleConfigFromEnv,
|
|
1673
1665
|
normalizeActivationSource,
|
|
1674
1666
|
normalizeClientKind,
|
|
1675
|
-
selectFreezeVictim,
|
|
1676
1667
|
selectParkVictim,
|
|
1677
1668
|
summarizeParkEligibility,
|
|
1678
1669
|
} = require('./lib/session-lifecycle');
|
|
@@ -2000,6 +1991,7 @@ const { splitMirrorAndBrowserChunks } = require('./lib/mirror-feed-sanitize');
|
|
|
2000
1991
|
const {
|
|
2001
1992
|
buildSessionStandupSnapshot,
|
|
2002
1993
|
isWalleOwnedSession,
|
|
1994
|
+
remoteStandupSession,
|
|
2003
1995
|
selectUnmatchedStartupTaskPlaceholders,
|
|
2004
1996
|
isStandupStartupTaskSeedEnabled,
|
|
2005
1997
|
} = require('./lib/session-standup');
|
|
@@ -2088,7 +2080,7 @@ const {
|
|
|
2088
2080
|
const {
|
|
2089
2081
|
createCodingAgentModelSync,
|
|
2090
2082
|
resolveModelToRegistry,
|
|
2091
|
-
|
|
2083
|
+
getCodexThreadAgentName,
|
|
2092
2084
|
} = require('./lib/coding-agent-models');
|
|
2093
2085
|
const { MODEL_RESULT_PAGE_SIZE, MAX_MODEL_RESULT_BYTES } = require('./lib/coding-session-model-projection'); const { createCodingSessionModelRuntime } = require('./lib/coding-session-model-runtime');
|
|
2094
2086
|
const { createCodingSessionGenerationRuntime } = require('./lib/coding-session-generation-runtime');
|
|
@@ -2145,6 +2137,10 @@ const { evaluateWaitState, shouldClearWaitCard, deriveChoiceLabel, shouldSuperse
|
|
|
2145
2137
|
const { selectionCardForViewport } = require('./lib/terminal-interaction-policy');
|
|
2146
2138
|
const promptIntent = require('./lib/prompt-intent');
|
|
2147
2139
|
const { canResumeAgent, getAgentCapabilities, getAgentSurfaceIds, normalizeAgentType } = require('./lib/agent-capabilities');
|
|
2140
|
+
// WP-4 (docs/remote-hub-followups-credential-projects-rows-idempotency-2026-10-23.html §5 D-12): the
|
|
2141
|
+
// SAME `created`-frame builder a fresh spawn uses, so a duplicate create is re-answered with one
|
|
2142
|
+
// frame shape, never a second one hand-rolled here.
|
|
2143
|
+
const { buildCreatedSessionPayload } = require('./lib/coding-session-model-public');
|
|
2148
2144
|
const { classifyResumeNoise, resumeFailureDetail } = require('./lib/resume-noise');
|
|
2149
2145
|
const { createResumeAttemptTracker } = require('./lib/resume-attempts');
|
|
2150
2146
|
const {
|
|
@@ -2194,6 +2190,60 @@ const resumeFromStoreApi = require('./lib/api/resume-from-store');
|
|
|
2194
2190
|
const servicesApi = require('./lib/api/services'); const devFaultApi = require('./lib/api/dev-fault');
|
|
2195
2191
|
const setupNetworkApi = require('./lib/api/setup-network'); const runtimeApprovalsApi = require('./lib/api/runtime-approvals');
|
|
2196
2192
|
const approvalDeliveryActionApi = require('./lib/api/approval-delivery-action');
|
|
2193
|
+
// WP-0 (remote-machine-hub-sessions-2026-10-22.html §6.1): the machine registry — routes in
|
|
2194
|
+
// lib/api/machines.js, policy/probe loop in lib/machine-registry.js.
|
|
2195
|
+
const machinesApi = require('./lib/api/machines');
|
|
2196
|
+
const machineRegistry = require('./lib/machine-registry');
|
|
2197
|
+
// WP-1 (machine-pool-picker-pairing-dashboard-2026-10-24.html §6.1): this machine's own host
|
|
2198
|
+
// stats. The sampler is ONE per process and is fed by the existing 60s resource tick (no new
|
|
2199
|
+
// timer); lib/api/host-stats.js is the read surface the picker and the hub's ?machine= proxy use.
|
|
2200
|
+
const { createHostStats } = require('./lib/host-stats');
|
|
2201
|
+
const hostStatsApi = require('./lib/api/host-stats');
|
|
2202
|
+
// WP-6 (remote-machine-hub-sessions-2026-10-22.html §6.6/S-3/S-4): the remote catalog. It is the
|
|
2203
|
+
// thin seam behind the two session-list projections below and the resume-on-origin branch; the
|
|
2204
|
+
// lookup, the merge and the wire fields all live in lib/remote-machine-catalog.js.
|
|
2205
|
+
const remoteMachineCatalog = require('./lib/remote-machine-catalog');
|
|
2206
|
+
// WP-6: the catalog's peers are wired HERE. Each of them transitively requires the catalog (the
|
|
2207
|
+
// link's relay hook, the create post-hook, the sync worker's probe hook all call into it), so the
|
|
2208
|
+
// catalog cannot `require` them without closing a lib/** dependency cycle (lint ceiling: 0). The
|
|
2209
|
+
// getters keep them lazy, so this changes no load order — the modules load on the catalog's first
|
|
2210
|
+
// use exactly as they did through the removed requires.
|
|
2211
|
+
// WP-3 (remote-hub-followups D-9/D-10): the catalog's change signal (lib/remote-machine-catalog.js
|
|
2212
|
+
// `_notifyChanged` — a box session-list change, or a sweep that flipped a machine's health). The
|
|
2213
|
+
// WS re-broadcast already rides it (`onChange` below); these two additions invalidate the HTTP
|
|
2214
|
+
// projections that never call `mergeSessionRows`: the materialized STANDUP snapshot (the phone home
|
|
2215
|
+
// list is built ONLY from /api/sessions/standup, so a box card would otherwise never appear or
|
|
2216
|
+
// disappear for the phone) and the published recent catalog (whose next build reads a fresh
|
|
2217
|
+
// `machineTagInputs()`). Recent rows come from the LOCAL DB and their tags depend only on the
|
|
2218
|
+
// registry, so the recent catalog is rebuilt only when the tag inputs change — never on every
|
|
2219
|
+
// probe sweep's box session-list refresh (a full read-pool rebuild per sweep).
|
|
2220
|
+
let _lastRecentMachineTagSignature = null;
|
|
2221
|
+
function _onRemoteMachineCatalogChanged() {
|
|
2222
|
+
_markStandupMaterializedDirty('remote-catalog');
|
|
2223
|
+
const signature = JSON.stringify(remoteMachineCatalog.machineTagInputs());
|
|
2224
|
+
if (signature === _lastRecentMachineTagSignature) return;
|
|
2225
|
+
_lastRecentMachineTagSignature = signature;
|
|
2226
|
+
_advanceRecentCatalogGeneration();
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
remoteMachineCatalog.configureProduction({
|
|
2230
|
+
link: () => require('./lib/remote-machine-link'),
|
|
2231
|
+
create: () => require('./lib/remote-machine-create'),
|
|
2232
|
+
proxy: () => require('./lib/remote-machine-proxy'),
|
|
2233
|
+
originFacts: () => require('./lib/resume-origin-facts'),
|
|
2234
|
+
db: () => require('./db-owner-client'),
|
|
2235
|
+
// debounced; an unchanged merge is elided by its signature. The getter is called exactly once per
|
|
2236
|
+
// _notifyChanged, so marking the standup snapshot and the recent catalog dirty here changes no
|
|
2237
|
+
// catalog notify semantics.
|
|
2238
|
+
onChange: () => { _onRemoteMachineCatalogChanged(); return broadcastSessionList; },
|
|
2239
|
+
// 2026-10-23 relay-hijack guard (lib/remote-machine-catalog.js _localSessionLookup): a session this
|
|
2240
|
+
// instance holds locally — live or a deferred boot placeholder — is never a box mirror.
|
|
2241
|
+
localSession: () => (ctmSessionId) => !!getSession(ctmSessionId),
|
|
2242
|
+
// WP-1 ingest fence (D-1/D-2): the HUB's own ownership truth — its instance id and its lineage
|
|
2243
|
+
// closure — which no row a box sends can forge. A missing/throwing peer fails OPEN.
|
|
2244
|
+
localInstanceId: () => () => dbModule.getCurrentHostedInstanceId?.() || '',
|
|
2245
|
+
ownerLineage: () => (instanceId) => dbModule.isSameOwnerLineage?.(instanceId) === true,
|
|
2246
|
+
});
|
|
2197
2247
|
const { sendConditionalJson } = require('./lib/api/conditional-json'), { manifestEtagBody: _manifestEtagBody } = require('./lib/instance-manifest-runtime'); // devtunnel-egress D-7: ETag/304 for the identity reads
|
|
2198
2248
|
const wsRouter = require('./lib/ws-router');
|
|
2199
2249
|
const { runWsMessageWithTrace } = require('./lib/ws-causal-context');
|
|
@@ -2494,7 +2544,7 @@ const {
|
|
|
2494
2544
|
loadConfig,
|
|
2495
2545
|
ensureWalleToken,
|
|
2496
2546
|
} = serverConfig;
|
|
2497
|
-
const providerLoginBroker = new ProviderLoginBroker({ configDir: CONFIG_DIR, createSession: (m) => handleCreatePrepared(null, m), getSession: (id) => getSession(id), resolveCli: (bin) => resolveAgentCliCommand(bin) }); // WP-1: sign-in opens a real terminal tab via the prepared-create entry point; all
|
|
2547
|
+
const providerLoginBroker = new ProviderLoginBroker({ configDir: CONFIG_DIR, createSession: (m) => handleCreatePrepared(null, m), getSession: (id) => getSession(id), resolveCli: (bin) => resolveAgentCliCommand(bin), writeToLoginTab: (s, d) => _writeLifecyclePty(s, d, 'provider-login-code') }); // WP-1: sign-in opens a real terminal tab via the prepared-create entry point; all args are hoisted functions, resolved at call time. The code-return write uses source 'provider-login-code', so it is CTM-generated (never 'input') and cannot move the user-recency clocks
|
|
2498
2548
|
const bestOfAttempts = createBestOfAttemptService({
|
|
2499
2549
|
statePath: path.join(CONFIG_DIR, 'best-of-attempts.json'),
|
|
2500
2550
|
launchAttempt: _launchBestOfAttempt,
|
|
@@ -2668,9 +2718,9 @@ const _diagBatcher = createSessionDiagnosticBatcher({
|
|
|
2668
2718
|
},
|
|
2669
2719
|
flush: (batch) => {
|
|
2670
2720
|
// Off-thread: route the durable batch write through the db-owner worker (the designated
|
|
2671
|
-
// serial writer) so the cross-process SQLite write
|
|
2672
|
-
//
|
|
2673
|
-
//
|
|
2721
|
+
// serial writer) so the cross-process SQLite write lock (and any wait for it) never blocks
|
|
2722
|
+
// the main event loop — the old link-claim lock was a ~110ms main-thread block under
|
|
2723
|
+
// multi-session contention. Best-effort: the in-memory ring still serves the UI.
|
|
2674
2724
|
_runCtmDbWorkerOp('flushSessionDiagnostics', { entries: batch }, { bestEffort: true })
|
|
2675
2725
|
.catch(() => {});
|
|
2676
2726
|
},
|
|
@@ -4890,7 +4940,7 @@ async function _launchBestOfAttempt(spec) {
|
|
|
4890
4940
|
const created = await handleCreatePrepared(null, {
|
|
4891
4941
|
id: sessionId,
|
|
4892
4942
|
title,
|
|
4893
|
-
label: title,
|
|
4943
|
+
label: title, titleIntent: 'default', // D-13: `_bestOfAttemptTitle` is CTM-composed (tier 3), never a user name
|
|
4894
4944
|
cmd: source.cmd || agentType,
|
|
4895
4945
|
args: bestOfFreshAgentArgs(agentType, source.args, { approvalHookToken: source._approvalHookToken || '' }),
|
|
4896
4946
|
cwd: workspace.cwd,
|
|
@@ -7478,19 +7528,11 @@ function _applyBootRepairsResult(repairs) {
|
|
|
7478
7528
|
const timer = setTimeout(continueWalleIdentityRekey, 250);
|
|
7479
7529
|
timer.unref?.();
|
|
7480
7530
|
}
|
|
7481
|
-
const repairedTitles = repairs?.titleRepair;
|
|
7482
|
-
if (repairedTitles?.promoted) {
|
|
7483
|
-
console.log(`[session-title] promoted ${repairedTitles.promoted} active startup title(s) into ctm_sessions.title`);
|
|
7484
|
-
telemetry.track('session_title_startup_repair', {
|
|
7485
|
-
promoted: repairedTitles.promoted,
|
|
7486
|
-
scanned: repairedTitles.scanned || 0,
|
|
7487
|
-
});
|
|
7488
|
-
}
|
|
7489
7531
|
return _refreshControlSnapshot({ reason: 'db-owner-ready' });
|
|
7490
7532
|
}
|
|
7491
7533
|
|
|
7492
7534
|
// WP-B / B1 (2026-07-31 boot-wedge outage): runBootRepairs performs REPAIRS
|
|
7493
|
-
// (
|
|
7535
|
+
// (codex identity/metadata repair, conversation-source
|
|
7494
7536
|
// migration, order-key backfill, Wall-E identity rekey) — none of it is
|
|
7495
7537
|
// load-bearing for serving HTTP. The old code awaited this with no
|
|
7496
7538
|
// try/catch, so a single rejection (that night: a worker-request timeout)
|
|
@@ -7857,7 +7899,10 @@ const sessionHostManager = createSessionHostManager({
|
|
|
7857
7899
|
// WP-0 (pty-host-threadpool-stranded-waiters-2026-09-25.html, D-3/D-6): report-only
|
|
7858
7900
|
// seam for the pool/dedicated-host threadpool monitor's frames -- see
|
|
7859
7901
|
// _recordThreadpoolDiagnostic above and apiServicesStatus's golden-signals feed below.
|
|
7860
|
-
|
|
7902
|
+
// WP-3 (§9 S-7): the record-only sink becomes the escalation's ingress too. `hostHealth` and
|
|
7903
|
+
// `_evacuateStalledHostSafe` are declared later in module evaluation -- this callback only ever
|
|
7904
|
+
// runs after it completes, and the function declaration below hoists.
|
|
7905
|
+
onThreadpoolReport: (snapshot) => { _recordThreadpoolDiagnostic(snapshot); const v = hostHealth.recordThreadpool(snapshot && snapshot.hostId, snapshot); if (v) _evacuateStalledHostSafe(v); },
|
|
7861
7906
|
// WP-1 (pty-host-threadpool-stranded-waiters-2026-09-25.html, D-2): the manager has no
|
|
7862
7907
|
// DB/session access of its own -- forward every input-ack frame to the one function
|
|
7863
7908
|
// that turns it into typing-trace/durable-phase truth.
|
|
@@ -7865,7 +7910,7 @@ const sessionHostManager = createSessionHostManager({
|
|
|
7865
7910
|
// Probe (mouse-mode handoff): host-reported per-session diagnostic rows (host-handoff-mirror-seed/-restored).
|
|
7866
7911
|
onSessionDiagnostic: (sessionId, event, details) => recordSessionDiagnostic(sessionId, event, details),
|
|
7867
7912
|
});
|
|
7868
|
-
_ownerRecovery = require('./lib/surviving-owner-bootstrap').createSurvivingOwnerBootstrap({ dataDir: require('./lib/data-dir').getDataDir(), bootId: _serverBootId, manager: sessionHostManager, getTasks: () => dbModule.listStartupTasks(), getSession: (id) => sessions.get(id), send: (ws, reply) => _wsSend(ws, JSON.stringify(reply)) });
|
|
7913
|
+
_ownerRecovery = require('./lib/surviving-owner-bootstrap').createSurvivingOwnerBootstrap({ dataDir: require('./lib/data-dir').getDataDir(), bootId: _serverBootId, manager: sessionHostManager, getTasks: () => dbModule.listStartupTasks(), getSession: (id) => sessions.get(id), send: (ws, reply) => _wsSend(ws, JSON.stringify(reply)), onSkip: _restoreWakeSkipSink }); // WP-3 (§9 S-20): wakeSkips('OwnerRecoveryFence'); the fence verdict is untouched
|
|
7869
7914
|
if (serverConfig.CTM_SESSION_HOST_PROCESS !== '0' && process.env.CTM_SESSION_HOST_POOL_PREWARM !== '0') {
|
|
7870
7915
|
try { sessionHostManager.prewarmPool?.(); }
|
|
7871
7916
|
catch (e) { console.error('[session-host-pool] prewarm error:', e.message); }
|
|
@@ -7876,8 +7921,16 @@ if (serverConfig.CTM_SESSION_HOST_PROCESS !== '0' && process.env.CTM_SESSION_HOS
|
|
|
7876
7921
|
// process globals inside lib/host-health-ledger.js itself); its default
|
|
7877
7922
|
// D-2(b) input is the WP-0 stall file a stalled pool host writes for its
|
|
7878
7923
|
// own hostId.
|
|
7924
|
+
// WP-3 (session-stops-mid-job-2026-10-24.html, §5 D-10/D-11): liveness is "the
|
|
7925
|
+
// host sent any frame". The manager's own per-host frame clock is this ledger's
|
|
7926
|
+
// one liveness input: a hard serialize miss on a host whose last frame is newer
|
|
7927
|
+
// than the request was issued is 'degraded' (readiness -- reported, never
|
|
7928
|
+
// killed), and only a host silent since the first missed request reaches
|
|
7929
|
+
// 'unresponsive'.
|
|
7879
7930
|
const hostHealth = createHostHealthLedger({
|
|
7880
7931
|
readStall: (hostId) => readStallFromDataDir(require('./lib/data-dir').getDataDir(), hostId),
|
|
7932
|
+
onEvacuate: (v) => _evacuateStalledHostSafe(v), // WP-3 (§9 S-7): the armed-timer half of the same escalation
|
|
7933
|
+
lastFrameAt: (hostId) => sessionHostManager.lastFrameAt(hostId),
|
|
7881
7934
|
});
|
|
7882
7935
|
// Final-review fix I6: a host that exits (normally OR by crash) must leave the
|
|
7883
7936
|
// ledger, or its miss history outlives it -- and a later host reusing the same
|
|
@@ -10460,41 +10513,25 @@ setDbMaintenanceRunner(runOnlineDbBackup);
|
|
|
10460
10513
|
setImageMetricsRecorder(event => _uploadMetrics.recordUpload(event));
|
|
10461
10514
|
// WP-0 §6.3/S-8: tpOldestMs on the [img-upload] line comes from the main-process monitor above.
|
|
10462
10515
|
setThreadpoolStatusReader(() => _mainThreadpoolMonitor.snapshot());
|
|
10516
|
+
const _legacyDraftCancellationRecovery = createLegacyDraftCancellationRecovery({
|
|
10517
|
+
getDirectory: () => path.join(serverConfig.CTM_DATA_DIR || path.dirname(dbModule.getDbPath()), 'media-journal'),
|
|
10518
|
+
getContext: () => _ctmDbClosingForProcessExit ? null : dbModule.getDb(),
|
|
10519
|
+
projectEntry: createMediaJournalProjector(_runCtmDbWorkerOp),
|
|
10520
|
+
});
|
|
10463
10521
|
async function _prepareMediaStore() {
|
|
10464
|
-
if (!_mediaWriteBehindEnabled) return null;
|
|
10522
|
+
if (!_mediaWriteBehindEnabled) { await _legacyDraftCancellationRecovery.prepare(); return null; }
|
|
10465
10523
|
if (_mediaStore) return _mediaStore;
|
|
10466
10524
|
if (_mediaStorePromise) return _mediaStorePromise;
|
|
10467
|
-
_mediaStorePromise = (
|
|
10468
|
-
|
|
10469
|
-
|
|
10470
|
-
// maintenance, while the journal projections below remain background work.
|
|
10471
|
-
priority: 'high',
|
|
10472
|
-
requestClass: 'maintenance',
|
|
10473
|
-
timeoutMs: 30_000,
|
|
10474
|
-
});
|
|
10475
|
-
return createMediaStore(buildMediaStoreOptions({
|
|
10476
|
-
dataDir: serverConfig.CTM_DATA_DIR || path.dirname(dbModule.getDbPath()),
|
|
10477
|
-
bootId: _serverBootId, registry, assertDraftOwner: _assertTerminalDraftOwnerAsync,
|
|
10525
|
+
_mediaStorePromise = prepareMainMediaStore({
|
|
10526
|
+
createStore: createMediaStore, getDataDir: () => serverConfig.CTM_DATA_DIR || path.dirname(dbModule.getDbPath()),
|
|
10527
|
+
bootId: _serverBootId, assertDraftOwner: _assertTerminalDraftOwnerAsync,
|
|
10478
10528
|
runDbWorkerOp: _runCtmDbWorkerOp,
|
|
10479
10529
|
onDrainProgress: () => { try { _loadPressure.recordProducerProgress('media-journal'); } catch {} },
|
|
10480
|
-
|
|
10481
|
-
// is the single draft version allocator. It recovers the highest pending journal version and
|
|
10482
|
-
// then applies verified owner-lineage normalization as one journaled mutation — the draft
|
|
10483
|
-
// restamp was removed from db.js restampOwnerLineage so the two can never allocate
|
|
10484
|
-
// concurrently.
|
|
10485
|
-
//
|
|
10486
|
-
// Review finding 9: the identity source is the LAZY, MEMOIZED accessor
|
|
10487
|
-
// (lib/instance-identity-access.js), not a synchronous main-thread `dbModule.…()` read on the
|
|
10488
|
-
// store-preparation path. getInstanceIdentity() derives at most once per process, only after
|
|
10489
|
-
// the DB is open, and returns null instead of ever inventing a hostname/port-derived default;
|
|
10490
|
-
// isSameOwnerLineage is the existing dbModule-free lineage snapshot used by the session-owner
|
|
10491
|
-
// hot path. Neither is a new DB boundary — both are already the reviewed identity seams.
|
|
10492
|
-
currentOwnerId: _instanceIdentityAccess.getInstanceIdentity({ required: false })?.instanceId
|
|
10530
|
+
getCurrentOwnerId: () => _instanceIdentityAccess.getInstanceIdentity({ required: false })?.instanceId
|
|
10493
10531
|
|| _instanceIdentityAccess.getCachedInstanceId()
|
|
10494
10532
|
|| serverConfig.CTM_INSTANCE_ID || '',
|
|
10495
10533
|
isSameOwnerLineage: _isSameOwnerLineageSnapshot,
|
|
10496
|
-
|
|
10497
|
-
})().then(store => {
|
|
10534
|
+
}).then(store => {
|
|
10498
10535
|
// Review finding 6: permanent-delete receipts observed while this store was preparing are
|
|
10499
10536
|
// applied FIRST — before `_mediaStore` is assigned and therefore before any reader (or the boot
|
|
10500
10537
|
// reconciler below) can observe the registry.
|
|
@@ -10517,6 +10554,7 @@ async function _prepareMediaStore() {
|
|
|
10517
10554
|
}
|
|
10518
10555
|
|
|
10519
10556
|
async function _closeMediaStore(timeoutMs = 2_000) {
|
|
10557
|
+
await _legacyDraftCancellationRecovery.close();
|
|
10520
10558
|
const store = _mediaStore;
|
|
10521
10559
|
if (!store) return;
|
|
10522
10560
|
_mediaStore = null;
|
|
@@ -10538,7 +10576,8 @@ setImageSaveRunner(async ({ promptId, buffer, filename, mimeType, deferRow, requ
|
|
|
10538
10576
|
dbModule.saveImageDecoupled(promptId, buffer, filename, mimeType, { deferRow: !!deferRow }));
|
|
10539
10577
|
});
|
|
10540
10578
|
|
|
10541
|
-
function _runLegacyTerminalDraftDbAction(action, args) {
|
|
10579
|
+
async function _runLegacyTerminalDraftDbAction(action, args) {
|
|
10580
|
+
await _legacyDraftCancellationRecovery.prepare();
|
|
10542
10581
|
return _runCtmDbWorkerOp('runDbMutation', { action, args }, {
|
|
10543
10582
|
priority: 'urgent',
|
|
10544
10583
|
requestClass: 'user-blocking',
|
|
@@ -11753,7 +11792,11 @@ function _auditAuthDecision({ req, auth, rule, decision, route, wsType, action,
|
|
|
11753
11792
|
function _rateLimitDecision(auth, rule, keyPath) {
|
|
11754
11793
|
if (auth?.isLoopback) return { ok: true };
|
|
11755
11794
|
const key = auth?.deviceId || auth?.tokenHashPrefix || 'anonymous';
|
|
11756
|
-
|
|
11795
|
+
// WP-3 (docs/remote-machine-hub-sessions-2026-10-22.html §6.4): the authenticated principal is
|
|
11796
|
+
// handed to the bucket selector so a `peer-control` hub relay is charged to the wide `peer`
|
|
11797
|
+
// bucket instead of `admin`/`mutation`. Every other caller's bucket is unchanged (rateKindForRule
|
|
11798
|
+
// ignores the argument without that scope).
|
|
11799
|
+
const kind = rateKindForRule(rule, keyPath, auth);
|
|
11757
11800
|
const decision = authRateLimiter.check(kind, key);
|
|
11758
11801
|
return decision.ok ? decision : { status: 429, ...decision };
|
|
11759
11802
|
}
|
|
@@ -14689,6 +14732,23 @@ async function _observeRemoteSubmission(context, attempt = 0) {
|
|
|
14689
14732
|
return;
|
|
14690
14733
|
}
|
|
14691
14734
|
|
|
14735
|
+
// The status-bus busy stamp is in memory; the durable match below resolves transcript files on
|
|
14736
|
+
// the read pool, which can queue for minutes under load (2026-10-21: a prompt the agent took up
|
|
14737
|
+
// 0.2 s after Enter stayed "Still confirming..." for 90 s behind resolveSessionFiles). An idle
|
|
14738
|
+
// submission is confirmed by the busy stamp the moment it exists. Mid-turn submissions are not:
|
|
14739
|
+
// the session was busy anyway (D-3).
|
|
14740
|
+
const submittedWhileGenerating = !!context.submittedWhileGenerating;
|
|
14741
|
+
if (!submittedWhileGenerating && Number(session?._lastBusyStatusAt || 0) >= Number(startedAt || 0)) {
|
|
14742
|
+
storeStatus('response_started', {
|
|
14743
|
+
reason: 'session_marked_busy_after_submit',
|
|
14744
|
+
message: `The agent started working after the ${submissionLabel} was submitted.`,
|
|
14745
|
+
attempts: attempt,
|
|
14746
|
+
source: submissionSource,
|
|
14747
|
+
evidenceSource: 'status-bus',
|
|
14748
|
+
});
|
|
14749
|
+
return;
|
|
14750
|
+
}
|
|
14751
|
+
|
|
14692
14752
|
const durableMatch = await _remoteSubmissionDurablePromptMatch(sessionId, text, {
|
|
14693
14753
|
minTimestampMs: Number(startedAt || 0) || 0,
|
|
14694
14754
|
timestampToleranceMs: 120000,
|
|
@@ -14712,7 +14772,6 @@ async function _observeRemoteSubmission(context, attempt = 0) {
|
|
|
14712
14772
|
// ONLY by its on-screen echo or a durable transcript row (checked above) —
|
|
14713
14773
|
// "status bus went busy" and "an agent progress line appeared" are both
|
|
14714
14774
|
// true anyway mid-turn, so they cannot be used as delivery evidence here.
|
|
14715
|
-
const submittedWhileGenerating = !!context.submittedWhileGenerating;
|
|
14716
14775
|
// Fix round 1, finding 3: the tri-state ('composer'/'echoed'/'absent', ❯-aware)
|
|
14717
14776
|
// drives ONLY the mid-turn path below. The idle path's `promptStillVisible`
|
|
14718
14777
|
// (fed into markerEvidence, enter-retry, and needs_attention) must keep using
|
|
@@ -14737,7 +14796,6 @@ async function _observeRemoteSubmission(context, attempt = 0) {
|
|
|
14737
14796
|
promptStillVisible,
|
|
14738
14797
|
});
|
|
14739
14798
|
const elapsedMs = Date.now() - Number(startedAt || Date.now());
|
|
14740
|
-
const busyAt = Number(session?._lastBusyStatusAt || 0) || 0;
|
|
14741
14799
|
|
|
14742
14800
|
if (!isCurrent()) return;
|
|
14743
14801
|
recordSessionDiagnostic(sessionId, 'remote-submission-observe-state', {
|
|
@@ -14780,17 +14838,6 @@ async function _observeRemoteSubmission(context, attempt = 0) {
|
|
|
14780
14838
|
return;
|
|
14781
14839
|
}
|
|
14782
14840
|
} else {
|
|
14783
|
-
if (busyAt >= Number(startedAt || 0)) {
|
|
14784
|
-
storeStatus('response_started', {
|
|
14785
|
-
reason: 'session_marked_busy_after_submit',
|
|
14786
|
-
message: `The agent started working after the ${submissionLabel} was submitted.`,
|
|
14787
|
-
attempts: attempt,
|
|
14788
|
-
source: submissionSource,
|
|
14789
|
-
evidenceSource: 'status-bus',
|
|
14790
|
-
});
|
|
14791
|
-
return;
|
|
14792
|
-
}
|
|
14793
|
-
|
|
14794
14841
|
if (legacyDetail.agentProgressAfterPrompt && elapsedMs >= 500) {
|
|
14795
14842
|
storeStatus('response_started', {
|
|
14796
14843
|
reason: 'terminal_progress_after_prompt',
|
|
@@ -17204,22 +17251,30 @@ async function handleApi(req, res, url) {
|
|
|
17204
17251
|
|
|
17205
17252
|
function apiGetProjects(req, res) {
|
|
17206
17253
|
const claudeProjectsDir = path.join(serverConfig.HOME, '.claude', 'projects');
|
|
17207
|
-
|
|
17254
|
+
|
|
17255
|
+
const send = (projects) => {
|
|
17256
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
17257
|
+
res.end(JSON.stringify(projects));
|
|
17258
|
+
};
|
|
17208
17259
|
|
|
17209
17260
|
if (fs.existsSync(claudeProjectsDir)) {
|
|
17210
|
-
|
|
17211
|
-
|
|
17212
|
-
|
|
17213
|
-
|
|
17214
|
-
|
|
17215
|
-
|
|
17216
|
-
|
|
17261
|
+
// Remote hub follow-ups WP-2 (D-5..D-8, §9 S-8/S-9): the naive `entry.replace(/-/g, '/')`
|
|
17262
|
+
// decode used to name a path that does not exist for most entries (Claude encodes EVERY
|
|
17263
|
+
// non-alphanumeric character as '-'), and every row was listed regardless. The lossless decode,
|
|
17264
|
+
// the existence stat, the gate-scratch filter and the normalized origin key all run in ONE
|
|
17265
|
+
// worktree-worker op now (214 ms cold for 540 entries — never on main). Row shape is unchanged:
|
|
17266
|
+
// {id, path, exists, gitRemoteKey}, with `exists` always true because a missing path is dropped.
|
|
17267
|
+
_runWorktreeWorkerOp('projectRows', { entries: readClaudeProjectEntries(claudeProjectsDir) })
|
|
17268
|
+
.then(({ rows } = {}) => send(Array.isArray(rows) ? rows : []), (error) => {
|
|
17269
|
+
console.warn('[api/projects] project row decode failed:', error?.message || error);
|
|
17270
|
+
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
17271
|
+
res.end(JSON.stringify({ ok: false, error: 'projects_unavailable' }));
|
|
17217
17272
|
});
|
|
17218
|
-
|
|
17273
|
+
return;
|
|
17219
17274
|
}
|
|
17220
17275
|
|
|
17221
|
-
|
|
17222
|
-
|
|
17276
|
+
// No ~/.claude/projects at all: today's empty answer, no worker round trip.
|
|
17277
|
+
send([]);
|
|
17223
17278
|
}
|
|
17224
17279
|
|
|
17225
17280
|
async function apiPathStatus(req, res) {
|
|
@@ -17335,6 +17390,22 @@ async function _handleClaudeWatcherNew(evt) {
|
|
|
17335
17390
|
const db = dbModule.getDomainDb('core');
|
|
17336
17391
|
const indexedOwner = (agentId) => db.prepare('SELECT agent_session_id, ctm_session_id FROM agent_sessions WHERE agent_session_id = ?').get(agentId)?.ctm_session_id || '';
|
|
17337
17392
|
const owner = indexedOwner(sessionId);
|
|
17393
|
+
// WP-0 (D-6): every early return between here and the owner's upsert drops that row's
|
|
17394
|
+
// transcript link silently. Name each skip so a missing link is diagnosable.
|
|
17395
|
+
const linkSkipped = (reason) => {
|
|
17396
|
+
if (!owner) return; // only an owner (claimed) row has a link to lose
|
|
17397
|
+
console.log(`[jsonl-watcher] link-skipped id=${String(sessionId).slice(0, 8)} reason=${reason}`);
|
|
17398
|
+
recordSessionDiagnostic(owner, 'transcript-link-skipped', { reason, filePath: String(filePath || '') });
|
|
17399
|
+
};
|
|
17400
|
+
// WP-1 (D-2): the link is an identity fact known before the observation — filePath + owner.
|
|
17401
|
+
// Write it first so an early return below (superseded / owner-changed / observation-null)
|
|
17402
|
+
// can no longer drop the row's transcript link. projectPath is deliberately omitted here:
|
|
17403
|
+
// upsertSession COALESCEs an empty project_path, so the metadata write below still owns it
|
|
17404
|
+
// (and this stays free of the decode path).
|
|
17405
|
+
if (owner) {
|
|
17406
|
+
await dbModule.upsertSession(owner, { agentSessionId: sessionId, provider: 'claude',
|
|
17407
|
+
jsonlPath: filePath, hostname: HOSTNAME });
|
|
17408
|
+
}
|
|
17338
17409
|
const collect = () => {
|
|
17339
17410
|
const candidates = [];
|
|
17340
17411
|
for (const [id, session] of sessions) {
|
|
@@ -17362,8 +17433,8 @@ async function _handleClaudeWatcherNew(evt) {
|
|
|
17362
17433
|
ctmSessionId: id, cwd: session.cwd || '', createdAt: session.createdAt,
|
|
17363
17434
|
})) },
|
|
17364
17435
|
});
|
|
17365
|
-
if (!prepared) return;
|
|
17366
|
-
if (indexedOwner(sessionId) !== owner) return;
|
|
17436
|
+
if (!prepared) { linkSkipped('observation-null'); return; }
|
|
17437
|
+
if (indexedOwner(sessionId) !== owner) { linkSkipped('owner-changed'); return; }
|
|
17367
17438
|
const { observation, projectPath, parsed } = prepared;
|
|
17368
17439
|
const metadata = () => currentMetadata() ? {
|
|
17369
17440
|
fileSize: observation.fileSize, compactBakSize: observation.compactBakSize, modifiedAt: observation.modifiedAt,
|
|
@@ -17372,7 +17443,7 @@ async function _handleClaudeWatcherNew(evt) {
|
|
|
17372
17443
|
if (currentMetadata()) _serverCompactBakSizeCache.seed(new Map([[observation.bak.path, observation.bak]]));
|
|
17373
17444
|
};
|
|
17374
17445
|
if (owner) {
|
|
17375
|
-
if (!currentMetadata()) return;
|
|
17446
|
+
if (!currentMetadata()) { linkSkipped('superseded'); return; }
|
|
17376
17447
|
await dbModule.upsertSession(owner, { agentSessionId: sessionId, provider: 'claude', projectPath,
|
|
17377
17448
|
jsonlPath: filePath, ...metadata(), hostname: HOSTNAME });
|
|
17378
17449
|
seedBak();
|
|
@@ -17404,8 +17475,7 @@ async function _handleClaudeWatcherNew(evt) {
|
|
|
17404
17475
|
seedBak();
|
|
17405
17476
|
broadcastToAll({ type: 'agent-linked', ctmSessionId: chosen.id, agentSessionId: sessionId });
|
|
17406
17477
|
if (_sessionStream) _sessionStream.linkSession(chosen.id, sessionId, filePath);
|
|
17407
|
-
|
|
17408
|
-
dir: projectPath, recordSessionDiagnostic });
|
|
17478
|
+
// WP-2 (D-6): the one-shot SDK title fill is deleted — the agent's own name arrives with the transcript ingest.
|
|
17409
17479
|
return;
|
|
17410
17480
|
}
|
|
17411
17481
|
if (!parsed) return;
|
|
@@ -17431,10 +17501,8 @@ async function _handleClaudeWatcherNew(evt) {
|
|
|
17431
17501
|
console.log(`[jsonl-watcher] Stitched compact-continuation ${sessionId.slice(0,8)} → ctm ${stitchedToCtmId.slice(0,8)}`);
|
|
17432
17502
|
broadcastToAll({ type: 'agent-linked', ctmSessionId: stitchedToCtmId, agentSessionId: sessionId, stitched: true });
|
|
17433
17503
|
if (_sessionStream) _sessionStream.linkSession(stitchedToCtmId, sessionId, filePath);
|
|
17434
|
-
} else {
|
|
17435
|
-
void fillClaudeAgentTitle({ db: dbModule, sessionId, provider: parsed.agent || 'claude', claudeSessionId: sessionId,
|
|
17436
|
-
dir: parsed.cwd || projectPath, recordSessionDiagnostic });
|
|
17437
17504
|
}
|
|
17505
|
+
// WP-2 (D-6/C-9): no title fill here — the continuation's own ingest resolves the predecessor's row as owner.
|
|
17438
17506
|
} catch (error) {
|
|
17439
17507
|
console.error('[jsonl-watcher] Error handling new file:', error.message);
|
|
17440
17508
|
} finally {
|
|
@@ -17460,11 +17528,15 @@ async function _handleClaudeWatcherChange(evt, transcriptDeltaCoalescer) {
|
|
|
17460
17528
|
const mtime = new Date(mtimeMs).toISOString();
|
|
17461
17529
|
const db = dbModule.getDomainDb('core');
|
|
17462
17530
|
let didShrink = false;
|
|
17531
|
+
let previous = null;
|
|
17463
17532
|
try {
|
|
17464
|
-
|
|
17533
|
+
previous = db.prepare('SELECT file_size, compact_bak_size, jsonl_path FROM agent_sessions WHERE agent_session_id = ?').get(sessionId);
|
|
17465
17534
|
const priorSize = previousJsonlSizeForShrink(previous);
|
|
17466
17535
|
didShrink = !!(previous && priorSize && size < priorSize);
|
|
17467
17536
|
} catch { /* metadata lookup failure retains the existing non-shrink policy */ }
|
|
17537
|
+
// WP-1 (D-3): re-link on every file-change while the row has no link. A file-change is proof
|
|
17538
|
+
// the transcript exists, so an unlinked row is repaired here even if its file-new was dropped.
|
|
17539
|
+
const relink = previous && !String(previous.jsonl_path || '').trim() ? filePath : '';
|
|
17468
17540
|
const statPath = claudeDesktopSessions.sourcePathForStat(filePath);
|
|
17469
17541
|
if (didShrink) _serverCompactBakSizeCache.clear(filePath, statPath);
|
|
17470
17542
|
if (_serverCompactBakSizeCache.peekFresh(filePath, statPath) === undefined) {
|
|
@@ -17479,9 +17551,14 @@ async function _handleClaudeWatcherChange(evt, transcriptDeltaCoalescer) {
|
|
|
17479
17551
|
if (!currentMetadata()) return;
|
|
17480
17552
|
const fileMeta = _claudeEffectiveFileMeta(filePath, size || 0);
|
|
17481
17553
|
await _runCtmDbWorkerOp('updateAgentSessionFileMetadata', {
|
|
17482
|
-
sessionId, fileSize: fileMeta.fileSize, compactBakSize: fileMeta.compactBakSize, modifiedAt: mtime,
|
|
17554
|
+
sessionId, fileSize: fileMeta.fileSize, compactBakSize: fileMeta.compactBakSize, modifiedAt: mtime, jsonlPath: relink,
|
|
17483
17555
|
});
|
|
17484
17556
|
if (!currentMetadata()) return;
|
|
17557
|
+
// WP-1 (D-3b): a no-jsonl-path park is the exact debt this write resolves — but only when the
|
|
17558
|
+
// row now has a link (just written, or already present). Other park causes stay parked.
|
|
17559
|
+
const debt = _conversationFreshnessDebt.get(sessionId);
|
|
17560
|
+
if (debt && debt.parked === true && /no-jsonl-path|E_NO_JSONL_PATH/.test(String(debt.cause || ''))
|
|
17561
|
+
&& (relink || String(previous?.jsonl_path || '').trim())) _clearConversationFreshnessDebt(sessionId);
|
|
17485
17562
|
publishWatcherFileChange({ sourceId: sessionId, ctmSessionId: sessionId, fileSize: fileMeta.fileSize,
|
|
17486
17563
|
mtimeMs: Number(mtimeMs || 0) });
|
|
17487
17564
|
if (didShrink && filePath && fileMeta.compactBakSize > 0) {
|
|
@@ -17781,6 +17858,10 @@ function _primaryApiAgentSession(row, agentSessions) {
|
|
|
17781
17858
|
return agentSessions.find(agent => agent.agentSessionId === primaryId) || agentSessions[0];
|
|
17782
17859
|
}
|
|
17783
17860
|
|
|
17861
|
+
// WP-3 (D-10): the ONE machine-tag rule for a recent row, shared with the CPU/worker projection
|
|
17862
|
+
// (lib/recent-catalog-query.js) so the legacy and worker outputs can never disagree.
|
|
17863
|
+
const { machineFactsForRecentRow: _machineFactsForRecentRow } = require('./lib/recent-catalog-query');
|
|
17864
|
+
|
|
17784
17865
|
function _recentSessionFromDbRow(row, options = {}) {
|
|
17785
17866
|
const agentSessions = _apiAgentSessionsFromDbRow(row, options);
|
|
17786
17867
|
const primaryAgent = _primaryApiAgentSession(row, agentSessions);
|
|
@@ -17833,6 +17914,9 @@ function _recentSessionFromDbRow(row, options = {}) {
|
|
|
17833
17914
|
parentAgentSessionId: row.parent_agent_session_id || primaryAgent?.parentAgentSessionId || '',
|
|
17834
17915
|
agentNickname: row.agent_nickname || primaryAgent?.agentNickname || '',
|
|
17835
17916
|
agentRole: row.agent_role || primaryAgent?.agentRole || '',
|
|
17917
|
+
// WP-3 (D-10): the machine tag, only when this row's execution host is a registered box — no
|
|
17918
|
+
// machines registered ⇒ no key ⇒ byte-identical to the pre-WP-3 projection (AC-3a).
|
|
17919
|
+
...(_machineFactsForRecentRow(row, options) || {}),
|
|
17836
17920
|
};
|
|
17837
17921
|
return sessionEmpty.applyRecentSessionEmptyState(session, row);
|
|
17838
17922
|
}
|
|
@@ -18175,6 +18259,9 @@ function _scheduleRecentCatalogBuild() {
|
|
|
18175
18259
|
hostname: HOSTNAME,
|
|
18176
18260
|
dbPath: dbModule.getDbPath(),
|
|
18177
18261
|
liveSessionIds: [...sessions.keys()],
|
|
18262
|
+
// WP-3 (D-10): main's cheap registry snapshot, so the worker can stamp machine/machineName
|
|
18263
|
+
// without any registry read of its own (the read-pool worker has no registry).
|
|
18264
|
+
machineTags: remoteMachineCatalog.machineTagInputs(),
|
|
18178
18265
|
}, {
|
|
18179
18266
|
timeoutMs: Math.max(5000, Number(process.env.CTM_RECENT_CATALOG_BUILD_TIMEOUT_MS) || 30_000),
|
|
18180
18267
|
telemetryReason: 'recent-catalog-publish',
|
|
@@ -18363,6 +18450,9 @@ async function _mapRecentSessionRows(rows) {
|
|
|
18363
18450
|
hostname: HOSTNAME,
|
|
18364
18451
|
sliceMs: _SESSION_CATALOG_SLICE_MS,
|
|
18365
18452
|
liveSessionIds: new Set(sessions.keys()),
|
|
18453
|
+
// WP-3 (D-10): this is the CPU projection the legacy path serves and persists from, so it
|
|
18454
|
+
// stamps the same machine tag the read-pool worker does (both from main's registry snapshot).
|
|
18455
|
+
machineTags: remoteMachineCatalog.machineTagInputs(),
|
|
18366
18456
|
});
|
|
18367
18457
|
}
|
|
18368
18458
|
// Shared read-pool boundary. onFail must serve cache/empty/degraded state; it must never run a
|
|
@@ -19941,6 +20031,7 @@ const _statusBindings = statusRuntime.createBindings({
|
|
|
19941
20031
|
restoreStarting: (session, now) => restoreRuntime.restoreStartingActive(session, now) || (_restoreInProgress && session._restoredFromStartup),
|
|
19942
20032
|
isLiveTerminal: (session) => _isLiveTerminalSession(session),
|
|
19943
20033
|
hasPendingWake: (session, now) => _sessionHasPendingScheduledWake(session, now),
|
|
20034
|
+
taskOutputPathFor: (s, id) => statusRuntime.claudeTaskOutputPathFor(s, id, { uid: process.getuid(), encodeClaudeProjectEntry }),
|
|
19944
20035
|
fsp: fs.promises, backgroundStore: require('./lib/status-background-store').createStore({ dir: path.join(require('./lib/data-dir').getDataDir(), 'status-background') }), isAdopted: (s) => s.ptyProcess?._adopted === true, pidOf: (s) => s.pid,
|
|
19945
20036
|
processEvidence: require('./lib/env-flags').boolFlag('CTM_STATUS_PROCESS_EVIDENCE', true), sampler: _sessionResourceSampler, resourceRoots: _lifecycleResourceRoots, liveSessions: _liveLifecycleSessions, getSession: (id) => sessions.get(id), baselineMs: () => _lifecycleConfig.resourceSampleMs,
|
|
19946
20037
|
dropWaitFingerprint: (s) => _waitStateDelete(s.id, s), probeIdle: (s) => checkIdleNotify(s.id, s, '', 250), clearWaitFields: (s) => _clearServerWaitingForInput(s.id, s), isBlockingWaitReason: (r) => _standupIsBlockingWaitingReason(r), evaluateWait: (id, opts) => reconcileSessionWaitState(id, opts).catch((error) => { errorSink.report('wait-reconcile:evaluate', error, { sessionId: id }); return { read: 'failed', kind: '', raised: false, cleared: false }; }), reconcileQuietMs: () => RECONCILE_QUIET_MS, pressureLevel: () => _lifecyclePressure.level, broadcastPressure: () => _broadcastLifecyclePressureStatus(),
|
|
@@ -19952,6 +20043,47 @@ const {
|
|
|
19952
20043
|
recordWaitCleared: _recordWaitCleared, syncWaitFields: _syncWaitFields, composerMayClose: _composerMayClose,
|
|
19953
20044
|
} = _statusBindings;
|
|
19954
20045
|
|
|
20046
|
+
// WP-1 (machine-pool-picker-pairing-dashboard-2026-10-24.html §6.1, D-15/D-16): the process's ONE
|
|
20047
|
+
// host-stats sampler. It is constructed HERE rather than beside _sessionResourceSampler because its
|
|
20048
|
+
// two injected readers close over the status bindings directly above and over _liveLifecycleSessions
|
|
20049
|
+
// (the accessor that owns the raw session map — session-access lint), so building it after them
|
|
20050
|
+
// removes any TDZ window rather than relying on record() only ever running post-boot.
|
|
20051
|
+
// countTasks (D-15): the SAME wire status the sidebar shows (liveStatus/verdictFor), never a new
|
|
20052
|
+
// heuristic — running ∪ stalled ∪ resuming → running, waiting_input → needsYou, idle → idle; plus
|
|
20053
|
+
// the worker count. A session with no status row ('' — non-live PTY, nothing running) lands in no
|
|
20054
|
+
// bucket, matching the pill it renders.
|
|
20055
|
+
// lastCreateAt (D-16): the newest create among LIVE sessions, 0 when none. Live-only is deliberate:
|
|
20056
|
+
// D-16's "use" is a session the machine is actually holding, so an exited tab cannot make the row
|
|
20057
|
+
// look freshly used.
|
|
20058
|
+
const _hostStats = createHostStats({
|
|
20059
|
+
db: dbModule,
|
|
20060
|
+
countTasks: () => {
|
|
20061
|
+
const now = Date.now();
|
|
20062
|
+
const tasks = { running: 0, needsYou: 0, idle: 0, workers: 0 };
|
|
20063
|
+
for (const session of _liveLifecycleSessions()) {
|
|
20064
|
+
if (isWorkerSessionObj(session)) tasks.workers += 1;
|
|
20065
|
+
let status = '';
|
|
20066
|
+
try {
|
|
20067
|
+
status = (_ledgerLiveStatus(session, _statusLedgerVerdict(session, now), now) || {}).status || '';
|
|
20068
|
+
} catch {
|
|
20069
|
+
status = ''; // REPORT-lite: a session mid-teardown has no status row; it counts in no bucket
|
|
20070
|
+
}
|
|
20071
|
+
if (status === 'running' || status === 'stalled' || status === 'resuming') tasks.running += 1;
|
|
20072
|
+
else if (status === 'waiting_input') tasks.needsYou += 1;
|
|
20073
|
+
else if (status === 'idle') tasks.idle += 1;
|
|
20074
|
+
}
|
|
20075
|
+
return tasks;
|
|
20076
|
+
},
|
|
20077
|
+
lastCreateAt: () => {
|
|
20078
|
+
let newest = 0;
|
|
20079
|
+
for (const session of _liveLifecycleSessions()) {
|
|
20080
|
+
const createdAt = Number(session.createdAt) || 0;
|
|
20081
|
+
if (createdAt > newest) newest = createdAt;
|
|
20082
|
+
}
|
|
20083
|
+
return newest;
|
|
20084
|
+
},
|
|
20085
|
+
});
|
|
20086
|
+
|
|
19955
20087
|
// A wheel-forwarded repaint is not agent output (the classifier's mouse veto, term-scroll-lag D-5).
|
|
19956
20088
|
function _mouseProvokedNow(session, now) {
|
|
19957
20089
|
return outputIsMouseProvoked({ lastMouseInputAtMs: session._lastMouseInputAt || 0, lastNonMouseInputAtMs: session._lastNonMouseInputAt || 0, nowMs: now });
|
|
@@ -20469,6 +20601,11 @@ async function _buildStandupSnapshot(options = {}) {
|
|
|
20469
20601
|
const standupPairs = [];
|
|
20470
20602
|
const now = Date.now();
|
|
20471
20603
|
const activeSessions = [];
|
|
20604
|
+
// WP-3 (D-9): the machine tag inputs, computed ONCE per rebuild (cheap: the registry's TTL
|
|
20605
|
+
// snapshot). With ZERO registered machines nothing below changes a byte — no key is stamped on a
|
|
20606
|
+
// local card and no remote card is appended — mirroring mergeSessionRows' byte-identity rule.
|
|
20607
|
+
const machineTags = remoteMachineCatalog.machineTagInputs();
|
|
20608
|
+
const machinesRegistered = machineTags.machines.length > 0;
|
|
20472
20609
|
|
|
20473
20610
|
// WP-A6: split the per-session work into a cheap synchronous pass (everything below is
|
|
20474
20611
|
// in-memory/sync) and the one slow await — the timeline-summary lookup. Sequentially
|
|
@@ -20544,6 +20681,9 @@ async function _buildStandupSnapshot(options = {}) {
|
|
|
20544
20681
|
waitingForInput: wallePendingPermission ? true : _isServerWaitingForInput(session.id, session),
|
|
20545
20682
|
waitingForInputAt: wallePendingPermission?.waitingForInputAt || session._waitingForInputAt || null,
|
|
20546
20683
|
waitingReason: wallePendingPermission?.waitingReason || session._waitingForInputReason || '',
|
|
20684
|
+
// WP-3 (D-9): a LOCAL card carries the three routing fields EMPTY — but only once a machine is
|
|
20685
|
+
// registered, so a single-machine hub's card keeps its exact pre-WP-3 key set (AC-3a).
|
|
20686
|
+
...(machinesRegistered ? { machine: '', machineName: '', machineHealth: '' } : {}),
|
|
20547
20687
|
};
|
|
20548
20688
|
activeSessions.push(standupSession);
|
|
20549
20689
|
standupPairs.push({ session: standupSession, summary });
|
|
@@ -20553,13 +20693,13 @@ async function _buildStandupSnapshot(options = {}) {
|
|
|
20553
20693
|
// matches the sidebar's (DB-backed, /api/recent-sessions) from the very first snapshot after
|
|
20554
20694
|
// boot, instead of only catching up ~13s later as the in-memory map fills (R1/R5). Read
|
|
20555
20695
|
// dynamically (not cached at module load) so the kill switch takes effect without a
|
|
20556
|
-
// restart.
|
|
20557
|
-
//
|
|
20558
|
-
//
|
|
20696
|
+
// restart. These are the only synchronous DB reads in the whole rebuild — the task list plus ONE
|
|
20697
|
+
// batched durable-title read (WP-5/D-13: the sidebar's ctm_sessions title) — inside the
|
|
20698
|
+
// materialized rebuild (already off the hot per-request path; see _standupSnapshotForRequest).
|
|
20559
20699
|
if (isStandupStartupTaskSeedEnabled(process.env)) {
|
|
20560
20700
|
try {
|
|
20561
20701
|
const startupTasks = dbModule.listStartupTasks();
|
|
20562
|
-
for (const placeholder of selectUnmatchedStartupTaskPlaceholders(activeSessions, startupTasks)) {
|
|
20702
|
+
for (const placeholder of selectUnmatchedStartupTaskPlaceholders(activeSessions, startupTasks, dbModule.getSessionTitlesByIds)) {
|
|
20563
20703
|
activeSessions.push(placeholder);
|
|
20564
20704
|
standupPairs.push({ session: placeholder, summary: null });
|
|
20565
20705
|
}
|
|
@@ -20568,6 +20708,26 @@ async function _buildStandupSnapshot(options = {}) {
|
|
|
20568
20708
|
}
|
|
20569
20709
|
}
|
|
20570
20710
|
|
|
20711
|
+
// WP-3 (D-9): the box sessions. A LOCAL id always wins (the same rule mergeSessionRows enforces):
|
|
20712
|
+
// an id already present — a live session or a startup-task placeholder — is never re-added. A
|
|
20713
|
+
// remote card is classified from the box row's OWN status fields (lib/session-standup.js
|
|
20714
|
+
// remoteStandupSession); no local PTY helper is ever consulted for a session this host does not
|
|
20715
|
+
// own, and it carries no local stream summary (summary: null). Deliberately NOT pushed into
|
|
20716
|
+
// standupPairs: attention classification reads local scrollback/terminal state by session id.
|
|
20717
|
+
if (machinesRegistered) {
|
|
20718
|
+
const seenIds = new Set();
|
|
20719
|
+
for (const session of activeSessions) {
|
|
20720
|
+
const id = String(session?.id || session?.sessionId || '').trim();
|
|
20721
|
+
if (id) seenIds.add(id);
|
|
20722
|
+
}
|
|
20723
|
+
for (const row of remoteMachineCatalog.remoteRows()) {
|
|
20724
|
+
const id = String(row?.id || row?.sessionId || '').trim();
|
|
20725
|
+
if (!id || seenIds.has(id)) continue;
|
|
20726
|
+
seenIds.add(id);
|
|
20727
|
+
activeSessions.push(remoteStandupSession(row));
|
|
20728
|
+
}
|
|
20729
|
+
}
|
|
20730
|
+
|
|
20571
20731
|
if (includeAttention) await _applyStandupAttention(standupPairs, now);
|
|
20572
20732
|
|
|
20573
20733
|
const snapshot = buildSessionStandupSnapshot({
|
|
@@ -20949,6 +21109,11 @@ async function apiRecentSessions(req, res, url) {
|
|
|
20949
21109
|
async function _apiRecentSessionsLegacy(req, res, url, options = {}) {
|
|
20950
21110
|
const limit = parseInt(url.searchParams.get('limit') || '50', 10);
|
|
20951
21111
|
const forceRescan = url.searchParams.get('rescan') === '1';
|
|
21112
|
+
// WP-3 (D-10): main's cheap registry snapshot for the DB-only projection below — the same input
|
|
21113
|
+
// the read-pool op carries, so the legacy and published projections stamp identically. Deliberately
|
|
21114
|
+
// read HERE, never in apiRecentSessions (tests/r4-9-recent-sessions-caps.test.js:144 pins that
|
|
21115
|
+
// handler body to a pure published-buffer lookup).
|
|
21116
|
+
const machineTags = remoteMachineCatalog.machineTagInputs();
|
|
20952
21117
|
|
|
20953
21118
|
// First access after (a cold) boot: seed the in-memory cache from the persisted snapshot so this
|
|
20954
21119
|
// request serves the last good list immediately instead of an empty one. It's marked stale, so
|
|
@@ -21117,7 +21282,7 @@ async function _apiRecentSessionsLegacy(req, res, url, options = {}) {
|
|
|
21117
21282
|
if (_isProviderChildSessionRow(row)) continue;
|
|
21118
21283
|
if (existingSessionIds.has(row.id)) continue;
|
|
21119
21284
|
if (dbRowAgentSessionIds(row).some(agentId => existingSessionIds.has(agentId))) continue;
|
|
21120
|
-
const session = _recentSessionFromDbRow(row, { fileStats });
|
|
21285
|
+
const session = _recentSessionFromDbRow(row, { fileStats, machineTags });
|
|
21121
21286
|
if (!_shouldIncludeDbOnlyRecentSession(row, session, fileStats)) continue;
|
|
21122
21287
|
result.push(session);
|
|
21123
21288
|
existingSessionIds.add(row.id);
|
|
@@ -24860,15 +25025,21 @@ function _resolveCodexOwnerCtmSessionId(requestedSessionId, agentSessionId) {
|
|
|
24860
25025
|
return requestedSessionId || agentSessionId || '';
|
|
24861
25026
|
}
|
|
24862
25027
|
|
|
24863
|
-
|
|
24864
|
-
|
|
24865
|
-
|
|
24866
|
-
|
|
24867
|
-
|
|
24868
|
-
|
|
24869
|
-
|
|
24870
|
-
}
|
|
24871
|
-
|
|
25028
|
+
// WP-4/D-10: read the Codex thread's OWN name (threads.name) and apply it through the shared
|
|
25029
|
+
// applyProviderTitle path (tier rules live there, never here) — the ONE entry point for the link
|
|
25030
|
+
// path and every Codex rollout event, never a timer. Main-thread-only effects: live label, the
|
|
25031
|
+
// startup-task label and one session-list broadcast.
|
|
25032
|
+
function _refreshCodexThreadName(ref, liveSession = findLiveSessionByAnyId(ref)) {
|
|
25033
|
+
const threadId = String(liveSession?._claudeSessionId || liveSession?._providerResumeId || '');
|
|
25034
|
+
const agentName = threadId ? getCodexThreadAgentName(threadId) : '';
|
|
25035
|
+
if (!liveSession || !agentName) return { applied: 'none' };
|
|
25036
|
+
const outcome = applyProviderTitle({ db: dbModule, sessionId: liveSession.id, provider: 'codex', agentName });
|
|
25037
|
+
if (outcome.applied !== 'agent') return outcome;
|
|
25038
|
+
const label = `Codex: ${agentName}`;
|
|
25039
|
+
if (liveSession.label !== label) { liveSession.label = label; _invalidateTitleInfoCache(liveSession); }
|
|
25040
|
+
void _detachOwnerMutation('agent-title-label', () => dbModule.updateStartupTaskLabel(liveSession.id, agentName));
|
|
25041
|
+
broadcastSessionList();
|
|
25042
|
+
return outcome;
|
|
24872
25043
|
}
|
|
24873
25044
|
|
|
24874
25045
|
function _shouldReclaimStandaloneAgentOwner(agentSessionId, ctmSessionId) {
|
|
@@ -25005,15 +25176,9 @@ function _linkCodexThreadToSession(ctmSessionId, liveSession, thread, source, re
|
|
|
25005
25176
|
}
|
|
25006
25177
|
|
|
25007
25178
|
const title = _codexThreadTitle(thread);
|
|
25008
|
-
|
|
25009
|
-
|
|
25010
|
-
|
|
25011
|
-
liveSession.label = `Codex: ${title}`;
|
|
25012
|
-
liveSession._codexTitleSynced = true;
|
|
25013
|
-
} else if (title) {
|
|
25014
|
-
liveSession._codexTitleSynced = true;
|
|
25015
|
-
}
|
|
25016
|
-
} catch {}
|
|
25179
|
+
// WP-4/D-10: the thread's OWN name (threads.name) is read at link through the shared
|
|
25180
|
+
// applyProviderTitle path; no `_codexTitleSynced` latch is set from the index title any more (S-23).
|
|
25181
|
+
try { _refreshCodexThreadName(ctmSessionId, liveSession); } catch (e) { console.error('[codex-link] thread name read error:', e.message); }
|
|
25017
25182
|
|
|
25018
25183
|
const candidateTransitionId = String(liveSession._modelExecutionRecipe?.transitionId || '');
|
|
25019
25184
|
const startupPointerWritten = persistCodexStartupResumeFromLink({ session: liveSession, dbModule,
|
|
@@ -26666,11 +26831,21 @@ function apiGenerateTitles(req, res) {
|
|
|
26666
26831
|
if (existing && (existing.userRenamed || existing.titleSource === 'provider_title' || existing.title)) return false;
|
|
26667
26832
|
return !!s.firstMessage;
|
|
26668
26833
|
});
|
|
26669
|
-
|
|
26670
|
-
for
|
|
26671
|
-
|
|
26834
|
+
// D-7 (WP-3): harness/provider boilerplate (<local-command-caveat>, <task-notification>,
|
|
26835
|
+
// "# AGENTS.md instructions for", a compaction summary) is not a human prompt — the same
|
|
26836
|
+
// predicate server/boot.js's background auto-title job gates its candidates on.
|
|
26837
|
+
const needTitles = [];
|
|
26838
|
+
for (const s of candidates) {
|
|
26839
|
+
if (isCtmInternalSession(s.firstMessage)) {
|
|
26840
|
+
persistTitleStatus(s.sessionId, 'skipped', 'ctm_internal');
|
|
26841
|
+
continue;
|
|
26842
|
+
}
|
|
26843
|
+
if (!isHumanPromptCandidate(s.provider, s.firstMessage)) {
|
|
26844
|
+
persistTitleStatus(s.sessionId, 'skipped', 'not_human_prompt');
|
|
26845
|
+
continue;
|
|
26846
|
+
}
|
|
26847
|
+
needTitles.push(s);
|
|
26672
26848
|
}
|
|
26673
|
-
const needTitles = candidates.filter(s => !isCtmInternalSession(s.firstMessage));
|
|
26674
26849
|
|
|
26675
26850
|
if (needTitles.length === 0) {
|
|
26676
26851
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
@@ -26686,19 +26861,9 @@ function apiGenerateTitles(req, res) {
|
|
|
26686
26861
|
// provider credentials itself; a credential-less setup surfaces as a normal
|
|
26687
26862
|
// callClaudeAsync failure below, classified by titleGenerationFailureInfo().
|
|
26688
26863
|
|
|
26689
|
-
//
|
|
26690
|
-
|
|
26691
|
-
|
|
26692
|
-
return msg
|
|
26693
|
-
.replace(/^This session is being continued from a previous conversation[^\n]*\n*/i, '')
|
|
26694
|
-
.replace(/^Continue the conversation[^\n]*\n*/i, '')
|
|
26695
|
-
.replace(/^Resume:?\s*/i, '')
|
|
26696
|
-
.replace(/^Summary:?\s*\n?/i, '')
|
|
26697
|
-
.replace(/^The summary below covers[^\n]*\n*/i, '')
|
|
26698
|
-
.replace(/^\[(?:Request interrupted|Tool use|Error|Retrying)[^\]]*\]\s*/g, '')
|
|
26699
|
-
.trim();
|
|
26700
|
-
}
|
|
26701
|
-
|
|
26864
|
+
// Resume/continuation boilerplate is stripped by server/boot.js's cleanFirstMessage, shared
|
|
26865
|
+
// with the background auto-title job (its `This session is being continued…` line is gone:
|
|
26866
|
+
// isHumanPromptCandidate already rejects that compaction row).
|
|
26702
26867
|
const prompt = `Generate short, descriptive titles (max 60 chars) for these Claude Code sessions.
|
|
26703
26868
|
Each session has an ID and the user's first message. Create a title that summarizes what the user was working on.
|
|
26704
26869
|
|
|
@@ -27382,9 +27547,6 @@ function _markMeaningfulActivity(session, source = 'activity', timestamp = Date.
|
|
|
27382
27547
|
function _markLifecycleFocus(sessionId, timestamp = Date.now()) {
|
|
27383
27548
|
const session = sessions.get(String(sessionId || ''));
|
|
27384
27549
|
if (!session) return 0;
|
|
27385
|
-
if (session._lifecycleState === 'frozen') {
|
|
27386
|
-
_thawLifecycleSession(session, { source: 'focus' });
|
|
27387
|
-
}
|
|
27388
27550
|
_claimLifecyclePrewarm(session, 'focus');
|
|
27389
27551
|
const at = Math.max(Number(session._lastFocusedAt || 0), Number(timestamp) || Date.now());
|
|
27390
27552
|
session._lastFocusedAt = at;
|
|
@@ -27519,7 +27681,7 @@ function _sessionLifecycleView(session, options = {}) {
|
|
|
27519
27681
|
state: session?._lifecycleState || (session?._restoreDeferred ? 'deferred' : (session?.ptyProcess ? 'live' : 'unavailable')),
|
|
27520
27682
|
liveStatus: session?._restoreDeferred
|
|
27521
27683
|
? 'parked'
|
|
27522
|
-
:
|
|
27684
|
+
: _standupLiveStatusForSession(session, now),
|
|
27523
27685
|
hasPendingApproval: !!(
|
|
27524
27686
|
_standupIsBlockingWaitingReason(waitingReason)
|
|
27525
27687
|
|| approver?.reviewing
|
|
@@ -27536,7 +27698,7 @@ function _sessionLifecycleView(session, options = {}) {
|
|
|
27536
27698
|
pinned: !!session?._lifecyclePinned,
|
|
27537
27699
|
isActiveTab: !!(session?.id && _sessionIsDeclaredShown(session.id)),
|
|
27538
27700
|
isMru: !!(session?.id && mruIds.has(session.id)),
|
|
27539
|
-
hasBackgroundShell: !!(_statusLedgerFor(session)?.
|
|
27701
|
+
hasBackgroundShell: !!(_statusLedgerFor(session)?.backgroundActive(now)) || (!_statusLedgerFor(session)?.backgroundDeclared && _backgroundAgentsActive(session, now)),
|
|
27540
27702
|
isRestoring: _inRestoreStorm() || restoreRuntime.restoreStartingActive(session),
|
|
27541
27703
|
recentNonStatusActivityAt: Number(session?._lastMeaningfulNonStatusAt || session?.lastMeaningfulActivityAt || 0),
|
|
27542
27704
|
lastActivatedAt: Number(session?._lastActivatedAt || 0),
|
|
@@ -27916,6 +28078,25 @@ function _lifecycleProvider(session) {
|
|
|
27916
28078
|
return detectAgentType(session?.cmd || '') || session?.agentType || session?.type || 'unknown';
|
|
27917
28079
|
}
|
|
27918
28080
|
|
|
28081
|
+
// WP-3 (§5 D-5/D-6, D-9): the one recorder for a skip that does NOT flow through the
|
|
28082
|
+
// _setDeferredRestoreStarting funnel. Per-session latch: a session skipped for the SAME reason
|
|
28083
|
+
// (drain passes, background-client storms) records once per episode; the claim clears it.
|
|
28084
|
+
function _noteWakeSkip(session, reason, detail = {}, id = '') {
|
|
28085
|
+
if (!reason) return;
|
|
28086
|
+
if (session && session._lastRestoreSkip?.reason === reason) return;
|
|
28087
|
+
if (session) session._lastRestoreSkip = { reason, at: Date.now() };
|
|
28088
|
+
const sid = String(id || session?.id || '');
|
|
28089
|
+
if (sid) {
|
|
28090
|
+
try { recordSessionDiagnostic(sid, 'deferred-restore-skip', { reason, ...detail }); }
|
|
28091
|
+
catch { /* expected: session diagnostics ring is not constructed in sliced unit harnesses */ }
|
|
28092
|
+
}
|
|
28093
|
+
try { _lifecycleDiagnostics.recordWakeSkip({ id: sid, provider: _lifecycleProvider(session), reason: String(reason) }); }
|
|
28094
|
+
catch { /* expected: lifecycle diagnostics are not constructed in sliced unit harnesses */ }
|
|
28095
|
+
}
|
|
28096
|
+
|
|
28097
|
+
// WP-3 (§9 S-10..S-20): the `onSkip(reason, id, session)` dep the extracted restore helpers use.
|
|
28098
|
+
function _restoreWakeSkipSink(reason, id, session) { _noteWakeSkip(session, reason, {}, id); }
|
|
28099
|
+
|
|
27919
28100
|
function _canonicalLifecycleWakePhases(phases = {}) {
|
|
27920
28101
|
return {
|
|
27921
28102
|
admitted: Number(phases.gateAdmittedAt || phases.activationRequestedAt || 0),
|
|
@@ -28158,12 +28339,23 @@ function _lifecycleOrphanTotals() {
|
|
|
28158
28339
|
}
|
|
28159
28340
|
|
|
28160
28341
|
async function _refreshLifecycleOrphanTotals() {
|
|
28342
|
+
if (_lifecycleOrphanGroups.size === 0) return _lifecycleOrphanTotals();
|
|
28343
|
+
// D-9 (WP-6, clock-free-tests-followups-2026-10-23 §5 D-9): ONE whole-table `ps` for the whole
|
|
28344
|
+
// sweep. N orphan groups used to mean N table reads; the rows are identical within a sweep.
|
|
28345
|
+
let rows;
|
|
28346
|
+
try {
|
|
28347
|
+
rows = await _sessionResourceSampler.sampleProcessTable();
|
|
28348
|
+
} catch {
|
|
28349
|
+
/* expected: best-effort sweep — a failed read skips this refresh and keeps the stored groups for
|
|
28350
|
+
one more sweep, exactly like the per-group catch below, instead of dropping known orphans. */
|
|
28351
|
+
return _lifecycleOrphanTotals();
|
|
28352
|
+
}
|
|
28161
28353
|
for (const [pgid, prev] of [..._lifecycleOrphanGroups]) {
|
|
28162
28354
|
try {
|
|
28163
28355
|
// D-4/D-6 (session-park-busy-background-shell-truth-2026-10-14): carry the stored survivor set
|
|
28164
28356
|
// forward, so a survivor that leads its OWN process group (invisible to the pgid rollup) is not
|
|
28165
28357
|
// forgotten by the next sweep while it is still alive.
|
|
28166
|
-
const group = await _sessionResourceSampler.sampleProcessGroup(pgid, prev?.survivorPids || []);
|
|
28358
|
+
const group = await _sessionResourceSampler.sampleProcessGroup(pgid, prev?.survivorPids || [], { rows });
|
|
28167
28359
|
if (group.procCount > 0) _lifecycleOrphanGroups.set(pgid, group);
|
|
28168
28360
|
else _lifecycleOrphanGroups.delete(pgid);
|
|
28169
28361
|
} catch {}
|
|
@@ -28208,76 +28400,6 @@ function _lifecycleProcessGroupId(session) {
|
|
|
28208
28400
|
return Number.isInteger(pgid) && pgid > 1 ? pgid : 0;
|
|
28209
28401
|
}
|
|
28210
28402
|
|
|
28211
|
-
function _freezeLifecycleSession(sessionId, options = {}) {
|
|
28212
|
-
const session = sessions.get(String(sessionId || ''));
|
|
28213
|
-
if (!session || !session.ptyProcess || session._restoreDeferred || session._closing || session._exited) {
|
|
28214
|
-
return { ok: false, code: 'session_not_live' };
|
|
28215
|
-
}
|
|
28216
|
-
if (!sessionPolicy(session).parkable) return { ok: false, code: 'session_not_parkable' };
|
|
28217
|
-
if (session._lifecycleState === 'frozen') return { ok: true, alreadyFrozen: true };
|
|
28218
|
-
const now = Number(options.now) || Date.now();
|
|
28219
|
-
const decision = evaluateFreeze(
|
|
28220
|
-
_sessionLifecycleView(session, { now, mruIds: options.mruIds }),
|
|
28221
|
-
{ now, connectedClients: Number(options.connectedClients ?? _connectedLifecycleClientCount()) },
|
|
28222
|
-
options.policyConfig || _lifecycleConfig,
|
|
28223
|
-
);
|
|
28224
|
-
if (!decision.eligible) {
|
|
28225
|
-
return { ok: false, code: 'session_protected', protections: decision.protections };
|
|
28226
|
-
}
|
|
28227
|
-
const pgid = _lifecycleProcessGroupId(session);
|
|
28228
|
-
if (!pgid) return { ok: false, code: 'process_group_unavailable' };
|
|
28229
|
-
try {
|
|
28230
|
-
process.kill(-pgid, 'SIGSTOP');
|
|
28231
|
-
} catch (error) {
|
|
28232
|
-
_lifecycleDiagnostics.recordFreeze({ id: session.id, action: 'failed', reason: error.code || error.message });
|
|
28233
|
-
return { ok: false, code: 'freeze_signal_failed', error: error.message };
|
|
28234
|
-
}
|
|
28235
|
-
session._lifecycleState = 'frozen';
|
|
28236
|
-
session._frozenAt = now;
|
|
28237
|
-
session._freezePgid = pgid;
|
|
28238
|
-
session._frozenInputQueue = [];
|
|
28239
|
-
_lifecycleDiagnostics.recordFreeze({ id: session.id, action: 'frozen', idleMs: decision.idleMs });
|
|
28240
|
-
broadcastToSession(session.id, session, { type: 'session-frozen', id: session.id, frozenAt: now });
|
|
28241
|
-
broadcastSessionList(true);
|
|
28242
|
-
console.log(`[lifecycle] freeze id=${String(session.id).slice(0, 8)} idleMin=${Math.floor(decision.idleMs / 60_000)} pgid=${pgid}`);
|
|
28243
|
-
return { ok: true, sessionId: session.id, frozenAt: now };
|
|
28244
|
-
}
|
|
28245
|
-
|
|
28246
|
-
function _thawLifecycleSession(sessionOrId, options = {}) {
|
|
28247
|
-
const session = typeof sessionOrId === 'string' ? sessions.get(sessionOrId) : sessionOrId;
|
|
28248
|
-
if (!session) return { ok: false, code: 'session_not_found' };
|
|
28249
|
-
if (session._lifecycleState !== 'frozen') return { ok: true, alreadyLive: true };
|
|
28250
|
-
const pgid = Number(session._freezePgid || _lifecycleProcessGroupId(session));
|
|
28251
|
-
if (!Number.isInteger(pgid) || pgid <= 1) return { ok: false, code: 'process_group_unavailable' };
|
|
28252
|
-
try {
|
|
28253
|
-
process.kill(-pgid, 'SIGCONT');
|
|
28254
|
-
} catch (error) {
|
|
28255
|
-
_lifecycleDiagnostics.recordFreeze({ id: session.id, action: 'thaw-failed', reason: error.code || error.message });
|
|
28256
|
-
return { ok: false, code: 'thaw_signal_failed', error: error.message };
|
|
28257
|
-
}
|
|
28258
|
-
const now = Date.now();
|
|
28259
|
-
const frozenForMs = Math.max(0, now - Number(session._frozenAt || now));
|
|
28260
|
-
const queuedInput = Array.isArray(session._frozenInputQueue) ? session._frozenInputQueue.splice(0) : [];
|
|
28261
|
-
session._lifecycleState = 'live';
|
|
28262
|
-
session._frozenAt = 0;
|
|
28263
|
-
session._freezePgid = 0;
|
|
28264
|
-
session._frozenInputQueue = [];
|
|
28265
|
-
if (options.preserveIdle !== true) session._lastActivatedAt = now;
|
|
28266
|
-
_lifecycleDiagnostics.recordFreeze({
|
|
28267
|
-
id: session.id,
|
|
28268
|
-
action: 'thawed',
|
|
28269
|
-
idleMs: frozenForMs,
|
|
28270
|
-
reason: String(options.source || 'activation'),
|
|
28271
|
-
});
|
|
28272
|
-
for (const data of queuedInput) _writeLifecyclePty(session, data, 'frozen-thaw');
|
|
28273
|
-
if (options.broadcast !== false) {
|
|
28274
|
-
broadcastToSession(session.id, session, { type: 'session-thawed', id: session.id, source: String(options.source || 'activation') });
|
|
28275
|
-
broadcastSessionList(true);
|
|
28276
|
-
}
|
|
28277
|
-
console.log(`[lifecycle] thaw id=${String(session.id).slice(0, 8)} source=${String(options.source || 'activation')} frozenMs=${frozenForMs}`);
|
|
28278
|
-
return { ok: true, sessionId: session.id, flushedInputs: queuedInput.length };
|
|
28279
|
-
}
|
|
28280
|
-
|
|
28281
28403
|
function _recordDroppedDeviceReply(session, source, classification) {
|
|
28282
28404
|
if (!session?.id || !classification) return;
|
|
28283
28405
|
recordSessionDiagnostic(session.id, 'input-dropped-device-reply', {
|
|
@@ -28353,7 +28475,7 @@ function _stripPtyOutputReplies(session, sessionId, data, site) {
|
|
|
28353
28475
|
}
|
|
28354
28476
|
|
|
28355
28477
|
function _writeLifecyclePty(session, data, source = 'input', options = {}) {
|
|
28356
|
-
if (!session?.ptyProcess) return { posted: false, inputSeq: 0 };
|
|
28478
|
+
if (!session?.ptyProcess) return { posted: false, inputSeq: 0, dispatchDisposition: 'preflight-refused' };
|
|
28357
28479
|
// Image, typing and lifecycle frames share one monotonic transport sequence. Typing keeps its
|
|
28358
28480
|
// separate echo counter and passes the exact allocated frame sequence through options.inputSeq.
|
|
28359
28481
|
// WP-2 (§6.2, D6): ONE monotonic allocator for every host frame. Keystrokes pass the exact seq they
|
|
@@ -28372,7 +28494,7 @@ function _writeLifecyclePty(session, data, source = 'input', options = {}) {
|
|
|
28372
28494
|
// user-input funnel (source 'input', the ONE path _writeInputWithEchoTracking uses for
|
|
28373
28495
|
// every keystroke and forwarded mouse report) stamps mouse/non-mouse recency for the
|
|
28374
28496
|
// mouse-provoked-output gate. Every other source here is CTM-generated (auto-answer,
|
|
28375
|
-
// dialog-skip, device-query reply, restore/
|
|
28497
|
+
// dialog-skip, device-query reply, restore/park replay) or an authoritative device
|
|
28376
28498
|
// reply -- never "the user just did X" -- and must not move either clock.
|
|
28377
28499
|
if (source === 'input' && !authoritativeDeviceReply) {
|
|
28378
28500
|
const stampAt = Date.now();
|
|
@@ -28384,35 +28506,23 @@ function _writeLifecyclePty(session, data, source = 'input', options = {}) {
|
|
|
28384
28506
|
if (data === '\x1b[I' || data === '\x1b[O') session._lastViewerProvokeAt = stampAt; // focus in/out
|
|
28385
28507
|
}
|
|
28386
28508
|
}
|
|
28387
|
-
if (session._lifecycleState === 'frozen') {
|
|
28388
|
-
const classification = authoritativeDeviceReply ? null : classifyHostBoundReply(data);
|
|
28389
|
-
if (classification) {
|
|
28390
|
-
_recordDroppedDeviceReply(session, source, classification);
|
|
28391
|
-
return { posted: false, inputSeq };
|
|
28392
|
-
}
|
|
28393
|
-
if (!Array.isArray(session._frozenInputQueue)) session._frozenInputQueue = [];
|
|
28394
|
-
session._frozenInputQueue.push(data);
|
|
28395
|
-
const result = _thawLifecycleSession(session, { source });
|
|
28396
|
-
return { posted: !!result.ok, inputSeq };
|
|
28397
|
-
}
|
|
28398
28509
|
session._lifecycleInputInFlight = true;
|
|
28510
|
+
let dispatchDisposition = 'preflight-refused';
|
|
28399
28511
|
try {
|
|
28400
|
-
//
|
|
28401
|
-
//
|
|
28402
|
-
//
|
|
28403
|
-
// _onHostInputAck below); nothing here may treat a truthy synchronous return as
|
|
28404
|
-
// delivery proof.
|
|
28512
|
+
// A1: preserve the selected writer's legacy result while observing its actual route.
|
|
28513
|
+
// Neither a Boolean nor this local disposition is byte-write or withdrawal proof;
|
|
28514
|
+
// the separate input-ack is still the host's byte-write observation.
|
|
28405
28515
|
const posted = writeWithDeviceReplyFirewall({
|
|
28406
28516
|
data,
|
|
28407
28517
|
exempt: authoritativeDeviceReply,
|
|
28408
|
-
|
|
28409
|
-
|
|
28410
|
-
|
|
28411
|
-
|
|
28412
|
-
|
|
28518
|
+
write: value => {
|
|
28519
|
+
const observed = _observePtyWrite(session.ptyProcess, value, { inputSeq });
|
|
28520
|
+
dispatchDisposition = observed.disposition;
|
|
28521
|
+
return observed.posted;
|
|
28522
|
+
},
|
|
28413
28523
|
onDrop: classification => _recordDroppedDeviceReply(session, source, classification),
|
|
28414
28524
|
});
|
|
28415
|
-
return { posted: posted === true, inputSeq };
|
|
28525
|
+
return { posted: posted === true, inputSeq, dispatchDisposition };
|
|
28416
28526
|
}
|
|
28417
28527
|
finally { session._lifecycleInputInFlight = false; }
|
|
28418
28528
|
}
|
|
@@ -28432,7 +28542,7 @@ function _lifecycleBackgroundLease(session, now = Date.now()) {
|
|
|
28432
28542
|
const ledger = session ? _statusLedgerFor(session) : null;
|
|
28433
28543
|
const snap = ledger && typeof ledger.backgroundSnapshot === 'function' ? ledger.backgroundSnapshot(now) : null;
|
|
28434
28544
|
if (!snap || !Array.isArray(snap.entries) || !snap.entries.length) return null;
|
|
28435
|
-
let shells = 0, tasks = 0, firstSeenAt = Infinity, renewedAt = 0, expiresAt = Infinity, renewals = 0, capCrossed = 0;
|
|
28545
|
+
let shells = 0, tasks = 0, firstSeenAt = Infinity, renewedAt = 0, expiresAt = Infinity, renewals = 0, capCrossed = 0, quiet = 0, activeAt = 0;
|
|
28436
28546
|
for (const e of snap.entries) {
|
|
28437
28547
|
if (e.type === 'shell') shells += 1; else tasks += 1;
|
|
28438
28548
|
firstSeenAt = Math.min(firstSeenAt, Number(e.firstSeenAt) || 0);
|
|
@@ -28440,8 +28550,10 @@ function _lifecycleBackgroundLease(session, now = Date.now()) {
|
|
|
28440
28550
|
expiresAt = Math.min(expiresAt, Number(e.expiresAt) || 0);
|
|
28441
28551
|
renewals += Number(e.renewals) || 0;
|
|
28442
28552
|
if (Number(e.capCrossedAt) > 0) capCrossed += 1;
|
|
28553
|
+
if (typeof ledger.backgroundQuiet === 'function' && ledger.backgroundQuiet(e, now)) quiet += 1; // D-5: no classifier, nothing is quiet
|
|
28554
|
+
activeAt = Math.max(activeAt, Number(e.activeAt) || 0);
|
|
28443
28555
|
}
|
|
28444
|
-
return { shells, tasks, firstSeenAt, renewedAt, expiresAt, renewals, capCrossed };
|
|
28556
|
+
return { shells, tasks, firstSeenAt, renewedAt, expiresAt, renewals, capCrossed, quiet, activeAt };
|
|
28445
28557
|
}
|
|
28446
28558
|
|
|
28447
28559
|
function _lifecycleJournalGate(session, decision, configName) {
|
|
@@ -28632,54 +28744,15 @@ async function parkSession(sessionId, options = {}) {
|
|
|
28632
28744
|
protections: preparedDecision.protections }, preparedDecision);
|
|
28633
28745
|
if (transcriptBefore.unknown) return finishParkAttempt({ ok: false, code: 'transcript_verification_unavailable', statusCode: 503 }, preparedDecision);
|
|
28634
28746
|
if (!transcriptBefore.missing && !resumeSpec) return finishParkAttempt({ ok: false, code: 'session_not_resumable', statusCode: 409 }, preparedDecision);
|
|
28747
|
+
// WP-2 (docs/composer-freeze-followups-load-sigstop-attribution-2026-10-20.html §5 D-B1..D-B4): a giant rollout used to divert
|
|
28748
|
+
// here into the deleted SIGSTOP "frozen" state. There is no such state any more -- the park
|
|
28749
|
+
// falls through to the one kill ladder below (SIGTERM -> parkTermGraceMs -> SIGKILL).
|
|
28635
28750
|
const rolloutSizeBytes = Math.max(
|
|
28636
28751
|
0,
|
|
28637
28752
|
Number(session._resumeLoadingFileSize || session.fileSize || session.file_size) || 0,
|
|
28638
28753
|
Number(prepared.transcript?.size || 0),
|
|
28639
28754
|
Number(transcriptBefore.size || 0),
|
|
28640
28755
|
);
|
|
28641
|
-
if (shouldFreezeParkGiant({
|
|
28642
|
-
enabled: FREEZE_PARK_GIANT_ENABLED,
|
|
28643
|
-
fileSizeBytes: rolloutSizeBytes,
|
|
28644
|
-
pressure: _lifecyclePressure.level,
|
|
28645
|
-
reason,
|
|
28646
|
-
alreadyFrozen: session._lifecycleState === 'frozen',
|
|
28647
|
-
})) {
|
|
28648
|
-
const latestDecision = parkDecision(Date.now());
|
|
28649
|
-
if (!latestDecision.eligible) {
|
|
28650
|
-
return finishParkAttempt({
|
|
28651
|
-
ok: false,
|
|
28652
|
-
code: 'session_protected',
|
|
28653
|
-
statusCode: 409,
|
|
28654
|
-
protections: latestDecision.protections,
|
|
28655
|
-
}, latestDecision);
|
|
28656
|
-
}
|
|
28657
|
-
const frozen = _freezeLifecycleSession(session.id, {
|
|
28658
|
-
policyConfig: {
|
|
28659
|
-
...policyConfig,
|
|
28660
|
-
freezeTier: true,
|
|
28661
|
-
freezeAfterMs: Math.max(1, Number(policyConfig.idleParkAfterMs) || 1),
|
|
28662
|
-
},
|
|
28663
|
-
});
|
|
28664
|
-
if (frozen.ok) {
|
|
28665
|
-
recordSessionDiagnostic(session.id, 'giant-rollout-freeze-park', {
|
|
28666
|
-
reason,
|
|
28667
|
-
fileSizeBytes: rolloutSizeBytes,
|
|
28668
|
-
pressure: _lifecyclePressure.level,
|
|
28669
|
-
});
|
|
28670
|
-
return finishParkAttempt({ ...frozen, lifecycleState: 'frozen', reason }, latestDecision);
|
|
28671
|
-
}
|
|
28672
|
-
recordSessionDiagnostic(session.id, 'giant-rollout-freeze-fallback', {
|
|
28673
|
-
reason,
|
|
28674
|
-
fileSizeBytes: rolloutSizeBytes,
|
|
28675
|
-
pressure: _lifecyclePressure.level,
|
|
28676
|
-
code: frozen.code || 'freeze_failed',
|
|
28677
|
-
});
|
|
28678
|
-
}
|
|
28679
|
-
if (session._lifecycleState === 'frozen') {
|
|
28680
|
-
const thaw = _thawLifecycleSession(session, { source: 'park', preserveIdle: true, broadcast: false });
|
|
28681
|
-
if (!thaw.ok) return finishParkAttempt({ ok: false, code: thaw.code, statusCode: 500 }, decision);
|
|
28682
|
-
}
|
|
28683
28756
|
|
|
28684
28757
|
if (options.prewarmExpiry === true && session._prewarmExpiryTimer) {
|
|
28685
28758
|
clearTimeout(session._prewarmExpiryTimer);
|
|
@@ -29136,6 +29209,17 @@ async function _sampleLifecycleResources(options = {}) {
|
|
|
29136
29209
|
_sessionResourceSampler.warnedPressureFailure = true;
|
|
29137
29210
|
console.warn('[lifecycle] memory-pressure probe was unparseable; treating pressure as normal');
|
|
29138
29211
|
}
|
|
29212
|
+
// WP-1 (machine-pool-picker-pairing-dashboard-2026-10-24.html §6.1/D-11): one host sample per
|
|
29213
|
+
// RESOURCE SAMPLE, hooked here rather than in _runLifecycleSweep on purpose. This function's own
|
|
29214
|
+
// due() gate (CTM_RESOURCE_SAMPLE_MS, default 60 s) is what makes host_samples hold "one row per
|
|
29215
|
+
// minute, ≤1440 rows/machine" -- the same number db.js's listHostSamples caps at, so a 24 h read
|
|
29216
|
+
// always fits. Hooking the sweep instead would record on the 30 s maintenance tick and halve the
|
|
29217
|
+
// readable window (the read takes the OLDEST `limit` rows in the window). No new timer either
|
|
29218
|
+
// way. Fire-and-forget: record() is defined never to throw and a failed sub-read degrades to null
|
|
29219
|
+
// fields, but a rejection here would still be a bug, so it is reported rather than swallowed.
|
|
29220
|
+
void _hostStats.record(snapshot).catch((error) => {
|
|
29221
|
+
errorSink.report('host-stats:record', error, { at: Number(snapshot && snapshot.sampledAt) || 0 });
|
|
29222
|
+
});
|
|
29139
29223
|
for (const resource of snapshot.sessions || []) {
|
|
29140
29224
|
const session = sessions.get(resource.id);
|
|
29141
29225
|
if (!session || session._restoreDeferred) continue;
|
|
@@ -29152,7 +29236,7 @@ async function _sampleLifecycleResources(options = {}) {
|
|
|
29152
29236
|
// matches every other footprint field's "sampled vs not yet" contract in this file.
|
|
29153
29237
|
session.footprintMB = resource.footprintMB == null ? null : resource.footprintMB;
|
|
29154
29238
|
_statusBindings.applyCpuSample(session, resource);
|
|
29155
|
-
if (resource.agentProcCount > 0) { session.descendantGroupLeaderCount = resource.descendantGroupLeaderCount; _statusBindings.applyProcessShape(session, resource); } // D-10: unknown agent pid is not zero
|
|
29239
|
+
if (resource.agentProcCount > 0) { session.descendantGroupLeaderCount = resource.descendantGroupLeaderCount; _statusBindings.applyProcessShape(session, resource); void _statusBindings.observeTaskOutputs(session, resource.sampledAt); } // D-10: unknown agent pid is not zero
|
|
29156
29240
|
}
|
|
29157
29241
|
const previousPressure = _lifecyclePressure.level;
|
|
29158
29242
|
const pressureResult = _lifecyclePressure.observe(snapshot.signals || snapshot.pressure, {
|
|
@@ -29223,7 +29307,7 @@ async function _runLifecycleSweep() {
|
|
|
29223
29307
|
const shouldTrim = pressure === 'warn' && _lifecycleConfig.pressureTrimFirst;
|
|
29224
29308
|
let trimFreedMB = 0;
|
|
29225
29309
|
let lastTrim = null;
|
|
29226
|
-
if (shouldTrim
|
|
29310
|
+
if (shouldTrim) {
|
|
29227
29311
|
// AC-4d: awaited so `trim.mirrorRowsTrimmed` is real by the time it is journaled --
|
|
29228
29312
|
// the mirror shrink is an IPC round-trip into the pooled host process.
|
|
29229
29313
|
// WP-3/S7: shouldTrim is only true when pressure === 'warn' (see above), so this is
|
|
@@ -29245,7 +29329,6 @@ async function _runLifecycleSweep() {
|
|
|
29245
29329
|
const policy = automaticParkSweepPolicy(_lifecycleConfig, {
|
|
29246
29330
|
pressure,
|
|
29247
29331
|
liveCount: _liveLifecycleSessions().length,
|
|
29248
|
-
freezeTier: _lifecycleConfig.freezeTier,
|
|
29249
29332
|
warnPersistedMs,
|
|
29250
29333
|
trimFreedMB,
|
|
29251
29334
|
});
|
|
@@ -29311,7 +29394,7 @@ async function _runLifecycleSweep() {
|
|
|
29311
29394
|
? 'pressure'
|
|
29312
29395
|
: (pressure === 'warn' && policy.warnPark === true
|
|
29313
29396
|
? 'warn'
|
|
29314
|
-
: (budget.actionable ? 'rss-budget' :
|
|
29397
|
+
: (budget.actionable ? 'rss-budget' : 'idle'));
|
|
29315
29398
|
if (_lifecycleConfig.idleParkShadow) {
|
|
29316
29399
|
const shadowSession = sessions.get(victim.id);
|
|
29317
29400
|
if (shadowSession && !shadowSession._lifecycleShadowCandidateAt) {
|
|
@@ -29353,26 +29436,6 @@ async function _runLifecycleSweep() {
|
|
|
29353
29436
|
}
|
|
29354
29437
|
if (pressureMode && parked + 1 < maxParks) await new Promise(resolve => setTimeout(resolve, 1_000));
|
|
29355
29438
|
}
|
|
29356
|
-
if (_lifecycleConfig.freezeTier) {
|
|
29357
|
-
const mruIds = _lifecycleMruIds();
|
|
29358
|
-
for (let frozen = 0; frozen < _lifecycleConfig.parkMaxPerSweep; frozen += 1) {
|
|
29359
|
-
const views = _liveLifecycleSessions().map(session => _sessionLifecycleView(session, { now, mruIds }));
|
|
29360
|
-
const victim = selectFreezeVictim(views, { now, connectedClients }, _lifecycleConfig);
|
|
29361
|
-
if (!victim) break;
|
|
29362
|
-
const result = _freezeLifecycleSession(victim.id, { now, connectedClients, mruIds });
|
|
29363
|
-
if (!result.ok) break;
|
|
29364
|
-
if (shouldTrim && _lifecycleConfig.parkJournal) {
|
|
29365
|
-
appendParkRecord({
|
|
29366
|
-
event: 'pressure-freeze',
|
|
29367
|
-
id: victim.id,
|
|
29368
|
-
reason: 'pressure',
|
|
29369
|
-
gate: _lifecycleJournalGate(sessions.get(victim.id), evaluateFreeze(victim, { now, connectedClients }, _lifecycleConfig), 'pressure'),
|
|
29370
|
-
host: _lifecycleJournalHost(),
|
|
29371
|
-
victimRssMB: Number(victim.rssMB) || 0,
|
|
29372
|
-
});
|
|
29373
|
-
}
|
|
29374
|
-
}
|
|
29375
|
-
}
|
|
29376
29439
|
if (snapshot?.pressure && snapshot.pressure !== pressure) {
|
|
29377
29440
|
console.log(`[lifecycle] pressure hysteresis observed=${snapshot.pressure} effective=${pressure}`);
|
|
29378
29441
|
}
|
|
@@ -30039,7 +30102,12 @@ wss.on('connection', async (ws, req) => {
|
|
|
30039
30102
|
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
30040
30103
|
const reqSnapshot = {
|
|
30041
30104
|
socket: { remoteAddress: _remoteIpFromReq(req) },
|
|
30042
|
-
|
|
30105
|
+
// X-CTM-Forwarded-By (WP-2a P-3) joins the snapshot so the WS create path can apply the
|
|
30106
|
+
// one-hop guard (lib/remote-machine-create.js forwardRemoteCreate reads it off ctx.ws here).
|
|
30107
|
+
headers: {
|
|
30108
|
+
'user-agent': req.headers?.['user-agent'] || '',
|
|
30109
|
+
'x-ctm-forwarded-by': req.headers?.['x-ctm-forwarded-by'] || '',
|
|
30110
|
+
},
|
|
30043
30111
|
};
|
|
30044
30112
|
const cfDecision = cloudflareAccessVerifier.verifyRequestSync(req, { isLocalhost });
|
|
30045
30113
|
if (!cfDecision.ok) {
|
|
@@ -30291,6 +30359,7 @@ wss.on('connection', async (ws, req) => {
|
|
|
30291
30359
|
let msg;
|
|
30292
30360
|
try { msg = JSON.parse(raw); } catch { return; }
|
|
30293
30361
|
if (msg.type === 'input') msg._typingWsReceivedAt = wsReceivedAt;
|
|
30362
|
+
if (msg.type === 'submit-prompt') msg._wsReceivedAt = wsReceivedAt;
|
|
30294
30363
|
// WP-4f (terminal-delivery-contract-followups-2026-09-23.html §Step 2, fix round 1): mark +
|
|
30295
30364
|
// gate move earlier, synchronously, pre-await, so a same-tick client-info/attach race can't
|
|
30296
30365
|
// drop a compatible attach as terminal-protocol-incompatible (the "viewer B" flake).
|
|
@@ -31532,6 +31601,9 @@ headlessWorker.on('message', _perfTracker.wrap('worker:headless-terminal-message
|
|
|
31532
31601
|
? _recordInputEchoFromFrame(session, msg.echoedThroughSeq, Date.now(), {
|
|
31533
31602
|
targetClientId: String(msg.targetClientId || ''),
|
|
31534
31603
|
targetSocketGeneration: Number(msg.targetSocketGeneration || 0),
|
|
31604
|
+
// WP-4 (R-7): session-host-manager.js's `_stampMainArrival` stamped this frame object
|
|
31605
|
+
// when it first touched the main thread; 0 when the sender predates the stamp.
|
|
31606
|
+
frameArrivedAt: Number(msg._mainArrivedAt) || 0,
|
|
31535
31607
|
})
|
|
31536
31608
|
: [];
|
|
31537
31609
|
const holdingRestoreFrames = _stateSyncHoldingRestoreFrames(session);
|
|
@@ -34039,6 +34111,35 @@ function _sendWalleSessionCreated(ws, session, extra = {}) {
|
|
|
34039
34111
|
return true;
|
|
34040
34112
|
}
|
|
34041
34113
|
|
|
34114
|
+
// WP-4 (§5 D-12): the `created` frame a DUPLICATE same-id create for an ALREADY LIVE native session
|
|
34115
|
+
// is re-answered with. Byte-for-byte the frame the spawn path sends (lib/session-create/response.js's
|
|
34116
|
+
// sendCreated): the SAME builder, the SAME `_sessionPayload` projection, the same arguments -- read
|
|
34117
|
+
// off the LIVE session object instead of the spawn's locals. No attach, no snapshot, no
|
|
34118
|
+
// client-list mutation, no state-sync subscription: the client attaches itself on `created`, exactly
|
|
34119
|
+
// as it does after a fresh create (public/index.html's onCreated replaces the tab in place).
|
|
34120
|
+
function _sendNativeSessionCreated(ws, session) {
|
|
34121
|
+
if (!ws || ws.readyState !== 1 || !session || typeof session !== 'object') return false;
|
|
34122
|
+
const cmd = session.cmd || session.shell || '';
|
|
34123
|
+
const createAgentType = session.agentType || detectAgentType(cmd);
|
|
34124
|
+
_wsSend(ws, JSON.stringify(buildCreatedSessionPayload({
|
|
34125
|
+
sessionPayload: _sessionPayload(session, { lightweight: true, stormSafe: true }),
|
|
34126
|
+
session,
|
|
34127
|
+
id: session.id,
|
|
34128
|
+
// The session's CURRENT pid. On a hosted PTY the adapter re-points `ptyProcess.pid` from the
|
|
34129
|
+
// wrapper to the real child once the host receipt lands, so this can differ from the pid the
|
|
34130
|
+
// first `created` named; the re-answer reports the live truth, not a replay.
|
|
34131
|
+
pid: session.ptyProcess.pid,
|
|
34132
|
+
cwd: session.cwd || '',
|
|
34133
|
+
cmd,
|
|
34134
|
+
args: Array.isArray(session.args) ? session.args : [],
|
|
34135
|
+
branch: session.branch ?? null,
|
|
34136
|
+
createAgentType,
|
|
34137
|
+
projectDirName: session._claudeProjectDir ? path.basename(session._claudeProjectDir) : null,
|
|
34138
|
+
getAgentCapabilities,
|
|
34139
|
+
})));
|
|
34140
|
+
return true;
|
|
34141
|
+
}
|
|
34142
|
+
|
|
34042
34143
|
// WP-3 (design/walle-conv-order-authority-2026-08-21 D-5, §6): min/max order key over a
|
|
34043
34144
|
// served page's messages, for the envelope's keyRange. Min/max (not first/last array
|
|
34044
34145
|
// position) because callers serve pages in different physical row orders (e.g. a
|
|
@@ -35641,7 +35742,6 @@ const RESUME_LIFECYCLE_V2_ENABLED = serverConfig.CTM_RESUME_LIFECYCLE_V2 !== '0'
|
|
|
35641
35742
|
const RESUME_VIEWPORT_READINESS_ENABLED = process.env.CTM_RESUME_VIEWPORT_READINESS !== '0';
|
|
35642
35743
|
const INPUT_READINESS_ENABLED = (process.env.CTM_INPUT_READINESS || '') !== '0';
|
|
35643
35744
|
const INPUT_READY_GATE_ENABLED = (process.env.CTM_INPUT_READY_GATE || '') !== '0';
|
|
35644
|
-
const FREEZE_PARK_GIANT_ENABLED = process.env.CTM_FREEZE_PARK_GIANT !== '0';
|
|
35645
35745
|
// Refuse to convert an unreadable terminal mirror into a "the agent never became interactive"
|
|
35646
35746
|
// verdict. Set to '0' to restore the pre-2026-07-19 behavior where an unreadable mirror killed
|
|
35647
35747
|
// the resume.
|
|
@@ -35859,6 +35959,8 @@ function _noteInputEchoSequence(session, inputSeq, acceptedAt = Date.now()) {
|
|
|
35859
35959
|
// before that synchronous write, then establish the worker barrier before a later PTY data
|
|
35860
35960
|
// callback can enqueue output attributed to this input.
|
|
35861
35961
|
session._flushHeadlessFeed?.();
|
|
35962
|
+
// WP-3 D-C2 (§6 W-1): the per-key payload the mirror's shadow echo matchers arm on.
|
|
35963
|
+
const tracedInput = session._typingInteractionByInputSeq?.get(inputSeq);
|
|
35862
35964
|
headlessWorker.postMessage({
|
|
35863
35965
|
type: 'note-input',
|
|
35864
35966
|
sessionId: session.id,
|
|
@@ -35866,6 +35968,7 @@ function _noteInputEchoSequence(session, inputSeq, acceptedAt = Date.now()) {
|
|
|
35866
35968
|
// WP-0b fix round 1 (S-10): 'note-input', not 'feed', is the op that reaches BOTH transports for every keystroke
|
|
35867
35969
|
// (session-host-manager.js:1815-1834); a pool-hosted session's 'feed' never leaves that child process.
|
|
35868
35970
|
interactionIds: pendingInteractionIdsFor(session._typingInteractionByInputSeq),
|
|
35971
|
+
echo: tracedInput?.echoChar ? { interactionId: tracedInput.interactionId, char: tracedInput.echoChar } : undefined,
|
|
35869
35972
|
});
|
|
35870
35973
|
} catch (error) {
|
|
35871
35974
|
recordSessionDiagnostic(session.id, 'resume-input-seq-note-failed', {
|
|
@@ -35958,6 +36061,8 @@ function _writeInputWithEchoTracking(session, data, source = 'input', trace = {}
|
|
|
35958
36061
|
clientId: String(trace.clientId || ''),
|
|
35959
36062
|
socketGeneration: Number(trace.socketGeneration || 0),
|
|
35960
36063
|
mirrored: false,
|
|
36064
|
+
// WP-3 D-C2: the predicted echo char for this key; '' for anything but one printable grapheme.
|
|
36065
|
+
echoChar: printable && isSingleGraphemeEcho(data) ? data : '',
|
|
35961
36066
|
});
|
|
35962
36067
|
while (session._typingInteractionByInputSeq.size > 64) {
|
|
35963
36068
|
session._typingInteractionByInputSeq.delete(session._typingInteractionByInputSeq.keys().next().value);
|
|
@@ -36052,6 +36157,37 @@ function _onHostInputAck(session, ack, origin = null) {
|
|
|
36052
36157
|
}
|
|
36053
36158
|
}
|
|
36054
36159
|
|
|
36160
|
+
// WP-4 (R-8): the top ≤3 recent main-thread spans overlapping [startMs, endMs], by overlapped ms
|
|
36161
|
+
// descending, as `{ name, overlapMs }` -- "what was main running while this frame was in flight".
|
|
36162
|
+
// Reads the same recent-span ring lib/perf-tracker.js's selectBlockCoverage reads (via its
|
|
36163
|
+
// getRecentSpans accessor; `eventLoopBlockOverlap` is the aggregate view of the same ring and
|
|
36164
|
+
// names no op, so it cannot answer this). A pure read + compute: no span is recorded, no counter
|
|
36165
|
+
// moves, and a caller can never make this loop do work it would not otherwise do. Spans on a
|
|
36166
|
+
// different clock (or absent) simply contribute nothing.
|
|
36167
|
+
function _mainSpansForWindow(startMs, endMs) {
|
|
36168
|
+
const start = Number(startMs);
|
|
36169
|
+
const end = Number(endMs);
|
|
36170
|
+
if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return [];
|
|
36171
|
+
// The window's own start can be older than the ring's default 3 s lookback; ask for the ring's
|
|
36172
|
+
// full 24-span depth and let the overlap test below decide.
|
|
36173
|
+
const spans = _perfTracker.getRecentSpans({ maxAgeMs: Math.max(3000, Date.now() - start + 1000), limit: 24 }).recent;
|
|
36174
|
+
const byName = new Map();
|
|
36175
|
+
for (const span of spans) {
|
|
36176
|
+
if (!span) continue;
|
|
36177
|
+
const spanStart = Number(span.start);
|
|
36178
|
+
const spanEnd = Number(span.end);
|
|
36179
|
+
if (!Number.isFinite(spanStart) || !Number.isFinite(spanEnd)) continue;
|
|
36180
|
+
const overlap = Math.min(spanEnd, end) - Math.max(spanStart, start);
|
|
36181
|
+
if (overlap <= 0) continue;
|
|
36182
|
+
const name = String(span.name || '').slice(0, 120);
|
|
36183
|
+
if (!name) continue;
|
|
36184
|
+
byName.set(name, (byName.get(name) || 0) + overlap);
|
|
36185
|
+
}
|
|
36186
|
+
return Array.from(byName, ([name, overlapMs]) => ({ name, overlapMs: Math.max(0, Math.floor(overlapMs)) }))
|
|
36187
|
+
.sort((a, b) => b.overlapMs - a.overlapMs)
|
|
36188
|
+
.slice(0, 3);
|
|
36189
|
+
}
|
|
36190
|
+
|
|
36055
36191
|
function _recordInputEchoFromFrame(session, echoedThroughSeq, at = Date.now(), target = {}) {
|
|
36056
36192
|
if (!session || !(session._inputEchoPending instanceof Map)) return [];
|
|
36057
36193
|
const echoed = Math.max(0, Number(echoedThroughSeq) || 0);
|
|
@@ -36069,6 +36205,10 @@ function _recordInputEchoFromFrame(session, echoedThroughSeq, at = Date.now(), t
|
|
|
36069
36205
|
}
|
|
36070
36206
|
const targetClientId = String(target.targetClientId || '');
|
|
36071
36207
|
const targetSocketGeneration = Number(target.targetSocketGeneration || 0);
|
|
36208
|
+
// WP-4 (clock-free-tests-followups-r4-2026-10-24.html §5 R-7/R-8, §6.5): the frame's arrival
|
|
36209
|
+
// stamp, set by session-host-manager.js's `_stampMainArrival` at the first main-thread touch.
|
|
36210
|
+
// 0 (an older/unstamped sender) means "no arrival leg to price" -- never a real epoch 0.
|
|
36211
|
+
const frameArrivedAt = Math.max(0, Math.floor(Number(target.frameArrivedAt) || 0));
|
|
36072
36212
|
for (const [seq, rawRecord] of session._typingInteractionByInputSeq || []) {
|
|
36073
36213
|
if (seq > echoed) continue;
|
|
36074
36214
|
const record = typeof rawRecord === 'string'
|
|
@@ -36077,6 +36217,14 @@ function _recordInputEchoFromFrame(session, echoedThroughSeq, at = Date.now(), t
|
|
|
36077
36217
|
if (!record?.interactionId) continue;
|
|
36078
36218
|
if (!record.mirrored) {
|
|
36079
36219
|
_noteTypingInteraction({ interactionId: record.interactionId, stage: 'mirror-revision', at });
|
|
36220
|
+
// R-7/R-8: exactly the interactions that just got `mirror-revision` also get
|
|
36221
|
+
// `frame-arrived`, at the arrival instant rather than at the instant main finished
|
|
36222
|
+
// stamping -- the difference between the two IS the receiver-loop leg this measures.
|
|
36223
|
+
// R-8's mainSpans are attached by the trace store once BOTH this stamp and the worker's
|
|
36224
|
+
// `frame-posted` are in (either order; see mainSpansForWindow at its construction).
|
|
36225
|
+
if (frameArrivedAt > 0) {
|
|
36226
|
+
_noteTypingInteraction({ interactionId: record.interactionId, stage: 'frame-arrived', at: frameArrivedAt });
|
|
36227
|
+
}
|
|
36080
36228
|
record.mirrored = true;
|
|
36081
36229
|
session._typingInteractionByInputSeq.set(seq, record);
|
|
36082
36230
|
}
|
|
@@ -36434,7 +36582,7 @@ function _terminateResumeLifecycle(session, failure) {
|
|
|
36434
36582
|
agentType: session._resumeAgentType || detectAgentType(session.cmd) || '',
|
|
36435
36583
|
generation,
|
|
36436
36584
|
};
|
|
36437
|
-
markSessionResumeBlocked(session.id, info);
|
|
36585
|
+
markSessionResumeBlocked(session.id, info, 'lifecycle-terminate');
|
|
36438
36586
|
session._resumeAttemptFailed = true;
|
|
36439
36587
|
// This is a system-enforced provider termination, not an explicit user close. The PTY exit
|
|
36440
36588
|
// handler already suppresses auto-restart while `resumeBlocked` is set, and
|
|
@@ -36985,6 +37133,10 @@ const _DETERMINISTIC_PREFLIGHT_CODES = new Set([
|
|
|
36985
37133
|
'missing_claude_transcript',
|
|
36986
37134
|
'missing_codex_rollout',
|
|
36987
37135
|
'agent_cli_not_found',
|
|
37136
|
+
// WP-1 (docs/codex-routed-fork-cold-restore-guard-2026-10-22.html §5 D-4, §9 S-7): a Codex
|
|
37137
|
+
// thread created on a gateway provider no native config load can resolve. Retrying cannot heal it
|
|
37138
|
+
// — the picker (or the journal-backed repair, D-3) is the only way out — so it quarantines.
|
|
37139
|
+
'codex_provider_missing',
|
|
36988
37140
|
]);
|
|
36989
37141
|
|
|
36990
37142
|
// Pure decision (test seam): is a resume-preflight failure DETERMINISTIC (quarantine) or
|
|
@@ -37010,7 +37162,7 @@ function _shouldRefreshLaunchReadinessForBlock(code, blockDeterministic) {
|
|
|
37010
37162
|
return code === 'launch_rollout_index_pending' && blockDeterministic !== true;
|
|
37011
37163
|
}
|
|
37012
37164
|
|
|
37013
|
-
function markSessionResumeBlocked(sessionId, info = {}) {
|
|
37165
|
+
function markSessionResumeBlocked(sessionId, info = {}, site = 'unspecified') {
|
|
37014
37166
|
const id = String(sessionId || '');
|
|
37015
37167
|
if (!id) return null;
|
|
37016
37168
|
const record = {
|
|
@@ -37031,24 +37183,18 @@ function markSessionResumeBlocked(sessionId, info = {}) {
|
|
|
37031
37183
|
checkedAt: info.checkedAt || '',
|
|
37032
37184
|
// WP-2: 'removed'|'never_seen'; only set on the two missing-transcript codes.
|
|
37033
37185
|
localHistory: info.localHistory || (['missing_claude_transcript', 'missing_codex_rollout'].includes(info.code) ? 'unknown' : ''), lastSeenBytes: Number(info.lastSeenBytes) || 0,
|
|
37034
|
-
lastSeenAt: info.lastSeenAt || '', trashPath: info.trashPath || '', originalPath: info.originalPath || '',
|
|
37186
|
+
lastSeenAt: info.lastSeenAt || '', trashPath: info.trashPath || '', originalPath: info.originalPath || '', trashReason: info.trashReason || '', trashedAt: info.trashedAt || '',
|
|
37035
37187
|
// WP-6: the resume ladder's answer, only on the two missing-transcript codes.
|
|
37036
37188
|
resumeRung: info.resumeRung || '', coverage: info.coverage || null, offerStoreFirst: info.offerStoreFirst === true, restoreError: info.restoreError || '',
|
|
37037
37189
|
};
|
|
37038
37190
|
_resumeBlockedSessions.set(id, record);
|
|
37191
|
+
// WP-0 (D-8): one diagnostic names the site that set this block, so a card the user saw is always
|
|
37192
|
+
// attributable to a code path -- including the sites that used to log nothing at all.
|
|
37193
|
+
recordSessionDiagnostic(id, 'resume-blocked-set', resumeBlockSetDetails(site, record));
|
|
37039
37194
|
// WP-2: callers that built an info object inline get the enriched record back instead of re-reading the map.
|
|
37040
37195
|
return record;
|
|
37041
37196
|
}
|
|
37042
37197
|
|
|
37043
|
-
// The ladder fields every resume-blocked message for a missing transcript carries (S-33).
|
|
37044
|
-
function _resumeBlockedRungWire(info) {
|
|
37045
|
-
return {
|
|
37046
|
-
resumeRung: info?.resumeRung || '', coverage: info?.coverage || null, offerStoreFirst: info?.offerStoreFirst === true, restoreError: info?.restoreError || '',
|
|
37047
|
-
deterministic: info?.deterministic === true, origin: info?.origin || 'unknown',
|
|
37048
|
-
storeTurns: normalizeStoreTurns(info?.storeTurns), checkedAt: info?.checkedAt || '',
|
|
37049
|
-
};
|
|
37050
|
-
}
|
|
37051
|
-
|
|
37052
37198
|
// WP-2 (multi-machine §C): the pure relative-origin rule now lives in lib/resume-origin-facts.js, so this
|
|
37053
37199
|
// launch-plan projection, the resume-blocked record and the transcripts route share exactly one rule.
|
|
37054
37200
|
// Re-exported here under the name every existing call site in this file already uses.
|
|
@@ -37083,9 +37229,17 @@ function isSessionResumeBlocked(sessionId) {
|
|
|
37083
37229
|
// explicit user action (attach, click, "Start fresh here") — those paths consult the block
|
|
37084
37230
|
// directly (see `_startDeferredRestoreSession`), not this drain-eligibility predicate.
|
|
37085
37231
|
function _sessionEligibleForAutoResumeDrain(session, isBlockedFn) {
|
|
37232
|
+
// WP-3 (§9 S-19): only the two NON-normal-state arms below are skips (a session merely not being a
|
|
37233
|
+
// drain candidate is not). Zero return-value change; sliced into bare vm contexts by unit tests.
|
|
37086
37234
|
if (!session || !session._restoreDeferred || session._restoreStarting || session._exited) return false;
|
|
37087
|
-
if (!sessionPolicy(session).relaunchFromRestore)
|
|
37088
|
-
|
|
37235
|
+
if (!sessionPolicy(session).relaunchFromRestore) {
|
|
37236
|
+
_noteWakeSkip(session, 'DrainIneligible');
|
|
37237
|
+
return false;
|
|
37238
|
+
}
|
|
37239
|
+
if (typeof isBlockedFn === 'function' && isBlockedFn(session.id)) {
|
|
37240
|
+
_noteWakeSkip(session, 'ResumeBlocked');
|
|
37241
|
+
return false;
|
|
37242
|
+
}
|
|
37089
37243
|
return true;
|
|
37090
37244
|
}
|
|
37091
37245
|
|
|
@@ -37160,16 +37314,16 @@ function _notifyResumePreflightFailure(id, ws, agentType, result) {
|
|
|
37160
37314
|
let blockedInfo = null;
|
|
37161
37315
|
if (deterministic) {
|
|
37162
37316
|
blockedInfo = {
|
|
37163
|
-
...
|
|
37317
|
+
...resumeBlockedRungWire(result), code: result.code || 'resume_blocked', message, agentType, detail: result.detail || '', attempts: Number(result.attempts || 0), deterministic: true,
|
|
37164
37318
|
origin: result.origin || _resumeOriginForSession(id), storeTurns: result.storeTurns, checkedAt: result.checkedAt || new Date().toISOString(),
|
|
37165
|
-
localHistory: result.localHistory, lastSeenBytes: result.lastSeenBytes, lastSeenAt: result.lastSeenAt, trashPath: result.trashPath, originalPath: result.originalPath,
|
|
37319
|
+
localHistory: result.localHistory, lastSeenBytes: result.lastSeenBytes, lastSeenAt: result.lastSeenAt, trashPath: result.trashPath, originalPath: result.originalPath, trashReason: result.trashReason, trashedAt: result.trashedAt,
|
|
37166
37320
|
};
|
|
37167
|
-
blockedInfo = markSessionResumeBlocked(id, blockedInfo) || blockedInfo; // WP-2: the wire carries the enriched record (host + peer + transcript state)
|
|
37321
|
+
blockedInfo = markSessionResumeBlocked(id, blockedInfo, 'preflight') || blockedInfo; // WP-2: the wire carries the enriched record (host + peer + transcript state)
|
|
37168
37322
|
}
|
|
37169
37323
|
if (!ws) return deterministic;
|
|
37170
37324
|
if (deterministic) {
|
|
37171
37325
|
try {
|
|
37172
|
-
_wsSend(ws, JSON.stringify(resumeBlockedWire(id, blockedInfo, { agentType, rungWire:
|
|
37326
|
+
_wsSend(ws, JSON.stringify(resumeBlockedWire(id, blockedInfo, { agentType, rungWire: resumeBlockedRungWire(blockedInfo) })));
|
|
37173
37327
|
} catch {}
|
|
37174
37328
|
} else {
|
|
37175
37329
|
try {
|
|
@@ -37230,7 +37384,7 @@ function _notifyPreparedResumePreflightFailure(id, ws, agentType, resumePrefligh
|
|
|
37230
37384
|
if (MISSING_TRANSCRIPT_CODES.has(resumePreflight.code) && existingSession) {
|
|
37231
37385
|
existingSession._resumeRung = {
|
|
37232
37386
|
code: resumePreflight.code, message: resumePreflight.message || '', resumeId: resumePreflight.resumeId || '',
|
|
37233
|
-
...
|
|
37387
|
+
...resumeBlockedRungWire(resumePreflight), origin: resumePreflight.origin || _resumeOriginForSession(id),
|
|
37234
37388
|
};
|
|
37235
37389
|
}
|
|
37236
37390
|
_notifyResumePreflightFailure(id, ws, agentType, resumePreflight);
|
|
@@ -37268,7 +37422,7 @@ function _blockResumeAttempt(session, code, message, detail) {
|
|
|
37268
37422
|
attempts: Number(attemptState.failures || attemptState.attempt || 1),
|
|
37269
37423
|
agentType: session._resumeAgentType || detectAgentType(session.cmd) || '',
|
|
37270
37424
|
};
|
|
37271
|
-
markSessionResumeBlocked(session.id, info);
|
|
37425
|
+
markSessionResumeBlocked(session.id, info, 'attempt-block');
|
|
37272
37426
|
markResumeFailureExit(session, { code, source: code });
|
|
37273
37427
|
_clearResumeLoading(session, code);
|
|
37274
37428
|
recordSessionDiagnostic(session.id, 'resume-blocked', info);
|
|
@@ -37502,6 +37656,9 @@ const preparedSessionCreate = require('./lib/session-create/prepared').createPre
|
|
|
37502
37656
|
ptyStartupCommitComplete,
|
|
37503
37657
|
_rollbackPtyStartupAttempt,
|
|
37504
37658
|
console,
|
|
37659
|
+
// WP-4 (§5 D-12): the duplicate-create re-answer frame, next to the walle one identity.js already
|
|
37660
|
+
// owns; the module sends it, server.js only builds it.
|
|
37661
|
+
sendNativeSessionCreated: _sendNativeSessionCreated,
|
|
37505
37662
|
get _ownerRecovery() { return _ownerRecovery; },
|
|
37506
37663
|
});
|
|
37507
37664
|
|
|
@@ -37830,6 +37987,8 @@ sessionCreateProvider.configure(() => ({
|
|
|
37830
37987
|
parseSessionStartMs,
|
|
37831
37988
|
path,
|
|
37832
37989
|
recordSessionDiagnostic,
|
|
37990
|
+
// WP-1 (§5 D-4): the deterministic refusal hook the launch-time native-provider gate uses.
|
|
37991
|
+
markSessionResumeBlocked,
|
|
37833
37992
|
resolveResumeGeneration,
|
|
37834
37993
|
// WP-2 (default-subagent-model-delegate-pick-2026-10-13.html §5 D-1..D-3): the Wall-E delegate
|
|
37835
37994
|
// pick reader for a lead launch. Injected here (not required in provider-process) so the launch
|
|
@@ -38108,6 +38267,40 @@ function apiResumeSession(req, res) {
|
|
|
38108
38267
|
if (_ownerDecisionInvalidRequest(ownerDecision)) return sendJsonResponse(res, 400, { ok: false, error: 'SESSION_ID_REQUIRED' });
|
|
38109
38268
|
_emitOwnerMismatchDiagnostic(ownerDecision);
|
|
38110
38269
|
}
|
|
38270
|
+
// WP-6 (S-8/S-25, AC-6d): a session whose ORIGIN is a registered, REACHABLE machine is
|
|
38271
|
+
// resumed ON that machine. The box owns the session and its transcript (D-2), and this hub
|
|
38272
|
+
// has no local copy to launch from — the pre-WP-6 code checked the local sessions Map and
|
|
38273
|
+
// 500'd ("Resume launch did not start"). The forward is a WP-2 create carrying the resume
|
|
38274
|
+
// args through the EXISTING allowlist; not registered / not reachable keeps today's flow,
|
|
38275
|
+
// byte-for-byte (resumeMachineFor returns null).
|
|
38276
|
+
const resumeMachine = remoteMachineCatalog.resumeMachineFor(launch.ctmSessionId || launch.id);
|
|
38277
|
+
if (resumeMachine) {
|
|
38278
|
+
const forwarded = await remoteMachineCatalog.forwardResumeOnMachine({ launch, machine: resumeMachine });
|
|
38279
|
+
if (!forwarded.ok) {
|
|
38280
|
+
res.writeHead(502, { 'Content-Type': 'application/json' });
|
|
38281
|
+
res.end(JSON.stringify({
|
|
38282
|
+
error: `Machine ${resumeMachine.displayName || resumeMachine.instanceId} could not start this resume`,
|
|
38283
|
+
code: forwarded.code || 'machine_unreachable',
|
|
38284
|
+
machine: resumeMachine.instanceId,
|
|
38285
|
+
machineName: resumeMachine.displayName || '',
|
|
38286
|
+
}));
|
|
38287
|
+
return;
|
|
38288
|
+
}
|
|
38289
|
+
res.writeHead(201, { 'Content-Type': 'application/json' });
|
|
38290
|
+
res.end(JSON.stringify({
|
|
38291
|
+
ok: true,
|
|
38292
|
+
id: forwarded.sessionId,
|
|
38293
|
+
tab_id: forwarded.sessionId,
|
|
38294
|
+
source_ctm_session_id: launch.ctmSessionId,
|
|
38295
|
+
session_id: launch.resumeSessionId,
|
|
38296
|
+
agent: launch.agentType,
|
|
38297
|
+
cwd: launch.cwd,
|
|
38298
|
+
title: launch.title,
|
|
38299
|
+
machine: resumeMachine.instanceId,
|
|
38300
|
+
machineName: resumeMachine.displayName || '',
|
|
38301
|
+
}));
|
|
38302
|
+
return;
|
|
38303
|
+
}
|
|
38111
38304
|
await handleCreatePrepared(null, {
|
|
38112
38305
|
id: launch.id,
|
|
38113
38306
|
title: launch.title,
|
|
@@ -38985,6 +39178,32 @@ const _serverMemorySampler = startServerMemorySamplerRuntime({
|
|
|
38985
39178
|
}
|
|
38986
39179
|
return out;
|
|
38987
39180
|
},
|
|
39181
|
+
// WP-A (freeze-probes) P-2/P-3: what was in flight when this row was taken -- the read-pool workers
|
|
39182
|
+
// currently occupied, both db-owner clients' oldest pending ops, and the main loop's recent spans --
|
|
39183
|
+
// plus the CATEGORIZED footprint regions of the main pid, so a 4<->9 GB "Malloc Large" step is
|
|
39184
|
+
// attributed instead of merely recorded. Measurement only: no gates, no behaviour change.
|
|
39185
|
+
inFlightSource: () => {
|
|
39186
|
+
const rp = _ctmReadPool?.getStatus?.();
|
|
39187
|
+
const readPool = [];
|
|
39188
|
+
for (const [i, w] of (rp?.workers || []).entries()) {
|
|
39189
|
+
if (!w?.currentOp) continue;
|
|
39190
|
+
readPool.push({ w: w.id ?? i, op: w.currentOp, ageMs: w.currentOpAgeMs, n: w.occupiedCount });
|
|
39191
|
+
}
|
|
39192
|
+
return {
|
|
39193
|
+
readPool,
|
|
39194
|
+
dbOwner: {
|
|
39195
|
+
base: _ctmDbWorker?.getStatus?.()?.pendingOps ?? [],
|
|
39196
|
+
maintenance: _ctmDbMaintenanceWorker?.getStatus?.()?.pendingOps ?? [],
|
|
39197
|
+
},
|
|
39198
|
+
// getRecentSpans() -> { recent: [{name, dur, start, end}], top: [{name, ms}] }; only the
|
|
39199
|
+
// main-loop spans worth naming (>= 50 ms), newest first, capped at 8.
|
|
39200
|
+
mainSpans: (_perfTracker?.getRecentSpans?.()?.recent || [])
|
|
39201
|
+
.map((span) => ({ name: span.name, ms: Math.round(span.dur) }))
|
|
39202
|
+
.filter((span) => span.ms >= 50)
|
|
39203
|
+
.slice(0, 8),
|
|
39204
|
+
};
|
|
39205
|
+
},
|
|
39206
|
+
regionSampler: _sessionResourceSampler,
|
|
38988
39207
|
sessionHostManager,
|
|
38989
39208
|
desktopIndex: _desktopIndex,
|
|
38990
39209
|
footprintSampler: _sessionResourceSampler,
|
|
@@ -39132,7 +39351,7 @@ function _terminalSnapshotProtected(session) {
|
|
|
39132
39351
|
const hasViewer = !!session?._primaryWs
|
|
39133
39352
|
|| (Array.isArray(session?.clients) && session.clients.some((client) => client?.readyState === 1));
|
|
39134
39353
|
const resumable = session?._restoreDeferred === true
|
|
39135
|
-
||
|
|
39354
|
+
|| String(session?._lifecycleState || '') === 'parked';
|
|
39136
39355
|
return hasViewer || resumable;
|
|
39137
39356
|
}
|
|
39138
39357
|
|
|
@@ -40176,7 +40395,9 @@ function sendDeferredRestoreStatus(ws, session, sessionId, cols, rows, source, o
|
|
|
40176
40395
|
id: sessionId,
|
|
40177
40396
|
cached: served,
|
|
40178
40397
|
starting,
|
|
40179
|
-
|
|
40398
|
+
// WP-3 (§5 D-7): fill the existing, always-'' field with the session's last skip reason
|
|
40399
|
+
// (no new wire name, no client change; an explicit caller reason still wins).
|
|
40400
|
+
reason: String(opts.reason || session?._lastRestoreSkip?.reason || ''),
|
|
40180
40401
|
message: opts.message || (starting
|
|
40181
40402
|
? 'Starting terminal process for the visible session.'
|
|
40182
40403
|
: 'Session terminal process is deferred until this tab is activated.'),
|
|
@@ -40186,9 +40407,10 @@ function sendDeferredRestoreStatus(ws, session, sessionId, cols, rows, source, o
|
|
|
40186
40407
|
}
|
|
40187
40408
|
|
|
40188
40409
|
// Extracted to lib/restore-prepare-retry.js (createScheduleDeferredRestoreStart); semantics unchanged.
|
|
40189
|
-
|
|
40410
|
+
// WP-3 (§9 S-16): onSkip is the wakeSkips sink -- reason plumbing only, no return value changes.
|
|
40411
|
+
const _scheduleDeferredRestoreStart = createScheduleDeferredRestoreStart({ getSession, startDeferredRestoreSession: (session, opts) => _startDeferredRestoreSession(session, opts), onSkip: _restoreWakeSkipSink });
|
|
40190
40412
|
|
|
40191
|
-
const _scheduleDeferredRestoreRetry = createScheduleDeferredRestoreRetry({ getSession, recordSessionDiagnostic, scheduleStart: _scheduleDeferredRestoreStart }); // WP-2 (D-4)
|
|
40413
|
+
const _scheduleDeferredRestoreRetry = createScheduleDeferredRestoreRetry({ getSession, recordSessionDiagnostic, scheduleStart: _scheduleDeferredRestoreStart, onSkip: _restoreWakeSkipSink }); // WP-2 (D-4)
|
|
40192
40414
|
|
|
40193
40415
|
function _maybeAutoStartDeferredRestore(session, msg, source, ws) {
|
|
40194
40416
|
if (!session?._restoreDeferred || !msg?.autoStartRestore) return false;
|
|
@@ -40201,6 +40423,9 @@ function _maybeAutoStartDeferredRestore(session, msg, source, ws) {
|
|
|
40201
40423
|
reason: msg.prewarm === true ? 'prewarm-disabled' : 'background-client',
|
|
40202
40424
|
clientKind: _lifecycleClientKind(ws, msg),
|
|
40203
40425
|
});
|
|
40426
|
+
// WP-3 (§9 S-19 / D-8 AutoStartNotAttach): same latch as every other skip -- a background
|
|
40427
|
+
// client that keeps re-attaching records once per episode, not once per message.
|
|
40428
|
+
_noteWakeSkip(session, 'AutoStartNotAttach', { autostartBlock: msg.prewarm === true ? 'prewarm-disabled' : 'background-client' });
|
|
40204
40429
|
return false;
|
|
40205
40430
|
}
|
|
40206
40431
|
if (!shouldAutoStartDeferredRestore(session, msg, source)) {
|
|
@@ -41082,12 +41307,12 @@ function handleResumeRetry(ws, msg) {
|
|
|
41082
41307
|
// A quarantine record gets its full facts again; no record means there is nothing blocking.
|
|
41083
41308
|
const record = id ? getSessionResumeBlocked(id) : null;
|
|
41084
41309
|
const send = (reply) => { if (ws) try { _wsSend(ws, JSON.stringify(reply)); } catch { /* expected: the asking socket closed; the next attach replays the record */ } };
|
|
41085
|
-
const wire = (r) => resumeBlockedWire(id, r, { agentType: session?.agentType, rungWire:
|
|
41310
|
+
const wire = (r) => resumeBlockedWire(id, r, { agentType: session?.agentType, rungWire: resumeBlockedRungWire(r) });
|
|
41086
41311
|
if (record) { // Check again re-asks the ladder for any blocked record; a stale record is never replayed as the answer
|
|
41087
41312
|
Promise.resolve().then(() => { // every outcome replies: a throw anywhere below answers with the unchanged (stale) record
|
|
41088
41313
|
const agent = dbModule.getSessionIdentity(id)?.primaryAgent || {};
|
|
41089
41314
|
return require('./lib/resume-preflight').recheckResumeBlockedRecord(record, { resolveRung: (input) => require('./lib/resume-from-store-client').resolveResumeRungOffThread({ ...input, dbPath: dbModule.getDbPath(), trashRoot: SESSION_TRANSCRIPT_TRASH_ROOT }), input: { ctmSessionId: id, agentSessionId: String(agent.agent_session_id || ''), provider: record.agentType || session?.agentType, jsonlPath: String(agent.jsonl_path || '') } });
|
|
41090
|
-
}).then((fresh) => { markSessionResumeBlocked(id, fresh); send(wire(getSessionResumeBlocked(id) || fresh)); })
|
|
41315
|
+
}).then((fresh) => { markSessionResumeBlocked(id, fresh, 'resume-retry-recheck'); send(wire(getSessionResumeBlocked(id) || fresh)); })
|
|
41091
41316
|
.catch((error) => { console.warn(`[resume-retry] recheck failed for ${id.slice(0, 8)}: ${error && error.message}`); send(wire(record)); });
|
|
41092
41317
|
} else send(record ? wire(record) : { type: 'resume-retry-noop', id, checkedAt: new Date().toISOString() });
|
|
41093
41318
|
return false;
|
|
@@ -41693,11 +41918,11 @@ function _imageDeliveryAckWindowMs() {
|
|
|
41693
41918
|
}
|
|
41694
41919
|
|
|
41695
41920
|
// The classified failure reason a refused/no-op image write leaves on the draft. Kept next to the
|
|
41696
|
-
// delivery path so
|
|
41697
|
-
//
|
|
41921
|
+
// delivery path so refusal, uncertain dispatch and a missing host ack stay distinct rather than
|
|
41922
|
+
// claiming that every failed publication was known not to have left this process.
|
|
41698
41923
|
function _imageWriteFailureReason(error) {
|
|
41699
41924
|
const code = String((error && error.code) || '');
|
|
41700
|
-
if (code === 'pty_write_not_posted') return code;
|
|
41925
|
+
if (code === 'pty_write_not_posted' || code === 'pty_dispatch_uncertain') return code;
|
|
41701
41926
|
if (code.startsWith('pty_write_ack_')) return code.slice(0, 64);
|
|
41702
41927
|
return 'Provider delivery could not be started';
|
|
41703
41928
|
}
|
|
@@ -41781,14 +42006,14 @@ async function _deliverImageDraftAttempt(sessionId, attachmentId, deliveryAttemp
|
|
|
41781
42006
|
// draw from, so this frame's ack number is unique for the session and can never be confused with a
|
|
41782
42007
|
// keystroke's echo identity (which is tracked by map membership, not by a numeric range).
|
|
41783
42008
|
const writeResult = _writeLifecyclePty(session, payload, 'image-input');
|
|
41784
|
-
// A POST is not a write
|
|
41785
|
-
//
|
|
41786
|
-
//
|
|
41787
|
-
// branch discarded it) can never be acked, and the draft must not conclude that it was delivered:
|
|
41788
|
-
// take the EXISTING failure transition (the catch below rolls the attempt back to 'failed').
|
|
42009
|
+
// A POST is not a byte write. Only an observed preflight refusal means this invocation
|
|
42010
|
+
// did not enter a transport; an exception after publication may still have effects.
|
|
42011
|
+
// Keep the existing failure transition, but never label uncertain dispatch "not posted".
|
|
41789
42012
|
if (!writeResult || writeResult.posted !== true) {
|
|
41790
|
-
const
|
|
41791
|
-
error
|
|
42013
|
+
const refused = writeResult?.dispatchDisposition === 'preflight-refused';
|
|
42014
|
+
const error = new Error(refused ? 'Image paste frame was not posted to the host PTY'
|
|
42015
|
+
: 'Image paste dispatch could not be confirmed');
|
|
42016
|
+
error.code = refused ? 'pty_write_not_posted' : 'pty_dispatch_uncertain';
|
|
41792
42017
|
throw error;
|
|
41793
42018
|
}
|
|
41794
42019
|
// §6.3/D6: a PATH-mode draft reaches 'attached' only on the host's actual written ack — a
|
|
@@ -44792,7 +45017,7 @@ function handleKill(ws, msg) {
|
|
|
44792
45017
|
async function handleKillAsync(ws, msg) {
|
|
44793
45018
|
if (_ownerRecovery.rejectRequest(ws, msg)) return null; const session = sessions.get(msg.id);
|
|
44794
45019
|
if (!session) return;
|
|
44795
|
-
const ownerDecision =
|
|
45020
|
+
const ownerDecision = _sessionOwnerDecision(msg.id);
|
|
44796
45021
|
if (!ownerDecision.ok) {
|
|
44797
45022
|
if (_ownerDecisionInvalidRequest(ownerDecision)) return;
|
|
44798
45023
|
_emitOwnerMismatchDiagnostic(ownerDecision);
|
|
@@ -44824,18 +45049,10 @@ async function handleKillAsync(ws, msg) {
|
|
|
44824
45049
|
broadcastSessionList(true);
|
|
44825
45050
|
return;
|
|
44826
45051
|
}
|
|
44827
|
-
if (session._lifecycleState === 'frozen') {
|
|
44828
|
-
const thaw = _thawLifecycleSession(session, {
|
|
44829
|
-
source: 'user-close',
|
|
44830
|
-
preserveIdle: true,
|
|
44831
|
-
broadcast: false,
|
|
44832
|
-
});
|
|
44833
|
-
if (!thaw.ok) {
|
|
44834
|
-
console.warn(`[lifecycle] close-thaw failed id=${String(msg.id).slice(0, 8)} code=${thaw.code || 'unknown'}`);
|
|
44835
|
-
}
|
|
44836
|
-
}
|
|
44837
45052
|
session._intentionalKill = true;
|
|
44838
45053
|
session._userCloseAccepted = true; // inventory stops listing it now; the kill always ends in exit + delete
|
|
45054
|
+
// D-6: announce the close before anything is awaited, so no racing list can put the tab back.
|
|
45055
|
+
broadcastSessionList(true);
|
|
44839
45056
|
if (RESUME_LIFECYCLE_V2_ENABLED && session._resumeAttemptState) {
|
|
44840
45057
|
_resumeLifecycleCoordinator.dispatch(session, {
|
|
44841
45058
|
type: RESUME_EVENT.CANCEL,
|
|
@@ -44855,14 +45072,12 @@ async function handleKillAsync(ws, msg) {
|
|
|
44855
45072
|
broadcastSessionList(true);
|
|
44856
45073
|
return;
|
|
44857
45074
|
}
|
|
44858
|
-
//
|
|
44859
|
-
//
|
|
44860
|
-
//
|
|
44861
|
-
|
|
44862
|
-
// the row at status='running' and the session would be resurrected by restore on
|
|
44863
|
-
// the next boot. The onExit retire stays as an idempotent backstop.
|
|
44864
|
-
await _retireStartupTaskForUserClose(msg.id, 'kill');
|
|
45075
|
+
// D-6: submit the durable startup_task retire FIRST — the op FIFO-queues on the DB owner, so a
|
|
45076
|
+
// restart racing this exit still finds it queued — then signal the PTY at once and await the
|
|
45077
|
+
// retire after. The PTY onExit retire stays as the idempotent backstop.
|
|
45078
|
+
const retire = _retireStartupTaskForUserClose(msg.id, 'kill');
|
|
44865
45079
|
try { _killSessionProcess(session, 'user_close'); } catch { /* expected: a PTY already gone on user close must not fail the close handler */ }
|
|
45080
|
+
await retire;
|
|
44866
45081
|
}
|
|
44867
45082
|
|
|
44868
45083
|
// Delegates to lib/session-workspace-binding.js's TTL-memoized safeRealpath (60s TTL, bounded
|
|
@@ -47115,7 +47330,10 @@ function handleList(ws) {
|
|
|
47115
47330
|
const listPayload = JSON.stringify({
|
|
47116
47331
|
type: 'sessions',
|
|
47117
47332
|
restoring: _restoreInProgress,
|
|
47118
|
-
|
|
47333
|
+
// WP-6 (S-3/S-4, AC-6c): local live rows ∪ remote rows, deduped by ctm session id with a LOCAL
|
|
47334
|
+
// id always winning. With zero registered machines mergeSessionRows returns this exact array, so
|
|
47335
|
+
// the payload is byte-identical to the pre-WP-6 one.
|
|
47336
|
+
sessions: remoteMachineCatalog.mergeSessionRows(_listableSessions().map(s => _sessionPayload(s, { lightweight: true }))),
|
|
47119
47337
|
});
|
|
47120
47338
|
_wsSend(ws, listPayload, { class: 'interactive', channel: 'sessions' });
|
|
47121
47339
|
_kickPendingAutoResumeDeferred('client-list');
|
|
@@ -48761,7 +48979,7 @@ function _sessionPayload(s, options = {}) {
|
|
|
48761
48979
|
// WP-2: only set on the two missing-transcript codes.
|
|
48762
48980
|
resumeBlockedLocalHistory: resumeBlocked?.localHistory || '', resumeBlockedLastSeenBytes: Math.max(0, Number(resumeBlocked?.lastSeenBytes || 0)),
|
|
48763
48981
|
resumeBlockedLastSeenAt: resumeBlocked?.lastSeenAt || '', resumeBlockedTrashPath: resumeBlocked?.trashPath || '',
|
|
48764
|
-
resumeBlockedOriginalPath: resumeBlocked?.originalPath || '',
|
|
48982
|
+
resumeBlockedOriginalPath: resumeBlocked?.originalPath || '', resumeBlockedTrashReason: resumeBlocked?.trashReason || '', resumeBlockedTrashedAt: resumeBlocked?.trashedAt || '',
|
|
48765
48983
|
resumeBlockedRung: resumeBlocked?.resumeRung || '', resumeBlockedCoverage: resumeBlocked?.coverage || null,
|
|
48766
48984
|
resumeBlockedOfferStoreFirst: resumeBlocked?.offerStoreFirst === true, resumeBlockedRestoreError: resumeBlocked?.restoreError || '',
|
|
48767
48985
|
resumeBlockedOrigin: resumeBlocked?.origin || 'unknown',
|
|
@@ -50290,8 +50508,13 @@ const _broadcastPayload = require('./lib/broadcast-serialize').createBroadcastPa
|
|
|
50290
50508
|
// `arr` is the session snapshot the payloads were built from (used for the elision signature so it stays
|
|
50291
50509
|
// consistent with `built` even if the live sessions Map mutated during a sliced build's yields).
|
|
50292
50510
|
function _finishBroadcast(arr, built, fresh) {
|
|
50293
|
-
|
|
50294
|
-
|
|
50511
|
+
// WP-6 (S-3/S-4): the same merge as handleList, so a broadcast row set matches a requested list.
|
|
50512
|
+
// `sourceSessions: arr` stays the LOCAL snapshot (it is the elision signature's subject); the
|
|
50513
|
+
// remote rows are appended after the merge and are cheap, already-built objects.
|
|
50514
|
+
const merged = remoteMachineCatalog.mergeSessionRows(built);
|
|
50515
|
+
return _broadcastPayload.broadcast({ type: 'sessions', restoring: _restoreInProgress, sessions: merged }, wss.clients,
|
|
50516
|
+
{ fresh, elideNoop: _BROADCAST_ELIDE_NOOP, sourceSessions: arr, extraSignature: remoteMachineCatalog.lastMergeSignature(),
|
|
50517
|
+
isMemoBypass: s => _lightPayloadMemo.isMemoBypass(s) });
|
|
50295
50518
|
}
|
|
50296
50519
|
|
|
50297
50520
|
function _maybePrewarmRestoreBroadcastPayloadCaches(sessionList) {
|
|
@@ -51287,9 +51510,6 @@ async function _quiesceSessionsForProcessExit(reason = 'shutdown') {
|
|
|
51287
51510
|
// REPORT: best-effort diagnostic write -- must never block shutdown teardown.
|
|
51288
51511
|
errorSink.report(`${reason}:persist-last-footprint`, error, { sessionId: session.id });
|
|
51289
51512
|
}
|
|
51290
|
-
if (session._lifecycleState === 'frozen') {
|
|
51291
|
-
_thawLifecycleSession(session, { source: reason, preserveIdle: true, broadcast: false });
|
|
51292
|
-
}
|
|
51293
51513
|
// R-2: do NOT mislabel a hosted session we are deliberately preserving across a
|
|
51294
51514
|
// restart as intentionally killed -- that flag is read by the next boot's exit-vs-
|
|
51295
51515
|
// crash classification, and this session's process never actually died here.
|
|
@@ -51385,7 +51605,8 @@ async function _quiesceSessionsForProcessExit(reason = 'shutdown') {
|
|
|
51385
51605
|
}
|
|
51386
51606
|
|
|
51387
51607
|
// --- Graceful Shutdown ---
|
|
51388
|
-
|
|
51608
|
+
// R3 WP-2: `signal` ('SIGTERM'/'SIGINT', passed by Node) is recorded in the clean-shutdown marker's reason.
|
|
51609
|
+
function shutdown(signal) {
|
|
51389
51610
|
if (_isShuttingDown) return;
|
|
51390
51611
|
console.log('\n Shutting down...');
|
|
51391
51612
|
_isShuttingDown = true;
|
|
@@ -51467,6 +51688,8 @@ function shutdown() {
|
|
|
51467
51688
|
new Promise((resolve) => { const t = setTimeout(resolve, 3000); if (t && typeof t.unref === 'function') t.unref(); }),
|
|
51468
51689
|
]);
|
|
51469
51690
|
} catch (e) { console.error('[shutdown] walle-transcript flush failed:', e.message); }
|
|
51691
|
+
let inFlight;
|
|
51692
|
+
try { inFlight = _collectInFlightSessionsForShutdown(); } catch { inFlight = undefined; /* expected: exit must never be blocked; marker omits inFlight */ }
|
|
51470
51693
|
await _quiesceSessionsForProcessExit('shutdown');
|
|
51471
51694
|
try { await headlessWorker.terminate({ intent: 'restart' }); } catch {} // hosts linger through SIGTERM (WP-2)
|
|
51472
51695
|
_ctmDbClosingForProcessExit = true;
|
|
@@ -51480,6 +51703,9 @@ function shutdown() {
|
|
|
51480
51703
|
if (_imageWriteProofs) _imageWriteProofs.dispose();
|
|
51481
51704
|
try { await _desktopIndex.stop(); } catch {}
|
|
51482
51705
|
const dbExit = await _closeDbForProcessExit('shutdown'); // D-7
|
|
51706
|
+
// R3 WP-2: a graceful signal stop is a clean exit, marked after the DB close; fatal/SIGKILL stay unmarked.
|
|
51707
|
+
const reason = `shutdown:${typeof signal === 'string' && signal ? signal : 'unknown'}`;
|
|
51708
|
+
_restartLedger.writeCleanShutdownMarker({ exitCode: 0, reason, oldPid: process.pid, dbClose: _dbCloseOutcome(dbExit), inFlight });
|
|
51483
51709
|
try { await managedBrowserBroker.close(); } catch (e) { console.error('[shutdown] managed browser close failed:', e.message); }
|
|
51484
51710
|
try { await runtimeGoalStore.close?.(); } catch (e) { console.error('[shutdown] runtime goal store close failed:', e.message); }
|
|
51485
51711
|
// Kill Wall-E child so it doesn't outlive us (detached + unref'd = orphan
|
|
@@ -51664,7 +51890,21 @@ function _startSdWatchdog() {
|
|
|
51664
51890
|
function _startHostedSyncWorker() {
|
|
51665
51891
|
if (_hostedSyncWorker) return;
|
|
51666
51892
|
try {
|
|
51667
|
-
const worker = startHostedSyncWorker({
|
|
51893
|
+
const worker = startHostedSyncWorker({
|
|
51894
|
+
env: process.env,
|
|
51895
|
+
db: dbModule,
|
|
51896
|
+
fetch: global.fetch,
|
|
51897
|
+
// WP-2 / P-8: the machine probe's credential is chosen PER TARGET — the shared pool secret for
|
|
51898
|
+
// a pool member, the legacy env bearer for a box that never joined. The builder is injected
|
|
51899
|
+
// from here because lib/remote-machine-create.js requires lib/hosted-sync-worker.js (for
|
|
51900
|
+
// parseSyncPeers), so the worker requiring it back would close a lib/** dependency cycle
|
|
51901
|
+
// (dependency-lint ceiling 0).
|
|
51902
|
+
peerHeadersFor: (instanceId) => require('./lib/remote-machine-create').buildPeerControlHeaders(process.env, { instanceId }) || {},
|
|
51903
|
+
// WP-2 / P-5: this machine's own minted id for the sweep's roster pull (a member never writes
|
|
51904
|
+
// its own row). Injected from here for the same cycle reason; read lazily so it is the id the
|
|
51905
|
+
// boot ladder actually minted, not a pre-DB guess.
|
|
51906
|
+
selfInstanceId: () => _instanceIdentityAccess.getInstanceIdentity({ required: false })?.instanceId || '',
|
|
51907
|
+
});
|
|
51668
51908
|
_hostedSyncWorker = worker;
|
|
51669
51909
|
if (worker.started) console.log(`[sync] hosted sync worker started for ${worker.peers.length} peer(s)`);
|
|
51670
51910
|
} catch (err) {
|
|
@@ -51907,10 +52147,20 @@ function _executeHook(hook, data) {
|
|
|
51907
52147
|
}
|
|
51908
52148
|
}
|
|
51909
52149
|
|
|
51910
|
-
|
|
52150
|
+
// D-7 (WP-5, docs/clock-free-tests-followups-2026-10-23.html §6) + r3 R-3/R-4
|
|
52151
|
+
// (docs/clock-free-tests-followups-r3-2026-10-24.html §5): EVERY GET path that reports whether
|
|
52152
|
+
// Wall-E is up — /api/services/status, /api/instance/manifest, /api/instance/sync-status and the
|
|
52153
|
+
// hosted runtime context behind /readyz, setup-status and friends — passes { snapshot: true } and
|
|
52154
|
+
// gets the supervisor's cached status (revalidated with kill(pid, 0), single-flight on a miss)
|
|
52155
|
+
// instead of running kill-authority discovery — a whole-table `ps` — on every request. The default
|
|
52156
|
+
// getStatus() is left to the paths that need fresh ownership: kill and repair (lib/walle-repair.js)
|
|
52157
|
+
// and the walle-bridge unavailable context (getWalleSupervisorStatus), which is a failure path.
|
|
52158
|
+
async function _getWalleStatusSafe({ snapshot = false } = {}) {
|
|
51911
52159
|
let walleStatus;
|
|
51912
52160
|
try {
|
|
51913
|
-
walleStatus =
|
|
52161
|
+
walleStatus = snapshot
|
|
52162
|
+
? await walleSupervisor.getStatusSnapshot()
|
|
52163
|
+
: await walleSupervisor.getStatus();
|
|
51914
52164
|
} catch (err) {
|
|
51915
52165
|
walleStatus = { running: false, pid: null, error: err.message };
|
|
51916
52166
|
}
|
|
@@ -51936,6 +52186,9 @@ function _buildInstanceManifest(walleStatus, ctmUptime) {
|
|
|
51936
52186
|
golivePlan: _golivePlanCache,
|
|
51937
52187
|
observedOrigin: _lastMicrosoftTunnelOrigin,
|
|
51938
52188
|
nowMs: Date.now(),
|
|
52189
|
+
// WP-2a D-4: the machine-owned settings come from the in-memory cache (lib/machine-self.js),
|
|
52190
|
+
// never a DB read on the manifest/ETag path.
|
|
52191
|
+
machineSelf: require('./lib/machine-self').current(),
|
|
51939
52192
|
});
|
|
51940
52193
|
}
|
|
51941
52194
|
|
|
@@ -52208,7 +52461,7 @@ function _storageRiskSnapshot() {
|
|
|
52208
52461
|
|
|
52209
52462
|
async function _buildHostedRuntimeContextUncached() {
|
|
52210
52463
|
const ctmUptime = Math.floor((Date.now() - _ctmStartTime) / 1000);
|
|
52211
|
-
const walleStatus = await _getWalleStatusSafe();
|
|
52464
|
+
const walleStatus = await _getWalleStatusSafe({ snapshot: true }); // R-4: never the full ownership probe or the recovery await
|
|
52212
52465
|
const instance = _buildInstanceManifest(walleStatus, ctmUptime);
|
|
52213
52466
|
const envWorkspaceBindings = buildWorkspaceBindingsStatus({ manifest: instance, env: process.env });
|
|
52214
52467
|
void _detachOwnerMutation('hosted-workspace-bindings-seed', () =>
|
|
@@ -52223,7 +52476,9 @@ async function _buildHostedRuntimeContextUncached() {
|
|
|
52223
52476
|
const localEdge = buildLocalEdgeStatus({ manifest: instance, env: process.env });
|
|
52224
52477
|
const syncSchema = hostedStorage.syncSchema;
|
|
52225
52478
|
syncSchema.worker = _hostedSyncWorkerStatus();
|
|
52226
|
-
|
|
52479
|
+
// S-30 (WP-0): the machine registry's rollup rides the sync status. Machine rows are read on
|
|
52480
|
+
// the main thread (they are one indexed SELECT), so no owner round-trip is needed here.
|
|
52481
|
+
const sync = buildInstanceSyncStatus({ manifest: instance, workspaceBindings, localEdge, env: process.env, schema: syncSchema, machines: machineRegistry.summarizeMachines({ db: dbModule }) });
|
|
52227
52482
|
const security = await _buildRemoteSecuritySummary(instance);
|
|
52228
52483
|
const terminalUx = buildTerminalRemoteUxStatus({ manifest: instance, env: process.env });
|
|
52229
52484
|
const deadMan = buildDeadManStatus({ env: process.env });
|
|
@@ -52306,7 +52561,7 @@ function _manifestCorsHeaders(req) {
|
|
|
52306
52561
|
|
|
52307
52562
|
async function apiInstanceManifest(req, res) {
|
|
52308
52563
|
const ctmUptime = Math.floor((Date.now() - _ctmStartTime) / 1000);
|
|
52309
|
-
const walleStatus = await _getWalleStatusSafe();
|
|
52564
|
+
const walleStatus = await _getWalleStatusSafe({ snapshot: true }); // D-7 (WP-5): status GETs read the snapshot, not the process table
|
|
52310
52565
|
const manifest = _buildInstanceManifest(walleStatus, ctmUptime);
|
|
52311
52566
|
sendConditionalJson(req, res, 200, manifest, _manifestCorsHeaders(req), { etagBody: _manifestEtagBody(manifest) }); // D-7 fix A: the tag skips per-read noise the phone never reads
|
|
52312
52567
|
}
|
|
@@ -53014,8 +53269,6 @@ function fetchMcpGatewayLedgerStatus(timeoutMs = 300) {
|
|
|
53014
53269
|
async function apiServicesStatus(req, res) {
|
|
53015
53270
|
const {
|
|
53016
53271
|
ctmUptime,
|
|
53017
|
-
walleStatus,
|
|
53018
|
-
instance,
|
|
53019
53272
|
sync,
|
|
53020
53273
|
workspaceBindings,
|
|
53021
53274
|
localEdge,
|
|
@@ -53024,6 +53277,8 @@ async function apiServicesStatus(req, res) {
|
|
|
53024
53277
|
deadMan,
|
|
53025
53278
|
hostedReadiness,
|
|
53026
53279
|
} = await _buildHostedRuntimeContext();
|
|
53280
|
+
const walleStatus = await _getWalleStatusSafe({ snapshot: true }); // R-3: per request, never the 20 s hosted context
|
|
53281
|
+
const instance = _buildInstanceManifest(walleStatus, ctmUptime); // R-3: the manifest block uses the same snapshot
|
|
53027
53282
|
const mcpGateway = await fetchMcpGatewayStatus();
|
|
53028
53283
|
const walleRuntimeStatus = walleStatus.running
|
|
53029
53284
|
? await getWalleStatusDataOrNull(500)
|
|
@@ -53664,6 +53919,22 @@ function _recordWorktreeCreateJobProgress(job, event) {
|
|
|
53664
53919
|
};
|
|
53665
53920
|
job.progress = progress;
|
|
53666
53921
|
job.updatedAt = now;
|
|
53922
|
+
// WP-4 (nightly-browser-tier-round2-2026-10-21 §6): this is the ONE chokepoint every
|
|
53923
|
+
// job transition passes through, so the log lines live here instead of in a response
|
|
53924
|
+
// observer in lib/api/worktrees.js -- that observer only saw the two dispatch routes,
|
|
53925
|
+
// so the GET poll route and 9 of 13 transitions never logged at all. One line per
|
|
53926
|
+
// distinct (stage, status); the transition is recorded on the job so a repeated
|
|
53927
|
+
// progress event (same stage+status) does not repeat the line.
|
|
53928
|
+
if (job._loggedStage !== progress.stage || job._loggedStatus !== progress.status) {
|
|
53929
|
+
job._loggedStage = progress.stage;
|
|
53930
|
+
job._loggedStatus = progress.status;
|
|
53931
|
+
const line = `[worktree-job] id=${job.id} stage=${progress.stage} status=${progress.status} elapsedMs=${progress.elapsedMs}`
|
|
53932
|
+
+ (progress.stage === 'queued'
|
|
53933
|
+
? ` name=${JSON.stringify(job.name)} namespace=${job.namespace || ''} cwd=${JSON.stringify(job.cwd || '')}`
|
|
53934
|
+
: '');
|
|
53935
|
+
if (progress.status === 'failed' || progress.status === 'cancelled') console.warn(line);
|
|
53936
|
+
else console.log(line);
|
|
53937
|
+
}
|
|
53667
53938
|
job.events.push({
|
|
53668
53939
|
at: now,
|
|
53669
53940
|
stage: progress.stage,
|
|
@@ -54307,15 +54578,24 @@ function apiAdoptBranch(req, res) {
|
|
|
54307
54578
|
});
|
|
54308
54579
|
}
|
|
54309
54580
|
|
|
54581
|
+
// A dispatch refused before a job exists never reaches _recordWorktreeCreateJobProgress,
|
|
54582
|
+
// so it logs its own one-liner with the same [worktree-job] prefix: a red browser artifact
|
|
54583
|
+
// can then tell "CTM refused the dispatch" apart from "the job started and never advanced".
|
|
54584
|
+
function _logWorktreeCreateJobRejected(code, message) {
|
|
54585
|
+
console.warn(`[worktree-job] rejected code=${code} error=${JSON.stringify(String(message || ''))}`);
|
|
54586
|
+
}
|
|
54587
|
+
|
|
54310
54588
|
function apiCreateWorktree(req, res) {
|
|
54311
54589
|
_worktreeReadJsonBody(req, res, async (payload) => {
|
|
54312
54590
|
const reqUrl = new URL(req.url, 'http://localhost');
|
|
54313
54591
|
const { name, base_branch, cwd: reqCwd, namespace, agentType } = payload;
|
|
54314
54592
|
if (!name || !/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
54593
|
+
_logWorktreeCreateJobRejected('invalid_name', 'Invalid worktree name. Use alphanumeric, hyphens, underscores only.');
|
|
54315
54594
|
res.writeHead(400, WORKTREE_JSON_HEADERS);
|
|
54316
54595
|
return res.end(JSON.stringify({ error: 'Invalid worktree name. Use alphanumeric, hyphens, underscores only.' }));
|
|
54317
54596
|
}
|
|
54318
54597
|
if (base_branch && !VALID_GIT_REF.test(base_branch)) {
|
|
54598
|
+
_logWorktreeCreateJobRejected('invalid_base_branch', 'Invalid base branch name');
|
|
54319
54599
|
res.writeHead(400, WORKTREE_JSON_HEADERS);
|
|
54320
54600
|
return res.end(JSON.stringify({ error: 'Invalid base branch name' }));
|
|
54321
54601
|
}
|
|
@@ -54327,6 +54607,7 @@ function apiCreateWorktree(req, res) {
|
|
|
54327
54607
|
const rawRequestId = payload.request_id || payload.requestId || '';
|
|
54328
54608
|
const requestId = normalizeWorktreeCreateRequestId(rawRequestId);
|
|
54329
54609
|
if (rawRequestId && !requestId) {
|
|
54610
|
+
_logWorktreeCreateJobRejected('invalid_request_id', 'Invalid worktree create request id.');
|
|
54330
54611
|
res.writeHead(400, WORKTREE_JSON_HEADERS);
|
|
54331
54612
|
return res.end(JSON.stringify({ error: 'Invalid worktree create request id.' }));
|
|
54332
54613
|
}
|
|
@@ -54342,6 +54623,7 @@ function apiCreateWorktree(req, res) {
|
|
|
54342
54623
|
job = findReusableWorktreeCreateJob(_worktreeCreateJobs, createParams)
|
|
54343
54624
|
|| _startWorktreeCreateJob(createParams);
|
|
54344
54625
|
} catch (e) {
|
|
54626
|
+
_logWorktreeCreateJobRejected(e.code || 'WORKTREE_CREATE_FAILED', e.message);
|
|
54345
54627
|
res.writeHead(e.statusCode || 500, WORKTREE_JSON_HEADERS);
|
|
54346
54628
|
return res.end(JSON.stringify({ error: e.message, code: e.code || 'WORKTREE_CREATE_FAILED' }));
|
|
54347
54629
|
}
|
|
@@ -54701,7 +54983,10 @@ function apiSyncAllWorktrees(req, res) {
|
|
|
54701
54983
|
});
|
|
54702
54984
|
}
|
|
54703
54985
|
|
|
54704
|
-
|
|
54986
|
+
// WP-3 D-10 (model-proxy-survives-ctm-restart): `proxy` is the model-proxy verdict from
|
|
54987
|
+
// oauthProxySupervisor.proxyPreflight() -- a LIVE probe, so only async callers can supply it
|
|
54988
|
+
// (GET /api/restart/ctm/preflight does); the sync callers get the preflight's own default.
|
|
54989
|
+
function _restartGuardSnapshot(now = Date.now(), { proxy = null } = {}) {
|
|
54705
54990
|
return summarizeRestartGuard(sessions.values(), {
|
|
54706
54991
|
statusForSession: (session) => _standupLiveStatusForSession(session, now),
|
|
54707
54992
|
isWaitingForInput: (session) => _isServerWaitingForInput(session.id, session, now),
|
|
@@ -54710,7 +54995,7 @@ function _restartGuardSnapshot(now = Date.now()) {
|
|
|
54710
54995
|
// WP-4 (S-16): reuse the existing hosted-session discriminator (WP-2's
|
|
54711
54996
|
// `_sessionIsHosted`, defined above at _quiesceSessionsForProcessExit) --
|
|
54712
54997
|
// do not redefine it here.
|
|
54713
|
-
isHostedSession: _sessionIsHosted, preflight: _computeRestartPreflight({ sessions: sessions.values(), sessionHostManager, isHosted: _sessionIsHosted, isRunning: (s) => !!s.ptyProcess, isExited: (s) => _standupLiveStatusForSession(s, now) === 'exited', hostCodeDiffers: (hostId) => !!sessionHostManager?.hostCodeDiffersFromDisk?.(hostId) }), // WP-6 (S-22); WP-2: loaded code vs disk
|
|
54998
|
+
isHostedSession: _sessionIsHosted, preflight: _computeRestartPreflight({ sessions: sessions.values(), sessionHostManager, isHosted: _sessionIsHosted, isRunning: (s) => !!s.ptyProcess, isExited: (s) => _standupLiveStatusForSession(s, now) === 'exited', hostCodeDiffers: (hostId) => !!sessionHostManager?.hostCodeDiffersFromDisk?.(hostId), proxy }), // WP-6 (S-22); WP-2: loaded code vs disk; WP-3 D-10: proxy verdict
|
|
54714
54999
|
});
|
|
54715
55000
|
}
|
|
54716
55001
|
|
|
@@ -55546,6 +55831,9 @@ async function _adoptStartupTaskSession(task, record, aliveHostIndex, tried) {
|
|
|
55546
55831
|
codexHomePath: adoptedStorageRoots.CODEX_HOME || (adoptedHome ? path.join(adoptedHome, '.codex') : null),
|
|
55547
55832
|
sessionsDir: adoptedSessionsDir || undefined,
|
|
55548
55833
|
}) : null,
|
|
55834
|
+
// The launch-time config.toml receipt rides the host recipe; a host spawned before it did
|
|
55835
|
+
// carries none, and its lead switch keeps refusing until the session is relaunched.
|
|
55836
|
+
nativeConfigReceipt: adoptedRecipe?.nativeConfigReceipt || { state: 'unknown' },
|
|
55549
55837
|
},
|
|
55550
55838
|
// P2-3: restores seed activity clocks from the task, not "now" -- matches
|
|
55551
55839
|
// _createDeferredRestoreSession (70328-70329) / handleCreate's restore branch;
|
|
@@ -56004,6 +56292,7 @@ function _announceDeferredRestoreRetry(session, opts) {
|
|
|
56004
56292
|
// Extracted to lib/restore-backoff-gate.js (semantics unchanged; 'prepare-retry' is user-initiated per design D-4).
|
|
56005
56293
|
const _awaitRestoreBackoffOutsideGate = createAwaitRestoreBackoffOutsideGate({
|
|
56006
56294
|
enabled: _RESTORE_BACKOFF_ENABLED, restoreBackoff: _restoreBackoff, recordSessionDiagnostic, getSession, maxCooldownMs: _RESTORE_BACKOFF_MAX_MS,
|
|
56295
|
+
onSkip: _restoreWakeSkipSink, // WP-3 (§9 S-10/S-11): BackoffTaskMissingSessionId / BackoffSessionReplaced
|
|
56007
56296
|
});
|
|
56008
56297
|
|
|
56009
56298
|
function _startDeferredRestoreSession(session, opts = {}) {
|
|
@@ -56077,12 +56366,18 @@ async function _startDeferredRestoreSessionAttempt(session, opts = {}) {
|
|
|
56077
56366
|
}
|
|
56078
56367
|
}
|
|
56079
56368
|
if (sharedCapacityAdmission) {
|
|
56080
|
-
|
|
56369
|
+
// WP-3 (§9 S-12): a shared admission that came back refused is a skip of THIS activation.
|
|
56370
|
+
if (!capacity.ok) {
|
|
56371
|
+
_noteWakeSkip(session, 'CapacityRejected', {}, id);
|
|
56372
|
+
_endInteractiveActivationIfNeeded();
|
|
56373
|
+
return false;
|
|
56374
|
+
}
|
|
56081
56375
|
if (session._activationAdmissionPromise) { _endInteractiveActivationIfNeeded(); return session._activationAdmissionPromise; }
|
|
56082
56376
|
}
|
|
56083
56377
|
if (!capacity.ok) {
|
|
56084
56378
|
session._lifecycleState = session._parkedAt ? 'parked' : 'deferred';
|
|
56085
56379
|
_notifyLifecycleWakeBlocked(session, capacity);
|
|
56380
|
+
_noteWakeSkip(session, 'WakeBlocked', {}, id); // WP-3 (§9 S-12)
|
|
56086
56381
|
console.log(`[lifecycle] wake-blocked id=${id.slice(0, 8)} reason=${capacity.code || 'live-session-cap'}`);
|
|
56087
56382
|
_endInteractiveActivationIfNeeded();
|
|
56088
56383
|
return false;
|
|
@@ -56158,7 +56453,10 @@ async function _startDeferredRestoreSessionAttempt(session, opts = {}) {
|
|
|
56158
56453
|
session._lifecycleState = session._parkedAt ? 'parked' : 'deferred';
|
|
56159
56454
|
_scheduleStandupMaterializedRefresh(`lifecycle:deferred:${id.slice(0, 8)}`, 0, { markDirty: true });
|
|
56160
56455
|
_clearLifecyclePrewarm(session);
|
|
56161
|
-
|
|
56456
|
+
// WP-3 (§9 S-17, AC-3d): recordPrewarm fires ONLY for a real prewarm; the non-prewarm case is a
|
|
56457
|
+
// genuine restore skip, counted GateBusy instead of polluting prewarmByOutcome.
|
|
56458
|
+
if (prewarm) _lifecycleDiagnostics.recordPrewarm({ id, outcome: 'skipped', reason: 'gate-busy' });
|
|
56459
|
+
else _noteWakeSkip(session, 'GateBusy', {}, id);
|
|
56162
56460
|
console.log(`[lifecycle] prewarm-skip id=${id.slice(0, 8)} reason=gate-busy`);
|
|
56163
56461
|
_endInteractiveActivationIfNeeded();
|
|
56164
56462
|
return false;
|
|
@@ -56256,6 +56554,16 @@ function _setDeferredRestoreStarting(session, value, reason = '') {
|
|
|
56256
56554
|
const next = value === true;
|
|
56257
56555
|
if (session._restoreStarting === next) return false;
|
|
56258
56556
|
session._restoreStarting = next;
|
|
56557
|
+
// WP-3 (§5 D-5, §9 S-15): this is the release funnel -- the ~20 sites that bail after claiming a
|
|
56558
|
+
// placeholder pass a reason that used to be discarded. Record it on all three surfaces.
|
|
56559
|
+
if (next) session._lastRestoreSkip = null; // the claim ends any skip episode (the D-7 wire reads it)
|
|
56560
|
+
if (!next && reason) {
|
|
56561
|
+
session._lastRestoreSkip = { reason, at: Date.now() };
|
|
56562
|
+
try { recordSessionDiagnostic(session.id, 'deferred-restore-skip', { reason }); }
|
|
56563
|
+
catch { /* expected: session diagnostics ring is not constructed in sliced unit harnesses */ }
|
|
56564
|
+
try { _lifecycleDiagnostics.recordWakeSkip({ id: String(session.id || ''), provider: _lifecycleProvider(session), reason: String(reason) }); }
|
|
56565
|
+
catch { /* expected: lifecycle diagnostics are not constructed in sliced unit harnesses */ }
|
|
56566
|
+
}
|
|
56259
56567
|
_markHeartbeatDirty(session);
|
|
56260
56568
|
return true;
|
|
56261
56569
|
}
|
|
@@ -56267,7 +56575,12 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56267
56575
|
return false;
|
|
56268
56576
|
}
|
|
56269
56577
|
let task = session._restoreTask || {};
|
|
56270
|
-
|
|
56578
|
+
// WP-3 (§9 S-14): a placeholder with no durable task row can never be claimed. (The
|
|
56579
|
+
// `_restoreStarting` re-entry above records nothing -- the in-flight attempt owns the outcome.)
|
|
56580
|
+
if (!task.session_id) {
|
|
56581
|
+
_noteWakeSkip(session, 'BackoffTaskMissingSessionId', {}, session.id);
|
|
56582
|
+
return false;
|
|
56583
|
+
}
|
|
56271
56584
|
// WI-2/WI-6: quarantine short-circuit. A session already marked resume_blocked (deterministic
|
|
56272
56585
|
// prerequisite failure from a prior attempt — missing transcript/rollout, agent CLI
|
|
56273
56586
|
// unresolvable) must never be re-driven by ANY of the auto-retry triggers that funnel through
|
|
@@ -56279,7 +56592,10 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56279
56592
|
// id stays quarantined until the deterministic cause changes or CTM restarts.
|
|
56280
56593
|
const _blockedInfo = getSessionResumeBlocked(task.session_id);
|
|
56281
56594
|
if (_blockedInfo) {
|
|
56282
|
-
|
|
56595
|
+
// WP-0 (S-3): this replay re-sends an existing record without going through markSessionResumeBlocked,
|
|
56596
|
+
// so it writes the same diagnostic itself -- otherwise this card is unattributable.
|
|
56597
|
+
recordSessionDiagnostic(task.session_id, 'resume-blocked-set', resumeBlockSetDetails('deferred-restore-replay', _blockedInfo));
|
|
56598
|
+
sendResumeBlockedToClients(session.clients, resumeBlockedWire(task.session_id, _blockedInfo, { agentType: session.agentType, rungWire: resumeBlockedRungWire(_blockedInfo) }), _wsSend);
|
|
56283
56599
|
return false;
|
|
56284
56600
|
}
|
|
56285
56601
|
if (task._codexLinkDeferred) {
|
|
@@ -56288,7 +56604,11 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56288
56604
|
// operation instead of falling back to cwd/creation-time discovery.
|
|
56289
56605
|
const codexLink = await _ensureCodexThreadLinkOffThread(task.session_id, 'deferred-restore-codex',
|
|
56290
56606
|
{ selectedIdentity: task._codexRestoreTarget || null });
|
|
56291
|
-
if (codexLink?.status === 'stale')
|
|
56607
|
+
if (codexLink?.status === 'stale') {
|
|
56608
|
+
// WP-3 (§9 S-13): carry the linker's own conflict reason (lib/codex-restore-linker.js).
|
|
56609
|
+
_noteWakeSkip(session, 'CodexLinkStale', { linkerReason: codexLink.reason || '' }, task.session_id);
|
|
56610
|
+
return false;
|
|
56611
|
+
}
|
|
56292
56612
|
if (codexLink?.status === 'unavailable') {
|
|
56293
56613
|
recordSessionDiagnostic(task.session_id, 'codex-link-resolution-deferred', {
|
|
56294
56614
|
source: String(opts.source || ''),
|
|
@@ -56300,6 +56620,8 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56300
56620
|
detail: codexLink.reason || 'Codex thread resolution is temporarily unavailable.',
|
|
56301
56621
|
retryable: true,
|
|
56302
56622
|
});
|
|
56623
|
+
// WP-3 (§9 S-13 / D-8 CodexLinkUnavailable): count the skip beside the existing records.
|
|
56624
|
+
_noteWakeSkip(session, 'CodexLinkUnavailable', { linkerReason: codexLink.reason || '' }, task.session_id);
|
|
56303
56625
|
return false;
|
|
56304
56626
|
}
|
|
56305
56627
|
if (codexLink?.blocked) {
|
|
@@ -56307,13 +56629,15 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56307
56629
|
code: 'codex_thread_owned_by_other_ctm',
|
|
56308
56630
|
message: 'The saved Codex thread is already attached to another CTM session.',
|
|
56309
56631
|
agentType: 'codex',
|
|
56310
|
-
});
|
|
56632
|
+
}, 'codex-thread-owned');
|
|
56311
56633
|
_recordRuntimeSessionLifecycle(session, 'degraded', {
|
|
56312
56634
|
adapter: 'codex',
|
|
56313
56635
|
reason: 'codex_thread_owned_by_other_ctm',
|
|
56314
56636
|
detail: 'The saved Codex thread is already attached to another CTM session.',
|
|
56315
56637
|
retryable: false,
|
|
56316
56638
|
});
|
|
56639
|
+
// WP-3 (§9 S-13 / D-8 CodexLinkBlocked): a hard block is still a skip of this activation.
|
|
56640
|
+
_noteWakeSkip(session, 'CodexLinkBlocked', { linkerReason: codexLink.reason || '' }, task.session_id);
|
|
56317
56641
|
return false;
|
|
56318
56642
|
}
|
|
56319
56643
|
if (codexLink?.agentSessionId) {
|
|
@@ -56349,7 +56673,10 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56349
56673
|
sessionLaunchOverrides: session._launchEnvOverrides, options: opts,
|
|
56350
56674
|
});
|
|
56351
56675
|
if (restoreLaunchProjection.sourceRecoveryEnvironmentInvalid) {
|
|
56352
|
-
_setDeferredRestoreStarting(session, false, '
|
|
56676
|
+
_setDeferredRestoreStarting(session, false, 'SourceRecoveryEnvironmentInvalid');
|
|
56677
|
+
// WP-3 (§9 S-15): PRE-CLAIM -- the funnel call above is a no-op here (_restoreStarting is already
|
|
56678
|
+
// false), so record the skip directly; the funnel call stays (D-9, no control-flow change).
|
|
56679
|
+
_noteWakeSkip(session, 'SourceRecoveryEnvironmentInvalid', {}, task.session_id);
|
|
56353
56680
|
recordSessionDiagnostic(task.session_id, 'source-recovery-environment-rejected', {
|
|
56354
56681
|
reason: 'captured_source_environment_owner_mismatch',
|
|
56355
56682
|
});
|
|
@@ -56399,7 +56726,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56399
56726
|
let launchGeneration = suppliedPlan.generation;
|
|
56400
56727
|
if (launchGeneration <= 0) {
|
|
56401
56728
|
const reserved = _codingSessionGenerationRuntime.revalidateReservation(await _codingSessionGenerationRuntime.reserveSessionGeneration(task.session_id, Math.max(inputGeneration - 1, Number.isSafeInteger(session._launchPlanGeneration) ? session._launchPlanGeneration : 0)));
|
|
56402
|
-
if (!reserved.ok) { recordSessionDiagnostic(task.session_id, 'launch-generation-refused', { code: reserved.code, message: reserved.message }); _setDeferredRestoreStarting(session, false, '
|
|
56729
|
+
if (!reserved.ok) { recordSessionDiagnostic(task.session_id, 'launch-generation-refused', { code: reserved.code, message: reserved.message }); _setDeferredRestoreStarting(session, false, 'LaunchGenerationUnavailable'); _releaseResumeStormBudgetIfHeld(session); return false; }
|
|
56403
56730
|
launchGeneration = reserved.generation;
|
|
56404
56731
|
}
|
|
56405
56732
|
session._launchPlanGeneration = launchGeneration;
|
|
@@ -56477,7 +56804,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56477
56804
|
: _hydrateCanonicalAgentEventCaches([task.session_id]),
|
|
56478
56805
|
]);
|
|
56479
56806
|
} catch (error) {
|
|
56480
|
-
_setDeferredRestoreStarting(session, false, '
|
|
56807
|
+
_setDeferredRestoreStarting(session, false, 'PrepareFailed');
|
|
56481
56808
|
// round-3 WP-3 T5 (S-24, I-8b): a read-pool ADMISSION failure (the lane was full/paused, or
|
|
56482
56809
|
// the pool itself is disabled) is a fact about the POOL at this instant, never about this
|
|
56483
56810
|
// session's data or its agent -- the prior code folded it into the same generic
|
|
@@ -56543,23 +56870,23 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56543
56870
|
return false;
|
|
56544
56871
|
}
|
|
56545
56872
|
if (launchPlanResult?.status === 'stale') {
|
|
56546
|
-
_setDeferredRestoreStarting(session, false, '
|
|
56873
|
+
_setDeferredRestoreStarting(session, false, 'LaunchPlanStale');
|
|
56547
56874
|
return false;
|
|
56548
56875
|
}
|
|
56549
56876
|
if (sessions.get(task.session_id) !== session || !session._restoreDeferred) {
|
|
56550
|
-
_setDeferredRestoreStarting(session, false, '
|
|
56877
|
+
_setDeferredRestoreStarting(session, false, 'SessionReplaced');
|
|
56551
56878
|
return false;
|
|
56552
56879
|
}
|
|
56553
56880
|
const rungHook = await _resolveMissingTranscriptRung(task.session_id, launchPlanResult.plan, detectAgentType(task.cmd));
|
|
56554
56881
|
if (rungHook.restored) {
|
|
56555
56882
|
// The archived copy is back on disk: prepare the launch again and continue natively.
|
|
56556
56883
|
try { launchPlanResult = await runDeferredLaunchPlan(launchPlanResult.plan); } catch (error) {
|
|
56557
|
-
_setDeferredRestoreStarting(session, false, '
|
|
56884
|
+
_setDeferredRestoreStarting(session, false, 'PrepareFailed');
|
|
56558
56885
|
errorSink.report('deferred-restore:replan-after-restore', error);
|
|
56559
56886
|
return false;
|
|
56560
56887
|
}
|
|
56561
56888
|
if (launchPlanResult?.status === 'stale') {
|
|
56562
|
-
_setDeferredRestoreStarting(session, false, '
|
|
56889
|
+
_setDeferredRestoreStarting(session, false, 'LaunchPlanStale');
|
|
56563
56890
|
return false;
|
|
56564
56891
|
}
|
|
56565
56892
|
}
|
|
@@ -56573,7 +56900,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56573
56900
|
rungHook.restored ? launchPlanResult.plan.resume?.preflight : (rungHook.preflight || launchPlanResult.plan.resume?.preflight),
|
|
56574
56901
|
);
|
|
56575
56902
|
const _planBlocked = getSessionResumeBlocked(task.session_id);
|
|
56576
|
-
if (_planBlocked) sendResumeBlockedToClients(session.clients, resumeBlockedWire(task.session_id, _planBlocked, { agentType: session.agentType, rungWire:
|
|
56903
|
+
if (_planBlocked) sendResumeBlockedToClients(session.clients, resumeBlockedWire(task.session_id, _planBlocked, { agentType: session.agentType, rungWire: resumeBlockedRungWire(_planBlocked) }), _wsSend);
|
|
56577
56904
|
recordSessionDiagnostic(task.session_id, 'deferred-restore-blocked', {
|
|
56578
56905
|
code,
|
|
56579
56906
|
source: _restoreSource,
|
|
@@ -56584,7 +56911,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56584
56911
|
detail: launchPlanResult.plan.blockReason.message || 'Session launch is blocked.',
|
|
56585
56912
|
retryable: code === 'launch_rollout_index_pending' && launchPlanResult.plan.blockReason.deterministic !== true,
|
|
56586
56913
|
});
|
|
56587
|
-
_setDeferredRestoreStarting(session, false, '
|
|
56914
|
+
_setDeferredRestoreStarting(session, false, 'LaunchBlocked');
|
|
56588
56915
|
if (_shouldRefreshLaunchReadinessForBlock(code, launchPlanResult.plan.blockReason.deterministic === true)) {
|
|
56589
56916
|
_refreshLaunchReadiness().catch((e) => errorSink.report('launch-readiness:refresh', e));
|
|
56590
56917
|
}
|
|
@@ -56619,7 +56946,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56619
56946
|
transition_id: String(pendingTransition.transition_id || '').slice(0, 16),
|
|
56620
56947
|
reason: targetAuthorization.reason || 'host_target_receipt_mismatch',
|
|
56621
56948
|
});
|
|
56622
|
-
_setDeferredRestoreStarting(session, false, '
|
|
56949
|
+
_setDeferredRestoreStarting(session, false, 'LeadTransitionTargetOwnerMismatch');
|
|
56623
56950
|
return false;
|
|
56624
56951
|
}
|
|
56625
56952
|
}
|
|
@@ -56638,7 +56965,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56638
56965
|
transition_id: String(pendingTransition.transition_id || '').slice(0, 16),
|
|
56639
56966
|
reason: 'startup_task_not_exact_source',
|
|
56640
56967
|
});
|
|
56641
|
-
_setDeferredRestoreStarting(session, false, '
|
|
56968
|
+
_setDeferredRestoreStarting(session, false, 'LeadTransitionSourceOwnerMismatch');
|
|
56642
56969
|
return false;
|
|
56643
56970
|
}
|
|
56644
56971
|
const reconciled = await leadModelSwitch.reconcileUnownedTransition({ sessionId: task.session_id,
|
|
@@ -56648,7 +56975,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56648
56975
|
transition_id: String(pendingTransition.transition_id || '').slice(0, 16),
|
|
56649
56976
|
reason: reconciled.reason || 'transition_reconciliation_failed',
|
|
56650
56977
|
});
|
|
56651
|
-
_setDeferredRestoreStarting(session, false, '
|
|
56978
|
+
_setDeferredRestoreStarting(session, false, 'LeadTransitionReconciliationFailed');
|
|
56652
56979
|
return false;
|
|
56653
56980
|
}
|
|
56654
56981
|
}
|
|
@@ -56657,7 +56984,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56657
56984
|
recordSessionDiagnostic(task.session_id, 'lead-transition-source-restore-refused', {
|
|
56658
56985
|
reason: 'transition_reconciliation_unavailable', message: String(error?.message || error).slice(0, 160),
|
|
56659
56986
|
});
|
|
56660
|
-
_setDeferredRestoreStarting(session, false, '
|
|
56987
|
+
_setDeferredRestoreStarting(session, false, 'LeadTransitionReconciliationUnavailable');
|
|
56661
56988
|
return false;
|
|
56662
56989
|
}
|
|
56663
56990
|
if (opts.expectedProviderThreadId) {
|
|
@@ -56670,7 +56997,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56670
56997
|
expectedThreadPrefix: String(opts.expectedProviderThreadId).slice(0, 8),
|
|
56671
56998
|
actualThreadPrefix: String(resolvedThreadId).slice(0, 8),
|
|
56672
56999
|
});
|
|
56673
|
-
_setDeferredRestoreStarting(session, false, '
|
|
57000
|
+
_setDeferredRestoreStarting(session, false, 'LeadSourceRecoveryResumeMismatch');
|
|
56674
57001
|
return false;
|
|
56675
57002
|
}
|
|
56676
57003
|
}
|
|
@@ -56690,7 +57017,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56690
57017
|
throw new Error(`backup restore not possible: ${decision?.reason || 'unavailable'}`);
|
|
56691
57018
|
}
|
|
56692
57019
|
} catch (error) {
|
|
56693
|
-
_setDeferredRestoreStarting(session, false, '
|
|
57020
|
+
_setDeferredRestoreStarting(session, false, 'TranscriptRestoreFailed');
|
|
56694
57021
|
_launchPlanCoordinator.markFailed(task.session_id, launchGeneration, 'launch_transcript_restore_failed', launchPlanResult.plan);
|
|
56695
57022
|
recordSessionDiagnostic(task.session_id, 'deferred-restore-transcript-copy-failed', { error: error.message });
|
|
56696
57023
|
_recordRuntimeSessionLifecycle(session, 'degraded', {
|
|
@@ -56714,7 +57041,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56714
57041
|
}),
|
|
56715
57042
|
});
|
|
56716
57043
|
if (sessions.get(task.session_id) !== session || !session._restoreDeferred) {
|
|
56717
|
-
_setDeferredRestoreStarting(session, false, '
|
|
57044
|
+
_setDeferredRestoreStarting(session, false, 'McpWaitStale');
|
|
56718
57045
|
return false;
|
|
56719
57046
|
}
|
|
56720
57047
|
// WI-8: gate a large-transcript resume BEFORE committing to spawn. `source` here is
|
|
@@ -56757,7 +57084,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56757
57084
|
// immediately — nothing downstream will ever reach _clearResumeLoading for it.
|
|
56758
57085
|
if (_resumeStormAdmission.gated) _resumeStormBudget.release(_resumeStormAdmission.ticket);
|
|
56759
57086
|
session._resumeStormBudgetTicket = null;
|
|
56760
|
-
_setDeferredRestoreStarting(session, false, '
|
|
57087
|
+
_setDeferredRestoreStarting(session, false, 'ResumeStormStale');
|
|
56761
57088
|
return false;
|
|
56762
57089
|
}
|
|
56763
57090
|
session._resumeStormBudgetHeld = _resumeStormAdmission.gated;
|
|
@@ -56774,7 +57101,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56774
57101
|
const geometryLeaseBeforeRestore = _geometryLease(session, task.session_id);
|
|
56775
57102
|
if (!_launchPlanCoordinator.markSpawning(task.session_id, launchGeneration, launchPlanResult.plan)) {
|
|
56776
57103
|
_releaseResumeStormBudgetIfHeld(session);
|
|
56777
|
-
_setDeferredRestoreStarting(session, false, '
|
|
57104
|
+
_setDeferredRestoreStarting(session, false, 'LaunchPlanNotSpawning');
|
|
56778
57105
|
return false;
|
|
56779
57106
|
}
|
|
56780
57107
|
// D-8 (WP-3): capture the placeholder's cached-snapshot fields BEFORE _destroySession
|
|
@@ -56797,7 +57124,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56797
57124
|
});
|
|
56798
57125
|
if (!issued) {
|
|
56799
57126
|
_releaseResumeStormBudgetIfHeld(session);
|
|
56800
|
-
_setDeferredRestoreStarting(session, false, '
|
|
57127
|
+
_setDeferredRestoreStarting(session, false, 'LeadTransitionAttemptIssueRefused');
|
|
56801
57128
|
recordSessionDiagnostic(task.session_id, 'lead-transition-attempt-issue-refused', {
|
|
56802
57129
|
generation: launchGeneration,
|
|
56803
57130
|
});
|
|
@@ -56971,7 +57298,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56971
57298
|
// finds this session not-live) instead of waiting out its deadline for a wake-ready that
|
|
56972
57299
|
// will never come, and push visibility unconditionally.
|
|
56973
57300
|
_announceRestoredSessionLive(session, 'create-rollback');
|
|
56974
|
-
_setDeferredRestoreStarting(session, false, '
|
|
57301
|
+
_setDeferredRestoreStarting(session, false, 'CreateRollback');
|
|
56975
57302
|
recordSessionDiagnostic(task.session_id, 'deferred-restore-create-rollback', {
|
|
56976
57303
|
source: _restoreSource,
|
|
56977
57304
|
code: createError
|
|
@@ -57001,7 +57328,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
57001
57328
|
// a missing file will not appear on the next tick, so there is nothing to wait out (and
|
|
57002
57329
|
// every future call into this function short-circuits at the top before reaching here).
|
|
57003
57330
|
if (_newlyBlocked) {
|
|
57004
|
-
sendResumeBlockedToClients(existingClients, resumeBlockedWire(task.session_id, _newlyBlocked, { agentType: session.agentType, rungWire:
|
|
57331
|
+
sendResumeBlockedToClients(existingClients, resumeBlockedWire(task.session_id, _newlyBlocked, { agentType: session.agentType, rungWire: resumeBlockedRungWire(_newlyBlocked) }), _wsSend);
|
|
57005
57332
|
} else {
|
|
57006
57333
|
for (const client of existingClients) {
|
|
57007
57334
|
if (client && client.readyState === 1) {
|
|
@@ -57183,6 +57510,8 @@ function _autoResumeDeferredSessions(source = 'auto-resume') {
|
|
|
57183
57510
|
idleHours: diet.idleHours,
|
|
57184
57511
|
bytes: diet.bytes,
|
|
57185
57512
|
});
|
|
57513
|
+
// WP-3 (§9 S-19): beside the existing diag; the latch keeps a long diet to one skip.
|
|
57514
|
+
_noteWakeSkip(s, 'DrainPressureSkipped');
|
|
57186
57515
|
}
|
|
57187
57516
|
continue;
|
|
57188
57517
|
}
|
|
@@ -57230,9 +57559,11 @@ function _autoResumeDeferredSessions(source = 'auto-resume') {
|
|
|
57230
57559
|
const resumeIds = warmDrain.selected;
|
|
57231
57560
|
// S-18/skipReason: a session skipped this pass stays a lazy placeholder as before, but
|
|
57232
57561
|
// now carries WHY -- surfaced on its eventual activation log (grep deferredForMin).
|
|
57562
|
+
// WP-3 (§9 S-19): the warm-drain policy skip (budget/pressure/limit) also reaches wakeSkips.
|
|
57233
57563
|
for (const skip of warmDrain.skipped) {
|
|
57234
57564
|
const s = sessions.get(skip.id);
|
|
57235
57565
|
if (s) s._resumeDrainSkipReason = skip.reason;
|
|
57566
|
+
_noteWakeSkip(s, 'DrainPressureSkipped');
|
|
57236
57567
|
}
|
|
57237
57568
|
for (const id of resumeIds) {
|
|
57238
57569
|
const s = sessions.get(id);
|
|
@@ -57329,6 +57660,7 @@ function _autoResumeDeferredSessions(source = 'auto-resume') {
|
|
|
57329
57660
|
coalesceBroadcast: true, // batch emits a single list refresh per tick
|
|
57330
57661
|
}),
|
|
57331
57662
|
onError: (id, e) => console.error(`[auto-resume] ${String(id).slice(0, 8)} error: ${e.message}`),
|
|
57663
|
+
onSkip: _restoreWakeSkipSink, // WP-3 (§9 S-18): DrainPaused, once per pause transition
|
|
57332
57664
|
broadcast: () => broadcastSessionList(true),
|
|
57333
57665
|
scheduleGap: _scheduleResumeGap,
|
|
57334
57666
|
// WI-7(d): yield the whole drain to an in-flight interactive activation.
|
|
@@ -57412,13 +57744,18 @@ const _reviveUnresponsiveHost = createReviveUnresponsiveHost({
|
|
|
57412
57744
|
recordSessionDiagnostic,
|
|
57413
57745
|
log: (msg) => console.warn(msg),
|
|
57414
57746
|
});
|
|
57747
|
+
// WP-3 (D-3): the stall action -- one handoffHost per episode, the existing revive as its fallback.
|
|
57748
|
+
// `_evacuateStalledHostSafe` is a function declaration so the hoisted references above (the manager's
|
|
57749
|
+
// onThreadpoolReport option and the ledger's onEvacuate) resolve to it.
|
|
57750
|
+
const _evacuateStalledHost = createEvacuateStalledHost({ manager: sessionHostManager, handoffHost: _hostHandoffOrchestrator.handoffHost, reviveUnresponsiveHost: _reviveUnresponsiveHost, recordSessionDiagnostic, hostHealth, log: (m) => console.warn(m), handoffOpts: { errorSink } });
|
|
57751
|
+
function _evacuateStalledHostSafe(v) { _evacuateStalledHost(v.hostId, v).catch((e) => errorSink.report('host-evacuate', e, { hostId: v.hostId })); }
|
|
57415
57752
|
|
|
57416
57753
|
// Reconcile previous-boot hosts before classifying startup tasks. Incompatible
|
|
57417
57754
|
// hosts are quiesced, then restored through the ordinary provider-resume path.
|
|
57418
57755
|
const RECONCILE_PROBE_CONCURRENCY = 4;
|
|
57419
57756
|
async function _reconcileSurvivingHostsForRestore(startupTasks) {
|
|
57420
57757
|
const aliveHostIndex = new Map();
|
|
57421
|
-
const hostRecordsById = new Map();
|
|
57758
|
+
const hostRecordsById = new Map(); const threadpoolByHostId = new Map(); // WP-3 (§9 S-8): adopted hosts' state-reply threadpool snapshots, keyed by hostId
|
|
57422
57759
|
const dataDir = require('./lib/data-dir').getDataDir();
|
|
57423
57760
|
// WP-4 (D-9 crash cap): >= 3 unexpected host deaths inside the trailing 10-
|
|
57424
57761
|
// minute window means the topology itself is unhealthy right now -- skip
|
|
@@ -57436,7 +57773,7 @@ async function _reconcileSurvivingHostsForRestore(startupTasks) {
|
|
|
57436
57773
|
await _restartOutcomeTruth.killHostsForCrashCap({ records: await _listHostRecords(dataDir), rollup: _hostAdoptionRollup, isAlive: _isProcessAlive, shutdownHost: _shutdownOrphanHost,
|
|
57437
57774
|
removeRecord: (r) => _removeHostRecordFile(dataDir, r.hostId), skip: (r) => r.serverBootId === _serverBootId || sessionHostManager.ownsHostId(r.hostId), errorSink });
|
|
57438
57775
|
} catch (e) { errorSink.report('boot-reconcile:crash-cap-kill', e); }
|
|
57439
|
-
_restartOutcomeTruth.recordCrashCapRows({ recordSessionDiagnostic, startupTasks, deaths }); return { aliveHostIndex, hostRecordsById };
|
|
57776
|
+
_restartOutcomeTruth.recordCrashCapRows({ recordSessionDiagnostic, startupTasks, deaths }); return { aliveHostIndex, hostRecordsById, threadpoolByHostId };
|
|
57440
57777
|
}
|
|
57441
57778
|
} catch (e) {
|
|
57442
57779
|
errorSink.report('boot-reconcile:read-host-crashes', e);
|
|
@@ -57446,9 +57783,9 @@ async function _reconcileSurvivingHostsForRestore(startupTasks) {
|
|
|
57446
57783
|
records = await _listHostRecords(dataDir);
|
|
57447
57784
|
} catch (e) {
|
|
57448
57785
|
errorSink.report('boot-reconcile:list-host-records', e);
|
|
57449
|
-
return { aliveHostIndex, hostRecordsById };
|
|
57786
|
+
return { aliveHostIndex, hostRecordsById, threadpoolByHostId };
|
|
57450
57787
|
}
|
|
57451
|
-
if (!records.length) return { aliveHostIndex, hostRecordsById };
|
|
57788
|
+
if (!records.length) return { aliveHostIndex, hostRecordsById, threadpoolByHostId };
|
|
57452
57789
|
const startupSessionIds = new Set(startupTasks.map((t) => t.session_id));
|
|
57453
57790
|
const aliveHosts = []; // { record, matching } per probed-alive host; indexed AFTER all probes, hostId order
|
|
57454
57791
|
let cursor = 0;
|
|
@@ -57510,7 +57847,7 @@ async function _reconcileSurvivingHostsForRestore(startupTasks) {
|
|
|
57510
57847
|
console.log(` [boot-reconcile] host ${record.hostId} runs different code (closure hash ${String(probed.hostClosureHash || '').slice(0, 8)}≠${String(probed.diskClosureHash || '').slice(0, 8)}) — adopting now; handoff to new code after restore`);
|
|
57511
57848
|
for (const sessionId of matching) recordSessionDiagnostic(sessionId, 'host-upgrade-pending', { hostId: record.hostId, upgrade: probed.upgrade });
|
|
57512
57849
|
}
|
|
57513
|
-
hostRecordsById.set(record.hostId, record);
|
|
57850
|
+
hostRecordsById.set(record.hostId, record); threadpoolByHostId.set(record.hostId, probed.state && probed.state.threadpool); // WP-3 (D-4/S-9): the boot-time stall reading, absent on a pre-WP-3 host
|
|
57514
57851
|
aliveHosts.push({ record, matching });
|
|
57515
57852
|
}
|
|
57516
57853
|
}
|
|
@@ -57527,7 +57864,7 @@ async function _reconcileSurvivingHostsForRestore(startupTasks) {
|
|
|
57527
57864
|
if (aliveHostIndex.size) {
|
|
57528
57865
|
console.log(` [boot-reconcile] ${aliveHostIndex.size} session(s) have a surviving host — will adopt instead of respawning`);
|
|
57529
57866
|
}
|
|
57530
|
-
return { aliveHostIndex, hostRecordsById };
|
|
57867
|
+
return { aliveHostIndex, hostRecordsById, threadpoolByHostId };
|
|
57531
57868
|
}
|
|
57532
57869
|
|
|
57533
57870
|
// D-2: one-line stamp for restore/adopt/placeholder sites (idempotent; no row = untouched).
|
|
@@ -57581,6 +57918,54 @@ function _reloadWorkerSessionMarkersAtBoot() {
|
|
|
57581
57918
|
if (rearmed > 0) console.log(` [restore] re-armed ${rearmed} worker retirement timer(s) from worker_sessions.hide_at.`);
|
|
57582
57919
|
}
|
|
57583
57920
|
|
|
57921
|
+
// WP-3 (worker-transcript-link-derived-not-discovered-2026-10-25.html §5 D-5): the derived-transcript
|
|
57922
|
+
// boot backfill — one bounded, idempotent, fire-and-forget pass that fills agent_sessions.jsonl_path
|
|
57923
|
+
// for Claude rows whose transcript already sits at the DERIVED path (nothing else ever does: resume,
|
|
57924
|
+
// the targeted import, the review view and usage attribution all come up empty for them).
|
|
57925
|
+
//
|
|
57926
|
+
// The latch is module scope, not a local in _restoreSessionsAsync, so a re-entrant restore cannot
|
|
57927
|
+
// re-run the pass; only a process restart clears it. It means "scheduled this process", not "finished
|
|
57928
|
+
// this process" — the single re-arm below is the only extra attempt. The dispatch lives in this helper
|
|
57929
|
+
// — rather than inline at the call site — because the restore-loop test files slice
|
|
57930
|
+
// _restoreSessionsAsync into a vm sandbox with a curated global list: the call site must read nothing
|
|
57931
|
+
// that sandbox does not inject (see the typeof guard at the call site). Nothing in the restore loop
|
|
57932
|
+
// waits on this pass.
|
|
57933
|
+
let _derivedTranscriptLinkBackfillStarted = false;
|
|
57934
|
+
|
|
57935
|
+
// Why a re-arm exists at all (fix round 2, live defect on a dev restart): the owner worker's
|
|
57936
|
+
// statement lock is fail-fast by design and the dispatcher retries a write-lock-busy op only 3x with
|
|
57937
|
+
// ~60 ms backoff, so an op that lands while the main thread holds `transaction:immediate` rejects in
|
|
57938
|
+
// about a second. The call site was moved after the restore loop drains for exactly that reason; this
|
|
57939
|
+
// single 60 s re-arm covers a burst that outlives the loop (a slow adoption write, a busy disk).
|
|
57940
|
+
// Bounded: attempt 2 is the last one, there is no loop, and the latch above is not consulted again.
|
|
57941
|
+
async function _linkDerivedClaudeTranscriptsOnce(attempt = 1) {
|
|
57942
|
+
if (attempt === 1) {
|
|
57943
|
+
if (_derivedTranscriptLinkBackfillStarted) return;
|
|
57944
|
+
_derivedTranscriptLinkBackfillStarted = true;
|
|
57945
|
+
}
|
|
57946
|
+
try {
|
|
57947
|
+
const r = await _runCtmDbWorkerOp(
|
|
57948
|
+
'linkDerivedClaudeTranscripts',
|
|
57949
|
+
{ limit: 500, homeDir: os.homedir(), claudeConfigDir: serverConfig.CLAUDE_CONFIG_DIR || '' },
|
|
57950
|
+
{ timeoutMs: 60 * 1000 },
|
|
57951
|
+
);
|
|
57952
|
+
console.log(`[transcript-link] backfill checked=${r.checked} linked=${r.linked} missing=${r.missing} mismatched=${r.mismatched} unreadable=${r.unreadable} ms=${r.ms}`);
|
|
57953
|
+
} catch (e) {
|
|
57954
|
+
const writeLockBusy = !!e && (e.code === 'SQLITE_WRITE_LOCK_BUSY' || e.code === 'SQLITE_BUSY' || /write lock busy/i.test(String(e.message || '')));
|
|
57955
|
+
if (!writeLockBusy) {
|
|
57956
|
+
console.warn(`[transcript-link] backfill failed: ${e && e.message}`);
|
|
57957
|
+
return;
|
|
57958
|
+
}
|
|
57959
|
+
if (attempt >= 2) {
|
|
57960
|
+
console.warn(`[transcript-link] backfill deferred (attempt 2/2, giving up): ${e && e.message}`);
|
|
57961
|
+
return;
|
|
57962
|
+
}
|
|
57963
|
+
console.warn(`[transcript-link] backfill deferred (attempt 1/2, retry in 60s): ${e && e.message}`);
|
|
57964
|
+
const retry = setTimeout(() => { void _linkDerivedClaudeTranscriptsOnce(2); }, 60 * 1000);
|
|
57965
|
+
if (retry && typeof retry.unref === 'function') retry.unref();
|
|
57966
|
+
}
|
|
57967
|
+
}
|
|
57968
|
+
|
|
57584
57969
|
async function _restoreSessionsAsync() {
|
|
57585
57970
|
await _ownerRecovery.beforeRestore();
|
|
57586
57971
|
_hostAdoptionRollup.markRestoreStarted(); _restoreAdmission.markTierAActive('restore-start');
|
|
@@ -57599,11 +57984,11 @@ async function _restoreSessionsAsync() {
|
|
|
57599
57984
|
);
|
|
57600
57985
|
_listStartupTasksMs = Date.now() - _listStartupTasksStartedAt;
|
|
57601
57986
|
_restoreWindowVerdict.fillRestorePendingIds(_startupTasks.map((t) => t.session_id));
|
|
57602
|
-
let _restoreAliveHostIndex = new Map(), _restoreHostRecordsById = new Map();
|
|
57987
|
+
let _restoreAliveHostIndex = new Map(), _restoreHostRecordsById = new Map(), _restoreThreadpoolByHostId = new Map(); // WP-3 (§9 S-8)
|
|
57603
57988
|
const _earlyAdopted = new Set(); const _earlyAdoptStartedAt = Date.now();
|
|
57604
57989
|
if (_startupTasks.length) {
|
|
57605
57990
|
try {
|
|
57606
|
-
({ aliveHostIndex: _restoreAliveHostIndex, hostRecordsById: _restoreHostRecordsById } = await _reconcileSurvivingHostsForRestore(_startupTasks));
|
|
57991
|
+
({ aliveHostIndex: _restoreAliveHostIndex, hostRecordsById: _restoreHostRecordsById, threadpoolByHostId: _restoreThreadpoolByHostId } = await _reconcileSurvivingHostsForRestore(_startupTasks));
|
|
57607
57992
|
} catch (e) { errorSink.report('boot-reconcile:reconcile', e); }
|
|
57608
57993
|
await Promise.all(_startupTasks.filter((t) => _restoreAliveHostIndex.has(t.session_id)).map((t) => _adoptStartupTaskSession(t, _restoreAliveHostIndex.get(t.session_id).record, _restoreAliveHostIndex)
|
|
57609
57994
|
.then((adopted) => { if (adopted) _earlyAdopted.add(t.session_id); }, (e) => errorSink.report('boot-adopt:restore-early', e, { sessionId: t.session_id }))));
|
|
@@ -57759,6 +58144,11 @@ async function _restoreSessionsAsync() {
|
|
|
57759
58144
|
try { await dbModule.clearScrollbackIdentities(); } catch {}
|
|
57760
58145
|
_resumeShadowAfterBoot('no-sessions'); // nothing to restore — resume mirroring now
|
|
57761
58146
|
_reloadWorkerSessionMarkersAtBoot(); // once per boot in either branch (quota holds, worker markers, lead notices)
|
|
58147
|
+
// WP-4 (model-proxy-survives-ctm-restart D-10): a boot with nothing to restore still ledgers its
|
|
58148
|
+
// restore_complete row -- the model proxy's adopt/handoff/spawn outcome is per RESTART, not per
|
|
58149
|
+
// session -- written after the proxy's adoption decision (waitForHealth awaits it, bounded 2 s) so
|
|
58150
|
+
// adoption.proxy names what happened, never the in-flight fallback. Not awaited: boot never waits on it.
|
|
58151
|
+
oauthProxySupervisor.waitForHealth().catch(() => { /* expected: waitForHealth rejects only on an aborted signal and none is passed; the row is ledgered either way */ }).then(() => _restartOutcomeTruth.appendRestoreCompleteLedger({ build: _bootBuildSha, summary: _hostAdoptionMetricsSummary, errorSink }));
|
|
57762
58152
|
broadcastToAll({ type: 'server-ready' });
|
|
57763
58153
|
return;
|
|
57764
58154
|
}
|
|
@@ -57810,6 +58200,16 @@ async function _restoreSessionsAsync() {
|
|
|
57810
58200
|
// auto-resume below runs shadowed; only the restore loop itself was the
|
|
57811
58201
|
// suppressed window.)
|
|
57812
58202
|
_resumeShadowAfterBoot('restore-complete');
|
|
58203
|
+
// D-5 (worker transcript link): one bounded, idempotent pass that links every Claude row whose
|
|
58204
|
+
// transcript exists at the derived path. Placed here — after the loop drained, not in the boot
|
|
58205
|
+
// write burst — because the owner worker's statement lock is fail-fast: dispatching it while the
|
|
58206
|
+
// main thread holds back-to-back `transaction:immediate` writes (0.8-4.8 s each during restore)
|
|
58207
|
+
// makes the op reject in ~1 s, and the once-latch would then skip it for the whole boot. The
|
|
58208
|
+
// helper re-arms once if a burst still outlives the loop. Fire-and-forget: nothing waits on it.
|
|
58209
|
+
// typeof guard, not a missing dependency: the restore-loop tests evaluate this function in a vm
|
|
58210
|
+
// sandbox whose curated globals do not include the helper, and an unguarded call would throw there.
|
|
58211
|
+
// Same reason as `typeof _markStreamBehind === 'function'` above.
|
|
58212
|
+
if (typeof _linkDerivedClaudeTranscriptsOnce === 'function') void _linkDerivedClaudeTranscriptsOnce();
|
|
57813
58213
|
// WP-3 (design doc §5 D-4, §8 WP-3, §9 S-18): force one real sampler snapshot
|
|
57814
58214
|
// before draining so the warm-drain budget's pressure/free/inactive reflect the
|
|
57815
58215
|
// ACTUAL host state at the exact moment deferred sessions would auto-resume,
|
|
@@ -57819,7 +58219,7 @@ async function _restoreSessionsAsync() {
|
|
|
57819
58219
|
try { await _sampleLifecycleResources({ force: true }); }
|
|
57820
58220
|
catch (error) { errorSink.report('restore:warm-drain-pre-sample', error); }
|
|
57821
58221
|
// Bring deferred placeholders back to life by themselves (staggered).
|
|
57822
|
-
_autoResumeDeferredSessions('post-restore-auto-resume'); _hostHandoffOrchestrator.runUpgradePendingHandoffs({ manager: sessionHostManager, rollup: _hostAdoptionRollup, recordSessionDiagnostic, hostHealth, errorSink }).finally(() => _restartOutcomeTruth.appendRestoreCompleteLedger({ build: _bootBuildSha, summary: _hostAdoptionMetricsSummary, errorSink })); // WP-4: upgrade-pending hosts hand off after the loop
|
|
58222
|
+
_autoResumeDeferredSessions('post-restore-auto-resume'); _hostHandoffOrchestrator.runUpgradePendingHandoffs({ manager: sessionHostManager, rollup: _hostAdoptionRollup, recordSessionDiagnostic, hostHealth, errorSink, skipHostIds: evacuateStalledAdoptedHosts({ hostHealth, evacuateStalledHost: _evacuateStalledHost, threadpoolByHostId: _restoreThreadpoolByHostId, ownsHostId: (id) => [...sessionHostManager.hosts.values()].some((a) => a._hostId === id && !a.closed) }) }).finally(() => oauthProxySupervisor.waitForHealth().catch(() => { /* expected: waitForHealth rejects only on an aborted signal and none is passed; the row is ledgered either way */ }).then(() => _restartOutcomeTruth.appendRestoreCompleteLedger({ build: _bootBuildSha, summary: _hostAdoptionMetricsSummary, errorSink }))) // WP-4 D-10: after the proxy's adoption decision, same as the empty branch; // WP-4: upgrade-pending hosts hand off after the loop
|
|
57823
58223
|
// Clean orphan scrollback identities — entries for sessions no longer in
|
|
57824
58224
|
// startup_tasks (e.g. a hard crash where pty.onExit cleanup didn't run).
|
|
57825
58225
|
// (scrollback_log was dropped in schema v10; snapshot files are reaped by the
|
|
@@ -58170,7 +58570,7 @@ async function _restoreSessionsAsync() {
|
|
|
58170
58570
|
// broadcastToAll, isCtmInternalSession, callClaudeAsync, _detachOwnerMutation,
|
|
58171
58571
|
// _inRestoreStorm, _offthreadRead) is already initialized. See server/boot.js's own
|
|
58172
58572
|
// header comment for the TDZ-safety argument.
|
|
58173
|
-
const { createBootJobs, readPackageVersion } = require('./server/boot');
|
|
58573
|
+
const { createBootJobs, readPackageVersion, isHumanPromptCandidate, cleanFirstMessage } = require('./server/boot');
|
|
58174
58574
|
const _bootJobs = createBootJobs({
|
|
58175
58575
|
installRoot: path.join(__dirname, '..'),
|
|
58176
58576
|
telemetry,
|
|
@@ -60318,7 +60718,6 @@ async function apiDiagnosticsLifecycle(req, res, url) {
|
|
|
60318
60718
|
activationGate: _lifecycleActivationGate.snapshot(),
|
|
60319
60719
|
counts: {
|
|
60320
60720
|
live: rows.filter(row => row.state === 'live').length,
|
|
60321
|
-
frozen: rows.filter(row => row.state === 'frozen').length,
|
|
60322
60721
|
parked: rows.filter(row => row.state === 'parked').length,
|
|
60323
60722
|
deferred: rows.filter(row => row.state === 'deferred').length,
|
|
60324
60723
|
waking: rows.filter(row => row.state === 'waking').length,
|
|
@@ -61649,6 +62048,129 @@ servicesApi.configure({
|
|
|
61649
62048
|
});
|
|
61650
62049
|
servicesApi.register();
|
|
61651
62050
|
|
|
62051
|
+
// WP-0 (remote-machine-hub-sessions-2026-10-22.html §6.1): machines route cluster. Unlike the
|
|
62052
|
+
// extraction slices above there is no legacy handleApi branch to delete — this is NEW surface, so
|
|
62053
|
+
// the handlers live in lib/api/machines.js (its header states the convention) and the policy is
|
|
62054
|
+
// lib/machine-registry.js. Auth comes from lib/auth-rules.js's three /api/machines rows
|
|
62055
|
+
// (loopbackOnly + admin), which is also why nothing here evaluates a token.
|
|
62056
|
+
machinesApi.configure({
|
|
62057
|
+
db: dbModule,
|
|
62058
|
+
registry: machineRegistry,
|
|
62059
|
+
// WP-3 §6.3: the pair/remove handlers join a pool, so they need the pool store and the boot read's
|
|
62060
|
+
// promise — both declared further down this file (the pool cluster below owns them), so they are
|
|
62061
|
+
// passed as LAZY accessors; the cluster is configured here to keep the `machines` route-group
|
|
62062
|
+
// registration order the route golden pins. F2: this machine's own dial list is no longer injected
|
|
62063
|
+
// here — the roster shape (members + self) is lib/api/machine-pool.js's `rosterMembers()` now.
|
|
62064
|
+
pool: () => require('./lib/machine-pool').getMachinePool(),
|
|
62065
|
+
ready: () => _machinePoolReady,
|
|
62066
|
+
readJsonRequestBody,
|
|
62067
|
+
sendJsonResponse,
|
|
62068
|
+
});
|
|
62069
|
+
machinesApi.register();
|
|
62070
|
+
|
|
62071
|
+
// WP-1 (machine-pool-picker-pairing-dashboard-2026-10-24.html §6.1/D-13): host-stats route
|
|
62072
|
+
// cluster. Same shape as the machines cluster above — NEW surface, so no legacy handleApi branch
|
|
62073
|
+
// exists to delete, and the auth row (read + peerControl, NOT loopbackOnly) lives in
|
|
62074
|
+
// lib/auth-rules.js.
|
|
62075
|
+
hostStatsApi.configure({
|
|
62076
|
+
hostStats: _hostStats,
|
|
62077
|
+
sendJsonResponse,
|
|
62078
|
+
});
|
|
62079
|
+
hostStatsApi.register();
|
|
62080
|
+
|
|
62081
|
+
// WP-2a (machine-pool-picker-pairing-dashboard-2026-10-24.html §6.2, D-4/D-13, §9 S-24): the pool
|
|
62082
|
+
// + machine-self route cluster. Same shape as the two clusters above — NEW surface, no legacy
|
|
62083
|
+
// handleApi branch to delete. lib/machine-pool.js's process singleton is the POOL STORE (its
|
|
62084
|
+
// load() ran at boot, above); lib/machine-self.js is the machine-owned settings CACHE (loaded at
|
|
62085
|
+
// boot too, so the manifest build below never reads the DB).
|
|
62086
|
+
const machinePoolApi = require('./lib/api/machine-pool');
|
|
62087
|
+
const machineSelfStore = require('./lib/machine-self');
|
|
62088
|
+
machineSelfStore.configure({ db: dbModule });
|
|
62089
|
+
// §6.2: pool.load() and the machine-self cache load run ONCE at boot, before routes are served.
|
|
62090
|
+
// Kicked here (module load, ahead of every listener's 'listening' / _runBootSequence, which is a
|
|
62091
|
+
// frozen function at its line ceiling) rather than inside the boot sequence. Both are a single
|
|
62092
|
+
// small file read / one-row SELECT; a failure leaves the member pool-less (verify() fails closed
|
|
62093
|
+
// to 401) which is the same state as "never paired", never a broken boot.
|
|
62094
|
+
function _loadMachinePoolState() {
|
|
62095
|
+
const fail = (name) => (error) => console.warn(`[machine-pool] ${name} load failed: ${error?.message || error}`);
|
|
62096
|
+
return Promise.all([
|
|
62097
|
+
require('./lib/machine-pool').getMachinePool().load().catch(fail('pool')),
|
|
62098
|
+
machineSelfStore.load().catch(fail('machine-self')),
|
|
62099
|
+
]);
|
|
62100
|
+
}
|
|
62101
|
+
// `_machinePoolReady` is that same promise, named: this kick is fire-and-forget (nothing on the
|
|
62102
|
+
// request path may await a boot read), so a handler that MUTATES the pool — WP-3's pair/remove —
|
|
62103
|
+
// awaits it first and never races a load that is still in flight. A read-only handler does not.
|
|
62104
|
+
const _machinePoolReady = _loadMachinePoolState();
|
|
62105
|
+
machinePoolApi.configure({
|
|
62106
|
+
db: dbModule,
|
|
62107
|
+
pool: require('./lib/machine-pool').getMachinePool(),
|
|
62108
|
+
machineSelf: require('./lib/machine-self'),
|
|
62109
|
+
// The boot load's promise, so a pool-MUTATING handler (WP-3's pair/remove) can await it.
|
|
62110
|
+
ready: _machinePoolReady,
|
|
62111
|
+
registry: machineRegistry,
|
|
62112
|
+
readJsonRequestBody,
|
|
62113
|
+
sendJsonResponse,
|
|
62114
|
+
listenPort: PORT,
|
|
62115
|
+
// §6.2 "then executionHost.publicOrigin if set": EXACTLY the origin lib/instance-manifest.js
|
|
62116
|
+
// publishes on executionHost.publicOrigin — read back from the builder rather than off
|
|
62117
|
+
// `serverConfig`.
|
|
62118
|
+
//
|
|
62119
|
+
// Why the builder and not the four names: server-config's seam (RAW_ENV_NAMES) does not carry
|
|
62120
|
+
// CTM_CANONICAL_PUBLIC_ORIGIN / CTM_STABLE_PUBLIC_ORIGIN / CTM_PUBLIC_ORIGIN / CTM_EXTERNAL_ORIGIN,
|
|
62121
|
+
// so every `serverConfig.<name>` read here was `undefined` and this fallback could NEVER fire —
|
|
62122
|
+
// a loopback- or tailnet-bound instance advertised only its LAN addresses, and selfUrls() (P-4)
|
|
62123
|
+
// had nothing dialable to append. WP-3's three-box pairing test hit it head-on: the roster push
|
|
62124
|
+
// to a peer whose only dial URL was a LAN address nothing was bound to never arrived.
|
|
62125
|
+
// lib/instance-manifest-runtime.js spreads `process.env` first, so this reads the same values the
|
|
62126
|
+
// manifest publishes, through the sanctioned seam, with no new raw env member read here.
|
|
62127
|
+
publicOrigin: () => {
|
|
62128
|
+
try {
|
|
62129
|
+
const manifest = _buildInstanceManifest(null, Math.floor((Date.now() - _ctmStartTime) / 1000));
|
|
62130
|
+
return String((manifest && manifest.executionHost && manifest.executionHost.publicOrigin) || '');
|
|
62131
|
+
} catch {
|
|
62132
|
+
return ''; // expected: a failed manifest build just leaves self URLs LAN-only (P-4 order preserved)
|
|
62133
|
+
}
|
|
62134
|
+
},
|
|
62135
|
+
});
|
|
62136
|
+
machinePoolApi.register();
|
|
62137
|
+
|
|
62138
|
+
// WP-3 (machine-pool-picker-pairing-dashboard-2026-10-24.html §6.3, D-6/D-9/D-13, §9 S-15/S-16/S-23):
|
|
62139
|
+
// the pairing route cluster — the routes a JOINING member talks to. Same shape as the three
|
|
62140
|
+
// clusters above: NEW surface, no legacy handleApi branch to delete.
|
|
62141
|
+
//
|
|
62142
|
+
// POST /api/machine-pairing/offers is an ordinary authorised route (admin + step-up remote, row in
|
|
62143
|
+
// lib/auth-rules.js). POST /api/machine-pairing/join AND GET /api/machine-pairing/identity are
|
|
62144
|
+
// registered with phase 'api-cloudflare' — the dispatcher server.js already runs BEFORE
|
|
62145
|
+
// `_authorizeHttpRequest` (line ~12278, the seam lib/api/microsoft-owner.js's public rows use),
|
|
62146
|
+
// because the caller does not hold this box's credential yet; the pool secret in the body IS the
|
|
62147
|
+
// thing being proven, and the identity probe exists exactly so a member can ask "is there a code
|
|
62148
|
+
// here" without one (D-9: /api/sync/manifest is `sync`-scoped and would 401 on a real LAN before the
|
|
62149
|
+
// pair began). The defence on both is the `machine_pair` bucket below plus the single-use code
|
|
62150
|
+
// (lib/api/machine-pairing.js states this in full). The limiter and the remote-IP reader are injected
|
|
62151
|
+
// rather than re-derived, so both routes are charged to the SAME AuthRateLimiter instance every
|
|
62152
|
+
// authorised route uses.
|
|
62153
|
+
const machinePairingApi = require('./lib/api/machine-pairing');
|
|
62154
|
+
const machinePairingStore = require('./lib/machine-pairing').createMachinePairing({ db: dbModule });
|
|
62155
|
+
machinePairingApi.configure({
|
|
62156
|
+
db: dbModule,
|
|
62157
|
+
pool: require('./lib/machine-pool').getMachinePool(),
|
|
62158
|
+
pairing: machinePairingStore,
|
|
62159
|
+
machineSelf: require('./lib/machine-self'),
|
|
62160
|
+
registry: machineRegistry,
|
|
62161
|
+
selfUrls: machinePoolApi.selfUrls,
|
|
62162
|
+
readJsonRequestBody,
|
|
62163
|
+
sendJsonResponse,
|
|
62164
|
+
// The PRE-AUTH rate limit: `_rateLimitDecision`'s own limiter and KDF, keyed by remote address
|
|
62165
|
+
// because there is no principal yet. Same `AuthRateLimiter` instance (`authRateLimiter`).
|
|
62166
|
+
rateLimit: (kind, key) => authRateLimiter.check(kind, key),
|
|
62167
|
+
remoteIp: (req) => _remoteIpFromReq(req),
|
|
62168
|
+
// F5: the offer/identity failure path logs here and echoes nothing to the caller.
|
|
62169
|
+
logError: (...args) => console.error(...args),
|
|
62170
|
+
ready: _machinePoolReady,
|
|
62171
|
+
});
|
|
62172
|
+
machinePairingApi.register();
|
|
62173
|
+
|
|
61652
62174
|
// WP-8 slice 3: setup/diagnostics/misc-ops route clusters.
|
|
61653
62175
|
const setupApi = require('./lib/api/setup');
|
|
61654
62176
|
setupApi.configure({
|
|
@@ -61743,6 +62265,7 @@ miscopsApi.configure({
|
|
|
61743
62265
|
apiListRules,
|
|
61744
62266
|
apiDeleteHook,
|
|
61745
62267
|
apiRestartCtm, restartGuardSnapshot: _restartGuardSnapshot,
|
|
62268
|
+
proxyPreflight: () => oauthProxySupervisor.proxyPreflight(), // WP-3 D-10: the live model-proxy verdict
|
|
61746
62269
|
wss,
|
|
61747
62270
|
clientBuildDiagnosticDetails,
|
|
61748
62271
|
attachedSessionIds,
|
|
@@ -61811,6 +62334,7 @@ ingressApi.configure({
|
|
|
61811
62334
|
observeWorkspaceActivity: _observeWorkspaceActivity,
|
|
61812
62335
|
dbModule,
|
|
61813
62336
|
delegateWorker,
|
|
62337
|
+
recordSessionDiagnostic,
|
|
61814
62338
|
});
|
|
61815
62339
|
ingressApi.register();
|
|
61816
62340
|
const { createMicrosoftOwnerServer } = require('./lib/microsoft-owner-server');
|
|
@@ -62133,10 +62657,10 @@ const rootPromptFlow = createRootPromptFlow({
|
|
|
62133
62657
|
}, Date.now(),
|
|
62134
62658
|
),
|
|
62135
62659
|
sendWsJson: (ws, frame) => _sendWsJsonBounded(ws, frame),
|
|
62136
|
-
reportDiagnostic: ({ phase, code, returnedRecordConfirmed, canonicalRecordConfirmed,
|
|
62660
|
+
reportDiagnostic: ({ phase, code, sessionId, clientMessageId, returnedRecordConfirmed, canonicalRecordConfirmed,
|
|
62137
62661
|
recordFieldsMatch, errorKind, sourceFile, sourceLine, nativeHandleOpen, nativeHandleClosed, failureClass }) =>
|
|
62138
62662
|
recordSessionDiagnostic('__host__', 'root-prompt-flow-suppressed-failure', {
|
|
62139
|
-
phase, code, returnedRecordConfirmed, canonicalRecordConfirmed, recordFieldsMatch,
|
|
62663
|
+
phase, code, sessionId, clientMessageId, returnedRecordConfirmed, canonicalRecordConfirmed, recordFieldsMatch,
|
|
62140
62664
|
errorKind, sourceFile, sourceLine, nativeHandleOpen, nativeHandleClosed, failureClass,
|
|
62141
62665
|
}),
|
|
62142
62666
|
markDirty: (sessionId, reason) => {
|
|
@@ -62702,7 +63226,7 @@ function _runBootSequence() {
|
|
|
62702
63226
|
// configDir/instanceTag/port are passed explicitly so a dev/test instance
|
|
62703
63227
|
// (its own CONFIG_DIR, per lib/server-config.js) never lands its journal
|
|
62704
63228
|
// under the real ~/.walle — see lib/ctm-boot-lifecycle.js.
|
|
62705
|
-
appendCtmBootLine({ configDir: CONFIG_DIR, instanceTag: INSTANCE_TAG, port: PORT });
|
|
63229
|
+
appendCtmBootLine({ configDir: CONFIG_DIR, instanceTag: INSTANCE_TAG, port: PORT, enteredAtServer: require.main === module });
|
|
62706
63230
|
|
|
62707
63231
|
// Stale CTM cleanup: with launchd KeepAlive, only one instance runs.
|
|
62708
63232
|
// No pgrep needed — launchd manages the lifecycle.
|
|
@@ -62727,8 +63251,13 @@ function _runBootSequence() {
|
|
|
62727
63251
|
// Wall-E daemon and surfaced through its HTTP API after startup.
|
|
62728
63252
|
const shouldStartOauthProxy = (() => {
|
|
62729
63253
|
if (isWalleSupervisorDisabled()) return false;
|
|
62730
|
-
// Sync boot path: Wall-E's stored provider is not readable here; the auth method
|
|
62731
|
-
|
|
63254
|
+
// Sync boot path: Wall-E's stored provider is not readable here; the auth method gates a fresh spawn.
|
|
63255
|
+
if (serverConfig.WALLE_AUTH_METHOD === 'oauth_proxy') return true;
|
|
63256
|
+
// WP-4 AC-4e (model-proxy-survives-ctm-restart D-4): a proxy that outlived the previous CTM (live
|
|
63257
|
+
// process named by this data dir's instance record) is adopted by start() regardless of the auth
|
|
63258
|
+
// method -- the primary's proxy is started by the setup surface, not this env var, and a survivor
|
|
63259
|
+
// left un-adopted keeps serving but no longer gets this CTM's lead tokens.
|
|
63260
|
+
return oauthProxySupervisor.hasLiveSurvivor();
|
|
62732
63261
|
})();
|
|
62733
63262
|
if (shouldStartOauthProxy) {
|
|
62734
63263
|
try {
|
|
@@ -63368,6 +63897,16 @@ function _runBootSequence() {
|
|
|
63368
63897
|
// coalescing keeps at most one queued behind the in-flight one).
|
|
63369
63898
|
_transcriptDeltaCoalescer.request(filePath, payload);
|
|
63370
63899
|
}
|
|
63900
|
+
// WP-2 (D-11): publish the title the db-owner worker wrote; a drifted tier-1 row asks for one re-assert.
|
|
63901
|
+
const titleApply = applied.titleApply;
|
|
63902
|
+
if (titleApply?.applied === 'agent') {
|
|
63903
|
+
if (liveSession) { liveSession.title = titleApply.title; _invalidateTitleInfoCache(liveSession); }
|
|
63904
|
+
void _detachOwnerMutation('agent-title-label', () => dbModule.updateStartupTaskLabel(ownerSessionId, titleApply.title));
|
|
63905
|
+
broadcastSessionList();
|
|
63906
|
+
}
|
|
63907
|
+
if (titleApply?.reassert) {
|
|
63908
|
+
void mirrorSessionTitle({ db: dbModule, sessionId: ownerSessionId, force: true }).catch(() => { /* expected: exit/relink and the next observation retry it */ });
|
|
63909
|
+
}
|
|
63371
63910
|
} catch (e) {
|
|
63372
63911
|
fallback('pipeline-error');
|
|
63373
63912
|
console.error(`[event-delta] ${String(sessionId).slice(0, 8)} pipeline error:`, e.message);
|