create-walle 0.10.2 → 0.10.3
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 +3 -1
- package/lib/release-schema.json +2 -2
- package/lib/runtime-closure-policy.json +8 -7
- package/package.json +1 -1
- package/template/coding-task-manager/api-prompts.js +45 -7
- package/template/coding-task-manager/bin/claude-hook.js +55 -1
- package/template/coding-task-manager/bin/ctm-relay-client.js +35 -12
- package/template/coding-task-manager/bin/land.js +2069 -0
- package/template/coding-task-manager/bin/memory-census.js +4 -2
- package/template/coding-task-manager/bin/memory-sync-verify.js +151 -0
- package/template/coding-task-manager/bin/probe-refusal-scope.js +530 -0
- package/template/coding-task-manager/db.js +156 -32
- package/template/coding-task-manager/lib/acp/acp-client.js +383 -0
- package/template/coding-task-manager/lib/acp/dsh-context-truth.js +176 -0
- package/template/coding-task-manager/lib/acp/dsh-launch-model.js +302 -0
- package/template/coding-task-manager/lib/acp/dsh-mcp-servers.js +194 -0
- package/template/coding-task-manager/lib/acp/dsh-prompt-lane.js +616 -0
- package/template/coding-task-manager/lib/acp/dsh-route-patch.js +308 -0
- package/template/coding-task-manager/lib/acp/dsh-session-runtime.js +522 -0
- package/template/coding-task-manager/lib/acp/dsh-transcript.js +395 -0
- package/template/coding-task-manager/lib/acp/dsh-usage-log.js +162 -0
- package/template/coding-task-manager/lib/acp/ndjson-stdio-transport.js +225 -0
- package/template/coding-task-manager/lib/adopted-host-redial.js +47 -11
- package/template/coding-task-manager/lib/adopted-launch-environment.js +123 -0
- package/template/coding-task-manager/lib/agent-auto-update-runtime.js +0 -3
- package/template/coding-task-manager/lib/agent-auto-update.js +26 -10
- package/template/coding-task-manager/lib/agent-bundle/build.js +657 -0
- package/template/coding-task-manager/lib/agent-bundle/codex-install.js +558 -0
- package/template/coding-task-manager/lib/agent-bundle/drift.js +120 -0
- package/template/coding-task-manager/lib/agent-bundle/gc.js +522 -0
- package/template/coding-task-manager/lib/agent-bundle/inventory.js +346 -0
- package/template/coding-task-manager/lib/agent-bundle/push.js +717 -0
- package/template/coding-task-manager/lib/agent-bundle/skill-transfer.js +724 -0
- package/template/coding-task-manager/lib/agent-bundle/spawn.js +119 -0
- package/template/coding-task-manager/lib/agent-bundle/store.js +568 -0
- package/template/coding-task-manager/lib/agent-capabilities.js +142 -12
- package/template/coding-task-manager/lib/agent-cli-cache.js +30 -5
- package/template/coding-task-manager/lib/agent-definitions.js +19 -0
- package/template/coding-task-manager/lib/agent-hooks-codex-toml.js +28 -16
- package/template/coding-task-manager/lib/agent-hooks-codex-workspace.js +9 -1
- package/template/coding-task-manager/lib/agent-hooks-installer.js +23 -8
- package/template/coding-task-manager/lib/agent-hooks-prune.js +37 -11
- package/template/coding-task-manager/lib/agent-hooks-shim.js +105 -0
- package/template/coding-task-manager/lib/agent-presets.js +84 -1
- package/template/coding-task-manager/lib/agent-upgrade-jobs.js +12 -1
- package/template/coding-task-manager/lib/agent-version-service.js +74 -11
- package/template/coding-task-manager/lib/all-sessions-query.js +6 -2
- package/template/coding-task-manager/lib/api/agent-bundles.js +436 -0
- package/template/coding-task-manager/lib/api/backup-retention.js +83 -0
- package/template/coding-task-manager/lib/api/conversation-sync-routes.js +124 -0
- package/template/coding-task-manager/lib/api/diagnostics.js +549 -69
- package/template/coding-task-manager/lib/api/fleet.js +118 -0
- package/template/coding-task-manager/lib/api/hub-bridge.js +243 -0
- package/template/coding-task-manager/lib/api/ingress.js +9 -1
- package/template/coding-task-manager/lib/api/landing.js +403 -0
- package/template/coding-task-manager/lib/api/machines.js +691 -2
- package/template/coding-task-manager/lib/api/memory-sync-routes.js +335 -0
- package/template/coding-task-manager/lib/api/misc-ops.js +8 -3
- package/template/coding-task-manager/lib/api/remote-sessions.js +1326 -0
- package/template/coding-task-manager/lib/api/session-attach.js +623 -0
- package/template/coding-task-manager/lib/api/session-messages.js +1384 -0
- package/template/coding-task-manager/lib/api/sessions.js +77 -51
- package/template/coding-task-manager/lib/api/setup-network.js +544 -16
- package/template/coding-task-manager/lib/api/setup-wizard-handlers.js +884 -0
- package/template/coding-task-manager/lib/api/setup.js +829 -10
- package/template/coding-task-manager/lib/api/walle-bridge.js +9 -1
- package/template/coding-task-manager/lib/attachment-reconciler-wiring.js +5 -2
- package/template/coding-task-manager/lib/auth-rate-limit.js +23 -0
- package/template/coding-task-manager/lib/auth-rules.js +292 -21
- package/template/coding-task-manager/lib/background-stop.js +369 -0
- package/template/coding-task-manager/lib/best-of-attempts.js +4 -1
- package/template/coding-task-manager/lib/best-of-launch-plan.js +52 -0
- package/template/coding-task-manager/lib/box-keep-awake.js +89 -0
- package/template/coding-task-manager/lib/broadcast-reason-stats.js +152 -0
- package/template/coding-task-manager/lib/broadcast-serialize.js +5 -0
- package/template/coding-task-manager/lib/build-info.js +4 -2
- package/template/coding-task-manager/lib/claude-model-truth.js +13 -2
- package/template/coding-task-manager/lib/claude-transcript-terminal-renderer.js +17 -13
- package/template/coding-task-manager/lib/client-diag-handler.js +64 -2
- package/template/coding-task-manager/lib/codex-app-server-client.js +14 -16
- package/template/coding-task-manager/lib/codex-configuration-history.js +55 -0
- package/template/coding-task-manager/lib/codex-harness-qualification.js +43 -20
- package/template/coding-task-manager/lib/codex-rollout-history.js +743 -0
- package/template/coding-task-manager/lib/coding-agent-models.js +27 -12
- package/template/coding-task-manager/lib/coding-cli-skill-installer.js +44 -14
- package/template/coding-task-manager/lib/coding-session-model-adapters.js +43 -2
- package/template/coding-task-manager/lib/coding-session-model-owner.js +1 -1
- package/template/coding-task-manager/lib/coding-session-model-public.js +7 -1
- package/template/coding-task-manager/lib/coding-session-model-runtime.js +23 -4
- package/template/coding-task-manager/lib/coding-session-model-scan.js +31 -0
- package/template/coding-task-manager/lib/coding-session-model-state.js +7 -0
- package/template/coding-task-manager/lib/conversation-freshness-diagnostics.js +280 -0
- package/template/coding-task-manager/lib/conversation-item-clock.js +261 -0
- package/template/coding-task-manager/lib/conversation-source-registry.js +5 -0
- package/template/coding-task-manager/lib/conversation-store-v2.js +44 -0
- package/template/coding-task-manager/lib/conversation-sync.js +355 -0
- package/template/coding-task-manager/lib/create/resolve-launch-params.js +96 -12
- package/template/coding-task-manager/lib/create/worker-role-profile.js +4 -4
- package/template/coding-task-manager/lib/create-intent-lease.js +51 -0
- package/template/coding-task-manager/lib/ctm-session-context-api.js +97 -6
- package/template/coding-task-manager/lib/db-domain-tables.js +1 -0
- package/template/coding-task-manager/lib/db-domains.js +1 -0
- package/template/coding-task-manager/lib/db-owner-dispatch-probe.js +32 -0
- package/template/coding-task-manager/lib/db-owner-request-counts.js +178 -0
- package/template/coding-task-manager/lib/db-owner-worker-client.js +68 -14
- package/template/coding-task-manager/lib/db-schema-source-files.js +5 -0
- package/template/coding-task-manager/lib/deferred-restore-attempt.js +6 -2
- package/template/coding-task-manager/lib/feature-manifest.js +4 -0
- package/template/coding-task-manager/lib/fleet-conversation-route.js +97 -0
- package/template/coding-task-manager/lib/fleet-fanout.js +78 -0
- package/template/coding-task-manager/lib/fleet-hub.js +762 -0
- package/template/coding-task-manager/lib/fleet-origin-follower.js +252 -0
- package/template/coding-task-manager/lib/fleet-resolver-effects.js +147 -0
- package/template/coding-task-manager/lib/fleet-resolver-tick.js +148 -0
- package/template/coding-task-manager/lib/fleet-resolver.js +454 -0
- package/template/coding-task-manager/lib/fleet-session-list-routes.js +114 -0
- package/template/coding-task-manager/lib/fleet-session-lists.js +672 -0
- package/template/coding-task-manager/lib/fleet-update.js +798 -0
- package/template/coding-task-manager/lib/fs-watcher-root-watch.js +141 -0
- package/template/coding-task-manager/lib/fs-watcher.js +58 -42
- package/template/coding-task-manager/lib/harness-history-transition.js +50 -9
- package/template/coding-task-manager/lib/headless-term-service.js +84 -8
- package/template/coding-task-manager/lib/hermetic-agent-launch-guard.js +10 -0
- package/template/coding-task-manager/lib/history-transition-read-op.js +175 -8
- package/template/coding-task-manager/lib/host-cpu-categories.js +310 -0
- package/template/coding-task-manager/lib/host-fseventsd-sampler.js +399 -0
- package/template/coding-task-manager/lib/host-handoff.js +16 -19
- package/template/coding-task-manager/lib/host-stats.js +150 -3
- package/template/coding-task-manager/lib/hosted-sync-worker.js +43 -320
- package/template/coding-task-manager/lib/hub-bridge-access.js +798 -0
- package/template/coding-task-manager/lib/hub-bridge-box.js +409 -0
- package/template/coding-task-manager/lib/hub-bridge-mcp.js +380 -0
- package/template/coding-task-manager/lib/hub-bridge-mesh.js +313 -0
- package/template/coding-task-manager/lib/hub-bridge-ops.js +721 -0
- package/template/coding-task-manager/lib/hub-bridge.js +398 -0
- package/template/coding-task-manager/lib/idle-notify.js +491 -0
- package/template/coding-task-manager/lib/instance-id-ownership-manifest.js +8 -0
- package/template/coding-task-manager/lib/instance-manifest.js +5 -1
- package/template/coding-task-manager/lib/instance-sync-status.js +18 -8
- package/template/coding-task-manager/lib/landing/agent-rule.txt +1 -0
- package/template/coding-task-manager/lib/landing/batch-runner.js +366 -0
- package/template/coding-task-manager/lib/landing/config.js +536 -0
- package/template/coding-task-manager/lib/landing/ctm-view.js +256 -0
- package/template/coding-task-manager/lib/landing/gate.js +360 -0
- package/template/coding-task-manager/lib/landing/git.js +344 -0
- package/template/coding-task-manager/lib/landing/lease.js +247 -0
- package/template/coding-task-manager/lib/landing/merge-delta.js +380 -0
- package/template/coding-task-manager/lib/landing/proof.js +223 -0
- package/template/coding-task-manager/lib/landing/queue.js +998 -0
- package/template/coding-task-manager/lib/landing/radar.js +489 -0
- package/template/coding-task-manager/lib/landing/resolve-cli.js +424 -0
- package/template/coding-task-manager/lib/landing/state.js +263 -0
- package/template/coding-task-manager/lib/landing/stop.js +49 -0
- package/template/coding-task-manager/lib/landing/sync-cli.js +475 -0
- package/template/coding-task-manager/lib/landing/sync.js +456 -0
- package/template/coding-task-manager/lib/landing/train.js +885 -0
- package/template/coding-task-manager/lib/landing/window.js +57 -0
- package/template/coding-task-manager/lib/launch-environment.js +121 -0
- package/template/coding-task-manager/lib/launch-model-override.js +36 -10
- package/template/coding-task-manager/lib/launch-plan-pool.js +2 -1
- package/template/coding-task-manager/lib/launch-plan-warm-cache.js +6 -0
- package/template/coding-task-manager/lib/lead-model-observer.js +57 -1
- package/template/coding-task-manager/lib/lead-model-options.js +160 -3
- package/template/coding-task-manager/lib/lead-model-restore-reconciliation.js +106 -24
- package/template/coding-task-manager/lib/lead-model-switch-runtime.js +352 -68
- package/template/coding-task-manager/lib/lead-model-switch.js +114 -10
- package/template/coding-task-manager/lib/lead-model-transition-runtime.js +14 -2
- package/template/coding-task-manager/lib/lead-model-transition-store.js +24 -6
- package/template/coding-task-manager/lib/lead-route-preflight.js +179 -0
- package/template/coding-task-manager/lib/lead-switch-history-mode.js +110 -0
- package/template/coding-task-manager/lib/lead-wire-agent.js +7 -0
- package/template/coding-task-manager/lib/lifecycle-wake-experiment.js +9 -2
- package/template/coding-task-manager/lib/live-defer-announce.js +45 -0
- package/template/coding-task-manager/lib/machine-registry.js +750 -5
- package/template/coding-task-manager/lib/macos-capabilities.js +32 -15
- package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +433 -53
- package/template/coding-task-manager/lib/mainloop-observability.js +14 -9
- package/template/coding-task-manager/lib/media-draft-owner.js +16 -3
- package/template/coding-task-manager/lib/memory-join.js +337 -0
- package/template/coding-task-manager/lib/memory-sync-verify.js +393 -0
- package/template/coding-task-manager/lib/memory-sync.js +565 -0
- package/template/coding-task-manager/lib/migrations/v57-delegate-pref-mirror-opencode.js +68 -0
- package/template/coding-task-manager/lib/migrations/v58-subagent-launch-native-subagents.js +42 -0
- package/template/coding-task-manager/lib/migrations/v59-coding-session-model-state-dsh.js +73 -0
- package/template/coding-task-manager/lib/migrations/v60-delegate-pref-mirror-dsh.js +78 -0
- package/template/coding-task-manager/lib/migrations/v61-conversation-item-clock.js +71 -0
- package/template/coding-task-manager/lib/model-availability.js +727 -0
- package/template/coding-task-manager/lib/model-execution-recipe-metadata.js +4 -6
- package/template/coding-task-manager/lib/model-host-receipt.js +6 -3
- package/template/coding-task-manager/lib/model-id-rule.js +19 -0
- package/template/coding-task-manager/lib/model-proxy/anthropic-face.js +4 -0
- package/template/coding-task-manager/lib/model-proxy/auto-mode-review.js +110 -23
- package/template/coding-task-manager/lib/model-proxy/chat-response-translators.js +3 -0
- package/template/coding-task-manager/lib/model-proxy/generation-probe.js +60 -17
- package/template/coding-task-manager/lib/model-proxy/historical-tool-declarations.js +37 -0
- package/template/coding-task-manager/lib/model-proxy/lead-request-validation.js +1 -0
- package/template/coding-task-manager/lib/model-proxy/lead-token-admin.js +130 -0
- package/template/coding-task-manager/lib/model-proxy/refusal-scope.js +151 -0
- package/template/coding-task-manager/lib/model-proxy/request-budget-store.js +11 -2
- package/template/coding-task-manager/lib/model-proxy/tokens.js +69 -5
- package/template/coding-task-manager/lib/model-proxy/upstream-outcome.js +9 -0
- package/template/coding-task-manager/lib/native-agent-model-args.js +25 -22
- package/template/coding-task-manager/lib/native-history-control.js +6 -4
- package/template/coding-task-manager/lib/native-lead-settings-client.js +44 -4
- package/template/coding-task-manager/lib/oauth-proxy-supervisor.js +173 -36
- package/template/coding-task-manager/lib/online-backup-worker-client.js +2 -0
- package/template/coding-task-manager/lib/opencode-cli.js +480 -0
- package/template/coding-task-manager/lib/opencode-launch-model.js +471 -0
- package/template/coding-task-manager/lib/opencode-v1-plugin/model-carrier.mjs +35 -0
- package/template/coding-task-manager/lib/opencode-v2-plugin/server.js +118 -0
- package/template/coding-task-manager/lib/owner-action-manifest-data.json +1 -1
- package/template/coding-task-manager/lib/park-composer-draft.js +54 -0
- package/template/coding-task-manager/lib/park-fresh-relaunch.js +41 -10
- package/template/coding-task-manager/lib/perf-phase-trace.js +72 -0
- package/template/coding-task-manager/lib/perf-summary.js +7 -0
- package/template/coding-task-manager/lib/perf-tracker.js +63 -2
- package/template/coding-task-manager/lib/project-config.js +7 -0
- package/template/coding-task-manager/lib/prompt-purge-v2.js +52 -1
- package/template/coding-task-manager/lib/provider-login-broker.js +72 -15
- package/template/coding-task-manager/lib/provider-login-tab.js +10 -2
- package/template/coding-task-manager/lib/read-pool-client.js +89 -2
- package/template/coding-task-manager/lib/read-pool-worker-handle.js +29 -4
- package/template/coding-task-manager/lib/recent-catalog-query.js +15 -0
- package/template/coding-task-manager/lib/recent-session-catalog-response.js +67 -10
- package/template/coding-task-manager/lib/recovery-evidence.js +26 -11
- package/template/coding-task-manager/lib/relay-client.js +57 -3
- package/template/coding-task-manager/lib/relink-audit.js +34 -5
- package/template/coding-task-manager/lib/remote-machine-catalog.js +321 -26
- package/template/coding-task-manager/lib/remote-machine-create.js +576 -36
- package/template/coding-task-manager/lib/remote-machine-link.js +546 -59
- package/template/coding-task-manager/lib/remote-machine-proxy.js +143 -9
- package/template/coding-task-manager/lib/remote-session-forward.js +65 -7
- package/template/coding-task-manager/lib/remote-terminal-target.js +9 -2
- package/template/coding-task-manager/lib/restart-guard.js +18 -6
- package/template/coding-task-manager/lib/restore-seed-modes.js +87 -0
- package/template/coding-task-manager/lib/restored-scrollback-hydration-runtime.js +22 -2
- package/template/coding-task-manager/lib/resume-admission.js +17 -3
- package/template/coding-task-manager/lib/review-repo-registry.js +58 -19
- package/template/coding-task-manager/lib/root-delivery-runtime.js +2 -2
- package/template/coding-task-manager/lib/root-prompt-flow.js +1 -1
- package/template/coding-task-manager/lib/runtime-contract.js +3 -1
- package/template/coding-task-manager/lib/schema-indexes.js +1 -0
- package/template/coding-task-manager/lib/session-context-locator.js +742 -0
- package/template/coding-task-manager/lib/session-create/attachment.js +2 -2
- package/template/coding-task-manager/lib/session-create/create-phases.js +118 -0
- package/template/coding-task-manager/lib/session-create/delegate-worker.js +86 -16
- package/template/coding-task-manager/lib/session-create/dsh-session.js +930 -0
- package/template/coding-task-manager/lib/session-create/lead-route-launch.js +186 -13
- package/template/coding-task-manager/lib/session-create/native-lead-lifetime.js +55 -3
- package/template/coding-task-manager/lib/session-create/opencode-lead-provider.js +50 -0
- package/template/coding-task-manager/lib/session-create/prepared.js +162 -38
- package/template/coding-task-manager/lib/session-create/provider-process.js +160 -28
- package/template/coding-task-manager/lib/session-create/response.js +7 -7
- package/template/coding-task-manager/lib/session-create/structured-session.js +27 -0
- package/template/coding-task-manager/lib/session-create/subagent-pick-table.js +3 -1
- package/template/coding-task-manager/lib/session-create/subagent-pick.js +458 -9
- package/template/coding-task-manager/lib/session-create/transcript.js +1 -1
- package/template/coding-task-manager/lib/session-create/worker-budget-setup.js +21 -2
- package/template/coding-task-manager/lib/session-create/worker-continuation.js +7 -1
- package/template/coding-task-manager/lib/session-create/worker-launch-sandbox.js +39 -10
- package/template/coding-task-manager/lib/session-create/worker-observability.js +2 -2
- package/template/coding-task-manager/lib/session-empty.js +13 -10
- package/template/coding-task-manager/lib/session-host-manager.js +60 -0
- package/template/coding-task-manager/lib/session-jobs.js +3 -1
- package/template/coding-task-manager/lib/session-lifecycle.js +10 -4
- package/template/coding-task-manager/lib/session-model-action-preconditions.js +6 -2
- package/template/coding-task-manager/lib/session-model-intent-contract.js +11 -8
- package/template/coding-task-manager/lib/session-park.js +738 -0
- package/template/coding-task-manager/lib/session-path-grant.js +13 -4
- package/template/coding-task-manager/lib/session-status-ledger.js +106 -4
- package/template/coding-task-manager/lib/session-transport.js +17 -0
- package/template/coding-task-manager/lib/session-workspace-projection.js +54 -5
- package/template/coding-task-manager/lib/session-workspace-tracking-runtime.js +213 -1
- package/template/coding-task-manager/lib/skill-autocomplete-query.js +120 -3
- package/template/coding-task-manager/lib/skill-autocomplete.js +15 -1
- package/template/coding-task-manager/lib/sqlite-file-set.js +39 -0
- package/template/coding-task-manager/lib/standup-search-adapter.js +16 -2
- package/template/coding-task-manager/lib/state-sync/client-ledger.js +12 -0
- package/template/coding-task-manager/lib/state-sync/frame-emitter.js +319 -100
- package/template/coding-task-manager/lib/storage/coding-session-model-store.js +4 -1
- package/template/coding-task-manager/lib/storage/delegate-pref-mirror-store.js +32 -9
- package/template/coding-task-manager/lib/structured-session-boot-sweep.js +235 -0
- package/template/coding-task-manager/lib/submission-ack.js +90 -2
- package/template/coding-task-manager/lib/systemd-scope.js +115 -0
- package/template/coding-task-manager/lib/telemetry-receiver.js +39 -0
- package/template/coding-task-manager/lib/terminal-attach-geometry.js +985 -0
- package/template/coding-task-manager/lib/terminal-bootstrap.js +30 -0
- package/template/coding-task-manager/lib/terminal-draft-operation.js +357 -0
- package/template/coding-task-manager/lib/terminal-input-admission.js +17 -1
- package/template/coding-task-manager/lib/terminal-input-prepared.js +563 -0
- package/template/coding-task-manager/lib/transcript-cache-temps.js +171 -0
- package/template/coding-task-manager/lib/transcript-frame-cache.js +8 -3
- package/template/coding-task-manager/lib/transcript-source.js +36 -4
- package/template/coding-task-manager/lib/transcript-store.js +9 -0
- package/template/coding-task-manager/lib/typing-interaction-trace.js +106 -3
- package/template/coding-task-manager/lib/usage-ledger/backfill.js +40 -5
- package/template/coding-task-manager/lib/usage-ledger/client.js +4 -1
- package/template/coding-task-manager/lib/usage-ledger/dsh-parse.js +152 -0
- package/template/coding-task-manager/lib/usage-ledger/dsh-read.js +73 -0
- package/template/coding-task-manager/lib/usage-ledger/ingest.js +34 -6
- package/template/coding-task-manager/lib/usage-ledger/live.js +35 -8
- package/template/coding-task-manager/lib/usage-ledger/read.js +29 -20
- package/template/coding-task-manager/lib/usage-ledger/schema.js +61 -1
- package/template/coding-task-manager/lib/usage-ledger/task-coordinator.js +4 -2
- package/template/coding-task-manager/lib/wake-skip-reasons.js +1 -0
- package/template/coding-task-manager/lib/walle/turn.js +1 -1
- package/template/coding-task-manager/lib/walle-history-serve.js +838 -0
- package/template/coding-task-manager/lib/walle-kill-authority.js +2 -2
- package/template/coding-task-manager/lib/walle-log-rotation.js +164 -1
- package/template/coding-task-manager/lib/walle-mcp-auto-config.js +56 -2
- package/template/coding-task-manager/lib/walle-row-history.js +495 -0
- package/template/coding-task-manager/lib/walle-supervisor.js +33 -14
- package/template/coding-task-manager/lib/watch-worker-client.js +203 -0
- package/template/coding-task-manager/lib/worker-terminal-output.js +5 -2
- package/template/coding-task-manager/lib/ws/hub-bridge.js +35 -0
- package/template/coding-task-manager/lib/ws/lifecycle.js +6 -11
- package/template/coding-task-manager/lib/ws/misc.js +1 -1
- package/template/coding-task-manager/lib/ws/state-sync.js +106 -4
- package/template/coding-task-manager/lib/ws/terminal-attach.js +91 -0
- package/template/coding-task-manager/oauth-proxy.js +112 -115
- package/template/coding-task-manager/package-lock.json +10 -94
- package/template/coding-task-manager/package.json +4 -3
- package/template/coding-task-manager/protocol/message-table.json +10 -0
- package/template/coding-task-manager/protocol/messages.js +10 -0
- package/template/coding-task-manager/protocol/ws-schema.json +20 -0
- package/template/coding-task-manager/protocol/ws-schema.md +2 -0
- package/template/coding-task-manager/public/css/app.css +170 -0
- package/template/coding-task-manager/public/css/walle-session.css +4 -0
- package/template/coding-task-manager/public/index.html +473 -243
- package/template/coding-task-manager/public/js/agent-command.js +86 -0
- package/template/coding-task-manager/public/js/app/backups-panel.js +111 -5
- package/template/coding-task-manager/public/js/app/coding-agent-upgrade.js +103 -4
- package/template/coding-task-manager/public/js/app/machines-page.js +1328 -8
- package/template/coding-task-manager/public/js/app/models-tab.js +113 -38
- package/template/coding-task-manager/public/js/app/restart-banner.js +53 -0
- package/template/coding-task-manager/public/js/app/worktrees-panel.js +184 -2
- package/template/coding-task-manager/public/js/background-work.js +196 -27
- package/template/coding-task-manager/public/js/coding-session-model-presentation.js +138 -10
- package/template/coding-task-manager/public/js/core/active-session-groups.js +179 -0
- package/template/coding-task-manager/public/js/core/pending-create.js +238 -0
- package/template/coding-task-manager/public/js/core/recent-machine-source.js +228 -0
- package/template/coding-task-manager/public/js/core/session-commands.js +180 -14
- package/template/coding-task-manager/public/js/dashboard-client-function-order.json +8 -0
- package/template/coding-task-manager/public/js/document-review-links.js +9 -1
- package/template/coding-task-manager/public/js/file-resolution-client.js +52 -1
- package/template/coding-task-manager/public/js/history-app-seam.js +22 -2
- package/template/coding-task-manager/public/js/machine-pool-shared.js +558 -3
- package/template/coding-task-manager/public/js/message-renderer.js +4 -3
- package/template/coding-task-manager/public/js/model-picker.js +3 -1
- package/template/coding-task-manager/public/js/native-subagent-review.js +15 -2
- package/template/coding-task-manager/public/js/new-session-agent-grid.js +346 -0
- package/template/coding-task-manager/public/js/pasted-content.js +236 -0
- package/template/coding-task-manager/public/js/pe-private-content-lifetime.js +39 -7
- package/template/coding-task-manager/public/js/private-model-lifetime.js +6 -2
- package/template/coding-task-manager/public/js/prompts.js +11 -10
- package/template/coding-task-manager/public/js/protocol.js +10 -0
- package/template/coding-task-manager/public/js/restart-banner-policy.js +29 -1
- package/template/coding-task-manager/public/js/session-model-picker.js +447 -57
- package/template/coding-task-manager/public/js/session-search-utils.js +18 -2
- package/template/coding-task-manager/public/js/session-usage-view.js +2 -1
- package/template/coding-task-manager/public/js/session-workspace-tracking.js +51 -1
- package/template/coding-task-manager/public/js/state-sync-client.js +10 -1
- package/template/coding-task-manager/public/js/stream-view.js +6 -5
- package/template/coding-task-manager/public/js/terminal/conv-only-session.js +287 -0
- package/template/coding-task-manager/public/js/terminal/runtime-09.js +2 -0
- package/template/coding-task-manager/public/js/terminal/runtime-10.js +5 -5
- package/template/coding-task-manager/public/js/terminal/runtime-12.js +8 -1
- package/template/coding-task-manager/public/js/terminal/runtime-16.js +4 -0
- package/template/coding-task-manager/public/js/terminal/runtime-20.js +1 -1
- package/template/coding-task-manager/public/js/terminal-attachment-draft.js +67 -13
- package/template/coding-task-manager/public/js/terminal-image-orchestration.js +52 -6
- package/template/coding-task-manager/public/js/terminal-wheel.js +23 -3
- package/template/coding-task-manager/public/js/walle/chat-conversation.js +4 -4
- package/template/coding-task-manager/public/js/walle.js +6 -0
- package/template/coding-task-manager/public/js/ws/live-dispatch.js +10 -0
- package/template/coding-task-manager/public/m/app.css +140 -0
- package/template/coding-task-manager/public/m/app.js +507 -68
- package/template/coding-task-manager/public/m/index.html +17 -0
- package/template/coding-task-manager/public/m/machines.js +838 -4
- package/template/coding-task-manager/public/m/private-api.js +31 -1
- package/template/coding-task-manager/public/usage.html +1 -0
- package/template/coding-task-manager/scripts/dev-route-fixture/synthetic-route-upstream.js +104 -0
- package/template/coding-task-manager/scripts/dev-route-fixture.js +339 -0
- package/template/coding-task-manager/scripts/opencode-lead-route-probe.js +195 -0
- package/template/coding-task-manager/scripts/opencode-v2-contract-probe/fake-anthropic.js +168 -0
- package/template/coding-task-manager/scripts/opencode-v2-contract-probe/probe-plugin/server.js +95 -0
- package/template/coding-task-manager/scripts/opencode-v2-contract-probe.js +673 -0
- package/template/coding-task-manager/server.js +1950 -11679
- package/template/coding-task-manager/session-integrity.js +64 -4
- package/template/coding-task-manager/workers/db-owner-worker.js +19 -2
- package/template/coding-task-manager/workers/online-backup-worker.js +1 -0
- package/template/coding-task-manager/workers/read-pool-model-collector.js +51 -3
- package/template/coding-task-manager/workers/read-pool-worker.js +76 -6
- package/template/coding-task-manager/workers/session-host-pool-process.js +6 -6
- package/template/coding-task-manager/workers/session-host-process.js +5 -5
- package/template/coding-task-manager/workers/watch-worker.js +105 -0
- package/template/coding-task-manager/workers/worktree-worker.js +168 -33
- package/template/ctm-relay/lib/protocol.js +14 -1
- package/template/package-lock.json +2 -2
- package/template/package.json +1 -1
- package/template/shared/backup-retention.js +82 -0
- package/template/shared/conversation-admission.js +2 -0
- package/template/shared/conversation-provenance.js +3 -7
- package/template/shared/conversation-storage-v2-contract.js +5 -0
- package/template/shared/linked-worktree.js +51 -0
- package/template/shared/macos-fda-target.js +57 -0
- package/template/shared/orphan-parents.js +55 -0
- package/template/shared/release-schema.json +2 -2
- package/template/shared/storage/contract.js +3 -0
- package/template/wall-e/adapters/imessage.js +12 -0
- package/template/wall-e/agent.js +71 -4
- package/template/wall-e/api-walle.js +21 -1
- package/template/wall-e/bin/train-gemma-e4b-tooluse.js +2 -2
- package/template/wall-e/bin/walle-mcp-stdio.js +5 -5
- package/template/wall-e/brain/brain-join.js +859 -0
- package/template/wall-e/brain/brain-sync-membership.js +20 -0
- package/template/wall-e/brain/brain-sync-store.js +131 -77
- package/template/wall-e/brain/brain-sync-v2-schema.js +203 -0
- package/template/wall-e/brain/brain-sync-v2.js +578 -0
- package/template/wall-e/brain/delegate-model-prefs-store.js +13 -4
- package/template/wall-e/brain/deterministic-id.js +51 -0
- package/template/wall-e/brain/memories.js +2 -2
- package/template/wall-e/brain/migrations.js +40 -6
- package/template/wall-e/brain/model-identity-migration.js +1 -1
- package/template/wall-e/brain/owner-call-whitelist.js +23 -7
- package/template/wall-e/brain/schema.js +45 -85
- package/template/wall-e/brain.js +171 -35
- package/template/wall-e/chat.js +3 -3
- package/template/wall-e/coding/agent-shell-env.js +7 -2
- package/template/wall-e/coding/prompt-bundle.js +66 -21
- package/template/wall-e/config/main-loop-exemptions.json +2 -2
- package/template/wall-e/http/admin-api.js +37 -0
- package/template/wall-e/http/gws-api.js +4 -0
- package/template/wall-e/http/helper-rules-api.js +7 -1
- package/template/wall-e/http/model-admin.js +8 -1
- package/template/wall-e/http/model-delegate-pref-admin.js +3 -3
- package/template/wall-e/http/model-proxy-providers.js +174 -1
- package/template/wall-e/http/model-route-health.js +157 -0
- package/template/wall-e/http/sync-api.js +278 -464
- package/template/wall-e/jobs/job-runner.js +28 -1
- package/template/wall-e/lib/brain-owner-op-priority.js +9 -1
- package/template/wall-e/lib/ctm-data-dir.js +26 -0
- package/template/wall-e/lib/derivation-home.js +147 -0
- package/template/wall-e/lib/helper-rules/evaluate.js +50 -9
- package/template/wall-e/lib/helper-rules/grade.js +51 -8
- package/template/wall-e/lib/helper-rules/lead-call-hook.js +38 -5
- package/template/wall-e/lib/helper-rules/store.js +4 -2
- package/template/wall-e/lib/landing-mcp-tools.js +170 -0
- package/template/wall-e/lib/mcp-client-config.js +133 -14
- package/template/wall-e/lib/mcp-gateway-migrate.js +28 -64
- package/template/wall-e/lib/mcp-integration.js +58 -13
- package/template/wall-e/lib/parent-death.js +7 -1
- package/template/wall-e/lib/scheduler-worker-jobs.js +6 -4
- package/template/wall-e/llm/delegate-lead-prefs.js +16 -1
- package/template/wall-e/llm/delegate-vendor-default.js +12 -5
- package/template/wall-e/llm/model-roles.js +1 -1
- package/template/wall-e/llm/native-delegate-selection.js +56 -8
- package/template/wall-e/llm/worker-delegate-args.js +72 -7
- package/template/wall-e/llm/worker-mcp-routing.js +66 -17
- package/template/wall-e/loops/brain-custom-writes.js +19 -0
- package/template/wall-e/loops/ingest.js +17 -1
- package/template/wall-e/loops/think.js +19 -4
- package/template/wall-e/mcp-contract.js +222 -6
- package/template/wall-e/mcp-protocol-contract.js +40 -5
- package/template/wall-e/mcp-server.js +128 -29
- package/template/wall-e/mcp-worker-schemas.js +5 -5
- package/template/wall-e/memory/ctm-context-client.js +85 -0
- package/template/wall-e/memory/ctm-session-context.js +9 -3
- package/template/wall-e/memory/hub-bridge-client.js +388 -0
- package/template/wall-e/memory/replica-origin.js +56 -0
- package/template/wall-e/memory/session-pool.js +135 -0
- package/template/wall-e/runtime/live-capabilities.js +8 -3
- package/template/wall-e/server.js +4 -2
- package/template/wall-e/shared/release-schema.json +2 -2
- package/template/wall-e/shared/sqlite-file-set.js +39 -0
- package/template/wall-e/shared/storage/contract.js +3 -0
- package/template/wall-e/skills/_templates/delegate-worker/VERSION +1 -1
- package/template/wall-e/skills/_templates/delegate-worker/released-sha256.json +7 -1
- package/template/wall-e/skills/_templates/delegate-worker/templates/claude/SKILL.md +4 -4
- package/template/wall-e/skills/_templates/delegate-worker/templates/codex/SKILL.md +6 -6
- package/template/wall-e/skills/_templates/fleet-sync/VERSION +1 -0
- package/template/wall-e/skills/_templates/fleet-sync/released-sha256.json +16 -0
- package/template/wall-e/skills/_templates/fleet-sync/templates/claude/SKILL.md +64 -0
- package/template/wall-e/skills/_templates/fleet-sync/templates/codex/SKILL.md +64 -0
- package/template/wall-e/tools/chrome-automation.js +6 -0
- package/template/wall-e/training/setup-cuda-train-env.sh +2 -2
- package/template/wall-e/workers/brain-owner-worker.js +18 -4
- package/template/wall-e/workers/mcp-gateway-process.js +5 -5
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WP-6 (docs/remote-agent-skills-bundles-2026-10-25.html §5 D-14, the D-14 decision row, §6.3, §8
|
|
5
|
+
* WP-6 row, AC-6a..AC-6c): CODEX INSTALL-MODE — the one place the box writes into its own home.
|
|
6
|
+
*
|
|
7
|
+
* Codex has no session-scoped skills root (`--add-dir` is a Claude-only flag, and `CODEX_HOME` also
|
|
8
|
+
* holds the user's sign-in, so it cannot be swapped per session — D-14's decision row, read
|
|
9
|
+
* codex-rs/ext/skills/src/host_roots.rs:48-185), so the bundle's `codex/<name>/` items are
|
|
10
|
+
* INSTALLED into `$HOME/.agents/skills/<name>/` with the SAME dpkg-conffile rule the box's own
|
|
11
|
+
* managed-skill installer already uses (lib/coding-cli-skill-installer.js): a pristine copy is
|
|
12
|
+
* created/updated, an edited copy is kept and reported, a folder CTM never wrote is untouched.
|
|
13
|
+
*
|
|
14
|
+
* REPLACE/REUSE, never a parallel installer: the marker/record constants, the root-record reader,
|
|
15
|
+
* the root-record writer and D-8's `writeAllowed` all come from lib/coding-cli-skill-installer.js.
|
|
16
|
+
* A directory-source plan is a genuinely different shape from that module's one-file template plan
|
|
17
|
+
* (its `buildPlan` classifies a single SKILL.md against a known-hash SET; here the unit is a whole
|
|
18
|
+
* directory compared hash-for-hash against the tree copy), so the planner lives here and calls the
|
|
19
|
+
* shared helpers; folding the two into one function would make the single-file path read worse.
|
|
20
|
+
*
|
|
21
|
+
* Classification (first match wins), per name:
|
|
22
|
+
* 1. target dir missing -> `removed` when the root record remembers an `agent-bundle`
|
|
23
|
+
* entry (the user deleted it — never resurrect), else `create`.
|
|
24
|
+
* EXCEPTION: a leftover `.<name>.ctm-retired-*` (crash
|
|
25
|
+
* mid-swap) means `create`.
|
|
26
|
+
* 2. no marker, or `source !== 'agent-bundle'` -> `conflict` (user-authored, or another
|
|
27
|
+
* installer's copy — untouched, not even hashed).
|
|
28
|
+
* 3. `diskDirHash !== marker.sha256` -> `edited` (kept; WP-7 reports "changed on box").
|
|
29
|
+
* 4. `diskDirHash === bundleDirHash` -> `noop`.
|
|
30
|
+
* 5. else -> `update`.
|
|
31
|
+
* retire: an installed `agent-bundle` copy of THIS hub, pristine, ABSENT from the current tree ->
|
|
32
|
+
* `retire` (the hub deleted that skill). Edited or other-hub copies are never retired.
|
|
33
|
+
*
|
|
34
|
+
* Write order per skill: staging dir (0644/0755 — the tree's read-only modes must NOT be copied, the
|
|
35
|
+
* user has to be able to edit) holding the copied bytes + the marker, then the swap (rename old to
|
|
36
|
+
* `.<name>.ctm-retired-*`, rename staging into place, remove the retired dir), then the root-record
|
|
37
|
+
* entry (atomic writes throughout, mirroring the installer's D-6 order).
|
|
38
|
+
*
|
|
39
|
+
* EVENT-DRIVEN, never a timer (stabilization lint): the store only changes at the two moments
|
|
40
|
+
* lib/agent-bundle/gc.js already names (boot, and a manifest PUT), so the codex install is scheduled
|
|
41
|
+
* from the manifest-PUT seam and is single-flight exactly like `scheduleCollectGarbage`.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
const fsp = require('node:fs/promises');
|
|
45
|
+
const path = require('node:path');
|
|
46
|
+
const os = require('node:os');
|
|
47
|
+
const crypto = require('node:crypto');
|
|
48
|
+
|
|
49
|
+
const { atomicWriteFile } = require('../../atomic-write');
|
|
50
|
+
const store = require('./store');
|
|
51
|
+
const installer = require('../coding-cli-skill-installer');
|
|
52
|
+
|
|
53
|
+
/** D-14: the box-home write target. NOT `$CODEX_HOME/skills` — Codex marks that root deprecated. */
|
|
54
|
+
const AGENTS_SKILLS_DIR = '.agents';
|
|
55
|
+
const SKILLS_DIR = 'skills';
|
|
56
|
+
/** The id shape an item name must have to be usable as a directory name (store.js's ID_RE). */
|
|
57
|
+
const NAME_RE = /^[A-Za-z0-9._-]{1,128}$/;
|
|
58
|
+
const STAGING_RE = /^\.(.+)\.ctm-staging-[0-9a-f]+$/;
|
|
59
|
+
const RETIRED_RE = /^\.(.+)\.ctm-retired-[0-9a-f]+$/;
|
|
60
|
+
/** The marker `source` THIS module writes (the installer's own copies carry its own source string). */
|
|
61
|
+
const SOURCE = 'agent-bundle';
|
|
62
|
+
|
|
63
|
+
const { writeAllowed, MANAGED_SKILLS } = installer;
|
|
64
|
+
const { MARKER_FILE, ROOT_RECORD_FILE, readMarker, readRootRecord, writeRootRecord } = installer._internals;
|
|
65
|
+
|
|
66
|
+
/** REPORT sink: a throwing reporting backend must never turn a per-skill failure into a throw. */
|
|
67
|
+
function _report(report, error) {
|
|
68
|
+
if (typeof report !== 'function') return;
|
|
69
|
+
try {
|
|
70
|
+
report(error);
|
|
71
|
+
} catch {
|
|
72
|
+
/* expected: the report sink is best-effort telemetry; its own failure is not an install failure */
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function _rand() {
|
|
77
|
+
return crypto.randomBytes(6).toString('hex');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function _sha256(data) {
|
|
81
|
+
return crypto.createHash('sha256').update(data).digest('hex');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The install root (D-14). Absolute; `homeDir` is injected so a test never reads the real home. */
|
|
85
|
+
function skillsRoot(homeDir) {
|
|
86
|
+
return path.join(homeDir, AGENTS_SKILLS_DIR, SKILLS_DIR);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function _statOrNull(target) {
|
|
90
|
+
try {
|
|
91
|
+
return await fsp.lstat(target);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (error && (error.code === 'ENOENT' || error.code === 'ENOTDIR')) return null;
|
|
94
|
+
throw error; // RETHROW: an unreadable path is a real fault the caller REPORTs
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function _isDir(target) {
|
|
99
|
+
const stat = await _statOrNull(target);
|
|
100
|
+
return stat !== null && stat.isDirectory();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** readdir of a directory that may not exist yet (an empty root is not an error). */
|
|
104
|
+
async function _readdirOrEmpty(dir) {
|
|
105
|
+
try {
|
|
106
|
+
return await fsp.readdir(dir, { withFileTypes: true });
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if (error && (error.code === 'ENOENT' || error.code === 'ENOTDIR')) return []; /* expected: no root yet */
|
|
109
|
+
throw error; // RETHROW: an unreadable directory is a real fault
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Recursive remove that tolerates a vanished path (a concurrent run, or the rename that consumed it). */
|
|
114
|
+
async function _removeQuiet(target) {
|
|
115
|
+
try {
|
|
116
|
+
await fsp.rm(target, { recursive: true, force: true });
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (error && error.code === 'ENOENT') return; /* expected: already gone */
|
|
119
|
+
throw error; // RETHROW: a failed removal is a real fault (the caller REPORTs it)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* sha256 over the sorted `{relPath, exec, sha256}` of every regular file under `dir`, EXCLUDING
|
|
125
|
+
* dot-names at any depth (so a `.ctm-managed` marker or a `.SKILL.md.edited-*.bak` backup never
|
|
126
|
+
* counts) and never following a symlink out of the directory (symlinks and other non-regular files
|
|
127
|
+
* are skipped — the hub builder already excludes them from the tree, so the tree copy and the
|
|
128
|
+
* installed copy hash equal iff their bytes match).
|
|
129
|
+
*
|
|
130
|
+
* @returns {Promise<string>} 64-hex digest
|
|
131
|
+
*/
|
|
132
|
+
async function dirHash(dir) {
|
|
133
|
+
const files = [];
|
|
134
|
+
await _walk(dir, '', files);
|
|
135
|
+
files.sort((a, b) => (a.relPath < b.relPath ? -1 : a.relPath > b.relPath ? 1 : 0));
|
|
136
|
+
const payload = files.map((file) => `${file.relPath}\0${file.exec ? 'x' : '-'}\0${file.sha256}\n`).join('');
|
|
137
|
+
return _sha256(payload);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function _walk(base, rel, out) {
|
|
141
|
+
const current = rel ? path.join(base, rel) : base;
|
|
142
|
+
for (const dirent of await _readdirOrEmpty(current)) {
|
|
143
|
+
if (dirent.name.startsWith('.')) continue; // markers/backups are never content
|
|
144
|
+
const childRel = rel ? `${rel}/${dirent.name}` : dirent.name;
|
|
145
|
+
if (dirent.isDirectory()) {
|
|
146
|
+
await _walk(base, childRel, out);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (!dirent.isFile()) continue; // symlink/socket/fifo: never followed, never hashed
|
|
150
|
+
const full = path.join(base, ...childRel.split('/'));
|
|
151
|
+
const stat = await fsp.lstat(full);
|
|
152
|
+
const content = await fsp.readFile(full);
|
|
153
|
+
out.push({ relPath: childRel, exec: (stat.mode & 0o111) !== 0, sha256: _sha256(content) });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Copy a tree item into `destDir` with USER-writable modes: files 0644 (the exec bit preserved as
|
|
158
|
+
* 0755), dirs 0755. The tree's 0444/0555 read-only modes must never be reproduced — the user edits
|
|
159
|
+
* these copies, and a 0444 SKILL.md would make that a chmod first. */
|
|
160
|
+
async function _copyDir(srcDir, destDir) {
|
|
161
|
+
await fsp.mkdir(destDir, { recursive: true });
|
|
162
|
+
await fsp.chmod(destDir, 0o755);
|
|
163
|
+
for (const dirent of await fsp.readdir(srcDir, { withFileTypes: true })) {
|
|
164
|
+
if (dirent.name.startsWith('.')) continue; // a tree never carries one, but a marker never travels
|
|
165
|
+
const src = path.join(srcDir, dirent.name);
|
|
166
|
+
const dest = path.join(destDir, dirent.name);
|
|
167
|
+
if (dirent.isDirectory()) {
|
|
168
|
+
await _copyDir(src, dest);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (!dirent.isFile()) continue;
|
|
172
|
+
const stat = await fsp.lstat(src);
|
|
173
|
+
await fsp.copyFile(src, dest);
|
|
174
|
+
await fsp.chmod(dest, (stat.mode & 0o111) !== 0 ? 0o755 : 0o644);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** The bundle's codex item names (directories under `<treeDir>/codex`), sorted, dot-names excluded. */
|
|
179
|
+
async function _bundleItemNames(treeDir) {
|
|
180
|
+
const dirents = await _readdirOrEmpty(path.join(treeDir, 'codex'));
|
|
181
|
+
return dirents
|
|
182
|
+
.filter((dirent) => dirent.isDirectory() && !dirent.name.startsWith('.') && NAME_RE.test(dirent.name))
|
|
183
|
+
.map((dirent) => dirent.name)
|
|
184
|
+
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** The names under `root` a hub copy could own: real directories that are not our bookkeeping dirs. */
|
|
188
|
+
function _installedNames(dirents) {
|
|
189
|
+
return dirents
|
|
190
|
+
.filter((dirent) => dirent.isDirectory() && !dirent.name.startsWith('.') && NAME_RE.test(dirent.name))
|
|
191
|
+
.map((dirent) => dirent.name)
|
|
192
|
+
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** The names a leftover DIRECTORY in `root` belongs to — the fingerprint of a crash between the two
|
|
196
|
+
* renames of a swap (or of a retire that never finished). */
|
|
197
|
+
function _leftoverPrefixes(dirents) {
|
|
198
|
+
const names = new Set();
|
|
199
|
+
for (const dirent of dirents) {
|
|
200
|
+
if (!dirent.isDirectory()) continue;
|
|
201
|
+
const match = RETIRED_RE.exec(dirent.name) || STAGING_RE.exec(dirent.name);
|
|
202
|
+
if (match) names.add(match[1]);
|
|
203
|
+
}
|
|
204
|
+
return names;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function _recordEntry(record, name) {
|
|
208
|
+
const entry = record && record.skills && record.skills[name];
|
|
209
|
+
return entry && typeof entry === 'object' ? entry : null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Plan the install. Read-only: not a single write happens here.
|
|
214
|
+
*
|
|
215
|
+
* @param {string} treeDir absolute materialized tree dir (`trees/<hex>`)
|
|
216
|
+
* @param {object} [options]
|
|
217
|
+
* @param {string} [options.hubPeerId] the hub whose copy this is (marker + retire ownership)
|
|
218
|
+
* @param {string} [options.homeDir=os.homedir()] the box user's home (a test passes a temp HOME)
|
|
219
|
+
* @param {boolean} [options.isPrimary=false] D-8: only a primary may write the real home
|
|
220
|
+
* @param {string} [options.realHome=os.userInfo().homedir] the password-database home (ignores $HOME)
|
|
221
|
+
* @param {Function} [options.log]
|
|
222
|
+
* @returns {Promise<{root: string, writeAllowed: boolean, plans: object[]}>} plus the re-plan inputs
|
|
223
|
+
* an apply needs (`treeDir`, `hubPeerId`, `treeHash`, `homeDir`, `isPrimary`, `realHome`).
|
|
224
|
+
*/
|
|
225
|
+
async function planBundleCodexInstall(treeDir, {
|
|
226
|
+
hubPeerId = '', homeDir = os.homedir(), isPrimary = false, realHome = os.userInfo().homedir, log,
|
|
227
|
+
// Apply's per-name re-plan passes the one name it is about to write, so a run hashes each
|
|
228
|
+
// directory a bounded number of times instead of re-hashing every skill once per skill.
|
|
229
|
+
onlyName = null,
|
|
230
|
+
} = {}) {
|
|
231
|
+
const root = skillsRoot(homeDir);
|
|
232
|
+
const hub = String(hubPeerId == null ? '' : hubPeerId).trim();
|
|
233
|
+
const baseName = path.basename(treeDir);
|
|
234
|
+
const treeHash = /^[0-9a-f]{64}$/.test(baseName) ? `sha256:${baseName}` : null;
|
|
235
|
+
|
|
236
|
+
const [allowed, record, bundleNames, rootDirents] = await Promise.all([
|
|
237
|
+
writeAllowed(root, { isPrimary, realHome }),
|
|
238
|
+
readRootRecord(root, { log }),
|
|
239
|
+
_bundleItemNames(treeDir),
|
|
240
|
+
_readdirOrEmpty(root),
|
|
241
|
+
]);
|
|
242
|
+
const leftovers = _leftoverPrefixes(rootDirents);
|
|
243
|
+
|
|
244
|
+
const plans = [];
|
|
245
|
+
for (const name of bundleNames) {
|
|
246
|
+
if (onlyName !== null && name !== onlyName) continue;
|
|
247
|
+
const targetDir = path.join(root, name);
|
|
248
|
+
const bundleDirHash = await dirHash(path.join(treeDir, 'codex', name));
|
|
249
|
+
if (Object.hasOwn(MANAGED_SKILLS, name)) {
|
|
250
|
+
// The box installs its OWN `delegate` from its own release; a hub copy must never shadow it.
|
|
251
|
+
plans.push({ name, action: 'skipped_managed', targetDir, bundleDirHash, diskDirHash: null, marker: null });
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
let action;
|
|
255
|
+
let diskDirHash = null;
|
|
256
|
+
let marker = null;
|
|
257
|
+
if (!(await _isDir(targetDir))) {
|
|
258
|
+
// Rule 1. The leftover-retired exception is checked FIRST: a name the tree still HAS must be
|
|
259
|
+
// restored even though a record entry (which the crashed run never dropped) would say `removed`.
|
|
260
|
+
if (leftovers.has(name)) action = 'create';
|
|
261
|
+
else {
|
|
262
|
+
const entry = _recordEntry(record, name);
|
|
263
|
+
action = entry && entry.source === SOURCE ? 'removed' : 'create';
|
|
264
|
+
}
|
|
265
|
+
} else {
|
|
266
|
+
marker = await readMarker(targetDir, name);
|
|
267
|
+
if (!marker || marker.source !== SOURCE) {
|
|
268
|
+
action = 'conflict'; // user-authored, or another installer's copy — not even hashed
|
|
269
|
+
} else {
|
|
270
|
+
diskDirHash = await dirHash(targetDir);
|
|
271
|
+
if (marker.sha256 !== diskDirHash) action = 'edited';
|
|
272
|
+
else if (diskDirHash === bundleDirHash) action = 'noop';
|
|
273
|
+
else action = 'update';
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
plans.push({ name, action, targetDir, bundleDirHash, diskDirHash, marker });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Retire: installed agent-bundle copies of THIS hub that the current tree no longer has.
|
|
280
|
+
const inTree = new Set(bundleNames);
|
|
281
|
+
for (const name of _installedNames(rootDirents)) {
|
|
282
|
+
if (inTree.has(name)) continue;
|
|
283
|
+
if (onlyName !== null && name !== onlyName) continue;
|
|
284
|
+
const targetDir = path.join(root, name);
|
|
285
|
+
const marker = await readMarker(targetDir, name);
|
|
286
|
+
if (!marker || marker.source !== SOURCE || marker.hubPeerId !== hub) continue; // not ours: never retired
|
|
287
|
+
const diskDirHash = await dirHash(targetDir);
|
|
288
|
+
if (marker.sha256 !== diskDirHash) continue; // edited on the box: kept, never retired
|
|
289
|
+
plans.push({ name, action: 'retire', targetDir, bundleDirHash: null, diskDirHash, marker });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return { root, writeAllowed: allowed, plans, treeDir, hubPeerId: hub, treeHash, homeDir, isPrimary, realHome };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Dirs a crashed run left behind, removed once they are older than this run (nothing of OURS is
|
|
297
|
+
* younger than `runStartMs`).
|
|
298
|
+
*
|
|
299
|
+
* `wanted(name)` splits the job in two, and that split is load-bearing: a `.<name>.ctm-retired-*` dir
|
|
300
|
+
* is the ONLY evidence that a swap crashed mid-flight (rule 1's exception), so it must still be there
|
|
301
|
+
* when the apply-time re-plan classifies the name — hence the start-of-apply sweep skips the names
|
|
302
|
+
* this run is about to act on, and those get their leftovers removed after the run's own swaps.
|
|
303
|
+
*/
|
|
304
|
+
async function _sweepLeftovers(root, runStartMs, wanted = () => true) {
|
|
305
|
+
for (const dirent of await _readdirOrEmpty(root)) {
|
|
306
|
+
if (!dirent.isDirectory()) continue;
|
|
307
|
+
const match = RETIRED_RE.exec(dirent.name) || STAGING_RE.exec(dirent.name);
|
|
308
|
+
if (!match || !wanted(match[1])) continue;
|
|
309
|
+
const target = path.join(root, dirent.name);
|
|
310
|
+
const stat = await _statOrNull(target);
|
|
311
|
+
if (!stat || stat.mtimeMs > runStartMs) continue; // younger than this run: not ours to touch
|
|
312
|
+
await _removeQuiet(target);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Remove the root-record entry for `name`. `writeRootRecord` only merges, so a drop reads and writes
|
|
317
|
+
* through the shared reader/format — a re-add by the hub must then classify `create`, not `removed`. */
|
|
318
|
+
async function _dropRecordEntry(root, name, log) {
|
|
319
|
+
const current = await readRootRecord(root, { log });
|
|
320
|
+
if (!current || !current.skills || !Object.hasOwn(current.skills, name)) return;
|
|
321
|
+
delete current.skills[name];
|
|
322
|
+
await atomicWriteFile(path.join(root, ROOT_RECORD_FILE), JSON.stringify(current, null, 2) + '\n');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Apply a plan. Never throws for a per-skill failure — that skill's result carries `skipped:'error'`
|
|
327
|
+
* and the fault reaches `report`; the run continues (REPORT + continue, like syncManagedSkills).
|
|
328
|
+
*
|
|
329
|
+
* @param {object} plan from planBundleCodexInstall
|
|
330
|
+
* @param {object} [options]
|
|
331
|
+
* @param {string} [options.hubPeerId] falls back to the plan's
|
|
332
|
+
* @param {string} [options.treeHash] falls back to the plan's (derived from the tree dir name)
|
|
333
|
+
* @param {() => Date} [options.now]
|
|
334
|
+
* @param {Function} [options.report]
|
|
335
|
+
* @param {Function} [options.log]
|
|
336
|
+
* @returns {Promise<{results: Array<{name: string, action: string, written: boolean, skipped: string|null}>}>}
|
|
337
|
+
*/
|
|
338
|
+
async function applyBundleCodexInstall(plan, {
|
|
339
|
+
hubPeerId = null, treeHash = null, now = () => new Date(), report = null, log,
|
|
340
|
+
} = {}) {
|
|
341
|
+
const root = plan.root;
|
|
342
|
+
const hub = String((hubPeerId == null ? plan.hubPeerId : hubPeerId) || '').trim();
|
|
343
|
+
const hash = treeHash == null ? plan.treeHash : treeHash;
|
|
344
|
+
|
|
345
|
+
if (!plan.writeAllowed) {
|
|
346
|
+
// D-8: a non-primary instance never writes the real home. Every result says so; nothing is touched.
|
|
347
|
+
return {
|
|
348
|
+
results: plan.plans.map((entry) => ({
|
|
349
|
+
name: entry.name, action: entry.action, written: false, skipped: 'write_blocked_real_home',
|
|
350
|
+
})),
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const stamp = now();
|
|
355
|
+
const plannedNames = new Set(plan.plans.map((entry) => entry.name));
|
|
356
|
+
const results = [];
|
|
357
|
+
try {
|
|
358
|
+
// Start of apply: everything older than this run EXCEPT the names below — their leftover is the
|
|
359
|
+
// crash fingerprint the re-plan still has to read (see _sweepLeftovers).
|
|
360
|
+
await _sweepLeftovers(root, stamp.getTime(), (name) => !plannedNames.has(name));
|
|
361
|
+
} catch (error) {
|
|
362
|
+
_report(report, error); // REPORT: a failed cleanup must not stop this run's real work
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
for (const entry of plan.plans) {
|
|
366
|
+
const result = { name: entry.name, action: entry.action, written: false, skipped: null };
|
|
367
|
+
try {
|
|
368
|
+
// Re-plan THIS name and refuse if its action moved (a lost race): never write against a stale plan.
|
|
369
|
+
const fresh = await planBundleCodexInstall(plan.treeDir, {
|
|
370
|
+
hubPeerId: hub, homeDir: plan.homeDir, isPrimary: plan.isPrimary, realHome: plan.realHome, log,
|
|
371
|
+
onlyName: entry.name,
|
|
372
|
+
});
|
|
373
|
+
const current = fresh.plans.find((candidate) => candidate.name === entry.name) || null;
|
|
374
|
+
if (!current || current.action !== entry.action) {
|
|
375
|
+
result.skipped = 'plan_changed';
|
|
376
|
+
results.push(result);
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (current.action === 'create' || current.action === 'update') {
|
|
380
|
+
await _installOne(root, current, fresh, { hub, hash, stamp, report, log });
|
|
381
|
+
result.written = true;
|
|
382
|
+
} else if (current.action === 'retire') {
|
|
383
|
+
await _retireOne(root, current, log);
|
|
384
|
+
result.written = true;
|
|
385
|
+
}
|
|
386
|
+
// noop / conflict / edited / removed / skipped_managed: nothing is written.
|
|
387
|
+
} catch (error) {
|
|
388
|
+
// REPORT: one skill's failure never aborts the others (and never rejects the caller).
|
|
389
|
+
result.skipped = 'error';
|
|
390
|
+
_report(report, error);
|
|
391
|
+
}
|
|
392
|
+
results.push(result);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
// End of apply: the names the start sweep had to leave alone (no swap of ours survives this).
|
|
397
|
+
await _sweepLeftovers(root, stamp.getTime(), (name) => plannedNames.has(name));
|
|
398
|
+
} catch (error) {
|
|
399
|
+
_report(report, error); // REPORT: leftover cleanup is best-effort and retried by the next run
|
|
400
|
+
}
|
|
401
|
+
return { results };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async function _installOne(root, current, fresh, { hub, hash, stamp, report, log }) {
|
|
405
|
+
const name = current.name;
|
|
406
|
+
const installedAt = (current.marker && current.marker.installedAt) || stamp.toISOString();
|
|
407
|
+
const staging = path.join(root, `.${name}.ctm-staging-${_rand()}`);
|
|
408
|
+
try {
|
|
409
|
+
await _copyDir(path.join(fresh.treeDir, 'codex', name), staging);
|
|
410
|
+
await atomicWriteFile(path.join(staging, MARKER_FILE), JSON.stringify({
|
|
411
|
+
schema: 2,
|
|
412
|
+
name,
|
|
413
|
+
source: SOURCE,
|
|
414
|
+
hubPeerId: hub,
|
|
415
|
+
treeHash: hash,
|
|
416
|
+
sha256: current.bundleDirHash,
|
|
417
|
+
installedAt,
|
|
418
|
+
updatedAt: stamp.toISOString(),
|
|
419
|
+
}, null, 2) + '\n');
|
|
420
|
+
if (current.action === 'update') {
|
|
421
|
+
// Crash window: between these two renames the target is missing and `.<name>.ctm-retired-*`
|
|
422
|
+
// is present — which is exactly what the planner's rule-1 exception turns back into `create`.
|
|
423
|
+
const retired = path.join(root, `.${name}.ctm-retired-${_rand()}`);
|
|
424
|
+
await fsp.rename(current.targetDir, retired);
|
|
425
|
+
try {
|
|
426
|
+
await fsp.rename(staging, current.targetDir);
|
|
427
|
+
} catch (error) {
|
|
428
|
+
await _removeQuiet(retired); // then RETHROW: never leave a half-swapped install behind
|
|
429
|
+
throw error;
|
|
430
|
+
}
|
|
431
|
+
await _removeQuiet(retired);
|
|
432
|
+
} else {
|
|
433
|
+
await fsp.rename(staging, current.targetDir);
|
|
434
|
+
}
|
|
435
|
+
} catch (error) {
|
|
436
|
+
await _removeQuiet(staging); // RETHROW: the caller REPORTs it and the original directory survives
|
|
437
|
+
throw error;
|
|
438
|
+
}
|
|
439
|
+
await writeRootRecord(root, name, {
|
|
440
|
+
source: SOURCE,
|
|
441
|
+
hubPeerId: hub,
|
|
442
|
+
lastSha256: current.bundleDirHash,
|
|
443
|
+
lastWrittenAt: stamp.toISOString(),
|
|
444
|
+
firstInstalledAt: installedAt,
|
|
445
|
+
}, log);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
async function _retireOne(root, current, log) {
|
|
449
|
+
const retired = path.join(root, `.${current.name}.ctm-retired-${_rand()}`);
|
|
450
|
+
await fsp.rename(current.targetDir, retired);
|
|
451
|
+
await _removeQuiet(retired);
|
|
452
|
+
// Drop the record entry: a later re-add by the hub is `create`, never `removed`.
|
|
453
|
+
await _dropRecordEntry(root, current.name, log);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
let _inFlight = null;
|
|
457
|
+
// hubPeerId -> { dataDir, deps } for calls that arrived while a run was in flight. Unlike the GC
|
|
458
|
+
// (one sweep covers everything), a run installs ONE hub's tree as of when it read the pointer, so a
|
|
459
|
+
// call for another hub — or the same hub after a newer PUT — must get a run of its own afterwards.
|
|
460
|
+
const _pending = new Map();
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Single-flight trigger for the manifest-PUT seam (and any future boot hook). A call while an install
|
|
464
|
+
* is running returns THAT run's promise and queues one follow-up run for its hub (coalesced: the
|
|
465
|
+
* latest deps per hub win), started as soon as the current run settles; a call after it settles
|
|
466
|
+
* starts a new one. Never rejects: the returned promise always resolves (a fault reaches `report`).
|
|
467
|
+
* The tree is resolved through the hub's CURRENT pointer, so the caller needs nothing but the hub
|
|
468
|
+
* identity.
|
|
469
|
+
*
|
|
470
|
+
* Deliberately not `async` — an async wrapper would hand every caller a fresh promise and break the
|
|
471
|
+
* "same promise" contract the single-flight test asserts (gc.js's own reasoning).
|
|
472
|
+
*
|
|
473
|
+
* @param {string} dataDir
|
|
474
|
+
* @param {object} [deps]
|
|
475
|
+
* @param {string} [deps.hubPeerId] the hub whose CURRENT tree to install
|
|
476
|
+
* @param {boolean} [deps.isPrimary]
|
|
477
|
+
* @param {string} [deps.homeDir]
|
|
478
|
+
* @param {string} [deps.realHome]
|
|
479
|
+
* @param {() => Date} [deps.now]
|
|
480
|
+
* @param {Function} [deps.report]
|
|
481
|
+
* @param {Function} [deps.log]
|
|
482
|
+
* @returns {Promise<{results: object[]}>}
|
|
483
|
+
*/
|
|
484
|
+
function scheduleBundleCodexInstall(dataDir, deps = {}) {
|
|
485
|
+
if (_inFlight) {
|
|
486
|
+
_pending.set(String((deps && deps.hubPeerId) || ''), { dataDir, deps });
|
|
487
|
+
return _inFlight;
|
|
488
|
+
}
|
|
489
|
+
const run = Promise.resolve()
|
|
490
|
+
.then(() => _runCodexInstall(dataDir, deps))
|
|
491
|
+
.catch((error) => {
|
|
492
|
+
// REPORT: a run that fails as a whole (not per skill) must not reject into a `void` caller.
|
|
493
|
+
_report(deps && deps.report, error);
|
|
494
|
+
return { results: [] };
|
|
495
|
+
});
|
|
496
|
+
_inFlight = run;
|
|
497
|
+
// `run` never rejects, so this settlement tail cannot itself create an unhandled rejection.
|
|
498
|
+
const settle = () => {
|
|
499
|
+
if (_inFlight !== run) return;
|
|
500
|
+
_inFlight = null;
|
|
501
|
+
if (!_pending.size) return;
|
|
502
|
+
const [key, next] = _pending.entries().next().value;
|
|
503
|
+
_pending.delete(key);
|
|
504
|
+
scheduleBundleCodexInstall(next.dataDir, next.deps); // never rejects (see above)
|
|
505
|
+
};
|
|
506
|
+
run.then(settle, settle);
|
|
507
|
+
return run;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async function _runCodexInstall(dataDir, deps) {
|
|
511
|
+
const hubPeerId = String((deps && deps.hubPeerId) == null ? '' : deps.hubPeerId).trim();
|
|
512
|
+
if (!hubPeerId) return { results: [] }; // no hub, no install (D-14: there is nothing to install)
|
|
513
|
+
const treeHash = await store.currentHubTree(dataDir, hubPeerId);
|
|
514
|
+
if (!treeHash) return { results: [] };
|
|
515
|
+
const treeDir = path.join(store.storeRoot(dataDir), 'trees', treeHash.replace(/^sha256:/, ''));
|
|
516
|
+
if (!(await _isDir(treeDir))) return { results: [] }; // the tree is gone: nothing to install, not a fault
|
|
517
|
+
const plan = await planBundleCodexInstall(treeDir, {
|
|
518
|
+
hubPeerId,
|
|
519
|
+
homeDir: deps.homeDir,
|
|
520
|
+
isPrimary: deps.isPrimary,
|
|
521
|
+
realHome: deps.realHome,
|
|
522
|
+
log: deps.log,
|
|
523
|
+
});
|
|
524
|
+
return applyBundleCodexInstall(plan, {
|
|
525
|
+
hubPeerId,
|
|
526
|
+
treeHash,
|
|
527
|
+
now: deps.now,
|
|
528
|
+
report: deps.report,
|
|
529
|
+
log: deps.log,
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/** Test-only: drop the single-flight latch so each test observes its own run. */
|
|
534
|
+
function _resetForTests() {
|
|
535
|
+
_inFlight = null;
|
|
536
|
+
_pending.clear();
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** Test-only: resolve once no run is in flight and none is queued. `settle` starts the queued run
|
|
540
|
+
* synchronously before any later continuation, so awaiting the current run and re-checking
|
|
541
|
+
* observes every follow-up without a timer. */
|
|
542
|
+
async function _whenIdleForTests() {
|
|
543
|
+
while (_inFlight) await _inFlight;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
module.exports = {
|
|
547
|
+
planBundleCodexInstall,
|
|
548
|
+
applyBundleCodexInstall,
|
|
549
|
+
scheduleBundleCodexInstall,
|
|
550
|
+
dirHash,
|
|
551
|
+
// WP-8: `retireBoxSkill` moves an edited agent-bundle copy aside and must ALSO drop its root-record
|
|
552
|
+
// entry, or the hub's next push would read the missing target as a user DELETION (`removed`) and
|
|
553
|
+
// never re-install the pristine bundle copy. Both helpers are named here rather than re-derived.
|
|
554
|
+
skillsRoot,
|
|
555
|
+
dropRecordEntry: _dropRecordEntry,
|
|
556
|
+
_resetForTests,
|
|
557
|
+
_whenIdleForTests,
|
|
558
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// WP-3 (docs/agent-bundle-darwin-readonly-rename-2026-10-27.html §16 D-6/D-9/D-13, §17): the PURE
|
|
4
|
+
// bundle-drift rules. No fs, no db, no net — the process argv arrives as a string (or an argv
|
|
5
|
+
// array), the store's answer arrives as an already-resolved tree hash, and a session arrives as a
|
|
6
|
+
// plain view object. Everything the reconciler needs to decide is decidable here, which is why the
|
|
7
|
+
// whole decision surface is unit-testable without a server.
|
|
8
|
+
//
|
|
9
|
+
// The contract (D-6): `applied` is the tree hex the RUNNING process was spawned with — null when it
|
|
10
|
+
// was spawned with none, the string 'unknown' when the process could not be read. `expected` is what
|
|
11
|
+
// `store.resolveSpawnBundle(dataDir, sessionId)` answers right now, as hex (null when the box has no
|
|
12
|
+
// hub tree). Drift is `expected !== null && applied !== expected`; 'unknown' is NEVER drift, because
|
|
13
|
+
// an unreadable process is not evidence that the process is wrong.
|
|
14
|
+
|
|
15
|
+
/** D-13: at most this many relaunch attempts per (session, expected tree) pair. */
|
|
16
|
+
const DRIFT_MAX_ATTEMPTS = 2;
|
|
17
|
+
|
|
18
|
+
// The spawn's own shape: `--add-dir <...>/trees/<64 hex>/claude`, followed by whitespace or the end
|
|
19
|
+
// of the line. Anchored on the literal `--add-dir` so a `/trees/<hex>/claude` path that appears in
|
|
20
|
+
// some OTHER token (a cwd, a log line, a `--settings` value) is not mistaken for the bundle.
|
|
21
|
+
const ADD_DIR_TREE_RE = /--add-dir (\S+?)\/trees\/([0-9a-f]{64})\/claude(?=\s|$)/;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Group 2 of the `--add-dir <dir>/trees/<hex>/claude` token in an argv (array) or a `ps` line
|
|
25
|
+
* (string), or null when the spawn carried no bundle. A `ps -o args=` line is the argv joined by
|
|
26
|
+
* single spaces, so both forms are read the same way.
|
|
27
|
+
*
|
|
28
|
+
* @param {string[]|string|null|undefined} argvOrLine
|
|
29
|
+
* @returns {string|null} 64 lowercase hex characters
|
|
30
|
+
*/
|
|
31
|
+
function treeHashFromArgv(argvOrLine) {
|
|
32
|
+
const line = Array.isArray(argvOrLine)
|
|
33
|
+
? argvOrLine.map((value) => String(value == null ? '' : value)).join(' ')
|
|
34
|
+
: String(argvOrLine == null ? '' : argvOrLine);
|
|
35
|
+
if (!line) return null;
|
|
36
|
+
const match = ADD_DIR_TREE_RE.exec(line);
|
|
37
|
+
return match ? match[2] : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The hex of `'sha256:<hex>'` or a bare `'<hex>'`; null for anything else (including null,
|
|
42
|
+
* undefined and the 'unknown' sentinel, which is not a tree).
|
|
43
|
+
*
|
|
44
|
+
* @param {string|null|undefined} treeHash
|
|
45
|
+
* @returns {string|null}
|
|
46
|
+
*/
|
|
47
|
+
function treeHex(treeHash) {
|
|
48
|
+
if (typeof treeHash !== 'string') return null;
|
|
49
|
+
const value = treeHash.trim();
|
|
50
|
+
if (/^sha256:[0-9a-f]{64}$/.test(value)) return value.slice('sha256:'.length);
|
|
51
|
+
if (/^[0-9a-f]{64}$/.test(value)) return value;
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* D-6: does the RUNNING process need to be respawned to carry the tree the box expects now?
|
|
57
|
+
*
|
|
58
|
+
* (null, X) -> no-bundle the process was spawned before a tree existed (deepseek)
|
|
59
|
+
* (X, Y≠X) -> stale the hub pushed a different tree
|
|
60
|
+
* (X, X) -> none
|
|
61
|
+
* (*, null) -> none the box has no hub tree: never force a relaunch onto nothing
|
|
62
|
+
* ('unknown', X) -> unknown-applied, drift:false
|
|
63
|
+
*
|
|
64
|
+
* @returns {{drift: boolean, case: 'none'|'no-bundle'|'stale'|'unknown-applied'}}
|
|
65
|
+
*/
|
|
66
|
+
function computeDrift({ applied = null, expected = null } = {}) {
|
|
67
|
+
if (expected === null || expected === undefined) return { drift: false, case: 'none' };
|
|
68
|
+
if (applied === 'unknown') return { drift: false, case: 'unknown-applied' };
|
|
69
|
+
if (applied === null || applied === undefined) return { drift: true, case: 'no-bundle' };
|
|
70
|
+
if (applied === expected) return { drift: false, case: 'none' };
|
|
71
|
+
return { drift: true, case: 'stale' };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* D-9: the manual-park gate (`evaluateManualPark`, so an unattended relaunch is exactly as
|
|
76
|
+
* acceptable as a park the user's own model switch performs) PLUS three drift-only refusals:
|
|
77
|
+
*
|
|
78
|
+
* - `composer-unknown`: manual park treats an unreadable composer as "no draft" because a user
|
|
79
|
+
* asked. Nobody asked here, so an unreadable composer protects (the opposite of D-4).
|
|
80
|
+
* - `waiting-input`: manual park also rewrites `waiting_input`/`resuming` to `idle` (the user who
|
|
81
|
+
* asked their own model switch is present to answer). An unattended relaunch would kill a turn
|
|
82
|
+
* that is waiting for input, so the drift gate keeps the protection the raw gate carries.
|
|
83
|
+
* - `input-in-flight`: a remote submission is mid-delivery (`_remoteTerminalSubmissionInFlight`).
|
|
84
|
+
* - `transition`: a deferred restore or a lead-model transition is already in flight.
|
|
85
|
+
* - `create-in-flight` (D-10): a new-session create is in flight on this box
|
|
86
|
+
* (`host.createInFlight`: lib/session-create/prepared.js `inflightCreateCount() > 0`). On
|
|
87
|
+
* 2026-11-01 15:33-15:35Z this pass parked and cold-relaunched three live leads while a
|
|
88
|
+
* create was in flight; the relaunch now waits until no create is in progress (not the
|
|
89
|
+
* 120 s restart lease, which outlives the create).
|
|
90
|
+
*
|
|
91
|
+
* A refusal DEFERS the relaunch (the caller records `drift-pending` and retries on the next sweep);
|
|
92
|
+
* it never force-parks.
|
|
93
|
+
*
|
|
94
|
+
* @returns {{ok: boolean, protections: string[]}} the shape §17 names
|
|
95
|
+
*/
|
|
96
|
+
function evaluateDriftRelaunch(session, host, config, { evaluateManualPark } = {}) {
|
|
97
|
+
if (typeof evaluateManualPark !== 'function') throw new TypeError('evaluateDriftRelaunch requires evaluateManualPark');
|
|
98
|
+
// AC-3c: `isActiveTab` / `isMru` are already dropped by evaluateManualPark's own view transform;
|
|
99
|
+
// `pinned` is NOT — it refuses there (tests/session-lifecycle.test.js:385) because a lifecycle pin
|
|
100
|
+
// is the user telling CTM not to RECLAIM the session's memory. A drift relaunch reclaims nothing:
|
|
101
|
+
// it respawns the same tab onto the tree the box already owes it, so pinning must not strand a
|
|
102
|
+
// session without skills. Cleared here rather than in evaluateManualPark (whose other callers rely
|
|
103
|
+
// on the refusal).
|
|
104
|
+
const base = evaluateManualPark({ ...session, pinned: false }, host, config) || { protections: [] };
|
|
105
|
+
const protections = Array.isArray(base.protections) ? base.protections.slice() : [];
|
|
106
|
+
if (session?.liveStatus === 'waiting_input' || session?.liveStatus === 'resuming') protections.push('waiting-input');
|
|
107
|
+
if (session?.composerDraft === 'unknown' && !protections.includes('composer-unknown')) protections.push('composer-unknown');
|
|
108
|
+
if (session?._remoteTerminalSubmissionInFlight) protections.push('input-in-flight');
|
|
109
|
+
if (session?._restoreDeferred === true || session?._activationStartPromise) protections.push('transition');
|
|
110
|
+
if (host?.createInFlight === true) protections.push('create-in-flight');
|
|
111
|
+
return { ok: protections.length === 0, protections };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = {
|
|
115
|
+
DRIFT_MAX_ATTEMPTS,
|
|
116
|
+
treeHashFromArgv,
|
|
117
|
+
treeHex,
|
|
118
|
+
computeDrift,
|
|
119
|
+
evaluateDriftRelaunch,
|
|
120
|
+
};
|