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
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// Session transcript archiving (design:
|
|
4
|
-
// of every provider are discovered from disk at archive time, compressed and
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
3
|
+
// Session transcript archiving (design: transcript-never-removed-honest-resume-card-2026-10-24). Idle
|
|
4
|
+
// transcripts of every provider are discovered from disk at archive time, compressed, verified and kept as
|
|
5
|
+
// an ADDITIVE backup copy; a unit can be restored from the archive (or from a legacy trash entry). Runs only
|
|
6
|
+
// inside workers/transcript-archive-worker.js (or a test): it does filesystem work and hashing, and reaches
|
|
7
|
+
// the manifest table only through an injected async `store` (readRows / listByState / rowsForUnit /
|
|
8
|
+
// applyArchive / ctmSessionIds / ...), which the worker forwards to the db-owner op `transcriptManifestOp`.
|
|
9
|
+
// Nothing here opens a database or reads process.env.
|
|
10
10
|
//
|
|
11
|
-
// Rules this file keeps:
|
|
12
|
-
// -
|
|
13
|
-
//
|
|
14
|
-
//
|
|
11
|
+
// Rules this file keeps (user ruling 2026-10-24: "ctm should never ... remove transcript"):
|
|
12
|
+
// - an original under a provider transcript root is NEVER moved, renamed, truncated, modified or deleted
|
|
13
|
+
// by this file (D-1); the archiver only reads originals;
|
|
14
|
+
// - every byte CTM writes lands under <dataDir>/transcript-archive/<provider>/<sha[0:2]>/<sha256>.zst,
|
|
15
|
+
// with temp files under <dataDir>/transcript-archive/.incoming/ (D-3, D-4) -- never beside an original;
|
|
16
|
+
// - `archived` is terminal: the recorded version is in place, or the provider removed it and a verified
|
|
17
|
+
// copy of exactly that version exists (D-5). The states `original_trashed` and `cold` are read-only
|
|
18
|
+
// legacy values that no code path here writes any more;
|
|
15
19
|
// - nothing is copied or linked before a unit is archived: a transcript deleted from disk before then
|
|
16
20
|
// leaves no copy behind, and no manifest row either;
|
|
17
21
|
// - sha256 is computed by streaming, once, in the compress pass.
|
|
@@ -22,7 +26,6 @@ const path = require('node:path');
|
|
|
22
26
|
const crypto = require('node:crypto');
|
|
23
27
|
const { getDataDir } = require('./data-dir');
|
|
24
28
|
const { compressToFrames, verifyArchive, decodeFrame } = require('./transcript-archive-codec');
|
|
25
|
-
const { trashSessionArtifacts, findTrashedTranscript } = require('./session-transcript-trash');
|
|
26
29
|
const { RESTORED_GRACE_MS } = require('./transcript-source-identity');
|
|
27
30
|
const { digestPassForMember } = require('./session-action-digest');
|
|
28
31
|
const digestRunner = require('./transcript-digest-runner');
|
|
@@ -150,13 +153,13 @@ async function discoverClaudeUnits(opts = {}) {
|
|
|
150
153
|
}
|
|
151
154
|
|
|
152
155
|
// ---------------------------------------------------------------------------------------------------
|
|
153
|
-
// WP-
|
|
156
|
+
// WP-1: archive (D-16..D-19, D-22), scrub (D-23) and boot recovery. No grace purge any more.
|
|
154
157
|
//
|
|
155
|
-
// A unit is archived only when EVERY member is eligible;
|
|
156
|
-
//
|
|
158
|
+
// A unit is archived only when EVERY member is eligible; an original is never moved or deleted, at any
|
|
159
|
+
// step: `archived` is the terminal state. The order per member is
|
|
157
160
|
// 1 planned row -> 2 compress (one pass, also yields sha256) -> 3 fsync tmp -> 4 full decode verify ->
|
|
158
|
-
// 5 re-stat the source -> 6 link tmp to its
|
|
159
|
-
//
|
|
161
|
+
// 5 re-stat the source -> 6 link tmp to its content-addressed name under the data dir, unlink tmp,
|
|
162
|
+
// fsync dir -> 7 row `archived`, and the original stays exactly where the provider put it.
|
|
160
163
|
// Every manifest write goes through store.applyArchive.
|
|
161
164
|
// ---------------------------------------------------------------------------------------------------
|
|
162
165
|
|
|
@@ -165,12 +168,15 @@ const IDLE_MS = 7 * DAY_MS; // D-18: every member's last write at least this lon
|
|
|
165
168
|
const MIN_ARCHIVE_BYTES = 64 * 1024; // D-18: a unit whose members are all smaller is left alone
|
|
166
169
|
const RESERVE_BYTES = 1 << 30; // D-18: free space >= 2 x expected archive + this
|
|
167
170
|
const EXPECTED_RATIO = 8; // expected archive size = size / 8 (measured 88.0 GB -> 7.2 GB)
|
|
168
|
-
const PURGE_GRACE_MS = 7 * DAY_MS;
|
|
169
171
|
const SCRUB_MAX_BYTES = 2 * 1024 * 1024 * 1024;
|
|
170
|
-
const
|
|
172
|
+
const ARCHIVE_ROOT_SUBDIR = 'transcript-archive';
|
|
173
|
+
function archiveSubdirFor(provider) { return path.join(ARCHIVE_ROOT_SUBDIR, provider); }
|
|
174
|
+
// Backward-compatible alias for readers that only ever knew the Claude layout (D-4/S-6).
|
|
175
|
+
const ARCHIVE_SUBDIR = archiveSubdirFor('claude');
|
|
171
176
|
const INCOMING_DIR = '.incoming';
|
|
172
177
|
const TMP_RE = /\.zst\.tmp-\d+-\d+-\d+$/;
|
|
173
|
-
|
|
178
|
+
// D-5: read-only legacy states. Nothing in this file writes them any more; recovery (WP-2) reads them.
|
|
179
|
+
const LEGACY_REMOVED_STATES = new Set(['original_trashed', 'cold']);
|
|
174
180
|
const WORK_STATES = new Set(['archived']);
|
|
175
181
|
// Rows no code writes any more (dev databases only): deleted at the start of a run, no file is touched.
|
|
176
182
|
const LEGACY_STATES = ['pinned', 'survivor', 'lost'];
|
|
@@ -190,7 +196,8 @@ function _archiveContext(opts) {
|
|
|
190
196
|
ctx.minBytes = Number.isFinite(Number(opts.minBytes)) && opts.minBytes !== undefined ? Number(opts.minBytes) : MIN_ARCHIVE_BYTES;
|
|
191
197
|
ctx.reserveBytes = Number.isFinite(Number(opts.reserveBytes)) && opts.reserveBytes !== undefined ? Number(opts.reserveBytes) : RESERVE_BYTES;
|
|
192
198
|
ctx.failpoint = typeof opts.failpoint === 'function' ? opts.failpoint : null;
|
|
193
|
-
ctx.archiveRoot = path.join(ctx.dataDir,
|
|
199
|
+
ctx.archiveRoot = path.join(ctx.dataDir, ARCHIVE_ROOT_SUBDIR);
|
|
200
|
+
// D-5: the archiver never moves an original, so nothing here can trash or purge one.
|
|
194
201
|
ctx.counters = { archived: 0, bytesIn: 0, bytesOut: 0, trashed: 0, purged: 0, verified: 0, corrupt: 0, skipped: {} };
|
|
195
202
|
return ctx;
|
|
196
203
|
}
|
|
@@ -212,11 +219,6 @@ async function _step(ctx, n, info) {
|
|
|
212
219
|
}
|
|
213
220
|
}
|
|
214
221
|
|
|
215
|
-
function _rootsForTrash(ctx) {
|
|
216
|
-
const unset = (name) => path.join(ctx.dataDir, `.unset-root-${name}`);
|
|
217
|
-
return [ctx.projectsRoot, ctx.walleSessionsDir || unset('walle'), ctx.codexSessionsDir || unset('codex')];
|
|
218
|
-
}
|
|
219
|
-
|
|
220
222
|
function _under(root, p) {
|
|
221
223
|
const rel = path.relative(root, p);
|
|
222
224
|
return Boolean(rel) && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
@@ -330,18 +332,15 @@ async function _publish(ctx, tmp, finalWanted, res, contentAddressed) {
|
|
|
330
332
|
throw Object.assign(new Error('archive path is occupied by different bytes'), { code: 'archive_path_occupied' });
|
|
331
333
|
}
|
|
332
334
|
|
|
335
|
+
// D-4: every provider's archive object is content-addressed under CTM's data dir, never beside the source.
|
|
333
336
|
function _finalPathFor(ctx, unit, member, res) {
|
|
334
|
-
|
|
335
|
-
return path.join(ctx.archiveRoot, res.sha256.slice(0, 2), `${res.sha256}.zst`);
|
|
336
|
-
}
|
|
337
|
-
return `${member.sourcePath}.zst`;
|
|
337
|
+
return path.join(ctx.dataDir, archiveSubdirFor(unit.provider), res.sha256.slice(0, 2), `${res.sha256}.zst`);
|
|
338
338
|
}
|
|
339
339
|
|
|
340
340
|
function _tmpPathFor(ctx, unit, member) {
|
|
341
341
|
tmpSeq += 1;
|
|
342
342
|
const tag = `.zst.tmp-${process.pid}-${ctx.now()}-${tmpSeq}`;
|
|
343
|
-
|
|
344
|
-
return `${member.sourcePath}${tag}`;
|
|
343
|
+
return path.join(ctx.dataDir, ARCHIVE_ROOT_SUBDIR, INCOMING_DIR, `${path.basename(member.sourcePath)}${tag}`);
|
|
345
344
|
}
|
|
346
345
|
|
|
347
346
|
// Steps 1..7 for one member. Returns { row } (state archived) or { aborted: reason }.
|
|
@@ -355,7 +354,7 @@ async function _archiveMember(ctx, unit, member, track) {
|
|
|
355
354
|
await _step(ctx, 1, { member });
|
|
356
355
|
|
|
357
356
|
const tmp = _tmpPathFor(ctx, unit, member);
|
|
358
|
-
|
|
357
|
+
await ctx.fsp.mkdir(path.dirname(tmp), { recursive: true });
|
|
359
358
|
track.tmp = tmp;
|
|
360
359
|
const digest = digestPassForMember(unit, member, size); // D-24: the compress pass is also the digest pass
|
|
361
360
|
let res;
|
|
@@ -368,7 +367,7 @@ async function _archiveMember(ctx, unit, member, track) {
|
|
|
368
367
|
}
|
|
369
368
|
await _step(ctx, 2, { member, tmp });
|
|
370
369
|
|
|
371
|
-
if (unit.provider !== 'claude') await ctx.fsp.utimes(tmp, st0.atime, st0.mtime); // Codex orders rollouts by mtime
|
|
370
|
+
if (unit.provider !== 'claude') await ctx.fsp.utimes(tmp, st0.atime, st0.mtime); // Codex orders rollouts by mtime (the archive copy only)
|
|
372
371
|
await _fsyncFile(ctx, tmp);
|
|
373
372
|
await _step(ctx, 3, { member, tmp });
|
|
374
373
|
|
|
@@ -382,13 +381,10 @@ async function _archiveMember(ctx, unit, member, track) {
|
|
|
382
381
|
return { aborted: 'source_changed' };
|
|
383
382
|
}
|
|
384
383
|
await _step(ctx, 5, { member, tmp });
|
|
385
|
-
if (digest) await ctx.store.replaceDigest(member.sourcePath, digest.finish()); // fail closed: no digest, no
|
|
384
|
+
if (digest) await ctx.store.replaceDigest(member.sourcePath, digest.finish()); // fail closed: no digest, no archive
|
|
386
385
|
|
|
387
386
|
const wanted = _finalPathFor(ctx, unit, member, res);
|
|
388
|
-
|
|
389
|
-
await _unlinkIfPresent(ctx, wanted); // our own stale archive of an older version of this source
|
|
390
|
-
}
|
|
391
|
-
const final = await _publish(ctx, tmp, wanted, res, unit.provider === 'claude');
|
|
387
|
+
const final = await _publish(ctx, tmp, wanted, res, true);
|
|
392
388
|
await _unlinkIfPresent(ctx, tmp);
|
|
393
389
|
track.tmp = '';
|
|
394
390
|
await _fsyncDir(ctx, path.dirname(final));
|
|
@@ -429,23 +425,6 @@ function _claudeProjectDir(ctx, members) {
|
|
|
429
425
|
return path.join(ctx.projectsRoot, rel.split(path.sep)[0]);
|
|
430
426
|
}
|
|
431
427
|
|
|
432
|
-
// An `archived` row whose original is gone: the crash window between the trash move and step 8's row
|
|
433
|
-
// write. Looks the original up in the trash manifests; '' when it cannot be proven to be there.
|
|
434
|
-
async function _findInTrash(ctx, member, unitId) {
|
|
435
|
-
if (!ctx.trashRoot) return '';
|
|
436
|
-
const names = new Set([path.basename(member.sourcePath)]);
|
|
437
|
-
if (unitId) names.add(unitId);
|
|
438
|
-
for (const fileName of names) {
|
|
439
|
-
const hit = await findTrashedTranscript({ trashRoot: ctx.trashRoot, fileName, fs: ctx.fsp });
|
|
440
|
-
if (!hit) continue;
|
|
441
|
-
const src = member.sourcePath;
|
|
442
|
-
if (hit.originalPath !== src && !src.startsWith(hit.originalPath + path.sep)) continue;
|
|
443
|
-
const trashPath = hit.trashPath + src.slice(hit.originalPath.length);
|
|
444
|
-
if (await _statOrNull(ctx, trashPath)) return trashPath;
|
|
445
|
-
}
|
|
446
|
-
return '';
|
|
447
|
-
}
|
|
448
|
-
|
|
449
428
|
async function _loadMembers(ctx, unit) {
|
|
450
429
|
if (unit.provider === 'claude') {
|
|
451
430
|
// The members found on disk, joined with their manifest rows, plus the rows of the unit whose file is not
|
|
@@ -462,7 +441,7 @@ async function _loadMembers(ctx, unit) {
|
|
|
462
441
|
return unit.members.map((m) => ({ ...m, row: rows.get(m.sourcePath) || null }));
|
|
463
442
|
}
|
|
464
443
|
|
|
465
|
-
// D-18 for one unit. Returns { skip } or { work: members still to be archived
|
|
444
|
+
// D-18 for one unit. Returns { skip } or { work: members still to be archived, total }.
|
|
466
445
|
async function _eligibility(ctx, unit, members) {
|
|
467
446
|
if (!ctx.isPrimary) return { skip: 'not_primary' };
|
|
468
447
|
if (ctx.live.has(String(unit.ctmSessionId)) || ctx.live.has(String(unit.unitId))) return { skip: 'live_session' };
|
|
@@ -471,8 +450,16 @@ async function _eligibility(ctx, unit, members) {
|
|
|
471
450
|
const work = [];
|
|
472
451
|
for (const member of members) {
|
|
473
452
|
const state = member.row ? member.row.state : '';
|
|
474
|
-
|
|
453
|
+
// D-5: a unit holding a legacy removed row is left entirely alone -- WP-2 restores it, never this file.
|
|
454
|
+
if (LEGACY_REMOVED_STATES.has(state)) return { skip: 'awaiting_recovery' };
|
|
475
455
|
if (state && !WORK_STATES.has(state) && state !== 'planned') return { skip: 'unit_not_archivable' };
|
|
456
|
+
if (state === 'archived') {
|
|
457
|
+
const st = await _statOrNull(ctx, member.sourcePath);
|
|
458
|
+
if (!st) continue; // D-6: the provider removed it; the archive is the only copy and rung `restore` serves it
|
|
459
|
+
member.stat = st;
|
|
460
|
+
if (_sameSource(st, member.row)) continue; // D-5: the recorded version is in place -- done
|
|
461
|
+
// D-6: the original changed (resumed/appended) -- re-archive it and replace the row below.
|
|
462
|
+
}
|
|
476
463
|
work.push(member);
|
|
477
464
|
}
|
|
478
465
|
if (!work.length) return { skip: 'already_archived' };
|
|
@@ -480,17 +467,13 @@ async function _eligibility(ctx, unit, members) {
|
|
|
480
467
|
let large = false;
|
|
481
468
|
const now = ctx.now();
|
|
482
469
|
for (const member of work) {
|
|
483
|
-
const st = await _statOrNull(ctx, member.sourcePath);
|
|
484
|
-
if (!st) {
|
|
485
|
-
if (member.row && member.row.state === 'archived') { member.missing = true; continue; }
|
|
486
|
-
return { skip: 'member_missing' };
|
|
487
|
-
}
|
|
470
|
+
const st = member.stat || await _statOrNull(ctx, member.sourcePath);
|
|
471
|
+
if (!st) return { skip: 'member_missing' };
|
|
488
472
|
if (now - _mtimeOf(st) < ctx.idleMs) return { skip: 'not_idle' };
|
|
489
473
|
member.stat = st;
|
|
490
474
|
total += Number(st.size);
|
|
491
475
|
if (Number(st.size) >= ctx.minBytes) large = true;
|
|
492
476
|
}
|
|
493
|
-
if (work.every((m) => m.missing)) return { work, total };
|
|
494
477
|
if (!large) return { skip: 'too_small' };
|
|
495
478
|
if (unit.provider === 'claude') {
|
|
496
479
|
const projectDir = _claudeProjectDir(ctx, members);
|
|
@@ -499,7 +482,7 @@ async function _eligibility(ctx, unit, members) {
|
|
|
499
482
|
const onDisk = await _enumerateClaudeUnit(ctx, projectDir, unit.unitId);
|
|
500
483
|
if (onDisk.some((p) => !known.has(p))) return { skip: 'unit_changed' };
|
|
501
484
|
}
|
|
502
|
-
const jsonl = work.filter((m) =>
|
|
485
|
+
const jsonl = work.filter((m) => /\.jsonl$/.test(m.sourcePath) && (m.memberKind === 'main' || m.memberKind === 'subagent'));
|
|
503
486
|
if (jsonl.length) {
|
|
504
487
|
const cursors = await ctx.store.importCursors(jsonl.map((m) => m.sourcePath));
|
|
505
488
|
for (const member of jsonl) {
|
|
@@ -508,93 +491,14 @@ async function _eligibility(ctx, unit, members) {
|
|
|
508
491
|
}
|
|
509
492
|
}
|
|
510
493
|
if (typeof ctx.fsp.statfs === 'function') {
|
|
511
|
-
|
|
512
|
-
const fsStat = await ctx.fsp.statfs(
|
|
494
|
+
// D-4: the archive (and its temp) always lands on the data dir's filesystem, whatever the provider.
|
|
495
|
+
const fsStat = await ctx.fsp.statfs(ctx.dataDir);
|
|
513
496
|
const free = Number(fsStat.bavail) * Number(fsStat.bsize);
|
|
514
497
|
if (free < 2 * Math.ceil(total / EXPECTED_RATIO) + ctx.reserveBytes) return { skip: 'low_disk' };
|
|
515
498
|
}
|
|
516
499
|
return { work, total };
|
|
517
500
|
}
|
|
518
501
|
|
|
519
|
-
function _trashPathFor(entries, sourcePath) {
|
|
520
|
-
for (const entry of entries) {
|
|
521
|
-
if (entry.originalPath === sourcePath) return entry.trashPath;
|
|
522
|
-
if (sourcePath.startsWith(entry.originalPath + path.sep)) return entry.trashPath + sourcePath.slice(entry.originalPath.length);
|
|
523
|
-
}
|
|
524
|
-
return '';
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
// Step 8 for every member whose row is `archived` with the original still in place.
|
|
528
|
-
async function _trashUnit(ctx, unit, members) {
|
|
529
|
-
const archived = [];
|
|
530
|
-
for (const member of members) {
|
|
531
|
-
if (member.row && member.row.state === 'archived' && await _statOrNull(ctx, member.sourcePath)) archived.push(member);
|
|
532
|
-
}
|
|
533
|
-
if (!archived.length) return { trashed: 0 };
|
|
534
|
-
// Nothing moves unless every original is exactly the version that was archived.
|
|
535
|
-
const changed = [];
|
|
536
|
-
for (const member of archived) {
|
|
537
|
-
if (!_sameSource(await _statOrNull(ctx, member.sourcePath), member.row)) changed.push(member);
|
|
538
|
-
}
|
|
539
|
-
if (changed.length) {
|
|
540
|
-
for (const member of changed) {
|
|
541
|
-
const change = _revertChange(ctx, unit, member);
|
|
542
|
-
await ctx.store.applyArchive(change);
|
|
543
|
-
}
|
|
544
|
-
_skip(ctx, 'source_changed');
|
|
545
|
-
return { trashed: 0, skipped: 'source_changed' };
|
|
546
|
-
}
|
|
547
|
-
const paths = [];
|
|
548
|
-
let unitDir = '';
|
|
549
|
-
if (unit.provider === 'claude') {
|
|
550
|
-
const projectDir = _claudeProjectDir(ctx, members);
|
|
551
|
-
const known = new Set(members.map((m) => m.sourcePath));
|
|
552
|
-
const onDisk = await _enumerateClaudeUnit(ctx, projectDir, unit.unitId);
|
|
553
|
-
if (onDisk.some((p) => !known.has(p))) {
|
|
554
|
-
_skip(ctx, 'unit_changed');
|
|
555
|
-
return { trashed: 0, skipped: 'unit_changed' };
|
|
556
|
-
}
|
|
557
|
-
const dir = path.join(projectDir, unit.unitId);
|
|
558
|
-
if (await _statOrNull(ctx, dir)) unitDir = dir;
|
|
559
|
-
}
|
|
560
|
-
const archivedPaths = new Set(archived.map((m) => m.sourcePath));
|
|
561
|
-
for (const member of archived) {
|
|
562
|
-
if (unitDir && member.sourcePath.startsWith(unitDir + path.sep)) continue; // moves with its directory
|
|
563
|
-
paths.push(member.sourcePath);
|
|
564
|
-
}
|
|
565
|
-
if (unitDir) {
|
|
566
|
-
const hasAnchor = [`${unit.unitId}.jsonl`, `${unit.unitId}.jsonl.bak`].some((n) => archivedPaths.has(path.join(path.dirname(unitDir), n)));
|
|
567
|
-
if (hasAnchor) paths.push(unitDir);
|
|
568
|
-
else for (const member of archived) if (member.sourcePath.startsWith(unitDir + path.sep)) paths.push(member.sourcePath);
|
|
569
|
-
}
|
|
570
|
-
const result = await trashSessionArtifacts({
|
|
571
|
-
paths,
|
|
572
|
-
roots: _rootsForTrash(ctx),
|
|
573
|
-
reason: 'archived',
|
|
574
|
-
ctmSessionId: unit.ctmSessionId || null,
|
|
575
|
-
isPrimary: true,
|
|
576
|
-
trashRoot: ctx.trashRoot,
|
|
577
|
-
fs: ctx.fsp,
|
|
578
|
-
now: () => new Date(ctx.now()),
|
|
579
|
-
warn: () => {},
|
|
580
|
-
error: (message) => ctx.stats.errors.push({ path: unit.unitId, code: String(message).slice(0, 200) }),
|
|
581
|
-
});
|
|
582
|
-
await _step(ctx, 8, { unit });
|
|
583
|
-
const now = ctx.now();
|
|
584
|
-
const upserts = [];
|
|
585
|
-
for (const member of archived) {
|
|
586
|
-
const trashPath = _trashPathFor(result.entries, member.sourcePath);
|
|
587
|
-
if (!trashPath) continue;
|
|
588
|
-
upserts.push({ ...member.row, state: 'original_trashed', trash_path: trashPath, state_changed_at_ms: now, last_error: '' });
|
|
589
|
-
}
|
|
590
|
-
if (upserts.length) await ctx.store.applyArchive({ upserts, deletes: [] });
|
|
591
|
-
if (upserts.length < archived.length) {
|
|
592
|
-
ctx.stats.errors.push({ path: unit.unitId, code: 'trash_incomplete' });
|
|
593
|
-
}
|
|
594
|
-
ctx.counters.trashed += upserts.length;
|
|
595
|
-
return { trashed: upserts.length };
|
|
596
|
-
}
|
|
597
|
-
|
|
598
502
|
async function _failMember(ctx, unit, track, err) {
|
|
599
503
|
if (track.tmp) await _unlinkIfPresent(ctx, track.tmp).catch((cleanup) => ctx.stats.errors.push({ path: track.tmp, code: cleanup && cleanup.code }));
|
|
600
504
|
if (track.member) {
|
|
@@ -604,30 +508,18 @@ async function _failMember(ctx, unit, track, err) {
|
|
|
604
508
|
}
|
|
605
509
|
|
|
606
510
|
/**
|
|
607
|
-
* D-19. Archive one unit
|
|
511
|
+
* D-19. Archive one unit as an additive backup. unit = { unitId, provider, ctmSessionId,
|
|
608
512
|
* members: [{ sourcePath, memberKind }] } (Claude members come from the manifest rows of the unit).
|
|
609
|
-
*
|
|
513
|
+
* Originals are only ever read: no member is moved, modified or deleted.
|
|
514
|
+
* @returns {{ archived: number, bytesIn: number, bytesOut: number, skipped: string, errors: object[] }}
|
|
610
515
|
*/
|
|
611
516
|
async function archiveUnit(unit, opts = {}) {
|
|
612
517
|
const ctx = _archiveContext(opts);
|
|
613
|
-
const out = { archived: 0,
|
|
518
|
+
const out = { archived: 0, bytesIn: 0, bytesOut: 0, skipped: '', errors: [] };
|
|
614
519
|
const finish = () => { out.errors = ctx.stats.errors.splice(0); return out; };
|
|
615
520
|
if (!ctx.isPrimary) { out.skipped = _skip(ctx, 'not_primary').skipped; return finish(); }
|
|
616
|
-
if (!ctx.trashRoot) throw new TypeError('transcript archiver: opts.trashRoot is required');
|
|
617
521
|
const members = await _loadMembers(ctx, unit);
|
|
618
522
|
|
|
619
|
-
// A crash between the trash move and its row write: find the original in the trash and finish the row.
|
|
620
|
-
for (const member of members) {
|
|
621
|
-
if (!member.row || member.row.state !== 'archived') continue;
|
|
622
|
-
if (await _statOrNull(ctx, member.sourcePath)) continue;
|
|
623
|
-
const trashPath = await _findInTrash(ctx, member, unit.provider === 'claude' ? unit.unitId : '');
|
|
624
|
-
if (trashPath) {
|
|
625
|
-
member.row = { ...member.row, state: 'original_trashed', trash_path: trashPath, state_changed_at_ms: ctx.now(), last_error: '' };
|
|
626
|
-
await ctx.store.applyArchive({ upserts: [member.row], deletes: [] });
|
|
627
|
-
ctx.counters.trashed += 1;
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
|
|
631
523
|
const eligible = await _eligibility(ctx, unit, members);
|
|
632
524
|
if (eligible.skip) { out.skipped = _skip(ctx, eligible.skip).skipped; return finish(); }
|
|
633
525
|
|
|
@@ -635,7 +527,6 @@ async function archiveUnit(unit, opts = {}) {
|
|
|
635
527
|
try {
|
|
636
528
|
for (const member of eligible.work) {
|
|
637
529
|
track.member = null;
|
|
638
|
-
if (member.missing) continue; // archived earlier, original already gone (trash lookup failed): nothing to do
|
|
639
530
|
const row = member.row;
|
|
640
531
|
if (row && row.state === 'archived' && _sameSource(member.stat, row) && await _statOrNull(ctx, row.archive_path)) continue; // finished earlier
|
|
641
532
|
const result = await _archiveMember(ctx, unit, member, track);
|
|
@@ -650,9 +541,6 @@ async function archiveUnit(unit, opts = {}) {
|
|
|
650
541
|
out.bytesIn += result.bytesIn;
|
|
651
542
|
out.bytesOut += result.bytesOut;
|
|
652
543
|
}
|
|
653
|
-
const trashed = await _trashUnit(ctx, unit, members);
|
|
654
|
-
out.trashed = trashed.trashed;
|
|
655
|
-
if (trashed.skipped) out.skipped = trashed.skipped;
|
|
656
544
|
} catch (err) {
|
|
657
545
|
if (err && err.failpoint) throw err;
|
|
658
546
|
await _failMember(ctx, unit, track, err);
|
|
@@ -664,7 +552,7 @@ async function archiveUnit(unit, opts = {}) {
|
|
|
664
552
|
return finish();
|
|
665
553
|
}
|
|
666
554
|
|
|
667
|
-
// ----
|
|
555
|
+
// ---- scrub --------------------------------------------------------------------------------------
|
|
668
556
|
|
|
669
557
|
function _framesOf(row) {
|
|
670
558
|
try {
|
|
@@ -711,44 +599,6 @@ async function _removeEmptyParents(ctx, start, stopAt) {
|
|
|
711
599
|
}
|
|
712
600
|
}
|
|
713
601
|
|
|
714
|
-
// 7 days after the move: decode + hash the archive again; only then unlink the trash entry (-> `cold`). A failed verify sets `corrupt` and deletes nothing.
|
|
715
|
-
async function purgeGraceExpired(opts = {}) {
|
|
716
|
-
const ctx = _archiveContext(opts);
|
|
717
|
-
const graceMs = Number.isFinite(Number(opts.graceMs)) && opts.graceMs !== undefined ? Number(opts.graceMs) : PURGE_GRACE_MS;
|
|
718
|
-
if (!ctx.isPrimary) { _skip(ctx, 'not_primary'); return { purged: 0, corrupt: 0, verified: 0, skipped: { not_primary: 1 }, errors: [] }; }
|
|
719
|
-
const out = { purged: 0, corrupt: 0, verified: 0, skipped: {}, errors: [] };
|
|
720
|
-
const rows = await ctx.store.listByState({
|
|
721
|
-
states: ['original_trashed'], olderThanMs: ctx.now() - graceMs, orderBy: 'changed', limit: Number(opts.limit) || 200,
|
|
722
|
-
});
|
|
723
|
-
const verify = _verifier(ctx);
|
|
724
|
-
for (const row of rows) {
|
|
725
|
-
try {
|
|
726
|
-
const verdict = await verify(row);
|
|
727
|
-
if (!verdict.ok) {
|
|
728
|
-
await ctx.store.applyArchive({ upserts: [{ ...row, state: 'corrupt', state_changed_at_ms: ctx.now(), last_error: `purge_${verdict.code}` }], deletes: [] });
|
|
729
|
-
out.corrupt += 1;
|
|
730
|
-
continue;
|
|
731
|
-
}
|
|
732
|
-
out.verified += 1;
|
|
733
|
-
if (row.trash_path && ctx.trashRoot && _under(ctx.trashRoot, row.trash_path)) {
|
|
734
|
-
await _unlinkIfPresent(ctx, row.trash_path);
|
|
735
|
-
await _removeEmptyParents(ctx, row.trash_path, ctx.trashRoot);
|
|
736
|
-
}
|
|
737
|
-
await ctx.store.applyArchive({
|
|
738
|
-
upserts: [{ ...row, state: 'cold', trash_path: '', pin_path: '', last_verified_at_ms: ctx.now(), state_changed_at_ms: ctx.now(), last_error: '' }],
|
|
739
|
-
deletes: [],
|
|
740
|
-
});
|
|
741
|
-
out.purged += 1;
|
|
742
|
-
} catch (err) {
|
|
743
|
-
out.errors.push({ path: row.source_path, code: (err && err.code) || String((err && err.message) || err) });
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
ctx.counters.purged += out.purged;
|
|
747
|
-
ctx.counters.verified += out.verified;
|
|
748
|
-
ctx.counters.corrupt += out.corrupt;
|
|
749
|
-
return out;
|
|
750
|
-
}
|
|
751
|
-
|
|
752
602
|
// D-23. Re-verify the least recently verified archives, up to `maxBytes` of archive bytes per run.
|
|
753
603
|
async function scrub(opts = {}) {
|
|
754
604
|
const ctx = _archiveContext(opts);
|
|
@@ -794,12 +644,13 @@ async function _removeTmpFiles(ctx, dir, out) {
|
|
|
794
644
|
}
|
|
795
645
|
|
|
796
646
|
/**
|
|
797
|
-
* Removes `*.zst.tmp-*` leftovers in the archive dirs, deletes rows stuck in `planned`, and repairs
|
|
798
|
-
*
|
|
647
|
+
* Removes `*.zst.tmp-*` leftovers in the archive dirs, deletes rows stuck in `planned`, and repairs rows
|
|
648
|
+
* whose archive went missing (a row is only ever dropped or marked `corrupt`). Never touches an original:
|
|
649
|
+
* D-5, an `archived` row whose original is gone is done, not repaired.
|
|
799
650
|
*/
|
|
800
651
|
async function recoverAtBoot(opts = {}) {
|
|
801
652
|
const ctx = _archiveContext(opts);
|
|
802
|
-
const out = { tmpRemoved: 0, deleted: 0, archiveMissing: 0,
|
|
653
|
+
const out = { tmpRemoved: 0, deleted: 0, archiveMissing: 0, errors: [] };
|
|
803
654
|
if (!ctx.isPrimary) return out;
|
|
804
655
|
await _removeTmpFiles(ctx, ctx.archiveRoot, out);
|
|
805
656
|
if (ctx.codexSessionsDir) await _removeTmpFiles(ctx, ctx.codexSessionsDir, out);
|
|
@@ -825,12 +676,6 @@ async function recoverAtBoot(opts = {}) {
|
|
|
825
676
|
} else {
|
|
826
677
|
await ctx.store.applyArchive({ upserts: [{ ...row, state: 'corrupt', state_changed_at_ms: now, last_error: 'archive_missing' }], deletes: [] });
|
|
827
678
|
}
|
|
828
|
-
} else if (row.state === 'archived' && !source) {
|
|
829
|
-
const trashPath = await _findInTrash(ctx, { sourcePath: row.source_path }, row.provider === 'claude' ? row.unit_id : '');
|
|
830
|
-
if (trashPath) {
|
|
831
|
-
await ctx.store.applyArchive({ upserts: [{ ...row, state: 'original_trashed', trash_path: trashPath, state_changed_at_ms: now, last_error: '' }], deletes: [] });
|
|
832
|
-
out.trashRecovered += 1;
|
|
833
|
-
}
|
|
834
679
|
}
|
|
835
680
|
} catch (err) {
|
|
836
681
|
out.errors.push({ path: row.source_path, code: (err && err.code) || String((err && err.message) || err) });
|
|
@@ -906,11 +751,11 @@ async function _retireRows(ctx) {
|
|
|
906
751
|
}
|
|
907
752
|
|
|
908
753
|
/**
|
|
909
|
-
* One archive run (scheduler job transcript-archive): boot recovery (first run only),
|
|
910
|
-
* units archived oldest first,
|
|
754
|
+
* One archive run (scheduler job transcript-archive): boot recovery (first run only), the D-7 recovery pass,
|
|
755
|
+
* up to `maxUnits` units archived oldest first, and a scrub when `scrub` is set. Every provider's units are
|
|
911
756
|
* discovered from disk (Claude by project directory, Codex and Wall-E by path), resumable by the cursors
|
|
912
757
|
* `claudeAfter` / `codexAfter` / `walleAfter`; a unit not idle for the threshold is skipped. Nothing runs
|
|
913
|
-
* while the host is shedding load. Non-primary instances do nothing.
|
|
758
|
+
* while the host is shedding load. Non-primary instances do nothing. Originals are never moved or deleted.
|
|
914
759
|
*/
|
|
915
760
|
async function runArchiveCycle(opts = {}) {
|
|
916
761
|
const ctx = _archiveContext(opts);
|
|
@@ -919,16 +764,15 @@ async function runArchiveCycle(opts = {}) {
|
|
|
919
764
|
if (opts.recover) result.recovered = await recoverAtBoot({ ...opts, _ctx: ctx });
|
|
920
765
|
if (ctx.hostShedding) { _skip(ctx, 'host_shedding'); return result; }
|
|
921
766
|
await _retireRows(ctx);
|
|
767
|
+
// D-7: put every original the pre-WP-1 archiver moved away back BEFORE discovery, so a unit restored here is
|
|
768
|
+
// never re-archived in the same run. Its unit budget is its own constant: the archive pass's `maxUnits`
|
|
769
|
+
// (20 per hour, lib/transcript-guard-job.js) must not throttle a recovery that has thousands of files to
|
|
770
|
+
// put back.
|
|
771
|
+
result.restoredOriginals = await restoreRemovedOriginals({ ...opts, _ctx: ctx, maxUnits: RESTORE_MAX_UNITS });
|
|
922
772
|
|
|
923
773
|
const now = ctx.now();
|
|
924
774
|
const maxUnits = Math.max(1, Number(opts.maxUnits) || 20);
|
|
925
775
|
const byUnit = new Map();
|
|
926
|
-
// An `archived` row whose original is already gone (the crash window before the trash move) is finished from the row.
|
|
927
|
-
for (const row of await ctx.store.listByState({ states: ['archived'], mtimeBeforeMs: now - ctx.idleMs, orderBy: 'mtime', limit: 2000 })) {
|
|
928
|
-
if (row.provider !== 'claude') continue;
|
|
929
|
-
if (!byUnit.has(row.unit_id)) byUnit.set(row.unit_id, { unitId: row.unit_id, provider: 'claude', ctmSessionId: row.ctm_session_id, members: [], mtimeMs: row.source_mtime_ms });
|
|
930
|
-
byUnit.get(row.unit_id).members.push({ sourcePath: row.source_path, memberKind: row.member_kind });
|
|
931
|
-
}
|
|
932
776
|
const found = await discoverClaudeUnits({ _ctx: ctx, afterPath: opts.claudeAfter || '', limit: opts.scanLimit });
|
|
933
777
|
result.nextClaudeAfter = found.nextAfter;
|
|
934
778
|
for (const unit of found.units) {
|
|
@@ -964,13 +808,11 @@ async function runArchiveCycle(opts = {}) {
|
|
|
964
808
|
try {
|
|
965
809
|
const r = await archiveUnit(unit, { ...opts, _ctx: ctx });
|
|
966
810
|
result.errors.push(...r.errors);
|
|
967
|
-
if (r.archived > 0
|
|
811
|
+
if (r.archived > 0) archivedUnits += 1;
|
|
968
812
|
} catch (err) {
|
|
969
813
|
result.errors.push({ path: unit.unitId, code: (err && err.code) || String((err && err.message) || err) });
|
|
970
814
|
}
|
|
971
815
|
}
|
|
972
|
-
const purge = await purgeGraceExpired({ ...opts, _ctx: ctx });
|
|
973
|
-
result.errors.push(...purge.errors);
|
|
974
816
|
if (opts.scrub) {
|
|
975
817
|
const scrubbed = await scrub({ ...opts, _ctx: ctx, maxBytes: opts.scrubMaxBytes });
|
|
976
818
|
result.errors.push(...scrubbed.errors);
|
|
@@ -1106,7 +948,10 @@ async function _restoreMember(ctx, row) {
|
|
|
1106
948
|
if (!await _matchesRow(ctx, target, row)) throw _restoreError('target_exists');
|
|
1107
949
|
} else {
|
|
1108
950
|
({ how, trashPath = '' } = await _placeMember(ctx, row));
|
|
1109
|
-
|
|
951
|
+
// `now` (default) is today's behaviour; `recorded` puts the manifest's own source_mtime_ms back so a
|
|
952
|
+
// recovery pass can restore a file exactly as the provider last saw it.
|
|
953
|
+
const when = ctx.restoreMtime === 'recorded' ? new Date(Number(row.source_mtime_ms)) : new Date(ctx.now());
|
|
954
|
+
await ctx.fsp.utimes(target, when, when);
|
|
1110
955
|
await _fsyncDir(ctx, path.dirname(target));
|
|
1111
956
|
}
|
|
1112
957
|
const st = await ctx.fsp.stat(target); // number-valued, as the importer's own stat reports
|
|
@@ -1136,9 +981,12 @@ async function _restoreMember(ctx, row) {
|
|
|
1136
981
|
}
|
|
1137
982
|
|
|
1138
983
|
/** D-20. Restore a unit (id, { unitId } or { sourcePath }); the store needs restampIdentity. Returns
|
|
1139
|
-
* { restored: [{path, how}], plain: [path], failed: [{path, code}], skipped } (`plain` = already in place).
|
|
984
|
+
* { restored: [{path, how}], plain: [path], failed: [{path, code}], skipped } (`plain` = already in place).
|
|
985
|
+
* `opts.mtime` is `'now'` (default; the restored file is stamped with today's time) or `'recorded'` (the
|
|
986
|
+
* row's own source_mtime_ms is put back for both atime and mtime). */
|
|
1140
987
|
async function restoreUnit(unit, opts = {}) {
|
|
1141
988
|
const ctx = _archiveContext(opts);
|
|
989
|
+
ctx.restoreMtime = opts.mtime === 'recorded' ? 'recorded' : 'now';
|
|
1142
990
|
const out = { restored: [], plain: [], failed: [], skipped: '' };
|
|
1143
991
|
if (!ctx.isPrimary) { out.skipped = 'not_primary'; return out; }
|
|
1144
992
|
if (typeof ctx.store.restampIdentity !== 'function') throw new TypeError('transcript archiver: store.restampIdentity is required to restore');
|
|
@@ -1165,17 +1013,169 @@ async function restoreUnit(unit, opts = {}) {
|
|
|
1165
1013
|
return out;
|
|
1166
1014
|
}
|
|
1167
1015
|
|
|
1016
|
+
// ---------------------------------------------------------------------------------------------------
|
|
1017
|
+
// WP-2 / D-7 recovery: put every original the pre-WP-1 archiver moved away back to its `source_path`.
|
|
1018
|
+
// ---------------------------------------------------------------------------------------------------
|
|
1019
|
+
|
|
1020
|
+
const RESTORE_FLOOR_BYTES = 10 * 1024 * 1024 * 1024; // D-7: never decode into the last bytes of a disk
|
|
1021
|
+
const RESTORE_MAX_DECODE_BYTES = 4 * 1024 * 1024 * 1024; // D-7: decoded bytes per call
|
|
1022
|
+
const RESTORE_MAX_UNITS = 200; // D-7: units per call (a trash link-back is cheap)
|
|
1023
|
+
|
|
1024
|
+
// Free bytes on the filesystem that will hold the restored file. An absent directory is resolved to its
|
|
1025
|
+
// nearest existing ancestor: restoreUnit creates the directory (same filesystem, barring a mount point inside
|
|
1026
|
+
// it), so this is the space the decode actually has. null = nothing on the chain could be read.
|
|
1027
|
+
async function _freeBytesAt(ctx, dir) {
|
|
1028
|
+
let probe = dir;
|
|
1029
|
+
for (;;) {
|
|
1030
|
+
try {
|
|
1031
|
+
const st = await ctx.fsp.statfs(probe);
|
|
1032
|
+
return Number(st.bavail) * Number(st.bsize);
|
|
1033
|
+
} catch (err) {
|
|
1034
|
+
if (!err || (err.code !== 'ENOENT' && err.code !== 'ENOTDIR')) throw err;
|
|
1035
|
+
}
|
|
1036
|
+
const up = path.dirname(probe);
|
|
1037
|
+
if (up === probe) return null;
|
|
1038
|
+
probe = up;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// D-9: `row.archive_path` is provably CTM's own legacy sibling copy of this original -- a `.jsonl.zst` beside
|
|
1043
|
+
// the source, outside the data dir, named exactly `<source_path>.zst`, holding the bytes the row recorded.
|
|
1044
|
+
async function _isOwnSiblingArchive(ctx, row) {
|
|
1045
|
+
const p = String(row.archive_path || '');
|
|
1046
|
+
if (!p.endsWith('.jsonl.zst') || p !== `${row.source_path}.zst`) return false;
|
|
1047
|
+
if (_under(ctx.dataDir, p)) return false;
|
|
1048
|
+
if (!row.archive_sha256 || !await _statOrNull(ctx, p)) return false;
|
|
1049
|
+
try {
|
|
1050
|
+
return (await _archiveFileSha(p)) === row.archive_sha256;
|
|
1051
|
+
} catch (err) {
|
|
1052
|
+
if (err && err.code === 'ENOENT') return false; // expected: gone between the stat and the read
|
|
1053
|
+
throw err;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// One unit through restoreUnit, with the counters D-7 reports and the D-9 sibling cleanup after a verified
|
|
1058
|
+
// restore. A failed member never changes another member's outcome.
|
|
1059
|
+
async function _recoverUnit(ctx, unitId, opts, out) {
|
|
1060
|
+
let result;
|
|
1061
|
+
try {
|
|
1062
|
+
result = await restoreUnit({ unitId }, opts);
|
|
1063
|
+
} catch (err) {
|
|
1064
|
+
out.failed.push({ path: String(unitId), code: (err && err.code) || String((err && err.message) || err) });
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
for (const failure of result.failed) {
|
|
1068
|
+
if (failure.code === 'target_exists') out.targetExists.push(failure.path);
|
|
1069
|
+
else out.failed.push(failure);
|
|
1070
|
+
}
|
|
1071
|
+
if (!result.restored.length) return;
|
|
1072
|
+
out.restoredUnits += 1;
|
|
1073
|
+
out.restoredFiles += result.restored.length;
|
|
1074
|
+
const rows = new Map((await ctx.store.rowsForUnit(unitId)).map((r) => [r.source_path, r]));
|
|
1075
|
+
for (const entry of result.restored) {
|
|
1076
|
+
const row = rows.get(entry.path) || {};
|
|
1077
|
+
if (entry.how === 'trash') out.linkedFromTrash += 1;
|
|
1078
|
+
else if (entry.how === 'archive') out.decodedBytes += Number(row.size_bytes) || 0;
|
|
1079
|
+
}
|
|
1080
|
+
for (const entry of result.restored) {
|
|
1081
|
+
const row = rows.get(entry.path);
|
|
1082
|
+
if (!row || row.state !== 'hot') continue;
|
|
1083
|
+
if (!await _isOwnSiblingArchive(ctx, row)) continue;
|
|
1084
|
+
// D-9: the original is verified back in place, so CTM's own sibling copy has no job left. The row keeps
|
|
1085
|
+
// running as a plain `hot` row; the next idle cycle re-archives the unit under D-4.
|
|
1086
|
+
await _unlinkIfPresent(ctx, row.archive_path);
|
|
1087
|
+
await ctx.store.applyArchive({
|
|
1088
|
+
upserts: [{ ...row, archive_path: '', archive_bytes: 0, archive_sha256: '', frames_json: '[]' }],
|
|
1089
|
+
deletes: [],
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* D-7. Put every original a pre-WP-1 archiver moved away back to its `source_path`, cheapest source first:
|
|
1096
|
+
* trash entries are hard-linked back (no extra bytes at all), then `cold` units are decoded newest source
|
|
1097
|
+
* first, while the free space left after the decode stays at or above `floorBytes` and the bytes decoded this
|
|
1098
|
+
* call stay under `maxDecodeBytes`. An occupied `source_path` is reported in `targetExists` and left alone,
|
|
1099
|
+
* with its row, its trash copy and its archive untouched. Primary only: a non-primary instance returns
|
|
1100
|
+
* `skipped: 'not_primary'` without making a single filesystem call.
|
|
1101
|
+
* @param {object} opts same context as runArchiveCycle (store, dataDir, trashRoot, isPrimary, fs, now, ...)
|
|
1102
|
+
* plus floorBytes / maxDecodeBytes / maxUnits.
|
|
1103
|
+
* @returns {{ restoredUnits: number, restoredFiles: number, linkedFromTrash: number, decodedBytes: number,
|
|
1104
|
+
* deferredLowDisk: number, targetExists: string[], failed: {path: string, code: string}[], skipped: string }}
|
|
1105
|
+
*/
|
|
1106
|
+
async function restoreRemovedOriginals(opts = {}) {
|
|
1107
|
+
const ctx = _archiveContext(opts);
|
|
1108
|
+
const floorBytes = Number.isFinite(Number(opts.floorBytes)) && opts.floorBytes !== undefined ? Number(opts.floorBytes) : RESTORE_FLOOR_BYTES;
|
|
1109
|
+
const maxDecodeBytes = Number.isFinite(Number(opts.maxDecodeBytes)) && opts.maxDecodeBytes !== undefined ? Number(opts.maxDecodeBytes) : RESTORE_MAX_DECODE_BYTES;
|
|
1110
|
+
const maxUnits = Math.max(1, Number(opts.maxUnits) || RESTORE_MAX_UNITS);
|
|
1111
|
+
const out = {
|
|
1112
|
+
restoredUnits: 0, restoredFiles: 0, linkedFromTrash: 0, decodedBytes: 0, deferredLowDisk: 0,
|
|
1113
|
+
targetExists: [], failed: [], skipped: '',
|
|
1114
|
+
};
|
|
1115
|
+
if (!ctx.isPrimary) { out.skipped = 'not_primary'; return out; }
|
|
1116
|
+
const perUnit = { ...opts, _ctx: ctx, mtime: 'recorded' };
|
|
1117
|
+
// Phase A (D-7): a unit with a trashed original, or a corrupt row that still names a trash copy. Cheapest
|
|
1118
|
+
// first -- a link-back costs no bytes -- and oldest state change first.
|
|
1119
|
+
const placed = new Set();
|
|
1120
|
+
const phaseA = [];
|
|
1121
|
+
for (const row of await ctx.store.listByState({ states: ['original_trashed', 'corrupt'], orderBy: 'changed', limit: 2000 })) {
|
|
1122
|
+
if (row.state !== 'original_trashed' && !(row.state === 'corrupt' && row.trash_path)) continue;
|
|
1123
|
+
if (placed.has(row.unit_id)) continue;
|
|
1124
|
+
placed.add(row.unit_id);
|
|
1125
|
+
phaseA.push(row.unit_id);
|
|
1126
|
+
}
|
|
1127
|
+
let done = 0;
|
|
1128
|
+
for (const unitId of phaseA) {
|
|
1129
|
+
if (done >= maxUnits) break;
|
|
1130
|
+
done += 1;
|
|
1131
|
+
await _recoverUnit(ctx, unitId, perUnit, out);
|
|
1132
|
+
}
|
|
1133
|
+
if (done >= maxUnits) return out;
|
|
1134
|
+
// Phase B (D-7): units whose removed rows are all `cold`, newest source first, and only while the disk
|
|
1135
|
+
// keeps `floorBytes` free and this call stays under `maxDecodeBytes`. The byte budget stops only a call
|
|
1136
|
+
// that has already decoded something, so a single unit larger than the whole budget still runs -- alone --
|
|
1137
|
+
// instead of breaking on every call and stalling every unit behind it (the phase B order is deterministic).
|
|
1138
|
+
const phaseB = [];
|
|
1139
|
+
for (const row of await ctx.store.listByState({ states: ['cold'], orderBy: 'mtime_desc', limit: 2000 })) {
|
|
1140
|
+
if (placed.has(row.unit_id)) continue;
|
|
1141
|
+
placed.add(row.unit_id);
|
|
1142
|
+
phaseB.push(row.unit_id);
|
|
1143
|
+
}
|
|
1144
|
+
for (const unitId of phaseB) {
|
|
1145
|
+
if (done >= maxUnits) break;
|
|
1146
|
+
const rows = await ctx.store.rowsForUnit(unitId);
|
|
1147
|
+
if (!rows.length) continue;
|
|
1148
|
+
if (rows.some((r) => r.state === 'original_trashed' || (r.state === 'corrupt' && r.trash_path))) continue; // phase A's unit
|
|
1149
|
+
let need = 0;
|
|
1150
|
+
for (const row of rows) if (!await _statOrNull(ctx, row.source_path)) need += Number(row.size_bytes) || 0;
|
|
1151
|
+
if (typeof ctx.fsp.statfs === 'function') {
|
|
1152
|
+
const free = await _freeBytesAt(ctx, path.dirname(String(rows[0].source_path)));
|
|
1153
|
+
if (free !== null && free - need < floorBytes) { out.deferredLowDisk += 1; break; }
|
|
1154
|
+
}
|
|
1155
|
+
if (out.decodedBytes > 0 && out.decodedBytes + need > maxDecodeBytes) break;
|
|
1156
|
+
done += 1;
|
|
1157
|
+
await _recoverUnit(ctx, unitId, perUnit, out);
|
|
1158
|
+
}
|
|
1159
|
+
return out;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1168
1162
|
module.exports = {
|
|
1169
1163
|
ARCHIVE_SUBDIR,
|
|
1164
|
+
ARCHIVE_ROOT_SUBDIR,
|
|
1165
|
+
archiveSubdirFor,
|
|
1166
|
+
LEGACY_REMOVED_STATES,
|
|
1170
1167
|
IDLE_MS,
|
|
1171
1168
|
MIN_ARCHIVE_BYTES,
|
|
1169
|
+
RESTORE_FLOOR_BYTES,
|
|
1170
|
+
RESTORE_MAX_DECODE_BYTES,
|
|
1171
|
+
RESTORE_MAX_UNITS,
|
|
1172
1172
|
discoverClaudeUnits,
|
|
1173
1173
|
discoverFlatUnits,
|
|
1174
1174
|
archiveUnit,
|
|
1175
|
-
purgeGraceExpired,
|
|
1176
1175
|
scrub,
|
|
1177
1176
|
recoverAtBoot,
|
|
1178
1177
|
runArchiveCycle, runDigestCycle,
|
|
1179
1178
|
restoreUnit,
|
|
1179
|
+
restoreRemovedOriginals,
|
|
1180
1180
|
RESTORED_GRACE_MS,
|
|
1181
1181
|
};
|