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,998 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// WP-1 (docs/local-landing-queue-multi-session-integration-2026-10-26.html §5 D-1/D-2, §6.3,
|
|
3
|
+
// §8 WP-1): job and queue state operations for the landing queue.
|
|
4
|
+
//
|
|
5
|
+
// All state lives under `<git-common-dir>/landing/` (WP-0 `state.js`), shared by every worktree of
|
|
6
|
+
// the repository. `queue.json` is the single ordering authority — its `order` array holds the id of
|
|
7
|
+
// every NON-TERMINAL job, sorted: the `in_progress` ids first in their existing relative order, then
|
|
8
|
+
// the `queued` ids by priority rank descending (`normal` < `high` < `urgent`, §5 D-1) and, within a
|
|
9
|
+
// level, by arrival (the id ascending, §5 D-3) — and each job's own `<id>.json` carries its state.
|
|
10
|
+
// Every mutation of that pair happens inside `state.withMutex`, so two `land` processes can never
|
|
11
|
+
// interleave a read-modify-write of the queue.
|
|
12
|
+
//
|
|
13
|
+
// There is no daemon and no scheduler (D-2): the runner is whichever `land` process holds the lease
|
|
14
|
+
// in `lease.js`, and this module is the state machine it drives. No git, no spawn, no config here.
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const crypto = require('crypto');
|
|
19
|
+
|
|
20
|
+
const state = require('./state');
|
|
21
|
+
const lease = require('./lease');
|
|
22
|
+
const { landingError } = require('./git');
|
|
23
|
+
|
|
24
|
+
const JOB_STATES = Object.freeze(['queued', 'in_progress', 'landed', 'failed', 'blocked', 'canceled']);
|
|
25
|
+
const TERMINAL_STATES = Object.freeze(['landed', 'failed', 'blocked', 'canceled']);
|
|
26
|
+
const ACTIVE_STATES = Object.freeze(['queued', 'in_progress']);
|
|
27
|
+
|
|
28
|
+
// D-1: three named levels, ascending rank. There is no numeric form and no `low`.
|
|
29
|
+
const PRIORITIES = Object.freeze(['normal', 'high', 'urgent']);
|
|
30
|
+
// D-7: a non-empty reason of at most this many characters is required for high and urgent.
|
|
31
|
+
const PRIORITY_REASON_MAX = 200;
|
|
32
|
+
|
|
33
|
+
// Sortable id (no deps): 10 Crockford-base32 characters of the submit timestamp (50 bits ≈ 35k
|
|
34
|
+
// years of milliseconds) followed by 6 random characters. Lexicographic order of the id IS arrival
|
|
35
|
+
// order, and two processes submitting in the same millisecond still cannot collide.
|
|
36
|
+
const BASE32 = '0123456789abcdefghjkmnpqrstvwxyz';
|
|
37
|
+
const ID_TIME_CHARS = 10;
|
|
38
|
+
const ID_RANDOM_CHARS = 6;
|
|
39
|
+
// OQ-3: the ETA stays `null` until this many gate durations have been recorded.
|
|
40
|
+
const ETA_SAMPLES = 20;
|
|
41
|
+
// OQ-3: the ETA is position × the MEDIAN of the last ETA_SAMPLES gate seconds.
|
|
42
|
+
const ETA_WINDOW = 20;
|
|
43
|
+
|
|
44
|
+
function nowIso(ms) {
|
|
45
|
+
return new Date(ms === undefined ? Date.now() : ms).toISOString();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function newJobId(nowMs) {
|
|
49
|
+
let value = Math.floor(nowMs === undefined ? Date.now() : nowMs);
|
|
50
|
+
let stamp = '';
|
|
51
|
+
for (let index = 0; index < ID_TIME_CHARS; index += 1) {
|
|
52
|
+
stamp = BASE32[value % 32] + stamp;
|
|
53
|
+
value = Math.floor(value / 32);
|
|
54
|
+
}
|
|
55
|
+
let suffix = '';
|
|
56
|
+
for (const byte of crypto.randomBytes(ID_RANDOM_CHARS)) suffix += BASE32[byte % 32];
|
|
57
|
+
return `${stamp}${suffix}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// WP-1 D-1 (§6): the batch id, the train id of a real batch (a singleton keeps its job id). A ULID:
|
|
61
|
+
// 48 bits of timestamp then 80 bits of randomness, Crockford base32, so two batches built in the
|
|
62
|
+
// same millisecond still cannot collide and the id sorts by creation time.
|
|
63
|
+
function newBatchId(nowMs) {
|
|
64
|
+
let value = Math.floor(nowMs === undefined ? Date.now() : nowMs);
|
|
65
|
+
let stamp = '';
|
|
66
|
+
for (let index = 0; index < 10; index += 1) {
|
|
67
|
+
stamp = BASE32[value % 32] + stamp;
|
|
68
|
+
value = Math.floor(value / 32);
|
|
69
|
+
}
|
|
70
|
+
let suffix = '';
|
|
71
|
+
for (const byte of crypto.randomBytes(16)) suffix += BASE32[byte % 32];
|
|
72
|
+
return `b-${stamp}${suffix}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// --- paths (§6.3 layout, on top of state.js's SUBDIRS) ---------------------------------------
|
|
76
|
+
|
|
77
|
+
function jobsDir(root) { return path.join(root, 'jobs'); }
|
|
78
|
+
function jobFile(root, id) { return path.join(jobsDir(root), `${id}.json`); }
|
|
79
|
+
function queueFile(root) { return path.join(root, 'queue.json'); }
|
|
80
|
+
function metricsDir(root) { return path.join(root, 'metrics'); }
|
|
81
|
+
function verdictsDir(root) { return path.join(root, 'verdicts'); }
|
|
82
|
+
function logsDir(root) { return path.join(root, 'logs'); }
|
|
83
|
+
function trainsDir(root) { return path.join(root, 'trains'); }
|
|
84
|
+
|
|
85
|
+
// `landing/logs/` is not in state.js's SUBDIRS (WP-0 owns that list) and is created lazily here
|
|
86
|
+
// because a job only needs a gate log once a gate actually runs.
|
|
87
|
+
function ensureLogsDir(root) {
|
|
88
|
+
fs.mkdirSync(logsDir(root), { recursive: true });
|
|
89
|
+
return logsDir(root);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// --- queue.json ------------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
function readQueue(root) {
|
|
95
|
+
const raw = state.readJsonSafe(queueFile(root));
|
|
96
|
+
if (!raw) return { contract: state.CONTRACT, order: [] };
|
|
97
|
+
state.assertContract(raw, { file: queueFile(root) });
|
|
98
|
+
const order = Array.isArray(raw.order) ? raw.order.filter((id) => typeof id === 'string') : [];
|
|
99
|
+
return { contract: state.CONTRACT, order };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function writeQueue(root, order) {
|
|
103
|
+
return state.writeJsonAtomic(queueFile(root), { contract: state.CONTRACT, order });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// --- jobs ------------------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
function readJob(root, id) {
|
|
109
|
+
const raw = state.readJsonSafe(jobFile(root, id));
|
|
110
|
+
if (!raw) return null;
|
|
111
|
+
state.assertContract(raw, { file: jobFile(root, id) });
|
|
112
|
+
return raw;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function writeJob(root, job) {
|
|
116
|
+
return state.writeJsonAtomic(jobFile(root, job.id), job);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function listJobs(root) {
|
|
120
|
+
let names;
|
|
121
|
+
try {
|
|
122
|
+
names = fs.readdirSync(jobsDir(root));
|
|
123
|
+
} catch (error) {
|
|
124
|
+
/* expected: no job has ever been submitted in this repository, so landing/jobs/ does not
|
|
125
|
+
exist yet — an empty queue is the correct answer, never an error */
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
const jobs = [];
|
|
129
|
+
for (const name of names.filter((n) => n.endsWith('.json')).sort()) {
|
|
130
|
+
const job = readJob(root, name.slice(0, -'.json'.length));
|
|
131
|
+
if (job) jobs.push(job);
|
|
132
|
+
}
|
|
133
|
+
return jobs;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// One line per transition (§6.3). `gateSeconds` is what OQ-3's ETA reads; the doc's five fields
|
|
137
|
+
// are all present.
|
|
138
|
+
//
|
|
139
|
+
// The `contract` default is the state contract, but an EVENT row (`refused`, `batch`) overrides it
|
|
140
|
+
// with the metrics-schema version of §6/D-8 — a transition row and an event row are different
|
|
141
|
+
// schemas in the same append-only file, and a reader can tell them apart by `contract`/`event`
|
|
142
|
+
// without knowing which release wrote them.
|
|
143
|
+
function appendMetric(root, entry) {
|
|
144
|
+
fs.mkdirSync(metricsDir(root), { recursive: true });
|
|
145
|
+
const file = path.join(metricsDir(root), `${nowIso().slice(0, 10)}.jsonl`);
|
|
146
|
+
fs.appendFileSync(file, `${JSON.stringify({ contract: state.CONTRACT, ...entry })}\n`);
|
|
147
|
+
return file;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// §6/D-8: the metrics-schema version an `event` row carries. A transition row stays at
|
|
151
|
+
// `state.CONTRACT`; an event row is contract 2, which is what §7's metrics-shape invariant reads.
|
|
152
|
+
const EVENT_CONTRACT = 2;
|
|
153
|
+
|
|
154
|
+
function isTerminal(job) {
|
|
155
|
+
return Boolean(job) && TERMINAL_STATES.includes(job.state);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function isActive(job) {
|
|
159
|
+
return Boolean(job) && ACTIVE_STATES.includes(job.state);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// --- priority (§5 D-1, D-2, D-3, D-7) -----------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
// D-1: a job with no `priority`, or with a value outside the enum, is `normal` — every job written
|
|
165
|
+
// before this change, and any job written by an older binary, keeps working unchanged.
|
|
166
|
+
function priorityOf(job) {
|
|
167
|
+
const value = job && job.priority;
|
|
168
|
+
return PRIORITIES.includes(value) ? value : 'normal';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function priorityRank(job) {
|
|
172
|
+
return PRIORITIES.indexOf(priorityOf(job));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function jobFrom(jobsById, id) {
|
|
176
|
+
if (!jobsById) return null;
|
|
177
|
+
if (typeof jobsById.get === 'function') return jobsById.get(id) || null;
|
|
178
|
+
return jobsById[id] || null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// D-2: pure. `jobsById` maps id -> job; an id with no job file and an id whose job is terminal are
|
|
182
|
+
// both dropped (a terminal job is not in the queue). The result is the canonical order: the
|
|
183
|
+
// `in_progress` ids first in their existing relative order, then the rest by rank descending, then
|
|
184
|
+
// by id ascending (D-3 — the id is arrival-sortable, queue.js:28-30).
|
|
185
|
+
function sortOrder(ids, jobsById) {
|
|
186
|
+
const running = [];
|
|
187
|
+
const queued = [];
|
|
188
|
+
for (const id of ids) {
|
|
189
|
+
const job = jobFrom(jobsById, id);
|
|
190
|
+
if (!job) continue;
|
|
191
|
+
if (TERMINAL_STATES.includes(job.state)) continue;
|
|
192
|
+
if (job.state === 'in_progress') running.push(id);
|
|
193
|
+
else queued.push(id);
|
|
194
|
+
}
|
|
195
|
+
queued.sort((a, b) => {
|
|
196
|
+
const byRank = priorityRank(jobFrom(jobsById, b)) - priorityRank(jobFrom(jobsById, a));
|
|
197
|
+
if (byRank !== 0) return byRank;
|
|
198
|
+
if (a === b) return 0;
|
|
199
|
+
return a < b ? -1 : 1;
|
|
200
|
+
});
|
|
201
|
+
return [...running, ...queued];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// The canonical order of the ids currently in `queue.json`, read from the job files themselves.
|
|
205
|
+
function sortedQueueOrder(root, ids, overrides) {
|
|
206
|
+
const jobsById = {};
|
|
207
|
+
for (const id of ids) {
|
|
208
|
+
jobsById[id] = overrides && overrides[id] ? overrides[id] : readJob(root, id);
|
|
209
|
+
}
|
|
210
|
+
return sortOrder(ids, jobsById);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Idempotency (§6.2): the same branch at the same tip that is already queued or in_progress is not
|
|
214
|
+
// a second job.
|
|
215
|
+
function activeJobFor(root, branch, tip) {
|
|
216
|
+
return listJobs(root).find((job) => job.branch === branch && job.tip === tip && isActive(job)) || null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// D-11 / I-5: a second `land` process for the same CTM session is a refusal, but only while the
|
|
220
|
+
// first one is actually alive — a session that died leaves its job reapable, not a lockout.
|
|
221
|
+
//
|
|
222
|
+
// A submit no longer drains in the foreground, so the submit process is usually already gone by the
|
|
223
|
+
// time anyone asks. The process that actually represents the job is the RUNNER working it
|
|
224
|
+
// (`runnerPid`, recorded by `land submit`); either pid being alive is what makes the session busy.
|
|
225
|
+
//
|
|
226
|
+
// D-7: the refusal is per (session, BRANCH), not per session — the I-5 race it guards (two resumed
|
|
227
|
+
// sessions sharing one index) is per worktree+branch, so one session landing two DIFFERENT branches
|
|
228
|
+
// from two worktrees is legitimate; only a second active job for the SAME branch collides.
|
|
229
|
+
function duplicateSubmitter(root, session, pid, branch) {
|
|
230
|
+
if (!session) return null;
|
|
231
|
+
return listJobs(root).find((job) => {
|
|
232
|
+
if (!isActive(job)) return false;
|
|
233
|
+
if (!job.submittedBy || job.submittedBy.session !== session) return false;
|
|
234
|
+
if (job.branch !== branch) return false;
|
|
235
|
+
const owners = [job.submittedBy.pid, job.runnerPid]
|
|
236
|
+
.filter((value) => typeof value === 'number' && value !== pid);
|
|
237
|
+
return owners.some((value) => state.pidIsAlive(value));
|
|
238
|
+
}) || null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function enqueue(root, job) {
|
|
242
|
+
return state.withMutex(root, () => {
|
|
243
|
+
state.ensureDirs(root);
|
|
244
|
+
writeJob(root, job);
|
|
245
|
+
const queued = readQueue(root).order;
|
|
246
|
+
// D-2: the sorted place of a new job is the canonical one — an older binary's plain append and
|
|
247
|
+
// this sort agree, because the end of the `queued` run IS a normal job's correct place.
|
|
248
|
+
const order = sortedQueueOrder(root, [...queued, job.id], { [job.id]: job });
|
|
249
|
+
writeQueue(root, order);
|
|
250
|
+
appendMetric(root, {
|
|
251
|
+
jobId: job.id,
|
|
252
|
+
from: null,
|
|
253
|
+
to: job.state,
|
|
254
|
+
at: job.timestamps && job.timestamps.queued ? job.timestamps.queued : nowIso(),
|
|
255
|
+
seconds: null,
|
|
256
|
+
gateSeconds: null,
|
|
257
|
+
});
|
|
258
|
+
return { position: order.indexOf(job.id) + 1, queued: order.length };
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function positionOf(root, id) {
|
|
263
|
+
const index = readQueue(root).order.indexOf(id);
|
|
264
|
+
return index === -1 ? null : index + 1;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function updateJob(root, id, patch) {
|
|
268
|
+
return state.withMutex(root, () => {
|
|
269
|
+
const job = readJob(root, id);
|
|
270
|
+
if (!job) return null;
|
|
271
|
+
const next = { ...job, ...patch };
|
|
272
|
+
writeJob(root, next);
|
|
273
|
+
return next;
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Terminal transition: the job leaves the queue and one metrics line is appended. `metric` carries
|
|
278
|
+
// the per-transition extras (`gateSeconds`) that the ETA needs.
|
|
279
|
+
//
|
|
280
|
+
// The body is mutex-FREE (`finishJobLocked`) so a batch can write N of them under ONE hold —
|
|
281
|
+
// `state.withMutex` is not re-entrant, so `finishBatch` below must not call the public wrapper.
|
|
282
|
+
function finishJob(root, id, patch, metric = {}) {
|
|
283
|
+
return state.withMutex(root, () => finishJobLocked(root, id, patch, metric));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function finishJobLocked(root, id, patch, metric = {}) {
|
|
287
|
+
const job = readJob(root, id);
|
|
288
|
+
const from = job ? job.state : null;
|
|
289
|
+
const next = { ...(job || { id }), ...patch, id };
|
|
290
|
+
writeJob(root, next);
|
|
291
|
+
writeQueue(root, readQueue(root).order.filter((queuedId) => queuedId !== id));
|
|
292
|
+
const startedAt = next.timestamps && next.timestamps.started ? Date.parse(next.timestamps.started) : NaN;
|
|
293
|
+
appendMetric(root, {
|
|
294
|
+
jobId: id,
|
|
295
|
+
from,
|
|
296
|
+
to: next.state,
|
|
297
|
+
at: nowIso(),
|
|
298
|
+
seconds: Number.isFinite(startedAt) ? Number(((Date.now() - startedAt) / 1000).toFixed(3)) : null,
|
|
299
|
+
gateSeconds: typeof metric.gateSeconds === 'number' ? metric.gateSeconds : null,
|
|
300
|
+
// WP-0 (§6.6/D-8): the terminal row carries the same `gate` object the job file does, so a
|
|
301
|
+
// later WP can measure every terminal transition from the metrics stream alone.
|
|
302
|
+
gate: metric.gate || null,
|
|
303
|
+
});
|
|
304
|
+
return next;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// WP-1 D-1/§6 (AC-1a): ONE terminal write for a whole batch. Every member lands with the same
|
|
308
|
+
// `train`/`tree`/`base`/`batch`, and exactly ONE `{ event: 'batch' }` metrics row records the
|
|
309
|
+
// verdict — a batch is one gate, so it must also be one row for the metrics readers to count.
|
|
310
|
+
// `patch` is an object (spread onto every member) or `(member) => object`. `metric` is the batch's
|
|
311
|
+
// ONE telemetry row; it is written under this same hold, so "the members landed" and "the row that
|
|
312
|
+
// records the verdict is on disk" are one transition, exactly as `finishJobLocked` writes a job's
|
|
313
|
+
// state and its row together. (A reader that does not take the mutex can still, in principle, catch
|
|
314
|
+
// the instant between the last member's write and the row's; a reader that needs both waits for both.)
|
|
315
|
+
function finishBatch(root, batchId, members, patch = {}, { gateSeconds = null, gate = null, metric = null } = {}) {
|
|
316
|
+
return state.withMutex(root, () => {
|
|
317
|
+
const results = [];
|
|
318
|
+
for (const member of members) {
|
|
319
|
+
const next = typeof patch === 'function' ? patch(member) : patch;
|
|
320
|
+
results.push(finishJobLocked(root, member.id, next, { gateSeconds, gate }));
|
|
321
|
+
}
|
|
322
|
+
if (metric) recordBatchMetric(root, { batchId, ...metric });
|
|
323
|
+
return results;
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// WP-1 D-1 (§6): the batch telemetry row, written under the caller's hold (`finishBatch`) or on its
|
|
328
|
+
// own (the FAIL path, which changes no job state). It carries `event`, so every state-transition
|
|
329
|
+
// reader skips it (`metricSequence` filters on `to`; the ETA reader skips `event` rows explicitly).
|
|
330
|
+
function recordBatchMetric(root, entry) {
|
|
331
|
+
return appendMetric(root, { contract: EVENT_CONTRACT, event: 'batch', ...entry, at: nowIso() });
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// WP-1 D-2 (§6, AC-1b): the second half of a bisected batch. Every member goes back to `queued` —
|
|
335
|
+
// NOT failed: the gate said the batch is bad, not which member is — carrying `batch.attempt + 1` so
|
|
336
|
+
// the next bisection round is visible on the record. One metrics row per member, copying the
|
|
337
|
+
// `requeuePreempted` shape with `reason: 'bisected'`. The ids sort by arrival (D-3), so the half is
|
|
338
|
+
// ahead of everything submitted after it without any new ordering state.
|
|
339
|
+
// `reason` names the metric row: 'bisected' (a FAIL halving) or 'withdrawn' (a member's cancel voided the run, D-4).
|
|
340
|
+
function requeueHalf(root, members, { batchId = null, attempt = 1, batchMembers = null, reason = 'bisected' } = {}) {
|
|
341
|
+
return state.withMutex(root, () => {
|
|
342
|
+
const ids = members.map((member) => member.id);
|
|
343
|
+
const results = [];
|
|
344
|
+
for (const member of members) {
|
|
345
|
+
const job = readJob(root, member.id);
|
|
346
|
+
if (!job) continue;
|
|
347
|
+
const timestamps = { ...(job.timestamps || {}) };
|
|
348
|
+
delete timestamps.started;
|
|
349
|
+
const next = {
|
|
350
|
+
...job,
|
|
351
|
+
state: 'queued',
|
|
352
|
+
timestamps,
|
|
353
|
+
batch: {
|
|
354
|
+
id: batchId || (job.batch && job.batch.id) || job.id,
|
|
355
|
+
members: (batchMembers || ids).slice(),
|
|
356
|
+
attempt,
|
|
357
|
+
index: ids.indexOf(member.id),
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
writeJob(root, next);
|
|
361
|
+
appendMetric(root, {
|
|
362
|
+
jobId: member.id,
|
|
363
|
+
from: 'in_progress',
|
|
364
|
+
to: 'queued',
|
|
365
|
+
at: nowIso(),
|
|
366
|
+
seconds: null,
|
|
367
|
+
gateSeconds: null,
|
|
368
|
+
reason,
|
|
369
|
+
});
|
|
370
|
+
results.push(next);
|
|
371
|
+
}
|
|
372
|
+
const order = readQueue(root).order;
|
|
373
|
+
const head = order.filter((queuedId) => !ids.includes(queuedId));
|
|
374
|
+
writeQueue(root, [...head, ...ids.filter((id) => order.includes(id))]);
|
|
375
|
+
return results;
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// WP-0 D-8 (§6, §9 S-13, AC-0a): a submit REFUSAL is an event. `duplicate_submitter` and
|
|
380
|
+
// `conflict_at_submit` are thrown before any job record exists, so until now the queue could not see
|
|
381
|
+
// its own back-pressure at all (census §1). One row per refusal, in the day's metrics file, through
|
|
382
|
+
// the same writer as every other row.
|
|
383
|
+
function recordRefusal(root, { reason, session = null, branch = null } = {}) {
|
|
384
|
+
return appendMetric(root, {
|
|
385
|
+
contract: EVENT_CONTRACT,
|
|
386
|
+
event: 'refused',
|
|
387
|
+
reason: reason === undefined ? null : reason,
|
|
388
|
+
session: session === undefined ? null : session,
|
|
389
|
+
branch: branch === undefined ? null : branch,
|
|
390
|
+
// §6/D-8: the row is stamped like every other one, so a reader can order refusals against the
|
|
391
|
+
// transitions around them.
|
|
392
|
+
at: nowIso(),
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// AC-0a/§6: how many refusals were recorded today, for `land status`'s `refused today: N` line. Pure
|
|
397
|
+
// over the day's metrics file — a missing file is 0, never an error.
|
|
398
|
+
function refusalsToday(root, day = nowIso().slice(0, 10)) {
|
|
399
|
+
const name = `${day}.jsonl`;
|
|
400
|
+
let raw;
|
|
401
|
+
try {
|
|
402
|
+
raw = fs.readFileSync(path.join(metricsDir(root), name), 'utf8');
|
|
403
|
+
} catch (error) {
|
|
404
|
+
/* expected: nothing has been refused (or nothing has happened at all) today */
|
|
405
|
+
return 0;
|
|
406
|
+
}
|
|
407
|
+
let count = 0;
|
|
408
|
+
for (const line of raw.split('\n')) {
|
|
409
|
+
if (!line) continue;
|
|
410
|
+
let entry;
|
|
411
|
+
try {
|
|
412
|
+
entry = JSON.parse(line);
|
|
413
|
+
} catch (error) {
|
|
414
|
+
/* expected: a torn final line from a process killed mid-append — one unparseable row never
|
|
415
|
+
invalidates the rows around it */
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (entry && entry.event === 'refused') count += 1;
|
|
419
|
+
}
|
|
420
|
+
return count;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// --- priority mutation (§5 D-1, D-7; §6.1) -----------------------------------------------------
|
|
424
|
+
|
|
425
|
+
// D-7/D-4: one effective change = one `priorityHistory` entry + exactly one metrics line. The
|
|
426
|
+
// metrics line deliberately carries `priorityFrom`/`priorityTo` and NEVER `from`/`to`, so the
|
|
427
|
+
// state-transition readers (metricSequence, the ETA) skip it.
|
|
428
|
+
//
|
|
429
|
+
// Reason VALIDATION is the caller's job (a usage error, exit 2, whose message names the flag), so
|
|
430
|
+
// nothing here re-formats it. There is no ownership check, matching `land cancel`.
|
|
431
|
+
//
|
|
432
|
+
// §6.1/D-7: the preempt field is the D-4 sentence's outcome, and asking for `urgent` really does
|
|
433
|
+
// write the stop request of the running lower-ranked job (WP-3) — inside this same mutex hold, so no
|
|
434
|
+
// runner checkpoint can read the queue between the rank change and the request.
|
|
435
|
+
function setPriority(root, id, { priority, reason = null, by = null, via = null } = {}) {
|
|
436
|
+
return state.withMutex(root, () => {
|
|
437
|
+
const job = readJob(root, id);
|
|
438
|
+
if (!job) throw landingError('job_not_found', `no landing job ${JSON.stringify(id)} in ${root}`, { jobId: id, root });
|
|
439
|
+
if (TERMINAL_STATES.includes(job.state)) {
|
|
440
|
+
throw landingError('job_terminal', `job ${id} is already ${job.state}`, { jobId: id, state: job.state });
|
|
441
|
+
}
|
|
442
|
+
if (!PRIORITIES.includes(priority)) {
|
|
443
|
+
throw landingError('priority_invalid', `priority ${JSON.stringify(priority)} is not one of ${PRIORITIES.join(', ')}`, { jobId: id, priority });
|
|
444
|
+
}
|
|
445
|
+
const from = priorityOf(job);
|
|
446
|
+
const order = readQueue(root).order;
|
|
447
|
+
const at = nowIso();
|
|
448
|
+
// D-7: a no-op writes no history and no metric — asking for the level a job already has is not a
|
|
449
|
+
// change, and a log full of no-ops is not an audit trail.
|
|
450
|
+
if (from === priority) {
|
|
451
|
+
const index = order.indexOf(id);
|
|
452
|
+
return { job, changed: false, position: index === -1 ? null : index + 1, preempt: null };
|
|
453
|
+
}
|
|
454
|
+
const history = Array.isArray(job.priorityHistory) ? job.priorityHistory : [];
|
|
455
|
+
const next = { ...job, priority, priorityHistory: [...history, { from, to: priority, reason, at, by, via }] };
|
|
456
|
+
writeJob(root, next);
|
|
457
|
+
const jobsById = {};
|
|
458
|
+
for (const queuedId of order) jobsById[queuedId] = queuedId === id ? next : readJob(root, queuedId);
|
|
459
|
+
const sorted = sortOrder(order, jobsById);
|
|
460
|
+
writeQueue(root, sorted);
|
|
461
|
+
appendMetric(root, {
|
|
462
|
+
event: 'priority',
|
|
463
|
+
jobId: id,
|
|
464
|
+
priorityFrom: from,
|
|
465
|
+
priorityTo: priority,
|
|
466
|
+
reason,
|
|
467
|
+
by,
|
|
468
|
+
at,
|
|
469
|
+
});
|
|
470
|
+
const index = sorted.indexOf(id);
|
|
471
|
+
return {
|
|
472
|
+
job: next,
|
|
473
|
+
changed: true,
|
|
474
|
+
position: index === -1 ? null : index + 1,
|
|
475
|
+
preempt: priority === 'urgent' ? requestPreemptLocked(root, id, { by, reason }) : null,
|
|
476
|
+
};
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// §6.1/D-4: `null` unless the requested level is `urgent`; then the D-4 sentence, decided and — when
|
|
481
|
+
// it allows it — WRITTEN, all inside the caller's mutex hold. The full outcome set, in the order the
|
|
482
|
+
// checks run:
|
|
483
|
+
// 'not_running' no `in_progress` job other than the bumped one — nothing to stop
|
|
484
|
+
// 'not_lower' the running job is itself `urgent`: urgent never stops urgent
|
|
485
|
+
// 'already_preempted' the running job already carries `preemptions >= 1` (D-4: at most once)
|
|
486
|
+
// 'no_runner' no lease record, or a dead one — nothing is working that job any more
|
|
487
|
+
// 'runner_cannot_stop' a LIVE lease without the `stop` capability (D-10): a runner spawned from an
|
|
488
|
+
// older `land.js` would land the job anyway, so nothing is written
|
|
489
|
+
// 'already_requested' the job already carries a stop request (cancel or preempt): a preempt never
|
|
490
|
+
// replaces one, and never downgrades a cancel
|
|
491
|
+
// 'requested' the request was written
|
|
492
|
+
//
|
|
493
|
+
// Mutex-free on purpose: both callers run inside one `state.withMutex` hold (`setPriority`'s urgent
|
|
494
|
+
// bump and a fresh urgent `cmdSubmit`), and `state.withMutex` is not re-entrant. Use
|
|
495
|
+
// `requestPreemptFor` from anywhere else — one implementation, two callers, no duplicated decision.
|
|
496
|
+
function requestPreemptLocked(root, urgentJobId, { by = null, reason = null } = {}) {
|
|
497
|
+
const order = readQueue(root).order;
|
|
498
|
+
let running = null;
|
|
499
|
+
for (const queuedId of order) {
|
|
500
|
+
if (queuedId === urgentJobId) continue;
|
|
501
|
+
const job = readJob(root, queuedId);
|
|
502
|
+
if (job && job.state === 'in_progress') {
|
|
503
|
+
running = job;
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (!running) return 'not_running';
|
|
508
|
+
if (priorityOf(running) === 'urgent') return 'not_lower';
|
|
509
|
+
if ((Number.isFinite(running.preemptions) ? running.preemptions : 0) >= 1) return 'already_preempted';
|
|
510
|
+
const record = lease.read(root);
|
|
511
|
+
if (!record || lease.isDead(record)) return 'no_runner';
|
|
512
|
+
if (!lease.hasCap(record, 'stop')) return 'runner_cannot_stop';
|
|
513
|
+
if (running.stopRequest) return 'already_requested';
|
|
514
|
+
writeJob(root, {
|
|
515
|
+
...running,
|
|
516
|
+
stopRequest: { kind: 'preempt', by, reason: reason || null, forJob: urgentJobId, at: nowIso() },
|
|
517
|
+
});
|
|
518
|
+
return 'requested';
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// §6.1: the mutex-taking entry point (a fresh `--priority urgent` submit).
|
|
522
|
+
function requestPreemptFor(root, urgentJobId, { by = null, reason = null } = {}) {
|
|
523
|
+
return state.withMutex(root, () => requestPreemptLocked(root, urgentJobId, { by, reason }));
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// D-4: the runner's preempt transition, inside the mutex. The job keeps its id, so `sortOrder` puts
|
|
527
|
+
// it back at the front of its own level (D-3 — an id is arrival-sortable), and the ONE metrics line
|
|
528
|
+
// copies the `runner_reaped` shape with `reason: 'preempted'`.
|
|
529
|
+
function requeuePreempted(root, id) {
|
|
530
|
+
return state.withMutex(root, () => {
|
|
531
|
+
const job = readJob(root, id);
|
|
532
|
+
if (!job) return null;
|
|
533
|
+
const request = job.stopRequest || null;
|
|
534
|
+
const timestamps = { ...(job.timestamps || {}) };
|
|
535
|
+
delete timestamps.started;
|
|
536
|
+
const next = {
|
|
537
|
+
...job,
|
|
538
|
+
state: 'queued',
|
|
539
|
+
preemptions: (Number.isFinite(job.preemptions) ? job.preemptions : 0) + 1,
|
|
540
|
+
preemptedBy: { jobId: request ? request.forJob : null, at: nowIso() },
|
|
541
|
+
timestamps,
|
|
542
|
+
};
|
|
543
|
+
delete next.stopRequest;
|
|
544
|
+
writeJob(root, next);
|
|
545
|
+
const order = readQueue(root).order;
|
|
546
|
+
const ids = order.includes(id) ? order : [...order, id];
|
|
547
|
+
const jobsById = {};
|
|
548
|
+
for (const queuedId of ids) jobsById[queuedId] = queuedId === id ? next : readJob(root, queuedId);
|
|
549
|
+
writeQueue(root, sortOrder(ids, jobsById));
|
|
550
|
+
appendMetric(root, {
|
|
551
|
+
jobId: id,
|
|
552
|
+
from: 'in_progress',
|
|
553
|
+
to: 'queued',
|
|
554
|
+
at: nowIso(),
|
|
555
|
+
seconds: null,
|
|
556
|
+
gateSeconds: null,
|
|
557
|
+
reason: 'preempted',
|
|
558
|
+
});
|
|
559
|
+
return next;
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// D-6 (§9 S-11, WP-2): the runner's base_broken transition, inside the mutex. The job is NOT
|
|
564
|
+
// terminal — every file the gate named fails on the base commit too, so this is main's failure, not
|
|
565
|
+
// the branch owner's, and re-gating the same tip would just burn another gate on the same broken
|
|
566
|
+
// main. It goes back to `queued` in its own place (the id is arrival-sortable, so `sortOrder` puts
|
|
567
|
+
// it where it was, and its priority is untouched) carrying `retryAfterTargetMoves`, which is what
|
|
568
|
+
// `takeNextJob` holds it against until the target tip actually moves. The ONE metrics line copies
|
|
569
|
+
// the `requeuePreempted` shape with `reason: 'base_broken'`.
|
|
570
|
+
function requeueBaseBroken(root, id, patch = {}, metric = {}) {
|
|
571
|
+
return state.withMutex(root, () => {
|
|
572
|
+
const job = readJob(root, id);
|
|
573
|
+
if (!job) return null;
|
|
574
|
+
const next = { ...job, ...patch, id, state: 'queued', retryAfterTargetMoves: true };
|
|
575
|
+
writeJob(root, next);
|
|
576
|
+
const order = readQueue(root).order;
|
|
577
|
+
const ids = order.includes(id) ? order : [...order, id];
|
|
578
|
+
const jobsById = {};
|
|
579
|
+
for (const queuedId of ids) jobsById[queuedId] = queuedId === id ? next : readJob(root, queuedId);
|
|
580
|
+
writeQueue(root, sortOrder(ids, jobsById));
|
|
581
|
+
const startedAt = next.timestamps && next.timestamps.started ? Date.parse(next.timestamps.started) : NaN;
|
|
582
|
+
appendMetric(root, {
|
|
583
|
+
jobId: id,
|
|
584
|
+
from: 'in_progress',
|
|
585
|
+
to: 'queued',
|
|
586
|
+
at: nowIso(),
|
|
587
|
+
// `seconds` is the run that just ended, exactly as `finishJob` reports it, so the ETA reader
|
|
588
|
+
// sees one shape whether a job left the queue by failing or by being parked on a broken base.
|
|
589
|
+
seconds: typeof metric.seconds === 'number'
|
|
590
|
+
? metric.seconds
|
|
591
|
+
: (Number.isFinite(startedAt) ? Number(((Date.now() - startedAt) / 1000).toFixed(3)) : null),
|
|
592
|
+
gateSeconds: typeof metric.gateSeconds === 'number' ? metric.gateSeconds : null,
|
|
593
|
+
reason: 'base_broken',
|
|
594
|
+
});
|
|
595
|
+
return next;
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// D-6/D-7: a job whose base rerun found the failure on base as well is held until the target moves.
|
|
600
|
+
// `triage.base` is the tip the rerun was measured against, so while the target still IS that tip the
|
|
601
|
+
// job has nothing new to learn — it is not `takeNextJob`'s turn to run, it is main's turn to be
|
|
602
|
+
// fixed. An unknown tip (`targetTip` undefined/null, e.g. a config this process cannot read) holds
|
|
603
|
+
// nothing: the safe direction is always to run the job rather than to park it forever.
|
|
604
|
+
function heldByTargetMove(job, targetTip) {
|
|
605
|
+
if (!job || job.retryAfterTargetMoves !== true || !targetTip) return false;
|
|
606
|
+
return Boolean(job.triage) && job.triage.base === targetTip;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// WP-1 D-1/§9 S-8: READ-ONLY peek at the runnable order — the ids `takeNextBatch` would consider,
|
|
610
|
+
// in the order it would consider them. `drain` uses it to decide the window BEFORE anything is
|
|
611
|
+
// marked `in_progress`: whether the head job must land alone (a proven/cached tip), and whether a
|
|
612
|
+
// lone job should wait for company (`landing.batch.waitSeconds`). Writes nothing, takes no mutex.
|
|
613
|
+
function peekRunnable(root, { targetTip, exclude = null } = {}) {
|
|
614
|
+
const excluded = exclude instanceof Set ? exclude : new Set(Array.isArray(exclude) ? exclude : []);
|
|
615
|
+
const order = readQueue(root).order;
|
|
616
|
+
const jobsById = new Map();
|
|
617
|
+
for (const id of order) jobsById.set(id, readJob(root, id));
|
|
618
|
+
const candidates = order.filter((id) => {
|
|
619
|
+
const job = jobsById.get(id);
|
|
620
|
+
if (!job || TERMINAL_STATES.includes(job.state)) return false;
|
|
621
|
+
// An `in_progress` job belongs to a runner; if that runner is dead, `takeNextBatch` reaps it back
|
|
622
|
+
// to `queued` and it is eligible again — the peek must not silently drop it, but it must also not
|
|
623
|
+
// claim the runner would gate it in this same tick. Treat it as eligible: the reap happens under
|
|
624
|
+
// the mutex immediately before the take.
|
|
625
|
+
if (excluded.has(id)) return false;
|
|
626
|
+
return !heldByTargetMove(job, targetTip);
|
|
627
|
+
});
|
|
628
|
+
return sortOrder(candidates, jobsById);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// --- the runner's queue operations (D-2) ------------------------------------------------------
|
|
632
|
+
|
|
633
|
+
// D-5: the ONLY way another process asks the runner to stop a job — never by writing a terminal
|
|
634
|
+
// state itself (the runner is the single writer of in-progress transitions). Inside the mutex, so a
|
|
635
|
+
// request and the runner's own checkpoint read cannot interleave. Refuses `job_terminal`: a job that
|
|
636
|
+
// already has an outcome has nothing left to stop.
|
|
637
|
+
//
|
|
638
|
+
// A pending `kind:'cancel'` is never DOWNGRADED to `'preempt'`: cancel is the stronger request, and
|
|
639
|
+
// a preempt arriving behind it must not turn a "stop and never land" into a "stop and requeue".
|
|
640
|
+
function requestStop(root, id, { kind, by = null, reason = null, forJob = null } = {}) {
|
|
641
|
+
return state.withMutex(root, () => {
|
|
642
|
+
if (kind !== 'cancel' && kind !== 'preempt') {
|
|
643
|
+
throw landingError('stop_kind_invalid', `stop kind ${JSON.stringify(kind)} is not one of cancel, preempt`, { jobId: id, kind });
|
|
644
|
+
}
|
|
645
|
+
const job = readJob(root, id);
|
|
646
|
+
if (!job) throw landingError('job_not_found', `no landing job ${JSON.stringify(id)} in ${root}`, { jobId: id, root });
|
|
647
|
+
if (TERMINAL_STATES.includes(job.state)) {
|
|
648
|
+
throw landingError('job_terminal', `job ${id} is already ${job.state}`, { jobId: id, state: job.state });
|
|
649
|
+
}
|
|
650
|
+
const existing = job.stopRequest;
|
|
651
|
+
const request = {
|
|
652
|
+
kind,
|
|
653
|
+
by,
|
|
654
|
+
reason: reason || null,
|
|
655
|
+
forJob: forJob || null,
|
|
656
|
+
at: nowIso(),
|
|
657
|
+
};
|
|
658
|
+
const stopRequest = existing && existing.kind === 'cancel' && kind === 'preempt' ? existing : request;
|
|
659
|
+
const next = { ...job, stopRequest };
|
|
660
|
+
writeJob(root, next);
|
|
661
|
+
return next;
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Take the head job. Two D-2 duties ride along, both safe because the caller already holds the
|
|
666
|
+
// lease (so no other runner exists for this repository):
|
|
667
|
+
// * ids of terminal jobs are dropped from `queue.json`;
|
|
668
|
+
// * a job left in `in_progress` by a previous, dead runner goes back to the HEAD of the queue.
|
|
669
|
+
//
|
|
670
|
+
// D-2/§7: the remaining ids are re-sorted with `sortOrder` before `order[0]` is taken and the sorted
|
|
671
|
+
// order is what gets written, so the invariant is SELF-HEALING — a hand edit, an older binary that
|
|
672
|
+
// appended a normal job, or a crash cannot make this runner take a lower-ranked job first.
|
|
673
|
+
//
|
|
674
|
+
// `onEmpty` runs INSIDE the mutex when the queue is empty: the runner releases its lease there, so
|
|
675
|
+
// a submitter's enqueue (also under the mutex) either lands before the check and is taken, or lands
|
|
676
|
+
// after the release and finds no live runner and starts one. Releasing outside the mutex would leave
|
|
677
|
+
// a window where a job is enqueued behind a runner that has already decided to exit.
|
|
678
|
+
// WP-1 D-1 (§6, §9 S-1): a job is not RUNNABLE while the merge it will be gated on is already known
|
|
679
|
+
// to conflict — the submit precheck wrote that preview, and re-gating a conflict the queue cannot
|
|
680
|
+
// resolve burns a gate to reach a verdict `buildTrain` computes in milliseconds. A job with a CLEAR
|
|
681
|
+
// preview (`null`) and a job that has never conflicted are both runnable.
|
|
682
|
+
function hasConflictPaths(job) {
|
|
683
|
+
const preview = job && job.conflictPreview;
|
|
684
|
+
return Boolean(preview) && Array.isArray(preview.paths) && preview.paths.length > 0;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// WP-1 D-1 (§6, §9 S-1, AC-1a/AC-1b): take the batch. The HEAD of the runnable order is always
|
|
688
|
+
// taken — that is the job `takeNextJob` has always taken, and a job the submitter queued with
|
|
689
|
+
// `--queue-anyway` must still reach the runner's `blocked (conflict)` verdict rather than sit in the
|
|
690
|
+
// queue forever. The remaining slots are filled, in the same order, with the runnable jobs that have
|
|
691
|
+
// no conflict preview, up to `window`.
|
|
692
|
+
//
|
|
693
|
+
// The two D-2 duties of `takeNextJob` ride along unchanged (they are safe because the caller already
|
|
694
|
+
// holds the lease): terminal ids are dropped from `queue.json`, and a job left `in_progress` by a
|
|
695
|
+
// previous, dead runner goes back to the HEAD of the queue.
|
|
696
|
+
//
|
|
697
|
+
// `exclude` is the set of ids the caller has already decided not to include (D-7: a member whose stop
|
|
698
|
+
// request arrived is dropped from the NEXT train build).
|
|
699
|
+
function takeNextBatch(root, { onEmpty, targetTip, window: windowSize = 1, exclude = null, batchId = null, attempt = 1 } = {}) {
|
|
700
|
+
const limit = Number.isInteger(windowSize) && windowSize >= 1 ? windowSize : 1;
|
|
701
|
+
const excluded = exclude instanceof Set ? exclude : new Set(Array.isArray(exclude) ? exclude : []);
|
|
702
|
+
return state.withMutex(root, () => {
|
|
703
|
+
const raw = readQueue(root);
|
|
704
|
+
const jobsById = {};
|
|
705
|
+
const requeued = [];
|
|
706
|
+
const rest = [];
|
|
707
|
+
for (const id of raw.order) {
|
|
708
|
+
const job = readJob(root, id);
|
|
709
|
+
jobsById[id] = job;
|
|
710
|
+
if (!job) continue;
|
|
711
|
+
if (TERMINAL_STATES.includes(job.state)) continue;
|
|
712
|
+
if (job.state === 'in_progress') requeued.push(id);
|
|
713
|
+
else rest.push(id);
|
|
714
|
+
}
|
|
715
|
+
for (const id of requeued) {
|
|
716
|
+
const job = jobsById[id];
|
|
717
|
+
const timestamps = { ...(job.timestamps || {}) };
|
|
718
|
+
delete timestamps.started;
|
|
719
|
+
jobsById[id] = { ...job, state: 'queued', timestamps };
|
|
720
|
+
writeJob(root, jobsById[id]);
|
|
721
|
+
appendMetric(root, { jobId: id, from: 'in_progress', to: 'queued', at: nowIso(), seconds: null, gateSeconds: null, reason: 'runner_reaped' });
|
|
722
|
+
}
|
|
723
|
+
const order = [...requeued, ...sortOrder(rest, jobsById)];
|
|
724
|
+
// D-6/D-7 (WP-2): a job the runner parked as `base_broken` is not runnable while the target tip
|
|
725
|
+
// is still the tip its rerun measured — re-gating it would report main's own failure again as
|
|
726
|
+
// this branch's. It stays in `queue.json` (the whole non-terminal order is written either way),
|
|
727
|
+
// so it is still visible to `land status` and still holds its place; `drain` passes the tip it
|
|
728
|
+
// resolved this iteration. EVERY remaining job held is the same as an empty queue for the runner:
|
|
729
|
+
// there is nothing to do, so `onEmpty` releases the lease (a later `land submit` or the next
|
|
730
|
+
// target move starts it again).
|
|
731
|
+
const runnable = order.filter((queuedId) => !heldByTargetMove(jobsById[queuedId], targetTip) && !excluded.has(queuedId));
|
|
732
|
+
if (!runnable.length) {
|
|
733
|
+
writeQueue(root, order);
|
|
734
|
+
if (onEmpty) onEmpty();
|
|
735
|
+
return [];
|
|
736
|
+
}
|
|
737
|
+
const head = runnable[0];
|
|
738
|
+
const members = [head];
|
|
739
|
+
for (const id of runnable.slice(1)) {
|
|
740
|
+
if (members.length >= limit) break;
|
|
741
|
+
if (hasConflictPaths(jobsById[id])) continue;
|
|
742
|
+
members.push(id);
|
|
743
|
+
}
|
|
744
|
+
const id = batchId || (members.length > 1 ? newBatchId() : members[0]);
|
|
745
|
+
const taken = [];
|
|
746
|
+
for (let index = 0; index < members.length; index += 1) {
|
|
747
|
+
const memberId = members[index];
|
|
748
|
+
const job = readJob(root, memberId);
|
|
749
|
+
const from = job.state;
|
|
750
|
+
const next = { ...job, state: 'in_progress', timestamps: { ...(job.timestamps || {}), started: nowIso() } };
|
|
751
|
+
// Taking it is the whole point of the flag: the hold is spent, and the record must not claim a
|
|
752
|
+
// wait that is over (D-7). Written explicitly as `false`, never deleted, so a reader sees the
|
|
753
|
+
// transition rather than a missing key.
|
|
754
|
+
if (job.retryAfterTargetMoves === true) next.retryAfterTargetMoves = false;
|
|
755
|
+
// D-1 (§6 job record): the batch rides on every member — but ONLY on a real batch. A window of
|
|
756
|
+
// one is today's serial runner, and its job record (and therefore every line `land status`
|
|
757
|
+
// prints) must stay byte-identical to what it was.
|
|
758
|
+
if (members.length > 1) next.batch = { id, members: members.slice(), attempt, index };
|
|
759
|
+
writeJob(root, next);
|
|
760
|
+
appendMetric(root, { jobId: memberId, from, to: 'in_progress', at: nowIso(), seconds: null, gateSeconds: null });
|
|
761
|
+
taken.push(next);
|
|
762
|
+
}
|
|
763
|
+
writeQueue(root, order);
|
|
764
|
+
return taken;
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// The serial entry point: one job, exactly as before this change. Every existing caller (and every
|
|
769
|
+
// existing test) keeps its contract because a window of one never writes a `batch` field.
|
|
770
|
+
function takeNextJob(root, opts = {}) {
|
|
771
|
+
return takeNextBatch(root, { ...opts, window: 1 })[0] || null;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// D-2: drop the lease of a runner that is provably gone. The in_progress job it left behind is
|
|
775
|
+
// returned to the head by `takeNextJob`, which only ever runs while we hold the lease.
|
|
776
|
+
function reapDeadRunner(root) {
|
|
777
|
+
const record = lease.read(root);
|
|
778
|
+
if (!record) return null;
|
|
779
|
+
if (!lease.isDead(record)) return null;
|
|
780
|
+
lease.reapIfDead(root);
|
|
781
|
+
return record;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// --- ETA (OQ-3) -------------------------------------------------------------------------------
|
|
785
|
+
|
|
786
|
+
function median(values) {
|
|
787
|
+
if (!values.length) return null;
|
|
788
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
789
|
+
const middle = Math.floor(sorted.length / 2);
|
|
790
|
+
return sorted.length % 2 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// The last `limit` gate durations recorded for this repository, newest file first.
|
|
794
|
+
function recentGateSeconds(root, limit = ETA_WINDOW) {
|
|
795
|
+
let names;
|
|
796
|
+
try {
|
|
797
|
+
names = fs.readdirSync(metricsDir(root));
|
|
798
|
+
} catch (error) {
|
|
799
|
+
/* expected: no job has completed yet, so landing/metrics/ does not exist — "no samples",
|
|
800
|
+
which the ETA caller renders as `null` (never as a guess) */
|
|
801
|
+
return [];
|
|
802
|
+
}
|
|
803
|
+
const values = [];
|
|
804
|
+
for (const name of names.filter((n) => n.endsWith('.jsonl')).sort().reverse()) {
|
|
805
|
+
let raw;
|
|
806
|
+
try {
|
|
807
|
+
raw = fs.readFileSync(path.join(metricsDir(root), name), 'utf8');
|
|
808
|
+
} catch (error) {
|
|
809
|
+
/* expected: a metrics file rotated away between readdir and read — skip it; older files
|
|
810
|
+
still provide the sample */
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
for (const line of raw.split('\n').filter(Boolean).reverse()) {
|
|
814
|
+
let entry;
|
|
815
|
+
try {
|
|
816
|
+
entry = JSON.parse(line);
|
|
817
|
+
} catch (error) {
|
|
818
|
+
/* expected: a torn final line from a process killed mid-append — a metrics file is
|
|
819
|
+
append-only evidence, and one unparseable line never invalidates the samples around it */
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
822
|
+
// WP-0/WP-1: an `event` row (`refused`, `priority`, `batch`) is NOT a state transition. The
|
|
823
|
+
// ETA reads transitions only, so the guard is explicit rather than a side effect of the row
|
|
824
|
+
// happening to lack `gateSeconds`.
|
|
825
|
+
if (entry && entry.event) continue;
|
|
826
|
+
if (typeof entry.gateSeconds === 'number' && Number.isFinite(entry.gateSeconds)) {
|
|
827
|
+
values.push(entry.gateSeconds);
|
|
828
|
+
if (values.length >= limit) return values;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
return values;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// `eta_s` = position × median of the last 20 gate seconds, or null with fewer than 20 samples.
|
|
836
|
+
function etaSeconds(root, position) {
|
|
837
|
+
if (!Number.isFinite(position) || position <= 0) return null;
|
|
838
|
+
const samples = recentGateSeconds(root, ETA_SAMPLES);
|
|
839
|
+
if (samples.length < ETA_SAMPLES) return null;
|
|
840
|
+
return Math.round(position * median(samples));
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// --- read models ------------------------------------------------------------------------------
|
|
844
|
+
|
|
845
|
+
function jobSummary(job) {
|
|
846
|
+
return {
|
|
847
|
+
id: job.id,
|
|
848
|
+
branch: job.branch,
|
|
849
|
+
tip: job.tip,
|
|
850
|
+
title: job.title,
|
|
851
|
+
state: job.state,
|
|
852
|
+
reason: job.reason === undefined ? null : job.reason,
|
|
853
|
+
priority: priorityOf(job),
|
|
854
|
+
priorityReason: priorityReasonOf(job),
|
|
855
|
+
preemptions: Number.isFinite(job.preemptions) ? job.preemptions : 0,
|
|
856
|
+
stopRequest: job.stopRequest === undefined ? null : job.stopRequest,
|
|
857
|
+
base: job.base === undefined ? null : job.base,
|
|
858
|
+
train: job.train === undefined ? null : job.train,
|
|
859
|
+
tree: job.tree === undefined ? null : job.tree,
|
|
860
|
+
verdict: job.verdict === undefined ? null : job.verdict,
|
|
861
|
+
// D-5 (design landing-throughput-and-big-file-decomposition-2026-10-29.html §6 job record): the
|
|
862
|
+
// conflict the queue measured for this job against the target tip, written by `land submit`
|
|
863
|
+
// (`--queue-anyway`) and re-written by the runner's post-landing sweep. `null` for a job that has
|
|
864
|
+
// never conflicted, and an explicit `null` (never `undefined`) so a JSON reader sees the key.
|
|
865
|
+
conflictPreview: job.conflictPreview === undefined ? null : job.conflictPreview,
|
|
866
|
+
// D-6/D-7 (WP-2): the base-rerun receipt (`{ base, files, failedOnBase, seconds }`, or null when
|
|
867
|
+
// the pass did not run) and the hold it implies. `land status` reads both: the first is the
|
|
868
|
+
// evidence for why a queued job is not this branch's fault, the second is why it is not running.
|
|
869
|
+
triage: job.triage === undefined ? null : job.triage,
|
|
870
|
+
retryAfterTargetMoves: job.retryAfterTargetMoves === true,
|
|
871
|
+
submittedBy: job.submittedBy || null,
|
|
872
|
+
timestamps: job.timestamps || {},
|
|
873
|
+
gateLog: job.gateLog === undefined ? null : job.gateLog,
|
|
874
|
+
// WP-1 D-1 (§6 job record): the batch this job is being gated in — `{ id, members, attempt,
|
|
875
|
+
// index }`, or an explicit `null` for the serial runner (a window of one writes no batch, so
|
|
876
|
+
// every pre-change job and every singleton row reads exactly as it did).
|
|
877
|
+
batch: job.batch === undefined ? null : job.batch,
|
|
878
|
+
// WP-0 (§6.6/D-8): the gate's self-description rides on every listing/`--json` row, so
|
|
879
|
+
// `land status` can print `gate <mode> <executed>/<selected>` without a second read.
|
|
880
|
+
gate: job.gate === undefined ? null : job.gate,
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// The latest reason a human is meant to read: the last `priorityHistory` entry's reason, or null for
|
|
885
|
+
// a job that has never been bumped (a normal job's audit trail is empty by D-7).
|
|
886
|
+
function priorityReasonOf(job) {
|
|
887
|
+
const history = job && Array.isArray(job.priorityHistory) ? job.priorityHistory : [];
|
|
888
|
+
if (!history.length) return null;
|
|
889
|
+
const last = history[history.length - 1];
|
|
890
|
+
return last && last.reason !== undefined ? last.reason : null;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// D-8 revisit trigger: how many `normal` jobs (submitted within the last `days`) waited longer than
|
|
894
|
+
// `thresholdMinutes` in the queue while a higher-ranked job started inside that same window. Pure over
|
|
895
|
+
// the job files — no new metrics format, no clock beyond the timestamps already recorded.
|
|
896
|
+
function priorityStarvationCount(root, { days = 14, thresholdMinutes = 180 } = {}) {
|
|
897
|
+
const since = Date.now() - days * 24 * 60 * 60 * 1000;
|
|
898
|
+
const thresholdMs = thresholdMinutes * 60 * 1000;
|
|
899
|
+
const all = listJobs(root);
|
|
900
|
+
let count = 0;
|
|
901
|
+
for (const job of all) {
|
|
902
|
+
if (priorityOf(job) !== 'normal') continue;
|
|
903
|
+
const queuedAt = job.timestamps && job.timestamps.queued ? Date.parse(job.timestamps.queued) : NaN;
|
|
904
|
+
const startedAt = job.timestamps && job.timestamps.started ? Date.parse(job.timestamps.started) : NaN;
|
|
905
|
+
if (!Number.isFinite(queuedAt) || !Number.isFinite(startedAt)) continue;
|
|
906
|
+
if (queuedAt < since) continue;
|
|
907
|
+
if (startedAt - queuedAt <= thresholdMs) continue;
|
|
908
|
+
const overtaken = all.some((other) => {
|
|
909
|
+
if (other.id === job.id) return false;
|
|
910
|
+
if (priorityRank(other) <= priorityRank(job)) return false;
|
|
911
|
+
const otherStarted = other.timestamps && other.timestamps.started ? Date.parse(other.timestamps.started) : NaN;
|
|
912
|
+
return Number.isFinite(otherStarted) && otherStarted >= queuedAt && otherStarted <= startedAt;
|
|
913
|
+
});
|
|
914
|
+
if (overtaken) count += 1;
|
|
915
|
+
}
|
|
916
|
+
return count;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function runnerSummary(root) {
|
|
920
|
+
const record = lease.read(root);
|
|
921
|
+
if (!record) return null;
|
|
922
|
+
return {
|
|
923
|
+
pid: record.pid,
|
|
924
|
+
jobId: record.jobId === undefined ? null : record.jobId,
|
|
925
|
+
host: record.host === undefined ? null : record.host,
|
|
926
|
+
startedAt: record.startedAt,
|
|
927
|
+
heartbeatAt: record.heartbeatAt,
|
|
928
|
+
alive: !lease.isDead(record),
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function jobOrThrow(root, id) {
|
|
933
|
+
const job = readJob(root, id);
|
|
934
|
+
if (!job) throw landingError('job_not_found', `no landing job ${JSON.stringify(id)} in ${root}`, { jobId: id, root });
|
|
935
|
+
return job;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
module.exports = {
|
|
939
|
+
JOB_STATES,
|
|
940
|
+
TERMINAL_STATES,
|
|
941
|
+
ACTIVE_STATES,
|
|
942
|
+
PRIORITIES,
|
|
943
|
+
PRIORITY_REASON_MAX,
|
|
944
|
+
BASE32,
|
|
945
|
+
ETA_SAMPLES,
|
|
946
|
+
ETA_WINDOW,
|
|
947
|
+
nowIso,
|
|
948
|
+
newJobId,
|
|
949
|
+
newBatchId,
|
|
950
|
+
jobsDir,
|
|
951
|
+
jobFile,
|
|
952
|
+
queueFile,
|
|
953
|
+
metricsDir,
|
|
954
|
+
verdictsDir,
|
|
955
|
+
logsDir,
|
|
956
|
+
trainsDir,
|
|
957
|
+
ensureLogsDir,
|
|
958
|
+
readQueue,
|
|
959
|
+
writeQueue,
|
|
960
|
+
readJob,
|
|
961
|
+
writeJob,
|
|
962
|
+
listJobs,
|
|
963
|
+
appendMetric,
|
|
964
|
+
isTerminal,
|
|
965
|
+
isActive,
|
|
966
|
+
priorityOf,
|
|
967
|
+
priorityRank,
|
|
968
|
+
sortOrder,
|
|
969
|
+
activeJobFor,
|
|
970
|
+
duplicateSubmitter,
|
|
971
|
+
enqueue,
|
|
972
|
+
setPriority,
|
|
973
|
+
requestStop,
|
|
974
|
+
requestPreemptFor,
|
|
975
|
+
requeuePreempted,
|
|
976
|
+
requeueBaseBroken,
|
|
977
|
+
heldByTargetMove,
|
|
978
|
+
priorityStarvationCount,
|
|
979
|
+
positionOf,
|
|
980
|
+
updateJob,
|
|
981
|
+
finishJob,
|
|
982
|
+
finishBatch,
|
|
983
|
+
recordBatchMetric,
|
|
984
|
+
recordRefusal,
|
|
985
|
+
refusalsToday,
|
|
986
|
+
requeueHalf,
|
|
987
|
+
hasConflictPaths,
|
|
988
|
+
peekRunnable,
|
|
989
|
+
takeNextJob,
|
|
990
|
+
takeNextBatch,
|
|
991
|
+
reapDeadRunner,
|
|
992
|
+
median,
|
|
993
|
+
recentGateSeconds,
|
|
994
|
+
etaSeconds,
|
|
995
|
+
jobSummary,
|
|
996
|
+
runnerSummary,
|
|
997
|
+
jobOrThrow,
|
|
998
|
+
};
|