elanous 0.2.18 → 0.2.20
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/chunks/{1307-30dff22c9e1f3073.js → 1307-77fad80cf54477c6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/1612-f60c0c93ec773f81.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2116-6429c188eb846ab8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/224-e7be8f762fe74986.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2489-ff55a35b202c0f94.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2549-89c518a4c8ed4d00.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3857-995ea9f5ef33d71e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4144-fad095b48a1862ee.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5755-ea7ed88eb956f47b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5977-94ed6f78e9814b2b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7748-6e51bfe927019f5c.js +25 -0
- package/apps/pwa/out/_next/static/chunks/8401-35e94e9091f5eb8d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9247-c1753a059e7bac08.js +1 -0
- package/apps/pwa/out/_next/static/chunks/a9f2b54d-33c0689b70ed8cf3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/{page-d92f7ee3994e84c7.js → page-c855c24de7db74fe.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-4733c967b56a4277.js → page-85b316bb4235980b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/{page-d22fd849cc22f104.js → page-9b2b26a33cb2cd42.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-bb742f995c6769e7.js → page-c9955fde67b1bcb5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-02b073a2ac793b83.js → page-7a852956c74015a3.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-c5a5c621be0e04ae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-8a9b3594bc23289b.js → page-4ba4abe096b6c2a8.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-575d4a8b4f7becb2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/{page-308111abadf35670.js → page-f5d56ff5f45ad164.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-2260e9eef0357421.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-5d97b2901e0d1ea6.js → page-7c058069c5e7fa63.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-dc5da0b8a2edb8a4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-8c3d8e63ae2e1c98.js → page-466bf0ee16ad1432.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-9b72a07d424701c6.js → page-a23bc71713bcc08f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-ed4a6f799f005d09.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-a572f58cd3ca5864.js → page-9cd565005b93aca6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-55f0f40c6db95f15.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/{page-be5ea965a07fbc41.js → page-474cb5888ea0e8f3.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-35bfec2e631d3eae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/{page-fa244bd146a00ae8.js → page-9ca21f9ad3208a61.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-44d56d6391965a6a.js → page-f9ccb1b1adbea14d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-83a700d4c754d802.js → page-ec102c9b8b691e37.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-7b3170f374f77b9d.js → not-found-a9c2cfa8dda922a5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-6da35144878feb6d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-f6055495e9ed7ea8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-25204694fa9984a5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a60e035f151d84d3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-189c587f36874538.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-c56634bde705a65d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-1467047104141297.js → page-db03ae3589a81c9e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-4a6463b97f0ad94f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-102310930791197a.js → page-75bffecc9aa1004e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-a8d2fe0c669e0de4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-6ed0b73bae8351d8.js → page-31d5b875760bb62d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-c26fa10aac6c21c4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-43336055451f6e37.js → page-68793c6e10331d66.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/{page-3cc47dca0922ab9a.js → page-46d5201d2207a511.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/{page-a77ee355a2304ff0.js → page-d81c07a7b4f618ae.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-b17cd46cf0e9b8cf.js → page-be1c4e8697f22360.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-cf0a4b4abead71ea.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/page-a4717e2c4bf7a28f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-7bf54c9bb9274af8.js → page-0fd895fab9b62c1b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-8b6c1454a8e8f306.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-c6e4364d9de8f717.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-0acc8e198f7b0dfb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-134da468538baf48.js → page-34882c84f84021c5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-0506a1e0e4bdb2f5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{webpack-c2782b330447ffb6.js → webpack-c260571513bf43c2.js} +1 -1
- package/apps/pwa/out/_next/static/css/{e99e3cff94ced78a.css → 599369d853c61df7.css} +1 -1
- package/apps/pwa/out/_next/static/css/c23d959d3ee15e57.css +1 -0
- package/apps/pwa/out/_next/static/css/d3427996da11e480.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 -1
- package/apps/pwa/out/decisions/index.txt +14 -14
- 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 +16 -15
- 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 +16 -15
- 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 +24 -23
- 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/demo/editor-recipes.yaml +22 -0
- package/graphs/ops/guardian.yaml +26 -0
- package/graphs/ops/recipes.yaml +3 -0
- package/graphs/retro/daily.yaml +25 -0
- package/graphs/retro/recipes.yaml +24 -0
- package/package.json +1 -1
- package/scripts/ask-marker-check.ts +3 -1
- package/scripts/coord-channel-watch.sh +2 -2
- package/scripts/discord-seat-probe.ts +3 -1
- package/scripts/land-global-checks.ts +89 -0
- package/scripts/lib/pch-regress-map.ts +24 -0
- package/scripts/morning-report.ts +18 -4
- package/scripts/pch-regress.ts +88 -0
- package/scripts/release-loop/gate-diff.ts +18 -0
- package/scripts/release-loop/gate-node.ts +628 -65
- package/scripts/release-loop/gate-partial.ts +106 -0
- package/scripts/release-loop/gate-timing.ts +229 -0
- package/scripts/release-loop/preflight.ts +6 -4
- package/scripts/release-loop/publish-window.ts +141 -0
- package/scripts/release-loop/release-docs.ts +1 -4
- package/scripts/release-loop/release-universe.ts +38 -0
- package/scripts/release-loop/resume-release.ts +67 -5
- package/scripts/release-loop/shard-plan.ts +65 -0
- package/scripts/release-loop/unattended-release.ts +12 -2
- package/scripts/release-story/draft.ts +5 -1
- package/scripts/retro/shadow-compare.ts +71 -0
- package/scripts/rhythm/daily.ts +6 -1
- package/scripts/video/scene-table.ts +159 -0
- package/src/acp/core-turn-bridge.ts +39 -2
- package/src/agent-mission/driver.ts +34 -6
- package/src/autopilot/build/llm-conflict-merge.ts +132 -7
- package/src/autopilot/build/merge-intent.ts +135 -7
- package/src/away/presence.ts +40 -0
- package/src/away/release-watch.ts +272 -0
- package/src/chat/index.ts +2 -1
- package/src/chat/short-question.ts +3 -40
- package/src/cli/away-cli.ts +68 -0
- package/src/cli/config-drift.ts +513 -0
- package/src/cli/guardian-cli.ts +49 -0
- package/src/cli/intake-cli.ts +13 -7
- package/src/cli/log-event-names.ts +5 -0
- package/src/cli/pod-cli.ts +5 -1
- package/src/cli/pr-cli.ts +20 -0
- package/src/cli/pty-drive-cli.ts +14 -3
- package/src/cli/release-cli.ts +52 -12
- package/src/cli/retro-cli.ts +78 -0
- package/src/cli/setup-cli.ts +31 -1
- package/src/cli/tasks-cli.ts +193 -14
- package/src/context-bus/context-now-public.ts +1 -1
- package/src/context-bus/context-now-surfaces.ts +16 -6
- package/src/context-bus/context-now.ts +97 -3
- package/src/core-turn/short-question.ts +100 -0
- package/src/dashboard/dashboard-status-lines.ts +24 -4
- package/src/dashboard/index.ts +50 -5
- package/src/dashboard/input/slash-executor.ts +6 -2
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +68 -40
- package/src/dashboard/slash-runtime/decide-slash.ts +60 -0
- package/src/dashboard/slash-runtime/help-from-registry.ts +62 -23
- package/src/dashboard/slash-runtime/help-groups.ts +44 -0
- package/src/dashboard/slash-runtime/model-choices.ts +125 -0
- package/src/domains/guardian-tick.ts +138 -0
- package/src/domains/outbound-alert.ts +44 -13
- package/src/domains/outbound-log-probe.ts +6 -1
- package/src/domains/schedule-registry.ts +23 -0
- package/src/domains/telegram-kind-route.ts +39 -0
- package/src/domains/watch-default-scheduler.ts +95 -0
- package/src/esc-abort-gate.ts +44 -0
- package/src/graph-runner/graph-run-api.ts +205 -0
- package/src/graph-runner/runner.ts +36 -6
- package/src/harness/harness-cli-command.ts +420 -51
- package/src/harness/harness-queue.ts +315 -25
- package/src/harness/harness-salvage-cli.ts +140 -0
- package/src/harness/harness-worktree-add.ts +68 -0
- package/src/harness/ledger-write-guard.ts +6 -16
- package/src/hq/fence-outcomes.ts +63 -0
- package/src/hq/fence-wrapper.ts +33 -0
- package/src/hq/hq.ts +17 -2
- package/src/index.ts +118 -6
- package/src/instance/test-write-guard-spy-first.fixture.ts +13 -0
- package/src/instance/test-write-guard.ts +200 -0
- package/src/intake-plane/digest.ts +39 -15
- package/src/intake-plane/harness-target.ts +4 -3
- package/src/intake-plane/intake-sources.ts +1 -0
- package/src/intake-plane/items.ts +1 -0
- package/src/loops/orchestrator/degradation.ts +57 -0
- package/src/loops/orchestrator/finish-rate.ts +33 -3
- package/src/loops/orchestrator/tick.ts +166 -45
- package/src/maturity/feature-maturity.ts +6 -4
- package/src/maturity/tui-slash-maturity.ts +1 -0
- package/src/mss/logging/log-store.ts +3 -0
- package/src/nexus/api/graph-kinds.ts +10 -4
- package/src/nexus/api/graphs-api.ts +178 -6
- package/src/nexus/api/http-server.ts +54 -8
- package/src/nexus/api/loop-edges.ts +292 -17
- package/src/nexus/api/ops-api.ts +112 -29
- package/src/nexus/api/ops-board-route.ts +48 -0
- package/src/nexus/index.ts +7 -0
- package/src/onboarding/entry-hints.ts +5 -0
- package/src/onboarding.ts +24 -6
- package/src/release-loop/checklist.ts +2 -0
- package/src/release-loop/feature-store.ts +68 -18
- package/src/release-loop/gate-shards.ts +158 -0
- package/src/release-loop/light-rc.ts +173 -0
- package/src/release-loop/placement.ts +51 -17
- package/src/release-loop/release-schedule.ts +6 -2
- package/src/release-loop/rubric.ts +57 -1
- package/src/retro/daily.ts +372 -0
- package/src/seat-loop/op-decide.ts +173 -0
- package/src/seat-loop/seat-loop.ts +60 -3
- package/src/seat-loop/seat-report.ts +58 -3
- package/src/self-dev/ask-launch-flow.ts +5 -4
- package/src/self-dev/ask-launch-io.ts +49 -1
- package/src/self-dev/ask-path-candidates.ts +40 -2
- package/src/self-dev/dev-cli.ts +36 -0
- package/src/self-dev/draft-sweep.ts +371 -16
- package/src/self-dev/draft-triage-rules.ts +19 -4
- package/src/self-dev/orchestrate.ts +1 -0
- package/src/self-dev/pr-preview.ts +247 -0
- package/src/self-dev/shard-run.ts +23 -7
- package/src/self-implement/blocked-draft-policy.ts +101 -0
- package/src/self-implement/execution-clarification.ts +11 -0
- package/src/self-implement/gate-cli.ts +29 -8
- package/src/self-implement/gate-env-retry.ts +83 -0
- package/src/self-implement/goal-author-clarification.ts +88 -13
- package/src/self-implement/goal-clarification-escalation.ts +2 -0
- package/src/self-implement/orchestrator.ts +220 -6
- package/src/self-implement/run-stop.ts +123 -0
- package/src/self-implement/run-ttl.ts +258 -0
- package/src/self-implement/salvage-branches.ts +254 -0
- package/src/self-implement/salvage-retention.ts +123 -0
- package/src/self-implement/seams.ts +4 -0
- package/src/self-implement/self-implement-cli.ts +2 -0
- package/src/self-implement/sibling-resync.ts +223 -0
- package/src/self-implement/stop-autoheal.ts +204 -0
- package/src/task-agent/actions.ts +63 -2
- package/src/task-agent/board.ts +132 -0
- package/src/task-agent/card-evidence.ts +292 -0
- package/src/task-agent/completion-evidence.ts +126 -0
- package/src/task-agent/judge.ts +25 -0
- package/src/task-agent/live-moves.ts +276 -0
- package/src/task-agent/mission.ts +33 -7
- package/src/task-agent/shadow.ts +192 -15
- package/src/task-agent/task-hand.ts +124 -17
- package/src/task-orchestrator/surfaces/pod-command-job.ts +82 -8
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +4 -0
- package/src/task-orchestrator/surfaces/pod-install-slots.ts +89 -0
- package/src/task-orchestrator/surfaces/pod-lease.ts +233 -21
- package/src/task-orchestrator/surfaces/pod-pool.ts +155 -34
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +21 -5
- package/src/telegram-away-command.ts +38 -0
- package/src/telegram-commands.ts +52 -0
- package/src/telegram-report-log-probe.ts +21 -0
- package/src/telegram-report.ts +27 -3
- package/src/testing/real-cli-spawn.ts +106 -0
- package/src/tui-client/dashboard-session.ts +1 -0
- package/src/user-config.ts +190 -5
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3857-4f936cdb30322aa6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4554-5761612531d0146b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5977-a11f37254196b659.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8401-8615a660c84cdb03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +0 -25
- package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/a9f2b54d-7865aba0f4d2ee49.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-a0eb02638b1e6f31.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-c39554c321576982.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-c130b63862437814.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-a3c58f5c72a5f3f9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-e8e1ecaea7cd71d2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-2da6f9b8a38c6978.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-44d5f767491692bf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-61553851aa606146.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-a1f585e154e61c59.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-5150ef17b5383a57.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +0 -1
- package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.css +0 -1
- /package/apps/pwa/out/_next/static/{P6YTVgzuZz5oTZhY4Din_ → Bam1YziK2iH9bN4K0utGk}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{P6YTVgzuZz5oTZhY4Din_ → Bam1YziK2iH9bN4K0utGk}/_ssgManifest.js +0 -0
|
@@ -7,6 +7,13 @@ import { join } from 'node:path';
|
|
|
7
7
|
import { dlopen } from 'bun:ffi';
|
|
8
8
|
import { hostLeaseBaseDir, type PendingJobIdentity } from '../../pod-lease/host-lease.js';
|
|
9
9
|
|
|
10
|
+
/** The harness Pod's container requests (self-implement-pod Job manifest). Lives here so admission reuses the real request. */
|
|
11
|
+
export const POD_CHILD_REQUESTS = { cpu: '1', memory: '6Gi' } as const;
|
|
12
|
+
/** POD-ADMIT-BY-USAGE — observed usage is scaled by this before it reserves memory. */
|
|
13
|
+
const POD_USAGE_HEADROOM = 1.5;
|
|
14
|
+
/** POD-ADMIT-BY-USAGE — a node keeps at least this share of allocatable memory free (real usage and bookkeeping). */
|
|
15
|
+
const POD_NODE_FREE_FLOOR_RATIO = 0.1;
|
|
16
|
+
|
|
10
17
|
/** Persisted on a Job admitted through the host lease; the file lease remains until its Pod is observed Running or terminal. */
|
|
11
18
|
export const POD_HOST_LEASE_ANNOTATION = 'elanous.dev/host-lease-admitted';
|
|
12
19
|
|
|
@@ -39,6 +46,39 @@ export interface PodLeaseMember {
|
|
|
39
46
|
/** Node-by-node memory left after all assigned Pod requests and harness Running limits. */
|
|
40
47
|
availableMemoryByNodeBytes: number[] | null;
|
|
41
48
|
reason: string | null;
|
|
49
|
+
/**
|
|
50
|
+
* POD-ADMIT-BY-USAGE (only when measured with `measureUsage`): observed memory of Running harness Pods
|
|
51
|
+
* (`kubectl top pods`). null = metrics unreadable (see usageReason) — every Pod is then reserved at its limit, as before.
|
|
52
|
+
*/
|
|
53
|
+
harnessUsageBytes?: number[] | null;
|
|
54
|
+
usageReason?: string | null;
|
|
55
|
+
/** GATE-RESERVE-AUTO — non-terminal release-gate shard Jobs (`gate-<uuid>`, kind=command). */
|
|
56
|
+
gateJobs?: number | null;
|
|
57
|
+
/** GATE-RESERVE-AUTO — of those, Jobs whose Pod is not bound to a node yet (not created, or Pending unscheduled). */
|
|
58
|
+
gateUnboundJobs?: number | null;
|
|
59
|
+
/** GATE-RESERVE-AUTO — CPU requested by the non-terminal gate Jobs' Pod templates (observability; Infinity: unreadable). */
|
|
60
|
+
gateCpuMillicores?: number | null;
|
|
61
|
+
/** GATE-RESERVE-AUTO — allocatable CPU of the schedulable (Ready, untainted, cordon-free) nodes. */
|
|
62
|
+
schedulableCpuMillicores?: number | null;
|
|
63
|
+
/** GATE-RESERVE-AUTO — per schedulable node: allocatable CPU − requests of every non-terminal Pod bound to it (gate Pods too). */
|
|
64
|
+
cpuFreeByNodeMillicores?: number[] | null;
|
|
65
|
+
/** GATE-RESERVE-AUTO — CPU requested by harness Pods still waiting for a node (they take CPU somewhere once bound). */
|
|
66
|
+
pendingHarnessCpuMillicores?: number | null;
|
|
67
|
+
/** Pool-wide harness reservation: Σ min(limit, max(request, usage×1.5)), or max(request, limit) when the Pod's usage is unmeasured. */
|
|
68
|
+
memoryReservedBytes?: number | null;
|
|
69
|
+
/** Parallel to availableMemoryByNodeBytes: each schedulable node's allocatable memory. */
|
|
70
|
+
allocatableMemoryByNodeBytes?: number[] | null;
|
|
71
|
+
/** Parallel to availableMemoryByNodeBytes: each node's real memory use (`kubectl top nodes`), null when unmeasured. */
|
|
72
|
+
usedMemoryByNodeBytes?: Array<number | null> | null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** POD-ADMIT-BY-USAGE — how much memory one new goal is counted at, and why. */
|
|
76
|
+
export interface PodAdmissionByUsage {
|
|
77
|
+
samples: number;
|
|
78
|
+
p95Bytes: number | null;
|
|
79
|
+
admitBytes: number;
|
|
80
|
+
/** Why admission stayed on the conservative per-goal size; null when usage drove it. */
|
|
81
|
+
fallback: string | null;
|
|
42
82
|
}
|
|
43
83
|
|
|
44
84
|
export interface PoolLeaseMeasure {
|
|
@@ -61,6 +101,28 @@ export interface PoolLeaseRecommendation {
|
|
|
61
101
|
pendingJobs?: PendingJobIdentity[];
|
|
62
102
|
/** Absent only for injected legacy status fixtures; real measurements always report a number or null. */
|
|
63
103
|
unleasedRunning?: number | null;
|
|
104
|
+
/** POD-ADMIT-BY-USAGE — the per-goal size this recommendation used. */
|
|
105
|
+
admission?: PodAdmissionByUsage;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** `kubectl top … --no-headers` → name → memory bytes; null when any line cannot be read (fail conservative). */
|
|
109
|
+
function topMemory(stdout: string, column: number): Map<string, number> | null {
|
|
110
|
+
const out = new Map<string, number>();
|
|
111
|
+
for (const line of stdout.split('\n')) {
|
|
112
|
+
if (!line.trim()) continue;
|
|
113
|
+
const cols = line.trim().split(/\s+/u);
|
|
114
|
+
const bytes = cols.length > column ? quantity(cols[column]!, 'memory') : null;
|
|
115
|
+
if (bytes === null || out.has(cols[0]!)) return null;
|
|
116
|
+
out.set(cols[0]!, bytes);
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Nearest-rank p95. */
|
|
122
|
+
function p95(values: readonly number[]): number | null {
|
|
123
|
+
if (!values.length) return null;
|
|
124
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
125
|
+
return sorted[Math.max(0, Math.ceil(sorted.length * 0.95) - 1)]!;
|
|
64
126
|
}
|
|
65
127
|
|
|
66
128
|
/** Kubernetes quantity to bytes (memory) or millicores (CPU). */
|
|
@@ -88,16 +150,19 @@ function object(value: unknown): Record<string, unknown> | null {
|
|
|
88
150
|
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
|
89
151
|
}
|
|
90
152
|
|
|
91
|
-
/**
|
|
92
|
-
|
|
153
|
+
/**
|
|
154
|
+
* Effective scheduling reservation: max(sum of app requests, largest init request, Pod-level request) + overhead.
|
|
155
|
+
* A container without a request reserves its limit (Kubernetes defaults the request to the limit), or 0 with neither.
|
|
156
|
+
*/
|
|
157
|
+
function requestedResource(spec: Record<string, unknown>, unit: 'memory' | 'cpu'): number | null {
|
|
93
158
|
const request = (resources: unknown): number | null => {
|
|
94
159
|
const r = object(resources);
|
|
95
160
|
if (resources !== undefined && !r) return null;
|
|
96
161
|
const requests = object(r?.requests);
|
|
97
162
|
const limits = object(r?.limits);
|
|
98
163
|
if ((r?.requests !== undefined && !requests) || (r?.limits !== undefined && !limits)) return null;
|
|
99
|
-
const raw = requests?.
|
|
100
|
-
return raw === undefined ? 0 : typeof raw === 'string' ? quantity(raw,
|
|
164
|
+
const raw = requests?.[unit] ?? limits?.[unit];
|
|
165
|
+
return raw === undefined ? 0 : typeof raw === 'string' ? quantity(raw, unit) : null;
|
|
101
166
|
};
|
|
102
167
|
const containers = spec.containers;
|
|
103
168
|
if (!Array.isArray(containers) || !containers.length) return null;
|
|
@@ -117,12 +182,16 @@ function requestedMemory(spec: Record<string, unknown>): number | null {
|
|
|
117
182
|
init = Math.max(init, v);
|
|
118
183
|
}
|
|
119
184
|
const pod = spec.resources === undefined ? 0 : request(spec.resources);
|
|
120
|
-
const overheadRaw = object(spec.overhead)?.
|
|
185
|
+
const overheadRaw = object(spec.overhead)?.[unit];
|
|
121
186
|
const overhead = spec.overhead !== undefined && !object(spec.overhead) ? null
|
|
122
|
-
: overheadRaw === undefined ? 0 : typeof overheadRaw === 'string' ? quantity(overheadRaw,
|
|
187
|
+
: overheadRaw === undefined ? 0 : typeof overheadRaw === 'string' ? quantity(overheadRaw, unit) : null;
|
|
123
188
|
return pod === null || overhead === null ? null : Math.max(app, init, pod) + overhead;
|
|
124
189
|
}
|
|
125
190
|
|
|
191
|
+
const requestedMemory = (spec: Record<string, unknown>): number | null => requestedResource(spec, 'memory');
|
|
192
|
+
/** GATE-RESERVE-AUTO — the CPU the scheduler reserves for a Pod (same rule as memory). */
|
|
193
|
+
const requestedCpu = (spec: Record<string, unknown>): number | null => requestedResource(spec, 'cpu');
|
|
194
|
+
|
|
126
195
|
function runningLimit(spec: Record<string, unknown>): number | null {
|
|
127
196
|
if (!Array.isArray(spec.containers) || !spec.containers.length) return null;
|
|
128
197
|
let total = 0;
|
|
@@ -238,7 +307,7 @@ function runPoolDnsProbe(context: string, kubectl: PoolKubectl): PoolDnsProbe {
|
|
|
238
307
|
}
|
|
239
308
|
|
|
240
309
|
/** Each cluster reads resource reservations; DNS requires a bounded disposable Pod. */
|
|
241
|
-
export function measurePoolLease(members: readonly PodPoolMember[], deps: { kubectl?: PoolKubectl; dns?: (context: string) => PoolDnsProbe; /** Read-only snapshots must not create the disposable DNS Pod or update its cache. */ skipDnsProbe?: boolean } = {}): PoolLeaseMeasure {
|
|
310
|
+
export function measurePoolLease(members: readonly PodPoolMember[], deps: { kubectl?: PoolKubectl; dns?: (context: string) => PoolDnsProbe; /** Read-only snapshots must not create the disposable DNS Pod or update its cache. */ skipDnsProbe?: boolean; /** POD-ADMIT-BY-USAGE — also read `kubectl top` so Running harness Pods reserve by observed usage. */ measureUsage?: boolean } = {}): PoolLeaseMeasure {
|
|
242
311
|
const kubectl = deps.kubectl ?? leaseKubectl;
|
|
243
312
|
return { members: members.map((member) => {
|
|
244
313
|
const base = ['--context', member.context, '--request-timeout=10s'];
|
|
@@ -249,12 +318,34 @@ export function measurePoolLease(members: readonly PodPoolMember[], deps: { kube
|
|
|
249
318
|
// All namespaces are necessary: unrelated workloads also reserve memory on these nodes.
|
|
250
319
|
const pods = kubectl([...base, 'get', 'pods', '--all-namespaces', '-o', 'json']);
|
|
251
320
|
const reasons: string[] = [];
|
|
321
|
+
// POD-ADMIT-BY-USAGE: metrics are optional — a failed `top` leaves every reservation at today's limit.
|
|
322
|
+
let podUsage: Map<string, number> | null = null;
|
|
323
|
+
let nodeUsage: Map<string, number> | null = null;
|
|
324
|
+
if (deps.measureUsage) {
|
|
325
|
+
const top = kubectl([...base, '-n', 'elanous-test', 'top', 'pods', '--no-headers']);
|
|
326
|
+
podUsage = top.status === 0 ? topMemory(top.stdout, 2) : null;
|
|
327
|
+
result.usageReason = podUsage ? null
|
|
328
|
+
: `top pods unavailable: ${top.status === 0 ? 'unparseable output' : top.stderr.trim().split('\n').pop() || `rc=${top.status}`}`.slice(0, 160);
|
|
329
|
+
if (podUsage) {
|
|
330
|
+
const topNodes = kubectl([...base, 'top', 'nodes', '--no-headers']);
|
|
331
|
+
nodeUsage = topNodes.status === 0 ? topMemory(topNodes.stdout, 3) : null;
|
|
332
|
+
// The node real-usage floor needs node metrics: without them stay on today's limit reservations.
|
|
333
|
+
if (!nodeUsage) {
|
|
334
|
+
podUsage = null;
|
|
335
|
+
result.usageReason = `top nodes unavailable: ${topNodes.status === 0 ? 'unparseable output' : topNodes.stderr.trim().split('\n').pop() || `rc=${topNodes.status}`}`.slice(0, 160);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const harnessUsage: number[] = [];
|
|
340
|
+
const nodeAllocatable = new Map<string, number>();
|
|
252
341
|
const parsedItems = (response: ReturnType<PoolKubectl>): Record<string, unknown>[] | null => {
|
|
253
342
|
if (response.status !== 0) return null;
|
|
254
343
|
try { return items(JSON.parse(response.stdout)); } catch { return null; }
|
|
255
344
|
};
|
|
256
345
|
const nodeItems = parsedItems(nodes);
|
|
257
346
|
const nodeFree = new Map<string, number>();
|
|
347
|
+
let schedulableCpu = 0;
|
|
348
|
+
const nodeCpuFree = new Map<string, number>();
|
|
258
349
|
const nodeNames = new Set<string>();
|
|
259
350
|
if (!nodeItems?.length) reasons.push(`cluster nodes: ${nodes.status === 0 ? 'invalid/empty response' : nodes.stderr.trim().split('\n').pop() || `rc=${nodes.status}`}`);
|
|
260
351
|
else {
|
|
@@ -280,12 +371,15 @@ export function measurePoolLease(members: readonly PodPoolMember[], deps: { kube
|
|
|
280
371
|
}
|
|
281
372
|
nodeNames.add(name);
|
|
282
373
|
const repels = taintList.some((t) => t!.effect === 'NoSchedule' || t!.effect === 'NoExecute');
|
|
283
|
-
if (spec?.unschedulable !== true && !repels && object(ready[0])?.status === 'True') nodeFree.set(name, mem);
|
|
374
|
+
if (spec?.unschedulable !== true && !repels && object(ready[0])?.status === 'True') { nodeFree.set(name, mem); nodeAllocatable.set(name, mem); nodeCpuFree.set(name, cores); schedulableCpu += cores; }
|
|
284
375
|
memory += mem; cpu += cores;
|
|
285
376
|
}
|
|
286
|
-
if (valid) { result.allocatableMemoryBytes = memory; result.allocatableCpuMillicores = cpu; }
|
|
287
|
-
else { nodeFree.clear(); nodeNames.clear(); reasons.push('cluster nodes: invalid name/allocatable memory/cpu/readiness'); }
|
|
377
|
+
if (valid) { result.allocatableMemoryBytes = memory; result.allocatableCpuMillicores = cpu; result.schedulableCpuMillicores = schedulableCpu; }
|
|
378
|
+
else { nodeFree.clear(); nodeAllocatable.clear(); nodeCpuFree.clear(); nodeNames.clear(); reasons.push('cluster nodes: invalid name/allocatable memory/cpu/readiness'); }
|
|
288
379
|
}
|
|
380
|
+
// POD-ADMIT-BY-USAGE: every schedulable node needs a real-usage reading, or the member stays on limits.
|
|
381
|
+
const unreadNode = podUsage && nodeUsage ? [...nodeFree.keys()].find((n) => !nodeUsage!.has(n)) : undefined;
|
|
382
|
+
if (unreadNode !== undefined) { podUsage = null; result.usageReason = `top nodes: no reading for ${unreadNode}`.slice(0, 160); }
|
|
289
383
|
const jobItems = parsedItems(jobs);
|
|
290
384
|
const podItems = parsedItems(pods);
|
|
291
385
|
if (!jobItems) reasons.push(`cluster jobs: ${jobs.status === 0 ? 'invalid response' : jobs.stderr.trim().split('\n').pop() || `rc=${jobs.status}`}`);
|
|
@@ -293,6 +387,8 @@ export function measurePoolLease(members: readonly PodPoolMember[], deps: { kube
|
|
|
293
387
|
if (podItems && jobItems) {
|
|
294
388
|
const jobNames = new Set<string>();
|
|
295
389
|
const leasedJobs = new Set<string>();
|
|
390
|
+
const gateJobs = new Map<string, number>();
|
|
391
|
+
const boundGateJobs = new Set<string>();
|
|
296
392
|
let jobsValid = true;
|
|
297
393
|
for (const job of jobItems) {
|
|
298
394
|
const meta = object(job.metadata);
|
|
@@ -300,12 +396,27 @@ export function measurePoolLease(members: readonly PodPoolMember[], deps: { kube
|
|
|
300
396
|
if (object(meta.labels)?.['elanous.substrate'] !== 'pod') continue;
|
|
301
397
|
if (typeof meta.name !== 'string' || !meta.name) { jobsValid = false; continue; }
|
|
302
398
|
jobNames.add(meta.name);
|
|
399
|
+
// GATE-RESERVE-AUTO: a release-gate shard Job (`gate-<uuid>`, kind=command) reserves its Pod template's CPU
|
|
400
|
+
// from creation until it is terminal — also while its Pod is still Pending or not created yet.
|
|
401
|
+
if (meta.name.startsWith('gate-') && object(meta.labels)?.['elanous.kind'] === 'command') {
|
|
402
|
+
const status = object(job.status);
|
|
403
|
+
const conditions = Array.isArray(status?.conditions) ? status.conditions as unknown[] : [];
|
|
404
|
+
const terminal = !!status?.completionTime || Number(status?.succeeded) > 0 ||
|
|
405
|
+
conditions.some((c) => (object(c)?.type === 'Failed' || object(c)?.type === 'Complete') && object(c)?.status === 'True');
|
|
406
|
+
if (!terminal) {
|
|
407
|
+
const spec = object(object(object(job.spec)?.template)?.spec);
|
|
408
|
+
// An unreadable request cannot be reserved as 0: it blocks harness admission until the gate ends.
|
|
409
|
+
gateJobs.set(meta.name, (spec ? requestedCpu(spec) : null) ?? Number.POSITIVE_INFINITY);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
303
412
|
if (object(meta.annotations)?.[POD_HOST_LEASE_ANNOTATION] === 'true') leasedJobs.add(meta.name);
|
|
304
413
|
}
|
|
305
414
|
if (!jobsValid) reasons.push('cluster jobs: missing name/labels');
|
|
306
|
-
let running = 0, pending = 0, unleasedRunning = 0, limits = 0, limitsValid = true, reservationsValid = nodeItems !== null && nodeNames.size === nodeItems.length;
|
|
415
|
+
let running = 0, pending = 0, unleasedRunning = 0, limits = 0, reserved = 0, limitsValid = true, reservationsValid = nodeItems !== null && nodeNames.size === nodeItems.length;
|
|
307
416
|
const pendingJobs = new Map<string, PendingJobIdentity>();
|
|
308
417
|
let unknownPhase = false;
|
|
418
|
+
let pendingHarnessCpu = 0;
|
|
419
|
+
let cpuValid = true;
|
|
309
420
|
const seenPods = new Set<string>();
|
|
310
421
|
for (const pod of podItems) {
|
|
311
422
|
const metadata = object(pod.metadata);
|
|
@@ -320,10 +431,13 @@ export function measurePoolLease(members: readonly PodPoolMember[], deps: { kube
|
|
|
320
431
|
if (seenPods.has(identity)) { reservationsValid = false; continue; }
|
|
321
432
|
seenPods.add(identity);
|
|
322
433
|
} else reservationsValid = false;
|
|
323
|
-
const
|
|
434
|
+
const ownerJob = String(labels?.['elanous.job'] ?? labels?.['job-name'] ?? '');
|
|
435
|
+
const gatePod = metadata?.namespace === 'elanous-test' && gateJobs.has(ownerJob);
|
|
436
|
+
const harness = metadata?.namespace === 'elanous-test' && labels?.['elanous.probe'] !== 'dns' && !gatePod &&
|
|
324
437
|
(labels?.['elanous.substrate'] === 'pod' || jobNames.has(String(labels?.['elanous.job'] ?? '')));
|
|
325
438
|
const spec = object(pod.spec);
|
|
326
439
|
let limit: number | null = null;
|
|
440
|
+
let reserve: number | null = null;
|
|
327
441
|
if (harness) {
|
|
328
442
|
if (phase === 'Pending') {
|
|
329
443
|
pending++;
|
|
@@ -338,22 +452,59 @@ export function measurePoolLease(members: readonly PodPoolMember[], deps: { kube
|
|
|
338
452
|
if (typeof jobName !== 'string' || !leasedJobs.has(jobName)) unleasedRunning++;
|
|
339
453
|
limit = spec ? runningLimit(spec) : null;
|
|
340
454
|
if (limit === null) limitsValid = false;
|
|
341
|
-
else
|
|
455
|
+
else {
|
|
456
|
+
limits += limit;
|
|
457
|
+
// Measured usage: max(request, usage×1.5) capped at the limit. Unmeasured: max(request, limit) (today's node reservation).
|
|
458
|
+
const used = typeof metadata.name === 'string' ? podUsage?.get(metadata.name) : undefined;
|
|
459
|
+
const request = spec ? requestedMemory(spec) : null;
|
|
460
|
+
if (used !== undefined) harnessUsage.push(used);
|
|
461
|
+
reserve = used !== undefined && request !== null ? Math.min(limit, Math.max(request, Math.ceil(used * POD_USAGE_HEADROOM))) : Math.max(request ?? 0, limit);
|
|
462
|
+
reserved += reserve;
|
|
463
|
+
}
|
|
342
464
|
}
|
|
343
465
|
}
|
|
344
466
|
// Pending Pods with no assigned node have no node reservation yet. They already occupy a pool slot.
|
|
345
467
|
const nodeName = spec?.nodeName;
|
|
468
|
+
// GATE-RESERVE-AUTO: CPU is placed per node (a free core split across two nodes is not one slot). Every Pod
|
|
469
|
+
// bound to a schedulable node — gate Pods too — takes its request there; harness Pods without a node yet
|
|
470
|
+
// take CPU somewhere once bound. A gate Pod bound to any node marks its Job as placed.
|
|
471
|
+
const boundNode = typeof nodeName === 'string' && nodeName !== '' ? nodeName : null;
|
|
472
|
+
if (gatePod && boundNode !== null && nodeNames.has(boundNode)) boundGateJobs.add(ownerJob);
|
|
473
|
+
if ((boundNode !== null && nodeCpuFree.has(boundNode)) || (boundNode === null && harness && phase === 'Pending')) {
|
|
474
|
+
const cpu = spec ? requestedCpu(spec) : null;
|
|
475
|
+
if (cpu === null) cpuValid = false;
|
|
476
|
+
else if (boundNode !== null) nodeCpuFree.set(boundNode, nodeCpuFree.get(boundNode)! - cpu);
|
|
477
|
+
else pendingHarnessCpu += cpu;
|
|
478
|
+
}
|
|
346
479
|
if ((nodeName === undefined || nodeName === '') && phase === 'Pending' && spec && typeof metadata?.namespace === 'string' &&
|
|
347
480
|
requestedMemory(spec) !== null) continue;
|
|
348
481
|
const requested = spec ? requestedMemory(spec) : null;
|
|
349
482
|
if (typeof nodeName !== 'string' || !nodeNames.has(nodeName) || requested === null) { reservationsValid = false; continue; }
|
|
350
|
-
if (nodeFree.has(nodeName)) nodeFree.set(nodeName, nodeFree.get(nodeName)! - Math.max(requested, limit ?? 0));
|
|
483
|
+
if (nodeFree.has(nodeName)) nodeFree.set(nodeName, nodeFree.get(nodeName)! - Math.max(requested, reserve ?? limit ?? 0));
|
|
351
484
|
}
|
|
352
485
|
if (unknownPhase) reasons.push('cluster pods: missing/Unknown phase');
|
|
486
|
+
if (jobsValid && !unknownPhase && result.schedulableCpuMillicores !== undefined) {
|
|
487
|
+
result.gateJobs = gateJobs.size;
|
|
488
|
+
result.gateUnboundJobs = [...gateJobs.keys()].filter((job) => !boundGateJobs.has(job)).length;
|
|
489
|
+
result.gateCpuMillicores = [...gateJobs.values()].reduce((sum, cpu) => sum + cpu, 0);
|
|
490
|
+
}
|
|
491
|
+
if (cpuValid && !unknownPhase && result.schedulableCpuMillicores !== undefined) {
|
|
492
|
+
result.cpuFreeByNodeMillicores = [...nodeCpuFree.values()];
|
|
493
|
+
result.pendingHarnessCpuMillicores = pendingHarnessCpu;
|
|
494
|
+
}
|
|
353
495
|
if (jobsValid && !unknownPhase) { result.running = running; result.pending = pending; result.pendingJobs = [...pendingJobs.values()]; result.unleasedRunning = unleasedRunning; }
|
|
354
|
-
if (limitsValid && jobsValid && !unknownPhase)
|
|
496
|
+
if (limitsValid && jobsValid && !unknownPhase) {
|
|
497
|
+
result.memoryLimitBytes = limits;
|
|
498
|
+
if (deps.measureUsage) { result.memoryReservedBytes = reserved; result.harnessUsageBytes = podUsage ? harnessUsage : null; }
|
|
499
|
+
}
|
|
355
500
|
else if (!limitsValid) reasons.push('cluster pods: Running Pod memory limit missing/invalid');
|
|
356
|
-
if (reservationsValid && limitsValid && jobsValid && !unknownPhase)
|
|
501
|
+
if (reservationsValid && limitsValid && jobsValid && !unknownPhase) {
|
|
502
|
+
result.availableMemoryByNodeBytes = [...nodeFree.values()];
|
|
503
|
+
if (deps.measureUsage) {
|
|
504
|
+
result.allocatableMemoryByNodeBytes = [...nodeFree.keys()].map((n) => nodeAllocatable.get(n)!);
|
|
505
|
+
result.usedMemoryByNodeBytes = [...nodeFree.keys()].map((n) => nodeUsage?.get(n) ?? null);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
357
508
|
else if (!reservationsValid && !unknownPhase) reasons.push('cluster reservations: node assignment or memory request/limit missing/invalid');
|
|
358
509
|
}
|
|
359
510
|
result.reason = reasons.length ? reasons.join('; ').slice(0, 240) : null;
|
|
@@ -366,18 +517,44 @@ export function measurePoolLease(members: readonly PodPoolMember[], deps: { kube
|
|
|
366
517
|
} catch (error) {
|
|
367
518
|
result.running = null; result.pending = null; result.unleasedRunning = null; result.memoryLimitBytes = null;
|
|
368
519
|
result.allocatableMemoryBytes = null; result.allocatableCpuMillicores = null; result.availableMemoryByNodeBytes = null;
|
|
520
|
+
if (deps.measureUsage) { result.memoryReservedBytes = null; result.harnessUsageBytes = null; result.allocatableMemoryByNodeBytes = null; result.usedMemoryByNodeBytes = null; }
|
|
369
521
|
result.reason = `cluster: ${error instanceof Error ? error.message : String(error)}`.split('\n')[0]!.slice(0, 240);
|
|
370
522
|
}
|
|
371
523
|
return result;
|
|
372
524
|
}) };
|
|
373
525
|
}
|
|
374
526
|
|
|
527
|
+
/**
|
|
528
|
+
* GATE-RESERVE-AUTO — while a release-gate shard Job is not terminal, how many more harness Pods (each requesting
|
|
529
|
+
* `POD_CHILD_REQUESTS.cpu`) fit beside it. The gate gets its CPU first: while any gate shard is still waiting for a
|
|
530
|
+
* node, no harness Pod is admitted (the scheduler may place new harness Pods where the shard would have gone). Once
|
|
531
|
+
* every shard is bound its CPU sits on its node, and the answer is Σ per-node ⌊free ÷ request⌋ minus the harness Pods
|
|
532
|
+
* still waiting for a node. `undefined`: no gate Job is active (normal admission) · `null`: a gate is active but CPU
|
|
533
|
+
* could not be measured — a Pod's or a gate template's request is unreadable (admit nothing more). The count comes from this one reading, never from earlier ones.
|
|
534
|
+
*/
|
|
535
|
+
export function harnessCpuSlotsBesideGate(measure: PoolLeaseMeasure): number | null | undefined {
|
|
536
|
+
if (!measure.members.some((m) => (m.gateJobs ?? 0) > 0)) return undefined;
|
|
537
|
+
const perPod = quantity(POD_CHILD_REQUESTS.cpu, 'cpu')!;
|
|
538
|
+
let slots = 0;
|
|
539
|
+
for (const m of measure.members) {
|
|
540
|
+
if (m.gateJobs == null || m.gateUnboundJobs == null || m.cpuFreeByNodeMillicores == null || m.pendingHarnessCpuMillicores == null) return null;
|
|
541
|
+
// A gate Job whose Pod template CPU cannot be read is unmeasured even when its Pod is bound (fail closed).
|
|
542
|
+
if (m.gateCpuMillicores == null || !Number.isFinite(m.gateCpuMillicores)) return null;
|
|
543
|
+
if (m.gateUnboundJobs > 0) return 0;
|
|
544
|
+
const placeable = m.cpuFreeByNodeMillicores.reduce((sum, free) => sum + Math.max(0, Math.floor(free / perPod)), 0);
|
|
545
|
+
slots += Math.max(0, placeable - Math.ceil(m.pendingHarnessCpuMillicores / perPod));
|
|
546
|
+
}
|
|
547
|
+
return slots;
|
|
548
|
+
}
|
|
549
|
+
|
|
375
550
|
/** Per-node memory and per-member slots cannot be exchanged across nodes/clusters. */
|
|
376
551
|
export function recommendConcurrency(measure: PoolLeaseMeasure, options: { capacity: number; perGoalMemory?: string; liteGoalMemory?: string; accounts: number; perAccount: number }): PoolLeaseRecommendation {
|
|
377
552
|
const empty: PoolLeaseRecommendation = { recommended: null, limitedBy: null, reason: null, capacitySlots: null, memorySlots: null, liteMemorySlots: null, placeableSlots: null, accountSlots: null, running: null, pending: null, unleasedRunning: null };
|
|
378
553
|
const goalBytes = quantity(options.perGoalMemory ?? '16Gi', 'memory');
|
|
379
554
|
if (goalBytes === null || goalBytes <= 0) return { ...empty, reason: '측정 불가: perGoalMemory' };
|
|
555
|
+
const admission = admissionByUsage(measure, goalBytes, options.perGoalMemory !== undefined);
|
|
380
556
|
if (!measure.members.length || measure.members.some((m) => m.running === null || m.pending === null || m.unleasedRunning === null || m.allocatableMemoryBytes === null || m.memoryLimitBytes === null || m.allocatableCpuMillicores === null || m.availableMemoryByNodeBytes === null)) {
|
|
557
|
+
debug.log('pod-lease', 'admit-by-usage', { samples: admission.samples, p95Bytes: admission.p95Bytes, admitBytes: admission.admitBytes, fallback: admission.fallback, recommended: null, limitedBy: null });
|
|
381
558
|
return { ...empty, reason: `측정 불가: cluster${measure.members.map((m) => m.reason ? ` ${m.context}: ${m.reason}` : '').join('')}` };
|
|
382
559
|
}
|
|
383
560
|
if (!Number.isSafeInteger(options.capacity) || options.capacity < 0) return { ...empty, reason: '측정 불가: capacity' };
|
|
@@ -392,12 +569,25 @@ export function recommendConcurrency(measure: PoolLeaseMeasure, options: { capac
|
|
|
392
569
|
const eligibleOccupied = measure.members.reduce((sum, m) => sum + (m.reason === 'dns' ? 0 : Math.min(m.capacity, m.running! + m.pending!)), 0);
|
|
393
570
|
const capacitySlots = Math.max(0, Math.min(options.capacity - eligibleOccupied,
|
|
394
571
|
memberFree.reduce((sum, free) => sum + free, 0)));
|
|
395
|
-
|
|
396
|
-
|
|
572
|
+
// The goal size may fall back to the conservative size pool-wide; the node floor is separate and follows each
|
|
573
|
+
// member: once its Running Pods reserve by usage (usage read), its nodes keep 10% free in bookkeeping and real use.
|
|
574
|
+
const admitBytes = admission.admitBytes;
|
|
575
|
+
const usageRead = (m: PodLeaseMember) => Array.isArray(m.harnessUsageBytes);
|
|
576
|
+
const memberMemorySlots = measure.members.map((m) => m.availableMemoryByNodeBytes!.reduce((sum, bytes, i) => {
|
|
577
|
+
if (!usageRead(m)) return sum + Math.max(0, Math.floor(bytes / admitBytes));
|
|
578
|
+
const allocatable = m.allocatableMemoryByNodeBytes?.[i];
|
|
579
|
+
const used = m.usedMemoryByNodeBytes?.[i];
|
|
580
|
+
// A usage-read member always carries both per-node readings; anything missing admits nothing on that node.
|
|
581
|
+
if (typeof allocatable !== 'number' || typeof used !== 'number') return sum;
|
|
582
|
+
const floor = allocatable * POD_NODE_FREE_FLOOR_RATIO;
|
|
583
|
+
const slots = Math.min(Math.floor((bytes - floor) / admitBytes), Math.floor((allocatable - used - floor) / admitBytes));
|
|
584
|
+
return sum + Math.max(0, slots);
|
|
585
|
+
}, 0));
|
|
397
586
|
// Preserve the requested allocatable-minus-harness-limit metric. The node-local
|
|
398
587
|
// placement bound additionally accounts for every namespace's reservations.
|
|
399
|
-
const memorySlots = measure.members.reduce((sum, m) => sum + (m.reason === 'dns' ? 0 : Math.max(0,
|
|
400
|
-
Math.floor((m.allocatableMemoryBytes! - m.memoryLimitBytes!) /
|
|
588
|
+
const memorySlots = measure.members.reduce((sum, m) => sum + (m.reason === 'dns' ? 0 : Math.max(0, usageRead(m)
|
|
589
|
+
? Math.floor((m.allocatableMemoryBytes! * (1 - POD_NODE_FREE_FLOOR_RATIO) - (m.memoryReservedBytes ?? m.memoryLimitBytes!)) / admitBytes)
|
|
590
|
+
: Math.floor((m.allocatableMemoryBytes! - m.memoryLimitBytes!) / admitBytes))), 0);
|
|
401
591
|
const placeableSlots = measure.members.reduce((sum, m, i) => sum + Math.min(
|
|
402
592
|
Math.max(0, m.capacity - m.running! - m.pending!), memberMemorySlots[i]!,
|
|
403
593
|
), 0);
|
|
@@ -411,5 +601,27 @@ export function recommendConcurrency(measure: PoolLeaseMeasure, options: { capac
|
|
|
411
601
|
const recommended = Math.max(0, Math.min(capacitySlots, memoryBound) - eligibleUnleasedRunning);
|
|
412
602
|
const limitedBy = memoryBound < capacitySlots ? 'memory' : 'capacity';
|
|
413
603
|
if (unleasedRunning > 0) debug.log('pod-lease', 'unleased', { unleasedRunning, running, recommended });
|
|
414
|
-
|
|
604
|
+
debug.log('pod-lease', 'admit-by-usage', { samples: admission.samples, p95Bytes: admission.p95Bytes, admitBytes: admission.admitBytes, fallback: admission.fallback, recommended, limitedBy });
|
|
605
|
+
return { recommended, limitedBy, reason: measure.members.some((m) => m.reason === 'dns') ? 'dns' : null, capacitySlots, memorySlots, liteMemorySlots, placeableSlots, accountSlots, running, pending, pendingJobs, unleasedRunning, admission };
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* POD-ADMIT-BY-USAGE — one new goal counts at max(the Pod's memory request, p95 of Running harness Pod usage × 1.5),
|
|
610
|
+
* capped at the per-goal size. No samples (metrics unreadable, nothing running, or an explicit per-goal size such as an
|
|
611
|
+
* OOM retry tier) keeps the conservative per-goal size.
|
|
612
|
+
*/
|
|
613
|
+
function admissionByUsage(measure: PoolLeaseMeasure, goalBytes: number, explicit: boolean): PodAdmissionByUsage {
|
|
614
|
+
const live = measure.members.filter((m) => m.reason !== 'dns');
|
|
615
|
+
const samples = live.flatMap((m) => m.harnessUsageBytes ?? []);
|
|
616
|
+
const p95Bytes = p95(samples);
|
|
617
|
+
const requestBytes = quantity(POD_CHILD_REQUESTS.memory, 'memory')!;
|
|
618
|
+
// Any live member whose usage was not read keeps the whole pool on today's per-goal size (fail conservative).
|
|
619
|
+
const fallback = explicit ? 'explicit perGoalMemory'
|
|
620
|
+
: live.every((m) => m.harnessUsageBytes === undefined) ? 'usage not measured'
|
|
621
|
+
: live.some((m) => m.harnessUsageBytes == null) ? (live.map((m) => m.harnessUsageBytes == null ? `${m.context}: ${m.usageReason ?? 'usage not measured'}` : '').filter(Boolean).join('; '))
|
|
622
|
+
: p95Bytes === null ? 'no Running harness Pod usage samples'
|
|
623
|
+
: null;
|
|
624
|
+
const admitBytes = fallback !== null || p95Bytes === null ? goalBytes
|
|
625
|
+
: Math.min(goalBytes, Math.max(requestBytes, Math.ceil(p95Bytes * POD_USAGE_HEADROOM)));
|
|
626
|
+
return { samples: samples.length, p95Bytes, admitBytes, fallback };
|
|
415
627
|
}
|