elanous 0.2.16 → 0.2.18
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/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/P6YTVgzuZz5oTZhY4Din_/_buildManifest.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1307-b46d45b705e8c9ae.js → 1307-30dff22c9e1f3073.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/195-fb1d7690bf30bab4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1986-33e13d648a9df765.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2537-6c289376caa88ca1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2699-325cc923ba7b8631.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3857-4f936cdb30322aa6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4554-5761612531d0146b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4565-3d7e3fdeb43686a8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5977-a11f37254196b659.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{6158.9c0449a9a8d0064b.js → 6158.af2e93b7adff709b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/6877-74399f69c7da1433.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7797-df5aa0381fdc078f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8401-8615a660c84cdb03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8695-846034ce44852c70.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-d92f7ee3994e84c7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-3cb52e5ea8fc2fa5.js → page-4733c967b56a4277.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-d22fd849cc22f104.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-c1e20d7d338713f1.js → page-bb742f995c6769e7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-482c5dbde699947c.js → page-02b073a2ac793b83.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-8a9b3594bc23289b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-a0eb02638b1e6f31.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/{page-a0e2a6d1bfb52fc5.js → page-308111abadf35670.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-c53598cfce6156f2.js → page-5d97b2901e0d1ea6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-13a63953f53c6a23.js → page-c39554c321576982.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-5d3c8429d04f3831.js → page-8c3d8e63ae2e1c98.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-3193bdc0bbab047a.js → page-9b72a07d424701c6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-6107ca8ddd5e3bdd.js → page-a572f58cd3ca5864.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-be5ea965a07fbc41.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-c130b63862437814.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-fa244bd146a00ae8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-44d56d6391965a6a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-4167348049166852.js → page-83a700d4c754d802.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-de52e8fdf9095f7a.js → not-found-7b3170f374f77b9d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-ca3952cd78a83ca2.js → page-a3c58f5c72a5f3f9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-e8e1ecaea7cd71d2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-1ebfe049c640e171.js → page-1467047104141297.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-2da6f9b8a38c6978.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-102310930791197a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-6ed0b73bae8351d8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-abe8e3b74bea1ffc.js → page-44d5f767491692bf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-43336055451f6e37.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/{page-d28573d6792b4275.js → page-3cc47dca0922ab9a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-a77ee355a2304ff0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-d751de5f8062ff5e.js → page-b17cd46cf0e9b8cf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/{page-fe1759f94003b32b.js → page-61553851aa606146.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/{page-d64c984e855c8c68.js → page-a1f585e154e61c59.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-7bf54c9bb9274af8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-9afdee66b820c76e.js → page-5150ef17b5383a57.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-3544f154c87b43ee.js → page-134da468538baf48.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{webpack-539cde99554be20f.js → webpack-c2782b330447ffb6.js} +1 -1
- package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +14 -14
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +14 -14
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +14 -14
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +14 -14
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +14 -14
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +14 -14
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +14 -14
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +14 -14
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +14 -14
- package/apps/pwa/out/decisions/index.html +1 -0
- package/apps/pwa/out/decisions/index.txt +30 -0
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +14 -14
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +14 -14
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +14 -14
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +14 -14
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +14 -14
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +14 -14
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +14 -14
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +14 -14
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +14 -14
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +14 -14
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +14 -14
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +14 -14
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +14 -14
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +14 -14
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +14 -14
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +14 -14
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +14 -14
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +15 -15
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +14 -14
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +14 -14
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +14 -14
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +14 -14
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +14 -14
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +14 -14
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +14 -14
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +14 -14
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +14 -14
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +14 -14
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +14 -14
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +14 -14
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +14 -14
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +14 -14
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +14 -14
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +14 -14
- package/graphs/intake/intake-daily.yaml +9 -2
- package/graphs/intake/news-sources.yaml +4 -0
- package/graphs/intake/recipes.yaml +6 -1
- package/package.json +2 -1
- package/samples/workflows/templates/approval-gated-deploy.yaml +30 -0
- package/samples/workflows/templates/chat-driven-research.yaml +35 -0
- package/samples/workflows/templates/discord-listen-and-respond.yaml +29 -0
- package/samples/workflows/templates/http-poll-then-alert.yaml +30 -0
- package/samples/workflows/templates/manual-quickstart.yaml +21 -0
- package/samples/workflows/templates/scheduled-daily-digest.yaml +30 -0
- package/samples/workflows/templates/webhook-deploy-hook.yaml +27 -0
- package/scripts/__fixtures__/intake-news.xml +7 -0
- package/scripts/ci-public-export-import-gate.ts +7 -2
- package/scripts/ci-typecheck-changed.ts +21 -6
- package/scripts/discord-seat-probe.ts +3 -2
- package/scripts/docs/generate-cli-anatomy.ts +162 -0
- package/scripts/goals/authoring-metrics.ts +73 -0
- package/scripts/intake-news.ts +169 -0
- package/scripts/landing-heal/must-fix-match.ts +7 -7
- package/scripts/landing-heal/run-mission.ts +3 -2
- package/scripts/public-export-test-run.ts +37 -2
- package/scripts/release-loop/checklist-gate-node.ts +13 -11
- package/scripts/release-loop/cut-branch.ts +2 -1
- package/scripts/release-loop/cutoff.ts +4 -2
- package/scripts/release-loop/docs-land-node.ts +77 -11
- package/scripts/release-loop/docs-node.ts +1 -1
- package/scripts/release-loop/gate-node.ts +99 -14
- package/scripts/release-loop/node-verdict.ts +2 -1
- package/scripts/release-loop/npm-publish-node.ts +11 -7
- package/scripts/release-loop/ops-upgrade-node.ts +8 -1
- package/scripts/release-loop/preflight.ts +246 -0
- package/scripts/release-loop/publish-node.ts +10 -3
- package/scripts/release-loop/release-docs.ts +134 -0
- package/scripts/release-loop/release-readiness.ts +11 -6
- package/scripts/release-loop/release-version.ts +48 -0
- package/scripts/release-loop/resume-release.ts +115 -0
- package/scripts/release-loop/unattended-release.ts +43 -7
- package/scripts/release-loop/version-node.ts +149 -41
- package/scripts/release-story/draft.ts +42 -14
- package/scripts/rhythm/daily.ts +25 -4
- package/scripts/skills/public-check.ts +62 -12
- package/scripts/test-deterministic.ts +51 -33
- package/scripts/test-diet/effectiveness.ts +62 -1
- package/scripts/test-diet/lib.ts +1 -1
- package/scripts/test-diet/node.ts +8 -1
- package/src/agent-mission/driver.ts +53 -10
- package/src/agent-mission/review-loop.ts +4 -3
- package/src/agent-substrate/pr-comment-meta.ts +20 -0
- package/src/autopilot/build/llm-conflict-merge.ts +28 -8
- package/src/autopilot/track-agent.ts +5 -3
- package/src/briefing/brief-items.ts +86 -17
- package/src/cli/autopilot-cli.ts +86 -1
- package/src/cli/brief-cli.ts +36 -2
- package/src/cli/decisions-cli.ts +4 -1
- package/src/cli/freeze-cli.ts +3 -2
- package/src/cli/intake-cli.ts +74 -2
- package/src/cli/log-event-names.ts +1 -0
- package/src/cli/notify-cli.ts +75 -0
- package/src/cli/pr-cli.ts +12 -1
- package/src/cli/provider-cli.ts +24 -0
- package/src/cli/release-cli.ts +101 -17
- package/src/cli/self-update-canary.ts +46 -0
- package/src/cli/self-update.ts +200 -1
- package/src/cli/tasks-cli.ts +309 -3
- package/src/context-bus/condense.ts +19 -3
- package/src/context-bus/context-day.ts +77 -24
- package/src/context-bus/context-first.ts +33 -0
- package/src/context-card/index.ts +45 -0
- package/src/context-card/task.ts +76 -0
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +2 -0
- package/src/decisions/decision-ledger.ts +10 -1
- package/src/discord-self-message.ts +20 -2
- package/src/discord-slash-wire.ts +1 -0
- package/src/domains/outbound-alert.ts +200 -27
- package/src/domains/outbound-log-probe.ts +16 -1
- package/src/domains/release-tool.ts +2 -2
- package/src/execution-loop/gates.ts +11 -2
- package/src/flow-loop/split.ts +8 -4
- package/src/git-fs/worktree.ts +23 -3
- package/src/harness/harness-cli-command.ts +151 -11
- package/src/harness/harness-pod-dispatch.ts +11 -0
- package/src/harness/harness-queue.ts +466 -32
- package/src/harness/helper-scan.ts +10 -3
- package/src/harness/nested-elanous-policy.ts +23 -4
- package/src/hq/fence-wrapper.ts +123 -0
- package/src/hq/hq.ts +8 -0
- package/src/hq/seats.ts +369 -0
- package/src/index.ts +190 -23
- package/src/intake-plane/adopt-gate.ts +49 -0
- package/src/intake-plane/check.ts +3 -0
- package/src/intake-plane/collect-github.ts +57 -25
- package/src/intake-plane/digest.ts +39 -4
- package/src/intake-plane/discord-seat-work.ts +8 -2
- package/src/intake-plane/requirement-funnel.ts +316 -0
- package/src/knowledge/know-find.ts +10 -1
- package/src/loops/budget.ts +36 -0
- package/src/loops/orchestrator/finish-rate.ts +73 -1
- package/src/loops/orchestrator/grid-place.ts +11 -0
- package/src/loops/orchestrator/tick.ts +295 -20
- package/src/maturity/feature-maturity.ts +3 -2
- package/src/nexus/api/decisions-route.ts +11 -1
- package/src/nexus/api/grid.ts +61 -0
- package/src/nexus/api/http-server.ts +19 -1
- package/src/nexus/api/loop-edges.ts +69 -13
- package/src/nexus/api/ops-seats-sources.ts +2 -2
- package/src/nexus/api/task-cards-api.ts +19 -0
- package/src/nexus/api/workflow-templates.ts +3 -0
- package/src/oauth/codex.ts +120 -2
- package/src/onboarding/first-chat-gate.ts +29 -0
- package/src/release-loop/checklist.ts +104 -6
- package/src/release-loop/feature-store.ts +10 -9
- package/src/release-loop/landing-freeze.ts +24 -14
- package/src/release-loop/placement.ts +13 -3
- package/src/release-loop/rubric.ts +80 -0
- package/src/seat-dispatch/seat-ask.ts +79 -16
- package/src/seat-loop/seat-loop.ts +107 -37
- package/src/self-dev/ask-launch-flow.ts +50 -10
- package/src/self-dev/ask-path-candidates.ts +68 -0
- package/src/self-dev/dev-pipeline.ts +4 -0
- package/src/self-dev/draft-triage-rules.ts +1 -1
- package/src/self-dev/orchestrate-cli.ts +22 -0
- package/src/self-dev/round-pr-supersede.ts +244 -0
- package/src/self-dev/run-store.ts +5 -1
- package/src/self-dev/run-supervisor.ts +21 -1
- package/src/self-dev/supervisor-verdict-edges.ts +46 -0
- package/src/self-implement/follow-up-goals.ts +2 -2
- package/src/self-implement/frozen-merges.ts +25 -20
- package/src/self-implement/gate-remote.ts +427 -0
- package/src/self-implement/goal-author-clarification.ts +81 -0
- package/src/self-implement/goal-author-cli.ts +33 -5
- package/src/self-implement/goal-author.ts +93 -12
- package/src/self-implement/goal-clarification-escalation.ts +37 -1
- package/src/self-implement/goal-document-write.ts +28 -0
- package/src/self-implement/heal-intake.ts +4 -0
- package/src/self-implement/host-regate.ts +3 -1
- package/src/self-implement/l8-merge-queue.ts +29 -5
- package/src/self-implement/orchestrator.ts +78 -13
- package/src/self-implement/prior-draft-findings.ts +4 -3
- package/src/skills/skill-feedback.ts +76 -0
- package/src/task-agent/actions.ts +151 -0
- package/src/task-agent/judge.ts +122 -0
- package/src/task-agent/mission.ts +423 -0
- package/src/task-agent/shadow.ts +90 -0
- package/src/task-agent/task-hand.ts +235 -0
- package/src/task-cards/card-store.ts +4 -1
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +43 -4
- package/src/task-orchestrator/surfaces/pod-lease.ts +5 -2
- package/src/task-orchestrator/surfaces/pod-pool.ts +81 -13
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +225 -35
- package/src/telegram-commands.ts +1 -1
- package/src/telegram-loops-command.ts +22 -15
- package/src/telegram.ts +5 -20
- package/src/user-config.ts +86 -4
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4614-21403aac730d4586.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6877-7c6208705c284124.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8827-e2f5b09a8d37e83b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8949-ecb4128a0858b911.js +0 -1
- package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9247-29d889b809288e65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-1e962b31d1f11081.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-8c2d9526164f5d09.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-76270efaee638f17.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-dce686e582c23937.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-d3d53b146416e422.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-1a61c7ca1ceb3ae7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-51f811fa119308b6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-dacff46049e1557e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-36a7d994f4ca0299.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-5b042736aa113756.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-acb749f1e432bcf3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-57924393e3e4e539.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-31fa1ce4c4aff9ae.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-0f5cd57ca928f42b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-59f03fda76c03d22.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d4aef67abf264874.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-1ea64acadd704ace.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-30c2073bfc21c004.js +0 -1
- package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +0 -1
- package/apps/pwa/out/_next/static/rv9cierV0jSOvDBm1g0XB/_buildManifest.js +0 -1
- /package/apps/pwa/out/_next/static/{rv9cierV0jSOvDBm1g0XB → P6YTVgzuZz5oTZhY4Din_}/_ssgManifest.js +0 -0
|
@@ -6,26 +6,34 @@ import { Database } from 'bun:sqlite';
|
|
|
6
6
|
import { debug } from '../debug/log.js';
|
|
7
7
|
import { effectiveInstanceRoot, prodInstanceRoot, releaseLedgerRoot } from '../instance/resolve.js';
|
|
8
8
|
import { listChecklist, type ChecklistItem } from '../release-loop/checklist.js';
|
|
9
|
+
import { readLandingFreeze } from '../release-loop/landing-freeze.js';
|
|
9
10
|
import { listSchedules } from '../release-loop/release-schedule.js';
|
|
11
|
+
import { parseRubric, readRubricItems, rubricScore } from '../release-loop/rubric.js';
|
|
10
12
|
import { withFileLockSync } from '../storage/file-lock.js';
|
|
11
13
|
import { trafficTick, TRAFFIC_SEATS, type TrafficCell } from '../loops/orchestrator/traffic.js';
|
|
12
|
-
import { decideSpawn, seatCapDetails, seatCapReason } from '../loops/budget.js';
|
|
14
|
+
import { decideSpawn, SEAT_CAP_STALE_RUN_MINUTES, SEAT_CAP_UNKNOWN_SEAT_CAP, seatCapDetails, seatCapExclusion, seatCapReason } from '../loops/budget.js';
|
|
13
15
|
import { listSelfDevRuns, processBirthId, selfDevRunsDir } from '../self-dev/run-store.js';
|
|
14
16
|
import { getUserConfig, userConfigPath, type OrchestratorLoopConfig } from '../user-config.js';
|
|
15
17
|
import { hostLeaseCounts, leaseHasPendingPod } from '../pod-lease/host-lease.js';
|
|
16
18
|
import { leaseKubectl, measurePoolLease } from '../task-orchestrator/surfaces/pod-lease.js';
|
|
17
19
|
import { parsePodPool, podPoolHostLease, resolvePodPoolSpec } from '../task-orchestrator/surfaces/pod-pool.js';
|
|
18
20
|
import { writeHarnessQueueReceipt } from './harness-queue-child.js';
|
|
19
|
-
import { finishAdvice, measureFinish, type FinishMetrics } from '../loops/orchestrator/finish-rate.js';
|
|
21
|
+
import { finishAdvice, measureFinish, recordFinishHistory, type FinishMetrics } from '../loops/orchestrator/finish-rate.js';
|
|
20
22
|
|
|
21
23
|
export type QueueSeat = 'OP' | 'TC' | 'MK' | 'UX';
|
|
22
24
|
export type QueueItem = {
|
|
23
25
|
id: string; seat: QueueSeat; kind: 'say' | 'ask'; input: string; hold: boolean; heavy: boolean;
|
|
24
26
|
at: string; status: 'queued' | 'launching' | 'launched' | 'finished'; pid?: number; launchId?: string; idempotencyKey?: string; waitingReason?: string;
|
|
27
|
+
launchArgs?: string[]; poolHint?: string; /** 힌트를 넣은 쪽 — 'grid' 면 GRID 가 소유(그것만 GRID 가 바꾸거나 지운다). */ poolHintSource?: 'grid'; launchCwd?: string; cellId?: string;
|
|
28
|
+
/** TASK-QUEUE: larger launches first within a seat; a row without it ranks below every prioritized row (FIFO among equals). */
|
|
29
|
+
priority?: number;
|
|
25
30
|
};
|
|
26
31
|
export type QueuePool = { running: number; pending: number; reserved: number; limit: number };
|
|
27
32
|
export type QueueTick = { outcome: 'launched' | 'waiting' | 'skipped'; item?: QueueItem; reason: string };
|
|
28
|
-
|
|
33
|
+
/** progressAt = newest run-ledger updatedAt among the tree's members; stopReason = the ledger's supervisor stop (SEAT-CAP-STALE).
|
|
34
|
+
* ledgerRunIds = run ids of the members' ledger records — the `elanous.run` label of their Pod Jobs (SEAT-CAP-STALE2). */
|
|
35
|
+
export type QueueProcess = { pid: number; seat?: QueueSeat; launchId?: string; runId?: string; rootPid?: number;
|
|
36
|
+
progressAt?: number; stopReason?: string; ledgerRunIds?: string[] };
|
|
29
37
|
type ProcessProbe = {
|
|
30
38
|
run?: typeof spawnSync; platform?: NodeJS.Platform;
|
|
31
39
|
cwd?: (pid: number) => string;
|
|
@@ -53,6 +61,10 @@ export interface HarnessQueueDeps {
|
|
|
53
61
|
cap?: (seat: QueueSeat) => number;
|
|
54
62
|
pool?: () => QueuePool;
|
|
55
63
|
launch?: (item: QueueItem, args: string[], root: string) => Promise<number>;
|
|
64
|
+
/** Test seam: runs after a direct CLI call enqueued its row and before that call's own tick (race tests). */
|
|
65
|
+
afterEnqueue?: (item: QueueItem) => void | Promise<void>;
|
|
66
|
+
/** Script the default launcher runs under the queue child wrapper (default bin/elanous.mjs). */
|
|
67
|
+
launchCommand?: string;
|
|
56
68
|
alive?: (pid: number) => boolean;
|
|
57
69
|
processes?: () => readonly QueueProcess[];
|
|
58
70
|
receipt?: (root: string, launchId: string) => 'started' | 'finished' | 'not-started' | null;
|
|
@@ -63,8 +75,13 @@ export interface HarnessQueueDeps {
|
|
|
63
75
|
next: readonly Pick<ChecklistItem, 'id' | 'title' | 'owner' | 'status' | 'evidence'>[] };
|
|
64
76
|
idleLog?: (category: string, event: string, data: Record<string, unknown>) => void;
|
|
65
77
|
log?: (event: 'enqueued' | 'launched' | 'waiting' | 'skipped', data: Record<string, unknown>) => void;
|
|
78
|
+
/** TASK-QUEUE seam: default priority for a cell at enqueue (default: rubric score of that cell in an unreleased schedule). */
|
|
79
|
+
cellPriority?: (cellId: string, root: string, now: Date) => number | undefined;
|
|
66
80
|
/** FINISH-RATE metrics source; a test process without it skips the real measurement. */
|
|
67
81
|
finishMetrics?: (now: Date) => FinishMetrics | Promise<FinishMetrics>;
|
|
82
|
+
/** SEAT-CAP-STALE2: run ids with an unfinished Pod Job (running or pending); throws when unreadable. A test process without
|
|
83
|
+
* it sees no Pod work (never a real kubectl). */
|
|
84
|
+
podRuns?: () => ReadonlySet<string>;
|
|
68
85
|
}
|
|
69
86
|
|
|
70
87
|
const FINISH_CACHE_MS = 10 * 60 * 1_000;
|
|
@@ -100,6 +117,11 @@ async function cachedFinishMetrics(root: string, now: Date, deps: HarnessQueueDe
|
|
|
100
117
|
mkdirSync(join(root, 'harness'), { recursive: true });
|
|
101
118
|
writeFileSync(path, JSON.stringify({ at: now.toISOString(), metrics }));
|
|
102
119
|
} catch (error) { debug.log('loop.orchestrator', 'finish-cache-write-failed', { reason: String(error) }); }
|
|
120
|
+
// 새로 잰 뒤에만 시간별 이력 한 줄(캐시 적중 틱은 안 쓴다) — 실패해도 발사 판정은 그대로다.
|
|
121
|
+
try { recordFinishHistory(root, metrics, now); }
|
|
122
|
+
catch (error) {
|
|
123
|
+
try { debug.log('loop.orchestrator', 'finish-history-write-failed', { reason: String(error) }); } catch { /* observation is fail-soft */ }
|
|
124
|
+
}
|
|
103
125
|
return metrics;
|
|
104
126
|
}
|
|
105
127
|
|
|
@@ -143,6 +165,12 @@ function read(path: string): QueueItem[] {
|
|
|
143
165
|
&& (row.pid === undefined || Number.isSafeInteger(row.pid))
|
|
144
166
|
&& (row.launchId === undefined || typeof row.launchId === 'string')
|
|
145
167
|
&& (row.idempotencyKey === undefined || typeof row.idempotencyKey === 'string')
|
|
168
|
+
&& (row.launchArgs === undefined || Array.isArray(row.launchArgs) && row.launchArgs.every((arg: unknown) => typeof arg === 'string'))
|
|
169
|
+
&& (row.poolHint === undefined || typeof row.poolHint === 'string')
|
|
170
|
+
&& (row.poolHintSource === undefined || row.poolHintSource === 'grid')
|
|
171
|
+
&& (row.launchCwd === undefined || typeof row.launchCwd === 'string' && row.launchCwd.startsWith('/'))
|
|
172
|
+
&& (row.cellId === undefined || typeof row.cellId === 'string')
|
|
173
|
+
&& (row.priority === undefined || typeof row.priority === 'number' && Number.isFinite(row.priority))
|
|
146
174
|
&& (row.waitingReason === undefined || typeof row.waitingReason === 'string'))) {
|
|
147
175
|
throw new Error('harness queue: invalid queue file (no launch)');
|
|
148
176
|
}
|
|
@@ -207,7 +235,67 @@ function seat(value: string): QueueSeat {
|
|
|
207
235
|
return value as QueueSeat;
|
|
208
236
|
}
|
|
209
237
|
|
|
210
|
-
|
|
238
|
+
/** Cell id named in a goal text (`칸: ONEDOOR-2` · `칸 ONEDOOR-2`) — ids carry at least one hyphen. */
|
|
239
|
+
export function queueCellId(text: string): string | undefined {
|
|
240
|
+
return /칸[:\s]\s*([A-Z][A-Z0-9]*(?:-[A-Z0-9]+)+)/u.exec(text)?.[1];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** A say row names its cell in the sentence; an ask row names it inside the goal document (unreadable → none). */
|
|
244
|
+
function queueRowCellId(kind: QueueItem['kind'], input: string): string | undefined {
|
|
245
|
+
if (kind === 'say') return queueCellId(input);
|
|
246
|
+
try { return queueCellId(readFileSync(input, 'utf8')); } catch { return undefined; }
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** A CEO directive mark on the cell (crown U+1F451 · 대표 지시) lifts it above every rubric score (max rubric ≈ 25.5). */
|
|
250
|
+
export const QUEUE_CEO_PRIORITY_BONUS = 100;
|
|
251
|
+
const CEO_MARK = /\u{1F451}|대표\s*지시/u;
|
|
252
|
+
|
|
253
|
+
/** Rubric score of a cell, searched in unreleased schedules by cut order; no ledger, no cell, or no rubric → undefined. */
|
|
254
|
+
export function queueCellRubricPriority(cellId: string, root: string, now: Date): number | undefined {
|
|
255
|
+
const ledger = root === effectiveInstanceRoot() ? releaseLedgerRoot() : root;
|
|
256
|
+
if (!existsSync(join(ledger, 'release', 'features.sqlite'))) return undefined;
|
|
257
|
+
const versions = listSchedules(ledger).filter((row) => Date.parse(row.cutAt) > now.getTime())
|
|
258
|
+
.sort((a, b) => Date.parse(a.cutAt) - Date.parse(b.cutAt)).map((row) => row.version);
|
|
259
|
+
for (const version of versions) {
|
|
260
|
+
const cell = readRubricItems(version, ledger).find((item) => item.id === cellId);
|
|
261
|
+
if (!cell) continue;
|
|
262
|
+
const text = `${cell.title}\n${cell.evidence ?? ''}`;
|
|
263
|
+
const rubric = parseRubric(text);
|
|
264
|
+
const ceo = CEO_MARK.test(text);
|
|
265
|
+
if (!rubric && !ceo) return undefined;
|
|
266
|
+
return (rubric ? rubricScore(rubric) : 0) + (ceo ? QUEUE_CEO_PRIORITY_BONUS : 0);
|
|
267
|
+
}
|
|
268
|
+
return undefined;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** A failed rubric lookup leaves the row without priority (FIFO) — it never refuses the enqueue. */
|
|
272
|
+
function defaultQueuePriority(cellId: string | undefined, root: string, deps: HarnessQueueDeps): number | undefined {
|
|
273
|
+
if (cellId === undefined) return undefined;
|
|
274
|
+
try {
|
|
275
|
+
const value = (deps.cellPriority ?? queueCellRubricPriority)(cellId, root, (deps.now ?? (() => new Date()))());
|
|
276
|
+
return value !== undefined && Number.isFinite(value) ? value : undefined;
|
|
277
|
+
} catch (error) {
|
|
278
|
+
try { debug.log('harness.queue', 'priority-lookup-failed', { cellId, reason: String(error) }); }
|
|
279
|
+
catch { /* Observation cannot affect enqueue. */ }
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Seat head order: priority descending (none = lowest), then arrival ascending, then file order. */
|
|
285
|
+
function queueHeadBefore(a: QueueItem, b: QueueItem): boolean {
|
|
286
|
+
const pa = a.priority ?? -Infinity, pb = b.priority ?? -Infinity;
|
|
287
|
+
if (pa !== pb) return pa > pb;
|
|
288
|
+
const ta = Date.parse(a.at), tb = Date.parse(b.at);
|
|
289
|
+
return Number.isFinite(ta) && Number.isFinite(tb) && ta < tb;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export class HarnessQueueDuplicateError extends Error {
|
|
293
|
+
constructor(existing: QueueItem, key: string) {
|
|
294
|
+
super(`같은 ${key} 이 이미 대기열에 있거나 도는 중이다 — ${existing.id} (${existing.status})`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export async function addHarnessQueue(input: { seat: string; say?: string; ask?: string; hold?: boolean; heavy?: boolean; idempotencyKey?: string; launchArgs?: string[]; poolHint?: string; poolHintSource?: 'grid'; launchCwd?: string; refuseDuplicate?: boolean }, deps: HarnessQueueDeps = {}): Promise<QueueItem> {
|
|
211
299
|
const assigned = seat(input.seat);
|
|
212
300
|
if ((input.say === undefined) === (input.ask === undefined)) throw new Error('harness queue add: --say 또는 --ask 중 하나만 필요');
|
|
213
301
|
const kind = input.ask === undefined ? 'say' : 'ask';
|
|
@@ -228,11 +316,30 @@ export async function addHarnessQueue(input: { seat: string; say?: string; ask?:
|
|
|
228
316
|
return existing;
|
|
229
317
|
}
|
|
230
318
|
}
|
|
319
|
+
// Recorded at enqueue: a later edit or deletion of an ask document cannot reopen its cell.
|
|
320
|
+
const cellId = queueRowCellId(kind, value);
|
|
321
|
+
if (input.refuseDuplicate) {
|
|
322
|
+
const cell = cellId;
|
|
323
|
+
const existing = rows.find((row) => row.status !== 'finished'
|
|
324
|
+
&& !['retryable', 'succeeded'].includes(queueRowOutcome(row, root))
|
|
325
|
+
&& ((row.kind === kind && row.input === value) || (cell !== undefined && (row.cellId ?? queueRowCellId(row.kind, row.input)) === cell)));
|
|
326
|
+
if (existing) {
|
|
327
|
+
const key = existing.kind === kind && existing.input === value ? '골' : `칸 ${cell}`;
|
|
328
|
+
observe('skipped', { id: existing.id, seat: assigned, reason: 'duplicate', key }, deps);
|
|
329
|
+
throw new HarnessQueueDuplicateError(existing, key);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const priority = defaultQueuePriority(cellId, root, deps);
|
|
231
333
|
const item: QueueItem = { id: `hq-${randomUUID()}`, seat: assigned, kind, input: value,
|
|
232
334
|
hold: input.hold === true, heavy: input.heavy === true, at: new Date().toISOString(), status: 'queued',
|
|
335
|
+
...(input.launchArgs === undefined ? {} : { launchArgs: input.launchArgs }),
|
|
336
|
+
...(input.poolHint === undefined ? {} : { poolHint: input.poolHint, ...(input.poolHintSource ? { poolHintSource: input.poolHintSource } : {}) }),
|
|
337
|
+
...(input.launchCwd === undefined ? {} : { launchCwd: input.launchCwd }),
|
|
338
|
+
...(cellId === undefined ? {} : { cellId }),
|
|
339
|
+
...(priority === undefined ? {} : { priority }),
|
|
233
340
|
...(input.idempotencyKey === undefined ? {} : { idempotencyKey: input.idempotencyKey }) };
|
|
234
341
|
save(path, [...rows, item]);
|
|
235
|
-
observe('enqueued', { id: item.id, seat: item.seat, kind }, deps);
|
|
342
|
+
observe('enqueued', { id: item.id, seat: item.seat, kind, ...(priority === undefined ? {} : { priority }) }, deps);
|
|
236
343
|
return item;
|
|
237
344
|
});
|
|
238
345
|
}
|
|
@@ -295,6 +402,46 @@ function queueRowOutcome(row: QueueItem, root: string): 'pending' | 'succeeded'
|
|
|
295
402
|
return state === 'started' ? 'pending' : 'unknown';
|
|
296
403
|
}
|
|
297
404
|
|
|
405
|
+
/** 아직 발사 전(queued)인 항목의 풀 힌트만 바꾼다 — 발사 중·발사 뒤 항목은 손대지 않는다. 바꿨으면 true. */
|
|
406
|
+
export async function setQueuedPoolHint(id: string, poolHint: string | undefined, deps: HarnessQueueDeps = {}, stillValid?: (item: QueueItem) => boolean): Promise<boolean> {
|
|
407
|
+
const root = deps.root ?? effectiveInstanceRoot();
|
|
408
|
+
return locked(root, async (path) => {
|
|
409
|
+
const rows = read(path);
|
|
410
|
+
const item = rows.find((row) => row.id === id);
|
|
411
|
+
if (!item || item.status !== 'queued' || item.poolHint === poolHint) return false;
|
|
412
|
+
// GRID 가 소유하지 않은 힌트(사람·다른 경로가 넣은 것)는 바꾸지도 지우지도 않는다. 힌트가 없던 항목에 새로 넣는 것은 허용.
|
|
413
|
+
if (item.poolHint !== undefined && item.poolHintSource !== 'grid') return false;
|
|
414
|
+
// 바꾸기 직전, 같은 잠금 안에서 호출자의 전제(예: 의뢰가 아직 queued)를 다시 확인한다.
|
|
415
|
+
if (stillValid && !stillValid(item)) return false;
|
|
416
|
+
if (poolHint === undefined) { delete item.poolHint; delete item.poolHintSource; } else { item.poolHint = poolHint; item.poolHintSource = 'grid'; }
|
|
417
|
+
save(path, rows);
|
|
418
|
+
return true;
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** TASK-QUEUE: re-rank a waiting row; launched or launching rows are refused (their slot is already taken).
|
|
423
|
+
* `by` is required — the caller names who asked; the audit never invents an actor. */
|
|
424
|
+
export async function setHarnessQueuePriority(id: string, priority: number, by: string, deps: HarnessQueueDeps = {}): Promise<QueueItem> {
|
|
425
|
+
if (!by.trim()) throw new Error('harness queue prio: 요청 주체(by)가 비었다');
|
|
426
|
+
if (!Number.isFinite(priority)) throw new Error(`harness queue prio: 우선순위는 유한한 수여야 한다 — ${priority}`);
|
|
427
|
+
const root = deps.root ?? effectiveInstanceRoot();
|
|
428
|
+
return locked(root, async (path) => {
|
|
429
|
+
const rows = read(path);
|
|
430
|
+
const item = rows.find((row) => row.id === id);
|
|
431
|
+
if (!item) throw new Error(`harness queue prio: 항목 없음 — ${id}`);
|
|
432
|
+
if (item.status !== 'queued') throw new Error(`harness queue prio: 대기(queued) 행만 바꾼다 — ${id} (${item.status})`);
|
|
433
|
+
const updated: QueueItem = { ...item, priority };
|
|
434
|
+
save(path, rows.map((row) => row.id === id ? updated : row));
|
|
435
|
+
// The audit is part of the change: a failed audit write restores the previous file and fails the call.
|
|
436
|
+
try { debug.log('harness.queue', 'prio', { id, from: item.priority ?? null, to: priority, by }); }
|
|
437
|
+
catch (error) {
|
|
438
|
+
save(path, rows);
|
|
439
|
+
throw new Error(`harness queue prio: 감사 기록 실패로 되돌림 — ${String(error)}`);
|
|
440
|
+
}
|
|
441
|
+
return updated;
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
298
445
|
export async function removeHarnessQueue(id: string, deps: HarnessQueueDeps = {}): Promise<boolean> {
|
|
299
446
|
const root = deps.root ?? effectiveInstanceRoot();
|
|
300
447
|
return locked(root, async (path) => {
|
|
@@ -317,18 +464,28 @@ export async function removeHarnessQueue(id: string, deps: HarnessQueueDeps = {}
|
|
|
317
464
|
});
|
|
318
465
|
}
|
|
319
466
|
|
|
320
|
-
export function queueLaunchArgs(item: QueueItem): string[] {
|
|
321
|
-
|
|
467
|
+
export function queueLaunchArgs(item: Pick<QueueItem, 'kind' | 'input' | 'hold' | 'heavy' | 'launchArgs' | 'poolHint'>): string[] {
|
|
468
|
+
const args = item.launchArgs ?? ['harness', item.kind, item.input, '--substrate', 'pod',
|
|
322
469
|
...(item.hold ? ['--no-auto-merge'] : []), ...(item.heavy ? ['--pod-memory', 'high'] : [])];
|
|
470
|
+
if (!item.poolHint || args[0] !== 'harness' || !['say', 'ask'].includes(args[1] ?? '')) return args;
|
|
471
|
+
const substrateIndex = args.indexOf('--substrate');
|
|
472
|
+
if (substrateIndex < 0 || args[substrateIndex + 1] !== 'pod') return args;
|
|
473
|
+
const poolIndex = args.indexOf('--pod-pool');
|
|
474
|
+
if (poolIndex >= 0) return args;
|
|
475
|
+
return [...args, '--pod-pool', item.poolHint];
|
|
323
476
|
}
|
|
324
477
|
|
|
325
|
-
|
|
478
|
+
function harnessQueuePoolMembers(): ReturnType<typeof parsePodPool> {
|
|
326
479
|
const config = getUserConfig();
|
|
327
480
|
const spec = resolvePodPoolSpec(undefined, process.env, () => config.harness?.podPool ?? config.pod?.pool);
|
|
328
481
|
const current = spec ? null : leaseKubectl(['config', 'current-context']);
|
|
329
482
|
const context = spec ?? (current?.status === 0 ? current.stdout.trim() : '');
|
|
330
483
|
if (!context) throw new Error('pod lease status: no pool/context');
|
|
331
|
-
|
|
484
|
+
return parsePodPool(context);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export function readHarnessQueuePool(): QueuePool {
|
|
488
|
+
const members = harnessQueuePoolMembers();
|
|
332
489
|
const observed = measurePoolLease(members);
|
|
333
490
|
if (observed.members.some((member) => member.running === null || member.pending === null)) throw new Error('pod lease status: incomplete running/pending measurement');
|
|
334
491
|
const pendingJobs = observed.members.flatMap((member) => member.pendingJobs ?? []);
|
|
@@ -341,6 +498,41 @@ export function readHarnessQueuePool(): QueuePool {
|
|
|
341
498
|
};
|
|
342
499
|
}
|
|
343
500
|
|
|
501
|
+
/** Same sanitizing as `k8sLabelValue` (self-implement-pod.ts), which writes the `elanous.run` label value. */
|
|
502
|
+
export function queueRunLabelValue(runId: string): string {
|
|
503
|
+
return runId.replace(/[^A-Za-z0-9._-]+/g, '-').slice(0, 63).replace(/^[^A-Za-z0-9]+|[^A-Za-z0-9]+$/g, '');
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** SEAT-CAP-STALE2: `elanous.run` / `elanous.child-run` labels of Jobs without a Complete/Failed condition (running or pending). */
|
|
507
|
+
export function queuePodRunsFromJobs(stdout: string): Set<string> {
|
|
508
|
+
const parsed = JSON.parse(stdout) as { items?: unknown };
|
|
509
|
+
if (!Array.isArray(parsed.items)) throw new Error('pod jobs: invalid response');
|
|
510
|
+
const runs = new Set<string>();
|
|
511
|
+
for (const job of parsed.items as { metadata?: { labels?: Record<string, unknown> };
|
|
512
|
+
status?: { conditions?: { type?: unknown; status?: unknown }[] } }[]) {
|
|
513
|
+
const finished = (job.status?.conditions ?? []).some((c) => (c.type === 'Complete' || c.type === 'Failed') && c.status === 'True');
|
|
514
|
+
if (finished) continue;
|
|
515
|
+
for (const key of ['elanous.run', 'elanous.child-run']) {
|
|
516
|
+
const value = job.metadata?.labels?.[key];
|
|
517
|
+
if (typeof value === 'string' && value) runs.add(value);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return runs;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** One read-only Job list per pool member; any unreadable member makes the whole answer unavailable (throws). */
|
|
524
|
+
export function readHarnessQueuePodRuns(kubectl: typeof leaseKubectl = leaseKubectl,
|
|
525
|
+
members: readonly { context: string }[] = harnessQueuePoolMembers()): ReadonlySet<string> {
|
|
526
|
+
const runs = new Set<string>();
|
|
527
|
+
for (const member of members) {
|
|
528
|
+
const result = kubectl(['--context', member.context, '--request-timeout=10s', '-n', 'elanous-test', 'get', 'jobs',
|
|
529
|
+
'-l', 'elanous.substrate=pod', '-o', 'json']);
|
|
530
|
+
if (result.status !== 0) throw new Error(`pod jobs ${member.context}: ${result.stderr.trim().split('\n').pop() || `rc=${result.status}`}`);
|
|
531
|
+
for (const run of queuePodRunsFromJobs(result.stdout)) runs.add(run);
|
|
532
|
+
}
|
|
533
|
+
return runs;
|
|
534
|
+
}
|
|
535
|
+
|
|
344
536
|
function alive(pid: number): boolean {
|
|
345
537
|
try { process.kill(pid, 0); return true; } catch { return false; }
|
|
346
538
|
}
|
|
@@ -378,14 +570,25 @@ export function readHarnessQueueProcesses(probe: ProcessProbe = {}): readonly Qu
|
|
|
378
570
|
const ambiguousPids = new Set<number>();
|
|
379
571
|
const candidatePids = new Set(candidates.map(({ pid }) => pid));
|
|
380
572
|
const births = new Map<number, string | undefined>();
|
|
573
|
+
const ledgerProgress = new Map<number, { at: number; stopReason?: string }>();
|
|
574
|
+
const ledgerRunIds = new Map<number, Set<string>>();
|
|
381
575
|
for (const record of listSelfDevRuns(probe.runsDir ?? selfDevRunsDir())) {
|
|
382
576
|
const pid = record.pid;
|
|
383
|
-
if (!pid || !candidatePids.has(pid) || !record.pidStart
|
|
577
|
+
if (!pid || !candidatePids.has(pid) || !record.pidStart) continue;
|
|
384
578
|
if (!births.has(pid)) {
|
|
385
579
|
try { births.set(pid, (probe.birthId ?? ((id: number) => processBirthId(id, platform)))(pid)); }
|
|
386
580
|
catch { births.set(pid, undefined); }
|
|
387
581
|
}
|
|
388
|
-
if (births.get(pid) !== record.pidStart
|
|
582
|
+
if (births.get(pid) !== record.pidStart) continue;
|
|
583
|
+
if (record.runId) ledgerRunIds.set(pid, (ledgerRunIds.get(pid) ?? new Set<string>()).add(record.runId));
|
|
584
|
+
// Newest updatedAt is the process's latest progress; it is stopped only when every matched record stopped.
|
|
585
|
+
if (Number.isFinite(record.updatedAt)) {
|
|
586
|
+
const prior = ledgerProgress.get(pid);
|
|
587
|
+
const stopped = (prior ? prior.stopReason !== undefined : true) && Boolean(record.supervisorStopReason);
|
|
588
|
+
const stopReason = stopped ? (prior?.stopReason ?? record.supervisorStopReason) : undefined;
|
|
589
|
+
ledgerProgress.set(pid, { at: Math.max(prior?.at ?? -Infinity, record.updatedAt), ...(stopReason ? { stopReason } : {}) });
|
|
590
|
+
}
|
|
591
|
+
if (!queueSeatNames.some((name) => name === record.seat) || ambiguousPids.has(pid)) continue;
|
|
389
592
|
const prior = ledgerSeats.get(pid);
|
|
390
593
|
if (prior && prior !== record.seat) { ledgerSeats.delete(pid); ambiguousPids.add(pid); }
|
|
391
594
|
else ledgerSeats.set(pid, record.seat!);
|
|
@@ -404,6 +607,8 @@ export function readHarnessQueueProcesses(probe: ProcessProbe = {}): readonly Qu
|
|
|
404
607
|
let launchId: string | undefined;
|
|
405
608
|
let runId: string | undefined;
|
|
406
609
|
for (const { process: { pid, command }, depth } of members) {
|
|
610
|
+
// The queue wrapper's argv carries its receipt path, so its launch id is its identity on every platform.
|
|
611
|
+
launchId ??= /\/harness\/(hq-[0-9a-f-]{36})\.receipt\.json(?:\s|$)/.exec(command)?.[1];
|
|
407
612
|
let envSeat: string | undefined;
|
|
408
613
|
if (platform === 'linux') {
|
|
409
614
|
try {
|
|
@@ -445,10 +650,16 @@ export function readHarnessQueueProcesses(probe: ProcessProbe = {}): readonly Qu
|
|
|
445
650
|
const top = eligible.filter((entry) => entry.depth === depth);
|
|
446
651
|
const seats = new Set(top.map((entry) => entry.seat));
|
|
447
652
|
const assigned = seats.size === 1 ? top[0] : undefined;
|
|
653
|
+
const progress = members.flatMap(({ process: { pid } }) => ledgerProgress.has(pid) ? [ledgerProgress.get(pid)!] : []);
|
|
654
|
+
const progressAt = progress.length ? Math.max(...progress.map((entry) => entry.at)) : undefined;
|
|
655
|
+
// Stopped only when every ledger-bearing member's supervisor stopped; one live supervisor keeps the run in progress.
|
|
656
|
+
const stopReason = progress.length && progress.every((entry) => entry.stopReason) ? progress[0]!.stopReason : undefined;
|
|
657
|
+
const runIds = [...new Set(members.flatMap(({ process: { pid } }) => [...(ledgerRunIds.get(pid) ?? [])]))];
|
|
448
658
|
try { debug.log('harness.queue', 'attributed', { pid: root.pid, seat: assigned?.seat ?? null, source: assigned?.source ?? 'all' }); }
|
|
449
659
|
catch { /* Observation does not block inventory. */ }
|
|
450
660
|
rows.push({ pid: root.pid, ...(assigned ? { seat: assigned.seat } : {}), ...(launchId ? { launchId } : {}),
|
|
451
|
-
...(runId ? { runId } : {}) })
|
|
661
|
+
...(runId ? { runId } : {}), ...(progressAt !== undefined ? { progressAt } : {}), ...(stopReason ? { stopReason } : {}),
|
|
662
|
+
...(runIds.length ? { ledgerRunIds: runIds } : {}) });
|
|
452
663
|
}
|
|
453
664
|
return rows;
|
|
454
665
|
}
|
|
@@ -468,15 +679,36 @@ function receipt(root: string, launchId: string): 'started' | 'finished' | 'not-
|
|
|
468
679
|
}
|
|
469
680
|
}
|
|
470
681
|
|
|
471
|
-
|
|
682
|
+
/** Launch time embedded in a UUIDv7 launch id (48-bit ms prefix); undefined for older v4 ids. */
|
|
683
|
+
export function queueLaunchTime(launchId: string): number | undefined {
|
|
684
|
+
const match = /^hq-([0-9a-f]{8})-([0-9a-f]{4})-7[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$/.exec(launchId);
|
|
685
|
+
if (!match) return undefined;
|
|
686
|
+
const at = parseInt(`${match[1]}${match[2]}`, 16);
|
|
687
|
+
return Number.isSafeInteger(at) && at > 0 ? at : undefined;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/** When the wrapper wrote its receipt (≈ spawn time); undefined when absent or unreadable. */
|
|
691
|
+
function receiptTime(root: string, launchId: string): number | undefined {
|
|
692
|
+
try {
|
|
693
|
+
const record = JSON.parse(readFileSync(harnessQueueReceiptPath(root, launchId), 'utf8')) as { at?: unknown };
|
|
694
|
+
const at = typeof record.at === 'string' ? Date.parse(record.at) : NaN;
|
|
695
|
+
return Number.isFinite(at) ? at : undefined;
|
|
696
|
+
} catch { return undefined; }
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function defaultLaunch(command?: string) {
|
|
700
|
+
return (item: QueueItem, args: string[], root: string) => launch(item, args, root, command);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
async function launch(item: QueueItem, args: string[], root: string, command?: string): Promise<number> {
|
|
472
704
|
const repo = resolve(import.meta.dir, '../..');
|
|
473
705
|
const logPath = join(root, 'harness', `${item.id}.log`);
|
|
474
706
|
const fd = openSync(logPath, 'a', 0o600);
|
|
475
707
|
try {
|
|
476
708
|
const child = spawn(process.execPath, [join(repo, 'src', 'harness', 'harness-queue-child.ts'),
|
|
477
|
-
harnessQueueReceiptPath(root, item.launchId!), join(repo, 'bin', 'elanous.mjs'),
|
|
709
|
+
harnessQueueReceiptPath(root, item.launchId!), command ?? join(repo, 'bin', 'elanous.mjs'),
|
|
478
710
|
...(resolve(root) === prodInstanceRoot() ? [] : [`--test=${root}`]), ...args], {
|
|
479
|
-
cwd: repo, detached: true, stdio: ['ignore', fd, fd],
|
|
711
|
+
cwd: item.launchCwd ?? repo, detached: true, stdio: ['ignore', fd, fd],
|
|
480
712
|
env: { ...process.env, ELANOUS_STATE_DIR: root, ELANOUS_HARNESS_SEAT: item.seat,
|
|
481
713
|
ELANOUS_HARNESS_QUEUE_LAUNCH: item.launchId! },
|
|
482
714
|
});
|
|
@@ -536,18 +768,23 @@ function warnLegacySeatCap(path: string): void {
|
|
|
536
768
|
} catch { /* An unreadable config cannot affect dispatch or observation. */ }
|
|
537
769
|
}
|
|
538
770
|
|
|
539
|
-
|
|
540
|
-
|
|
771
|
+
type QueueRun = { seat?: QueueSeat; launchIds: Set<string>; pids: Set<number>; runIds: Set<string>; progressAt?: number; stopReason?: string;
|
|
772
|
+
/** Every run id of the group (env and ledger) — matched against Pod Job labels only, never used for grouping. */
|
|
773
|
+
podRunIds: Set<string> };
|
|
774
|
+
|
|
775
|
+
function groupQueueRuns(processes: readonly QueueProcess[]): QueueRun[] {
|
|
776
|
+
const groups: { seats: Set<QueueSeat>; unknown: boolean; launchIds: Set<string>; keys: Set<string>; rows: QueueProcess[] }[] = [];
|
|
541
777
|
for (const row of processes) {
|
|
542
778
|
const keys = [row.runId && `run:${row.runId}`, `root:${row.rootPid ?? row.pid}`,
|
|
543
779
|
row.launchId && `launch:${row.launchId}`, `pid:${row.pid}`].filter((key): key is string => Boolean(key));
|
|
544
780
|
const matching = groups.filter((group) => keys.some((key) => group.keys.has(key)));
|
|
545
781
|
const group = matching[0] ?? { seats: new Set<QueueSeat>(), unknown: false,
|
|
546
|
-
launchIds: new Set<string>(), keys: new Set<string>() };
|
|
782
|
+
launchIds: new Set<string>(), keys: new Set<string>(), rows: [] };
|
|
547
783
|
for (const other of matching.slice(1)) {
|
|
548
784
|
for (const key of other.keys) group.keys.add(key);
|
|
549
785
|
for (const seat of other.seats) group.seats.add(seat);
|
|
550
786
|
for (const id of other.launchIds) group.launchIds.add(id);
|
|
787
|
+
group.rows.push(...other.rows);
|
|
551
788
|
group.unknown ||= other.unknown;
|
|
552
789
|
groups.splice(groups.indexOf(other), 1);
|
|
553
790
|
}
|
|
@@ -555,12 +792,40 @@ function groupQueueRuns(processes: readonly QueueProcess[]): { seat?: QueueSeat;
|
|
|
555
792
|
if (row.seat) group.seats.add(row.seat);
|
|
556
793
|
else group.unknown = true;
|
|
557
794
|
if (row.launchId) group.launchIds.add(row.launchId);
|
|
795
|
+
group.rows.push(row);
|
|
558
796
|
if (!matching.length) groups.push(group);
|
|
559
797
|
}
|
|
560
|
-
return groups.map((group) =>
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
798
|
+
return groups.map((group) => {
|
|
799
|
+
const progress = group.rows.filter((row) => row.progressAt !== undefined);
|
|
800
|
+
const progressAt = progress.length ? Math.max(...progress.map((row) => row.progressAt!)) : undefined;
|
|
801
|
+
const stopReason = progress.length && progress.every((row) => row.stopReason) ? progress[0]!.stopReason : undefined;
|
|
802
|
+
return {
|
|
803
|
+
...(group.unknown || group.seats.size !== 1 ? {} : { seat: [...group.seats][0] }),
|
|
804
|
+
launchIds: group.launchIds,
|
|
805
|
+
pids: new Set(group.rows.flatMap((row) => [row.pid, ...(row.rootPid ? [row.rootPid] : [])])),
|
|
806
|
+
runIds: new Set(group.rows.flatMap((row) => row.runId ? [row.runId] : [])),
|
|
807
|
+
podRunIds: new Set(group.rows.flatMap((row) => [...(row.runId ? [row.runId] : []), ...(row.ledgerRunIds ?? [])])),
|
|
808
|
+
...(progressAt !== undefined ? { progressAt } : {}), ...(stopReason ? { stopReason } : {}),
|
|
809
|
+
};
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/** SEAT-CAP-STALE limits: config harness.queue > env > default. */
|
|
814
|
+
export function harnessQueueCountingLimits(queue: { staleRunMinutes?: number; unknownSeatCap?: number } | undefined,
|
|
815
|
+
env: NodeJS.ProcessEnv = process.env): { staleRunMinutes: number; unknownSeatCap: number } {
|
|
816
|
+
const fromEnv = (name: string, min: number): number | undefined => {
|
|
817
|
+
const raw = env[name];
|
|
818
|
+
if (raw === undefined || !/^\d+$/.test(raw.trim())) return undefined;
|
|
819
|
+
const value = Number(raw.trim());
|
|
820
|
+
return Number.isSafeInteger(value) && value >= min ? value : undefined;
|
|
821
|
+
};
|
|
822
|
+
const valid = (value: unknown, min: number): value is number => typeof value === 'number' && Number.isSafeInteger(value) && value >= min;
|
|
823
|
+
return {
|
|
824
|
+
staleRunMinutes: valid(queue?.staleRunMinutes, 1) ? queue!.staleRunMinutes!
|
|
825
|
+
: fromEnv('ELANOUS_HARNESS_QUEUE_STALE_RUN_MINUTES', 1) ?? SEAT_CAP_STALE_RUN_MINUTES,
|
|
826
|
+
unknownSeatCap: valid(queue?.unknownSeatCap, 0) ? queue!.unknownSeatCap!
|
|
827
|
+
: fromEnv('ELANOUS_HARNESS_QUEUE_UNKNOWN_SEAT_CAP', 0) ?? SEAT_CAP_UNKNOWN_SEAT_CAP,
|
|
828
|
+
};
|
|
564
829
|
}
|
|
565
830
|
|
|
566
831
|
const IDLE_REQUEST_INTERVAL_MS = 15 * 60_000;
|
|
@@ -675,7 +940,16 @@ export function requestIdleSeats(root: string, now: Date, items: readonly QueueI
|
|
|
675
940
|
}
|
|
676
941
|
}
|
|
677
942
|
|
|
678
|
-
|
|
943
|
+
/** ORCH2 ③ — 발사를 막는 동결이면 사유(waitingReason)와 관측 칸을, 아니면 null. 동결 파일을 못 읽으면 닫힌 쪽(막는다). */
|
|
944
|
+
function launchFreezeHold(root: string, now: Date): { reason: string; freezeReason: string; until: string | null } | null {
|
|
945
|
+
let freeze: ReturnType<typeof readLandingFreeze>;
|
|
946
|
+
try { freeze = readLandingFreeze(root, now); }
|
|
947
|
+
catch { return { reason: '동결 상태 읽기 실패', freezeReason: '동결 상태 읽기 실패', until: null }; }
|
|
948
|
+
if (freeze?.holdLaunches !== true) return null;
|
|
949
|
+
return { reason: `동결 — ${freeze.reason}${freeze.until ? ` · ~${freeze.until}` : ''}`, freezeReason: freeze.reason, until: freeze.until };
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
export async function tickHarnessQueue(deps: HarnessQueueDeps = {}, requestedId?: string): Promise<QueueTick> {
|
|
679
953
|
const root = deps.root ?? effectiveInstanceRoot();
|
|
680
954
|
return locked(root, async (path) => {
|
|
681
955
|
try {
|
|
@@ -692,17 +966,47 @@ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}): Promise<Que
|
|
|
692
966
|
catch { /* Shadow observation cannot affect queue dispatch. */ }
|
|
693
967
|
}
|
|
694
968
|
const items = read(path);
|
|
969
|
+
// ORCH2 ③ 동결 창의 «발사» 절반 — `freeze on --hold-launches` 면 이 틱은 발사 0. 동결 파일을 못 읽으면 착지 쪽과 같이 닫힌 쪽(발사 0).
|
|
970
|
+
const hold = launchFreezeHold(root, (deps.now ?? (() => new Date()))());
|
|
971
|
+
if (hold) {
|
|
972
|
+
const reason = hold.reason;
|
|
973
|
+
const queued = items.filter((item) => item.status === 'queued');
|
|
974
|
+
const updated = items.map((item) => item.status === 'queued' && item.waitingReason !== reason ? { ...item, waitingReason: reason } : item);
|
|
975
|
+
if (updated.some((item, index) => item !== items[index])) save(path, updated);
|
|
976
|
+
for (const item of queued) observe('waiting', { id: item.id, seat: item.seat, reason }, deps);
|
|
977
|
+
if (queued.length) {
|
|
978
|
+
try { debug.log('loop.orchestrator', 'freeze-held-launch', { reason: hold.freezeReason, until: hold.until, queued: queued.length, phase: 'tick-start' }); }
|
|
979
|
+
catch { /* Observation cannot affect dispatch. */ }
|
|
980
|
+
}
|
|
981
|
+
const subject = (requestedId ? queued.find((item) => item.id === requestedId) : undefined) ?? (!requestedId ? queued[0] : undefined);
|
|
982
|
+
return subject ? { outcome: 'waiting', item: { ...subject, waitingReason: reason }, reason }
|
|
983
|
+
: { outcome: 'skipped', reason: requestedId ? '이미 처리됨 또는 항목 없음' : 'empty' };
|
|
984
|
+
}
|
|
695
985
|
try { await (deps.idleRequest ?? requestIdleSeats)(root, (deps.now ?? (() => new Date()))(), items, deps); }
|
|
696
986
|
catch (error) {
|
|
697
987
|
try { debug.log('loop.orchestrator', 'idle-request-failed', { reason: String(error) }); }
|
|
698
988
|
catch { /* Idle observation cannot affect queue dispatch. */ }
|
|
699
989
|
}
|
|
700
990
|
const heads = new Map<QueueSeat, QueueItem>();
|
|
701
|
-
for (const row of items)
|
|
991
|
+
for (const row of items) {
|
|
992
|
+
if (row.status !== 'queued') continue;
|
|
993
|
+
const head = heads.get(row.seat);
|
|
994
|
+
if (!head || queueHeadBefore(row, head)) heads.set(row.seat, row);
|
|
995
|
+
}
|
|
996
|
+
if (requestedId && ![...heads.values()].some((row) => row.id === requestedId)) {
|
|
997
|
+
const row = items.find((item) => item.id === requestedId);
|
|
998
|
+
if (!row || row.status !== 'queued') return { outcome: 'skipped', item: row, reason: '이미 처리됨 또는 항목 없음' };
|
|
999
|
+
const reason = '앞선 대기열 항목 차례';
|
|
1000
|
+
if (row.waitingReason !== reason) save(path, items.map((item) => item.id === requestedId ? { ...item, waitingReason: reason } : item));
|
|
1001
|
+
observe('waiting', { id: row.id, seat: row.seat, reason }, deps);
|
|
1002
|
+
return { outcome: 'waiting', item: { ...row, waitingReason: reason }, reason };
|
|
1003
|
+
}
|
|
702
1004
|
if (!heads.size) { observe('skipped', { reason: 'empty' }, deps); return { outcome: 'skipped', reason: 'empty' }; }
|
|
703
1005
|
const lastSeat = lastLaunchedSeat(path);
|
|
704
1006
|
const first = lastSeat ? (queueSeatNames.indexOf(lastSeat) + 1) % queueSeatNames.length
|
|
705
1007
|
: queueSeatNames.indexOf(items.find((row) => row.status === 'queued')!.seat);
|
|
1008
|
+
// A requested tick (direct CLI call) keeps the seat round-robin: it launches its row only when that row is the first
|
|
1009
|
+
// head with room; an earlier seat's head with room means the requested row waits for its turn.
|
|
706
1010
|
const candidates = [...queueSeatNames.slice(first), ...queueSeatNames.slice(0, first)]
|
|
707
1011
|
.flatMap((name) => heads.get(name) ? [heads.get(name)!] : []);
|
|
708
1012
|
const config = deps.configPath === undefined ? getUserConfig() : getUserConfig(deps.configPath);
|
|
@@ -729,7 +1033,9 @@ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}): Promise<Que
|
|
|
729
1033
|
...(details.injectedCap === undefined ? {} : { injectedCap: details.injectedCap }), capKeys: details.winners,
|
|
730
1034
|
attributed, unattributed }, deps);
|
|
731
1035
|
}
|
|
732
|
-
|
|
1036
|
+
const subject = (requestedId ? candidates.find((item) => item.id === requestedId) : undefined) ?? candidates[0]!;
|
|
1037
|
+
const subjectReason = blocked.get(subject.seat)!.reason;
|
|
1038
|
+
return { outcome: 'waiting', item: { ...subject, waitingReason: subjectReason }, reason: requestedId ? subjectReason : reason };
|
|
733
1039
|
};
|
|
734
1040
|
for (const item of candidates) {
|
|
735
1041
|
const injectedCap = deps.cap?.(item.seat);
|
|
@@ -751,6 +1057,109 @@ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}): Promise<Que
|
|
|
751
1057
|
if (run.seat) attributed[run.seat]++;
|
|
752
1058
|
else unattributed++;
|
|
753
1059
|
}
|
|
1060
|
+
const limits = harnessQueueCountingLimits(config.harness?.queue);
|
|
1061
|
+
const nowMs = (deps.now ?? (() => new Date()))().getTime();
|
|
1062
|
+
const excludedLogged = new Set<string>();
|
|
1063
|
+
const logExcluded = (key: string, data: Record<string, unknown>): void => {
|
|
1064
|
+
if (excludedLogged.has(key)) return;
|
|
1065
|
+
excludedLogged.add(key);
|
|
1066
|
+
try { debug.log('harness.queue', 'seat-cap-excluded', data); }
|
|
1067
|
+
catch { /* Observation cannot affect dispatch. */ }
|
|
1068
|
+
};
|
|
1069
|
+
const runLabel = (run: QueueRun): Record<string, unknown> => ({ runId: [...run.runIds][0] ?? [...run.podRunIds][0] ?? null,
|
|
1070
|
+
launchId: [...run.launchIds][0] ?? null, pid: [...run.pids][0] });
|
|
1071
|
+
const runKey = (run: QueueRun): string => `run:${[...run.pids].join(',')}`;
|
|
1072
|
+
// SEAT-CAP-STALE2: a run quiet in its host ledger may be working in a Pod (POD-OBS writes elsewhere). Its unfinished Job
|
|
1073
|
+
// keeps it progressing. One Job read per tick, only once some run would go stale; an unreadable read keeps every such run.
|
|
1074
|
+
let podRuns: { ok: true; runs: ReadonlySet<string> } | { ok: false } | undefined;
|
|
1075
|
+
const podState = (): typeof podRuns & {} => {
|
|
1076
|
+
if (podRuns) return podRuns;
|
|
1077
|
+
const testProcess = process.env.NODE_ENV === 'test' || Boolean(process.env.ELANOUS_TEST_HOME);
|
|
1078
|
+
try { podRuns = { ok: true, runs: (deps.podRuns ?? (testProcess ? () => new Set<string>() : () => readHarnessQueuePodRuns()))() }; }
|
|
1079
|
+
catch (error) {
|
|
1080
|
+
podRuns = { ok: false };
|
|
1081
|
+
try { debug.log('harness.queue', 'progress-source-unavailable', { source: 'pod-jobs', error: String(error).slice(0, 240) }, { level: 'warn' }); }
|
|
1082
|
+
catch { /* Observation cannot affect dispatch. */ }
|
|
1083
|
+
}
|
|
1084
|
+
return podRuns;
|
|
1085
|
+
};
|
|
1086
|
+
const progressLogged = new Set<string>();
|
|
1087
|
+
const runVerdict = (run: QueueRun, seat: QueueSeat, extra: Record<string, unknown> = {}): ReturnType<typeof seatCapExclusion> => {
|
|
1088
|
+
const verdict = seatCapExclusion({ kind: 'run', seat, progressAt: run.progressAt, stopReason: run.stopReason }, nowMs, limits);
|
|
1089
|
+
if (verdict?.reason !== 'stale') return verdict;
|
|
1090
|
+
const pod = podState();
|
|
1091
|
+
const podRunId = pod.ok ? [...run.podRunIds].find((id) => pod.runs.has(queueRunLabelValue(id))) : undefined;
|
|
1092
|
+
if (pod.ok && !podRunId) return verdict;
|
|
1093
|
+
if (!progressLogged.has(runKey(run))) {
|
|
1094
|
+
progressLogged.add(runKey(run));
|
|
1095
|
+
try { debug.log('harness.queue', 'seat-cap-progress', { ...runLabel(run), ...extra, seat, source: pod.ok ? 'pod-job' : 'pod-unavailable',
|
|
1096
|
+
...(podRunId ? { podRunId } : {}), ledgerIdleMin: verdict.idleMin }); }
|
|
1097
|
+
catch { /* Observation cannot affect dispatch. */ }
|
|
1098
|
+
}
|
|
1099
|
+
return null;
|
|
1100
|
+
};
|
|
1101
|
+
// Identity is the launch id only: a recycled wrapper pid must never adopt an unrelated run.
|
|
1102
|
+
const matches = (run: QueueRun, row: QueueItem): boolean => Boolean(row.launchId && run.launchIds.has(row.launchId));
|
|
1103
|
+
let unknownLogged = false;
|
|
1104
|
+
// A seat-less run that is a queue row's launch takes that row's seat; only the rest form the unknown-seat bucket.
|
|
1105
|
+
const logUnknownBucket = (): void => {
|
|
1106
|
+
if (unknownLogged) return;
|
|
1107
|
+
unknownLogged = true;
|
|
1108
|
+
const launchedRows = current.filter((row) => row.status === 'launching' || row.status === 'launched');
|
|
1109
|
+
const bucket = runs.filter((run) => !run.seat && !launchedRows.some((row) => matches(run, row)));
|
|
1110
|
+
for (const run of bucket) {
|
|
1111
|
+
const verdict = seatCapExclusion({ kind: 'run', progressAt: run.progressAt }, nowMs, limits);
|
|
1112
|
+
logExcluded(runKey(run), { ...runLabel(run), seat: null, reason: 'unknown-seat', idleMin: verdict?.idleMin ?? null });
|
|
1113
|
+
}
|
|
1114
|
+
if (bucket.length > limits.unknownSeatCap) {
|
|
1115
|
+
try { debug.log('harness.queue', 'unknown-seat-over-cap', { count: bucket.length, cap: limits.unknownSeatCap }, { level: 'warn' }); }
|
|
1116
|
+
catch { /* Observation cannot affect dispatch. */ }
|
|
1117
|
+
}
|
|
1118
|
+
};
|
|
1119
|
+
// Seat-less runs live in their own bucket; a seat is charged only by its own live, progressing runs and rows.
|
|
1120
|
+
const countSeat = (seat: QueueSeat): number => {
|
|
1121
|
+
logUnknownBucket();
|
|
1122
|
+
const own = runs.filter((run) => run.seat === seat);
|
|
1123
|
+
let active = 0;
|
|
1124
|
+
for (const run of own) {
|
|
1125
|
+
const verdict = runVerdict(run, seat);
|
|
1126
|
+
if (!verdict) { active++; continue; }
|
|
1127
|
+
logExcluded(runKey(run), { ...runLabel(run), seat, reason: verdict.reason, idleMin: verdict.idleMin });
|
|
1128
|
+
}
|
|
1129
|
+
for (const row of current) {
|
|
1130
|
+
if (row.seat !== seat || (row.status !== 'launching' && row.status !== 'launched')) continue;
|
|
1131
|
+
// A row whose own-seat run is already in the inventory is that run (matched by launch id).
|
|
1132
|
+
if (own.some((run) => matches(run, row))) continue;
|
|
1133
|
+
// A seat-less run that is this row's launch takes the row's seat; its progress still decides whether it holds it.
|
|
1134
|
+
const unattributedRun = runs.find((run) => !run.seat && matches(run, row));
|
|
1135
|
+
if (unattributedRun) {
|
|
1136
|
+
const verdict = runVerdict(unattributedRun, seat, { launchId: row.launchId ?? null });
|
|
1137
|
+
if (!verdict) { active++; continue; }
|
|
1138
|
+
logExcluded(runKey(unattributedRun), { ...runLabel(unattributedRun), launchId: row.launchId ?? null, seat, reason: verdict.reason, idleMin: verdict.idleMin });
|
|
1139
|
+
continue;
|
|
1140
|
+
}
|
|
1141
|
+
// A legacy row without a launch id has no receipt; wrapper liveness and the launch grace still decide.
|
|
1142
|
+
const state = row.launchId ? (deps.receipt ?? receipt)(root, row.launchId) : null;
|
|
1143
|
+
// A row with a launch id is live only through the inventory (its wrapper argv carries the id; matched above), so a
|
|
1144
|
+
// recycled pid can never keep it; the launch grace covers the moment before the wrapper appears. Only a legacy row
|
|
1145
|
+
// without a launch id falls back to its recorded pid's liveness.
|
|
1146
|
+
const live = !row.launchId && row.pid !== undefined && (deps.alive ?? alive)(row.pid);
|
|
1147
|
+
// The grace runs from the receipt time (written at spawn), else the launch id's own time (UUIDv7), else enqueue time
|
|
1148
|
+
// (legacy v4 ids only).
|
|
1149
|
+
const launchedAt = (row.launchId ? receiptTime(root, row.launchId) ?? queueLaunchTime(row.launchId) : undefined)
|
|
1150
|
+
?? Date.parse(row.at);
|
|
1151
|
+
const verdict = seatCapExclusion({ kind: 'row', status: row.status, receipt: state, live,
|
|
1152
|
+
...(Number.isFinite(launchedAt) ? { launchedAt } : {}) }, nowMs, limits);
|
|
1153
|
+
if (!verdict) { active++; continue; }
|
|
1154
|
+
logExcluded(`row:${row.id}`, { runId: null, launchId: row.launchId ?? null, queueId: row.id, seat, reason: verdict.reason, idleMin: verdict.idleMin });
|
|
1155
|
+
}
|
|
1156
|
+
return active;
|
|
1157
|
+
};
|
|
1158
|
+
const seatCounts = new Map<QueueSeat, number>();
|
|
1159
|
+
const seatActive = (seat: QueueSeat): number => {
|
|
1160
|
+
if (!seatCounts.has(seat)) seatCounts.set(seat, countSeat(seat));
|
|
1161
|
+
return seatCounts.get(seat)!;
|
|
1162
|
+
};
|
|
754
1163
|
current = items.flatMap((row) => {
|
|
755
1164
|
if ((row.status !== 'launched' && row.status !== 'launching') || !row.launchId) return [row];
|
|
756
1165
|
const state = (deps.receipt ?? receipt)(root, row.launchId);
|
|
@@ -762,17 +1171,15 @@ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}): Promise<Que
|
|
|
762
1171
|
return [];
|
|
763
1172
|
});
|
|
764
1173
|
if (current.length !== items.length || current.some((row, index) => row !== items[index])) save(path, current);
|
|
1174
|
+
// Every seat is counted once per tick so each excluded run or row is observed even when its seat has no queued head.
|
|
1175
|
+
for (const seat of queueSeatNames) seatActive(seat);
|
|
765
1176
|
let pool: QueuePool | undefined;
|
|
766
1177
|
let poolReason: string | undefined;
|
|
767
1178
|
let finishHeld: string | null | undefined;
|
|
768
1179
|
for (const item of candidates) {
|
|
769
1180
|
const injectedCap = deps.cap?.(item.seat);
|
|
770
1181
|
const details = seatCapDetails({ seat: item.seat, caps, gate, injectedCap });
|
|
771
|
-
const
|
|
772
|
-
const launchIds = new Set(charged.flatMap((run) => [...run.launchIds]));
|
|
773
|
-
const active = charged.length + current.filter((row) => row.seat === item.seat &&
|
|
774
|
-
(row.status === 'launching' || row.status === 'launched')
|
|
775
|
-
&& (!row.launchId || !launchIds.has(row.launchId))).length;
|
|
1182
|
+
const active = seatActive(item.seat);
|
|
776
1183
|
if (active >= details.cap) { blocked.set(item.seat, { reason: seatCapReason(item.seat, active, details), active }); continue; }
|
|
777
1184
|
if (!pool && !poolReason) {
|
|
778
1185
|
try { pool = (deps.pool ?? readHarnessQueuePool)(); }
|
|
@@ -799,11 +1206,38 @@ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}): Promise<Que
|
|
|
799
1206
|
+ current.filter((row) => row.status === 'launching' || row.status === 'launched').length, pool!.limit, deps);
|
|
800
1207
|
}
|
|
801
1208
|
if (finishHeld) { blocked.set(item.seat, { reason: finishHeld, active }); continue; }
|
|
802
|
-
|
|
1209
|
+
if (requestedId && item.id !== requestedId) {
|
|
1210
|
+
const requested = current.find((row) => row.id === requestedId)!;
|
|
1211
|
+
// The requested row's own block is the truthful reason — even when its seat comes later in the rotation and was not
|
|
1212
|
+
// evaluated yet; «another seat's turn» only when the requested seat itself has room.
|
|
1213
|
+
const own = blocked.get(requested.seat)?.reason ?? (() => {
|
|
1214
|
+
const ownCap = deps.cap?.(requested.seat);
|
|
1215
|
+
const details = seatCapDetails({ seat: requested.seat, caps, gate, injectedCap: ownCap });
|
|
1216
|
+
const active = seatActive(requested.seat);
|
|
1217
|
+
if (active >= details.cap) return seatCapReason(requested.seat, active, details);
|
|
1218
|
+
const budget = decideSpawn({ seat: requested.seat, running: active, caps, gate, injectedCap: ownCap });
|
|
1219
|
+
return budget.allow ? undefined : `${budget.reason}: ${seatCapReason(requested.seat, active, details)}`;
|
|
1220
|
+
})();
|
|
1221
|
+
const reason = own ?? `다른 자리 차례 — ${item.seat}`;
|
|
1222
|
+
if (requested.waitingReason !== reason) save(path, current.map((row) => row.id === requestedId ? { ...row, waitingReason: reason } : row));
|
|
1223
|
+
observe('waiting', { id: requested.id, seat: requested.seat, reason }, deps);
|
|
1224
|
+
return { outcome: 'waiting', item: { ...requested, waitingReason: reason }, reason };
|
|
1225
|
+
}
|
|
1226
|
+
// 발사 직전 다시 본다 — 틱 앞의 확인과 여기 사이에 await(idleRequest·pool·finish 측정)가 있어 그새 켜진 동결을 놓치지 않게.
|
|
1227
|
+
const lateHold = launchFreezeHold(root, (deps.now ?? (() => new Date()))());
|
|
1228
|
+
if (lateHold) {
|
|
1229
|
+
save(path, current.map((row) => row.id === item.id ? { ...row, waitingReason: lateHold.reason } : row));
|
|
1230
|
+
observe('waiting', { id: item.id, seat: item.seat, reason: lateHold.reason }, deps);
|
|
1231
|
+
try { debug.log('loop.orchestrator', 'freeze-held-launch', { reason: lateHold.freezeReason, until: lateHold.until, queued: 1, phase: 'pre-launch' }); }
|
|
1232
|
+
catch { /* Observation cannot affect dispatch. */ }
|
|
1233
|
+
return { outcome: 'waiting', item: { ...item, waitingReason: lateHold.reason }, reason: lateHold.reason };
|
|
1234
|
+
}
|
|
1235
|
+
// UUIDv7 carries the launch time, so the dead-row grace can run from launch without a new queue field.
|
|
1236
|
+
const launching: QueueItem = { ...item, status: 'launching', launchId: `hq-${Bun.randomUUIDv7()}` };
|
|
803
1237
|
delete launching.waitingReason;
|
|
804
1238
|
save(path, current.map((row) => row.id === item.id ? launching : row));
|
|
805
1239
|
try {
|
|
806
|
-
const pid = await (deps.launch ??
|
|
1240
|
+
const pid = await (deps.launch ?? defaultLaunch(deps.launchCommand))(launching, queueLaunchArgs(launching), root);
|
|
807
1241
|
if (!Number.isSafeInteger(pid) || pid < 1) throw new Error('launcher returned no pid');
|
|
808
1242
|
const launched: QueueItem = { ...launching, pid, status: 'launched' };
|
|
809
1243
|
save(path, current.map((row) => row.id === item.id ? launched : row));
|