create-walle 0.10.1 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- 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 +6 -4
- package/package.json +1 -1
- 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/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/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 +5 -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-launch-health.js +29 -2
- package/template/coding-task-manager/lib/codex-link-preparation.js +9 -8
- package/template/coding-task-manager/lib/codex-paths.js +2 -1
- 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-schema-source-files.js +1 -0
- package/template/coding-task-manager/lib/feature-manifest.js +1 -1
- package/template/coding-task-manager/lib/fs-watcher.js +3 -2
- package/template/coding-task-manager/lib/harness-history-transition.js +3 -3
- 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 +55 -17
- 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/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-switch-runtime.js +57 -4
- package/template/coding-task-manager/lib/lead-model-switch.js +3 -2
- 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 +1 -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 +52 -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-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/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/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 +2 -2
- 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-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/read-pool-client.js +5 -2
- package/template/coding-task-manager/lib/read-pool-op-registry.js +10 -1
- 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 +13 -1
- 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 +33 -2
- package/template/coding-task-manager/lib/root-prompt-flow.js +167 -33
- package/template/coding-task-manager/lib/scrollback-archive-store.js +7 -4
- 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 +34 -37
- 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 +18 -0
- package/template/coding-task-manager/lib/session-model-action-preconditions.js +9 -0
- package/template/coding-task-manager/lib/session-resource-sampler.js +19 -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-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 +92 -0
- package/template/coding-task-manager/lib/typing-worker-stages.js +60 -0
- 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/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/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 -1
- package/template/coding-task-manager/public/css/reviews.css +4 -0
- package/template/coding-task-manager/public/index.html +294 -223
- 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/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 -8
- package/template/coding-task-manager/public/js/prompt-submission-tray.js +281 -0
- 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 +4 -2
- 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/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 -1
- 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 +902 -239
- 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/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/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 +6 -4
- 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
|
@@ -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,
|
|
@@ -453,7 +455,7 @@ if (_shadowBootSafetyMs > 0) {
|
|
|
453
455
|
if (t && typeof t.unref === 'function') t.unref();
|
|
454
456
|
}
|
|
455
457
|
|
|
456
|
-
const { mirrorSessionTitle,
|
|
458
|
+
const { mirrorSessionTitle, applyProviderTitle } = require('./lib/session-title-mirror');
|
|
457
459
|
const streamResizePolicy = require('./public/js/stream-resize-policy');
|
|
458
460
|
const { turnSettleQuietResizeAction, deferredResizeReleaseDecision } = require('./lib/deferred-resize-quiet-policy');
|
|
459
461
|
const { rowsFlapResizeDecision, DEBOUNCE_MS: ROWS_FLAP_DEBOUNCE_MS, MAX_HOLD_MS: ROWS_FLAP_MAX_HOLD_MS } = require('./lib/rows-flap-debounce');
|
|
@@ -649,6 +651,8 @@ const { isCrashCapTripped: _isHostCrashCapTripped } = require('./lib/host-crash-
|
|
|
649
651
|
// _shutdownOrphanHost below).
|
|
650
652
|
const { createHostHealthLedger, readStallFromDataDir } = require('./lib/host-health-ledger');
|
|
651
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');
|
|
652
656
|
const {
|
|
653
657
|
createPtyStartupReservation,
|
|
654
658
|
ptyStartupCommitComplete,
|
|
@@ -697,6 +701,7 @@ const _typingInteractionTraces = createTypingInteractionTraceStore({
|
|
|
697
701
|
publicLimit: 128,
|
|
698
702
|
ttlMs: 60_000,
|
|
699
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
|
|
700
705
|
// D-1 (composer-typing-freeze-keystroke-slo-2026-09-11.html §5 D-1, §8 WP-0): persist the
|
|
701
706
|
// keystroke-to-paint distribution past the 60s in-memory ring. _eventLoopIncidents is
|
|
702
707
|
// resolved lazily here (this callback only ever runs later, on the flush timer below) even
|
|
@@ -1505,17 +1510,7 @@ _perfTracker.namedInterval('timer:visible-revision-watchdog', () => {
|
|
|
1505
1510
|
recordDiagnostic: recordSessionDiagnostic,
|
|
1506
1511
|
coalescer: _visibleRevisionMarginalCoalescer,
|
|
1507
1512
|
});
|
|
1508
|
-
|
|
1509
|
-
const target = (session?.clients || []).find((client) => (
|
|
1510
|
-
client.clientId === incident.clientId
|
|
1511
|
-
&& Number(client.socketGeneration || 0) === Number(incident.socketGeneration || 0)
|
|
1512
|
-
));
|
|
1513
|
-
if (target?.readyState !== 1 || incident.recoveryAction === 'report-only') continue;
|
|
1514
|
-
// WP-3b (delivery-path reset): the 'fence-generation' recovery action used to fence the
|
|
1515
|
-
// client here (server-side belief the client's baseline generation is invalid); the client
|
|
1516
|
-
// fence is deleted, so this incident now only ever asks for a keyframe, same as any other
|
|
1517
|
-
// recovery action.
|
|
1518
|
-
_requestKeyframeGrant(target, incident.sessionId, { priority: true });
|
|
1513
|
+
// Report-only (lib/visible-revision-watchdog.js): a late boundary never forces a keyframe.
|
|
1519
1514
|
}
|
|
1520
1515
|
}, 25).unref?.();
|
|
1521
1516
|
// WP-3b (delivery-path reset): the fence-breaker sweep (lib/state-sync/fence-breaker.js) existed
|
|
@@ -1996,6 +1991,7 @@ const { splitMirrorAndBrowserChunks } = require('./lib/mirror-feed-sanitize');
|
|
|
1996
1991
|
const {
|
|
1997
1992
|
buildSessionStandupSnapshot,
|
|
1998
1993
|
isWalleOwnedSession,
|
|
1994
|
+
remoteStandupSession,
|
|
1999
1995
|
selectUnmatchedStartupTaskPlaceholders,
|
|
2000
1996
|
isStandupStartupTaskSeedEnabled,
|
|
2001
1997
|
} = require('./lib/session-standup');
|
|
@@ -2084,7 +2080,7 @@ const {
|
|
|
2084
2080
|
const {
|
|
2085
2081
|
createCodingAgentModelSync,
|
|
2086
2082
|
resolveModelToRegistry,
|
|
2087
|
-
|
|
2083
|
+
getCodexThreadAgentName,
|
|
2088
2084
|
} = require('./lib/coding-agent-models');
|
|
2089
2085
|
const { MODEL_RESULT_PAGE_SIZE, MAX_MODEL_RESULT_BYTES } = require('./lib/coding-session-model-projection'); const { createCodingSessionModelRuntime } = require('./lib/coding-session-model-runtime');
|
|
2090
2086
|
const { createCodingSessionGenerationRuntime } = require('./lib/coding-session-generation-runtime');
|
|
@@ -2141,6 +2137,10 @@ const { evaluateWaitState, shouldClearWaitCard, deriveChoiceLabel, shouldSuperse
|
|
|
2141
2137
|
const { selectionCardForViewport } = require('./lib/terminal-interaction-policy');
|
|
2142
2138
|
const promptIntent = require('./lib/prompt-intent');
|
|
2143
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');
|
|
2144
2144
|
const { classifyResumeNoise, resumeFailureDetail } = require('./lib/resume-noise');
|
|
2145
2145
|
const { createResumeAttemptTracker } = require('./lib/resume-attempts');
|
|
2146
2146
|
const {
|
|
@@ -2190,6 +2190,60 @@ const resumeFromStoreApi = require('./lib/api/resume-from-store');
|
|
|
2190
2190
|
const servicesApi = require('./lib/api/services'); const devFaultApi = require('./lib/api/dev-fault');
|
|
2191
2191
|
const setupNetworkApi = require('./lib/api/setup-network'); const runtimeApprovalsApi = require('./lib/api/runtime-approvals');
|
|
2192
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
|
+
});
|
|
2193
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
|
|
2194
2248
|
const wsRouter = require('./lib/ws-router');
|
|
2195
2249
|
const { runWsMessageWithTrace } = require('./lib/ws-causal-context');
|
|
@@ -2490,7 +2544,7 @@ const {
|
|
|
2490
2544
|
loadConfig,
|
|
2491
2545
|
ensureWalleToken,
|
|
2492
2546
|
} = serverConfig;
|
|
2493
|
-
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
|
|
2494
2548
|
const bestOfAttempts = createBestOfAttemptService({
|
|
2495
2549
|
statePath: path.join(CONFIG_DIR, 'best-of-attempts.json'),
|
|
2496
2550
|
launchAttempt: _launchBestOfAttempt,
|
|
@@ -2664,9 +2718,9 @@ const _diagBatcher = createSessionDiagnosticBatcher({
|
|
|
2664
2718
|
},
|
|
2665
2719
|
flush: (batch) => {
|
|
2666
2720
|
// Off-thread: route the durable batch write through the db-owner worker (the designated
|
|
2667
|
-
// serial writer) so the cross-process SQLite write
|
|
2668
|
-
//
|
|
2669
|
-
//
|
|
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.
|
|
2670
2724
|
_runCtmDbWorkerOp('flushSessionDiagnostics', { entries: batch }, { bestEffort: true })
|
|
2671
2725
|
.catch(() => {});
|
|
2672
2726
|
},
|
|
@@ -4886,7 +4940,7 @@ async function _launchBestOfAttempt(spec) {
|
|
|
4886
4940
|
const created = await handleCreatePrepared(null, {
|
|
4887
4941
|
id: sessionId,
|
|
4888
4942
|
title,
|
|
4889
|
-
label: title,
|
|
4943
|
+
label: title, titleIntent: 'default', // D-13: `_bestOfAttemptTitle` is CTM-composed (tier 3), never a user name
|
|
4890
4944
|
cmd: source.cmd || agentType,
|
|
4891
4945
|
args: bestOfFreshAgentArgs(agentType, source.args, { approvalHookToken: source._approvalHookToken || '' }),
|
|
4892
4946
|
cwd: workspace.cwd,
|
|
@@ -7474,19 +7528,11 @@ function _applyBootRepairsResult(repairs) {
|
|
|
7474
7528
|
const timer = setTimeout(continueWalleIdentityRekey, 250);
|
|
7475
7529
|
timer.unref?.();
|
|
7476
7530
|
}
|
|
7477
|
-
const repairedTitles = repairs?.titleRepair;
|
|
7478
|
-
if (repairedTitles?.promoted) {
|
|
7479
|
-
console.log(`[session-title] promoted ${repairedTitles.promoted} active startup title(s) into ctm_sessions.title`);
|
|
7480
|
-
telemetry.track('session_title_startup_repair', {
|
|
7481
|
-
promoted: repairedTitles.promoted,
|
|
7482
|
-
scanned: repairedTitles.scanned || 0,
|
|
7483
|
-
});
|
|
7484
|
-
}
|
|
7485
7531
|
return _refreshControlSnapshot({ reason: 'db-owner-ready' });
|
|
7486
7532
|
}
|
|
7487
7533
|
|
|
7488
7534
|
// WP-B / B1 (2026-07-31 boot-wedge outage): runBootRepairs performs REPAIRS
|
|
7489
|
-
// (
|
|
7535
|
+
// (codex identity/metadata repair, conversation-source
|
|
7490
7536
|
// migration, order-key backfill, Wall-E identity rekey) — none of it is
|
|
7491
7537
|
// load-bearing for serving HTTP. The old code awaited this with no
|
|
7492
7538
|
// try/catch, so a single rejection (that night: a worker-request timeout)
|
|
@@ -7853,7 +7899,10 @@ const sessionHostManager = createSessionHostManager({
|
|
|
7853
7899
|
// WP-0 (pty-host-threadpool-stranded-waiters-2026-09-25.html, D-3/D-6): report-only
|
|
7854
7900
|
// seam for the pool/dedicated-host threadpool monitor's frames -- see
|
|
7855
7901
|
// _recordThreadpoolDiagnostic above and apiServicesStatus's golden-signals feed below.
|
|
7856
|
-
|
|
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); },
|
|
7857
7906
|
// WP-1 (pty-host-threadpool-stranded-waiters-2026-09-25.html, D-2): the manager has no
|
|
7858
7907
|
// DB/session access of its own -- forward every input-ack frame to the one function
|
|
7859
7908
|
// that turns it into typing-trace/durable-phase truth.
|
|
@@ -7861,7 +7910,7 @@ const sessionHostManager = createSessionHostManager({
|
|
|
7861
7910
|
// Probe (mouse-mode handoff): host-reported per-session diagnostic rows (host-handoff-mirror-seed/-restored).
|
|
7862
7911
|
onSessionDiagnostic: (sessionId, event, details) => recordSessionDiagnostic(sessionId, event, details),
|
|
7863
7912
|
});
|
|
7864
|
-
_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
|
|
7865
7914
|
if (serverConfig.CTM_SESSION_HOST_PROCESS !== '0' && process.env.CTM_SESSION_HOST_POOL_PREWARM !== '0') {
|
|
7866
7915
|
try { sessionHostManager.prewarmPool?.(); }
|
|
7867
7916
|
catch (e) { console.error('[session-host-pool] prewarm error:', e.message); }
|
|
@@ -7872,8 +7921,16 @@ if (serverConfig.CTM_SESSION_HOST_PROCESS !== '0' && process.env.CTM_SESSION_HOS
|
|
|
7872
7921
|
// process globals inside lib/host-health-ledger.js itself); its default
|
|
7873
7922
|
// D-2(b) input is the WP-0 stall file a stalled pool host writes for its
|
|
7874
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'.
|
|
7875
7930
|
const hostHealth = createHostHealthLedger({
|
|
7876
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),
|
|
7877
7934
|
});
|
|
7878
7935
|
// Final-review fix I6: a host that exits (normally OR by crash) must leave the
|
|
7879
7936
|
// ledger, or its miss history outlives it -- and a later host reusing the same
|
|
@@ -10456,41 +10513,25 @@ setDbMaintenanceRunner(runOnlineDbBackup);
|
|
|
10456
10513
|
setImageMetricsRecorder(event => _uploadMetrics.recordUpload(event));
|
|
10457
10514
|
// WP-0 §6.3/S-8: tpOldestMs on the [img-upload] line comes from the main-process monitor above.
|
|
10458
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
|
+
});
|
|
10459
10521
|
async function _prepareMediaStore() {
|
|
10460
|
-
if (!_mediaWriteBehindEnabled) return null;
|
|
10522
|
+
if (!_mediaWriteBehindEnabled) { await _legacyDraftCancellationRecovery.prepare(); return null; }
|
|
10461
10523
|
if (_mediaStore) return _mediaStore;
|
|
10462
10524
|
if (_mediaStorePromise) return _mediaStorePromise;
|
|
10463
|
-
_mediaStorePromise = (
|
|
10464
|
-
|
|
10465
|
-
|
|
10466
|
-
// maintenance, while the journal projections below remain background work.
|
|
10467
|
-
priority: 'high',
|
|
10468
|
-
requestClass: 'maintenance',
|
|
10469
|
-
timeoutMs: 30_000,
|
|
10470
|
-
});
|
|
10471
|
-
return createMediaStore(buildMediaStoreOptions({
|
|
10472
|
-
dataDir: serverConfig.CTM_DATA_DIR || path.dirname(dbModule.getDbPath()),
|
|
10473
|
-
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,
|
|
10474
10528
|
runDbWorkerOp: _runCtmDbWorkerOp,
|
|
10475
10529
|
onDrainProgress: () => { try { _loadPressure.recordProducerProgress('media-journal'); } catch {} },
|
|
10476
|
-
|
|
10477
|
-
// is the single draft version allocator. It recovers the highest pending journal version and
|
|
10478
|
-
// then applies verified owner-lineage normalization as one journaled mutation — the draft
|
|
10479
|
-
// restamp was removed from db.js restampOwnerLineage so the two can never allocate
|
|
10480
|
-
// concurrently.
|
|
10481
|
-
//
|
|
10482
|
-
// Review finding 9: the identity source is the LAZY, MEMOIZED accessor
|
|
10483
|
-
// (lib/instance-identity-access.js), not a synchronous main-thread `dbModule.…()` read on the
|
|
10484
|
-
// store-preparation path. getInstanceIdentity() derives at most once per process, only after
|
|
10485
|
-
// the DB is open, and returns null instead of ever inventing a hostname/port-derived default;
|
|
10486
|
-
// isSameOwnerLineage is the existing dbModule-free lineage snapshot used by the session-owner
|
|
10487
|
-
// hot path. Neither is a new DB boundary — both are already the reviewed identity seams.
|
|
10488
|
-
currentOwnerId: _instanceIdentityAccess.getInstanceIdentity({ required: false })?.instanceId
|
|
10530
|
+
getCurrentOwnerId: () => _instanceIdentityAccess.getInstanceIdentity({ required: false })?.instanceId
|
|
10489
10531
|
|| _instanceIdentityAccess.getCachedInstanceId()
|
|
10490
10532
|
|| serverConfig.CTM_INSTANCE_ID || '',
|
|
10491
10533
|
isSameOwnerLineage: _isSameOwnerLineageSnapshot,
|
|
10492
|
-
|
|
10493
|
-
})().then(store => {
|
|
10534
|
+
}).then(store => {
|
|
10494
10535
|
// Review finding 6: permanent-delete receipts observed while this store was preparing are
|
|
10495
10536
|
// applied FIRST — before `_mediaStore` is assigned and therefore before any reader (or the boot
|
|
10496
10537
|
// reconciler below) can observe the registry.
|
|
@@ -10513,6 +10554,7 @@ async function _prepareMediaStore() {
|
|
|
10513
10554
|
}
|
|
10514
10555
|
|
|
10515
10556
|
async function _closeMediaStore(timeoutMs = 2_000) {
|
|
10557
|
+
await _legacyDraftCancellationRecovery.close();
|
|
10516
10558
|
const store = _mediaStore;
|
|
10517
10559
|
if (!store) return;
|
|
10518
10560
|
_mediaStore = null;
|
|
@@ -10534,7 +10576,8 @@ setImageSaveRunner(async ({ promptId, buffer, filename, mimeType, deferRow, requ
|
|
|
10534
10576
|
dbModule.saveImageDecoupled(promptId, buffer, filename, mimeType, { deferRow: !!deferRow }));
|
|
10535
10577
|
});
|
|
10536
10578
|
|
|
10537
|
-
function _runLegacyTerminalDraftDbAction(action, args) {
|
|
10579
|
+
async function _runLegacyTerminalDraftDbAction(action, args) {
|
|
10580
|
+
await _legacyDraftCancellationRecovery.prepare();
|
|
10538
10581
|
return _runCtmDbWorkerOp('runDbMutation', { action, args }, {
|
|
10539
10582
|
priority: 'urgent',
|
|
10540
10583
|
requestClass: 'user-blocking',
|
|
@@ -11749,7 +11792,11 @@ function _auditAuthDecision({ req, auth, rule, decision, route, wsType, action,
|
|
|
11749
11792
|
function _rateLimitDecision(auth, rule, keyPath) {
|
|
11750
11793
|
if (auth?.isLoopback) return { ok: true };
|
|
11751
11794
|
const key = auth?.deviceId || auth?.tokenHashPrefix || 'anonymous';
|
|
11752
|
-
|
|
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);
|
|
11753
11800
|
const decision = authRateLimiter.check(kind, key);
|
|
11754
11801
|
return decision.ok ? decision : { status: 429, ...decision };
|
|
11755
11802
|
}
|
|
@@ -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
|
|
|
@@ -27533,7 +27698,7 @@ function _sessionLifecycleView(session, options = {}) {
|
|
|
27533
27698
|
pinned: !!session?._lifecyclePinned,
|
|
27534
27699
|
isActiveTab: !!(session?.id && _sessionIsDeclaredShown(session.id)),
|
|
27535
27700
|
isMru: !!(session?.id && mruIds.has(session.id)),
|
|
27536
|
-
hasBackgroundShell: !!(_statusLedgerFor(session)?.
|
|
27701
|
+
hasBackgroundShell: !!(_statusLedgerFor(session)?.backgroundActive(now)) || (!_statusLedgerFor(session)?.backgroundDeclared && _backgroundAgentsActive(session, now)),
|
|
27537
27702
|
isRestoring: _inRestoreStorm() || restoreRuntime.restoreStartingActive(session),
|
|
27538
27703
|
recentNonStatusActivityAt: Number(session?._lastMeaningfulNonStatusAt || session?.lastMeaningfulActivityAt || 0),
|
|
27539
27704
|
lastActivatedAt: Number(session?._lastActivatedAt || 0),
|
|
@@ -27913,6 +28078,25 @@ function _lifecycleProvider(session) {
|
|
|
27913
28078
|
return detectAgentType(session?.cmd || '') || session?.agentType || session?.type || 'unknown';
|
|
27914
28079
|
}
|
|
27915
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
|
+
|
|
27916
28100
|
function _canonicalLifecycleWakePhases(phases = {}) {
|
|
27917
28101
|
return {
|
|
27918
28102
|
admitted: Number(phases.gateAdmittedAt || phases.activationRequestedAt || 0),
|
|
@@ -28155,12 +28339,23 @@ function _lifecycleOrphanTotals() {
|
|
|
28155
28339
|
}
|
|
28156
28340
|
|
|
28157
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
|
+
}
|
|
28158
28353
|
for (const [pgid, prev] of [..._lifecycleOrphanGroups]) {
|
|
28159
28354
|
try {
|
|
28160
28355
|
// D-4/D-6 (session-park-busy-background-shell-truth-2026-10-14): carry the stored survivor set
|
|
28161
28356
|
// forward, so a survivor that leads its OWN process group (invisible to the pgid rollup) is not
|
|
28162
28357
|
// forgotten by the next sweep while it is still alive.
|
|
28163
|
-
const group = await _sessionResourceSampler.sampleProcessGroup(pgid, prev?.survivorPids || []);
|
|
28358
|
+
const group = await _sessionResourceSampler.sampleProcessGroup(pgid, prev?.survivorPids || [], { rows });
|
|
28164
28359
|
if (group.procCount > 0) _lifecycleOrphanGroups.set(pgid, group);
|
|
28165
28360
|
else _lifecycleOrphanGroups.delete(pgid);
|
|
28166
28361
|
} catch {}
|
|
@@ -28280,7 +28475,7 @@ function _stripPtyOutputReplies(session, sessionId, data, site) {
|
|
|
28280
28475
|
}
|
|
28281
28476
|
|
|
28282
28477
|
function _writeLifecyclePty(session, data, source = 'input', options = {}) {
|
|
28283
|
-
if (!session?.ptyProcess) return { posted: false, inputSeq: 0 };
|
|
28478
|
+
if (!session?.ptyProcess) return { posted: false, inputSeq: 0, dispatchDisposition: 'preflight-refused' };
|
|
28284
28479
|
// Image, typing and lifecycle frames share one monotonic transport sequence. Typing keeps its
|
|
28285
28480
|
// separate echo counter and passes the exact allocated frame sequence through options.inputSeq.
|
|
28286
28481
|
// WP-2 (§6.2, D6): ONE monotonic allocator for every host frame. Keystrokes pass the exact seq they
|
|
@@ -28312,23 +28507,22 @@ function _writeLifecyclePty(session, data, source = 'input', options = {}) {
|
|
|
28312
28507
|
}
|
|
28313
28508
|
}
|
|
28314
28509
|
session._lifecycleInputInFlight = true;
|
|
28510
|
+
let dispatchDisposition = 'preflight-refused';
|
|
28315
28511
|
try {
|
|
28316
|
-
//
|
|
28317
|
-
//
|
|
28318
|
-
//
|
|
28319
|
-
// _onHostInputAck below); nothing here may treat a truthy synchronous return as
|
|
28320
|
-
// 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.
|
|
28321
28515
|
const posted = writeWithDeviceReplyFirewall({
|
|
28322
28516
|
data,
|
|
28323
28517
|
exempt: authoritativeDeviceReply,
|
|
28324
|
-
|
|
28325
|
-
|
|
28326
|
-
|
|
28327
|
-
|
|
28328
|
-
|
|
28518
|
+
write: value => {
|
|
28519
|
+
const observed = _observePtyWrite(session.ptyProcess, value, { inputSeq });
|
|
28520
|
+
dispatchDisposition = observed.disposition;
|
|
28521
|
+
return observed.posted;
|
|
28522
|
+
},
|
|
28329
28523
|
onDrop: classification => _recordDroppedDeviceReply(session, source, classification),
|
|
28330
28524
|
});
|
|
28331
|
-
return { posted: posted === true, inputSeq };
|
|
28525
|
+
return { posted: posted === true, inputSeq, dispatchDisposition };
|
|
28332
28526
|
}
|
|
28333
28527
|
finally { session._lifecycleInputInFlight = false; }
|
|
28334
28528
|
}
|
|
@@ -28348,7 +28542,7 @@ function _lifecycleBackgroundLease(session, now = Date.now()) {
|
|
|
28348
28542
|
const ledger = session ? _statusLedgerFor(session) : null;
|
|
28349
28543
|
const snap = ledger && typeof ledger.backgroundSnapshot === 'function' ? ledger.backgroundSnapshot(now) : null;
|
|
28350
28544
|
if (!snap || !Array.isArray(snap.entries) || !snap.entries.length) return null;
|
|
28351
|
-
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;
|
|
28352
28546
|
for (const e of snap.entries) {
|
|
28353
28547
|
if (e.type === 'shell') shells += 1; else tasks += 1;
|
|
28354
28548
|
firstSeenAt = Math.min(firstSeenAt, Number(e.firstSeenAt) || 0);
|
|
@@ -28356,8 +28550,10 @@ function _lifecycleBackgroundLease(session, now = Date.now()) {
|
|
|
28356
28550
|
expiresAt = Math.min(expiresAt, Number(e.expiresAt) || 0);
|
|
28357
28551
|
renewals += Number(e.renewals) || 0;
|
|
28358
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);
|
|
28359
28555
|
}
|
|
28360
|
-
return { shells, tasks, firstSeenAt, renewedAt, expiresAt, renewals, capCrossed };
|
|
28556
|
+
return { shells, tasks, firstSeenAt, renewedAt, expiresAt, renewals, capCrossed, quiet, activeAt };
|
|
28361
28557
|
}
|
|
28362
28558
|
|
|
28363
28559
|
function _lifecycleJournalGate(session, decision, configName) {
|
|
@@ -29013,6 +29209,17 @@ async function _sampleLifecycleResources(options = {}) {
|
|
|
29013
29209
|
_sessionResourceSampler.warnedPressureFailure = true;
|
|
29014
29210
|
console.warn('[lifecycle] memory-pressure probe was unparseable; treating pressure as normal');
|
|
29015
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
|
+
});
|
|
29016
29223
|
for (const resource of snapshot.sessions || []) {
|
|
29017
29224
|
const session = sessions.get(resource.id);
|
|
29018
29225
|
if (!session || session._restoreDeferred) continue;
|
|
@@ -29029,7 +29236,7 @@ async function _sampleLifecycleResources(options = {}) {
|
|
|
29029
29236
|
// matches every other footprint field's "sampled vs not yet" contract in this file.
|
|
29030
29237
|
session.footprintMB = resource.footprintMB == null ? null : resource.footprintMB;
|
|
29031
29238
|
_statusBindings.applyCpuSample(session, resource);
|
|
29032
|
-
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
|
|
29033
29240
|
}
|
|
29034
29241
|
const previousPressure = _lifecyclePressure.level;
|
|
29035
29242
|
const pressureResult = _lifecyclePressure.observe(snapshot.signals || snapshot.pressure, {
|
|
@@ -29895,7 +30102,12 @@ wss.on('connection', async (ws, req) => {
|
|
|
29895
30102
|
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
29896
30103
|
const reqSnapshot = {
|
|
29897
30104
|
socket: { remoteAddress: _remoteIpFromReq(req) },
|
|
29898
|
-
|
|
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
|
+
},
|
|
29899
30111
|
};
|
|
29900
30112
|
const cfDecision = cloudflareAccessVerifier.verifyRequestSync(req, { isLocalhost });
|
|
29901
30113
|
if (!cfDecision.ok) {
|
|
@@ -31389,6 +31601,9 @@ headlessWorker.on('message', _perfTracker.wrap('worker:headless-terminal-message
|
|
|
31389
31601
|
? _recordInputEchoFromFrame(session, msg.echoedThroughSeq, Date.now(), {
|
|
31390
31602
|
targetClientId: String(msg.targetClientId || ''),
|
|
31391
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,
|
|
31392
31607
|
})
|
|
31393
31608
|
: [];
|
|
31394
31609
|
const holdingRestoreFrames = _stateSyncHoldingRestoreFrames(session);
|
|
@@ -33896,6 +34111,35 @@ function _sendWalleSessionCreated(ws, session, extra = {}) {
|
|
|
33896
34111
|
return true;
|
|
33897
34112
|
}
|
|
33898
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
|
+
|
|
33899
34143
|
// WP-3 (design/walle-conv-order-authority-2026-08-21 D-5, §6): min/max order key over a
|
|
33900
34144
|
// served page's messages, for the envelope's keyRange. Min/max (not first/last array
|
|
33901
34145
|
// position) because callers serve pages in different physical row orders (e.g. a
|
|
@@ -35913,6 +36157,37 @@ function _onHostInputAck(session, ack, origin = null) {
|
|
|
35913
36157
|
}
|
|
35914
36158
|
}
|
|
35915
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
|
+
|
|
35916
36191
|
function _recordInputEchoFromFrame(session, echoedThroughSeq, at = Date.now(), target = {}) {
|
|
35917
36192
|
if (!session || !(session._inputEchoPending instanceof Map)) return [];
|
|
35918
36193
|
const echoed = Math.max(0, Number(echoedThroughSeq) || 0);
|
|
@@ -35930,6 +36205,10 @@ function _recordInputEchoFromFrame(session, echoedThroughSeq, at = Date.now(), t
|
|
|
35930
36205
|
}
|
|
35931
36206
|
const targetClientId = String(target.targetClientId || '');
|
|
35932
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));
|
|
35933
36212
|
for (const [seq, rawRecord] of session._typingInteractionByInputSeq || []) {
|
|
35934
36213
|
if (seq > echoed) continue;
|
|
35935
36214
|
const record = typeof rawRecord === 'string'
|
|
@@ -35938,6 +36217,14 @@ function _recordInputEchoFromFrame(session, echoedThroughSeq, at = Date.now(), t
|
|
|
35938
36217
|
if (!record?.interactionId) continue;
|
|
35939
36218
|
if (!record.mirrored) {
|
|
35940
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
|
+
}
|
|
35941
36228
|
record.mirrored = true;
|
|
35942
36229
|
session._typingInteractionByInputSeq.set(seq, record);
|
|
35943
36230
|
}
|
|
@@ -36295,7 +36582,7 @@ function _terminateResumeLifecycle(session, failure) {
|
|
|
36295
36582
|
agentType: session._resumeAgentType || detectAgentType(session.cmd) || '',
|
|
36296
36583
|
generation,
|
|
36297
36584
|
};
|
|
36298
|
-
markSessionResumeBlocked(session.id, info);
|
|
36585
|
+
markSessionResumeBlocked(session.id, info, 'lifecycle-terminate');
|
|
36299
36586
|
session._resumeAttemptFailed = true;
|
|
36300
36587
|
// This is a system-enforced provider termination, not an explicit user close. The PTY exit
|
|
36301
36588
|
// handler already suppresses auto-restart while `resumeBlocked` is set, and
|
|
@@ -36846,6 +37133,10 @@ const _DETERMINISTIC_PREFLIGHT_CODES = new Set([
|
|
|
36846
37133
|
'missing_claude_transcript',
|
|
36847
37134
|
'missing_codex_rollout',
|
|
36848
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',
|
|
36849
37140
|
]);
|
|
36850
37141
|
|
|
36851
37142
|
// Pure decision (test seam): is a resume-preflight failure DETERMINISTIC (quarantine) or
|
|
@@ -36871,7 +37162,7 @@ function _shouldRefreshLaunchReadinessForBlock(code, blockDeterministic) {
|
|
|
36871
37162
|
return code === 'launch_rollout_index_pending' && blockDeterministic !== true;
|
|
36872
37163
|
}
|
|
36873
37164
|
|
|
36874
|
-
function markSessionResumeBlocked(sessionId, info = {}) {
|
|
37165
|
+
function markSessionResumeBlocked(sessionId, info = {}, site = 'unspecified') {
|
|
36875
37166
|
const id = String(sessionId || '');
|
|
36876
37167
|
if (!id) return null;
|
|
36877
37168
|
const record = {
|
|
@@ -36892,24 +37183,18 @@ function markSessionResumeBlocked(sessionId, info = {}) {
|
|
|
36892
37183
|
checkedAt: info.checkedAt || '',
|
|
36893
37184
|
// WP-2: 'removed'|'never_seen'; only set on the two missing-transcript codes.
|
|
36894
37185
|
localHistory: info.localHistory || (['missing_claude_transcript', 'missing_codex_rollout'].includes(info.code) ? 'unknown' : ''), lastSeenBytes: Number(info.lastSeenBytes) || 0,
|
|
36895
|
-
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 || '',
|
|
36896
37187
|
// WP-6: the resume ladder's answer, only on the two missing-transcript codes.
|
|
36897
37188
|
resumeRung: info.resumeRung || '', coverage: info.coverage || null, offerStoreFirst: info.offerStoreFirst === true, restoreError: info.restoreError || '',
|
|
36898
37189
|
};
|
|
36899
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));
|
|
36900
37194
|
// WP-2: callers that built an info object inline get the enriched record back instead of re-reading the map.
|
|
36901
37195
|
return record;
|
|
36902
37196
|
}
|
|
36903
37197
|
|
|
36904
|
-
// The ladder fields every resume-blocked message for a missing transcript carries (S-33).
|
|
36905
|
-
function _resumeBlockedRungWire(info) {
|
|
36906
|
-
return {
|
|
36907
|
-
resumeRung: info?.resumeRung || '', coverage: info?.coverage || null, offerStoreFirst: info?.offerStoreFirst === true, restoreError: info?.restoreError || '',
|
|
36908
|
-
deterministic: info?.deterministic === true, origin: info?.origin || 'unknown',
|
|
36909
|
-
storeTurns: normalizeStoreTurns(info?.storeTurns), checkedAt: info?.checkedAt || '',
|
|
36910
|
-
};
|
|
36911
|
-
}
|
|
36912
|
-
|
|
36913
37198
|
// WP-2 (multi-machine §C): the pure relative-origin rule now lives in lib/resume-origin-facts.js, so this
|
|
36914
37199
|
// launch-plan projection, the resume-blocked record and the transcripts route share exactly one rule.
|
|
36915
37200
|
// Re-exported here under the name every existing call site in this file already uses.
|
|
@@ -36944,9 +37229,17 @@ function isSessionResumeBlocked(sessionId) {
|
|
|
36944
37229
|
// explicit user action (attach, click, "Start fresh here") — those paths consult the block
|
|
36945
37230
|
// directly (see `_startDeferredRestoreSession`), not this drain-eligibility predicate.
|
|
36946
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.
|
|
36947
37234
|
if (!session || !session._restoreDeferred || session._restoreStarting || session._exited) return false;
|
|
36948
|
-
if (!sessionPolicy(session).relaunchFromRestore)
|
|
36949
|
-
|
|
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
|
+
}
|
|
36950
37243
|
return true;
|
|
36951
37244
|
}
|
|
36952
37245
|
|
|
@@ -37021,16 +37314,16 @@ function _notifyResumePreflightFailure(id, ws, agentType, result) {
|
|
|
37021
37314
|
let blockedInfo = null;
|
|
37022
37315
|
if (deterministic) {
|
|
37023
37316
|
blockedInfo = {
|
|
37024
|
-
...
|
|
37317
|
+
...resumeBlockedRungWire(result), code: result.code || 'resume_blocked', message, agentType, detail: result.detail || '', attempts: Number(result.attempts || 0), deterministic: true,
|
|
37025
37318
|
origin: result.origin || _resumeOriginForSession(id), storeTurns: result.storeTurns, checkedAt: result.checkedAt || new Date().toISOString(),
|
|
37026
|
-
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,
|
|
37027
37320
|
};
|
|
37028
|
-
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)
|
|
37029
37322
|
}
|
|
37030
37323
|
if (!ws) return deterministic;
|
|
37031
37324
|
if (deterministic) {
|
|
37032
37325
|
try {
|
|
37033
|
-
_wsSend(ws, JSON.stringify(resumeBlockedWire(id, blockedInfo, { agentType, rungWire:
|
|
37326
|
+
_wsSend(ws, JSON.stringify(resumeBlockedWire(id, blockedInfo, { agentType, rungWire: resumeBlockedRungWire(blockedInfo) })));
|
|
37034
37327
|
} catch {}
|
|
37035
37328
|
} else {
|
|
37036
37329
|
try {
|
|
@@ -37091,7 +37384,7 @@ function _notifyPreparedResumePreflightFailure(id, ws, agentType, resumePrefligh
|
|
|
37091
37384
|
if (MISSING_TRANSCRIPT_CODES.has(resumePreflight.code) && existingSession) {
|
|
37092
37385
|
existingSession._resumeRung = {
|
|
37093
37386
|
code: resumePreflight.code, message: resumePreflight.message || '', resumeId: resumePreflight.resumeId || '',
|
|
37094
|
-
...
|
|
37387
|
+
...resumeBlockedRungWire(resumePreflight), origin: resumePreflight.origin || _resumeOriginForSession(id),
|
|
37095
37388
|
};
|
|
37096
37389
|
}
|
|
37097
37390
|
_notifyResumePreflightFailure(id, ws, agentType, resumePreflight);
|
|
@@ -37129,7 +37422,7 @@ function _blockResumeAttempt(session, code, message, detail) {
|
|
|
37129
37422
|
attempts: Number(attemptState.failures || attemptState.attempt || 1),
|
|
37130
37423
|
agentType: session._resumeAgentType || detectAgentType(session.cmd) || '',
|
|
37131
37424
|
};
|
|
37132
|
-
markSessionResumeBlocked(session.id, info);
|
|
37425
|
+
markSessionResumeBlocked(session.id, info, 'attempt-block');
|
|
37133
37426
|
markResumeFailureExit(session, { code, source: code });
|
|
37134
37427
|
_clearResumeLoading(session, code);
|
|
37135
37428
|
recordSessionDiagnostic(session.id, 'resume-blocked', info);
|
|
@@ -37363,6 +37656,9 @@ const preparedSessionCreate = require('./lib/session-create/prepared').createPre
|
|
|
37363
37656
|
ptyStartupCommitComplete,
|
|
37364
37657
|
_rollbackPtyStartupAttempt,
|
|
37365
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,
|
|
37366
37662
|
get _ownerRecovery() { return _ownerRecovery; },
|
|
37367
37663
|
});
|
|
37368
37664
|
|
|
@@ -37691,6 +37987,8 @@ sessionCreateProvider.configure(() => ({
|
|
|
37691
37987
|
parseSessionStartMs,
|
|
37692
37988
|
path,
|
|
37693
37989
|
recordSessionDiagnostic,
|
|
37990
|
+
// WP-1 (§5 D-4): the deterministic refusal hook the launch-time native-provider gate uses.
|
|
37991
|
+
markSessionResumeBlocked,
|
|
37694
37992
|
resolveResumeGeneration,
|
|
37695
37993
|
// WP-2 (default-subagent-model-delegate-pick-2026-10-13.html §5 D-1..D-3): the Wall-E delegate
|
|
37696
37994
|
// pick reader for a lead launch. Injected here (not required in provider-process) so the launch
|
|
@@ -37969,6 +38267,40 @@ function apiResumeSession(req, res) {
|
|
|
37969
38267
|
if (_ownerDecisionInvalidRequest(ownerDecision)) return sendJsonResponse(res, 400, { ok: false, error: 'SESSION_ID_REQUIRED' });
|
|
37970
38268
|
_emitOwnerMismatchDiagnostic(ownerDecision);
|
|
37971
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
|
+
}
|
|
37972
38304
|
await handleCreatePrepared(null, {
|
|
37973
38305
|
id: launch.id,
|
|
37974
38306
|
title: launch.title,
|
|
@@ -40063,7 +40395,9 @@ function sendDeferredRestoreStatus(ws, session, sessionId, cols, rows, source, o
|
|
|
40063
40395
|
id: sessionId,
|
|
40064
40396
|
cached: served,
|
|
40065
40397
|
starting,
|
|
40066
|
-
|
|
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 || ''),
|
|
40067
40401
|
message: opts.message || (starting
|
|
40068
40402
|
? 'Starting terminal process for the visible session.'
|
|
40069
40403
|
: 'Session terminal process is deferred until this tab is activated.'),
|
|
@@ -40073,9 +40407,10 @@ function sendDeferredRestoreStatus(ws, session, sessionId, cols, rows, source, o
|
|
|
40073
40407
|
}
|
|
40074
40408
|
|
|
40075
40409
|
// Extracted to lib/restore-prepare-retry.js (createScheduleDeferredRestoreStart); semantics unchanged.
|
|
40076
|
-
|
|
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 });
|
|
40077
40412
|
|
|
40078
|
-
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)
|
|
40079
40414
|
|
|
40080
40415
|
function _maybeAutoStartDeferredRestore(session, msg, source, ws) {
|
|
40081
40416
|
if (!session?._restoreDeferred || !msg?.autoStartRestore) return false;
|
|
@@ -40088,6 +40423,9 @@ function _maybeAutoStartDeferredRestore(session, msg, source, ws) {
|
|
|
40088
40423
|
reason: msg.prewarm === true ? 'prewarm-disabled' : 'background-client',
|
|
40089
40424
|
clientKind: _lifecycleClientKind(ws, msg),
|
|
40090
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' });
|
|
40091
40429
|
return false;
|
|
40092
40430
|
}
|
|
40093
40431
|
if (!shouldAutoStartDeferredRestore(session, msg, source)) {
|
|
@@ -40969,12 +41307,12 @@ function handleResumeRetry(ws, msg) {
|
|
|
40969
41307
|
// A quarantine record gets its full facts again; no record means there is nothing blocking.
|
|
40970
41308
|
const record = id ? getSessionResumeBlocked(id) : null;
|
|
40971
41309
|
const send = (reply) => { if (ws) try { _wsSend(ws, JSON.stringify(reply)); } catch { /* expected: the asking socket closed; the next attach replays the record */ } };
|
|
40972
|
-
const wire = (r) => resumeBlockedWire(id, r, { agentType: session?.agentType, rungWire:
|
|
41310
|
+
const wire = (r) => resumeBlockedWire(id, r, { agentType: session?.agentType, rungWire: resumeBlockedRungWire(r) });
|
|
40973
41311
|
if (record) { // Check again re-asks the ladder for any blocked record; a stale record is never replayed as the answer
|
|
40974
41312
|
Promise.resolve().then(() => { // every outcome replies: a throw anywhere below answers with the unchanged (stale) record
|
|
40975
41313
|
const agent = dbModule.getSessionIdentity(id)?.primaryAgent || {};
|
|
40976
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 || '') } });
|
|
40977
|
-
}).then((fresh) => { markSessionResumeBlocked(id, fresh); send(wire(getSessionResumeBlocked(id) || fresh)); })
|
|
41315
|
+
}).then((fresh) => { markSessionResumeBlocked(id, fresh, 'resume-retry-recheck'); send(wire(getSessionResumeBlocked(id) || fresh)); })
|
|
40978
41316
|
.catch((error) => { console.warn(`[resume-retry] recheck failed for ${id.slice(0, 8)}: ${error && error.message}`); send(wire(record)); });
|
|
40979
41317
|
} else send(record ? wire(record) : { type: 'resume-retry-noop', id, checkedAt: new Date().toISOString() });
|
|
40980
41318
|
return false;
|
|
@@ -41580,11 +41918,11 @@ function _imageDeliveryAckWindowMs() {
|
|
|
41580
41918
|
}
|
|
41581
41919
|
|
|
41582
41920
|
// The classified failure reason a refused/no-op image write leaves on the draft. Kept next to the
|
|
41583
|
-
// delivery path so
|
|
41584
|
-
//
|
|
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.
|
|
41585
41923
|
function _imageWriteFailureReason(error) {
|
|
41586
41924
|
const code = String((error && error.code) || '');
|
|
41587
|
-
if (code === 'pty_write_not_posted') return code;
|
|
41925
|
+
if (code === 'pty_write_not_posted' || code === 'pty_dispatch_uncertain') return code;
|
|
41588
41926
|
if (code.startsWith('pty_write_ack_')) return code.slice(0, 64);
|
|
41589
41927
|
return 'Provider delivery could not be started';
|
|
41590
41928
|
}
|
|
@@ -41668,14 +42006,14 @@ async function _deliverImageDraftAttempt(sessionId, attachmentId, deliveryAttemp
|
|
|
41668
42006
|
// draw from, so this frame's ack number is unique for the session and can never be confused with a
|
|
41669
42007
|
// keystroke's echo identity (which is tracked by map membership, not by a numeric range).
|
|
41670
42008
|
const writeResult = _writeLifecyclePty(session, payload, 'image-input');
|
|
41671
|
-
// A POST is not a write
|
|
41672
|
-
//
|
|
41673
|
-
//
|
|
41674
|
-
// branch discarded it) can never be acked, and the draft must not conclude that it was delivered:
|
|
41675
|
-
// 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".
|
|
41676
42012
|
if (!writeResult || writeResult.posted !== true) {
|
|
41677
|
-
const
|
|
41678
|
-
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';
|
|
41679
42017
|
throw error;
|
|
41680
42018
|
}
|
|
41681
42019
|
// §6.3/D6: a PATH-mode draft reaches 'attached' only on the host's actual written ack — a
|
|
@@ -44679,7 +45017,7 @@ function handleKill(ws, msg) {
|
|
|
44679
45017
|
async function handleKillAsync(ws, msg) {
|
|
44680
45018
|
if (_ownerRecovery.rejectRequest(ws, msg)) return null; const session = sessions.get(msg.id);
|
|
44681
45019
|
if (!session) return;
|
|
44682
|
-
const ownerDecision =
|
|
45020
|
+
const ownerDecision = _sessionOwnerDecision(msg.id);
|
|
44683
45021
|
if (!ownerDecision.ok) {
|
|
44684
45022
|
if (_ownerDecisionInvalidRequest(ownerDecision)) return;
|
|
44685
45023
|
_emitOwnerMismatchDiagnostic(ownerDecision);
|
|
@@ -44713,6 +45051,8 @@ async function handleKillAsync(ws, msg) {
|
|
|
44713
45051
|
}
|
|
44714
45052
|
session._intentionalKill = true;
|
|
44715
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);
|
|
44716
45056
|
if (RESUME_LIFECYCLE_V2_ENABLED && session._resumeAttemptState) {
|
|
44717
45057
|
_resumeLifecycleCoordinator.dispatch(session, {
|
|
44718
45058
|
type: RESUME_EVENT.CANCEL,
|
|
@@ -44732,14 +45072,12 @@ async function handleKillAsync(ws, msg) {
|
|
|
44732
45072
|
broadcastSessionList(true);
|
|
44733
45073
|
return;
|
|
44734
45074
|
}
|
|
44735
|
-
//
|
|
44736
|
-
//
|
|
44737
|
-
//
|
|
44738
|
-
|
|
44739
|
-
// the row at status='running' and the session would be resurrected by restore on
|
|
44740
|
-
// the next boot. The onExit retire stays as an idempotent backstop.
|
|
44741
|
-
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');
|
|
44742
45079
|
try { _killSessionProcess(session, 'user_close'); } catch { /* expected: a PTY already gone on user close must not fail the close handler */ }
|
|
45080
|
+
await retire;
|
|
44743
45081
|
}
|
|
44744
45082
|
|
|
44745
45083
|
// Delegates to lib/session-workspace-binding.js's TTL-memoized safeRealpath (60s TTL, bounded
|
|
@@ -46992,7 +47330,10 @@ function handleList(ws) {
|
|
|
46992
47330
|
const listPayload = JSON.stringify({
|
|
46993
47331
|
type: 'sessions',
|
|
46994
47332
|
restoring: _restoreInProgress,
|
|
46995
|
-
|
|
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 }))),
|
|
46996
47337
|
});
|
|
46997
47338
|
_wsSend(ws, listPayload, { class: 'interactive', channel: 'sessions' });
|
|
46998
47339
|
_kickPendingAutoResumeDeferred('client-list');
|
|
@@ -48638,7 +48979,7 @@ function _sessionPayload(s, options = {}) {
|
|
|
48638
48979
|
// WP-2: only set on the two missing-transcript codes.
|
|
48639
48980
|
resumeBlockedLocalHistory: resumeBlocked?.localHistory || '', resumeBlockedLastSeenBytes: Math.max(0, Number(resumeBlocked?.lastSeenBytes || 0)),
|
|
48640
48981
|
resumeBlockedLastSeenAt: resumeBlocked?.lastSeenAt || '', resumeBlockedTrashPath: resumeBlocked?.trashPath || '',
|
|
48641
|
-
resumeBlockedOriginalPath: resumeBlocked?.originalPath || '',
|
|
48982
|
+
resumeBlockedOriginalPath: resumeBlocked?.originalPath || '', resumeBlockedTrashReason: resumeBlocked?.trashReason || '', resumeBlockedTrashedAt: resumeBlocked?.trashedAt || '',
|
|
48642
48983
|
resumeBlockedRung: resumeBlocked?.resumeRung || '', resumeBlockedCoverage: resumeBlocked?.coverage || null,
|
|
48643
48984
|
resumeBlockedOfferStoreFirst: resumeBlocked?.offerStoreFirst === true, resumeBlockedRestoreError: resumeBlocked?.restoreError || '',
|
|
48644
48985
|
resumeBlockedOrigin: resumeBlocked?.origin || 'unknown',
|
|
@@ -50167,8 +50508,13 @@ const _broadcastPayload = require('./lib/broadcast-serialize').createBroadcastPa
|
|
|
50167
50508
|
// `arr` is the session snapshot the payloads were built from (used for the elision signature so it stays
|
|
50168
50509
|
// consistent with `built` even if the live sessions Map mutated during a sliced build's yields).
|
|
50169
50510
|
function _finishBroadcast(arr, built, fresh) {
|
|
50170
|
-
|
|
50171
|
-
|
|
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) });
|
|
50172
50518
|
}
|
|
50173
50519
|
|
|
50174
50520
|
function _maybePrewarmRestoreBroadcastPayloadCaches(sessionList) {
|
|
@@ -51259,7 +51605,8 @@ async function _quiesceSessionsForProcessExit(reason = 'shutdown') {
|
|
|
51259
51605
|
}
|
|
51260
51606
|
|
|
51261
51607
|
// --- Graceful Shutdown ---
|
|
51262
|
-
|
|
51608
|
+
// R3 WP-2: `signal` ('SIGTERM'/'SIGINT', passed by Node) is recorded in the clean-shutdown marker's reason.
|
|
51609
|
+
function shutdown(signal) {
|
|
51263
51610
|
if (_isShuttingDown) return;
|
|
51264
51611
|
console.log('\n Shutting down...');
|
|
51265
51612
|
_isShuttingDown = true;
|
|
@@ -51341,6 +51688,8 @@ function shutdown() {
|
|
|
51341
51688
|
new Promise((resolve) => { const t = setTimeout(resolve, 3000); if (t && typeof t.unref === 'function') t.unref(); }),
|
|
51342
51689
|
]);
|
|
51343
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 */ }
|
|
51344
51693
|
await _quiesceSessionsForProcessExit('shutdown');
|
|
51345
51694
|
try { await headlessWorker.terminate({ intent: 'restart' }); } catch {} // hosts linger through SIGTERM (WP-2)
|
|
51346
51695
|
_ctmDbClosingForProcessExit = true;
|
|
@@ -51354,6 +51703,9 @@ function shutdown() {
|
|
|
51354
51703
|
if (_imageWriteProofs) _imageWriteProofs.dispose();
|
|
51355
51704
|
try { await _desktopIndex.stop(); } catch {}
|
|
51356
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 });
|
|
51357
51709
|
try { await managedBrowserBroker.close(); } catch (e) { console.error('[shutdown] managed browser close failed:', e.message); }
|
|
51358
51710
|
try { await runtimeGoalStore.close?.(); } catch (e) { console.error('[shutdown] runtime goal store close failed:', e.message); }
|
|
51359
51711
|
// Kill Wall-E child so it doesn't outlive us (detached + unref'd = orphan
|
|
@@ -51538,7 +51890,21 @@ function _startSdWatchdog() {
|
|
|
51538
51890
|
function _startHostedSyncWorker() {
|
|
51539
51891
|
if (_hostedSyncWorker) return;
|
|
51540
51892
|
try {
|
|
51541
|
-
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
|
+
});
|
|
51542
51908
|
_hostedSyncWorker = worker;
|
|
51543
51909
|
if (worker.started) console.log(`[sync] hosted sync worker started for ${worker.peers.length} peer(s)`);
|
|
51544
51910
|
} catch (err) {
|
|
@@ -51781,10 +52147,20 @@ function _executeHook(hook, data) {
|
|
|
51781
52147
|
}
|
|
51782
52148
|
}
|
|
51783
52149
|
|
|
51784
|
-
|
|
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 } = {}) {
|
|
51785
52159
|
let walleStatus;
|
|
51786
52160
|
try {
|
|
51787
|
-
walleStatus =
|
|
52161
|
+
walleStatus = snapshot
|
|
52162
|
+
? await walleSupervisor.getStatusSnapshot()
|
|
52163
|
+
: await walleSupervisor.getStatus();
|
|
51788
52164
|
} catch (err) {
|
|
51789
52165
|
walleStatus = { running: false, pid: null, error: err.message };
|
|
51790
52166
|
}
|
|
@@ -51810,6 +52186,9 @@ function _buildInstanceManifest(walleStatus, ctmUptime) {
|
|
|
51810
52186
|
golivePlan: _golivePlanCache,
|
|
51811
52187
|
observedOrigin: _lastMicrosoftTunnelOrigin,
|
|
51812
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(),
|
|
51813
52192
|
});
|
|
51814
52193
|
}
|
|
51815
52194
|
|
|
@@ -52082,7 +52461,7 @@ function _storageRiskSnapshot() {
|
|
|
52082
52461
|
|
|
52083
52462
|
async function _buildHostedRuntimeContextUncached() {
|
|
52084
52463
|
const ctmUptime = Math.floor((Date.now() - _ctmStartTime) / 1000);
|
|
52085
|
-
const walleStatus = await _getWalleStatusSafe();
|
|
52464
|
+
const walleStatus = await _getWalleStatusSafe({ snapshot: true }); // R-4: never the full ownership probe or the recovery await
|
|
52086
52465
|
const instance = _buildInstanceManifest(walleStatus, ctmUptime);
|
|
52087
52466
|
const envWorkspaceBindings = buildWorkspaceBindingsStatus({ manifest: instance, env: process.env });
|
|
52088
52467
|
void _detachOwnerMutation('hosted-workspace-bindings-seed', () =>
|
|
@@ -52097,7 +52476,9 @@ async function _buildHostedRuntimeContextUncached() {
|
|
|
52097
52476
|
const localEdge = buildLocalEdgeStatus({ manifest: instance, env: process.env });
|
|
52098
52477
|
const syncSchema = hostedStorage.syncSchema;
|
|
52099
52478
|
syncSchema.worker = _hostedSyncWorkerStatus();
|
|
52100
|
-
|
|
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 }) });
|
|
52101
52482
|
const security = await _buildRemoteSecuritySummary(instance);
|
|
52102
52483
|
const terminalUx = buildTerminalRemoteUxStatus({ manifest: instance, env: process.env });
|
|
52103
52484
|
const deadMan = buildDeadManStatus({ env: process.env });
|
|
@@ -52180,7 +52561,7 @@ function _manifestCorsHeaders(req) {
|
|
|
52180
52561
|
|
|
52181
52562
|
async function apiInstanceManifest(req, res) {
|
|
52182
52563
|
const ctmUptime = Math.floor((Date.now() - _ctmStartTime) / 1000);
|
|
52183
|
-
const walleStatus = await _getWalleStatusSafe();
|
|
52564
|
+
const walleStatus = await _getWalleStatusSafe({ snapshot: true }); // D-7 (WP-5): status GETs read the snapshot, not the process table
|
|
52184
52565
|
const manifest = _buildInstanceManifest(walleStatus, ctmUptime);
|
|
52185
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
|
|
52186
52567
|
}
|
|
@@ -52888,8 +53269,6 @@ function fetchMcpGatewayLedgerStatus(timeoutMs = 300) {
|
|
|
52888
53269
|
async function apiServicesStatus(req, res) {
|
|
52889
53270
|
const {
|
|
52890
53271
|
ctmUptime,
|
|
52891
|
-
walleStatus,
|
|
52892
|
-
instance,
|
|
52893
53272
|
sync,
|
|
52894
53273
|
workspaceBindings,
|
|
52895
53274
|
localEdge,
|
|
@@ -52898,6 +53277,8 @@ async function apiServicesStatus(req, res) {
|
|
|
52898
53277
|
deadMan,
|
|
52899
53278
|
hostedReadiness,
|
|
52900
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
|
|
52901
53282
|
const mcpGateway = await fetchMcpGatewayStatus();
|
|
52902
53283
|
const walleRuntimeStatus = walleStatus.running
|
|
52903
53284
|
? await getWalleStatusDataOrNull(500)
|
|
@@ -53538,6 +53919,22 @@ function _recordWorktreeCreateJobProgress(job, event) {
|
|
|
53538
53919
|
};
|
|
53539
53920
|
job.progress = progress;
|
|
53540
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
|
+
}
|
|
53541
53938
|
job.events.push({
|
|
53542
53939
|
at: now,
|
|
53543
53940
|
stage: progress.stage,
|
|
@@ -54181,15 +54578,24 @@ function apiAdoptBranch(req, res) {
|
|
|
54181
54578
|
});
|
|
54182
54579
|
}
|
|
54183
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
|
+
|
|
54184
54588
|
function apiCreateWorktree(req, res) {
|
|
54185
54589
|
_worktreeReadJsonBody(req, res, async (payload) => {
|
|
54186
54590
|
const reqUrl = new URL(req.url, 'http://localhost');
|
|
54187
54591
|
const { name, base_branch, cwd: reqCwd, namespace, agentType } = payload;
|
|
54188
54592
|
if (!name || !/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
54593
|
+
_logWorktreeCreateJobRejected('invalid_name', 'Invalid worktree name. Use alphanumeric, hyphens, underscores only.');
|
|
54189
54594
|
res.writeHead(400, WORKTREE_JSON_HEADERS);
|
|
54190
54595
|
return res.end(JSON.stringify({ error: 'Invalid worktree name. Use alphanumeric, hyphens, underscores only.' }));
|
|
54191
54596
|
}
|
|
54192
54597
|
if (base_branch && !VALID_GIT_REF.test(base_branch)) {
|
|
54598
|
+
_logWorktreeCreateJobRejected('invalid_base_branch', 'Invalid base branch name');
|
|
54193
54599
|
res.writeHead(400, WORKTREE_JSON_HEADERS);
|
|
54194
54600
|
return res.end(JSON.stringify({ error: 'Invalid base branch name' }));
|
|
54195
54601
|
}
|
|
@@ -54201,6 +54607,7 @@ function apiCreateWorktree(req, res) {
|
|
|
54201
54607
|
const rawRequestId = payload.request_id || payload.requestId || '';
|
|
54202
54608
|
const requestId = normalizeWorktreeCreateRequestId(rawRequestId);
|
|
54203
54609
|
if (rawRequestId && !requestId) {
|
|
54610
|
+
_logWorktreeCreateJobRejected('invalid_request_id', 'Invalid worktree create request id.');
|
|
54204
54611
|
res.writeHead(400, WORKTREE_JSON_HEADERS);
|
|
54205
54612
|
return res.end(JSON.stringify({ error: 'Invalid worktree create request id.' }));
|
|
54206
54613
|
}
|
|
@@ -54216,6 +54623,7 @@ function apiCreateWorktree(req, res) {
|
|
|
54216
54623
|
job = findReusableWorktreeCreateJob(_worktreeCreateJobs, createParams)
|
|
54217
54624
|
|| _startWorktreeCreateJob(createParams);
|
|
54218
54625
|
} catch (e) {
|
|
54626
|
+
_logWorktreeCreateJobRejected(e.code || 'WORKTREE_CREATE_FAILED', e.message);
|
|
54219
54627
|
res.writeHead(e.statusCode || 500, WORKTREE_JSON_HEADERS);
|
|
54220
54628
|
return res.end(JSON.stringify({ error: e.message, code: e.code || 'WORKTREE_CREATE_FAILED' }));
|
|
54221
54629
|
}
|
|
@@ -54575,7 +54983,10 @@ function apiSyncAllWorktrees(req, res) {
|
|
|
54575
54983
|
});
|
|
54576
54984
|
}
|
|
54577
54985
|
|
|
54578
|
-
|
|
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 } = {}) {
|
|
54579
54990
|
return summarizeRestartGuard(sessions.values(), {
|
|
54580
54991
|
statusForSession: (session) => _standupLiveStatusForSession(session, now),
|
|
54581
54992
|
isWaitingForInput: (session) => _isServerWaitingForInput(session.id, session, now),
|
|
@@ -54584,7 +54995,7 @@ function _restartGuardSnapshot(now = Date.now()) {
|
|
|
54584
54995
|
// WP-4 (S-16): reuse the existing hosted-session discriminator (WP-2's
|
|
54585
54996
|
// `_sessionIsHosted`, defined above at _quiesceSessionsForProcessExit) --
|
|
54586
54997
|
// do not redefine it here.
|
|
54587
|
-
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
|
|
54588
54999
|
});
|
|
54589
55000
|
}
|
|
54590
55001
|
|
|
@@ -55881,6 +56292,7 @@ function _announceDeferredRestoreRetry(session, opts) {
|
|
|
55881
56292
|
// Extracted to lib/restore-backoff-gate.js (semantics unchanged; 'prepare-retry' is user-initiated per design D-4).
|
|
55882
56293
|
const _awaitRestoreBackoffOutsideGate = createAwaitRestoreBackoffOutsideGate({
|
|
55883
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
|
|
55884
56296
|
});
|
|
55885
56297
|
|
|
55886
56298
|
function _startDeferredRestoreSession(session, opts = {}) {
|
|
@@ -55954,12 +56366,18 @@ async function _startDeferredRestoreSessionAttempt(session, opts = {}) {
|
|
|
55954
56366
|
}
|
|
55955
56367
|
}
|
|
55956
56368
|
if (sharedCapacityAdmission) {
|
|
55957
|
-
|
|
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
|
+
}
|
|
55958
56375
|
if (session._activationAdmissionPromise) { _endInteractiveActivationIfNeeded(); return session._activationAdmissionPromise; }
|
|
55959
56376
|
}
|
|
55960
56377
|
if (!capacity.ok) {
|
|
55961
56378
|
session._lifecycleState = session._parkedAt ? 'parked' : 'deferred';
|
|
55962
56379
|
_notifyLifecycleWakeBlocked(session, capacity);
|
|
56380
|
+
_noteWakeSkip(session, 'WakeBlocked', {}, id); // WP-3 (§9 S-12)
|
|
55963
56381
|
console.log(`[lifecycle] wake-blocked id=${id.slice(0, 8)} reason=${capacity.code || 'live-session-cap'}`);
|
|
55964
56382
|
_endInteractiveActivationIfNeeded();
|
|
55965
56383
|
return false;
|
|
@@ -56035,7 +56453,10 @@ async function _startDeferredRestoreSessionAttempt(session, opts = {}) {
|
|
|
56035
56453
|
session._lifecycleState = session._parkedAt ? 'parked' : 'deferred';
|
|
56036
56454
|
_scheduleStandupMaterializedRefresh(`lifecycle:deferred:${id.slice(0, 8)}`, 0, { markDirty: true });
|
|
56037
56455
|
_clearLifecyclePrewarm(session);
|
|
56038
|
-
|
|
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);
|
|
56039
56460
|
console.log(`[lifecycle] prewarm-skip id=${id.slice(0, 8)} reason=gate-busy`);
|
|
56040
56461
|
_endInteractiveActivationIfNeeded();
|
|
56041
56462
|
return false;
|
|
@@ -56133,6 +56554,16 @@ function _setDeferredRestoreStarting(session, value, reason = '') {
|
|
|
56133
56554
|
const next = value === true;
|
|
56134
56555
|
if (session._restoreStarting === next) return false;
|
|
56135
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
|
+
}
|
|
56136
56567
|
_markHeartbeatDirty(session);
|
|
56137
56568
|
return true;
|
|
56138
56569
|
}
|
|
@@ -56144,7 +56575,12 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56144
56575
|
return false;
|
|
56145
56576
|
}
|
|
56146
56577
|
let task = session._restoreTask || {};
|
|
56147
|
-
|
|
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
|
+
}
|
|
56148
56584
|
// WI-2/WI-6: quarantine short-circuit. A session already marked resume_blocked (deterministic
|
|
56149
56585
|
// prerequisite failure from a prior attempt — missing transcript/rollout, agent CLI
|
|
56150
56586
|
// unresolvable) must never be re-driven by ANY of the auto-retry triggers that funnel through
|
|
@@ -56156,7 +56592,10 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56156
56592
|
// id stays quarantined until the deterministic cause changes or CTM restarts.
|
|
56157
56593
|
const _blockedInfo = getSessionResumeBlocked(task.session_id);
|
|
56158
56594
|
if (_blockedInfo) {
|
|
56159
|
-
|
|
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);
|
|
56160
56599
|
return false;
|
|
56161
56600
|
}
|
|
56162
56601
|
if (task._codexLinkDeferred) {
|
|
@@ -56165,7 +56604,11 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56165
56604
|
// operation instead of falling back to cwd/creation-time discovery.
|
|
56166
56605
|
const codexLink = await _ensureCodexThreadLinkOffThread(task.session_id, 'deferred-restore-codex',
|
|
56167
56606
|
{ selectedIdentity: task._codexRestoreTarget || null });
|
|
56168
|
-
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
|
+
}
|
|
56169
56612
|
if (codexLink?.status === 'unavailable') {
|
|
56170
56613
|
recordSessionDiagnostic(task.session_id, 'codex-link-resolution-deferred', {
|
|
56171
56614
|
source: String(opts.source || ''),
|
|
@@ -56177,6 +56620,8 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56177
56620
|
detail: codexLink.reason || 'Codex thread resolution is temporarily unavailable.',
|
|
56178
56621
|
retryable: true,
|
|
56179
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);
|
|
56180
56625
|
return false;
|
|
56181
56626
|
}
|
|
56182
56627
|
if (codexLink?.blocked) {
|
|
@@ -56184,13 +56629,15 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56184
56629
|
code: 'codex_thread_owned_by_other_ctm',
|
|
56185
56630
|
message: 'The saved Codex thread is already attached to another CTM session.',
|
|
56186
56631
|
agentType: 'codex',
|
|
56187
|
-
});
|
|
56632
|
+
}, 'codex-thread-owned');
|
|
56188
56633
|
_recordRuntimeSessionLifecycle(session, 'degraded', {
|
|
56189
56634
|
adapter: 'codex',
|
|
56190
56635
|
reason: 'codex_thread_owned_by_other_ctm',
|
|
56191
56636
|
detail: 'The saved Codex thread is already attached to another CTM session.',
|
|
56192
56637
|
retryable: false,
|
|
56193
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);
|
|
56194
56641
|
return false;
|
|
56195
56642
|
}
|
|
56196
56643
|
if (codexLink?.agentSessionId) {
|
|
@@ -56226,7 +56673,10 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56226
56673
|
sessionLaunchOverrides: session._launchEnvOverrides, options: opts,
|
|
56227
56674
|
});
|
|
56228
56675
|
if (restoreLaunchProjection.sourceRecoveryEnvironmentInvalid) {
|
|
56229
|
-
_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);
|
|
56230
56680
|
recordSessionDiagnostic(task.session_id, 'source-recovery-environment-rejected', {
|
|
56231
56681
|
reason: 'captured_source_environment_owner_mismatch',
|
|
56232
56682
|
});
|
|
@@ -56276,7 +56726,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56276
56726
|
let launchGeneration = suppliedPlan.generation;
|
|
56277
56727
|
if (launchGeneration <= 0) {
|
|
56278
56728
|
const reserved = _codingSessionGenerationRuntime.revalidateReservation(await _codingSessionGenerationRuntime.reserveSessionGeneration(task.session_id, Math.max(inputGeneration - 1, Number.isSafeInteger(session._launchPlanGeneration) ? session._launchPlanGeneration : 0)));
|
|
56279
|
-
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; }
|
|
56280
56730
|
launchGeneration = reserved.generation;
|
|
56281
56731
|
}
|
|
56282
56732
|
session._launchPlanGeneration = launchGeneration;
|
|
@@ -56354,7 +56804,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56354
56804
|
: _hydrateCanonicalAgentEventCaches([task.session_id]),
|
|
56355
56805
|
]);
|
|
56356
56806
|
} catch (error) {
|
|
56357
|
-
_setDeferredRestoreStarting(session, false, '
|
|
56807
|
+
_setDeferredRestoreStarting(session, false, 'PrepareFailed');
|
|
56358
56808
|
// round-3 WP-3 T5 (S-24, I-8b): a read-pool ADMISSION failure (the lane was full/paused, or
|
|
56359
56809
|
// the pool itself is disabled) is a fact about the POOL at this instant, never about this
|
|
56360
56810
|
// session's data or its agent -- the prior code folded it into the same generic
|
|
@@ -56420,23 +56870,23 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56420
56870
|
return false;
|
|
56421
56871
|
}
|
|
56422
56872
|
if (launchPlanResult?.status === 'stale') {
|
|
56423
|
-
_setDeferredRestoreStarting(session, false, '
|
|
56873
|
+
_setDeferredRestoreStarting(session, false, 'LaunchPlanStale');
|
|
56424
56874
|
return false;
|
|
56425
56875
|
}
|
|
56426
56876
|
if (sessions.get(task.session_id) !== session || !session._restoreDeferred) {
|
|
56427
|
-
_setDeferredRestoreStarting(session, false, '
|
|
56877
|
+
_setDeferredRestoreStarting(session, false, 'SessionReplaced');
|
|
56428
56878
|
return false;
|
|
56429
56879
|
}
|
|
56430
56880
|
const rungHook = await _resolveMissingTranscriptRung(task.session_id, launchPlanResult.plan, detectAgentType(task.cmd));
|
|
56431
56881
|
if (rungHook.restored) {
|
|
56432
56882
|
// The archived copy is back on disk: prepare the launch again and continue natively.
|
|
56433
56883
|
try { launchPlanResult = await runDeferredLaunchPlan(launchPlanResult.plan); } catch (error) {
|
|
56434
|
-
_setDeferredRestoreStarting(session, false, '
|
|
56884
|
+
_setDeferredRestoreStarting(session, false, 'PrepareFailed');
|
|
56435
56885
|
errorSink.report('deferred-restore:replan-after-restore', error);
|
|
56436
56886
|
return false;
|
|
56437
56887
|
}
|
|
56438
56888
|
if (launchPlanResult?.status === 'stale') {
|
|
56439
|
-
_setDeferredRestoreStarting(session, false, '
|
|
56889
|
+
_setDeferredRestoreStarting(session, false, 'LaunchPlanStale');
|
|
56440
56890
|
return false;
|
|
56441
56891
|
}
|
|
56442
56892
|
}
|
|
@@ -56450,7 +56900,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56450
56900
|
rungHook.restored ? launchPlanResult.plan.resume?.preflight : (rungHook.preflight || launchPlanResult.plan.resume?.preflight),
|
|
56451
56901
|
);
|
|
56452
56902
|
const _planBlocked = getSessionResumeBlocked(task.session_id);
|
|
56453
|
-
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);
|
|
56454
56904
|
recordSessionDiagnostic(task.session_id, 'deferred-restore-blocked', {
|
|
56455
56905
|
code,
|
|
56456
56906
|
source: _restoreSource,
|
|
@@ -56461,7 +56911,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56461
56911
|
detail: launchPlanResult.plan.blockReason.message || 'Session launch is blocked.',
|
|
56462
56912
|
retryable: code === 'launch_rollout_index_pending' && launchPlanResult.plan.blockReason.deterministic !== true,
|
|
56463
56913
|
});
|
|
56464
|
-
_setDeferredRestoreStarting(session, false, '
|
|
56914
|
+
_setDeferredRestoreStarting(session, false, 'LaunchBlocked');
|
|
56465
56915
|
if (_shouldRefreshLaunchReadinessForBlock(code, launchPlanResult.plan.blockReason.deterministic === true)) {
|
|
56466
56916
|
_refreshLaunchReadiness().catch((e) => errorSink.report('launch-readiness:refresh', e));
|
|
56467
56917
|
}
|
|
@@ -56496,7 +56946,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56496
56946
|
transition_id: String(pendingTransition.transition_id || '').slice(0, 16),
|
|
56497
56947
|
reason: targetAuthorization.reason || 'host_target_receipt_mismatch',
|
|
56498
56948
|
});
|
|
56499
|
-
_setDeferredRestoreStarting(session, false, '
|
|
56949
|
+
_setDeferredRestoreStarting(session, false, 'LeadTransitionTargetOwnerMismatch');
|
|
56500
56950
|
return false;
|
|
56501
56951
|
}
|
|
56502
56952
|
}
|
|
@@ -56515,7 +56965,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56515
56965
|
transition_id: String(pendingTransition.transition_id || '').slice(0, 16),
|
|
56516
56966
|
reason: 'startup_task_not_exact_source',
|
|
56517
56967
|
});
|
|
56518
|
-
_setDeferredRestoreStarting(session, false, '
|
|
56968
|
+
_setDeferredRestoreStarting(session, false, 'LeadTransitionSourceOwnerMismatch');
|
|
56519
56969
|
return false;
|
|
56520
56970
|
}
|
|
56521
56971
|
const reconciled = await leadModelSwitch.reconcileUnownedTransition({ sessionId: task.session_id,
|
|
@@ -56525,7 +56975,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56525
56975
|
transition_id: String(pendingTransition.transition_id || '').slice(0, 16),
|
|
56526
56976
|
reason: reconciled.reason || 'transition_reconciliation_failed',
|
|
56527
56977
|
});
|
|
56528
|
-
_setDeferredRestoreStarting(session, false, '
|
|
56978
|
+
_setDeferredRestoreStarting(session, false, 'LeadTransitionReconciliationFailed');
|
|
56529
56979
|
return false;
|
|
56530
56980
|
}
|
|
56531
56981
|
}
|
|
@@ -56534,7 +56984,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56534
56984
|
recordSessionDiagnostic(task.session_id, 'lead-transition-source-restore-refused', {
|
|
56535
56985
|
reason: 'transition_reconciliation_unavailable', message: String(error?.message || error).slice(0, 160),
|
|
56536
56986
|
});
|
|
56537
|
-
_setDeferredRestoreStarting(session, false, '
|
|
56987
|
+
_setDeferredRestoreStarting(session, false, 'LeadTransitionReconciliationUnavailable');
|
|
56538
56988
|
return false;
|
|
56539
56989
|
}
|
|
56540
56990
|
if (opts.expectedProviderThreadId) {
|
|
@@ -56547,7 +56997,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56547
56997
|
expectedThreadPrefix: String(opts.expectedProviderThreadId).slice(0, 8),
|
|
56548
56998
|
actualThreadPrefix: String(resolvedThreadId).slice(0, 8),
|
|
56549
56999
|
});
|
|
56550
|
-
_setDeferredRestoreStarting(session, false, '
|
|
57000
|
+
_setDeferredRestoreStarting(session, false, 'LeadSourceRecoveryResumeMismatch');
|
|
56551
57001
|
return false;
|
|
56552
57002
|
}
|
|
56553
57003
|
}
|
|
@@ -56567,7 +57017,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56567
57017
|
throw new Error(`backup restore not possible: ${decision?.reason || 'unavailable'}`);
|
|
56568
57018
|
}
|
|
56569
57019
|
} catch (error) {
|
|
56570
|
-
_setDeferredRestoreStarting(session, false, '
|
|
57020
|
+
_setDeferredRestoreStarting(session, false, 'TranscriptRestoreFailed');
|
|
56571
57021
|
_launchPlanCoordinator.markFailed(task.session_id, launchGeneration, 'launch_transcript_restore_failed', launchPlanResult.plan);
|
|
56572
57022
|
recordSessionDiagnostic(task.session_id, 'deferred-restore-transcript-copy-failed', { error: error.message });
|
|
56573
57023
|
_recordRuntimeSessionLifecycle(session, 'degraded', {
|
|
@@ -56591,7 +57041,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56591
57041
|
}),
|
|
56592
57042
|
});
|
|
56593
57043
|
if (sessions.get(task.session_id) !== session || !session._restoreDeferred) {
|
|
56594
|
-
_setDeferredRestoreStarting(session, false, '
|
|
57044
|
+
_setDeferredRestoreStarting(session, false, 'McpWaitStale');
|
|
56595
57045
|
return false;
|
|
56596
57046
|
}
|
|
56597
57047
|
// WI-8: gate a large-transcript resume BEFORE committing to spawn. `source` here is
|
|
@@ -56634,7 +57084,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56634
57084
|
// immediately — nothing downstream will ever reach _clearResumeLoading for it.
|
|
56635
57085
|
if (_resumeStormAdmission.gated) _resumeStormBudget.release(_resumeStormAdmission.ticket);
|
|
56636
57086
|
session._resumeStormBudgetTicket = null;
|
|
56637
|
-
_setDeferredRestoreStarting(session, false, '
|
|
57087
|
+
_setDeferredRestoreStarting(session, false, 'ResumeStormStale');
|
|
56638
57088
|
return false;
|
|
56639
57089
|
}
|
|
56640
57090
|
session._resumeStormBudgetHeld = _resumeStormAdmission.gated;
|
|
@@ -56651,7 +57101,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56651
57101
|
const geometryLeaseBeforeRestore = _geometryLease(session, task.session_id);
|
|
56652
57102
|
if (!_launchPlanCoordinator.markSpawning(task.session_id, launchGeneration, launchPlanResult.plan)) {
|
|
56653
57103
|
_releaseResumeStormBudgetIfHeld(session);
|
|
56654
|
-
_setDeferredRestoreStarting(session, false, '
|
|
57104
|
+
_setDeferredRestoreStarting(session, false, 'LaunchPlanNotSpawning');
|
|
56655
57105
|
return false;
|
|
56656
57106
|
}
|
|
56657
57107
|
// D-8 (WP-3): capture the placeholder's cached-snapshot fields BEFORE _destroySession
|
|
@@ -56674,7 +57124,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56674
57124
|
});
|
|
56675
57125
|
if (!issued) {
|
|
56676
57126
|
_releaseResumeStormBudgetIfHeld(session);
|
|
56677
|
-
_setDeferredRestoreStarting(session, false, '
|
|
57127
|
+
_setDeferredRestoreStarting(session, false, 'LeadTransitionAttemptIssueRefused');
|
|
56678
57128
|
recordSessionDiagnostic(task.session_id, 'lead-transition-attempt-issue-refused', {
|
|
56679
57129
|
generation: launchGeneration,
|
|
56680
57130
|
});
|
|
@@ -56848,7 +57298,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56848
57298
|
// finds this session not-live) instead of waiting out its deadline for a wake-ready that
|
|
56849
57299
|
// will never come, and push visibility unconditionally.
|
|
56850
57300
|
_announceRestoredSessionLive(session, 'create-rollback');
|
|
56851
|
-
_setDeferredRestoreStarting(session, false, '
|
|
57301
|
+
_setDeferredRestoreStarting(session, false, 'CreateRollback');
|
|
56852
57302
|
recordSessionDiagnostic(task.session_id, 'deferred-restore-create-rollback', {
|
|
56853
57303
|
source: _restoreSource,
|
|
56854
57304
|
code: createError
|
|
@@ -56878,7 +57328,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
|
|
|
56878
57328
|
// a missing file will not appear on the next tick, so there is nothing to wait out (and
|
|
56879
57329
|
// every future call into this function short-circuits at the top before reaching here).
|
|
56880
57330
|
if (_newlyBlocked) {
|
|
56881
|
-
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);
|
|
56882
57332
|
} else {
|
|
56883
57333
|
for (const client of existingClients) {
|
|
56884
57334
|
if (client && client.readyState === 1) {
|
|
@@ -57060,6 +57510,8 @@ function _autoResumeDeferredSessions(source = 'auto-resume') {
|
|
|
57060
57510
|
idleHours: diet.idleHours,
|
|
57061
57511
|
bytes: diet.bytes,
|
|
57062
57512
|
});
|
|
57513
|
+
// WP-3 (§9 S-19): beside the existing diag; the latch keeps a long diet to one skip.
|
|
57514
|
+
_noteWakeSkip(s, 'DrainPressureSkipped');
|
|
57063
57515
|
}
|
|
57064
57516
|
continue;
|
|
57065
57517
|
}
|
|
@@ -57107,9 +57559,11 @@ function _autoResumeDeferredSessions(source = 'auto-resume') {
|
|
|
57107
57559
|
const resumeIds = warmDrain.selected;
|
|
57108
57560
|
// S-18/skipReason: a session skipped this pass stays a lazy placeholder as before, but
|
|
57109
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.
|
|
57110
57563
|
for (const skip of warmDrain.skipped) {
|
|
57111
57564
|
const s = sessions.get(skip.id);
|
|
57112
57565
|
if (s) s._resumeDrainSkipReason = skip.reason;
|
|
57566
|
+
_noteWakeSkip(s, 'DrainPressureSkipped');
|
|
57113
57567
|
}
|
|
57114
57568
|
for (const id of resumeIds) {
|
|
57115
57569
|
const s = sessions.get(id);
|
|
@@ -57206,6 +57660,7 @@ function _autoResumeDeferredSessions(source = 'auto-resume') {
|
|
|
57206
57660
|
coalesceBroadcast: true, // batch emits a single list refresh per tick
|
|
57207
57661
|
}),
|
|
57208
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
|
|
57209
57664
|
broadcast: () => broadcastSessionList(true),
|
|
57210
57665
|
scheduleGap: _scheduleResumeGap,
|
|
57211
57666
|
// WI-7(d): yield the whole drain to an in-flight interactive activation.
|
|
@@ -57289,13 +57744,18 @@ const _reviveUnresponsiveHost = createReviveUnresponsiveHost({
|
|
|
57289
57744
|
recordSessionDiagnostic,
|
|
57290
57745
|
log: (msg) => console.warn(msg),
|
|
57291
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 })); }
|
|
57292
57752
|
|
|
57293
57753
|
// Reconcile previous-boot hosts before classifying startup tasks. Incompatible
|
|
57294
57754
|
// hosts are quiesced, then restored through the ordinary provider-resume path.
|
|
57295
57755
|
const RECONCILE_PROBE_CONCURRENCY = 4;
|
|
57296
57756
|
async function _reconcileSurvivingHostsForRestore(startupTasks) {
|
|
57297
57757
|
const aliveHostIndex = new Map();
|
|
57298
|
-
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
|
|
57299
57759
|
const dataDir = require('./lib/data-dir').getDataDir();
|
|
57300
57760
|
// WP-4 (D-9 crash cap): >= 3 unexpected host deaths inside the trailing 10-
|
|
57301
57761
|
// minute window means the topology itself is unhealthy right now -- skip
|
|
@@ -57313,7 +57773,7 @@ async function _reconcileSurvivingHostsForRestore(startupTasks) {
|
|
|
57313
57773
|
await _restartOutcomeTruth.killHostsForCrashCap({ records: await _listHostRecords(dataDir), rollup: _hostAdoptionRollup, isAlive: _isProcessAlive, shutdownHost: _shutdownOrphanHost,
|
|
57314
57774
|
removeRecord: (r) => _removeHostRecordFile(dataDir, r.hostId), skip: (r) => r.serverBootId === _serverBootId || sessionHostManager.ownsHostId(r.hostId), errorSink });
|
|
57315
57775
|
} catch (e) { errorSink.report('boot-reconcile:crash-cap-kill', e); }
|
|
57316
|
-
_restartOutcomeTruth.recordCrashCapRows({ recordSessionDiagnostic, startupTasks, deaths }); return { aliveHostIndex, hostRecordsById };
|
|
57776
|
+
_restartOutcomeTruth.recordCrashCapRows({ recordSessionDiagnostic, startupTasks, deaths }); return { aliveHostIndex, hostRecordsById, threadpoolByHostId };
|
|
57317
57777
|
}
|
|
57318
57778
|
} catch (e) {
|
|
57319
57779
|
errorSink.report('boot-reconcile:read-host-crashes', e);
|
|
@@ -57323,9 +57783,9 @@ async function _reconcileSurvivingHostsForRestore(startupTasks) {
|
|
|
57323
57783
|
records = await _listHostRecords(dataDir);
|
|
57324
57784
|
} catch (e) {
|
|
57325
57785
|
errorSink.report('boot-reconcile:list-host-records', e);
|
|
57326
|
-
return { aliveHostIndex, hostRecordsById };
|
|
57786
|
+
return { aliveHostIndex, hostRecordsById, threadpoolByHostId };
|
|
57327
57787
|
}
|
|
57328
|
-
if (!records.length) return { aliveHostIndex, hostRecordsById };
|
|
57788
|
+
if (!records.length) return { aliveHostIndex, hostRecordsById, threadpoolByHostId };
|
|
57329
57789
|
const startupSessionIds = new Set(startupTasks.map((t) => t.session_id));
|
|
57330
57790
|
const aliveHosts = []; // { record, matching } per probed-alive host; indexed AFTER all probes, hostId order
|
|
57331
57791
|
let cursor = 0;
|
|
@@ -57387,7 +57847,7 @@ async function _reconcileSurvivingHostsForRestore(startupTasks) {
|
|
|
57387
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`);
|
|
57388
57848
|
for (const sessionId of matching) recordSessionDiagnostic(sessionId, 'host-upgrade-pending', { hostId: record.hostId, upgrade: probed.upgrade });
|
|
57389
57849
|
}
|
|
57390
|
-
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
|
|
57391
57851
|
aliveHosts.push({ record, matching });
|
|
57392
57852
|
}
|
|
57393
57853
|
}
|
|
@@ -57404,7 +57864,7 @@ async function _reconcileSurvivingHostsForRestore(startupTasks) {
|
|
|
57404
57864
|
if (aliveHostIndex.size) {
|
|
57405
57865
|
console.log(` [boot-reconcile] ${aliveHostIndex.size} session(s) have a surviving host — will adopt instead of respawning`);
|
|
57406
57866
|
}
|
|
57407
|
-
return { aliveHostIndex, hostRecordsById };
|
|
57867
|
+
return { aliveHostIndex, hostRecordsById, threadpoolByHostId };
|
|
57408
57868
|
}
|
|
57409
57869
|
|
|
57410
57870
|
// D-2: one-line stamp for restore/adopt/placeholder sites (idempotent; no row = untouched).
|
|
@@ -57458,6 +57918,54 @@ function _reloadWorkerSessionMarkersAtBoot() {
|
|
|
57458
57918
|
if (rearmed > 0) console.log(` [restore] re-armed ${rearmed} worker retirement timer(s) from worker_sessions.hide_at.`);
|
|
57459
57919
|
}
|
|
57460
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
|
+
|
|
57461
57969
|
async function _restoreSessionsAsync() {
|
|
57462
57970
|
await _ownerRecovery.beforeRestore();
|
|
57463
57971
|
_hostAdoptionRollup.markRestoreStarted(); _restoreAdmission.markTierAActive('restore-start');
|
|
@@ -57476,11 +57984,11 @@ async function _restoreSessionsAsync() {
|
|
|
57476
57984
|
);
|
|
57477
57985
|
_listStartupTasksMs = Date.now() - _listStartupTasksStartedAt;
|
|
57478
57986
|
_restoreWindowVerdict.fillRestorePendingIds(_startupTasks.map((t) => t.session_id));
|
|
57479
|
-
let _restoreAliveHostIndex = new Map(), _restoreHostRecordsById = new Map();
|
|
57987
|
+
let _restoreAliveHostIndex = new Map(), _restoreHostRecordsById = new Map(), _restoreThreadpoolByHostId = new Map(); // WP-3 (§9 S-8)
|
|
57480
57988
|
const _earlyAdopted = new Set(); const _earlyAdoptStartedAt = Date.now();
|
|
57481
57989
|
if (_startupTasks.length) {
|
|
57482
57990
|
try {
|
|
57483
|
-
({ aliveHostIndex: _restoreAliveHostIndex, hostRecordsById: _restoreHostRecordsById } = await _reconcileSurvivingHostsForRestore(_startupTasks));
|
|
57991
|
+
({ aliveHostIndex: _restoreAliveHostIndex, hostRecordsById: _restoreHostRecordsById, threadpoolByHostId: _restoreThreadpoolByHostId } = await _reconcileSurvivingHostsForRestore(_startupTasks));
|
|
57484
57992
|
} catch (e) { errorSink.report('boot-reconcile:reconcile', e); }
|
|
57485
57993
|
await Promise.all(_startupTasks.filter((t) => _restoreAliveHostIndex.has(t.session_id)).map((t) => _adoptStartupTaskSession(t, _restoreAliveHostIndex.get(t.session_id).record, _restoreAliveHostIndex)
|
|
57486
57994
|
.then((adopted) => { if (adopted) _earlyAdopted.add(t.session_id); }, (e) => errorSink.report('boot-adopt:restore-early', e, { sessionId: t.session_id }))));
|
|
@@ -57636,6 +58144,11 @@ async function _restoreSessionsAsync() {
|
|
|
57636
58144
|
try { await dbModule.clearScrollbackIdentities(); } catch {}
|
|
57637
58145
|
_resumeShadowAfterBoot('no-sessions'); // nothing to restore — resume mirroring now
|
|
57638
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 }));
|
|
57639
58152
|
broadcastToAll({ type: 'server-ready' });
|
|
57640
58153
|
return;
|
|
57641
58154
|
}
|
|
@@ -57687,6 +58200,16 @@ async function _restoreSessionsAsync() {
|
|
|
57687
58200
|
// auto-resume below runs shadowed; only the restore loop itself was the
|
|
57688
58201
|
// suppressed window.)
|
|
57689
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();
|
|
57690
58213
|
// WP-3 (design doc §5 D-4, §8 WP-3, §9 S-18): force one real sampler snapshot
|
|
57691
58214
|
// before draining so the warm-drain budget's pressure/free/inactive reflect the
|
|
57692
58215
|
// ACTUAL host state at the exact moment deferred sessions would auto-resume,
|
|
@@ -57696,7 +58219,7 @@ async function _restoreSessionsAsync() {
|
|
|
57696
58219
|
try { await _sampleLifecycleResources({ force: true }); }
|
|
57697
58220
|
catch (error) { errorSink.report('restore:warm-drain-pre-sample', error); }
|
|
57698
58221
|
// Bring deferred placeholders back to life by themselves (staggered).
|
|
57699
|
-
_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
|
|
57700
58223
|
// Clean orphan scrollback identities — entries for sessions no longer in
|
|
57701
58224
|
// startup_tasks (e.g. a hard crash where pty.onExit cleanup didn't run).
|
|
57702
58225
|
// (scrollback_log was dropped in schema v10; snapshot files are reaped by the
|
|
@@ -58047,7 +58570,7 @@ async function _restoreSessionsAsync() {
|
|
|
58047
58570
|
// broadcastToAll, isCtmInternalSession, callClaudeAsync, _detachOwnerMutation,
|
|
58048
58571
|
// _inRestoreStorm, _offthreadRead) is already initialized. See server/boot.js's own
|
|
58049
58572
|
// header comment for the TDZ-safety argument.
|
|
58050
|
-
const { createBootJobs, readPackageVersion } = require('./server/boot');
|
|
58573
|
+
const { createBootJobs, readPackageVersion, isHumanPromptCandidate, cleanFirstMessage } = require('./server/boot');
|
|
58051
58574
|
const _bootJobs = createBootJobs({
|
|
58052
58575
|
installRoot: path.join(__dirname, '..'),
|
|
58053
58576
|
telemetry,
|
|
@@ -61525,6 +62048,129 @@ servicesApi.configure({
|
|
|
61525
62048
|
});
|
|
61526
62049
|
servicesApi.register();
|
|
61527
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
|
+
|
|
61528
62174
|
// WP-8 slice 3: setup/diagnostics/misc-ops route clusters.
|
|
61529
62175
|
const setupApi = require('./lib/api/setup');
|
|
61530
62176
|
setupApi.configure({
|
|
@@ -61619,6 +62265,7 @@ miscopsApi.configure({
|
|
|
61619
62265
|
apiListRules,
|
|
61620
62266
|
apiDeleteHook,
|
|
61621
62267
|
apiRestartCtm, restartGuardSnapshot: _restartGuardSnapshot,
|
|
62268
|
+
proxyPreflight: () => oauthProxySupervisor.proxyPreflight(), // WP-3 D-10: the live model-proxy verdict
|
|
61622
62269
|
wss,
|
|
61623
62270
|
clientBuildDiagnosticDetails,
|
|
61624
62271
|
attachedSessionIds,
|
|
@@ -61687,6 +62334,7 @@ ingressApi.configure({
|
|
|
61687
62334
|
observeWorkspaceActivity: _observeWorkspaceActivity,
|
|
61688
62335
|
dbModule,
|
|
61689
62336
|
delegateWorker,
|
|
62337
|
+
recordSessionDiagnostic,
|
|
61690
62338
|
});
|
|
61691
62339
|
ingressApi.register();
|
|
61692
62340
|
const { createMicrosoftOwnerServer } = require('./lib/microsoft-owner-server');
|
|
@@ -62009,10 +62657,10 @@ const rootPromptFlow = createRootPromptFlow({
|
|
|
62009
62657
|
}, Date.now(),
|
|
62010
62658
|
),
|
|
62011
62659
|
sendWsJson: (ws, frame) => _sendWsJsonBounded(ws, frame),
|
|
62012
|
-
reportDiagnostic: ({ phase, code, returnedRecordConfirmed, canonicalRecordConfirmed,
|
|
62660
|
+
reportDiagnostic: ({ phase, code, sessionId, clientMessageId, returnedRecordConfirmed, canonicalRecordConfirmed,
|
|
62013
62661
|
recordFieldsMatch, errorKind, sourceFile, sourceLine, nativeHandleOpen, nativeHandleClosed, failureClass }) =>
|
|
62014
62662
|
recordSessionDiagnostic('__host__', 'root-prompt-flow-suppressed-failure', {
|
|
62015
|
-
phase, code, returnedRecordConfirmed, canonicalRecordConfirmed, recordFieldsMatch,
|
|
62663
|
+
phase, code, sessionId, clientMessageId, returnedRecordConfirmed, canonicalRecordConfirmed, recordFieldsMatch,
|
|
62016
62664
|
errorKind, sourceFile, sourceLine, nativeHandleOpen, nativeHandleClosed, failureClass,
|
|
62017
62665
|
}),
|
|
62018
62666
|
markDirty: (sessionId, reason) => {
|
|
@@ -62578,7 +63226,7 @@ function _runBootSequence() {
|
|
|
62578
63226
|
// configDir/instanceTag/port are passed explicitly so a dev/test instance
|
|
62579
63227
|
// (its own CONFIG_DIR, per lib/server-config.js) never lands its journal
|
|
62580
63228
|
// under the real ~/.walle — see lib/ctm-boot-lifecycle.js.
|
|
62581
|
-
appendCtmBootLine({ configDir: CONFIG_DIR, instanceTag: INSTANCE_TAG, port: PORT });
|
|
63229
|
+
appendCtmBootLine({ configDir: CONFIG_DIR, instanceTag: INSTANCE_TAG, port: PORT, enteredAtServer: require.main === module });
|
|
62582
63230
|
|
|
62583
63231
|
// Stale CTM cleanup: with launchd KeepAlive, only one instance runs.
|
|
62584
63232
|
// No pgrep needed — launchd manages the lifecycle.
|
|
@@ -62603,8 +63251,13 @@ function _runBootSequence() {
|
|
|
62603
63251
|
// Wall-E daemon and surfaced through its HTTP API after startup.
|
|
62604
63252
|
const shouldStartOauthProxy = (() => {
|
|
62605
63253
|
if (isWalleSupervisorDisabled()) return false;
|
|
62606
|
-
// Sync boot path: Wall-E's stored provider is not readable here; the auth method
|
|
62607
|
-
|
|
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();
|
|
62608
63261
|
})();
|
|
62609
63262
|
if (shouldStartOauthProxy) {
|
|
62610
63263
|
try {
|
|
@@ -63244,6 +63897,16 @@ function _runBootSequence() {
|
|
|
63244
63897
|
// coalescing keeps at most one queued behind the in-flight one).
|
|
63245
63898
|
_transcriptDeltaCoalescer.request(filePath, payload);
|
|
63246
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
|
+
}
|
|
63247
63910
|
} catch (e) {
|
|
63248
63911
|
fallback('pipeline-error');
|
|
63249
63912
|
console.error(`[event-delta] ${String(sessionId).slice(0, 8)} pipeline error:`, e.message);
|