elanous 0.2.17 → 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-aac7332576c4d6fc.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/app/approvals/{page-3143b07a817d1da9.js → page-d92f7ee3994e84c7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-17ba086ef82d339b.js → page-4733c967b56a4277.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/{page-7eb661ff23af7da2.js → page-d22fd849cc22f104.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-bb742f995c6769e7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-6e5c3a867557298b.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-5e77cd1802567c89.js → page-a0eb02638b1e6f31.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-308111abadf35670.js +1 -0
- 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-4187666132fc98c4.js → page-5d97b2901e0d1ea6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-09328d499162bae4.js → page-c39554c321576982.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-29ff3a8f84f03b6a.js → page-8c3d8e63ae2e1c98.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-80ec7401bc83f511.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-fc7d86467291ae38.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-0e2d6985fb42cd2f.js → page-c130b63862437814.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/market/{page-13e4108999f58d13.js → page-fa244bd146a00ae8.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-e8296ee284aa1293.js → page-44d56d6391965a6a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-043a9111e74f6b24.js → page-83a700d4c754d802.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-23a4601ceb7f452e.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-10d958605a77b3bd.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-7a1d85e121065cc8.js → page-e8e1ecaea7cd71d2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-12916918988ac97e.js → page-1467047104141297.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-d8a3347a9d0c3f4a.js → page-2da6f9b8a38c6978.js} +1 -1
- 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-1d824510df025d18.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-7f49fe645d5132f7.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-27d71f131acd9320.js → page-b17cd46cf0e9b8cf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-61553851aa606146.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/page-a1f585e154e61c59.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-424659bacfb8cdd7.js → page-7bf54c9bb9274af8.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-5150ef17b5383a57.js +1 -0
- 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-5d4fef9ccd7d406b.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/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/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/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 +8 -1
- 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 +8 -1
- 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 +145 -40
- 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/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/briefing/brief-items.ts +86 -17
- package/src/cli/autopilot-cli.ts +30 -11
- package/src/cli/brief-cli.ts +36 -2
- package/src/cli/freeze-cli.ts +3 -2
- package/src/cli/intake-cli.ts +73 -1
- package/src/cli/notify-cli.ts +75 -0
- package/src/cli/provider-cli.ts +12 -2
- package/src/cli/release-cli.ts +101 -17
- 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/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 +10 -2
- package/src/harness/harness-queue.ts +388 -33
- package/src/harness/helper-scan.ts +10 -3
- package/src/hq/seats.ts +369 -0
- package/src/index.ts +133 -15
- 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/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/loop-edges.ts +69 -13
- package/src/nexus/api/ops-seats-sources.ts +2 -2
- package/src/nexus/api/workflow-templates.ts +3 -0
- package/src/oauth/codex.ts +38 -0
- 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 +6 -4
- 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 +12 -6
- package/src/self-dev/ask-launch-flow.ts +24 -3
- 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-supervisor.ts +14 -0
- package/src/self-implement/follow-up-goals.ts +2 -2
- package/src/self-implement/gate-remote.ts +427 -0
- package/src/self-implement/goal-author.ts +8 -3
- package/src/self-implement/l8-merge-queue.ts +29 -5
- package/src/self-implement/orchestrator.ts +59 -12
- package/src/self-implement/prior-draft-findings.ts +4 -3
- package/src/skills/skill-feedback.ts +76 -0
- package/src/task-agent/actions.ts +39 -6
- package/src/task-agent/judge.ts +31 -5
- 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-orchestrator/surfaces/pod-failure-reason.ts +10 -0
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +86 -18
- package/src/telegram-loops-command.ts +1 -1
- package/src/telegram.ts +5 -20
- package/src/user-config.ts +63 -3
- 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-fe94e76a5841ee9b.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/8401-4ce1c42c8d89903f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8949-ecb4128a0858b911.js +0 -1
- package/apps/pwa/out/_next/static/chunks/899-f41be85e8a680651.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-6b8444914da74ca6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-7110f732f45bf944.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-9a41d65355fc25b9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-41afec0b255ed2bb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-ab93b240f687d69a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-de43a186e5ba5683.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-8b08cafad27759c9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-1ed1324b1dc36754.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-166745e51ac0c7b0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a04260b8ccc968d4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-1f50e3d4e13435f4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-a9117d2cd3093d36.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-5a64cb5208c6a8fd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-1407ec83ea64c1f9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-6e878ebbe1737d00.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-81a4e7f8dbfe74cb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-3ab59d19913b200e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-bf85b2f5bf077925.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-ab32a8f522a60371.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-fc2df261f8a2c7df.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-09461366af82ef13.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ea90a8ac73ce2749.js +0 -1
- package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +0 -1
- package/apps/pwa/out/_next/static/yRA5ur6DcPl0YHSRheH2O/_buildManifest.js +0 -1
- /package/apps/pwa/out/_next/static/{yRA5ur6DcPl0YHSRheH2O → P6YTVgzuZz5oTZhY4Din_}/_ssgManifest.js +0 -0
|
@@ -6,27 +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;
|
|
25
|
-
launchArgs?: string[]; launchCwd?: string; cellId?: 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;
|
|
26
30
|
};
|
|
27
31
|
export type QueuePool = { running: number; pending: number; reserved: number; limit: number };
|
|
28
32
|
export type QueueTick = { outcome: 'launched' | 'waiting' | 'skipped'; item?: QueueItem; reason: string };
|
|
29
|
-
|
|
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[] };
|
|
30
37
|
type ProcessProbe = {
|
|
31
38
|
run?: typeof spawnSync; platform?: NodeJS.Platform;
|
|
32
39
|
cwd?: (pid: number) => string;
|
|
@@ -68,8 +75,13 @@ export interface HarnessQueueDeps {
|
|
|
68
75
|
next: readonly Pick<ChecklistItem, 'id' | 'title' | 'owner' | 'status' | 'evidence'>[] };
|
|
69
76
|
idleLog?: (category: string, event: string, data: Record<string, unknown>) => void;
|
|
70
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;
|
|
71
80
|
/** FINISH-RATE metrics source; a test process without it skips the real measurement. */
|
|
72
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>;
|
|
73
85
|
}
|
|
74
86
|
|
|
75
87
|
const FINISH_CACHE_MS = 10 * 60 * 1_000;
|
|
@@ -105,6 +117,11 @@ async function cachedFinishMetrics(root: string, now: Date, deps: HarnessQueueDe
|
|
|
105
117
|
mkdirSync(join(root, 'harness'), { recursive: true });
|
|
106
118
|
writeFileSync(path, JSON.stringify({ at: now.toISOString(), metrics }));
|
|
107
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
|
+
}
|
|
108
125
|
return metrics;
|
|
109
126
|
}
|
|
110
127
|
|
|
@@ -149,8 +166,11 @@ function read(path: string): QueueItem[] {
|
|
|
149
166
|
&& (row.launchId === undefined || typeof row.launchId === 'string')
|
|
150
167
|
&& (row.idempotencyKey === undefined || typeof row.idempotencyKey === 'string')
|
|
151
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')
|
|
152
171
|
&& (row.launchCwd === undefined || typeof row.launchCwd === 'string' && row.launchCwd.startsWith('/'))
|
|
153
172
|
&& (row.cellId === undefined || typeof row.cellId === 'string')
|
|
173
|
+
&& (row.priority === undefined || typeof row.priority === 'number' && Number.isFinite(row.priority))
|
|
154
174
|
&& (row.waitingReason === undefined || typeof row.waitingReason === 'string'))) {
|
|
155
175
|
throw new Error('harness queue: invalid queue file (no launch)');
|
|
156
176
|
}
|
|
@@ -226,13 +246,56 @@ function queueRowCellId(kind: QueueItem['kind'], input: string): string | undefi
|
|
|
226
246
|
try { return queueCellId(readFileSync(input, 'utf8')); } catch { return undefined; }
|
|
227
247
|
}
|
|
228
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
|
+
|
|
229
292
|
export class HarnessQueueDuplicateError extends Error {
|
|
230
293
|
constructor(existing: QueueItem, key: string) {
|
|
231
294
|
super(`같은 ${key} 이 이미 대기열에 있거나 도는 중이다 — ${existing.id} (${existing.status})`);
|
|
232
295
|
}
|
|
233
296
|
}
|
|
234
297
|
|
|
235
|
-
export async function addHarnessQueue(input: { seat: string; say?: string; ask?: string; hold?: boolean; heavy?: boolean; idempotencyKey?: string; launchArgs?: string[]; launchCwd?: string; refuseDuplicate?: boolean }, deps: HarnessQueueDeps = {}): Promise<QueueItem> {
|
|
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> {
|
|
236
299
|
const assigned = seat(input.seat);
|
|
237
300
|
if ((input.say === undefined) === (input.ask === undefined)) throw new Error('harness queue add: --say 또는 --ask 중 하나만 필요');
|
|
238
301
|
const kind = input.ask === undefined ? 'say' : 'ask';
|
|
@@ -266,14 +329,17 @@ export async function addHarnessQueue(input: { seat: string; say?: string; ask?:
|
|
|
266
329
|
throw new HarnessQueueDuplicateError(existing, key);
|
|
267
330
|
}
|
|
268
331
|
}
|
|
332
|
+
const priority = defaultQueuePriority(cellId, root, deps);
|
|
269
333
|
const item: QueueItem = { id: `hq-${randomUUID()}`, seat: assigned, kind, input: value,
|
|
270
334
|
hold: input.hold === true, heavy: input.heavy === true, at: new Date().toISOString(), status: 'queued',
|
|
271
335
|
...(input.launchArgs === undefined ? {} : { launchArgs: input.launchArgs }),
|
|
336
|
+
...(input.poolHint === undefined ? {} : { poolHint: input.poolHint, ...(input.poolHintSource ? { poolHintSource: input.poolHintSource } : {}) }),
|
|
272
337
|
...(input.launchCwd === undefined ? {} : { launchCwd: input.launchCwd }),
|
|
273
338
|
...(cellId === undefined ? {} : { cellId }),
|
|
339
|
+
...(priority === undefined ? {} : { priority }),
|
|
274
340
|
...(input.idempotencyKey === undefined ? {} : { idempotencyKey: input.idempotencyKey }) };
|
|
275
341
|
save(path, [...rows, item]);
|
|
276
|
-
observe('enqueued', { id: item.id, seat: item.seat, kind }, deps);
|
|
342
|
+
observe('enqueued', { id: item.id, seat: item.seat, kind, ...(priority === undefined ? {} : { priority }) }, deps);
|
|
277
343
|
return item;
|
|
278
344
|
});
|
|
279
345
|
}
|
|
@@ -336,6 +402,46 @@ function queueRowOutcome(row: QueueItem, root: string): 'pending' | 'succeeded'
|
|
|
336
402
|
return state === 'started' ? 'pending' : 'unknown';
|
|
337
403
|
}
|
|
338
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
|
+
|
|
339
445
|
export async function removeHarnessQueue(id: string, deps: HarnessQueueDeps = {}): Promise<boolean> {
|
|
340
446
|
const root = deps.root ?? effectiveInstanceRoot();
|
|
341
447
|
return locked(root, async (path) => {
|
|
@@ -358,19 +464,28 @@ export async function removeHarnessQueue(id: string, deps: HarnessQueueDeps = {}
|
|
|
358
464
|
});
|
|
359
465
|
}
|
|
360
466
|
|
|
361
|
-
export function queueLaunchArgs(item: QueueItem): string[] {
|
|
362
|
-
|
|
363
|
-
return ['harness', item.kind, item.input, '--substrate', 'pod',
|
|
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',
|
|
364
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];
|
|
365
476
|
}
|
|
366
477
|
|
|
367
|
-
|
|
478
|
+
function harnessQueuePoolMembers(): ReturnType<typeof parsePodPool> {
|
|
368
479
|
const config = getUserConfig();
|
|
369
480
|
const spec = resolvePodPoolSpec(undefined, process.env, () => config.harness?.podPool ?? config.pod?.pool);
|
|
370
481
|
const current = spec ? null : leaseKubectl(['config', 'current-context']);
|
|
371
482
|
const context = spec ?? (current?.status === 0 ? current.stdout.trim() : '');
|
|
372
483
|
if (!context) throw new Error('pod lease status: no pool/context');
|
|
373
|
-
|
|
484
|
+
return parsePodPool(context);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export function readHarnessQueuePool(): QueuePool {
|
|
488
|
+
const members = harnessQueuePoolMembers();
|
|
374
489
|
const observed = measurePoolLease(members);
|
|
375
490
|
if (observed.members.some((member) => member.running === null || member.pending === null)) throw new Error('pod lease status: incomplete running/pending measurement');
|
|
376
491
|
const pendingJobs = observed.members.flatMap((member) => member.pendingJobs ?? []);
|
|
@@ -383,6 +498,41 @@ export function readHarnessQueuePool(): QueuePool {
|
|
|
383
498
|
};
|
|
384
499
|
}
|
|
385
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
|
+
|
|
386
536
|
function alive(pid: number): boolean {
|
|
387
537
|
try { process.kill(pid, 0); return true; } catch { return false; }
|
|
388
538
|
}
|
|
@@ -420,14 +570,25 @@ export function readHarnessQueueProcesses(probe: ProcessProbe = {}): readonly Qu
|
|
|
420
570
|
const ambiguousPids = new Set<number>();
|
|
421
571
|
const candidatePids = new Set(candidates.map(({ pid }) => pid));
|
|
422
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>>();
|
|
423
575
|
for (const record of listSelfDevRuns(probe.runsDir ?? selfDevRunsDir())) {
|
|
424
576
|
const pid = record.pid;
|
|
425
|
-
if (!pid || !candidatePids.has(pid) || !record.pidStart
|
|
577
|
+
if (!pid || !candidatePids.has(pid) || !record.pidStart) continue;
|
|
426
578
|
if (!births.has(pid)) {
|
|
427
579
|
try { births.set(pid, (probe.birthId ?? ((id: number) => processBirthId(id, platform)))(pid)); }
|
|
428
580
|
catch { births.set(pid, undefined); }
|
|
429
581
|
}
|
|
430
|
-
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;
|
|
431
592
|
const prior = ledgerSeats.get(pid);
|
|
432
593
|
if (prior && prior !== record.seat) { ledgerSeats.delete(pid); ambiguousPids.add(pid); }
|
|
433
594
|
else ledgerSeats.set(pid, record.seat!);
|
|
@@ -446,6 +607,8 @@ export function readHarnessQueueProcesses(probe: ProcessProbe = {}): readonly Qu
|
|
|
446
607
|
let launchId: string | undefined;
|
|
447
608
|
let runId: string | undefined;
|
|
448
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];
|
|
449
612
|
let envSeat: string | undefined;
|
|
450
613
|
if (platform === 'linux') {
|
|
451
614
|
try {
|
|
@@ -487,10 +650,16 @@ export function readHarnessQueueProcesses(probe: ProcessProbe = {}): readonly Qu
|
|
|
487
650
|
const top = eligible.filter((entry) => entry.depth === depth);
|
|
488
651
|
const seats = new Set(top.map((entry) => entry.seat));
|
|
489
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) ?? [])]))];
|
|
490
658
|
try { debug.log('harness.queue', 'attributed', { pid: root.pid, seat: assigned?.seat ?? null, source: assigned?.source ?? 'all' }); }
|
|
491
659
|
catch { /* Observation does not block inventory. */ }
|
|
492
660
|
rows.push({ pid: root.pid, ...(assigned ? { seat: assigned.seat } : {}), ...(launchId ? { launchId } : {}),
|
|
493
|
-
...(runId ? { runId } : {}) })
|
|
661
|
+
...(runId ? { runId } : {}), ...(progressAt !== undefined ? { progressAt } : {}), ...(stopReason ? { stopReason } : {}),
|
|
662
|
+
...(runIds.length ? { ledgerRunIds: runIds } : {}) });
|
|
494
663
|
}
|
|
495
664
|
return rows;
|
|
496
665
|
}
|
|
@@ -510,6 +679,23 @@ function receipt(root: string, launchId: string): 'started' | 'finished' | 'not-
|
|
|
510
679
|
}
|
|
511
680
|
}
|
|
512
681
|
|
|
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
|
+
|
|
513
699
|
function defaultLaunch(command?: string) {
|
|
514
700
|
return (item: QueueItem, args: string[], root: string) => launch(item, args, root, command);
|
|
515
701
|
}
|
|
@@ -582,18 +768,23 @@ function warnLegacySeatCap(path: string): void {
|
|
|
582
768
|
} catch { /* An unreadable config cannot affect dispatch or observation. */ }
|
|
583
769
|
}
|
|
584
770
|
|
|
585
|
-
|
|
586
|
-
|
|
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[] }[] = [];
|
|
587
777
|
for (const row of processes) {
|
|
588
778
|
const keys = [row.runId && `run:${row.runId}`, `root:${row.rootPid ?? row.pid}`,
|
|
589
779
|
row.launchId && `launch:${row.launchId}`, `pid:${row.pid}`].filter((key): key is string => Boolean(key));
|
|
590
780
|
const matching = groups.filter((group) => keys.some((key) => group.keys.has(key)));
|
|
591
781
|
const group = matching[0] ?? { seats: new Set<QueueSeat>(), unknown: false,
|
|
592
|
-
launchIds: new Set<string>(), keys: new Set<string>() };
|
|
782
|
+
launchIds: new Set<string>(), keys: new Set<string>(), rows: [] };
|
|
593
783
|
for (const other of matching.slice(1)) {
|
|
594
784
|
for (const key of other.keys) group.keys.add(key);
|
|
595
785
|
for (const seat of other.seats) group.seats.add(seat);
|
|
596
786
|
for (const id of other.launchIds) group.launchIds.add(id);
|
|
787
|
+
group.rows.push(...other.rows);
|
|
597
788
|
group.unknown ||= other.unknown;
|
|
598
789
|
groups.splice(groups.indexOf(other), 1);
|
|
599
790
|
}
|
|
@@ -601,12 +792,40 @@ function groupQueueRuns(processes: readonly QueueProcess[]): { seat?: QueueSeat;
|
|
|
601
792
|
if (row.seat) group.seats.add(row.seat);
|
|
602
793
|
else group.unknown = true;
|
|
603
794
|
if (row.launchId) group.launchIds.add(row.launchId);
|
|
795
|
+
group.rows.push(row);
|
|
604
796
|
if (!matching.length) groups.push(group);
|
|
605
797
|
}
|
|
606
|
-
return groups.map((group) =>
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
+
};
|
|
610
829
|
}
|
|
611
830
|
|
|
612
831
|
const IDLE_REQUEST_INTERVAL_MS = 15 * 60_000;
|
|
@@ -721,6 +940,15 @@ export function requestIdleSeats(root: string, now: Date, items: readonly QueueI
|
|
|
721
940
|
}
|
|
722
941
|
}
|
|
723
942
|
|
|
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
|
+
|
|
724
952
|
export async function tickHarnessQueue(deps: HarnessQueueDeps = {}, requestedId?: string): Promise<QueueTick> {
|
|
725
953
|
const root = deps.root ?? effectiveInstanceRoot();
|
|
726
954
|
return locked(root, async (path) => {
|
|
@@ -738,13 +966,33 @@ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}, requestedId?
|
|
|
738
966
|
catch { /* Shadow observation cannot affect queue dispatch. */ }
|
|
739
967
|
}
|
|
740
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
|
+
}
|
|
741
985
|
try { await (deps.idleRequest ?? requestIdleSeats)(root, (deps.now ?? (() => new Date()))(), items, deps); }
|
|
742
986
|
catch (error) {
|
|
743
987
|
try { debug.log('loop.orchestrator', 'idle-request-failed', { reason: String(error) }); }
|
|
744
988
|
catch { /* Idle observation cannot affect queue dispatch. */ }
|
|
745
989
|
}
|
|
746
990
|
const heads = new Map<QueueSeat, QueueItem>();
|
|
747
|
-
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
|
+
}
|
|
748
996
|
if (requestedId && ![...heads.values()].some((row) => row.id === requestedId)) {
|
|
749
997
|
const row = items.find((item) => item.id === requestedId);
|
|
750
998
|
if (!row || row.status !== 'queued') return { outcome: 'skipped', item: row, reason: '이미 처리됨 또는 항목 없음' };
|
|
@@ -809,6 +1057,109 @@ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}, requestedId?
|
|
|
809
1057
|
if (run.seat) attributed[run.seat]++;
|
|
810
1058
|
else unattributed++;
|
|
811
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
|
+
};
|
|
812
1163
|
current = items.flatMap((row) => {
|
|
813
1164
|
if ((row.status !== 'launched' && row.status !== 'launching') || !row.launchId) return [row];
|
|
814
1165
|
const state = (deps.receipt ?? receipt)(root, row.launchId);
|
|
@@ -820,17 +1171,15 @@ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}, requestedId?
|
|
|
820
1171
|
return [];
|
|
821
1172
|
});
|
|
822
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);
|
|
823
1176
|
let pool: QueuePool | undefined;
|
|
824
1177
|
let poolReason: string | undefined;
|
|
825
1178
|
let finishHeld: string | null | undefined;
|
|
826
1179
|
for (const item of candidates) {
|
|
827
1180
|
const injectedCap = deps.cap?.(item.seat);
|
|
828
1181
|
const details = seatCapDetails({ seat: item.seat, caps, gate, injectedCap });
|
|
829
|
-
const
|
|
830
|
-
const launchIds = new Set(charged.flatMap((run) => [...run.launchIds]));
|
|
831
|
-
const active = charged.length + current.filter((row) => row.seat === item.seat &&
|
|
832
|
-
(row.status === 'launching' || row.status === 'launched')
|
|
833
|
-
&& (!row.launchId || !launchIds.has(row.launchId))).length;
|
|
1182
|
+
const active = seatActive(item.seat);
|
|
834
1183
|
if (active >= details.cap) { blocked.set(item.seat, { reason: seatCapReason(item.seat, active, details), active }); continue; }
|
|
835
1184
|
if (!pool && !poolReason) {
|
|
836
1185
|
try { pool = (deps.pool ?? readHarnessQueuePool)(); }
|
|
@@ -864,11 +1213,7 @@ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}, requestedId?
|
|
|
864
1213
|
const own = blocked.get(requested.seat)?.reason ?? (() => {
|
|
865
1214
|
const ownCap = deps.cap?.(requested.seat);
|
|
866
1215
|
const details = seatCapDetails({ seat: requested.seat, caps, gate, injectedCap: ownCap });
|
|
867
|
-
const
|
|
868
|
-
const launchIds = new Set(charged.flatMap((run) => [...run.launchIds]));
|
|
869
|
-
const active = charged.length + current.filter((row) => row.seat === requested.seat &&
|
|
870
|
-
(row.status === 'launching' || row.status === 'launched')
|
|
871
|
-
&& (!row.launchId || !launchIds.has(row.launchId))).length;
|
|
1216
|
+
const active = seatActive(requested.seat);
|
|
872
1217
|
if (active >= details.cap) return seatCapReason(requested.seat, active, details);
|
|
873
1218
|
const budget = decideSpawn({ seat: requested.seat, running: active, caps, gate, injectedCap: ownCap });
|
|
874
1219
|
return budget.allow ? undefined : `${budget.reason}: ${seatCapReason(requested.seat, active, details)}`;
|
|
@@ -878,7 +1223,17 @@ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}, requestedId?
|
|
|
878
1223
|
observe('waiting', { id: requested.id, seat: requested.seat, reason }, deps);
|
|
879
1224
|
return { outcome: 'waiting', item: { ...requested, waitingReason: reason }, reason };
|
|
880
1225
|
}
|
|
881
|
-
|
|
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()}` };
|
|
882
1237
|
delete launching.waitingReason;
|
|
883
1238
|
save(path, current.map((row) => row.id === item.id ? launching : row));
|
|
884
1239
|
try {
|