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,424 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// WP-6 (docs/fleet-origin-main-sync-skill-2026-10-31.html §13.4 R-R3/R-R4/R-R6, §13.5, §13.7): the
|
|
3
|
+
// CLI half of the resolver — `land resolve <jobId> --worktree <dir> [--wait] [--json]`.
|
|
4
|
+
//
|
|
5
|
+
// WP-13 (§14) adds the other half: `land resolve <jobId> --claim [--session <id>] [--json]` marks the
|
|
6
|
+
// job's resolver key as owned by the session that is about to resolve it, so the follower's `decide()`
|
|
7
|
+
// skips it instead of opening a second resolver (see `cmdClaim` below).
|
|
8
|
+
//
|
|
9
|
+
// The verb does not resolve anything itself. An agent resolves the merge in its OWN worktree, on the
|
|
10
|
+
// branch `sync/origin-<origin12>` (R-R2/R-R3: only the queue lands, so a hand commit on main in that
|
|
11
|
+
// worktree is never the answer), and this verb VALIDATES that commit and submits it as an ordinary
|
|
12
|
+
// landing job with `kind:"resolve"`. Validation is four refusals, each a typed `landingError` with no
|
|
13
|
+
// job file written:
|
|
14
|
+
//
|
|
15
|
+
// job_not_resolvable the id is unknown, or the job is not `kind:"sync"`, or it is not in
|
|
16
|
+
// `blocked`/`failed` — the two states a resolver session is opened for
|
|
17
|
+
// (§13.4 R-R1).
|
|
18
|
+
// dirty_worktree tracked changes or unmerged paths in `--worktree` (R-R3: the resolution is
|
|
19
|
+
// committed first; a resolution that exists only as a working-tree edit is not
|
|
20
|
+
// a resolution).
|
|
21
|
+
// head_not_merge nothing in `job.base..HEAD` is a merge of EXACTLY the two sides this job
|
|
22
|
+
// conflicted (`{job.base, job.tip}`, order-insensitive) — a hand-built commit,
|
|
23
|
+
// or a rebase that flattened the merge (§13.4 R-R6).
|
|
24
|
+
// conflict_markers a path this job conflicted on still carries `<<<<<<<` / `=======` / `>>>>>>>`
|
|
25
|
+
// in HEAD (R-R3's marker refusal: `git add` of a half-resolved file must not
|
|
26
|
+
// land).
|
|
27
|
+
//
|
|
28
|
+
// Submission reuses the queue primitives `land submit` itself uses — `queue.enqueue` and
|
|
29
|
+
// `syncCli.ensureRunner` — rather than a second copy of the submit path (bin/land.js is under a
|
|
30
|
+
// frozen size ceiling, so the shared internals stay where they already are).
|
|
31
|
+
//
|
|
32
|
+
// The only new state the job carries is `kind:"resolve"`, `resolves:<jobId>` and `gateLane`, which
|
|
33
|
+
// the runner turns into a gate choice: a resolved CONFLICT (`blocked`) is re-gated by the FAST gate
|
|
34
|
+
// (§13.4 R-R4), a resolved GATE FAILURE (`failed`) by the ordinary `landing.gates` (§13.4 R-R6).
|
|
35
|
+
// Publishing is not this module's business: a landed resolve job goes through the existing
|
|
36
|
+
// `afterTrainLanded` auto-publish path like any other landing.
|
|
37
|
+
|
|
38
|
+
const path = require('path');
|
|
39
|
+
|
|
40
|
+
const { landingError, runGitRaw, revParseOrNull } = require('./git');
|
|
41
|
+
const { strFlag } = require('../env-flags');
|
|
42
|
+
const queue = require('./queue');
|
|
43
|
+
const syncCli = require('./sync-cli');
|
|
44
|
+
const { loadLandingConfig, loadTargetLandingConfig } = require('./config');
|
|
45
|
+
// WP-13 (docs/fleet-origin-main-sync-skill-2026-10-31.html §14 WP-13/§14.6): the claim path writes the
|
|
46
|
+
// SAME resolver record `lib/fleet-resolver.js` reads, keyed the same way, through that module's own
|
|
47
|
+
// atomic writer — never a second copy of the state file's shape.
|
|
48
|
+
const resolver = require('../fleet-resolver');
|
|
49
|
+
|
|
50
|
+
// Injected by bin/land.js once, at load (the same handshake sync-cli.js takes). Held as bare
|
|
51
|
+
// bindings so the emitters are the process's own, never a second formatting of the same payload.
|
|
52
|
+
let progress = () => {};
|
|
53
|
+
let emitJsonOrLine = () => {};
|
|
54
|
+
let context = null;
|
|
55
|
+
let branchRef = null;
|
|
56
|
+
let CONTRACT = 1;
|
|
57
|
+
let WAIT_TIMEOUT_MS = 4 * 60 * 60 * 1000;
|
|
58
|
+
|
|
59
|
+
// `git status --porcelain` v1 unmerged codes (`git help status`): both sides of the merge still in
|
|
60
|
+
// the index. Every other non-`??` code is an ordinary tracked change.
|
|
61
|
+
const UNMERGED_CODES = Object.freeze(['DD', 'AU', 'UD', 'UA', 'DU', 'AA', 'UU']);
|
|
62
|
+
// §13.4 R-R3: a marker line is seven identical characters at column 0, then a space or end of line —
|
|
63
|
+
// the same shape `git diff --check` reports.
|
|
64
|
+
const MARKER_RE = new RegExp('^(?:<{7}|={7}|>{7})(?: |$)');
|
|
65
|
+
const RESOLVABLE_STATES = Object.freeze(['blocked', 'failed']);
|
|
66
|
+
|
|
67
|
+
function lines(stdout) {
|
|
68
|
+
return String(stdout || '').split('\n').map((line) => line.trim()).filter(Boolean);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// --- validation ------------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
// The refusal R-R3 makes first: the resolution has to be committed in `--worktree`. Untracked files
|
|
74
|
+
// are not an answer either way (they cannot be part of a commit this verb is about to land), so they
|
|
75
|
+
// are ignored exactly as `land submit` ignores them.
|
|
76
|
+
function worktreeDirt(worktree) {
|
|
77
|
+
const res = runGitRaw(worktree, ['status', '--porcelain']);
|
|
78
|
+
if (res.status !== 0) {
|
|
79
|
+
throw landingError('git_failed', `git status failed in ${worktree}`, { cwd: worktree, stderr: String(res.stderr || '') });
|
|
80
|
+
}
|
|
81
|
+
const dirty = [];
|
|
82
|
+
const unmerged = [];
|
|
83
|
+
for (const raw of String(res.stdout || '').split('\n')) {
|
|
84
|
+
if (!raw.trim()) continue;
|
|
85
|
+
const code = raw.slice(0, 2);
|
|
86
|
+
if (code === '??') continue;
|
|
87
|
+
const file = raw.slice(3).trim();
|
|
88
|
+
if (UNMERGED_CODES.includes(code)) unmerged.push(file);
|
|
89
|
+
else dirty.push(file);
|
|
90
|
+
}
|
|
91
|
+
return { dirty, unmerged };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// R-R6: the commit that IS the resolution — a merge commit in `job.base..HEAD` whose parents are
|
|
95
|
+
// exactly the two sides this job conflicted. Order-insensitive, because which side git names first
|
|
96
|
+
// depends on which branch was checked out. `null` means the worktree holds no such commit.
|
|
97
|
+
function resolutionCommit(worktree, job) {
|
|
98
|
+
if (!job.base || !job.tip) return null;
|
|
99
|
+
const res = runGitRaw(worktree, ['rev-list', '--merges', '--parents', `${job.base}..HEAD`]);
|
|
100
|
+
if (res.status !== 0) return null;
|
|
101
|
+
for (const line of lines(res.stdout)) {
|
|
102
|
+
const [commit, ...parents] = line.split(/\s+/);
|
|
103
|
+
if (parents.length !== 2 || !commit) continue;
|
|
104
|
+
const forward = parents[0] === job.base && parents[1] === job.tip;
|
|
105
|
+
const reverse = parents[0] === job.tip && parents[1] === job.base;
|
|
106
|
+
if (forward || reverse) return commit;
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// R-R3's marker refusal: HEAD's version of every path this job conflicted on, scanned line by line.
|
|
112
|
+
// A path that does not exist in HEAD cannot hold a marker and is skipped (a resolution may legitimately
|
|
113
|
+
// delete a path).
|
|
114
|
+
function conflictMarkerHits(worktree, paths) {
|
|
115
|
+
const hits = [];
|
|
116
|
+
for (const rel of Array.isArray(paths) ? paths : []) {
|
|
117
|
+
if (typeof rel !== 'string' || !rel) continue;
|
|
118
|
+
const res = runGitRaw(worktree, ['show', `HEAD:${rel}`]);
|
|
119
|
+
if (res.status !== 0) continue;
|
|
120
|
+
const fileLines = String(res.stdout || '').split('\n');
|
|
121
|
+
for (let index = 0; index < fileLines.length; index += 1) {
|
|
122
|
+
if (MARKER_RE.test(fileLines[index])) hits.push(`${rel}:${index + 1}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return hits;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// --- submission ---------------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
// §13.5: the resolve job is an ordinary queue job with three extra fields. `gateLane` is decided
|
|
131
|
+
// HERE, from the state the sync job died in, and never re-derived later: `blocked` is a resolved
|
|
132
|
+
// conflict (fast), `failed` is a gate failure (full).
|
|
133
|
+
function resolveJobRecord(ctx, job, { branch, tip, worktree, config }) {
|
|
134
|
+
const session = strFlag('CTM_SESSION_ID') || null;
|
|
135
|
+
return {
|
|
136
|
+
contract: CONTRACT,
|
|
137
|
+
id: queue.newJobId(),
|
|
138
|
+
kind: 'resolve',
|
|
139
|
+
resolves: job.id,
|
|
140
|
+
gateLane: job.state === 'blocked' ? 'fast' : 'full',
|
|
141
|
+
repo: ctx.common,
|
|
142
|
+
branch,
|
|
143
|
+
tip,
|
|
144
|
+
title: `resolve ${job.id} (${branch})`,
|
|
145
|
+
submittedBy: { session, pid: process.pid, cwd: worktree },
|
|
146
|
+
state: 'queued',
|
|
147
|
+
reason: null,
|
|
148
|
+
priority: 'normal',
|
|
149
|
+
priorityHistory: [],
|
|
150
|
+
preemptions: 0,
|
|
151
|
+
base: null,
|
|
152
|
+
train: null,
|
|
153
|
+
tree: null,
|
|
154
|
+
verdict: null,
|
|
155
|
+
conflictPaths: [],
|
|
156
|
+
conflictPreview: null,
|
|
157
|
+
overlapWithTarget: [],
|
|
158
|
+
policySource: config.policySource || null,
|
|
159
|
+
runnerPid: null,
|
|
160
|
+
timestamps: { queued: queue.nowIso() },
|
|
161
|
+
gateLog: null,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// --- WP-13 `--claim` ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
// §14 WP-13: the two states that mean "something already holds this box's resolver slot" — the same
|
|
168
|
+
// pair `lib/fleet-resolver.js` calls LIVE_STATES (not exported there; `lib/fleet-resolver-tick.js`
|
|
169
|
+
// repeats it the same way rather than widening the module). Keep the copies in step.
|
|
170
|
+
const RESOLVER_LIVE_STATES = Object.freeze(['starting', 'resolving']);
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* §14 WP-13: who owns this claim — `--session <id>` wins, then the session this CLI runs inside, then
|
|
174
|
+
* the literal `session:unknown` (the readonly `CTM_SESSION_ID` flag read, never a raw `process.env`).
|
|
175
|
+
*/
|
|
176
|
+
function claimOwner(opts) {
|
|
177
|
+
const fromArg = opts && opts.values && typeof opts.values.session === 'string' ? opts.values.session : '';
|
|
178
|
+
const session = fromArg || strFlag('CTM_SESSION_ID') || '';
|
|
179
|
+
return `session:${session || 'unknown'}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** §14 WP-13: `resolveMaxMinutes` from the target's committed landing policy, else the module default. */
|
|
183
|
+
function claimMaxMinutes(cwd) {
|
|
184
|
+
let publish = null;
|
|
185
|
+
try {
|
|
186
|
+
publish = loadLandingConfig(cwd).publish;
|
|
187
|
+
} catch (error) {
|
|
188
|
+
/* HANDLE: an unreadable/invalid landing policy is not fatal to a claim — the default deadline
|
|
189
|
+
is the same one lib/fleet-resolver.js would have started the resolver with. */
|
|
190
|
+
void error;
|
|
191
|
+
}
|
|
192
|
+
const minutes = publish && Number.isFinite(publish.resolveMaxMinutes) && publish.resolveMaxMinutes > 0
|
|
193
|
+
? publish.resolveMaxMinutes
|
|
194
|
+
: resolver.DEFAULT_RESOLVE_MAX_MINUTES;
|
|
195
|
+
return minutes;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* §14 WP-13: the live record a claim writes — the `lib/fleet-resolver.js` record shape (so the
|
|
200
|
+
* follower's own readers, `keep`/`claimed` rule and history all see a record they already understand)
|
|
201
|
+
* plus the two fields that make it a claim: `owner` and `claimedAt`. A record already on disk for the
|
|
202
|
+
* same key keeps whatever it knew (a CTM-started session's id, worktree, model) — the claim takes
|
|
203
|
+
* ownership, it does not pretend that session never existed.
|
|
204
|
+
*/
|
|
205
|
+
function claimRecord(job, { owner, claimedAt, deadlineAt, prior }) {
|
|
206
|
+
const before = prior && typeof prior === 'object' ? prior : {};
|
|
207
|
+
return {
|
|
208
|
+
...before,
|
|
209
|
+
key: resolver.jobKey(job),
|
|
210
|
+
trigger: job.state === 'blocked' ? 'conflict' : 'gate_failed',
|
|
211
|
+
jobId: job.id || job.jobId || null,
|
|
212
|
+
conflictPaths: Array.isArray(job.conflictPaths) ? job.conflictPaths : [],
|
|
213
|
+
gateLog: job.gateLog === undefined ? null : job.gateLog,
|
|
214
|
+
base: job.base,
|
|
215
|
+
tip: job.tip,
|
|
216
|
+
sessionId: before.sessionId === undefined ? null : before.sessionId,
|
|
217
|
+
branch: resolver.branchFor(job.tip),
|
|
218
|
+
worktree: before.worktree === undefined ? null : before.worktree,
|
|
219
|
+
agent: before.agent === undefined ? null : before.agent,
|
|
220
|
+
model: before.model || '',
|
|
221
|
+
modelSource: before.modelSource || '',
|
|
222
|
+
state: 'resolving',
|
|
223
|
+
startedAt: claimedAt,
|
|
224
|
+
deadlineAt,
|
|
225
|
+
updatedAt: claimedAt,
|
|
226
|
+
reason: '',
|
|
227
|
+
owner,
|
|
228
|
+
claimedAt,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* §14 WP-13 AC-13a: `land resolve <jobId> --claim [--session <id>] [--json]`. It runs NO gate and
|
|
234
|
+
* submits nothing: it marks this job's resolver key as owned by the session that is about to resolve
|
|
235
|
+
* it (`/fleet-sync` in the user's own session), so the follower's `decide()` skips the key instead of
|
|
236
|
+
* opening a second resolver for the same conflict.
|
|
237
|
+
*/
|
|
238
|
+
async function cmdClaim(opts, cwd) {
|
|
239
|
+
const jobId = opts.positionals[0];
|
|
240
|
+
if (!jobId) throw usageError('land resolve <jobId> --claim [--session <id>] [--json]');
|
|
241
|
+
const ctx = context(cwd);
|
|
242
|
+
const job = queue.readJob(ctx.root, jobId);
|
|
243
|
+
if (!job || job.kind !== 'sync' || !RESOLVABLE_STATES.includes(job.state)) {
|
|
244
|
+
throw landingError(
|
|
245
|
+
'job_not_resolvable',
|
|
246
|
+
`job ${jobId} is not a resolvable sync job`
|
|
247
|
+
+ `${job ? ` (kind ${job.kind || 'none'}, state ${job.state})` : ' (no such job)'}; `
|
|
248
|
+
+ 'only a kind:"sync" job in blocked or failed can be claimed',
|
|
249
|
+
{ jobId, kind: job ? job.kind || null : null, state: job ? job.state : null },
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const owner = claimOwner(opts);
|
|
254
|
+
const key = resolver.jobKey(job);
|
|
255
|
+
if (!key) {
|
|
256
|
+
throw landingError('job_not_resolvable', `job ${jobId} has no base/tip pair to key a resolver record on`, { jobId });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const prev = await resolver.readState(ctx.common);
|
|
260
|
+
const onDisk = prev.current && typeof prev.current === 'object' ? prev.current : null;
|
|
261
|
+
const live = onDisk && onDisk.key === key && RESOLVER_LIVE_STATES.includes(onDisk.state) ? onDisk : null;
|
|
262
|
+
// §14 WP-13: a session CTM started for this very key stands down — the user's session wins. Say so,
|
|
263
|
+
// then claim anyway: the follower must not start a second resolver, and the record is the only place
|
|
264
|
+
// it reads that from.
|
|
265
|
+
if (live && !resolver.claimedBySession(live)) {
|
|
266
|
+
progress(
|
|
267
|
+
`resolve: ${key} already has a CTM-started resolver session ${live.sessionId || '(unnamed)'}; `
|
|
268
|
+
+ `${owner} claims it — CTM will not start a second one`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const claimedAt = queue.nowIso();
|
|
273
|
+
const deadlineAt = new Date(Date.parse(claimedAt) + claimMaxMinutes(cwd) * 60 * 1000).toISOString();
|
|
274
|
+
await resolver.writeState(ctx.common, {
|
|
275
|
+
current: claimRecord(job, { owner, claimedAt, deadlineAt, prior: live }),
|
|
276
|
+
history: prev.history,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const payload = { ok: true, key, owner };
|
|
280
|
+
emitJsonOrLine(
|
|
281
|
+
opts,
|
|
282
|
+
payload,
|
|
283
|
+
`resolve: claimed ${key} for ${owner}${live && live.sessionId ? ` (CTM's ${live.sessionId} stands down)` : ''}`,
|
|
284
|
+
);
|
|
285
|
+
return 0;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function exitCodeForVerdict(verdict) {
|
|
289
|
+
if (verdict === 'landed' || verdict === 'queued') return 0;
|
|
290
|
+
if (verdict === 'timeout') return 75;
|
|
291
|
+
return 1;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function cmdResolve(opts, cwd) {
|
|
295
|
+
// §14 WP-13: `--claim` is the same verb's second half — mark the key owned and stop, before any of
|
|
296
|
+
// the validate-and-submit work below (no --worktree, no gate, no job file).
|
|
297
|
+
if (opts.flags.has('claim')) return cmdClaim(opts, cwd);
|
|
298
|
+
const jobId = opts.positionals[0];
|
|
299
|
+
if (!jobId) throw usageError('land resolve <jobId> --worktree <dir> [--wait] [--json]');
|
|
300
|
+
const worktreeArg = opts.values.worktree;
|
|
301
|
+
if (!worktreeArg) throw usageError('land resolve <jobId> requires --worktree <dir>');
|
|
302
|
+
const worktree = path.resolve(cwd, worktreeArg);
|
|
303
|
+
// The argument is proved through the git layer this module already uses, not `fs`: a path that is
|
|
304
|
+
// not a worktree at all must be an argument error here, not a `git_failed` stack out of the first
|
|
305
|
+
// `git status` below.
|
|
306
|
+
if (runGitRaw(worktree, ['rev-parse', '--show-toplevel']).status !== 0) {
|
|
307
|
+
throw usageError(`--worktree ${worktree} is not a git worktree`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const ctx = context(cwd);
|
|
311
|
+
const job = queue.readJob(ctx.root, jobId);
|
|
312
|
+
if (!job || job.kind !== 'sync' || !RESOLVABLE_STATES.includes(job.state)) {
|
|
313
|
+
throw landingError(
|
|
314
|
+
'job_not_resolvable',
|
|
315
|
+
`job ${jobId} is not a resolvable sync job`
|
|
316
|
+
+ `${job ? ` (kind ${job.kind || 'none'}, state ${job.state})` : ' (no such job)'}; `
|
|
317
|
+
+ 'only a kind:"sync" job in blocked or failed can be resolved',
|
|
318
|
+
{ jobId, kind: job ? job.kind || null : null, state: job ? job.state : null },
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const dirt = worktreeDirt(worktree);
|
|
323
|
+
if (dirt.unmerged.length || dirt.dirty.length) {
|
|
324
|
+
const paths = [...dirt.unmerged, ...dirt.dirty];
|
|
325
|
+
throw landingError(
|
|
326
|
+
'dirty_worktree',
|
|
327
|
+
`${worktree} has ${dirt.unmerged.length} unmerged path(s) and ${dirt.dirty.length} modified tracked file(s) `
|
|
328
|
+
+ `(${paths.join(', ')}); commit the resolution before resolving`,
|
|
329
|
+
{ paths, unmerged: dirt.unmerged, dirty: dirt.dirty },
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const merge = resolutionCommit(worktree, job);
|
|
334
|
+
if (!merge) {
|
|
335
|
+
throw landingError(
|
|
336
|
+
'head_not_merge',
|
|
337
|
+
`no commit in ${job.base}..HEAD of ${worktree} merges exactly {${job.base}, ${job.tip}}; `
|
|
338
|
+
+ `merge origin/${job.branch || 'main'} into ${worktree} and commit the resolution`,
|
|
339
|
+
{ base: job.base, tip: job.tip, worktree },
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const hits = conflictMarkerHits(worktree, job.conflictPaths);
|
|
344
|
+
if (hits.length) {
|
|
345
|
+
throw landingError(
|
|
346
|
+
'conflict_markers',
|
|
347
|
+
`unresolved conflict markers in ${hits.join(', ')}`,
|
|
348
|
+
{ hits, paths: job.conflictPaths || [] },
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Gates are read from the TARGET's committed policy, exactly as `land submit` reads them.
|
|
353
|
+
const config = loadTargetLandingConfig(worktree);
|
|
354
|
+
const branch = currentBranch(worktree);
|
|
355
|
+
const tip = revParseOrNull(worktree, branchRef(branch));
|
|
356
|
+
if (!tip) {
|
|
357
|
+
throw landingError('git_failed', `cannot resolve ${branch} in ${worktree}`, { branch, worktree });
|
|
358
|
+
}
|
|
359
|
+
queue.reapDeadRunner(ctx.root);
|
|
360
|
+
const record = resolveJobRecord(ctx, job, { branch, tip, worktree, config });
|
|
361
|
+
progress(`resolve: submitting ${record.id} for ${job.id} (${branch}@${tip.slice(0, 12)}, gate lane ${record.gateLane})`);
|
|
362
|
+
queue.enqueue(ctx.root, record);
|
|
363
|
+
const started = syncCli.ensureRunner(ctx);
|
|
364
|
+
if (started.pid !== null) queue.updateJob(ctx.root, record.id, { runnerPid: started.pid });
|
|
365
|
+
|
|
366
|
+
const payload = { verdict: null, jobId: record.id, resolves: job.id, gateLane: record.gateLane };
|
|
367
|
+
if (opts.flags.has('wait')) {
|
|
368
|
+
// §13.5: the wait is `land sync`'s own (sync-cli.js `waitForSyncJob`) — it already knows how to
|
|
369
|
+
// tell "the runner died" from "still working", which a bare poll of the terminal state does not.
|
|
370
|
+
const final = await syncCli.waitForSyncJob(ctx, record.id);
|
|
371
|
+
payload.verdict = final.state;
|
|
372
|
+
emitJsonOrLine(
|
|
373
|
+
opts,
|
|
374
|
+
payload,
|
|
375
|
+
`resolve: job ${record.id} ${final.state}${final.reason ? ` (${final.reason})` : ''} for ${job.id}${record.gateLane === 'fast' ? ' [fast gate]' : ''}`,
|
|
376
|
+
);
|
|
377
|
+
return exitCodeForVerdict(final.state);
|
|
378
|
+
}
|
|
379
|
+
payload.verdict = 'queued';
|
|
380
|
+
emitJsonOrLine(
|
|
381
|
+
opts,
|
|
382
|
+
payload,
|
|
383
|
+
`resolve: job ${record.id} queued for ${job.id} on ${branch} [${record.gateLane} gate] (runner: ${started.runner})`,
|
|
384
|
+
);
|
|
385
|
+
return 0;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function currentBranch(worktree) {
|
|
389
|
+
const res = runGitRaw(worktree, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
390
|
+
const name = res.status === 0 ? res.stdout.trim() : '';
|
|
391
|
+
if (!name || name === 'HEAD') {
|
|
392
|
+
throw landingError('detached_head', `${worktree} is on a detached HEAD; check out the resolver branch first`, { worktree });
|
|
393
|
+
}
|
|
394
|
+
return name;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function usageError(message) {
|
|
398
|
+
const error = new Error(message);
|
|
399
|
+
error.code = 'usage';
|
|
400
|
+
return error;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// bin/land.js calls this once, at load, with its own process-level helpers — the same handshake
|
|
404
|
+
// sync-cli.js takes. A caller that forgets it gets no-op emitters, not a TypeError mid-command.
|
|
405
|
+
function configure(injected) {
|
|
406
|
+
({
|
|
407
|
+
progress,
|
|
408
|
+
emitJsonOrLine,
|
|
409
|
+
context,
|
|
410
|
+
branchRef,
|
|
411
|
+
CONTRACT,
|
|
412
|
+
WAIT_TIMEOUT_MS,
|
|
413
|
+
} = injected);
|
|
414
|
+
return module.exports;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
module.exports = {
|
|
418
|
+
configure,
|
|
419
|
+
cmdResolve,
|
|
420
|
+
cmdClaim,
|
|
421
|
+
worktreeDirt,
|
|
422
|
+
resolutionCommit,
|
|
423
|
+
conflictMarkerHits,
|
|
424
|
+
};
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// WP-0 (docs/local-landing-queue-multi-session-integration-2026-10-26.html §5 D-1, §6.3, §8 WP-0,
|
|
3
|
+
// §7 E-5): per-repository state for the landing queue.
|
|
4
|
+
//
|
|
5
|
+
// State lives at `$(git rev-parse --git-common-dir)/landing/` — shared by every worktree of the
|
|
6
|
+
// repo, never inside a worktree, never under os.tmpdir() (D-1). Files are small JSON documents
|
|
7
|
+
// written tmp+rename, and every one of them carries `contract: 1` so an old CLI reading new
|
|
8
|
+
// state refuses with `contract_unsupported` instead of corrupting it (E-5).
|
|
9
|
+
//
|
|
10
|
+
// The queue has no daemon (D-2); the only cross-process serialization primitive is a `.mutex`
|
|
11
|
+
// directory next to the state files, exactly like the broker's `.admission` lock. The mutex is
|
|
12
|
+
// won by renaming a fully-stamped staging directory into place and released the same way, so the
|
|
13
|
+
// path is never observable without an owner stamp and never briefly empty (§5 D-1/D-2).
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const crypto = require('crypto');
|
|
18
|
+
const { commonDir, landingError } = require('./git');
|
|
19
|
+
|
|
20
|
+
const CONTRACT = 1;
|
|
21
|
+
const MUTEX_DIRNAME = '.mutex';
|
|
22
|
+
// Staging directories for a mutex claim live NEXT TO the mutex (same filesystem, so the rename
|
|
23
|
+
// into place is atomic) and are named so they can never be mistaken for the mutex itself.
|
|
24
|
+
const MUTEX_STAGE_PREFIX = '.mutex-claim-';
|
|
25
|
+
const OWNER_FILENAME = 'owner.json';
|
|
26
|
+
|
|
27
|
+
// §6.3 directory layout under landing/.
|
|
28
|
+
const SUBDIRS = Object.freeze(['jobs', 'runner', 'verdicts', 'trains', 'radar', 'claims', 'metrics']);
|
|
29
|
+
|
|
30
|
+
// `$(git rev-parse --git-common-dir)/landing` (absolutized by git itself).
|
|
31
|
+
function landingDir(cwd) {
|
|
32
|
+
return path.join(commonDir(cwd), 'landing');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function mutexPath(root) {
|
|
36
|
+
return path.join(root, MUTEX_DIRNAME);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function ensureDirs(root) {
|
|
40
|
+
fs.mkdirSync(root, { recursive: true });
|
|
41
|
+
for (const name of SUBDIRS) fs.mkdirSync(path.join(root, name), { recursive: true });
|
|
42
|
+
return root;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Atomic JSON write: `<file>.<pid>.<rand>.tmp` then renameSync (same-filesystem, so the rename
|
|
46
|
+
// is atomic). A plain object without an explicit `contract` is stamped with contract 1 — every
|
|
47
|
+
// state file in §6.3 carries it.
|
|
48
|
+
function writeJsonAtomic(file, obj) {
|
|
49
|
+
let payload = obj;
|
|
50
|
+
if (obj && typeof obj === 'object' && !Array.isArray(obj) && obj.contract === undefined) {
|
|
51
|
+
payload = { ...obj, contract: CONTRACT };
|
|
52
|
+
}
|
|
53
|
+
const tmp = `${file}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`;
|
|
54
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
55
|
+
fs.writeFileSync(tmp, `${JSON.stringify(payload, null, 2)}\n`);
|
|
56
|
+
try {
|
|
57
|
+
fs.renameSync(tmp, file);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
try {
|
|
60
|
+
fs.rmSync(tmp, { force: true });
|
|
61
|
+
} catch (cleanupError) {
|
|
62
|
+
/* expected: best-effort removal of our own tmp file; the rename failure below is the
|
|
63
|
+
error that matters and is rethrown unchanged */
|
|
64
|
+
}
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
return file;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// object | null (ENOENT) | throws `state_unreadable` (bad JSON or unreadable file). Callers that
|
|
71
|
+
// must never treat a torn stamp as "free" should let the throw propagate, or use readJsonSafe.
|
|
72
|
+
function readJson(file) {
|
|
73
|
+
let raw;
|
|
74
|
+
try {
|
|
75
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error && error.code === 'ENOENT') return null;
|
|
78
|
+
throw landingError('state_unreadable', `cannot read ${file}: ${error.message}`, { file, cause: error });
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(raw);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
throw landingError('state_unreadable', `bad JSON in ${file}: ${error.message}`, { file, cause: error });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Same as readJson, but an unreadable file becomes `null` instead of a throw — for callers whose
|
|
88
|
+
// safety property is "unknown holder", never "free" (the broker's UNREADABLE disposition).
|
|
89
|
+
function readJsonSafe(file) {
|
|
90
|
+
try {
|
|
91
|
+
return readJson(file);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
/* expected: unreadable/torn stamp is "unknown", and every caller of readJsonSafe treats
|
|
94
|
+
null as "not ours / not reapable" rather than as an absent record */
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// E-5: refuse state written by a contract this build does not understand.
|
|
100
|
+
function assertContract(obj, meta = {}) {
|
|
101
|
+
if (!obj || typeof obj !== 'object') {
|
|
102
|
+
throw landingError('contract_unsupported', `expected a contract-${CONTRACT} object`, { ...meta, contract: null });
|
|
103
|
+
}
|
|
104
|
+
if (obj.contract !== CONTRACT) {
|
|
105
|
+
throw landingError(
|
|
106
|
+
'contract_unsupported',
|
|
107
|
+
`state contract ${JSON.stringify(obj.contract)} is not supported (this build speaks ${CONTRACT})`,
|
|
108
|
+
{ ...meta, contract: obj.contract, expected: CONTRACT },
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
return obj;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Synchronous sleep, used only for the mutex backoff.
|
|
115
|
+
function sleepSync(ms) {
|
|
116
|
+
if (!(ms > 0)) return;
|
|
117
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function pidIsAlive(pid, kill) {
|
|
121
|
+
try {
|
|
122
|
+
kill(pid, 0);
|
|
123
|
+
return true;
|
|
124
|
+
} catch (error) {
|
|
125
|
+
// ESRCH = no such process (dead). EPERM = alive but not ours — still alive.
|
|
126
|
+
return !(error && error.code === 'ESRCH');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Reap a mutex whose owner stamp is readable AND names a dead pid. An unreadable stamp is never
|
|
131
|
+
// reaped (we cannot tell a dead owner from one we merely failed to read). Removal is the same
|
|
132
|
+
// verified pattern as the lease: re-read, require the same record, rename to a private tombstone,
|
|
133
|
+
// then remove the tombstone.
|
|
134
|
+
function reapMutexIfDead(mutexDir, { kill }) {
|
|
135
|
+
const stampFile = path.join(mutexDir, OWNER_FILENAME);
|
|
136
|
+
const stamp = readJsonSafe(stampFile);
|
|
137
|
+
if (!stamp || typeof stamp.pid !== 'number') return false;
|
|
138
|
+
if (pidIsAlive(stamp.pid, kill)) return false;
|
|
139
|
+
const again = readJsonSafe(stampFile);
|
|
140
|
+
if (!again || again.pid !== stamp.pid || again.since !== stamp.since) return false;
|
|
141
|
+
const tombstone = `${mutexDir}.dead-${process.pid}-${crypto.randomBytes(4).toString('hex')}`;
|
|
142
|
+
try {
|
|
143
|
+
fs.renameSync(mutexDir, tombstone);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
/* expected: another process won the reap or reclaimed the mutex between the read and the
|
|
146
|
+
rename; its holder is unaffected and we simply retry */
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
fs.rmSync(tombstone, { recursive: true, force: true });
|
|
151
|
+
} catch (error) {
|
|
152
|
+
/* expected: the tombstone is now private to this call, so a failed cleanup can only leave a
|
|
153
|
+
stray .dead-* directory behind; the mutex path itself is already free */
|
|
154
|
+
}
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Claim the mutex the way the lease claims its directory: stage a fully-stamped directory under
|
|
159
|
+
// a private mkdtemp name, then renameSync it ONTO <root>/.mutex. A rename onto a NON-EMPTY
|
|
160
|
+
// directory fails atomically (ENOTEMPTY/EEXIST), so the mutex path is never observable as
|
|
161
|
+
// "exists but has no owner stamp" — a process killed mid-claim leaves a stray `.mutex-claim-*`
|
|
162
|
+
// staging directory, never a lock that every later caller times out on. A rename onto an EMPTY
|
|
163
|
+
// .mutex succeeds, which is why that stray state is not a deadlock either.
|
|
164
|
+
function tryClaimMutex(root, now) {
|
|
165
|
+
const mutexDir = mutexPath(root);
|
|
166
|
+
let staging;
|
|
167
|
+
try {
|
|
168
|
+
staging = fs.mkdtempSync(path.join(root, MUTEX_STAGE_PREFIX));
|
|
169
|
+
} catch (error) {
|
|
170
|
+
throw landingError('mutex_stage_failed', `cannot stage a mutex claim under ${root}: ${error.message}`, { root, cause: error });
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
writeJsonAtomic(path.join(staging, OWNER_FILENAME), { pid: process.pid, since: new Date(now).toISOString() });
|
|
174
|
+
} catch (error) {
|
|
175
|
+
removeDir(staging);
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
fs.renameSync(staging, mutexDir);
|
|
180
|
+
return true;
|
|
181
|
+
} catch (error) {
|
|
182
|
+
removeDir(staging);
|
|
183
|
+
if (error && (error.code === 'ENOTEMPTY' || error.code === 'EEXIST')) return false;
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function removeDir(dir) {
|
|
189
|
+
try {
|
|
190
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
191
|
+
} catch (error) {
|
|
192
|
+
/* expected: best-effort cleanup of a directory this call created (a staging dir, or the
|
|
193
|
+
private tombstone of a release); the caller's own outcome is unaffected */
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// §5 (file-based state, "queue.json rewritten under a mkdir mutex like .admission").
|
|
198
|
+
// Sync `fn` holds the mutex for its duration; a thenable return value holds it until settlement.
|
|
199
|
+
function withMutex(root, fn, opts = {}) {
|
|
200
|
+
const timeoutMs = Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : 10000;
|
|
201
|
+
const now = typeof opts.now === 'function' ? opts.now : () => (typeof opts.now === 'number' ? opts.now : Date.now());
|
|
202
|
+
const kill = opts.kill || process.kill;
|
|
203
|
+
const mutexDir = mutexPath(root);
|
|
204
|
+
fs.mkdirSync(root, { recursive: true });
|
|
205
|
+
const deadline = now() + timeoutMs;
|
|
206
|
+
let delay = 2;
|
|
207
|
+
for (;;) {
|
|
208
|
+
if (tryClaimMutex(root, now())) break;
|
|
209
|
+
reapMutexIfDead(mutexDir, { kill });
|
|
210
|
+
if (now() >= deadline) {
|
|
211
|
+
throw landingError('mutex_timeout', `timed out after ${timeoutMs}ms waiting for ${mutexDir}`, { mutexDir, timeoutMs });
|
|
212
|
+
}
|
|
213
|
+
sleepSync(delay);
|
|
214
|
+
delay = Math.min(delay * 2, 25);
|
|
215
|
+
}
|
|
216
|
+
let released = false;
|
|
217
|
+
// Release by renaming the whole directory to a private tombstone FIRST and only then removing
|
|
218
|
+
// the tombstone: rmSync(recursive) unlinks owner.json and rmdirs the path in two steps, which
|
|
219
|
+
// leaves a briefly-EMPTY .mutex — exactly the directory a concurrent staged rename would then
|
|
220
|
+
// land in, giving two holders.
|
|
221
|
+
const release = () => {
|
|
222
|
+
if (released) return;
|
|
223
|
+
released = true;
|
|
224
|
+
const tombstone = `${mutexDir}.dead-${process.pid}-${crypto.randomBytes(4).toString('hex')}`;
|
|
225
|
+
try {
|
|
226
|
+
fs.renameSync(mutexDir, tombstone);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
/* expected: the mutex directory is already gone (a reap that raced our own release, or an
|
|
229
|
+
earlier release in this process); there is nothing left to remove */
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
removeDir(tombstone);
|
|
233
|
+
};
|
|
234
|
+
let result;
|
|
235
|
+
try {
|
|
236
|
+
result = fn();
|
|
237
|
+
} catch (error) {
|
|
238
|
+
release();
|
|
239
|
+
throw error;
|
|
240
|
+
}
|
|
241
|
+
if (result && typeof result.then === 'function') return Promise.resolve(result).finally(release);
|
|
242
|
+
release();
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
module.exports = {
|
|
247
|
+
CONTRACT,
|
|
248
|
+
MUTEX_DIRNAME,
|
|
249
|
+
MUTEX_STAGE_PREFIX,
|
|
250
|
+
OWNER_FILENAME,
|
|
251
|
+
SUBDIRS,
|
|
252
|
+
landingDir,
|
|
253
|
+
mutexPath,
|
|
254
|
+
ensureDirs,
|
|
255
|
+
writeJsonAtomic,
|
|
256
|
+
readJson,
|
|
257
|
+
readJsonSafe,
|
|
258
|
+
assertContract,
|
|
259
|
+
withMutex,
|
|
260
|
+
reapMutexIfDead,
|
|
261
|
+
pidIsAlive,
|
|
262
|
+
sleepSync,
|
|
263
|
+
};
|