create-walle 0.10.0 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -41
- package/lib/owner-write-safety.js +135 -0
- package/lib/public-runtime.js +13 -5
- package/lib/release-schema.json +3 -2
- package/lib/runtime-closure-policy.json +7 -5
- package/package.json +10 -4
- package/template/bin/deps-doctor.js +1 -1
- package/template/bin/node-bin.sh +11 -0
- package/template/coding-task-manager/api-prompts.js +10 -22
- package/template/coding-task-manager/bin/capture-codex-rollout-records.js +417 -0
- package/template/coding-task-manager/bin/census-codex-lead-history.js +325 -0
- package/template/coding-task-manager/bin/claude-hook.js +7 -3
- package/template/coding-task-manager/bin/claude-table-parity.js +307 -0
- package/template/coding-task-manager/bin/conversation-storage-auto-migration-lib.js +16 -1
- package/template/coding-task-manager/bin/conversation-storage-generation-evidence.js +9 -2
- package/template/coding-task-manager/bin/conversation-storage-service-entry-lib.js +16 -1
- package/template/coding-task-manager/bin/conversation-storage-walle-owner.js +247 -0
- package/template/coding-task-manager/bin/ctm-machine-pair.js +160 -0
- package/template/coding-task-manager/bin/lead-wait-census.js +1348 -0
- package/template/coding-task-manager/bin/repair-foreign-clone.js +564 -0
- package/template/coding-task-manager/bin/repair-resume-echo-titles.js +248 -0
- package/template/coding-task-manager/bin/seam-replay-codex.js +301 -0
- package/template/coding-task-manager/bin/write-lock-bench.js +111 -0
- package/template/coding-task-manager/db.js +705 -286
- package/template/coding-task-manager/git-utils.js +44 -0
- package/template/coding-task-manager/lib/adopted-host-redial.js +4 -0
- package/template/coding-task-manager/lib/agent-presets.js +15 -0
- package/template/coding-task-manager/lib/agent-title-apply.js +88 -0
- package/template/coding-task-manager/lib/agent-title-backfill-read.js +92 -0
- package/template/coding-task-manager/lib/api/host-stats.js +98 -0
- package/template/coding-task-manager/lib/api/ingress.js +35 -12
- package/template/coding-task-manager/lib/api/machine-pairing.js +285 -0
- package/template/coding-task-manager/lib/api/machine-pool.js +307 -0
- package/template/coding-task-manager/lib/api/machines.js +461 -0
- package/template/coding-task-manager/lib/api/misc-ops.js +20 -5
- package/template/coding-task-manager/lib/api/providers.js +53 -6
- package/template/coding-task-manager/lib/api/resume-from-store.js +25 -0
- package/template/coding-task-manager/lib/api/sessions.js +160 -8
- package/template/coding-task-manager/lib/api/transcript-history.js +17 -4
- package/template/coding-task-manager/lib/api/worktrees.js +38 -37
- package/template/coding-task-manager/lib/approval-drift.js +16 -1
- package/template/coding-task-manager/lib/approval-logical-frame.js +5 -1
- package/template/coding-task-manager/lib/approval-scope.js +68 -4
- package/template/coding-task-manager/lib/auth-context.js +32 -2
- package/template/coding-task-manager/lib/auth-rate-limit.js +41 -1
- package/template/coding-task-manager/lib/auth-rules.js +186 -45
- package/template/coding-task-manager/lib/broadcast-serialize.js +2 -1
- package/template/coding-task-manager/lib/browser-session-broker.js +9 -1
- package/template/coding-task-manager/lib/browser-use-adapter.js +33 -2
- package/template/coding-task-manager/lib/claude-markdown-ansi.js +27 -85
- package/template/coding-task-manager/lib/claude-session-title.js +6 -30
- package/template/coding-task-manager/lib/claude-table-layout.js +322 -0
- package/template/coding-task-manager/lib/client-diag-handler.js +25 -0
- package/template/coding-task-manager/lib/codex-config-guard.js +33 -3
- package/template/coding-task-manager/lib/codex-harness-qualification.js +12 -0
- package/template/coding-task-manager/lib/codex-history-terminal-renderer.js +33 -33
- package/template/coding-task-manager/lib/codex-launch-health.js +29 -2
- package/template/coding-task-manager/lib/codex-link-preparation.js +9 -8
- package/template/coding-task-manager/lib/codex-local-links.js +267 -0
- package/template/coding-task-manager/lib/codex-markdown-lists.js +160 -0
- package/template/coding-task-manager/lib/codex-native-history-prefix.js +4 -0
- package/template/coding-task-manager/lib/codex-paths.js +2 -1
- package/template/coding-task-manager/lib/codex-source-handoff.js +186 -0
- package/template/coding-task-manager/lib/coding-agent-models.js +21 -30
- package/template/coding-task-manager/lib/coding-session-model-adapters.js +27 -2
- package/template/coding-task-manager/lib/coding-session-model-projection.js +5 -0
- package/template/coding-task-manager/lib/coding-session-model-runtime.js +1 -2
- package/template/coding-task-manager/lib/coding-session-model-state.js +10 -0
- package/template/coding-task-manager/lib/coding-session-model-sync-state.js +0 -1
- package/template/coding-task-manager/lib/conversation-import/cursor-guard-owner.js +10 -0
- package/template/coding-task-manager/lib/create/resolve-launch-params.js +8 -1
- package/template/coding-task-manager/lib/ctm-boot-lifecycle.js +17 -1
- package/template/coding-task-manager/lib/db-domain-tables.js +7 -0
- package/template/coding-task-manager/lib/db-domains.js +21 -2
- package/template/coding-task-manager/lib/db-owner-op-registry.js +7 -0
- package/template/coding-task-manager/lib/db-owner-worker-client.js +22 -0
- package/template/coding-task-manager/lib/db-schema-source-files.js +1 -0
- package/template/coding-task-manager/lib/feature-manifest.js +1 -3
- package/template/coding-task-manager/lib/fs-watcher.js +3 -2
- package/template/coding-task-manager/lib/harness-history-transition.js +41 -14
- package/template/coding-task-manager/lib/headless-inflight-ops.js +203 -0
- package/template/coding-task-manager/lib/headless-missing-reply.js +42 -0
- package/template/coding-task-manager/lib/headless-term-service.js +60 -23
- package/template/coding-task-manager/lib/history-transition-read-op.js +179 -23
- package/template/coding-task-manager/lib/host-control-dial.js +167 -0
- package/template/coding-task-manager/lib/host-handoff-orchestrator.js +16 -2
- package/template/coding-task-manager/lib/host-handoff.js +51 -13
- package/template/coding-task-manager/lib/host-health-ledger.js +164 -12
- package/template/coding-task-manager/lib/host-self-truth-watchdog.js +4 -0
- package/template/coding-task-manager/lib/host-stats.js +328 -0
- package/template/coding-task-manager/lib/host-threadpool-evacuation.js +160 -0
- package/template/coding-task-manager/lib/host-threadpool-monitor.js +14 -1
- package/template/coding-task-manager/lib/hosted-input-dispatch.js +203 -0
- package/template/coding-task-manager/lib/hosted-sync-worker.js +113 -5
- package/template/coding-task-manager/lib/instance-id-ownership-manifest.js +4 -0
- package/template/coding-task-manager/lib/instance-manifest-runtime.js +3 -0
- package/template/coding-task-manager/lib/instance-manifest.js +13 -1
- package/template/coding-task-manager/lib/instance-sync-status.js +15 -0
- package/template/coding-task-manager/lib/kernel/diag-log.js +12 -0
- package/template/coding-task-manager/lib/launch-model-override.js +160 -1
- package/template/coding-task-manager/lib/launch-plan-worker.js +59 -2
- package/template/coding-task-manager/lib/lead-model-observer.js +17 -2
- package/template/coding-task-manager/lib/lead-model-switch-runtime.js +57 -4
- package/template/coding-task-manager/lib/lead-model-switch.js +11 -2
- package/template/coding-task-manager/lib/lead-model-transition-runtime.js +42 -4
- package/template/coding-task-manager/lib/lead-model-transition-store.js +61 -3
- package/template/coding-task-manager/lib/legacy-draft-cancellation-recovery.js +111 -0
- package/template/coding-task-manager/lib/login-tripwire.js +18 -0
- package/template/coding-task-manager/lib/machine-pairing.js +238 -0
- package/template/coding-task-manager/lib/machine-pool.js +409 -0
- package/template/coding-task-manager/lib/machine-registry.js +749 -0
- package/template/coding-task-manager/lib/machine-self.js +66 -0
- package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +56 -4
- package/template/coding-task-manager/lib/media-journal-control-reader.js +473 -0
- package/template/coding-task-manager/lib/media-journal.js +84 -27
- package/template/coding-task-manager/lib/media-store-options.js +43 -15
- package/template/coding-task-manager/lib/migrations/v56-host-samples-and-machine-pool.js +76 -0
- package/template/coding-task-manager/lib/mirror-feed-sanitize.js +12 -3
- package/template/coding-task-manager/lib/model-execution-recipe-metadata.js +16 -3
- package/template/coding-task-manager/lib/model-proxy/anthropic-face.js +4 -0
- package/template/coding-task-manager/lib/model-proxy/chat-response-translators.js +3 -0
- package/template/coding-task-manager/lib/model-proxy/collaboration-codec.js +94 -0
- package/template/coding-task-manager/lib/model-proxy/handoff.js +494 -0
- package/template/coding-task-manager/lib/model-proxy/lead-worker-picks.js +25 -0
- package/template/coding-task-manager/lib/model-proxy/native-lead-listener.js +61 -7
- package/template/coding-task-manager/lib/model-proxy/proxy-cli.js +200 -0
- package/template/coding-task-manager/lib/model-proxy/proxy-instance-record.js +257 -0
- package/template/coding-task-manager/lib/model-proxy/responses-face.js +24 -2
- package/template/coding-task-manager/lib/model-proxy/stream-accounting.js +205 -0
- package/template/coding-task-manager/lib/model-proxy/tokens.js +78 -0
- package/template/coding-task-manager/lib/model-proxy/upstream-outcome.js +9 -0
- package/template/coding-task-manager/lib/native-history-control.js +62 -14
- package/template/coding-task-manager/lib/native-lead-settings-client.js +2 -1
- package/template/coding-task-manager/lib/native-lead-tls.js +18 -4
- package/template/coding-task-manager/lib/oauth-proxy-supervisor.js +628 -64
- package/template/coding-task-manager/lib/owner-action-manifest-data.json +14 -0
- package/template/coding-task-manager/lib/painted-row-recorder.js +9 -1
- package/template/coding-task-manager/lib/prompt-admission-trace.js +69 -0
- package/template/coding-task-manager/lib/prompt-submission-contract.js +21 -1
- package/template/coding-task-manager/lib/provider-login-broker.js +192 -9
- package/template/coding-task-manager/lib/provider-login-capture.js +153 -0
- package/template/coding-task-manager/lib/queue-draft-status-overlay.js +36 -0
- package/template/coding-task-manager/lib/raw-codex-history-window.js +5 -3
- package/template/coding-task-manager/lib/raw-history-block.js +3 -1
- package/template/coding-task-manager/lib/read-pool-client.js +36 -24
- package/template/coding-task-manager/lib/read-pool-op-registry.js +10 -1
- package/template/coding-task-manager/lib/read-pool-telemetry.js +14 -5
- package/template/coding-task-manager/lib/recent-catalog-query.js +26 -0
- package/template/coding-task-manager/lib/recent-session-catalog-response.js +9 -0
- package/template/coding-task-manager/lib/remote-command-context.js +12 -0
- package/template/coding-task-manager/lib/remote-command-runtime.js +18 -5
- package/template/coding-task-manager/lib/remote-machine-catalog.js +1062 -0
- package/template/coding-task-manager/lib/remote-machine-create.js +693 -0
- package/template/coding-task-manager/lib/remote-machine-link.js +939 -0
- package/template/coding-task-manager/lib/remote-machine-proxy.js +520 -0
- package/template/coding-task-manager/lib/remote-session-forward.js +306 -0
- package/template/coding-task-manager/lib/remote-submission-status.js +6 -0
- package/template/coding-task-manager/lib/restart-outcome-truth.js +20 -3
- package/template/coding-task-manager/lib/restart-preflight.js +9 -2
- package/template/coding-task-manager/lib/restore-backoff-gate.js +10 -2
- package/template/coding-task-manager/lib/restore-prepare-retry.js +12 -5
- package/template/coding-task-manager/lib/restore-resume-batch.js +22 -1
- package/template/coding-task-manager/lib/resume-blocked-wire.js +62 -1
- package/template/coding-task-manager/lib/resume-cwd.js +13 -0
- package/template/coding-task-manager/lib/resume-from-store.js +12 -1
- package/template/coding-task-manager/lib/resume-origin-facts.js +38 -1
- package/template/coding-task-manager/lib/resume-preflight.js +21 -16
- package/template/coding-task-manager/lib/review-repo-admission.js +39 -6
- package/template/coding-task-manager/lib/review-repo-registry.js +17 -0
- package/template/coding-task-manager/lib/root-delivery-runtime.js +72 -15
- package/template/coding-task-manager/lib/root-delivery-selection.js +37 -1
- package/template/coding-task-manager/lib/root-prompt-admission.js +43 -11
- package/template/coding-task-manager/lib/root-prompt-flow.js +175 -38
- package/template/coding-task-manager/lib/scrollback-archive-store.js +7 -4
- package/template/coding-task-manager/lib/server-memory-sampler-runtime.js +10 -0
- package/template/coding-task-manager/lib/server-memory-sampler.js +41 -2
- package/template/coding-task-manager/lib/session-analysis-memory.js +2 -0
- package/template/coding-task-manager/lib/session-create/codex-native-provider-gate.js +218 -0
- package/template/coding-task-manager/lib/session-create/codex-resume-exit-classify.js +52 -0
- package/template/coding-task-manager/lib/session-create/delegate-worker.js +95 -6
- package/template/coding-task-manager/lib/session-create/identity.js +20 -1
- package/template/coding-task-manager/lib/session-create/prepared.js +69 -1
- package/template/coding-task-manager/lib/session-create/provider-launch-context.js +77 -0
- package/template/coding-task-manager/lib/session-create/provider-process.js +38 -39
- package/template/coding-task-manager/lib/session-create/response.js +30 -12
- package/template/coding-task-manager/lib/session-create/transcript.js +13 -4
- package/template/coding-task-manager/lib/session-create/worker-continuation.js +43 -1
- package/template/coding-task-manager/lib/session-create/worker-notices.js +94 -5
- package/template/coding-task-manager/lib/session-create/worker-observability.js +99 -7
- package/template/coding-task-manager/lib/session-create/worker-stop.js +11 -3
- package/template/coding-task-manager/lib/session-create/worker-wait-watcher.js +315 -0
- package/template/coding-task-manager/lib/session-geometry-authority-handler.js +10 -1
- package/template/coding-task-manager/lib/session-host-manager.js +152 -206
- package/template/coding-task-manager/lib/session-jobs.js +17 -9
- package/template/coding-task-manager/lib/session-lifecycle.js +20 -50
- package/template/coding-task-manager/lib/session-model-action-preconditions.js +68 -3
- package/template/coding-task-manager/lib/session-resource-sampler.js +67 -9
- package/template/coding-task-manager/lib/session-standup.js +67 -4
- package/template/coding-task-manager/lib/session-status-ledger.js +42 -3
- package/template/coding-task-manager/lib/session-status-runtime.js +45 -1
- package/template/coding-task-manager/lib/session-status.js +2 -2
- package/template/coding-task-manager/lib/session-title-mirror.js +25 -57
- package/template/coding-task-manager/lib/session-transcript-trash.js +8 -1
- package/template/coding-task-manager/lib/startup-pointer-write.js +57 -0
- package/template/coding-task-manager/lib/state-sync/painted-frame-field.js +11 -0
- package/template/coding-task-manager/lib/storage/coding-session-model-store.js +21 -22
- package/template/coding-task-manager/lib/surviving-owner-bootstrap.js +7 -2
- package/template/coding-task-manager/lib/surviving-owner-recovery.js +2 -1
- package/template/coding-task-manager/lib/terminal-draft-http-status.js +25 -0
- package/template/coding-task-manager/lib/transcript-archiver.js +229 -229
- package/template/coding-task-manager/lib/transcript-frame-cache.js +12 -2
- package/template/coding-task-manager/lib/transcript-guard-job.js +55 -4
- package/template/coding-task-manager/lib/transcript-link.js +77 -0
- package/template/coding-task-manager/lib/transcript-manifest-store.js +8 -4
- package/template/coding-task-manager/lib/transcript-source.js +8 -3
- package/template/coding-task-manager/lib/transcript-store.js +40 -0
- package/template/coding-task-manager/lib/tunnel-lane-governor.js +6 -3
- package/template/coding-task-manager/lib/typing-interaction-trace.js +136 -2
- package/template/coding-task-manager/lib/typing-worker-stages.js +208 -1
- package/template/coding-task-manager/lib/usage-ledger/claude-parse.js +53 -2
- package/template/coding-task-manager/lib/usage-ledger/ingest.js +13 -4
- package/template/coding-task-manager/lib/usage-ledger/read.js +14 -0
- package/template/coding-task-manager/lib/usage-ledger/schema.js +49 -1
- package/template/coding-task-manager/lib/usage-ledger/store.js +337 -6
- package/template/coding-task-manager/lib/usage-ledger/task-read.js +123 -5
- package/template/coding-task-manager/lib/visible-revision-watchdog.js +8 -12
- package/template/coding-task-manager/lib/wake-skip-reasons.js +46 -0
- package/template/coding-task-manager/lib/walle-supervisor.js +98 -14
- package/template/coding-task-manager/lib/worker-failure-classify.js +13 -1
- package/template/coding-task-manager/lib/ws/input.js +1 -0
- package/template/coding-task-manager/lib/ws/state-sync.js +19 -2
- package/template/coding-task-manager/lib/ws/terminal-attach.js +32 -1
- package/template/coding-task-manager/lib/ws-router.js +68 -1
- package/template/coding-task-manager/lib/ws-send-admission.js +22 -4
- package/template/coding-task-manager/oauth-proxy.js +99 -55
- package/template/coding-task-manager/package-lock.json +24 -0
- package/template/coding-task-manager/package.json +3 -0
- package/template/coding-task-manager/protocol/message-table.json +0 -10
- package/template/coding-task-manager/protocol/messages.js +0 -10
- package/template/coding-task-manager/protocol/ws-schema.json +0 -20
- package/template/coding-task-manager/protocol/ws-schema.md +0 -2
- package/template/coding-task-manager/providers/codex-mcp-extract.js +121 -19
- package/template/coding-task-manager/providers/codex-mcp.js +14 -5
- package/template/coding-task-manager/public/css/app.css +279 -2
- package/template/coding-task-manager/public/css/reviews.css +4 -0
- package/template/coding-task-manager/public/index.html +297 -240
- package/template/coding-task-manager/public/js/app/machines-page.js +1252 -0
- package/template/coding-task-manager/public/js/app/models-tab.js +546 -36
- package/template/coding-task-manager/public/js/app/worktrees-panel.js +7 -5
- package/template/coding-task-manager/public/js/background-work.js +13 -7
- package/template/coding-task-manager/public/js/client-health-core.js +81 -0
- package/template/coding-task-manager/public/js/client-health-main.js +61 -0
- package/template/coding-task-manager/public/js/coding-session-model-presentation.js +44 -4
- package/template/coding-task-manager/public/js/core/session-commands.js +676 -0
- package/template/coding-task-manager/public/js/core/session-selectors.js +5 -1
- package/template/coding-task-manager/public/js/history-app-seam.js +26 -2
- package/template/coding-task-manager/public/js/history-painted-rows.js +17 -2
- package/template/coding-task-manager/public/js/history-reader-park.js +13 -1
- package/template/coding-task-manager/public/js/history-scroller.js +187 -9
- package/template/coding-task-manager/public/js/history-transcript-pages.js +5 -5
- package/template/coding-task-manager/public/js/machine-pool-shared.js +436 -0
- package/template/coding-task-manager/public/js/private-state-scope.js +31 -3
- package/template/coding-task-manager/public/js/prompt-submission-client.js +173 -4
- package/template/coding-task-manager/public/js/prompt-submission-tray.js +281 -0
- package/template/coding-task-manager/public/js/protocol.js +0 -10
- package/template/coding-task-manager/public/js/queue-panel-submission-reconcile.js +174 -1
- package/template/coding-task-manager/public/js/reviews.js +7 -3
- package/template/coding-task-manager/public/js/root-draft-lifetime.js +139 -2
- package/template/coding-task-manager/public/js/root-queue-builder-lifetime.js +16 -0
- package/template/coding-task-manager/public/js/row-join.js +18 -3
- package/template/coding-task-manager/public/js/session-model-picker.js +1 -1
- package/template/coding-task-manager/public/js/session-search-utils.js +46 -19
- package/template/coding-task-manager/public/js/session-usage-view.js +1 -0
- package/template/coding-task-manager/public/js/state-sync-client.js +4 -0
- package/template/coding-task-manager/public/js/terminal/runtime-02.js +16 -7
- package/template/coding-task-manager/public/js/terminal/runtime-03.js +41 -0
- package/template/coding-task-manager/public/js/terminal/runtime-04.js +41 -4
- package/template/coding-task-manager/public/js/terminal/runtime-12.js +52 -2
- package/template/coding-task-manager/public/js/terminal/runtime-16.js +12 -12
- package/template/coding-task-manager/public/js/terminal/runtime-18.js +14 -0
- package/template/coding-task-manager/public/js/terminal/runtime-19.js +8 -4
- package/template/coding-task-manager/public/js/terminal/runtime-20.js +210 -73
- package/template/coding-task-manager/public/js/terminal/runtime-21.js +19 -6
- package/template/coding-task-manager/public/js/terminal/runtime-22.js +86 -0
- package/template/coding-task-manager/public/js/terminal-history-viewport.js +1 -3
- package/template/coding-task-manager/public/js/terminal-wheel.js +39 -2
- package/template/coding-task-manager/public/js/walle/chat-conversation.js +11 -3
- package/template/coding-task-manager/public/js/walle-session.js +30 -7
- package/template/coding-task-manager/public/m/app.css +226 -8
- package/template/coding-task-manager/public/m/app.js +502 -9
- package/template/coding-task-manager/public/m/index.html +38 -1
- package/template/coding-task-manager/public/m/machines.js +1313 -0
- package/template/coding-task-manager/public/m/pool-failover.js +391 -0
- package/template/coding-task-manager/public/m/private-queue-actions.js +26 -7
- package/template/coding-task-manager/public/m/sw.js +5 -0
- package/template/coding-task-manager/queue-engine.js +13 -0
- package/template/coding-task-manager/server/approvals.js +5 -6
- package/template/coding-task-manager/server/boot.js +136 -17
- package/template/coding-task-manager/server.js +964 -425
- package/template/coding-task-manager/session-integrity.js +7 -9
- package/template/coding-task-manager/workers/db-owner-worker.js +53 -7
- package/template/coding-task-manager/workers/read-pool-worker.js +17 -6
- package/template/coding-task-manager/workers/session-host-pool-process.js +18 -4
- package/template/coding-task-manager/workers/session-host-process.js +14 -3
- package/template/coding-task-manager/workers/transcript-archive-worker.js +1 -1
- package/template/coding-task-manager/workers/worktree-worker.js +34 -0
- package/template/package-lock.json +2 -2
- package/template/package.json +1 -1
- package/template/scripts/conversation-storage-v2-migration.js +78 -11
- package/template/shared/conversation-storage-v2-contract.js +42 -6
- package/template/shared/fixtures/worker-status-payloads.json +392 -0
- package/template/shared/git-remote-key.js +83 -0
- package/template/shared/host-identity.js +98 -2
- package/template/shared/owner-write-safety.js +135 -0
- package/template/shared/public-runtime.js +13 -5
- package/template/shared/release-schema.json +3 -2
- package/template/shared/report-citations.js +14 -0
- package/template/shared/sqlite-write-lock.js +361 -223
- package/template/shared/storage/contract.js +35 -3
- package/template/wall-e/agent.js +21 -0
- package/template/wall-e/api-walle.js +5 -0
- package/template/wall-e/bin/brain-purge-legacy-ctm.js +3 -2
- package/template/wall-e/bin/walle-mcp-stdio.js +12 -6
- package/template/wall-e/brain/helper-rules-schema.js +129 -0
- package/template/wall-e/brain/migrations.js +51 -22
- package/template/wall-e/brain/model-deployment-facts.js +31 -1
- package/template/wall-e/brain/owner-call-whitelist.js +16 -0
- package/template/wall-e/brain/schema-ensures.js +22 -0
- package/template/wall-e/brain.js +1 -1
- package/template/wall-e/coding/host-capabilities.js +2 -2
- package/template/wall-e/coding/journal-dispatcher.js +51 -0
- package/template/wall-e/coding/session-journal.js +28 -40
- package/template/wall-e/coding-orchestrator.js +23 -26
- package/template/wall-e/fly.toml +1 -1
- package/template/wall-e/http/auth.js +18 -1
- package/template/wall-e/http/blog-views-api.js +318 -0
- package/template/wall-e/http/helper-rules-api.js +321 -0
- package/template/wall-e/http/model-proxy-providers.js +3 -1
- package/template/wall-e/http/telemetry-api.js +12 -0
- package/template/wall-e/lib/blog-view-policy.js +88 -0
- package/template/wall-e/lib/blog-view-store.js +169 -0
- package/template/wall-e/lib/client-config-lock.js +2 -1
- package/template/wall-e/lib/coding-journal-writer-child-client.js +67 -18
- package/template/wall-e/lib/error-kinds.js +10 -1
- package/template/wall-e/lib/feature-manifest.js +1 -1
- package/template/wall-e/lib/helper-rules/cell-state.js +123 -0
- package/template/wall-e/lib/helper-rules/evaluate.js +357 -0
- package/template/wall-e/lib/helper-rules/explore-rubric.md +12 -0
- package/template/wall-e/lib/helper-rules/grade.js +277 -0
- package/template/wall-e/lib/helper-rules/label.js +247 -0
- package/template/wall-e/lib/helper-rules/lead-call-hook.js +522 -0
- package/template/wall-e/lib/helper-rules/store.js +667 -0
- package/template/wall-e/lib/mcp-integration.js +5 -6
- package/template/wall-e/lib/runtime-worker-pool.js +1 -0
- package/template/wall-e/lib/scheduler-worker-jobs.js +6 -0
- package/template/wall-e/lib/telemetry-db-schema.js +37 -0
- package/template/wall-e/lib/telemetry-process-client.js +76 -1
- package/template/wall-e/llm/default-fallback.js +4 -0
- package/template/wall-e/llm/model-catalog.js +13 -4
- package/template/wall-e/llm/model-roles.js +6 -0
- package/template/wall-e/llm/portkey-http.js +150 -0
- package/template/wall-e/llm/portkey-sync.js +15 -145
- package/template/wall-e/llm/worker-mcp-routing.js +61 -0
- package/template/wall-e/llm/worker-route.js +43 -1
- package/template/wall-e/loops/brain-custom-writes.js +45 -0
- package/template/wall-e/mcp-protocol-contract.js +41 -1
- package/template/wall-e/mcp-server.js +13 -2
- package/template/wall-e/mcp-worker-schemas.js +20 -2
- package/template/wall-e/memory/ctm-context-client.js +25 -5
- package/template/wall-e/shared/release-schema.json +3 -2
- package/template/wall-e/shared/sqlite-write-lock.js +361 -223
- package/template/wall-e/shared/storage/contract.js +35 -3
- package/template/wall-e/skills/_templates/delegate-worker/VERSION +1 -1
- package/template/wall-e/skills/_templates/delegate-worker/released-sha256.json +6 -0
- package/template/wall-e/skills/_templates/delegate-worker/templates/claude/SKILL.md +7 -2
- package/template/wall-e/skills/_templates/delegate-worker/templates/codex/SKILL.md +7 -2
- package/template/wall-e/tools/local-tools.js +12 -5
- package/template/wall-e/workers/brain-owner-worker.js +9 -0
- package/template/wall-e/workers/coding-journal-writer-child.js +8 -1
- package/template/wall-e/workers/telemetry-worker.js +8 -0
- package/template/coding-task-manager/lib/giant-freeze-park.js +0 -31
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// §5 D-7 / §5 D-9 / §9 S-6, S-10, S-13 (docs/model-proxy-survives-ctm-restart-2026-10-22.html): the
|
|
4
|
+
// UPGRADE HANDOFF -- both halves of it, in one place.
|
|
5
|
+
//
|
|
6
|
+
// The proxy's code changed on disk (CTM adopted it with `upgrade-pending`), so the running proxy cannot
|
|
7
|
+
// keep serving new code it never loaded, and CTM must not simply kill it: every in-flight model stream of
|
|
8
|
+
// every live CLI dies with it. Instead the OLD proxy spawns its own path from disk -- its successor --
|
|
9
|
+
// passes over the two LISTENING sockets it owns (plaintext HTTP, and the native mTLS listener when it is
|
|
10
|
+
// up), the memory-only state that has no other authority (the token registry, the per-token last lead
|
|
11
|
+
// model), and then stops ACCEPTING, drains its in-flight streams, and exits 0.
|
|
12
|
+
//
|
|
13
|
+
// The pillars this is built from (§3 P-1..P-4): the fd is passed, never re-bound (no EADDRINUSE window,
|
|
14
|
+
// no dropped syscall between two binds); the successor's `ready` is a handshake BEFORE the predecessor
|
|
15
|
+
// stops accepting; stop-accepting is split from stop-serving (`native-lead-listener.stopAccepting()`,
|
|
16
|
+
// `wrapHttpServerClose(..., { drain:true })`); and the drain is BOUNDED -- at `DRAIN_DEADLINE_MS` the
|
|
17
|
+
// remaining streams are cut and the process still exits 0 so nothing is left half-upgraded.
|
|
18
|
+
//
|
|
19
|
+
// What this module deliberately does NOT do: it never picks a different port, never retries a failed
|
|
20
|
+
// handoff (the predecessor keeps serving and CTM's next adoption tries again), and never writes the
|
|
21
|
+
// instance record of its successor -- the successor publishes its own identity, and a predecessor that
|
|
22
|
+
// is still draining must not unlink it (`isDraining()` is what keeps its SIGTERM handler off that file).
|
|
23
|
+
|
|
24
|
+
const { spawn } = require('node:child_process');
|
|
25
|
+
|
|
26
|
+
const tokens = require('./tokens');
|
|
27
|
+
const leadWorkerPicks = require('./lead-worker-picks');
|
|
28
|
+
const instanceRecord = require('./proxy-instance-record');
|
|
29
|
+
const streamAccounting = require('./stream-accounting');
|
|
30
|
+
const { wrapHttpServerClose } = require('./native-lead-listener');
|
|
31
|
+
|
|
32
|
+
// §5 D-7: the successor must answer `ready` within this; the old proxy keeps serving unchanged if it does
|
|
33
|
+
// not. Host handoffs measured 179-256 ms for five sessions, and the successor also loads the TLS
|
|
34
|
+
// authority, so 15 s is two orders of magnitude of headroom and still bounded.
|
|
35
|
+
const HANDOFF_READY_DEADLINE_MS = 15_000;
|
|
36
|
+
// §5 D-7: the failsafe only -- a drain normally ends the moment the counter reaches zero. 45 real
|
|
37
|
+
// streamed requests exceeded 10 minutes (§1.3), so the ceiling is above the longest observed turn.
|
|
38
|
+
const DRAIN_DEADLINE_MS = 1_800_000;
|
|
39
|
+
|
|
40
|
+
const HANDOFF_MESSAGE = 'handoff/v1';
|
|
41
|
+
const HANDOFF_NATIVE_MESSAGE = 'handoff/v1/native';
|
|
42
|
+
const HANDOFF_STATE_MESSAGE = 'handoff/v1/state';
|
|
43
|
+
const READY_MESSAGE = 'ready';
|
|
44
|
+
const HANDOFF_INHERITANCE_TIMEOUT_CODE = 'handoff_inheritance_timeout';
|
|
45
|
+
|
|
46
|
+
// The successor is always THIS file's sibling from disk, never a caller-supplied path.
|
|
47
|
+
const PROXY_SCRIPT = require.resolve('../../oauth-proxy.js');
|
|
48
|
+
|
|
49
|
+
// Whether THIS process has taken itself out of the accepting role: the record on disk now belongs to the
|
|
50
|
+
// successor, so a SIGTERM arriving after step 6 must not unlink it (WP-0's handler asks exactly this).
|
|
51
|
+
let _predecessorState = null;
|
|
52
|
+
|
|
53
|
+
function isDraining() {
|
|
54
|
+
return _predecessorState !== null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── §5 D-8: the proxy's own linger ────────────────────────────────────────────────────────────────────
|
|
58
|
+
// A proxy outlives the CTM that spawned it (§5 D-4) on purpose: a restart is a reconnect, and the
|
|
59
|
+
// in-flight streams of every live CLI survive it. The other half of that bargain is that a proxy which
|
|
60
|
+
// outlives CTM FOR GOOD must not serve forever -- it is a credential-holding listener on a loopback port.
|
|
61
|
+
// So it keeps serving a while after the last sign of life from above and then leaves by itself, exactly
|
|
62
|
+
// as a session host does (workers/session-host-pool-process.js:49 `HOST_LINGER_MS`, same value on
|
|
63
|
+
// purpose: one linger policy to reason about, and an operator reads "15 minutes" once, in both logs).
|
|
64
|
+
const PROXY_LINGER_MS = 15 * 60_000; // === HOST_LINGER_MS
|
|
65
|
+
|
|
66
|
+
// "Contact" is exactly one thing: a request that passed the admin-secret check in oauth-proxy.js's
|
|
67
|
+
// `_handleAdmin` -- CTM's boot probe, its lead-worker-picks read, a token mint, a handoff request. The
|
|
68
|
+
// unauthenticated surfaces (`/_health`, `/_status`, `/r/*`) are NOT contact: anyone on the loopback
|
|
69
|
+
// interface can poll them, so counting them would let a spectator keep a stale proxy alive for ever.
|
|
70
|
+
let _lastAdminContactAt = null;
|
|
71
|
+
// The ONE timer, armed by the CLI's start() only (see armLinger): null means "this process never lingers".
|
|
72
|
+
let _lingerTimer = null;
|
|
73
|
+
|
|
74
|
+
/** §5 D-8: record contact from above. Called once per authenticated admin request, and nowhere else. */
|
|
75
|
+
function noteAdminContact({ now = Date.now() } = {}) {
|
|
76
|
+
_lastAdminContactAt = now;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The moment of the last authenticated admin request, or null when this process has never had one. */
|
|
80
|
+
function lastAdminContactAt() {
|
|
81
|
+
return _lastAdminContactAt;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Test seam (§6): whether a linger timer is armed. Nothing in production reads it. */
|
|
85
|
+
function _lingerArmedForTest() {
|
|
86
|
+
return _lingerTimer !== null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function _armLingerTimer({ lingerMs, ctx }) {
|
|
90
|
+
_lingerTimer = setTimeout(() => {
|
|
91
|
+
_lingerTimer = null;
|
|
92
|
+
_lingerTick({ lingerMs, ctx });
|
|
93
|
+
}, lingerMs);
|
|
94
|
+
// §5 D-8: never a reason for this process to stay alive -- the listeners and the sockets are.
|
|
95
|
+
_lingerTimer.unref();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* §5 D-8: one tick. The window is the cadence as well as the test, so there is one constant here, not two.
|
|
100
|
+
* Anything that could still be serving -- an in-flight stream, an open connection on either lane, a
|
|
101
|
+
* handoff being brought up -- re-arms and the process stays.
|
|
102
|
+
*/
|
|
103
|
+
function _lingerTick({ lingerMs, ctx }) {
|
|
104
|
+
const idleMs = _lastAdminContactAt === null ? 0 : Date.now() - _lastAdminContactAt;
|
|
105
|
+
const rearm = () => _armLingerTimer({ lingerMs, ctx });
|
|
106
|
+
// A predecessor mid-handoff is not idle: its successor is starting, and its own drain owns its exit.
|
|
107
|
+
if (idleMs < lingerMs || isDraining()) { rearm(); return; }
|
|
108
|
+
// The counter outlives the transfer for an upgraded stream, so it is checked as well as the sockets.
|
|
109
|
+
if (ctx.streamAccounting.count() > 0) { rearm(); return; }
|
|
110
|
+
const nativeCount = ctx.nativeListener && typeof ctx.nativeListener.socketCount === 'function'
|
|
111
|
+
? ctx.nativeListener.socketCount()
|
|
112
|
+
: 0;
|
|
113
|
+
if (nativeCount > 0) { rearm(); return; }
|
|
114
|
+
ctx.httpServer.getConnections((err, count) => {
|
|
115
|
+
// REPORT: a count that cannot be read is treated as busy -- an unanswerable question must never be
|
|
116
|
+
// the reason a proxy holding live credentials disappears.
|
|
117
|
+
if (err || count > 0) { rearm(); return; }
|
|
118
|
+
_exitOnLinger({ idleMs, ctx });
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** §5 D-8: the exit itself -- record it, remove this process's own record, and leave 0. */
|
|
123
|
+
function _exitOnLinger({ idleMs, ctx }) {
|
|
124
|
+
console.log(`[model-proxy] exit reason=linger idle_ms=${idleMs}`);
|
|
125
|
+
// The record names a process that is about to be gone, so the next CTM spawns rather than adopts. It is
|
|
126
|
+
// removed BEFORE the exit record is written: that one is the ledger's, and it is written once.
|
|
127
|
+
instanceRecord.unlinkInstanceRecord({ dataDir: ctx.dataDir });
|
|
128
|
+
streamAccounting.writeExitRecord({ dataDir: ctx.dataDir, reason: 'linger', openStreams: 0 })
|
|
129
|
+
.catch((err) => {
|
|
130
|
+
// REPORT: the exit code below still classifies this exit, so a failed write is not fatal.
|
|
131
|
+
console.error(`[model-proxy] exit record not written: ${(err && err.code) || 'unknown'}`);
|
|
132
|
+
})
|
|
133
|
+
.then(() => process.exit(0));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* §5 D-8: arm the linger timer for THIS process. Only the CLI calls this (proxy-cli.js, with an optional
|
|
138
|
+
* `CTM_TEST_PROXY_LINGER_MS` override) -- an embedded start() from a test or another module passes no
|
|
139
|
+
* `lingerMs`, so no test process can ever be exited by a proxy's own linger.
|
|
140
|
+
*
|
|
141
|
+
* "Contact" starts at arming time: a process that has just started (a handoff SUCCESSOR above all) has
|
|
142
|
+
* been in contact by construction -- its predecessor only hands over after an authenticated request.
|
|
143
|
+
*
|
|
144
|
+
* @param {object} input
|
|
145
|
+
* @param {number} [input.lingerMs] the idle window, defaulting to PROXY_LINGER_MS
|
|
146
|
+
* @param {object} input.ctx this server's context (`httpServer`, `nativeListener`, `streamAccounting`,
|
|
147
|
+
* `dataDir` -- everything the tick needs to prove nothing is being served)
|
|
148
|
+
*/
|
|
149
|
+
function armLinger({ lingerMs = PROXY_LINGER_MS, ctx } = {}) {
|
|
150
|
+
if (!ctx || !ctx.httpServer || !ctx.streamAccounting) return false;
|
|
151
|
+
if (_lingerTimer) clearTimeout(_lingerTimer);
|
|
152
|
+
_lastAdminContactAt = _lastAdminContactAt === null ? Date.now() : _lastAdminContactAt;
|
|
153
|
+
_armLingerTimer({ lingerMs: _positiveIntOr(lingerMs, PROXY_LINGER_MS), ctx });
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** A bounded deadline comes from the admin body or not at all: anything that is not a positive integer
|
|
158
|
+
* (a float, a string, 0, a negative, a missing field) is IGNORED and the constant stands. */
|
|
159
|
+
function _positiveIntOr(value, fallback) {
|
|
160
|
+
return Number.isSafeInteger(value) && value > 0 ? value : fallback;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* §5 D-7 steps 1-6: hand this proxy over to a successor spawned from disk.
|
|
165
|
+
*
|
|
166
|
+
* @param {object} input
|
|
167
|
+
* @param {object} input.ctx the running server's context (its `streamAccounting` is the drain signal)
|
|
168
|
+
* @param {import('http').Server} input.httpServer the plaintext server whose listening socket is passed
|
|
169
|
+
* @param {object|null} [input.nativeListener] the mTLS controller; its server is passed only when ready
|
|
170
|
+
* @param {string} input.dataDir the canonical data directory (record, secret and log live under it)
|
|
171
|
+
* @param {number} input.port the bound plaintext port
|
|
172
|
+
* @param {number|null} [input.nativePort] the bound native port, when the listener was up
|
|
173
|
+
* @param {string|null} [input.adminSecret] this process's admin secret, re-used by the successor
|
|
174
|
+
* @param {number} [input.generation] this process's generation; the successor is +1
|
|
175
|
+
* @param {number} [input.readyDeadlineMs] test override (admin body only, never env)
|
|
176
|
+
* @param {number} [input.drainDeadlineMs] test override (admin body only, never env)
|
|
177
|
+
* @param {object} [input.log] `log`/`error` sink, default console
|
|
178
|
+
* @returns {Promise<{ok:true, generation:number, successorPid:number, handoffMs:number, armDrain:Function}
|
|
179
|
+
* | {ok:false, status:409, reason:string}>} the caller's `armDrain()` runs the drain (step 7) once its
|
|
180
|
+
* own 202 has been written, so the exit this schedules can never preempt the response.
|
|
181
|
+
*/
|
|
182
|
+
async function initiateHandoff({
|
|
183
|
+
ctx = null,
|
|
184
|
+
httpServer = null,
|
|
185
|
+
nativeListener = null,
|
|
186
|
+
dataDir = null,
|
|
187
|
+
port = null,
|
|
188
|
+
nativePort = null,
|
|
189
|
+
adminSecret = null,
|
|
190
|
+
generation = 1,
|
|
191
|
+
readyDeadlineMs,
|
|
192
|
+
drainDeadlineMs,
|
|
193
|
+
log = console,
|
|
194
|
+
} = {}) {
|
|
195
|
+
// step 1 -- one handoff per process, and the first one is final: a drained predecessor has nothing
|
|
196
|
+
// left to hand over, and a second caller must not spawn a second successor.
|
|
197
|
+
if (_predecessorState) return { ok: false, status: 409, reason: 'already_draining' };
|
|
198
|
+
_predecessorState = 'draining';
|
|
199
|
+
const startedAt = Date.now();
|
|
200
|
+
const readyMs = _positiveIntOr(readyDeadlineMs, HANDOFF_READY_DEADLINE_MS);
|
|
201
|
+
const drainMs = _positiveIntOr(drainDeadlineMs, DRAIN_DEADLINE_MS);
|
|
202
|
+
const accounting = ctx && ctx.streamAccounting ? ctx.streamAccounting : null;
|
|
203
|
+
|
|
204
|
+
const abort = (reason) => {
|
|
205
|
+
// §5 D-7 step 5 ("abort restores"): the predecessor kept serving, so both truths it wrote must say
|
|
206
|
+
// so again -- the record stops claiming a drain, and /_health stops reporting one. The IDENTITY is
|
|
207
|
+
// restored with them: a successor that got as far as publishing its own record and then never
|
|
208
|
+
// answered is killed here, so the record must name the process that is really serving rather than
|
|
209
|
+
// the one that just died (otherwise the next boot reads a dead pid and spawns a second proxy onto a
|
|
210
|
+
// port this process still holds).
|
|
211
|
+
_predecessorState = null;
|
|
212
|
+
instanceRecord.updateInstanceRecord({
|
|
213
|
+
dataDir, patch: { pid: process.pid, generation, predecessorPid: null, draining: false },
|
|
214
|
+
});
|
|
215
|
+
if (accounting && typeof accounting.setDraining === 'function') accounting.setDraining(false);
|
|
216
|
+
return { ok: false, status: 409, reason };
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// step 2 -- the record is truthful BEFORE the successor exists: a predecessor that dies mid-handoff
|
|
220
|
+
// leaves a record that says a handoff was in progress rather than one that silently claims to serve.
|
|
221
|
+
instanceRecord.updateInstanceRecord({ dataDir, patch: { draining: true } });
|
|
222
|
+
if (accounting && typeof accounting.setDraining === 'function') accounting.setDraining(true);
|
|
223
|
+
|
|
224
|
+
// step 3 -- detached, its own log file, an IPC channel for the two handles, and this process's own
|
|
225
|
+
// environment (the successor is the same proxy with the same configurability).
|
|
226
|
+
let logFd = null;
|
|
227
|
+
let child = null;
|
|
228
|
+
try {
|
|
229
|
+
logFd = instanceRecord.openProxyLogFd(dataDir);
|
|
230
|
+
child = spawn(process.execPath, [PROXY_SCRIPT], {
|
|
231
|
+
detached: true,
|
|
232
|
+
stdio: ['ignore', logFd, logFd, 'ipc'],
|
|
233
|
+
env: process.env,
|
|
234
|
+
serialization: 'json',
|
|
235
|
+
});
|
|
236
|
+
} catch (err) {
|
|
237
|
+
// REPORT: a successor that could not be created is the same outcome as one that never answered.
|
|
238
|
+
log.error(`[model-proxy] handoff spawn failed: ${(err && err.code) || 'unknown'}`);
|
|
239
|
+
instanceRecord.closeProxyLogFd(logFd);
|
|
240
|
+
return abort('spawn_failed');
|
|
241
|
+
}
|
|
242
|
+
instanceRecord.closeProxyLogFd(logFd); // the successor holds its own copy of the descriptor
|
|
243
|
+
child.unref(); // CTM owns neither proxy; a successor must not be reaped with this process
|
|
244
|
+
|
|
245
|
+
// steps 4 + 5 -- the handshake. The wait is armed BEFORE the first send, so a successor that answers
|
|
246
|
+
// instantly cannot be missed.
|
|
247
|
+
const ready = _waitForReady(child, readyMs);
|
|
248
|
+
const handed = _sendHandoff(child, httpServer, nativeListener, {
|
|
249
|
+
adminSecret, dataDir, port, nativePort, generation: generation + 1,
|
|
250
|
+
});
|
|
251
|
+
const outcome = handed ? await ready : { ok: false, reason: 'successor_not_ready' };
|
|
252
|
+
if (!outcome.ok) {
|
|
253
|
+
try {
|
|
254
|
+
child.kill('SIGKILL');
|
|
255
|
+
} catch (err) {
|
|
256
|
+
// expected: the successor may already be gone; the predecessor's own state is what matters.
|
|
257
|
+
void err;
|
|
258
|
+
}
|
|
259
|
+
log.error(`[model-proxy] handoff aborted: ${outcome.reason}`);
|
|
260
|
+
return abort(outcome.reason);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// step 6 -- stop ACCEPTING (never destroy): the plaintext listening socket is closed here and the
|
|
264
|
+
// native listener releases its own, while every connection either one already accepted keeps serving.
|
|
265
|
+
const handoffMs = Date.now() - startedAt;
|
|
266
|
+
if (nativeListener) wrapHttpServerClose(httpServer, nativeListener, { drain: true });
|
|
267
|
+
httpServer.close();
|
|
268
|
+
if (typeof httpServer.closeIdleConnections === 'function') httpServer.closeIdleConnections();
|
|
269
|
+
_predecessorState = 'drained';
|
|
270
|
+
log.log(`[model-proxy] handoff successor_pid=${outcome.pid} generation=${generation + 1} handoff_ms=${handoffMs}`);
|
|
271
|
+
return {
|
|
272
|
+
ok: true,
|
|
273
|
+
generation: generation + 1,
|
|
274
|
+
successorPid: outcome.pid,
|
|
275
|
+
handoffMs,
|
|
276
|
+
// step 7 is armed by the CALLER, once its own 202 is on the wire.
|
|
277
|
+
armDrain: () => startDrain({ accounting, httpServer, nativeListener, drainMs, log }),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* §5 D-7 step 5 (successor side): everything that must be true before `ready` may be sent. The inherited
|
|
283
|
+
* TLS socket is re-bound first -- when the predecessor had one, its listener was serving, so the
|
|
284
|
+
* successor must be serving on the same socket before the predecessor stops accepting on it.
|
|
285
|
+
* @returns {Promise<void>} resolves when this process is genuinely serving
|
|
286
|
+
*/
|
|
287
|
+
function announceInheritedStart({ ctx, inherited, record }) {
|
|
288
|
+
if (!inherited.nativeHandle || !ctx || !ctx.nativeListener) return Promise.resolve(sendReady(inherited, record));
|
|
289
|
+
return ctx.nativeListener.ensureReady().then(
|
|
290
|
+
() => sendReady(inherited, record),
|
|
291
|
+
(err) => {
|
|
292
|
+
// REPORT: the inherited TLS socket could not be re-bound; the plaintext lane is unaffected and the
|
|
293
|
+
// native lane falls back to its own bind on the next registration.
|
|
294
|
+
process.stderr.write(`[model-proxy] inherited native listener unavailable: ${(err && err.code) || 'unknown'}\n`);
|
|
295
|
+
return sendReady(inherited, record);
|
|
296
|
+
},
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** §6 test seam (AC-2b): a successor that is held back does everything else and never answers. */
|
|
301
|
+
function sendReady(inherited, record) {
|
|
302
|
+
if (inherited.holdReady) return;
|
|
303
|
+
process.send({ t: READY_MESSAGE, pid: process.pid, closureHash: record ? record.closureHash : null });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* §5 D-7 step 4: the three messages, in the order the successor expects them. The plaintext server goes
|
|
308
|
+
* with the first one, the TLS server with the second -- and only when it is really up, because a
|
|
309
|
+
* listener that never bound has no socket to give (the successor binds the port itself in that case).
|
|
310
|
+
* @returns {boolean} false when the channel refused a send, which aborts the handoff
|
|
311
|
+
*/
|
|
312
|
+
function _sendHandoff(child, httpServer, nativeListener, payload) {
|
|
313
|
+
try {
|
|
314
|
+
child.send({ t: HANDOFF_MESSAGE, ...payload, predecessorPid: process.pid }, httpServer);
|
|
315
|
+
if (nativeListener && nativeListener.isReady() && typeof nativeListener.server === 'function') {
|
|
316
|
+
child.send({ t: HANDOFF_NATIVE_MESSAGE }, nativeListener.server());
|
|
317
|
+
}
|
|
318
|
+
child.send({
|
|
319
|
+
t: HANDOFF_STATE_MESSAGE,
|
|
320
|
+
tokens: tokens.exportAll(),
|
|
321
|
+
lastLeadModel: leadWorkerPicks.exportLastLeadModel(),
|
|
322
|
+
});
|
|
323
|
+
return true;
|
|
324
|
+
} catch (err) {
|
|
325
|
+
// REPORT: the successor never became reachable, so the caller kills it and keeps serving.
|
|
326
|
+
process.stderr.write(`[model-proxy] handoff send failed: ${(err && err.code) || 'unknown'}\n`);
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* §5 D-7 step 5: `{ t:'ready', pid, closureHash }` within the deadline; a successor that exits first is
|
|
333
|
+
* the same failure as one that never answers.
|
|
334
|
+
* @returns {Promise<{ok:true, pid:number, closureHash:string}|{ok:false, reason:'successor_not_ready'}>}
|
|
335
|
+
*/
|
|
336
|
+
function _waitForReady(child, readyMs) {
|
|
337
|
+
return new Promise((resolve) => {
|
|
338
|
+
let settled = false;
|
|
339
|
+
let timer = null;
|
|
340
|
+
const settle = (value) => {
|
|
341
|
+
if (settled) return;
|
|
342
|
+
settled = true;
|
|
343
|
+
clearTimeout(timer);
|
|
344
|
+
child.removeListener('message', onMessage);
|
|
345
|
+
child.removeListener('exit', onExit);
|
|
346
|
+
resolve(value);
|
|
347
|
+
};
|
|
348
|
+
const onMessage = (message) => {
|
|
349
|
+
if (message && message.t === READY_MESSAGE) settle({ ok: true, pid: message.pid, closureHash: message.closureHash });
|
|
350
|
+
};
|
|
351
|
+
const onExit = () => settle({ ok: false, reason: 'successor_not_ready' });
|
|
352
|
+
timer = setTimeout(() => settle({ ok: false, reason: 'successor_not_ready' }), readyMs);
|
|
353
|
+
child.on('message', onMessage);
|
|
354
|
+
child.on('exit', onExit);
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* §5 D-7 step 7: exit 0 the moment the counter reaches zero (the decrement notifies -- there is no
|
|
360
|
+
* polling loop), or at the deadline cut whatever is left and exit 0 anyway. `drain_forced_cuts` is the
|
|
361
|
+
* count AT the moment of the cut, so a clean drain logs 0 and the number is never a guess.
|
|
362
|
+
*/
|
|
363
|
+
function startDrain({ accounting = null, httpServer = null, nativeListener = null, drainMs = DRAIN_DEADLINE_MS, log = console } = {}) {
|
|
364
|
+
let done = false;
|
|
365
|
+
let timer = null;
|
|
366
|
+
const finish = (forcedCuts) => {
|
|
367
|
+
if (done) return;
|
|
368
|
+
done = true;
|
|
369
|
+
clearTimeout(timer);
|
|
370
|
+
log.log(`[model-proxy] drain_forced_cuts=${forcedCuts}`);
|
|
371
|
+
process.exit(0);
|
|
372
|
+
};
|
|
373
|
+
timer = setTimeout(() => {
|
|
374
|
+
const remaining = accounting ? accounting.count() : 0;
|
|
375
|
+
try {
|
|
376
|
+
if (httpServer && typeof httpServer.closeAllConnections === 'function') httpServer.closeAllConnections();
|
|
377
|
+
} catch (err) {
|
|
378
|
+
// expected: a server that is already closed owns no connection to cut; the exit below still runs.
|
|
379
|
+
void err;
|
|
380
|
+
}
|
|
381
|
+
if (nativeListener && typeof nativeListener.close === 'function') {
|
|
382
|
+
try {
|
|
383
|
+
nativeListener.close();
|
|
384
|
+
} catch (err) {
|
|
385
|
+
// expected: the native listener may already be closed; the exit below still runs.
|
|
386
|
+
void err;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
finish(remaining);
|
|
390
|
+
}, drainMs);
|
|
391
|
+
if (!accounting || accounting.count() === 0) {
|
|
392
|
+
// Nothing is in flight: leave on the next turn, so the caller's own response has been queued first.
|
|
393
|
+
setImmediate(() => finish(0));
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
accounting.onIdle(() => finish(0));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* §5 D-7 (successor side): collect the predecessor's three messages and resolve what they carry. Only a
|
|
401
|
+
* process spawned with an IPC channel calls this -- and it is the SUCCESSOR, so it never reads stdin.
|
|
402
|
+
*
|
|
403
|
+
* Termination is the STATE message, not a count: the native message is optional (it is sent only when
|
|
404
|
+
* the predecessor's TLS listener was up), and IPC preserves order, so the state message proves whether a
|
|
405
|
+
* native handle was coming.
|
|
406
|
+
*
|
|
407
|
+
* @param {{timeoutMs?:number}} [options] bounded wait; a predecessor that dies mid-handshake must not
|
|
408
|
+
* leave a successor waiting forever
|
|
409
|
+
* @returns {Promise<{inherited:object, state:{tokens:Array, lastLeadModel:Array}}>}
|
|
410
|
+
*/
|
|
411
|
+
function receiveInheritance({ timeoutMs = HANDOFF_READY_DEADLINE_MS } = {}) {
|
|
412
|
+
return new Promise((resolve, reject) => {
|
|
413
|
+
if (typeof process.send !== 'function' || typeof process.on !== 'function') {
|
|
414
|
+
const err = new Error('handoff inheritance requested without an IPC channel');
|
|
415
|
+
err.code = HANDOFF_INHERITANCE_TIMEOUT_CODE;
|
|
416
|
+
reject(err);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
let first = null;
|
|
420
|
+
let nativeHandle = null;
|
|
421
|
+
let state = null;
|
|
422
|
+
let settled = false;
|
|
423
|
+
let timer = null;
|
|
424
|
+
const cleanup = () => {
|
|
425
|
+
clearTimeout(timer);
|
|
426
|
+
process.removeListener('message', onMessage);
|
|
427
|
+
};
|
|
428
|
+
const maybe = () => {
|
|
429
|
+
if (settled || !first || !state) return;
|
|
430
|
+
settled = true;
|
|
431
|
+
cleanup();
|
|
432
|
+
resolve({
|
|
433
|
+
inherited: {
|
|
434
|
+
httpHandle: first.httpHandle,
|
|
435
|
+
nativeHandle,
|
|
436
|
+
adminSecret: first.adminSecret,
|
|
437
|
+
dataDir: first.dataDir,
|
|
438
|
+
port: first.port,
|
|
439
|
+
nativePort: first.nativePort === undefined ? null : first.nativePort,
|
|
440
|
+
generation: first.generation,
|
|
441
|
+
predecessorPid: first.predecessorPid,
|
|
442
|
+
},
|
|
443
|
+
state: {
|
|
444
|
+
tokens: Array.isArray(state.tokens) ? state.tokens : [],
|
|
445
|
+
lastLeadModel: Array.isArray(state.lastLeadModel) ? state.lastLeadModel : [],
|
|
446
|
+
},
|
|
447
|
+
});
|
|
448
|
+
};
|
|
449
|
+
const onMessage = (message, handle) => {
|
|
450
|
+
if (!message || typeof message !== 'object') return;
|
|
451
|
+
if (message.t === HANDOFF_MESSAGE) {
|
|
452
|
+
first = { ...message, httpHandle: handle };
|
|
453
|
+
maybe();
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (message.t === HANDOFF_NATIVE_MESSAGE) {
|
|
457
|
+
nativeHandle = handle;
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (message.t === HANDOFF_STATE_MESSAGE) {
|
|
461
|
+
state = message;
|
|
462
|
+
maybe();
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
timer = setTimeout(() => {
|
|
466
|
+
if (settled) return;
|
|
467
|
+
settled = true;
|
|
468
|
+
cleanup();
|
|
469
|
+
const err = new Error('handoff inheritance timed out');
|
|
470
|
+
err.code = HANDOFF_INHERITANCE_TIMEOUT_CODE;
|
|
471
|
+
reject(err);
|
|
472
|
+
}, _positiveIntOr(timeoutMs, HANDOFF_READY_DEADLINE_MS));
|
|
473
|
+
process.on('message', onMessage);
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
module.exports = {
|
|
478
|
+
HANDOFF_READY_DEADLINE_MS,
|
|
479
|
+
DRAIN_DEADLINE_MS,
|
|
480
|
+
// §5 D-8: the proxy's own idle window, and the two functions that implement it. `noteAdminContact` is
|
|
481
|
+
// the ONLY writer of the contact clock; oauth-proxy.js calls it from one place, after the secret check.
|
|
482
|
+
PROXY_LINGER_MS,
|
|
483
|
+
armLinger,
|
|
484
|
+
noteAdminContact,
|
|
485
|
+
lastAdminContactAt,
|
|
486
|
+
_lingerArmedForTest,
|
|
487
|
+
HANDOFF_INHERITANCE_TIMEOUT_CODE,
|
|
488
|
+
initiateHandoff,
|
|
489
|
+
receiveInheritance,
|
|
490
|
+
announceInheritedStart,
|
|
491
|
+
startDrain,
|
|
492
|
+
isDraining,
|
|
493
|
+
_positiveIntOr,
|
|
494
|
+
};
|
|
@@ -112,6 +112,29 @@ function forgetToken(token) {
|
|
|
112
112
|
_lastLeadModel.delete(token);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
/** §9 S-10 (WP-2 handoff): the memory-only per-token model memory, as IPC-safe rows. */
|
|
116
|
+
function exportLastLeadModel() {
|
|
117
|
+
return [..._lastLeadModel.entries()];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Register each exported pair verbatim (§9 S-10). A malformed row is skipped rather than thrown on:
|
|
122
|
+
* one unreadable pair must not keep a successor from serving the pairs that are readable.
|
|
123
|
+
* @returns {number} how many pairs were registered
|
|
124
|
+
*/
|
|
125
|
+
function importLastLeadModel(rows) {
|
|
126
|
+
if (!Array.isArray(rows)) return 0;
|
|
127
|
+
let imported = 0;
|
|
128
|
+
for (const row of rows) {
|
|
129
|
+
if (!Array.isArray(row) || row.length !== 2) continue;
|
|
130
|
+
const [token, model] = row;
|
|
131
|
+
if (!_isNonEmptyString(token) || !_isNonEmptyString(model)) continue;
|
|
132
|
+
_lastLeadModel.set(token, model);
|
|
133
|
+
imported += 1;
|
|
134
|
+
}
|
|
135
|
+
return imported;
|
|
136
|
+
}
|
|
137
|
+
|
|
115
138
|
/**
|
|
116
139
|
* Resolve a `walle-worker` model request against this lead session's pick table (§5 D-6/D-7).
|
|
117
140
|
* @returns {{swapped:false} | {swapped:true, alias:string, model:string, source:'pick'|'last_lead_model'} | {error:'subagent_pick_unset'}}
|
|
@@ -146,5 +169,7 @@ module.exports = {
|
|
|
146
169
|
noteLeadModel,
|
|
147
170
|
lastLeadModel,
|
|
148
171
|
forgetToken,
|
|
172
|
+
exportLastLeadModel,
|
|
173
|
+
importLastLeadModel,
|
|
149
174
|
resolveWorkerAlias,
|
|
150
175
|
};
|
|
@@ -95,6 +95,9 @@ function _peerCertificateFingerprint(socket) {
|
|
|
95
95
|
|
|
96
96
|
function _deny(res, status, code) {
|
|
97
97
|
if (res.writableEnded || res.headersSent) return;
|
|
98
|
+
// §9 S-9: one line, the finite reason code only — never the certificate, header value or path that
|
|
99
|
+
// caused the refusal, and never the body this connection never got to send.
|
|
100
|
+
console.log(`[model-proxy] refused status=${status} reason=${code}`);
|
|
98
101
|
res.writeHead(status, { 'content-type': 'application/json' });
|
|
99
102
|
res.end(JSON.stringify({ type: 'error', error: { type: 'authentication_error', code, message: code } }));
|
|
100
103
|
}
|
|
@@ -111,7 +114,7 @@ function _deny(res, status, code) {
|
|
|
111
114
|
* @param {Function} input.onAuthorizedRequest the host callback into the shared routing pipeline,
|
|
112
115
|
* invoked as `(req, res, {token, binding, suffix, query, fingerprint})` after authorization
|
|
113
116
|
*/
|
|
114
|
-
function createNativeLeadListener({ httpPort, dataDir, loadAuthority, onAuthorizedRequest } = {}) {
|
|
117
|
+
function createNativeLeadListener({ httpPort, dataDir, loadAuthority, onAuthorizedRequest, streamAccounting, inheritedHandle = null } = {}) {
|
|
115
118
|
// Static refusals, before anything can bind. A port whose successor is not a TCP port is refused
|
|
116
119
|
// rather than renumbered (D-8: a collision fails this transport explicitly; it never strands an
|
|
117
120
|
// adopted host on a port nobody recorded).
|
|
@@ -253,6 +256,9 @@ function createNativeLeadListener({ httpPort, dataDir, loadAuthority, onAuthoriz
|
|
|
253
256
|
server.on('tlsClientError', (_err, socket) => {
|
|
254
257
|
try { socket.destroy(); } catch { /* expected: the failed handshake socket is already unusable. */ }
|
|
255
258
|
});
|
|
259
|
+
// §5 D-9: this lane's requests are counted by the SAME counter as the plaintext lane when the host
|
|
260
|
+
// supplies one; a caller that wires none counts none.
|
|
261
|
+
if (streamAccounting) streamAccounting.track(server);
|
|
256
262
|
server.on('request', (req, res) => {
|
|
257
263
|
_handleRequest(req, res).catch(() => {
|
|
258
264
|
if (!res.headersSent && !res.writableEnded) _deny(res, 500, 'native_lead_request_failed');
|
|
@@ -274,7 +280,11 @@ function createNativeLeadListener({ httpPort, dataDir, loadAuthority, onAuthoriz
|
|
|
274
280
|
server.once('error', onError);
|
|
275
281
|
server.once('listening', onListening);
|
|
276
282
|
try {
|
|
277
|
-
|
|
283
|
+
// §5 D-7 step 4 / S-6: a handoff successor binds the TLS LISTENING SOCKET its predecessor passed
|
|
284
|
+
// over IPC, so the port never has a gap and no second bind can race an EADDRINUSE. Without a
|
|
285
|
+
// handle this is today's lazy port bind, byte for byte.
|
|
286
|
+
if (inheritedHandle) server.listen(inheritedHandle);
|
|
287
|
+
else server.listen(nativePort, LOOPBACK_ADDRESS);
|
|
278
288
|
} catch {
|
|
279
289
|
onError();
|
|
280
290
|
}
|
|
@@ -318,6 +328,29 @@ function createNativeLeadListener({ httpPort, dataDir, loadAuthority, onAuthoriz
|
|
|
318
328
|
_admissionClosed = true;
|
|
319
329
|
}
|
|
320
330
|
|
|
331
|
+
/**
|
|
332
|
+
* §5 D-7 step 6 / S-6: stop ACCEPTING on this listener without touching a single accepted connection.
|
|
333
|
+
* This is the handoff's half of the stop-accepting/stop-serving split (P-3): the predecessor is on its
|
|
334
|
+
* way out, so it must not take another connection, and every mTLS request already in flight finishes
|
|
335
|
+
* on the socket it arrived on. Idempotent, and a listener that never became ready resolves at once.
|
|
336
|
+
*
|
|
337
|
+
* The listening handle is released SYNCHRONOUSLY by `close()`; close()'s own callback fires only once
|
|
338
|
+
* every open connection has ended, which is exactly what this function must not wait for.
|
|
339
|
+
* @returns {Promise<void>}
|
|
340
|
+
*/
|
|
341
|
+
function stopAccepting() {
|
|
342
|
+
_admissionClosed = true;
|
|
343
|
+
const server = _server;
|
|
344
|
+
if (!server) return Promise.resolve();
|
|
345
|
+
try {
|
|
346
|
+
server.close();
|
|
347
|
+
} catch (err) {
|
|
348
|
+
// expected: a server that is already closed owns nothing to release; the refusal flag above stands.
|
|
349
|
+
void err;
|
|
350
|
+
}
|
|
351
|
+
return Promise.resolve();
|
|
352
|
+
}
|
|
353
|
+
|
|
321
354
|
async function _joinInit() {
|
|
322
355
|
if (!_readyPromise) return;
|
|
323
356
|
try {
|
|
@@ -364,6 +397,9 @@ function createNativeLeadListener({ httpPort, dataDir, loadAuthority, onAuthoriz
|
|
|
364
397
|
ensureReady,
|
|
365
398
|
close,
|
|
366
399
|
closeHttpAdmission,
|
|
400
|
+
stopAccepting,
|
|
401
|
+
/** §5 D-7 step 4 / S-6: the LIVE server, for `child.send(msg, listener.server())`. Null until ready. */
|
|
402
|
+
server: () => _server,
|
|
367
403
|
nativePort,
|
|
368
404
|
baseUrl: `https://${LOOPBACK_ADDRESS}:${nativePort}/native-lead/v1`,
|
|
369
405
|
isReady: () => _server !== null,
|
|
@@ -372,6 +408,12 @@ function createNativeLeadListener({ httpPort, dataDir, loadAuthority, onAuthoriz
|
|
|
372
408
|
};
|
|
373
409
|
}
|
|
374
410
|
|
|
411
|
+
// The un-wrapped close is remembered ON the server (never in a module global: two servers can be alive
|
|
412
|
+
// in one process) so a LATER wrap can replace the behaviour instead of stacking on top of it. The
|
|
413
|
+
// handoff drain (§5 D-7 step 6) installs its wrap after start() already installed the normal-mode one,
|
|
414
|
+
// and a stacked wrap would run the destroying close underneath the non-destroying stop.
|
|
415
|
+
const kOriginalServerClose = Symbol('ctmHttpServerOriginalClose');
|
|
416
|
+
|
|
375
417
|
/**
|
|
376
418
|
* Wrap an HTTP server's `close` so the native listener starts closing IMMEDIATELY and the caller's
|
|
377
419
|
* callback fires only once BOTH joins completed.
|
|
@@ -383,15 +425,27 @@ function createNativeLeadListener({ httpPort, dataDir, loadAuthority, onAuthoriz
|
|
|
383
425
|
* controller first removes that ordering hazard; the callback still waits for both, and `close()`
|
|
384
426
|
* returns the server, exactly as Node's does.
|
|
385
427
|
*
|
|
428
|
+
* `drain:false` (every caller before WP-2, and every caller that passes no options) is today's
|
|
429
|
+
* behaviour byte for byte: admission closed, sockets destroyed. `drain:true` is the handoff's
|
|
430
|
+
* stop-ACCEPTING half — the listener releases its listening socket and every accepted connection keeps
|
|
431
|
+
* serving; the predecessor exits once the counter reaches zero, or at the drain deadline (§5 D-7 step 7).
|
|
432
|
+
*
|
|
386
433
|
* @param {import('http').Server} server
|
|
387
|
-
* @param {{close:Function, closeHttpAdmission:Function}} listener
|
|
434
|
+
* @param {{close:Function, closeHttpAdmission:Function, stopAccepting?:Function}} listener
|
|
435
|
+
* @param {{drain?:boolean}} [options]
|
|
388
436
|
* @returns {import('http').Server} the same server
|
|
389
437
|
*/
|
|
390
|
-
function wrapHttpServerClose(server, listener) {
|
|
391
|
-
const originalClose = server.close;
|
|
438
|
+
function wrapHttpServerClose(server, listener, { drain = false } = {}) {
|
|
439
|
+
const originalClose = server[kOriginalServerClose] || server.close;
|
|
440
|
+
server[kOriginalServerClose] = originalClose;
|
|
392
441
|
server.close = function close(callback) {
|
|
393
|
-
|
|
394
|
-
|
|
442
|
+
let nativeDone;
|
|
443
|
+
if (drain && typeof listener.stopAccepting === 'function') {
|
|
444
|
+
nativeDone = listener.stopAccepting();
|
|
445
|
+
} else {
|
|
446
|
+
listener.closeHttpAdmission();
|
|
447
|
+
nativeDone = listener.close();
|
|
448
|
+
}
|
|
395
449
|
const httpDone = new Promise((resolve) => {
|
|
396
450
|
try {
|
|
397
451
|
originalClose.call(this, () => resolve());
|