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
|
@@ -8,6 +8,9 @@ function listPurgedDraftAttachmentIds(dbHandle) {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
const path = require('path');
|
|
11
|
+
// WP-6 (session-name-agent-title-precedence §6): the single tier-2 agent-title apply, reused by the
|
|
12
|
+
// one-shot backfill (applyAgentTitleBackfillRows below) exactly as the ingest path uses it.
|
|
13
|
+
const { applyAgentTitleSignals } = require('./lib/agent-title-apply');
|
|
11
14
|
const { createLegacyTerminalDraftCreate } = require('./lib/terminal-draft-create');
|
|
12
15
|
// WP-1 (§6, §6.1): the shared canonical draft lifecycle contract — lifecycle-version reader,
|
|
13
16
|
// complete canonical field projection + comparator, and the single version allocator. Shared with
|
|
@@ -43,6 +46,8 @@ const conversationStoreV2 = require('./lib/conversation-store-v2');
|
|
|
43
46
|
const walleHistoryCutoverV2 = require('./lib/walle-history-cutover-v2');
|
|
44
47
|
const { createReviewReposStore } = require('./lib/storage/review-repos-store');
|
|
45
48
|
const { insertResumeLineage } = require('./lib/storage/resume-lineage-store');
|
|
49
|
+
// WP-4 (§9 S-21): workspace_bindings.git_remote stores the NORMALIZED key, never a raw remote URL.
|
|
50
|
+
const { normalizeGitRemote } = require('../shared/git-remote-key');
|
|
46
51
|
|
|
47
52
|
const { execFileSync } = require('child_process');
|
|
48
53
|
const { normalizeAgentType } = require('./lib/agent-capabilities');
|
|
@@ -122,7 +127,7 @@ const {
|
|
|
122
127
|
ensureSessionModelIntentSchema,
|
|
123
128
|
} = require('./lib/migrations/v53-session-model-intent');
|
|
124
129
|
const { createCodingSessionGenerationStore } = require('./lib/storage/coding-session-generation-store');
|
|
125
|
-
const { createLeadModelTransitionStore } = require('./lib/lead-model-transition-store');
|
|
130
|
+
const { createLeadModelTransitionStore, resumesCandidateFork } = require('./lib/lead-model-transition-store');
|
|
126
131
|
const { createSessionModelIntentStore } = require('./lib/storage/session-model-intent-store');
|
|
127
132
|
const { createSessionLeadEffortStore } = require('./lib/storage/session-lead-effort-store');
|
|
128
133
|
const { createSessionLeadModelStore } = require('./lib/storage/session-lead-model-store');
|
|
@@ -207,7 +212,9 @@ const {
|
|
|
207
212
|
ensureHostIdentity,
|
|
208
213
|
identityFilePath,
|
|
209
214
|
isSameOwnerLineage: _isSameOwnerLineagePure,
|
|
215
|
+
lineageCrossHost: _lineageCrossHost,
|
|
210
216
|
ownerLineageComponent: _ownerLineageComponentPure,
|
|
217
|
+
readOsMachineIdHash,
|
|
211
218
|
} = require('./lib/host-identity');
|
|
212
219
|
const {
|
|
213
220
|
ensureResourceLeaseSchema,
|
|
@@ -350,8 +357,9 @@ let hostedIdentityState = null;
|
|
|
350
357
|
// import, clone/re-key handling) ever needs to change the instance identity WHILE the process is
|
|
351
358
|
// already serving traffic, it MUST also null out hostedIdentitySnapshot right there, or every
|
|
352
359
|
// caller downstream of getOrCreateInstanceIdentity silently keeps serving the stale identity for
|
|
353
|
-
// the rest of the process's life.
|
|
354
|
-
//
|
|
360
|
+
// the rest of the process's life. NOTHING test-enforces this today: the grep-based
|
|
361
|
+
// tests/instance-identity-snapshot-invariant.test.js was deleted on 2026-09-15
|
|
362
|
+
// (tests/.deflake/wp5-deletions-2026-09-15.txt) -- a new writer/reset site must be reviewed by hand.
|
|
355
363
|
let hostedIdentitySnapshot = null;
|
|
356
364
|
|
|
357
365
|
// WP-0 (docs/instance-identity-boot-order design, C3): lib/instance-identity-access.js keeps its
|
|
@@ -1219,6 +1227,10 @@ function _ensureHostedPrimarySchema(d = getDb()) {
|
|
|
1219
1227
|
|
|
1220
1228
|
_invalidateTableExistsMemo(); // R4-10: the exec above may have created the sync/instance tables
|
|
1221
1229
|
_ensureSyncedEntitiesOriginKey(target);
|
|
1230
|
+
// WP-0 (remote-machine-hub-sessions-2026-10-22.html §6.1): the hub-side machine registry.
|
|
1231
|
+
// The DDL body lives in _ensureMachineRegistrySchema (defined further down this file, next to
|
|
1232
|
+
// the other schema-ensure helpers) for the reason stated there.
|
|
1233
|
+
_ensureMachineRegistrySchema(target);
|
|
1222
1234
|
for (const tableName of ['ctm_sessions', 'prompts', 'folders']) {
|
|
1223
1235
|
_safeAlterAddColumn(tableName, "origin_instance_id TEXT DEFAULT ''");
|
|
1224
1236
|
_safeAlterAddColumn(tableName, "current_owner_instance_id TEXT DEFAULT ''");
|
|
@@ -1234,6 +1246,25 @@ function _ensureHostedPrimarySchema(d = getDb()) {
|
|
|
1234
1246
|
_safeAlterAddColumn('startup_tasks', "current_owner_instance_id TEXT DEFAULT ''");
|
|
1235
1247
|
_safeAlterAddColumn('startup_tasks', "parked_at TEXT DEFAULT ''");
|
|
1236
1248
|
_safeAlterAddColumn('startup_tasks', "park_reason TEXT DEFAULT ''");
|
|
1249
|
+
// WP-2 (docs/remote-hub-ownership-fences-and-restore-skip-diagnostics-2026-10-23.html §5 D-3,
|
|
1250
|
+
// §6, §9 S-5): was this re-mint edge minted ACROSS a host boundary?
|
|
1251
|
+
// 1 — the prior instance_identity row's stored host_fingerprint was non-empty and was NOT
|
|
1252
|
+
// in this host's live ∪ compatible fingerprint set at detection (_hostIdentityFingerprints,
|
|
1253
|
+
// below): the DATABASE FILE itself arrived from another machine (the 2026-10-14 Spark
|
|
1254
|
+
// file-clone whose re-mint rewrote 3779 Mac-owned rows to Spark-owned).
|
|
1255
|
+
// 0 — the stored fingerprint matches this host: a legitimate same-host re-mint (the Mac's
|
|
1256
|
+
// own 2026-09-30 ctm-LLXYK6XTNF-3456 → …-1a2c9c), or no stored fingerprint at all
|
|
1257
|
+
// (empty ⇒ cannot prove foreign ⇒ fail toward today's behavior, AC-2c).
|
|
1258
|
+
// NULL — legacy edge minted before this column existed. Treated exactly like 0 everywhere
|
|
1259
|
+
// (restampOwnerLineage only refuses to traverse a literal 1), so pre-existing installs
|
|
1260
|
+
// keep today's claiming behavior.
|
|
1261
|
+
// The ALTER lives HERE, in the hosted-schema block, and deliberately NOT in a new
|
|
1262
|
+
// lib/migrations/vNN file: instance_lineage is created by the DDL above (not by the versioned
|
|
1263
|
+
// ladder), and a post-v18 ALTER against an enrolled table breaks the rollback tests
|
|
1264
|
+
// (memory: ctm-domain-split-post-v18-alter-on-enrolled-table-breaks-rollback-tests).
|
|
1265
|
+
_safeAlterAddColumn('instance_lineage', 'cross_host INTEGER');
|
|
1266
|
+
// WP-B §5 D-B1: the hashed OS machine id of the boot that wrote this row — the cross-host verdict's primary fact.
|
|
1267
|
+
_safeAlterAddColumn('instance_identity', "machine_id TEXT NOT NULL DEFAULT ''");
|
|
1237
1268
|
_safeAlterAddColumn('ctm_sessions', "continued_from_session_id TEXT DEFAULT ''");
|
|
1238
1269
|
_safeAlterAddColumn('ctm_sessions', "handoff_target_session_id TEXT DEFAULT ''");
|
|
1239
1270
|
// WP-1 (design doc §5/§8/§9 S-19): last known physical-footprint sample, persisted at
|
|
@@ -1284,6 +1315,9 @@ function _hostIdentityDataDir() {
|
|
|
1284
1315
|
return path.dirname(currentDbPath || DEFAULT_DB_PATH);
|
|
1285
1316
|
}
|
|
1286
1317
|
|
|
1318
|
+
let _osMachineIdHashMemo = null; // WP-B §9 B-4: OS machine id read ONCE per process ('' memoized too); tests inject `machineId`
|
|
1319
|
+
function _osMachineIdHashOnce() { if (_osMachineIdHashMemo === null) _osMachineIdHashMemo = readOsMachineIdHash(); return _osMachineIdHashMemo; }
|
|
1320
|
+
|
|
1287
1321
|
function _hostIdentityFingerprints(options = {}) {
|
|
1288
1322
|
const hostname = options.hostname || _identityHostname();
|
|
1289
1323
|
const port = options.port || process.env.CTM_PORT || '';
|
|
@@ -1320,6 +1354,7 @@ function _hostIdentityFingerprints(options = {}) {
|
|
|
1320
1354
|
|
|
1321
1355
|
function _recordInstanceLineage(d, lineage) {
|
|
1322
1356
|
if (!lineage || !lineage.priorInstanceId || !lineage.newInstanceId) return null;
|
|
1357
|
+
const crossHost = Number(lineage.crossHost) === 1 ? 1 : 0;
|
|
1323
1358
|
const existing = d.prepare(`
|
|
1324
1359
|
SELECT * FROM instance_lineage
|
|
1325
1360
|
WHERE prior_instance_id = ? AND new_instance_id = ? AND reason = ?
|
|
@@ -1327,17 +1362,28 @@ function _recordInstanceLineage(d, lineage) {
|
|
|
1327
1362
|
`).get(lineage.priorInstanceId, lineage.newInstanceId, lineage.reason || 'clone_detected');
|
|
1328
1363
|
if (existing) return existing;
|
|
1329
1364
|
d.prepare(`
|
|
1330
|
-
INSERT INTO instance_lineage (prior_instance_id, new_instance_id, reason, detected_at)
|
|
1331
|
-
VALUES (?, ?, ?, ?)
|
|
1365
|
+
INSERT INTO instance_lineage (prior_instance_id, new_instance_id, reason, detected_at, cross_host)
|
|
1366
|
+
VALUES (?, ?, ?, ?, ?)
|
|
1332
1367
|
`).run(
|
|
1333
1368
|
lineage.priorInstanceId,
|
|
1334
1369
|
lineage.newInstanceId,
|
|
1335
1370
|
lineage.reason || 'clone_detected',
|
|
1336
|
-
lineage.detectedAt || new Date().toISOString()
|
|
1371
|
+
lineage.detectedAt || new Date().toISOString(),
|
|
1372
|
+
crossHost
|
|
1337
1373
|
);
|
|
1338
1374
|
return d.prepare('SELECT * FROM instance_lineage WHERE rowid = last_insert_rowid()').get();
|
|
1339
1375
|
}
|
|
1340
1376
|
|
|
1377
|
+
// WP-C §9 C-2: both read wrappers below must see each edge's cross_host verdict, or they would
|
|
1378
|
+
// keep traversing a cloned-DB edge and treat the source machine's ids as this host's own (design
|
|
1379
|
+
// §1.3 C-1..C-9). The column is added by the hosted-schema ALTER block, so it exists on any DB
|
|
1380
|
+
// this build has booted; a legacy schema without it reads no key, which the pure closure treats
|
|
1381
|
+
// as NULL — traversable — i.e. exactly today's behavior.
|
|
1382
|
+
function _ownerLineageReadRows() {
|
|
1383
|
+
const crossHost = _tableHasColumn('instance_lineage', 'cross_host') ? ', cross_host' : '';
|
|
1384
|
+
return getDomainDb('core').prepare(`SELECT prior_instance_id, new_instance_id${crossHost} FROM instance_lineage`).all();
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1341
1387
|
// WP-3 (docs/ctm-session-restore-loss-and-render-wedge-2026-07-26.html) — DB-backed wrapper
|
|
1342
1388
|
// around lib/host-identity.js's pure isSameOwnerLineage: reads the full instance_lineage table
|
|
1343
1389
|
// (small, control-plane, per BASE_RESIDENT-adjacent core classification — never large) and
|
|
@@ -1347,8 +1393,7 @@ function _recordInstanceLineage(d, lineage) {
|
|
|
1347
1393
|
function isSameOwnerLineage(candidateId, currentId) {
|
|
1348
1394
|
const current = String(currentId || _currentHostedInstanceId() || '').trim();
|
|
1349
1395
|
if (!_tableExists('instance_lineage')) return String(candidateId || '').trim() === current;
|
|
1350
|
-
|
|
1351
|
-
return _isSameOwnerLineagePure(candidateId, current, rows);
|
|
1396
|
+
return _isSameOwnerLineagePure(candidateId, current, _ownerLineageReadRows());
|
|
1352
1397
|
}
|
|
1353
1398
|
|
|
1354
1399
|
// WP-3: returns the full lineage CONNECTED COMPONENT for `currentId` (every instance id
|
|
@@ -1361,8 +1406,7 @@ function getOwnerLineageComponent(currentId) {
|
|
|
1361
1406
|
const current = String(currentId || _currentHostedInstanceId() || '').trim();
|
|
1362
1407
|
if (!current) return [];
|
|
1363
1408
|
if (!_tableExists('instance_lineage')) return [current];
|
|
1364
|
-
|
|
1365
|
-
return [..._ownerLineageComponentPure(current, rows)];
|
|
1409
|
+
return [..._ownerLineageComponentPure(current, _ownerLineageReadRows())];
|
|
1366
1410
|
}
|
|
1367
1411
|
|
|
1368
1412
|
// WP-3 re-stamp migration (docs/ctm-session-restore-loss-and-render-wedge-2026-07-26.html):
|
|
@@ -1383,9 +1427,9 @@ const _RESTAMP_EMPTY_RESULT = Object.freeze({
|
|
|
1383
1427
|
ctm_sessions: 0,
|
|
1384
1428
|
startup_tasks: 0,
|
|
1385
1429
|
ctm_sessions_execution_host: 0,
|
|
1386
|
-
// WP-5 (docs/instance-identity-boot-order-2026-08-08.html S-8/S-9/S-10):
|
|
1387
|
-
//
|
|
1388
|
-
|
|
1430
|
+
// WP-5 (docs/instance-identity-boot-order-2026-08-08.html S-8/S-9/S-10): more live ownership
|
|
1431
|
+
// pointers the census found unrestamped. Drafts are not one of them: the media-store allocator
|
|
1432
|
+
// normalizes their owner at boot, never this transaction (R2 OQ-4 removed the dead counter).
|
|
1389
1433
|
workspace_bindings: 0,
|
|
1390
1434
|
workspace_bindings_conflicts_resolved: 0,
|
|
1391
1435
|
startup_tasks_ctm_instance: 0,
|
|
@@ -1395,28 +1439,39 @@ const _RESTAMP_EMPTY_RESULT = Object.freeze({
|
|
|
1395
1439
|
resource_leases: 0,
|
|
1396
1440
|
});
|
|
1397
1441
|
|
|
1442
|
+
// WP-C §5 D-C2: the one closure (shared/host-identity.js ownerLineageComponent) is called twice
|
|
1443
|
+
// here — with the default options for the set this host may claim, and with
|
|
1444
|
+
// `{ traverseCrossHost: true }` to discover the ids that hang off the current host's component
|
|
1445
|
+
// ONLY across a cloned-DB edge. Those are foreign mirrors: rows they own belong to another
|
|
1446
|
+
// machine and are never claimed (P-2). The private cross-host-only twin this replaces is gone —
|
|
1447
|
+
// its one extra predicate now lives in the shared closure above (REPLACE IN PLACE).
|
|
1398
1448
|
function restampOwnerLineage(options = {}) {
|
|
1399
1449
|
if (process.env.CTM_OWNER_LINEAGE_RESTAMP === '0') return { ok: false, skipped: true, reason: 'disabled' };
|
|
1400
1450
|
const currentId = String(options.currentId || _currentHostedInstanceId() || '').trim();
|
|
1401
1451
|
if (!currentId) return { ok: false, skipped: true, reason: 'no-current-identity' };
|
|
1402
|
-
if (!_tableExists('instance_lineage'))
|
|
1403
|
-
|
|
1404
|
-
const lineageRows = d.prepare('SELECT prior_instance_id, new_instance_id FROM instance_lineage').all();
|
|
1405
|
-
// Every DISTINCT owner id currently on record that is lineage-linked to currentId (excluding
|
|
1406
|
-
// currentId itself — nothing to restamp there). Bounded by the lineage graph's own size, never
|
|
1407
|
-
// a full table scan of distinct owner ids beyond what instance_lineage actually references.
|
|
1408
|
-
const candidateIds = new Set();
|
|
1409
|
-
for (const row of lineageRows) {
|
|
1410
|
-
for (const id of [row.prior_instance_id, row.new_instance_id]) {
|
|
1411
|
-
const clean = String(id || '').trim();
|
|
1412
|
-
if (clean && clean !== currentId) candidateIds.add(clean);
|
|
1413
|
-
}
|
|
1452
|
+
if (!_tableExists('instance_lineage')) {
|
|
1453
|
+
return { ok: true, restamped: { ..._RESTAMP_EMPTY_RESULT }, foreignParked: { startup_tasks: 0, ctm_sessions: 0 } };
|
|
1414
1454
|
}
|
|
1415
|
-
const
|
|
1455
|
+
const d = getDomainDb('core');
|
|
1456
|
+
const lineageRows = _ownerLineageReadRows();
|
|
1457
|
+
// Every owner id transitively lineage-linked to currentId (excluding currentId itself — nothing
|
|
1458
|
+
// to restamp there). Bounded by the lineage graph's own size, never a full table scan of distinct
|
|
1459
|
+
// owner ids beyond what instance_lineage actually references.
|
|
1460
|
+
const lineageLinked = [..._ownerLineageComponentPure(currentId, lineageRows, { traverseCrossHost: true })]
|
|
1461
|
+
.filter((id) => id !== currentId);
|
|
1416
1462
|
const result = { ..._RESTAMP_EMPTY_RESULT };
|
|
1417
|
-
|
|
1463
|
+
// WP-2 §5 D-3 / §9 S-6/S-8: split the lineage-linked owners by WHICH EDGES connect them to
|
|
1464
|
+
// currentId. `claimable` reached currentId without crossing a cross_host = 1 edge (this host's
|
|
1465
|
+
// own history, including a live same-host re-mint) — the legacy claim set. `foreign` reached it
|
|
1466
|
+
// only across a cloned-DB edge: rows they own belong to another machine and are MIRRORED, never
|
|
1467
|
+
// claimed (P-2: foreign state is a mirror, never a peer write).
|
|
1468
|
+
const sameHostComponent = _ownerLineageComponentPure(currentId, lineageRows);
|
|
1469
|
+
const claimable = lineageLinked.filter((id) => sameHostComponent.has(id));
|
|
1470
|
+
const foreign = lineageLinked.filter((id) => !sameHostComponent.has(id));
|
|
1471
|
+
const foreignParked = { startup_tasks: 0, ctm_sessions: 0 };
|
|
1472
|
+
if (!lineageLinked.length) return { ok: true, restamped: result, foreignParked };
|
|
1418
1473
|
const txn = d.transaction(() => {
|
|
1419
|
-
for (const staleId of
|
|
1474
|
+
for (const staleId of claimable) {
|
|
1420
1475
|
if (_tableHasColumn('ctm_sessions', 'current_owner_instance_id')) {
|
|
1421
1476
|
result.ctm_sessions += d.prepare(
|
|
1422
1477
|
'UPDATE ctm_sessions SET current_owner_instance_id = ? WHERE current_owner_instance_id = ?'
|
|
@@ -1483,15 +1538,46 @@ function restampOwnerLineage(options = {}) {
|
|
|
1483
1538
|
).run(currentId, staleId).changes || 0;
|
|
1484
1539
|
}
|
|
1485
1540
|
}
|
|
1541
|
+
// WP-2 §5 D-3 / §9 S-6: the rows owned by the FOREIGN ids are mirrored, not claimed — in the
|
|
1542
|
+
// SAME transaction, so a boot can never observe half a decision. The owner column stays
|
|
1543
|
+
// untouched on every table (P-2: foreign state is a mirror; ownership leaves a host only via
|
|
1544
|
+
// the explicit transfer path, design R-14), and startup_tasks gets the established parked
|
|
1545
|
+
// representation (status='parked' + park_reason='foreign-clone'), the same shape
|
|
1546
|
+
// lib/remote-machine-catalog.js reads as "exists, owned elsewhere" and _startupTaskRestorable
|
|
1547
|
+
// already refuses to restore. Idempotent: the != 'foreign-clone' predicate makes a second boot
|
|
1548
|
+
// over the same cloned DB park nothing new, and an already-'exited' row is left alone (its
|
|
1549
|
+
// bookkeeping is terminal).
|
|
1550
|
+
//
|
|
1551
|
+
// ctm_sessions has NO status/park_reason columns (verified with PRAGMA table_info on a temp-db
|
|
1552
|
+
// clone built by this file's own schema: it carries sync_status and parked_at but no park
|
|
1553
|
+
// vocabulary — the hosted ALTER block above never adds one), so there is no park
|
|
1554
|
+
// representation to write there; leaving its owner untouched IS the fence (no claim, no
|
|
1555
|
+
// park), and foreignParked.ctm_sessions stays 0 rather than inventing a second representation.
|
|
1556
|
+
if (foreign.length && _tableHasColumn('startup_tasks', 'status')
|
|
1557
|
+
&& _tableHasColumn('startup_tasks', 'park_reason') && _tableHasColumn('startup_tasks', 'parked_at')) {
|
|
1558
|
+
const parkedAt = new Date().toISOString();
|
|
1559
|
+
for (const foreignId of foreign) {
|
|
1560
|
+
foreignParked.startup_tasks += d.prepare(`
|
|
1561
|
+
UPDATE startup_tasks
|
|
1562
|
+
SET status = 'parked', park_reason = 'foreign-clone', parked_at = ?
|
|
1563
|
+
WHERE current_owner_instance_id = ?
|
|
1564
|
+
AND COALESCE(status, 'running') != 'exited'
|
|
1565
|
+
AND COALESCE(park_reason, '') != 'foreign-clone'
|
|
1566
|
+
`).run(parkedAt, foreignId).changes || 0;
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1486
1569
|
});
|
|
1487
1570
|
txn();
|
|
1488
1571
|
const total = result.ctm_sessions + result.startup_tasks + result.ctm_sessions_execution_host
|
|
1489
|
-
+ result.
|
|
1572
|
+
+ result.workspace_bindings + result.workspace_bindings_conflicts_resolved
|
|
1490
1573
|
+ result.startup_tasks_ctm_instance + result.resource_leases;
|
|
1491
1574
|
if (total > 0) {
|
|
1492
|
-
console.log(`[owner-lineage-restamp] restamped ${total} row(s) to ${currentId} (ctm_sessions=${result.ctm_sessions}, startup_tasks=${result.startup_tasks}, ctm_sessions_execution_host=${result.ctm_sessions_execution_host},
|
|
1575
|
+
console.log(`[owner-lineage-restamp] restamped ${total} row(s) to ${currentId} (ctm_sessions=${result.ctm_sessions}, startup_tasks=${result.startup_tasks}, ctm_sessions_execution_host=${result.ctm_sessions_execution_host}, workspace_bindings=${result.workspace_bindings}, workspace_bindings_conflicts_resolved=${result.workspace_bindings_conflicts_resolved}, startup_tasks_ctm_instance=${result.startup_tasks_ctm_instance}, resource_leases=${result.resource_leases})`);
|
|
1493
1576
|
}
|
|
1494
|
-
|
|
1577
|
+
if (foreignParked.startup_tasks > 0) {
|
|
1578
|
+
console.log(`[owner-lineage-restamp] parked ${foreignParked.startup_tasks} foreign row(s) as park_reason='foreign-clone' (owner preserved) — ${foreign.length} id(s) reached ${currentId} only across a cross-host lineage edge`);
|
|
1579
|
+
}
|
|
1580
|
+
return { ok: true, restamped: result, foreignParked };
|
|
1495
1581
|
}
|
|
1496
1582
|
|
|
1497
1583
|
// Explicit, idempotent retirement utility for an auto-learned approval rule. Callers must
|
|
@@ -1558,21 +1644,28 @@ function _latestInstanceLineage(d, instanceId) {
|
|
|
1558
1644
|
reason: row.reason || '',
|
|
1559
1645
|
detectedAt: row.detected_at || '',
|
|
1560
1646
|
detected_at: row.detected_at || '',
|
|
1647
|
+
// WP-2 §5 D-3: read the STORED verdict — never recomputed at read time (the prior identity
|
|
1648
|
+
// row this was derived from no longer exists). NULL = legacy edge (pre-WP-2), reported as 0
|
|
1649
|
+
// so consumers see the same "same host" default restampOwnerLineage applies.
|
|
1650
|
+
crossHost: Number(row.cross_host) === 1 ? 1 : 0,
|
|
1651
|
+
cross_host: Number(row.cross_host) === 1 ? 1 : 0,
|
|
1561
1652
|
};
|
|
1562
1653
|
}
|
|
1563
1654
|
|
|
1564
|
-
function _replaceInstanceIdentityRow(d, identity, source) {
|
|
1655
|
+
function _replaceInstanceIdentityRow(d, identity, source, machineId = '') {
|
|
1656
|
+
const hasMachineId = _tableHasColumn('instance_identity', 'machine_id');
|
|
1565
1657
|
d.prepare('DELETE FROM instance_identity').run();
|
|
1566
1658
|
d.prepare(`
|
|
1567
|
-
INSERT INTO instance_identity (instance_id, created_at, host_fingerprint, source)
|
|
1568
|
-
VALUES (?, ?, ?, ?)
|
|
1659
|
+
INSERT INTO instance_identity (instance_id, created_at, host_fingerprint, source${hasMachineId ? ', machine_id' : ''})
|
|
1660
|
+
VALUES (?, ?, ?, ?${hasMachineId ? ', ?' : ''})
|
|
1569
1661
|
`).run(
|
|
1570
1662
|
identity.instanceId,
|
|
1571
1663
|
identity.createdAt || new Date().toISOString(),
|
|
1572
1664
|
identity.hostFingerprint || '',
|
|
1573
|
-
source || identity.source || 'generated'
|
|
1574
|
-
|
|
1575
|
-
|
|
1665
|
+
source || identity.source || 'generated',
|
|
1666
|
+
...(hasMachineId ? [String(machineId || '')] : []));
|
|
1667
|
+
// WP-B D-B3: machine_id is not selected — this row spreads into the boot identity state & manifest.
|
|
1668
|
+
return d.prepare('SELECT instance_id, created_at, host_fingerprint, source FROM instance_identity WHERE instance_id = ?').get(identity.instanceId);
|
|
1576
1669
|
}
|
|
1577
1670
|
|
|
1578
1671
|
function _backfillHostedIdentityProvenance(d, state) {
|
|
@@ -1653,6 +1746,7 @@ function getOrCreateInstanceIdentity(options = {}) {
|
|
|
1653
1746
|
const hostname = options.hostname || _identityHostname();
|
|
1654
1747
|
const port = options.port || process.env.CTM_PORT || '';
|
|
1655
1748
|
const fingerprints = _hostIdentityFingerprints({ hostname, port });
|
|
1749
|
+
const machineId = options.machineId ?? _osMachineIdHashOnce(); // WP-B D-B2: the verdict's primary fact
|
|
1656
1750
|
const existing = d.prepare('SELECT * FROM instance_identity ORDER BY created_at LIMIT 1').get();
|
|
1657
1751
|
const ensured = ensureHostIdentity({
|
|
1658
1752
|
dataDir: _hostIdentityDataDir(),
|
|
@@ -1684,8 +1778,15 @@ function getOrCreateInstanceIdentity(options = {}) {
|
|
|
1684
1778
|
console.log(mintLog);
|
|
1685
1779
|
}
|
|
1686
1780
|
}
|
|
1687
|
-
const row = _replaceInstanceIdentityRow(d, identity, source);
|
|
1688
|
-
|
|
1781
|
+
const row = _replaceInstanceIdentityRow(d, identity, source, machineId);
|
|
1782
|
+
// WP-2 §5 D-3 / §9 S-5 / WP-B D-B2: stamp whether THIS re-mint edge crosses a host boundary,
|
|
1783
|
+
// computed from the prior instance_identity row (`existing`, read above and churned by the line
|
|
1784
|
+
// just before) plus the live OS machine id (WP-B: it decides alone when both sides know theirs),
|
|
1785
|
+
// falling back to this host's fingerprint set (`fingerprints`, :1308-1325) when either is unknown.
|
|
1786
|
+
// After _replaceInstanceIdentityRow the prior row is gone, so the verdict is stored (AC-2c).
|
|
1787
|
+
const lineageRow = _recordInstanceLineage(d, ensured.lineage
|
|
1788
|
+
? { ...ensured.lineage, crossHost: _lineageCrossHost(existing, fingerprints, (priorPort) => _hostIdentityFingerprints({ hostname, port: priorPort }), machineId) }
|
|
1789
|
+
: ensured.lineage);
|
|
1689
1790
|
const backfill = _backfillHostedIdentityProvenance(d, ensured);
|
|
1690
1791
|
const lineage = ensured.lineage
|
|
1691
1792
|
? _latestInstanceLineage(d, identity.instanceId)
|
|
@@ -1763,7 +1864,7 @@ function upsertHostedWorkspaceBinding(binding = {}) {
|
|
|
1763
1864
|
const logicalId = _hostedToken(binding.logicalId || binding.logical_id || binding.id || '', '');
|
|
1764
1865
|
const hostInstanceId = _hostedToken(binding.hostInstanceId || binding.host_instance_id || binding.currentOwnerInstanceId || '', '');
|
|
1765
1866
|
const hostPath = String(binding.hostPath || binding.host_path || binding.path || '').trim();
|
|
1766
|
-
const gitRemote =
|
|
1867
|
+
const gitRemote = normalizeGitRemote(binding.gitRemote || binding.git_remote || binding.repoRemote || '');
|
|
1767
1868
|
if (!logicalId) throw new Error('logical_id required');
|
|
1768
1869
|
if (!hostInstanceId) throw new Error('host_instance_id required');
|
|
1769
1870
|
if (!hostPath) throw new Error('host_path required');
|
|
@@ -1799,7 +1900,7 @@ function seedHostedWorkspaceBindings(bindings = [], instanceId = '') {
|
|
|
1799
1900
|
const hostPath = String(binding.hostPath || binding.host_path || '').trim();
|
|
1800
1901
|
const logicalId = _hostedToken(binding.logicalId || binding.logical_id || binding.id || '', '');
|
|
1801
1902
|
if (!hostPath || !logicalId) continue;
|
|
1802
|
-
stmt.run(logicalId, id, hostPath,
|
|
1903
|
+
stmt.run(logicalId, id, hostPath, normalizeGitRemote(binding.gitRemote || binding.git_remote || binding.repoRemote || ''));
|
|
1803
1904
|
count += 1;
|
|
1804
1905
|
}
|
|
1805
1906
|
});
|
|
@@ -3326,22 +3427,15 @@ function applySyncSnapshot(options = {}) {
|
|
|
3326
3427
|
return value;
|
|
3327
3428
|
}
|
|
3328
3429
|
|
|
3430
|
+
// S-19 (remote-machine-hub-sessions-2026-10-22.html §9): this used to carry its own copy of
|
|
3431
|
+
// the CTM_SYNC_PEERS grammar (JSON array vs `id=url` CSV) — for every caller that read it and
|
|
3432
|
+
// for lib/hosted-sync-worker.js#parseSyncPeers, which parses the same env. One parser now:
|
|
3433
|
+
// the sync worker's, which the machines registry also seeds from. (The old copy additionally
|
|
3434
|
+
// ignored CTM_SYNC_PEER_ALLOWLIST; nothing depends on that difference — this helper has no
|
|
3435
|
+
// callers in the tree today, and the allowlist-filtered set is the correct peer list.)
|
|
3329
3436
|
function _syncConfiguredPeerIds(env = process.env) {
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
if (raw.startsWith('[')) {
|
|
3333
|
-
try {
|
|
3334
|
-
const parsed = JSON.parse(raw);
|
|
3335
|
-
if (Array.isArray(parsed)) {
|
|
3336
|
-
return parsed.map((entry) => _hostedToken(entry.instanceId || entry.instance_id || entry.id || entry.name || '', '')).filter(Boolean);
|
|
3337
|
-
}
|
|
3338
|
-
} catch {}
|
|
3339
|
-
}
|
|
3340
|
-
return raw.split(',')
|
|
3341
|
-
.map((part) => {
|
|
3342
|
-
const [id, ...rest] = part.split('=');
|
|
3343
|
-
return rest.length ? _hostedToken(id, '') : '';
|
|
3344
|
-
})
|
|
3437
|
+
return require('./lib/hosted-sync-worker').parseSyncPeers(env)
|
|
3438
|
+
.map((peer) => _hostedToken(peer.instanceId, ''))
|
|
3345
3439
|
.filter(Boolean);
|
|
3346
3440
|
}
|
|
3347
3441
|
|
|
@@ -4174,7 +4268,7 @@ function importHostedMigrationPack(pack = {}, options = {}) {
|
|
|
4174
4268
|
host_path = excluded.host_path,
|
|
4175
4269
|
git_remote = excluded.git_remote,
|
|
4176
4270
|
updated_at = datetime('now')
|
|
4177
|
-
`).run(logicalId, targetInstanceId, hostPath,
|
|
4271
|
+
`).run(logicalId, targetInstanceId, hostPath, normalizeGitRemote(binding.gitRemote || binding.git_remote || ''));
|
|
4178
4272
|
bindingsImported += 1;
|
|
4179
4273
|
}
|
|
4180
4274
|
for (const task of tasks) {
|
|
@@ -4689,7 +4783,6 @@ function _openDbConnection(dbPath) {
|
|
|
4689
4783
|
label: 'ctm-task-manager',
|
|
4690
4784
|
mainWriteInventory: isMainThread,
|
|
4691
4785
|
timeoutMs: _sqliteTimeoutMs('CTM_SQLITE_WRITE_LOCK_TIMEOUT_MS', 0),
|
|
4692
|
-
staleMs: Number(process.env.CTM_SQLITE_WRITE_LOCK_STALE_MS || 10 * 60 * 1000),
|
|
4693
4786
|
// Phase 3 fairness: the db-owner worker (a background thread that waits up to
|
|
4694
4787
|
// 5s) polls tightly so it reliably claims the lock the instant the main thread
|
|
4695
4788
|
// releases, instead of starving behind a burst of fast main-thread writes. The
|
|
@@ -7474,8 +7567,8 @@ function _defaultMigrationSteps() {
|
|
|
7474
7567
|
// v53 (WP-2A): the approved session-model-intent table. Schema-only + a one-time row
|
|
7475
7568
|
// integrity pass; it creates no other table, seeds nothing and stamps only after both hold.
|
|
7476
7569
|
53: () => _migrateToV53Impl({ getDb: () => dbFor('session_model_intent'), setSchemaVersion }),
|
|
7477
|
-
// v54 (WP-0) the per-session lead model pin — an explicit model choice replayed on resume; v55 (WP-2, multi-machine) stamps blank agent_sessions.origin_instance_id rows with this database's identity (one idempotent UPDATE, sharing this line because db.js is at its size ceiling).
|
|
7478
|
-
54: () => _migrateToV54Impl({ getDb: () => dbFor('session_lead_model'), setSchemaVersion }), 55: () => require('./lib/migrations/v55-agent-origin-backfill').migrateToV55({ getDb: () => dbFor('agent_sessions'), getIdentityDb: () => dbFor('instance_identity'), setSchemaVersion }),
|
|
7570
|
+
// v54 (WP-0) the per-session lead model pin — an explicit model choice replayed on resume; v55 (WP-2, multi-machine) stamps blank agent_sessions.origin_instance_id rows with this database's identity (one idempotent UPDATE, sharing this line because db.js is at its size ceiling); v56 (WP-1, machine pool) is host_samples + machine_self + machine_pairing_offers + machines.urls_json, all domain 'core'.
|
|
7571
|
+
54: () => _migrateToV54Impl({ getDb: () => dbFor('session_lead_model'), setSchemaVersion }), 55: () => require('./lib/migrations/v55-agent-origin-backfill').migrateToV55({ getDb: () => dbFor('agent_sessions'), getIdentityDb: () => dbFor('instance_identity'), setSchemaVersion }), 56: () => require('./lib/migrations/v56-host-samples-and-machine-pool').migrateToV56({ getDb: () => dbFor('machines'), setSchemaVersion }),
|
|
7479
7572
|
}, { reservedVersions: [18] });
|
|
7480
7573
|
}
|
|
7481
7574
|
let _migrationSteps = _defaultMigrationSteps();
|
|
@@ -7991,6 +8084,366 @@ function _ensureSessionModelPreferenceRoutingColumns(d = getDb()) {
|
|
|
7991
8084
|
}
|
|
7992
8085
|
}
|
|
7993
8086
|
|
|
8087
|
+
// ---------------------------------------------------------------------------
|
|
8088
|
+
// WP-0 (remote-machine-hub-sessions-2026-10-22.html §6.1) — the hub-side machine registry:
|
|
8089
|
+
// the durable list of boxes this CTM can drive sessions on (lib/machine-registry.js owns the
|
|
8090
|
+
// policy; the SQL lives here, in the owner zone, like every other CTM table).
|
|
8091
|
+
//
|
|
8092
|
+
// WHY THE DDL SITS HERE RATHER THAN IN _ensureHostedPrimarySchema'S EXEC BLOB: it is called
|
|
8093
|
+
// from there (one line), but the CREATE TABLE literal itself must stay late in the file.
|
|
8094
|
+
// persistence-format surfaces are ORDINAL-keyed in release/ownership-registry.json
|
|
8095
|
+
// (`coding-task-manager/db.js:ddl#N`, N = the statement's position in file order), so putting
|
|
8096
|
+
// the statement in the schema blob would renumber every later DDL surface in db.js and force
|
|
8097
|
+
// ~230 unrelated registry re-registrations for one new table. The contract is identical — one
|
|
8098
|
+
// idempotent CREATE TABLE IF NOT EXISTS, reached from the same ensure pass as sync_*.
|
|
8099
|
+
//
|
|
8100
|
+
// DOMAIN: `machines` has no entry in lib/db-domain-tables.js yet (that file is outside this
|
|
8101
|
+
// work package), so the handles below resolve via getDomainDb('core') — the same flip-aware
|
|
8102
|
+
// CORE handle dbFor('machines') returns once the classification lands.
|
|
8103
|
+
function _machinesDb() {
|
|
8104
|
+
return getDomainDb('core');
|
|
8105
|
+
}
|
|
8106
|
+
|
|
8107
|
+
/** Idempotent: creates the hub machine registry (called from _ensureHostedPrimarySchema). */
|
|
8108
|
+
function _ensureMachineRegistrySchema(target = _machinesDb()) {
|
|
8109
|
+
target.exec(`
|
|
8110
|
+
CREATE TABLE IF NOT EXISTS machines (
|
|
8111
|
+
instance_id TEXT PRIMARY KEY, -- pinned minted id of the box (D-3)
|
|
8112
|
+
display_name TEXT NOT NULL DEFAULT '', -- user label, e.g. "spark"
|
|
8113
|
+
url TEXT NOT NULL, -- e.g. http://127.0.0.1:3556
|
|
8114
|
+
walle_url TEXT NOT NULL DEFAULT '', -- '' = derive (CTM port + 1), same rule as parseSyncPeers
|
|
8115
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
8116
|
+
last_seen_at INTEGER NOT NULL DEFAULT 0, -- hub clock, ms
|
|
8117
|
+
health TEXT NOT NULL DEFAULT 'unknown', -- reachable|unknown|unreachable|unhealthy
|
|
8118
|
+
health_detail TEXT NOT NULL DEFAULT '',
|
|
8119
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
8120
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
8121
|
+
urls_json TEXT NOT NULL DEFAULT '[]' -- rev 2 P-1/P-4: the member's dialable URLs, in dial order
|
|
8122
|
+
);
|
|
8123
|
+
`);
|
|
8124
|
+
_invalidateTableExistsMemo();
|
|
8125
|
+
return target;
|
|
8126
|
+
}
|
|
8127
|
+
|
|
8128
|
+
const MACHINE_ROW_COLUMNS = 'instance_id, display_name, url, walle_url, enabled, last_seen_at, health, health_detail, created_at, updated_at, urls_json';
|
|
8129
|
+
|
|
8130
|
+
function _machineRow(row = {}) {
|
|
8131
|
+
return {
|
|
8132
|
+
instanceId: String(row.instance_id || ''),
|
|
8133
|
+
displayName: String(row.display_name || ''),
|
|
8134
|
+
url: String(row.url || ''),
|
|
8135
|
+
walleUrl: String(row.walle_url || ''),
|
|
8136
|
+
enabled: Number(row.enabled || 0) || 0,
|
|
8137
|
+
lastSeenAt: Number(row.last_seen_at || 0) || 0,
|
|
8138
|
+
health: String(row.health || 'unknown'),
|
|
8139
|
+
healthDetail: String(row.health_detail || ''),
|
|
8140
|
+
createdAt: String(row.created_at || ''),
|
|
8141
|
+
updatedAt: String(row.updated_at || ''),
|
|
8142
|
+
urls: _machineUrls(row.urls_json),
|
|
8143
|
+
};
|
|
8144
|
+
}
|
|
8145
|
+
|
|
8146
|
+
/** P-1/P-4: `urls_json` is a JSON array of dialable URLs; bad JSON reads as [] (never throws). */
|
|
8147
|
+
function _machineUrls(value) {
|
|
8148
|
+
try { const parsed = JSON.parse(String(value || '[]')); return Array.isArray(parsed) ? parsed.map((u) => String(u || '')).filter(Boolean) : []; }
|
|
8149
|
+
catch { return []; } // expected: a hand-edited/garbled column must not break every machine list
|
|
8150
|
+
}
|
|
8151
|
+
|
|
8152
|
+
/** Read the hub machine registry. Pre-boot/pre-migration reads return [] rather than throwing. */
|
|
8153
|
+
function listMachineRows() {
|
|
8154
|
+
try {
|
|
8155
|
+
const d = _machinesDb();
|
|
8156
|
+
return d.prepare(`SELECT ${MACHINE_ROW_COLUMNS} FROM machines ORDER BY instance_id ASC`)
|
|
8157
|
+
.all()
|
|
8158
|
+
.map(_machineRow);
|
|
8159
|
+
} catch {
|
|
8160
|
+
return []; // expected: the machines table does not exist yet (pre-boot handle)
|
|
8161
|
+
}
|
|
8162
|
+
}
|
|
8163
|
+
|
|
8164
|
+
/**
|
|
8165
|
+
* Insert-or-update one machine. `enabled` is honoured on conflict: registration is an explicit
|
|
8166
|
+
* "use this box" act, seeding (lib/machine-registry.js) never calls this for a row it did not
|
|
8167
|
+
* just find missing.
|
|
8168
|
+
*
|
|
8169
|
+
* WP-3 P-4: an optional `urls` (dial list) is written to `urls_json`. It is written ONLY when the
|
|
8170
|
+
* caller supplies a non-empty list — `registerMachine` and the probe sweep never pass one, and they
|
|
8171
|
+
* must not blank the dial list a roster push (replaceRosterMachines) or a pairing (POST
|
|
8172
|
+
* /api/machines/pair) put there. The `CASE` below is that guard: an empty `excluded` list leaves the
|
|
8173
|
+
* stored column alone instead of resetting it to '[]' (the column DEFAULT only applies to INSERT).
|
|
8174
|
+
*/
|
|
8175
|
+
function upsertMachineRow(row = {}) {
|
|
8176
|
+
const instanceId = _hostedToken(row.instanceId || row.instance_id || '', '');
|
|
8177
|
+
const url = String(row.url || '').trim().replace(/\/+$/, '');
|
|
8178
|
+
if (!instanceId || !url) return 0;
|
|
8179
|
+
const urls = (Array.isArray(row.urls) ? row.urls : [])
|
|
8180
|
+
.map((entry) => String(entry || '').trim().replace(/\/+$/, ''))
|
|
8181
|
+
.filter(Boolean);
|
|
8182
|
+
const d = _machinesDb();
|
|
8183
|
+
_ensureMachineRegistrySchema();
|
|
8184
|
+
const changes = d.prepare(`
|
|
8185
|
+
INSERT INTO machines (instance_id, display_name, url, walle_url, enabled, updated_at, urls_json)
|
|
8186
|
+
VALUES (?, ?, ?, ?, ?, datetime('now'), ?)
|
|
8187
|
+
ON CONFLICT(instance_id) DO UPDATE SET
|
|
8188
|
+
display_name = excluded.display_name,
|
|
8189
|
+
url = excluded.url,
|
|
8190
|
+
walle_url = excluded.walle_url,
|
|
8191
|
+
enabled = excluded.enabled,
|
|
8192
|
+
urls_json = CASE WHEN excluded.urls_json = '[]' THEN machines.urls_json ELSE excluded.urls_json END,
|
|
8193
|
+
updated_at = datetime('now')
|
|
8194
|
+
`).run(
|
|
8195
|
+
instanceId,
|
|
8196
|
+
String(row.displayName || row.display_name || '').slice(0, 200),
|
|
8197
|
+
url,
|
|
8198
|
+
String(row.walleUrl || row.walle_url || '').replace(/\/+$/, ''),
|
|
8199
|
+
Number(row.enabled ?? 1) ? 1 : 0,
|
|
8200
|
+
JSON.stringify(urls),
|
|
8201
|
+
).changes || 0;
|
|
8202
|
+
if (changes) flushWal();
|
|
8203
|
+
return changes;
|
|
8204
|
+
}
|
|
8205
|
+
|
|
8206
|
+
/** Probe outcome for one machine: health + detail, and last_seen_at when the box answered. */
|
|
8207
|
+
function recordMachineProbe(instanceId, patch = {}) {
|
|
8208
|
+
const id = _hostedToken(instanceId || '', '');
|
|
8209
|
+
if (!id) return 0;
|
|
8210
|
+
const health = String(patch.health || '').trim();
|
|
8211
|
+
if (!health) return 0;
|
|
8212
|
+
const d = _machinesDb();
|
|
8213
|
+
const lastSeenAt = Number(patch.lastSeenAt ?? patch.last_seen_at ?? 0) || 0;
|
|
8214
|
+
const changes = d.prepare(`
|
|
8215
|
+
UPDATE machines
|
|
8216
|
+
SET health = ?, health_detail = ?, last_seen_at = ?, updated_at = datetime('now')
|
|
8217
|
+
WHERE instance_id = ?
|
|
8218
|
+
`).run(health, String(patch.healthDetail || patch.health_detail || '').slice(0, 500), lastSeenAt, id).changes || 0;
|
|
8219
|
+
if (changes) flushWal();
|
|
8220
|
+
return changes;
|
|
8221
|
+
}
|
|
8222
|
+
|
|
8223
|
+
/** Unregister a machine (route: POST /api/machines/:id/remove). Nothing else keys on the row. */
|
|
8224
|
+
function deleteMachineRow(instanceId) {
|
|
8225
|
+
const id = _hostedToken(instanceId || '', '');
|
|
8226
|
+
if (!id) return 0;
|
|
8227
|
+
const d = _machinesDb();
|
|
8228
|
+
const changes = d.prepare('DELETE FROM machines WHERE instance_id = ?').run(id).changes || 0;
|
|
8229
|
+
if (changes) flushWal();
|
|
8230
|
+
return changes;
|
|
8231
|
+
}
|
|
8232
|
+
|
|
8233
|
+
/**
|
|
8234
|
+
* WP-2a (machine-pool-picker-pairing-dashboard-2026-10-24.html §6.1, D-4 rev 2, §9 S-25): this
|
|
8235
|
+
* machine's OWN settings. Exactly one row (id = 1); a missing row reads as the defaults, so no
|
|
8236
|
+
* seeding step is needed. lib/machine-self.js caches this so the manifest path never hits the DB.
|
|
8237
|
+
*/
|
|
8238
|
+
function getMachineSelf() {
|
|
8239
|
+
try {
|
|
8240
|
+
const d = _machinesDb();
|
|
8241
|
+
const row = d.prepare('SELECT display_name, favorite, hidden, updated_at FROM machine_self WHERE id = 1').get();
|
|
8242
|
+
return _machineSelfRow(row);
|
|
8243
|
+
} catch {
|
|
8244
|
+
return _machineSelfRow(null); // expected: machine_self does not exist yet (pre-boot/pre-migration)
|
|
8245
|
+
}
|
|
8246
|
+
}
|
|
8247
|
+
|
|
8248
|
+
function _machineSelfRow(row) {
|
|
8249
|
+
return {
|
|
8250
|
+
displayName: String((row && row.display_name) || ''),
|
|
8251
|
+
favorite: Number((row && row.favorite) || 0) || 0,
|
|
8252
|
+
hidden: Number((row && row.hidden) || 0) || 0,
|
|
8253
|
+
updatedAt: Number((row && row.updated_at) || 0) || 0,
|
|
8254
|
+
};
|
|
8255
|
+
}
|
|
8256
|
+
|
|
8257
|
+
/**
|
|
8258
|
+
* D-4 write: ONLY the given fields change (a partial patch merges over the stored row, so a
|
|
8259
|
+
* favorite toggle never clears the display name). `at` is the caller's clock.
|
|
8260
|
+
*/
|
|
8261
|
+
function setMachineSelf({ displayName, favorite, hidden, at = 0 } = {}) {
|
|
8262
|
+
const d = _machinesDb();
|
|
8263
|
+
const current = getMachineSelf();
|
|
8264
|
+
const next = {
|
|
8265
|
+
displayName: displayName === undefined ? current.displayName : String(displayName || '').slice(0, 80),
|
|
8266
|
+
favorite: favorite === undefined ? current.favorite : (favorite ? 1 : 0),
|
|
8267
|
+
hidden: hidden === undefined ? current.hidden : (hidden ? 1 : 0),
|
|
8268
|
+
updatedAt: Math.trunc(Number(at) || 0) || current.updatedAt,
|
|
8269
|
+
};
|
|
8270
|
+
d.prepare(`
|
|
8271
|
+
INSERT INTO machine_self (id, display_name, favorite, hidden, updated_at)
|
|
8272
|
+
VALUES (1, ?, ?, ?, ?)
|
|
8273
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
8274
|
+
display_name = excluded.display_name,
|
|
8275
|
+
favorite = excluded.favorite,
|
|
8276
|
+
hidden = excluded.hidden,
|
|
8277
|
+
updated_at = excluded.updated_at
|
|
8278
|
+
`).run(next.displayName, next.favorite, next.hidden, next.updatedAt);
|
|
8279
|
+
flushWal();
|
|
8280
|
+
return getMachineSelf();
|
|
8281
|
+
}
|
|
8282
|
+
|
|
8283
|
+
/**
|
|
8284
|
+
* P-5 receive side (§6.1): replace this member's machinery membership with the pushed set in ONE
|
|
8285
|
+
* transaction — upsert every listed member (url = urls[0], urls_json = urls), delete every row not
|
|
8286
|
+
* listed, and NEVER write this instance's own row (a member is not in its own roster). `enabled` is
|
|
8287
|
+
* left alone on conflict (it stays a per-primary local switch, D-4).
|
|
8288
|
+
*/
|
|
8289
|
+
// `removable` = the PREVIOUS pool members: only those may be deleted when absent from `members`. A legacy
|
|
8290
|
+
// env-token row (P-8) is never a pool member, so a roster push/pull can never drop it.
|
|
8291
|
+
function replaceRosterMachines({ members = [], selfInstanceId = '', removable = [] } = {}) {
|
|
8292
|
+
const self = _hostedToken(selfInstanceId || '', '');
|
|
8293
|
+
const listed = [];
|
|
8294
|
+
const seen = new Set();
|
|
8295
|
+
for (const member of Array.isArray(members) ? members : []) {
|
|
8296
|
+
const instanceId = _hostedToken((member && (member.instanceId || member.instance_id)) || '', '');
|
|
8297
|
+
if (!instanceId || instanceId === self || seen.has(instanceId)) continue;
|
|
8298
|
+
const urls = (Array.isArray(member.urls) ? member.urls : []).map((u) => String(u || '').trim().replace(/\/+$/, '')).filter(Boolean);
|
|
8299
|
+
const url = String((member && (member.url || urls[0])) || '').trim().replace(/\/+$/, '');
|
|
8300
|
+
if (!url) continue;
|
|
8301
|
+
seen.add(instanceId);
|
|
8302
|
+
listed.push({ instanceId, url, urls: urls.length ? urls : [url], displayName: String((member && (member.displayName || member.display_name)) || '').slice(0, 200) });
|
|
8303
|
+
}
|
|
8304
|
+
const d = _machinesDb();
|
|
8305
|
+
_ensureMachineRegistrySchema();
|
|
8306
|
+
const txn = d.transaction(() => {
|
|
8307
|
+
let upserted = 0;
|
|
8308
|
+
for (const row of listed) {
|
|
8309
|
+
upserted += d.prepare(`
|
|
8310
|
+
INSERT INTO machines (instance_id, display_name, url, walle_url, enabled, updated_at, urls_json)
|
|
8311
|
+
VALUES (?, ?, ?, '', 1, datetime('now'), ?)
|
|
8312
|
+
ON CONFLICT(instance_id) DO UPDATE SET
|
|
8313
|
+
url = excluded.url,
|
|
8314
|
+
urls_json = excluded.urls_json,
|
|
8315
|
+
updated_at = datetime('now')
|
|
8316
|
+
`).run(row.instanceId, row.displayName, row.url, JSON.stringify(row.urls)).changes || 0;
|
|
8317
|
+
}
|
|
8318
|
+
let deleted = 0;
|
|
8319
|
+
const keep = new Set(listed.map((row) => row.instanceId));
|
|
8320
|
+
// A blank `self` means identity is not ready: delete nothing in that state.
|
|
8321
|
+
if (self) {
|
|
8322
|
+
const del = d.prepare('DELETE FROM machines WHERE instance_id = ?');
|
|
8323
|
+
for (const raw of Array.isArray(removable) ? removable : []) {
|
|
8324
|
+
const id = _hostedToken(raw || '', '');
|
|
8325
|
+
if (id && id !== self && !keep.has(id)) deleted += del.run(id).changes || 0;
|
|
8326
|
+
}
|
|
8327
|
+
}
|
|
8328
|
+
return { upserted, deleted };
|
|
8329
|
+
});
|
|
8330
|
+
const result = txn();
|
|
8331
|
+
flushWal();
|
|
8332
|
+
return result;
|
|
8333
|
+
}
|
|
8334
|
+
|
|
8335
|
+
/** WP-2a P-6 leave(): drop every roster row (the member is no longer in any pool). */
|
|
8336
|
+
function deleteAllMachineRows() {
|
|
8337
|
+
const d = _machinesDb();
|
|
8338
|
+
const changes = d.prepare('DELETE FROM machines').run().changes || 0;
|
|
8339
|
+
if (changes) flushWal();
|
|
8340
|
+
return changes;
|
|
8341
|
+
}
|
|
8342
|
+
|
|
8343
|
+
/**
|
|
8344
|
+
* WP-3 (machine-pool-picker-pairing-dashboard-2026-10-24.html §6.3, D-6/D-9, §9 S-15/S-16): the
|
|
8345
|
+
* pairing-offer rows. Table `machine_pairing_offers` (id, code_hash UNIQUE, created_at, expires_at,
|
|
8346
|
+
* redeemed_at, redeemed_by) comes from migration v56 and is classified 'core' in
|
|
8347
|
+
* lib/db-domain-tables.js.
|
|
8348
|
+
*
|
|
8349
|
+
* INVARIANT: the plaintext CODE never reaches this file. The caller (lib/machine-pairing.js) hashes
|
|
8350
|
+
* it and passes only `codeHash`; nothing here formats, logs or returns a code.
|
|
8351
|
+
*/
|
|
8352
|
+
|
|
8353
|
+
/** Mint one offer. Returns the number of rows written (0 for a blank id/hash). */
|
|
8354
|
+
function insertPairingOffer({ id, codeHash, createdAt = 0, expiresAt = 0 } = {}) {
|
|
8355
|
+
const offerId = String(id || '').trim();
|
|
8356
|
+
const hash = String(codeHash || '').trim();
|
|
8357
|
+
if (!offerId || !hash) return 0;
|
|
8358
|
+
const d = _machinesDb();
|
|
8359
|
+
const changes = d.prepare(`
|
|
8360
|
+
INSERT INTO machine_pairing_offers (id, code_hash, created_at, expires_at)
|
|
8361
|
+
VALUES (?, ?, ?, ?)
|
|
8362
|
+
`).run(offerId, hash, Math.trunc(Number(createdAt) || 0), Math.trunc(Number(expiresAt) || 0)).changes || 0;
|
|
8363
|
+
if (changes) flushWal();
|
|
8364
|
+
return changes;
|
|
8365
|
+
}
|
|
8366
|
+
|
|
8367
|
+
/**
|
|
8368
|
+
* Single-use: the `redeemed_at IS NULL` predicate makes two concurrent redemptions of the same offer
|
|
8369
|
+
* a 1-and-0 instead of a double-spend, so the loser is answered `pairing_code_invalid`.
|
|
8370
|
+
*/
|
|
8371
|
+
function markPairingOfferRedeemed({ id, at = 0, redeemedBy = '' } = {}) {
|
|
8372
|
+
const offerId = String(id || '').trim();
|
|
8373
|
+
if (!offerId) return 0;
|
|
8374
|
+
const d = _machinesDb();
|
|
8375
|
+
const changes = d.prepare(`
|
|
8376
|
+
UPDATE machine_pairing_offers
|
|
8377
|
+
SET redeemed_at = ?, redeemed_by = ?
|
|
8378
|
+
WHERE id = ? AND redeemed_at IS NULL
|
|
8379
|
+
`).run(Math.trunc(Number(at) || 0), String(redeemedBy || '').slice(0, 200), offerId).changes || 0;
|
|
8380
|
+
if (changes) flushWal();
|
|
8381
|
+
return changes;
|
|
8382
|
+
}
|
|
8383
|
+
|
|
8384
|
+
/**
|
|
8385
|
+
* AC-3c: the 5th consecutive wrong redeem burns every LIVE offer (unspent and unexpired) in one
|
|
8386
|
+
* statement. `redeemed_by = 'burned'` is what makes the burn visible in the table without ever
|
|
8387
|
+
* naming a code — and it is a reserved sentinel, not an instance id.
|
|
8388
|
+
*/
|
|
8389
|
+
function burnLivePairingOffers({ at = 0 } = {}) {
|
|
8390
|
+
const ts = Math.trunc(Number(at) || 0);
|
|
8391
|
+
const d = _machinesDb();
|
|
8392
|
+
const changes = d.prepare(`
|
|
8393
|
+
UPDATE machine_pairing_offers
|
|
8394
|
+
SET redeemed_at = ?, redeemed_by = 'burned'
|
|
8395
|
+
WHERE redeemed_at IS NULL AND expires_at > ?
|
|
8396
|
+
`).run(ts, ts).changes || 0;
|
|
8397
|
+
if (changes) flushWal();
|
|
8398
|
+
return changes;
|
|
8399
|
+
}
|
|
8400
|
+
|
|
8401
|
+
/**
|
|
8402
|
+
* The live set — unspent and unexpired as of `at`. Ordered oldest-first so the 3-offer ceiling is
|
|
8403
|
+
* deterministic. Pre-boot/pre-migration reads return [] rather than throwing (house read rule).
|
|
8404
|
+
*/
|
|
8405
|
+
function listLivePairingOffers({ at = 0 } = {}) {
|
|
8406
|
+
try {
|
|
8407
|
+
const d = _machinesDb();
|
|
8408
|
+
return d.prepare(`
|
|
8409
|
+
SELECT id, code_hash, created_at, expires_at
|
|
8410
|
+
FROM machine_pairing_offers
|
|
8411
|
+
WHERE redeemed_at IS NULL AND expires_at > ?
|
|
8412
|
+
ORDER BY created_at ASC
|
|
8413
|
+
`).all(Math.trunc(Number(at) || 0)).map((row) => ({
|
|
8414
|
+
id: String(row.id || ''),
|
|
8415
|
+
codeHash: String(row.code_hash || ''),
|
|
8416
|
+
createdAt: Number(row.created_at) || 0,
|
|
8417
|
+
expiresAt: Number(row.expires_at) || 0,
|
|
8418
|
+
}));
|
|
8419
|
+
} catch {
|
|
8420
|
+
return []; // expected: machine_pairing_offers does not exist yet (pre-boot / pre-migration)
|
|
8421
|
+
}
|
|
8422
|
+
}
|
|
8423
|
+
|
|
8424
|
+
/** WP-1 D-11 (§6.1): one sample per resource tick; the 24 h purge rides the SAME transaction. */
|
|
8425
|
+
function recordHostSample({ at = 0, sampleJson = '', purgeBeforeMs = 0 } = {}) {
|
|
8426
|
+
const d = _machinesDb();
|
|
8427
|
+
const txn = d.transaction(() => {
|
|
8428
|
+
d.prepare('INSERT OR REPLACE INTO host_samples (at, sample_json) VALUES (?, ?)').run(Math.trunc(Number(at) || 0), String(sampleJson || ''));
|
|
8429
|
+
return d.prepare('DELETE FROM host_samples WHERE at < ?').run(Math.trunc(Number(purgeBeforeMs) || 0)).changes || 0;
|
|
8430
|
+
});
|
|
8431
|
+
const purged = txn() || 0;
|
|
8432
|
+
flushWal();
|
|
8433
|
+
return purged;
|
|
8434
|
+
}
|
|
8435
|
+
|
|
8436
|
+
/** Oldest first — the wire order in §6.1. Pre-migration reads return [] rather than throwing. */
|
|
8437
|
+
function listHostSamples({ sinceMs = 0, limit = 1440 } = {}) {
|
|
8438
|
+
try {
|
|
8439
|
+
const d = _machinesDb();
|
|
8440
|
+
return d.prepare('SELECT at, sample_json FROM host_samples WHERE at >= ? ORDER BY at ASC LIMIT ?')
|
|
8441
|
+
.all(Math.trunc(Number(sinceMs) || 0), Math.max(1, Math.trunc(Number(limit) || 1440)));
|
|
8442
|
+
} catch {
|
|
8443
|
+
return []; // expected: host_samples does not exist yet (pre-boot handle)
|
|
8444
|
+
}
|
|
8445
|
+
}
|
|
8446
|
+
|
|
7994
8447
|
/**
|
|
7995
8448
|
* Schema v12: durable smart-routing manager state.
|
|
7996
8449
|
*
|
|
@@ -8413,6 +8866,11 @@ const SESSION_DIAGNOSTIC_CLIENT_MEMORY_SQL = `(event = 'client-memory')`;
|
|
|
8413
8866
|
const SESSION_DIAGNOSTIC_SEAM_RESERVE = 64;
|
|
8414
8867
|
const SESSION_DIAGNOSTIC_SEAM_SQL = `(event = 'client-history-seam-crossing')`;
|
|
8415
8868
|
|
|
8869
|
+
// reader-evidence-retention D-1/D-2: the reader incident beacons (scroll-write, chain-advance, page-fetch, parked) are the
|
|
8870
|
+
// evidence of a reader jump; a fifth lane, newest 10 rows PER EVENT, never a glob.
|
|
8871
|
+
const SESSION_DIAGNOSTIC_READER_PER_EVENT_RESERVE = 10;
|
|
8872
|
+
const SESSION_DIAGNOSTIC_READER_SQL = `(event IN ('client-history-reader-scroll-write', 'client-history-reader-chain-advance', 'client-history-page-fetch', 'client-history-reader-parked'))`;
|
|
8873
|
+
|
|
8416
8874
|
function _sessionDiagnosticRetentionStatements(d) {
|
|
8417
8875
|
return {
|
|
8418
8876
|
// 1) Protected rows keep their OWN reserve — trimmed to the newest
|
|
@@ -8457,19 +8915,35 @@ function _sessionDiagnosticRetentionStatements(d) {
|
|
|
8457
8915
|
LIMIT ${SESSION_DIAGNOSTIC_SEAM_RESERVE}
|
|
8458
8916
|
)
|
|
8459
8917
|
`),
|
|
8460
|
-
// 4)
|
|
8461
|
-
//
|
|
8462
|
-
//
|
|
8463
|
-
//
|
|
8918
|
+
// 4) Reader-beacon rows (the four incident events in SESSION_DIAGNOSTIC_READER_SQL) get their
|
|
8919
|
+
// OWN reserve too — a FIFTH lane, independent of the three above and of the unprotected budget
|
|
8920
|
+
// below. Unlike the other lanes it is partitioned PER EVENT (ROW_NUMBER() OVER (PARTITION BY
|
|
8921
|
+
// event)), because one incident chain fetched 11 pages: a shared lane would let a page-fetch
|
|
8922
|
+
// burst evict the single scroll-write row that names the cause (reader-evidence-retention D-2).
|
|
8923
|
+
trimReader: d.prepare(`
|
|
8924
|
+
DELETE FROM session_diagnostics
|
|
8925
|
+
WHERE id IN (
|
|
8926
|
+
SELECT id FROM (
|
|
8927
|
+
SELECT id, ROW_NUMBER() OVER (PARTITION BY event ORDER BY id DESC) AS rn
|
|
8928
|
+
FROM session_diagnostics
|
|
8929
|
+
WHERE session_id = ? AND ${SESSION_DIAGNOSTIC_READER_SQL}
|
|
8930
|
+
) WHERE rn > ${SESSION_DIAGNOSTIC_READER_PER_EVENT_RESERVE}
|
|
8931
|
+
)
|
|
8932
|
+
`),
|
|
8933
|
+
// 5) Unprotected rows (neither protected-lifecycle, client-memory, seam-beacon, nor
|
|
8934
|
+
// reader-beacon) are trimmed to whatever budget remains under the TOTAL cap once the
|
|
8935
|
+
// (already-trimmed) protected, client-memory, seam and reader rows are accounted for — run
|
|
8936
|
+
// AFTER all four trims above so this subquery sees their post-trim counts, not the pre-trim ones.
|
|
8464
8937
|
trimUnprotected: d.prepare(`
|
|
8465
8938
|
DELETE FROM session_diagnostics
|
|
8466
8939
|
WHERE session_id = ?
|
|
8467
8940
|
AND NOT ${SESSION_DIAGNOSTIC_PROTECTED_SQL}
|
|
8468
8941
|
AND NOT ${SESSION_DIAGNOSTIC_CLIENT_MEMORY_SQL}
|
|
8469
8942
|
AND NOT ${SESSION_DIAGNOSTIC_SEAM_SQL}
|
|
8943
|
+
AND NOT ${SESSION_DIAGNOSTIC_READER_SQL}
|
|
8470
8944
|
AND id NOT IN (
|
|
8471
8945
|
SELECT id FROM session_diagnostics
|
|
8472
|
-
WHERE session_id = ? AND NOT ${SESSION_DIAGNOSTIC_PROTECTED_SQL} AND NOT ${SESSION_DIAGNOSTIC_CLIENT_MEMORY_SQL} AND NOT ${SESSION_DIAGNOSTIC_SEAM_SQL}
|
|
8946
|
+
WHERE session_id = ? AND NOT ${SESSION_DIAGNOSTIC_PROTECTED_SQL} AND NOT ${SESSION_DIAGNOSTIC_CLIENT_MEMORY_SQL} AND NOT ${SESSION_DIAGNOSTIC_SEAM_SQL} AND NOT ${SESSION_DIAGNOSTIC_READER_SQL}
|
|
8473
8947
|
ORDER BY id DESC
|
|
8474
8948
|
LIMIT (${SESSION_DIAGNOSTIC_TOTAL_CAP} - (
|
|
8475
8949
|
SELECT COUNT(*) FROM session_diagnostics WHERE session_id = ? AND ${SESSION_DIAGNOSTIC_PROTECTED_SQL}
|
|
@@ -8477,6 +8951,8 @@ function _sessionDiagnosticRetentionStatements(d) {
|
|
|
8477
8951
|
SELECT COUNT(*) FROM session_diagnostics WHERE session_id = ? AND ${SESSION_DIAGNOSTIC_CLIENT_MEMORY_SQL}
|
|
8478
8952
|
) - (
|
|
8479
8953
|
SELECT COUNT(*) FROM session_diagnostics WHERE session_id = ? AND ${SESSION_DIAGNOSTIC_SEAM_SQL}
|
|
8954
|
+
) - (
|
|
8955
|
+
SELECT COUNT(*) FROM session_diagnostics WHERE session_id = ? AND ${SESSION_DIAGNOSTIC_READER_SQL}
|
|
8480
8956
|
))
|
|
8481
8957
|
)
|
|
8482
8958
|
`),
|
|
@@ -8487,7 +8963,8 @@ function _sweepSessionDiagnosticsRetention(d, statements, sessionId) {
|
|
|
8487
8963
|
statements.trimProtected.run(sessionId, sessionId);
|
|
8488
8964
|
statements.trimClientMemory.run(sessionId, sessionId);
|
|
8489
8965
|
statements.trimSeam.run(sessionId, sessionId);
|
|
8490
|
-
statements.
|
|
8966
|
+
statements.trimReader.run(sessionId);
|
|
8967
|
+
statements.trimUnprotected.run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
|
|
8491
8968
|
}
|
|
8492
8969
|
|
|
8493
8970
|
/*
|
|
@@ -8557,6 +9034,25 @@ function flushSessionMetadataUpdates(entries) {
|
|
|
8557
9034
|
return { updated: prepared.length };
|
|
8558
9035
|
}
|
|
8559
9036
|
|
|
9037
|
+
// WP-2 (worker-transcript-link-derived-not-discovered-2026-10-25.html §5 D-4/D-7, §8 WP-2): the
|
|
9038
|
+
// ONE writer for the "link a provider transcript to an agent_session" repair. A resume preflight
|
|
9039
|
+
// that DERIVED the transcript path (lib/transcript-link.js verified the file really is that
|
|
9040
|
+
// session's) calls this so the next resume finds the link instead of deriving it again.
|
|
9041
|
+
//
|
|
9042
|
+
// The WHERE clause is the safety property: only a row whose jsonl_path is empty (or NULL) is
|
|
9043
|
+
// touched, so a derived guess can never REPLACE a link someone else recorded -- `updated: 0`
|
|
9044
|
+
// means "nothing was written", which is the correct answer for a row that is already linked.
|
|
9045
|
+
function updateAgentSessionTranscriptLink(agentSessionId, jsonlPath) {
|
|
9046
|
+
const id = String(agentSessionId || '').trim();
|
|
9047
|
+
const p = String(jsonlPath || '').trim();
|
|
9048
|
+
if (!id || !p) return { updated: 0 };
|
|
9049
|
+
// agent_sessions is CORE -- flip-aware handle, matching every other writer of jsonl_path.
|
|
9050
|
+
const result = dbFor('agent_sessions').prepare(
|
|
9051
|
+
"UPDATE agent_sessions SET jsonl_path = COALESCE(NULLIF(?, ''), jsonl_path), updated_at = datetime('now') WHERE agent_session_id = ? AND COALESCE(jsonl_path, '') = ''",
|
|
9052
|
+
).run(p, id);
|
|
9053
|
+
return { updated: result.changes };
|
|
9054
|
+
}
|
|
9055
|
+
|
|
8560
9056
|
function listSessionDiagnostics(sessionId, limit = 120) {
|
|
8561
9057
|
const cleanSessionId = String(sessionId || '').trim();
|
|
8562
9058
|
if (!cleanSessionId) return [];
|
|
@@ -15141,6 +15637,24 @@ function getSmartRoutingSummaryRefreshInput(ctmSessionId, { limit = 24 } = {}) {
|
|
|
15141
15637
|
}
|
|
15142
15638
|
|
|
15143
15639
|
// --- Startup Tasks (crash-safe session restore) ---
|
|
15640
|
+
//
|
|
15641
|
+
// S-28 (remote-machine-hub-sessions-2026-10-22.html §9): THREE different things in this repo
|
|
15642
|
+
// are called an "instance id". They are not interchangeable, and only ONE of them is identity:
|
|
15643
|
+
//
|
|
15644
|
+
// 1. THE MINTED IDENTITY ROW — instance_identity.instance_id (db.js getOrCreateInstanceIdentity,
|
|
15645
|
+
// lib/instance-identity-*.js). A random, durable, per-DATABASE id minted once at boot,
|
|
15646
|
+
// transferable on an ownership handoff. THE HUB KEYS ON THIS ONE (D-3: `machines.instance_id`
|
|
15647
|
+
// is exactly this value, pinned from the box's GET /api/sync/manifest) and it is what
|
|
15648
|
+
// workspace/ownership lineage and the sync layer compare against. Reminting it is a visible
|
|
15649
|
+
// event with a restamp path (restampOwnerLineage) — never a side effect.
|
|
15650
|
+
// 2. THE LIVENESS STAMP BELOW — `${hostname}:${pid}:${Date.now()}`, deliberately NOT durable and
|
|
15651
|
+
// NOT unique per database: it is regenerated on every process start and exists to answer
|
|
15652
|
+
// "is the process that wrote this startup_tasks row still the one running?" inside one boot.
|
|
15653
|
+
// 3. CTM_EXECUTION_HOST — the env var a remote/forwarded launch may name for the PTY child
|
|
15654
|
+
// (server.js's create path), i.e. "where the work ran", a display/attribution field.
|
|
15655
|
+
//
|
|
15656
|
+
// The hub (lib/machine-registry.js) keys ONLY on (1). (2) must never be registered as a machine
|
|
15657
|
+
// — it changes on every restart; (3) is not an identity at all.
|
|
15144
15658
|
const CTM_INSTANCE_ID = `${require('os').hostname()}:${process.pid}:${Date.now()}`;
|
|
15145
15659
|
|
|
15146
15660
|
function _insertStartupTaskRow(d, {
|
|
@@ -15157,7 +15671,18 @@ function _insertStartupTaskRow(d, {
|
|
|
15157
15671
|
if (cols.has('parked_at')) { names.push('parked_at'); values.push(''); }
|
|
15158
15672
|
if (cols.has('park_reason')) { names.push('park_reason'); values.push(''); }
|
|
15159
15673
|
const hostedInstanceId = (cols.has('origin_instance_id') || cols.has('current_owner_instance_id')) ? _currentHostedInstanceId() : '';
|
|
15160
|
-
if (cols.has('origin_instance_id')) {
|
|
15674
|
+
if (cols.has('origin_instance_id')) {
|
|
15675
|
+
// S-6 (WP-2, docs/remote-machine-hub-sessions-2026-10-22.html §9): origin_instance_id is
|
|
15676
|
+
// WRITE-ONCE. INSERT OR REPLACE deletes and reinserts the row, so a RE-CREATE of an existing
|
|
15677
|
+
// ctm_session_id (crash recovery, restore of an exited session) used to re-stamp origin with
|
|
15678
|
+
// whatever instance is running now — destroying the only record of where the session was
|
|
15679
|
+
// first created (the hub's D-3/D-7 machine routing keys on it). Preserve the recorded origin;
|
|
15680
|
+
// fill from this instance only when the column is blank/missing (the same fill-if-blank shape
|
|
15681
|
+
// the sync/ownership writers use, e.g. the COALESCE(NULLIF(origin_instance_id, ''), ?) seed).
|
|
15682
|
+
const priorOrigin = _startupTaskRawOnHandle(d, sessionId);
|
|
15683
|
+
names.push('origin_instance_id');
|
|
15684
|
+
values.push(_hostedToken(priorOrigin && priorOrigin.origin_instance_id, '') || hostedInstanceId);
|
|
15685
|
+
}
|
|
15161
15686
|
if (cols.has('current_owner_instance_id')) { names.push('current_owner_instance_id'); values.push(hostedInstanceId); }
|
|
15162
15687
|
const placeholders = names.map(() => '?').join(', ');
|
|
15163
15688
|
d.prepare(`INSERT OR REPLACE INTO startup_tasks (${names.join(', ')}) VALUES (${placeholders})`).run(...values);
|
|
@@ -15245,6 +15770,7 @@ function updateStartupTaskAgentSession(sessionId, agentSessionId, agentProjectDi
|
|
|
15245
15770
|
vals.push(sessionId);
|
|
15246
15771
|
let changes = 0;
|
|
15247
15772
|
d.transaction(() => {
|
|
15773
|
+
if (cols.has('args') && resumesCandidateFork(d, sessionId, resumeArgs)) return; // only the transition commit moves it
|
|
15248
15774
|
changes = d.prepare(`UPDATE startup_tasks SET ${sets.join(', ')} WHERE ctm_session_id = ?`).run(...vals).changes || 0;
|
|
15249
15775
|
if (changes) _maybeEnqueueStartupTaskOutbox(d, sessionId, 'upsert');
|
|
15250
15776
|
})();
|
|
@@ -15521,6 +16047,19 @@ function listStartupTasks(opts = {}) {
|
|
|
15521
16047
|
return rows.filter((row) => _startupTaskRestorable(row, opts)).map(_startupTaskRow);
|
|
15522
16048
|
}
|
|
15523
16049
|
|
|
16050
|
+
// Ingest-fence D-1 clause (d) (docs/remote-hub-ownership-fences-and-restore-skip-diagnostics-2026-10-23.html):
|
|
16051
|
+
// the durable-ledger read. ctm_sessions is the one ledger that still holds an ENDED session —
|
|
16052
|
+
// listStartupTasks() above drops exited rows — so "does this hub's ledger know the id, and who owns
|
|
16053
|
+
// it" is answered here. Deliberately throw-through: the catalog's per-clause guard counts the fault
|
|
16054
|
+
// and fails OPEN, which a silent empty answer here would hide.
|
|
16055
|
+
function getCtmSessionOwnerRow(ctmSessionId) {
|
|
16056
|
+
const id = String(ctmSessionId || '').trim();
|
|
16057
|
+
if (!id) return { exists: false, currentOwner: '' };
|
|
16058
|
+
const row = getDomainDb('core').prepare('SELECT current_owner_instance_id FROM ctm_sessions WHERE id = ?').get(id);
|
|
16059
|
+
if (!row) return { exists: false, currentOwner: '' };
|
|
16060
|
+
return { exists: true, currentOwner: String(row.current_owner_instance_id || '').trim() };
|
|
16061
|
+
}
|
|
16062
|
+
|
|
15524
16063
|
// WP-2 restore-side backstop (docs/ctm-session-restore-loss-and-render-wedge-2026-07-26.html):
|
|
15525
16064
|
// listStartupTasks() above is restore's SOLE input — a ctm_sessions row that has no
|
|
15526
16065
|
// startup_tasks row (whatever the cause: RC-1's domain-routing bug, a write that exhausted its
|
|
@@ -18771,196 +19310,10 @@ function _normalizeSessionTitle(value, maxLen = 120) {
|
|
|
18771
19310
|
return String(value || '').replace(/\s+/g, ' ').trim().slice(0, maxLen);
|
|
18772
19311
|
}
|
|
18773
19312
|
|
|
18774
|
-
function _branchTitleCandidates(branch) {
|
|
18775
|
-
const full = String(branch || '').trim().replace(/^refs\/heads\//, '');
|
|
18776
|
-
if (!full) return [];
|
|
18777
|
-
const parts = full.split('/').filter(Boolean);
|
|
18778
|
-
const leaf = parts.length ? parts[parts.length - 1] : full;
|
|
18779
|
-
const namespace = parts.length > 1 ? parts.slice(0, -1).join('/') : '';
|
|
18780
|
-
const displayNamespace = namespace.replace(/^ctm\//, '');
|
|
18781
|
-
return Array.from(new Set([
|
|
18782
|
-
full,
|
|
18783
|
-
leaf,
|
|
18784
|
-
namespace && leaf ? `${namespace}/${leaf}` : '',
|
|
18785
|
-
displayNamespace && leaf ? `${displayNamespace}/${leaf}` : '',
|
|
18786
|
-
].filter(Boolean).map(_normalizeSessionTitle)));
|
|
18787
|
-
}
|
|
18788
|
-
|
|
18789
|
-
function _looksLikeWorktreeCwdTitle(title, cwd) {
|
|
18790
|
-
const t = _normalizeSessionTitle(title);
|
|
18791
|
-
const dir = String(cwd || '').trim();
|
|
18792
|
-
if (!t || !dir) return false;
|
|
18793
|
-
const base = _normalizeSessionTitle(path.basename(dir));
|
|
18794
|
-
if (!base || t.toLowerCase() !== base.toLowerCase()) return false;
|
|
18795
|
-
return /(?:^|[/\\])(?:\.claude[/\\])?worktrees[/\\][^/\\]+$/i.test(dir);
|
|
18796
|
-
}
|
|
18797
|
-
|
|
18798
|
-
function isUserSessionTitleCandidate(title, context = {}) {
|
|
18799
|
-
const t = _normalizeSessionTitle(title);
|
|
18800
|
-
if (!t) return false;
|
|
18801
|
-
if (/^Resume:\s*/i.test(t)) return false;
|
|
18802
|
-
if (/^Wall-?E session$/i.test(t)) return false;
|
|
18803
|
-
if (/^(Claude Code|Codex|Gemini|Aider):\s*(~\/|\/|[A-Za-z]:[\\/])/i.test(t)) return false;
|
|
18804
|
-
if (/^Shell:\s*/i.test(t)) return false;
|
|
18805
|
-
if (/^(~\/|\/|[A-Za-z]:[\\/])/.test(t)) return false;
|
|
18806
|
-
const cwd = _normalizeSessionTitle(context.cwd || '');
|
|
18807
|
-
if (cwd && (t === cwd || t === cwd.replace(process.env.HOME || '', '~'))) return false;
|
|
18808
|
-
if (_looksLikeWorktreeCwdTitle(t, context.cwd)) return false;
|
|
18809
|
-
if (_branchTitleCandidates(context.branch).some(candidate => candidate.toLowerCase() === t.toLowerCase())) return false;
|
|
18810
|
-
const cmd = _normalizeSessionTitle(context.cmd || '');
|
|
18811
|
-
if (cmd && t === cmd) return false;
|
|
18812
|
-
return true;
|
|
18813
|
-
}
|
|
18814
|
-
|
|
18815
|
-
function promoteStartupTaskTitleIfUserNamed(sessionId, { dryRun = false, reason = 'startup_task_title' } = {}) {
|
|
18816
|
-
const cleanId = String(sessionId || '').trim();
|
|
18817
|
-
if (!cleanId || !_tableExists('startup_tasks') || !_tableExists('ctm_sessions')) {
|
|
18818
|
-
return { promoted: false, reason: 'missing_table_or_id' };
|
|
18819
|
-
}
|
|
18820
|
-
const ctmId = _resolveCtmSessionIdForWrite(cleanId);
|
|
18821
|
-
const row = getDomainDb('core').prepare(`
|
|
18822
|
-
SELECT
|
|
18823
|
-
c.id,
|
|
18824
|
-
c.title,
|
|
18825
|
-
COALESCE(c.user_renamed, 0) AS user_renamed,
|
|
18826
|
-
s.label,
|
|
18827
|
-
s.cmd,
|
|
18828
|
-
s.cwd,
|
|
18829
|
-
s.branch,
|
|
18830
|
-
s.type
|
|
18831
|
-
FROM ctm_sessions c
|
|
18832
|
-
JOIN startup_tasks s ON s.ctm_session_id = c.id
|
|
18833
|
-
WHERE c.id = ?
|
|
18834
|
-
LIMIT 1
|
|
18835
|
-
`).get(ctmId);
|
|
18836
|
-
if (!row) return { promoted: false, reason: 'no_startup_task' };
|
|
18837
|
-
if (row.user_renamed) return { promoted: false, protected: true, title: row.title || '', reason: 'already_user_title' };
|
|
18838
|
-
|
|
18839
|
-
const title = _normalizeSessionTitle(row.label);
|
|
18840
|
-
if (!isUserSessionTitleCandidate(title, row)) {
|
|
18841
|
-
return { promoted: false, reason: 'runtime_generated_title', title };
|
|
18842
|
-
}
|
|
18843
|
-
|
|
18844
|
-
if (dryRun) {
|
|
18845
|
-
return {
|
|
18846
|
-
promoted: row.title !== title || !row.user_renamed,
|
|
18847
|
-
title,
|
|
18848
|
-
previousTitle: row.title || '',
|
|
18849
|
-
reason,
|
|
18850
|
-
};
|
|
18851
|
-
}
|
|
18852
|
-
|
|
18853
|
-
const hasTitleStatus = _tableHasColumn('ctm_sessions', 'title_status');
|
|
18854
|
-
const now = new Date().toISOString();
|
|
18855
|
-
const result = hasTitleStatus
|
|
18856
|
-
? getDomainDb('core').prepare(`
|
|
18857
|
-
UPDATE ctm_sessions SET
|
|
18858
|
-
title = ?,
|
|
18859
|
-
title_source = 'user',
|
|
18860
|
-
title_status = 'user_named',
|
|
18861
|
-
title_status_reason = ?,
|
|
18862
|
-
title_updated_at = ?,
|
|
18863
|
-
user_renamed = 1,
|
|
18864
|
-
updated_at = datetime('now')
|
|
18865
|
-
WHERE id = ? AND COALESCE(user_renamed, 0) = 0
|
|
18866
|
-
`).run(title, reason, now, ctmId)
|
|
18867
|
-
: getDomainDb('core').prepare(`
|
|
18868
|
-
UPDATE ctm_sessions SET
|
|
18869
|
-
title = ?,
|
|
18870
|
-
user_renamed = 1,
|
|
18871
|
-
updated_at = datetime('now')
|
|
18872
|
-
WHERE id = ? AND COALESCE(user_renamed, 0) = 0
|
|
18873
|
-
`).run(title, ctmId);
|
|
18874
|
-
if (result.changes) flushWal();
|
|
18875
|
-
return {
|
|
18876
|
-
promoted: result.changes > 0,
|
|
18877
|
-
protected: true,
|
|
18878
|
-
title,
|
|
18879
|
-
previousTitle: row.title || '',
|
|
18880
|
-
reason,
|
|
18881
|
-
};
|
|
18882
|
-
}
|
|
18883
|
-
|
|
18884
|
-
function repairSessionTitlesFromStartupTasks({ dryRun = false, limit = 1000 } = {}) {
|
|
18885
|
-
if (!_tableExists('startup_tasks') || !_tableExists('ctm_sessions')) {
|
|
18886
|
-
return { scanned: 0, promoted: 0, skipped: 0, items: [] };
|
|
18887
|
-
}
|
|
18888
|
-
const rows = getDomainDb('core').prepare(`
|
|
18889
|
-
SELECT
|
|
18890
|
-
c.id,
|
|
18891
|
-
c.title,
|
|
18892
|
-
COALESCE(c.user_renamed, 0) AS user_renamed,
|
|
18893
|
-
s.label,
|
|
18894
|
-
s.cmd,
|
|
18895
|
-
s.cwd,
|
|
18896
|
-
s.branch,
|
|
18897
|
-
s.type
|
|
18898
|
-
FROM startup_tasks s
|
|
18899
|
-
JOIN ctm_sessions c ON c.id = s.ctm_session_id
|
|
18900
|
-
WHERE COALESCE(c.user_renamed, 0) = 0
|
|
18901
|
-
AND COALESCE(s.label, '') != ''
|
|
18902
|
-
ORDER BY s.created_at DESC
|
|
18903
|
-
LIMIT ?
|
|
18904
|
-
`).all(Math.max(1, Number(limit) || 1000));
|
|
18905
|
-
const items = [];
|
|
18906
|
-
for (const row of rows) {
|
|
18907
|
-
const title = _normalizeSessionTitle(row.label);
|
|
18908
|
-
if (!isUserSessionTitleCandidate(title, row)) continue;
|
|
18909
|
-
items.push({
|
|
18910
|
-
sessionId: row.id,
|
|
18911
|
-
title,
|
|
18912
|
-
previousTitle: row.title || '',
|
|
18913
|
-
});
|
|
18914
|
-
}
|
|
18915
|
-
if (!dryRun && items.length) {
|
|
18916
|
-
const hasTitleStatus = _tableHasColumn('ctm_sessions', 'title_status');
|
|
18917
|
-
const now = new Date().toISOString();
|
|
18918
|
-
const update = hasTitleStatus
|
|
18919
|
-
? getDomainDb('core').prepare(`
|
|
18920
|
-
UPDATE ctm_sessions SET
|
|
18921
|
-
title = ?,
|
|
18922
|
-
title_source = 'user',
|
|
18923
|
-
title_status = 'user_named',
|
|
18924
|
-
title_status_reason = 'startup_task_title_repair',
|
|
18925
|
-
title_updated_at = ?,
|
|
18926
|
-
user_renamed = 1,
|
|
18927
|
-
updated_at = datetime('now')
|
|
18928
|
-
WHERE id = ? AND COALESCE(user_renamed, 0) = 0
|
|
18929
|
-
`)
|
|
18930
|
-
: getDomainDb('core').prepare(`
|
|
18931
|
-
UPDATE ctm_sessions SET
|
|
18932
|
-
title = ?,
|
|
18933
|
-
user_renamed = 1,
|
|
18934
|
-
updated_at = datetime('now')
|
|
18935
|
-
WHERE id = ? AND COALESCE(user_renamed, 0) = 0
|
|
18936
|
-
`);
|
|
18937
|
-
const txn = getDomainDb('core').transaction(() => {
|
|
18938
|
-
for (const item of items) {
|
|
18939
|
-
if (hasTitleStatus) update.run(item.title, now, item.sessionId);
|
|
18940
|
-
else update.run(item.title, item.sessionId);
|
|
18941
|
-
}
|
|
18942
|
-
});
|
|
18943
|
-
txn();
|
|
18944
|
-
flushWal();
|
|
18945
|
-
}
|
|
18946
|
-
return {
|
|
18947
|
-
scanned: rows.length,
|
|
18948
|
-
promoted: items.length,
|
|
18949
|
-
skipped: rows.length - items.length,
|
|
18950
|
-
items,
|
|
18951
|
-
};
|
|
18952
|
-
}
|
|
18953
|
-
|
|
18954
19313
|
function setSessionTitleNew(id, title, userRenamed = false, meta = {}) {
|
|
18955
19314
|
const ctmId = _resolveCtmSessionIdForWrite(id);
|
|
18956
19315
|
const cleanTitle = _normalizeSessionTitle(title);
|
|
18957
19316
|
if (!cleanTitle) return;
|
|
18958
|
-
if (!userRenamed) {
|
|
18959
|
-
const protectedTitle = promoteStartupTaskTitleIfUserNamed(ctmId, {
|
|
18960
|
-
reason: meta.source === 'ai_title' ? 'protected_from_ai_title' : 'protected_from_auto_title',
|
|
18961
|
-
});
|
|
18962
|
-
if (protectedTitle && protectedTitle.protected) return;
|
|
18963
|
-
}
|
|
18964
19317
|
const hasTitleStatus = _tableHasColumn('ctm_sessions', 'title_status');
|
|
18965
19318
|
const source = userRenamed ? 'user' : (meta.source || '');
|
|
18966
19319
|
const status = userRenamed ? 'user_renamed' : (meta.status || '');
|
|
@@ -19051,12 +19404,16 @@ function setSessionTitleStatusNew(id, status, reason = '', source = 'ai_title')
|
|
|
19051
19404
|
|
|
19052
19405
|
// WP-1 (session-rename-mirror §6b/§6c): outcome of mirroring a CTM title to its provider.
|
|
19053
19406
|
// UPDATE only, no stub rows (a mirror attempt is meaningless for a row that doesn't exist).
|
|
19407
|
+
// WP-4/D-12 (S-24): the REASON is persisted with the outcome — title_mirror_error is
|
|
19408
|
+
// `reason + (error ? ': ' + error : '')`, so an `unsupported` outcome can no longer be stored with
|
|
19409
|
+
// an empty error and no recorded cause.
|
|
19054
19410
|
const TITLE_MIRROR_STATES = new Set(['mirrored', 'unsupported', 'failed']);
|
|
19055
|
-
function setSessionTitleMirror(ctmSessionId, { state, error = '', value = '', at = new Date().toISOString() } = {}) {
|
|
19411
|
+
function setSessionTitleMirror(ctmSessionId, { state, reason = '', error = '', value = '', at = new Date().toISOString() } = {}) {
|
|
19056
19412
|
if (!TITLE_MIRROR_STATES.has(state)) throw new TypeError(`setSessionTitleMirror: invalid state ${JSON.stringify(state)}`);
|
|
19057
19413
|
const ctmId = _resolveCtmSessionIdForWrite(ctmSessionId);
|
|
19414
|
+
const mirrorError = reason ? (error ? `${reason}: ${error}` : reason) : (error || '');
|
|
19058
19415
|
const result = getDomainDb('core').prepare('UPDATE ctm_sessions SET title_mirror_state=?, title_mirror_error=?, title_mirror_value=?, title_mirror_at=? WHERE id=?')
|
|
19059
|
-
.run(state,
|
|
19416
|
+
.run(state, mirrorError, value || '', at, ctmId);
|
|
19060
19417
|
if (result.changes > 0) flushWal();
|
|
19061
19418
|
}
|
|
19062
19419
|
function getSessionTitleMirror(ctmSessionId) {
|
|
@@ -19160,12 +19517,7 @@ function getSessionDisplayTitleInfo(id) {
|
|
|
19160
19517
|
COALESCE(a.rename_name, '') as rename_name,
|
|
19161
19518
|
COALESCE(a.first_message, '') as first_message,
|
|
19162
19519
|
COALESCE(a.last_user_content, '') as last_user_content,
|
|
19163
|
-
COALESCE(a.first_assistant_text, '') as first_assistant_text
|
|
19164
|
-
st.label AS startup_label,
|
|
19165
|
-
st.cmd AS startup_cmd,
|
|
19166
|
-
st.cwd AS startup_cwd,
|
|
19167
|
-
st.branch AS startup_branch,
|
|
19168
|
-
st.type AS startup_type
|
|
19520
|
+
COALESCE(a.first_assistant_text, '') as first_assistant_text
|
|
19169
19521
|
FROM ctm_sessions c
|
|
19170
19522
|
LEFT JOIN (
|
|
19171
19523
|
SELECT *, ROW_NUMBER() OVER (
|
|
@@ -19177,8 +19529,6 @@ function getSessionDisplayTitleInfo(id) {
|
|
|
19177
19529
|
FROM agent_sessions
|
|
19178
19530
|
WHERE ctm_session_id = ?
|
|
19179
19531
|
) a ON a.ctm_session_id = c.id AND a.rn = 1
|
|
19180
|
-
LEFT JOIN startup_tasks st
|
|
19181
|
-
ON st.ctm_session_id = c.id
|
|
19182
19532
|
WHERE c.id = ?
|
|
19183
19533
|
LIMIT 1
|
|
19184
19534
|
`).get(ctmId, ctmId);
|
|
@@ -19189,27 +19539,19 @@ function getSessionDisplayTitleInfo(id) {
|
|
|
19189
19539
|
if (agent && agent.ctm_session_id) row = rowForCtmId(agent.ctm_session_id);
|
|
19190
19540
|
}
|
|
19191
19541
|
if (!row) return null;
|
|
19192
|
-
|
|
19193
|
-
|
|
19194
|
-
|
|
19195
|
-
|
|
19196
|
-
type: row.startup_type,
|
|
19197
|
-
}) ? _normalizeSessionTitle(row.startup_label) : '';
|
|
19198
|
-
const effectiveRow = startupTitle ? {
|
|
19199
|
-
...row,
|
|
19200
|
-
title: startupTitle,
|
|
19201
|
-
user_renamed: 1,
|
|
19202
|
-
} : row;
|
|
19203
|
-
const displayTitle = pickDisplayTitle(effectiveRow);
|
|
19542
|
+
// The row as stored. Title provenance (userRenamed/titleSource/…) is read from
|
|
19543
|
+
// ctm_sessions only — a user-looking startup_tasks.label is never inferred here
|
|
19544
|
+
// (design 2026-10-23 D-3: store provenance with the value, never infer it).
|
|
19545
|
+
const displayTitle = pickDisplayTitle(row);
|
|
19204
19546
|
return {
|
|
19205
19547
|
ctmSessionId: row.id,
|
|
19206
|
-
title:
|
|
19548
|
+
title: row.title || '',
|
|
19207
19549
|
aiTitle: row.user_renamed ? '' : (row.title || ''),
|
|
19208
19550
|
displayTitle: displayTitle || '',
|
|
19209
|
-
userRenamed: !!
|
|
19210
|
-
titleSource:
|
|
19211
|
-
titleStatus:
|
|
19212
|
-
titleStatusReason:
|
|
19551
|
+
userRenamed: !!row.user_renamed,
|
|
19552
|
+
titleSource: row.title_source || '',
|
|
19553
|
+
titleStatus: row.title_status || '',
|
|
19554
|
+
titleStatusReason: row.title_status_reason || '',
|
|
19213
19555
|
titleUpdatedAt: row.title_updated_at || '',
|
|
19214
19556
|
renameName: row.rename_name || '',
|
|
19215
19557
|
};
|
|
@@ -19344,6 +19686,7 @@ function repairCodexSessionMetadataFromConversations({ dryRun = false, limit = 5
|
|
|
19344
19686
|
title_updated_at = ?,
|
|
19345
19687
|
updated_at = datetime('now')
|
|
19346
19688
|
WHERE id = ? AND COALESCE(user_renamed, 0) = 0
|
|
19689
|
+
AND COALESCE(title_source, '') <> 'provider_title'
|
|
19347
19690
|
`)
|
|
19348
19691
|
: d.prepare(`
|
|
19349
19692
|
UPDATE ctm_sessions SET
|
|
@@ -19867,6 +20210,39 @@ function getDirectShadowStats() {
|
|
|
19867
20210
|
return null;
|
|
19868
20211
|
}
|
|
19869
20212
|
|
|
20213
|
+
// WP-6 (design doc session-name-agent-title-precedence §6, §8 WP-6): the ONE-SHOT agent-title
|
|
20214
|
+
// backfill's write half. The read pool did the transcript I/O (workers/read-pool-worker.js's
|
|
20215
|
+
// 'backfillAgentTitles'); this is the owner-side apply, so it runs on the DB-owner worker's core
|
|
20216
|
+
// handle and is reached from the main thread through DB_OWNER_MUTATION_ACTIONS like every other
|
|
20217
|
+
// mutation. Every row goes through lib/agent-title-apply.js — the SINGLE tier-2 apply — so the
|
|
20218
|
+
// backfill obeys exactly the ingest rules: tier 1 is never rewritten, a custom-title equal to
|
|
20219
|
+
// title_mirror_value is CTM's own echo (the ai-title wins), and a uuid-like name is not a name.
|
|
20220
|
+
// A row whose apply lands also moves startup_tasks.label (D-13: the restore label and the durable
|
|
20221
|
+
// title must never disagree).
|
|
20222
|
+
/**
|
|
20223
|
+
* @param {Array<{ id: string, customTitle?: string, aiTitle?: string }>} rows one read-pool page
|
|
20224
|
+
* @returns {{ scanned: number, applied: number, reasons: Record<string, number> }}
|
|
20225
|
+
*/
|
|
20226
|
+
function applyAgentTitleBackfillRows(rows = []) {
|
|
20227
|
+
const coreDb = getDomainDb('core');
|
|
20228
|
+
const result = { scanned: 0, applied: 0, reasons: {} };
|
|
20229
|
+
for (const row of (Array.isArray(rows) ? rows : [])) {
|
|
20230
|
+
const id = String(row?.id || '').trim();
|
|
20231
|
+
if (!id) continue;
|
|
20232
|
+
result.scanned += 1;
|
|
20233
|
+
const outcome = applyAgentTitleSignals(coreDb, {
|
|
20234
|
+
ctmSessionId: id, customTitle: row?.customTitle, aiTitle: row?.aiTitle,
|
|
20235
|
+
});
|
|
20236
|
+
const reason = String(outcome.reason || 'unknown');
|
|
20237
|
+
result.reasons[reason] = (result.reasons[reason] || 0) + 1;
|
|
20238
|
+
if (outcome.applied === 'agent' && outcome.title) {
|
|
20239
|
+
result.applied += 1;
|
|
20240
|
+
try { updateStartupTaskLabel(id, outcome.title); } catch (error) { console.error('[agent-title-backfill] label sync failed:', error.message); }
|
|
20241
|
+
}
|
|
20242
|
+
}
|
|
20243
|
+
return result;
|
|
20244
|
+
}
|
|
20245
|
+
|
|
19870
20246
|
// Explicit mutation surface allowed to cross from the main process to the DB-owner
|
|
19871
20247
|
// worker. Keep this as a closed map of local function references: the worker never
|
|
19872
20248
|
// accepts an arbitrary exported method name, SQL string, or callback from the caller.
|
|
@@ -19967,6 +20343,7 @@ const DB_OWNER_MUTATION_ACTIONS = Object.freeze({
|
|
|
19967
20343
|
setSessionTitle,
|
|
19968
20344
|
setSessionTitleNew,
|
|
19969
20345
|
setSessionTitleStatusNew,
|
|
20346
|
+
applyAgentTitleBackfillRows,
|
|
19970
20347
|
createTemplate,
|
|
19971
20348
|
deleteTemplate,
|
|
19972
20349
|
trackPromptUsage,
|
|
@@ -20024,6 +20401,37 @@ const DB_OWNER_MUTATION_ACTIONS = Object.freeze({
|
|
|
20024
20401
|
transferLease,
|
|
20025
20402
|
transferSessionOwnership,
|
|
20026
20403
|
upsertHostedWorkspaceBinding,
|
|
20404
|
+
// WP-2 (docs/remote-machine-hub-sessions-2026-10-22.html §6.1/§8, deferred from WP-0): the
|
|
20405
|
+
// machine-registry writes are reachable as named db.js exports so they route through the DB-owner
|
|
20406
|
+
// mutation Proxy — the same shape as the Review Hub registry writes above. Without these three
|
|
20407
|
+
// entries lib/machine-registry.js's dispatchOwnerMutation() calls landed on the MAIN thread
|
|
20408
|
+
// (db-owner-client's Proxy only crosses for a member of this map), which is exactly the
|
|
20409
|
+
// serial-writer contention the owner worker exists to prevent. Their OP_META rows
|
|
20410
|
+
// (shared/storage/contract.js) tag them domain:'core' on `machines`.
|
|
20411
|
+
upsertMachineRow,
|
|
20412
|
+
recordMachineProbe,
|
|
20413
|
+
deleteMachineRow,
|
|
20414
|
+
// WP-1 (docs/machine-pool-picker-pairing-dashboard-2026-10-24.html §6.1, D-11): the 60 s host
|
|
20415
|
+
// sample write. It joins this map for the same reason the three rows above did — the tick must
|
|
20416
|
+
// not put an insert+purge on the main thread — and its OP_META row tags domain:'core' on
|
|
20417
|
+
// `host_samples` (lib/db-domains.js CTM_WRITE_OPS carries the same name).
|
|
20418
|
+
recordHostSample,
|
|
20419
|
+
// WP-2a (machine-pool-picker-pairing-dashboard-2026-10-24.html §6.1, D-4/P-5/P-6): the
|
|
20420
|
+
// machine-owned settings write, the P-5 roster replacement, and the leave() sweep. All three are
|
|
20421
|
+
// single-domain 'core' mutations over `machines`/`machine_self`, so they join this map for the
|
|
20422
|
+
// same main-thread reason as the rows above; lib/db-domains.js CTM_WRITE_OPS and
|
|
20423
|
+
// shared/storage/contract.js OP_META carry the matching names.
|
|
20424
|
+
setMachineSelf,
|
|
20425
|
+
replaceRosterMachines,
|
|
20426
|
+
deleteAllMachineRows,
|
|
20427
|
+
// WP-3 (machine-pool-picker-pairing-dashboard-2026-10-24.html §6.3, D-6/D-9): the three
|
|
20428
|
+
// pairing-offer writes. They join this map for the same main-thread reason as the rows above —
|
|
20429
|
+
// `machine_pairing_offers` is domain 'core' (lib/db-domain-tables.js), and each op's OP_META row in
|
|
20430
|
+
// shared/storage/contract.js carries the matching kind:'write' + tables:['machine_pairing_offers'].
|
|
20431
|
+
// `listLivePairingOffers` is the bounded read beside them and is deliberately NOT here.
|
|
20432
|
+
insertPairingOffer,
|
|
20433
|
+
markPairingOfferRedeemed,
|
|
20434
|
+
burnLivePairingOffers,
|
|
20027
20435
|
seedHostedWorkspaceBindings,
|
|
20028
20436
|
updateStartupTaskWorkspaceMapping,
|
|
20029
20437
|
repointStartupTaskWorkspace,
|
|
@@ -20088,8 +20496,6 @@ const DB_OWNER_MUTATION_ACTIONS = Object.freeze({
|
|
|
20088
20496
|
upsertAgentSessionIdentity,
|
|
20089
20497
|
recordResumeLineage,
|
|
20090
20498
|
setSessionStar,
|
|
20091
|
-
promoteStartupTaskTitleIfUserNamed,
|
|
20092
|
-
repairSessionTitlesFromStartupTasks,
|
|
20093
20499
|
repairCodexSessionMetadataFromConversations,
|
|
20094
20500
|
repairCodexRolloutAgentIdentities,
|
|
20095
20501
|
detachPromotedProviderChildSessions,
|
|
@@ -20128,6 +20534,7 @@ module.exports = {
|
|
|
20128
20534
|
SESSION_DIAGNOSTIC_PROTECTED_RESERVE,
|
|
20129
20535
|
SESSION_DIAGNOSTIC_CLIENT_MEMORY_RESERVE,
|
|
20130
20536
|
SESSION_DIAGNOSTIC_SEAM_RESERVE,
|
|
20537
|
+
SESSION_DIAGNOSTIC_READER_PER_EVENT_RESERVE,
|
|
20131
20538
|
MigrationFailedError,
|
|
20132
20539
|
migrateSchemaIfNeeded,
|
|
20133
20540
|
// WP-6 (docs/approval-delivery-truth-host-skew-2026-09-01.html §5 D-9, §8
|
|
@@ -20243,7 +20650,7 @@ module.exports = {
|
|
|
20243
20650
|
deleteSessionLeadModel: sessionLeadModelStore.remove,
|
|
20244
20651
|
getSessionLeadRoute: sessionLeadRouteStore.get, setSessionLeadRoute: sessionLeadRouteStore.set, deleteSessionLeadRoute: sessionLeadRouteStore.remove,
|
|
20245
20652
|
getCodingModelTransition: leadModelTransitionStore.get,
|
|
20246
|
-
getActiveCodingModelTransitionForSession: leadModelTransitionStore.getActiveForSession,
|
|
20653
|
+
getActiveCodingModelTransitionForSession: leadModelTransitionStore.getActiveForSession, getCodingModelTransitionsForThread: leadModelTransitionStore.listForThread,
|
|
20247
20654
|
recordLeadRouteLaunchOwnership,
|
|
20248
20655
|
getCodingModelTransitionStagedToken: leadModelTransitionStore.getStagedToken,
|
|
20249
20656
|
// WP-2A: the bounded owner-side approved-intent read. Synchronous BY DESIGN — it is the DB-owner
|
|
@@ -20273,10 +20680,22 @@ module.exports = {
|
|
|
20273
20680
|
updateStartupTaskWorkspaceMapping, repointStartupTaskWorkspace,
|
|
20274
20681
|
_syncFeatureEnabled,
|
|
20275
20682
|
enqueueSyncOutbox, listSyncOutbox, markSyncOutboxDeliveredThrough, recordSyncOutboxServed, getSyncOutboxDeliveredSeq, reconcileSyncOutbox, pruneSyncOutbox,
|
|
20683
|
+
// WP-0 hub machine registry (lib/machine-registry.js): read + the three writes it takes so
|
|
20684
|
+
// far (register/enable, probe outcome, unregister). Handles resolve via getDomainDb('core').
|
|
20685
|
+
listMachineRows, upsertMachineRow, recordMachineProbe, deleteMachineRow, _ensureMachineRegistrySchema,
|
|
20686
|
+
// WP-1 D-11: the host-sample pair. `recordHostSample` is a DB_OWNER_MUTATION_ACTIONS entry
|
|
20687
|
+
// (above); `listHostSamples` is the bounded read beside it, both on the domain-'core' handle.
|
|
20688
|
+
recordHostSample, listHostSamples,
|
|
20689
|
+
// WP-2a D-4/P-5/P-6: the machine-owned settings read/write, the P-5 roster replacement and the
|
|
20690
|
+
// leave() sweep — all on the domain-'core' handle beside the registry pair above.
|
|
20691
|
+
getMachineSelf, setMachineSelf, replaceRosterMachines, deleteAllMachineRows,
|
|
20692
|
+
// WP-3 §6.3: the pairing-offer trio (DB_OWNER_MUTATION_ACTIONS entries above) plus its read —
|
|
20693
|
+
// all on the domain-'core' handle beside the machine rows.
|
|
20694
|
+
insertPairingOffer, markPairingOfferRedeemed, burnLivePairingOffers, listLivePairingOffers,
|
|
20276
20695
|
createSyncResyncSnapshot, createSyncLocalSnapshot, applySyncSnapshot, applySyncEntries, validateSyncEntries, applySyncPurgeCommand,
|
|
20277
20696
|
getSessionContentGeneration, getSyncHighWaterMark, getOpenSyncGapConflict, recordSyncPeerConflict, offerStoppedSessionHandoff,
|
|
20278
20697
|
createHostedMigrationPack, importHostedMigrationPack,
|
|
20279
|
-
addStartupTask, updateStartupTaskLabel, updateStartupTaskTitle, updateStartupTaskMcpProfile, updateStartupTaskClaudeSession, updateStartupTaskAgentSession, updateStartupTaskBranch, updateStartupTaskCwd, updateStartupTaskAttentionDismissal, updateStartupTaskLifecycle, backfillWalleStartupTaskActivity, markStartupTaskUserPromptInput, removeStartupTask, markStartupTaskExited, markStartupTaskParked, reviveStartupTask, heartbeatStartupTasks, getStartupTask, listStartupTasks, findSessionsMissingStartupTask, clearStartupTasks, listSessionLinkedRefs,
|
|
20698
|
+
addStartupTask, updateStartupTaskLabel, updateStartupTaskTitle, updateStartupTaskMcpProfile, updateStartupTaskClaudeSession, updateStartupTaskAgentSession, updateStartupTaskBranch, updateStartupTaskCwd, updateStartupTaskAttentionDismissal, updateStartupTaskLifecycle, backfillWalleStartupTaskActivity, markStartupTaskUserPromptInput, removeStartupTask, markStartupTaskExited, markStartupTaskParked, reviveStartupTask, heartbeatStartupTasks, getStartupTask, listStartupTasks, getCtmSessionOwnerRow, findSessionsMissingStartupTask, clearStartupTasks, listSessionLinkedRefs,
|
|
20280
20699
|
createSessionWithStartupTask,
|
|
20281
20700
|
appendScrollbackBatch, cleanupScrollbackChunkSeqIntegrity, getNextScrollbackSeq, loadScrollback, loadScrollbackTail, clearScrollback,
|
|
20282
20701
|
getScrollbackIdentity, setScrollbackIdentity, inspectScrollbackForSession, prepareScrollbackForSession,
|
|
@@ -20302,11 +20721,11 @@ module.exports = {
|
|
|
20302
20721
|
upsertSessionIndex, resolveSessionId,
|
|
20303
20722
|
// CTM Sessions + Agent Sessions (v1 schema)
|
|
20304
20723
|
getSessionIdentity, getLatestAgentSessionForCtm, getSessionConversationSourceIds, getSessionMessageIndexFreshness, getSessionUserPromptFreshness, resolveSession, batchRestoreIdentities, upsertSession, upsertAgentSessionIdentity, recordResumeLineage, setSessionStar, getStarredSessionIds,
|
|
20305
|
-
setSessionTitleNew, setSessionTitleStatusNew, getSessionTitleNew, getSessionTitlesByIds, getSessionTitleSourceById, getSessionDisplayTitleInfo,
|
|
20724
|
+
setSessionTitleNew, setSessionTitleStatusNew, getSessionTitleNew, getSessionTitlesByIds, getSessionTitleSourceById, getSessionDisplayTitleInfo, getAllSessionsData, setSessionTitleMirror, getSessionTitleMirror, applyAgentTitleBackfillRows,
|
|
20306
20725
|
repairCodexSessionMetadataFromConversations, repairCodexRolloutAgentIdentities, detachPromotedProviderChildSessions,
|
|
20307
20726
|
listProviderChildAgentOwnerMappings,
|
|
20308
20727
|
getAgentSessions, getAgentSession, getAgentSessionByCtmId, getForkForDesktopUuid, listDesktopForks, deleteCtmSession,
|
|
20309
|
-
deleteAgentSessionIdentity, clearScrollbackIdentities,
|
|
20728
|
+
deleteAgentSessionIdentity, clearScrollbackIdentities, updateAgentSessionTranscriptLink,
|
|
20310
20729
|
// worker_sessions (WP-0 §6.2) — one writer: lib/session-create/delegate-worker.js
|
|
20311
20730
|
insertWorkerSession, getWorkerSession, listWorkerTranscriptRows, updateWorkerSession, listLiveWorkerSessions, listWorkerSessionsByParent, listHiddenWorkerStartupTaskIds, listWorkerStartupTaskIds, listLatestTerminalWorkerSessionsByRoute, listTerminalWorkerSessionsSince,
|
|
20312
20731
|
// Schema version
|