elanous 0.2.19 → 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/{J5H7BvqgSI8DUjtRRcA_S → Bam1YziK2iH9bN4K0utGk}/_buildManifest.js +1 -1
- package/apps/pwa/out/_next/static/chunks/{4323.c5dfb6f503304775.js → 1268.0840ff91e083fcbd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/1307-77fad80cf54477c6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1612-f60c0c93ec773f81.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1826-5d279da64bbbc2de.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1844-9ec4b1d12f6e80eb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1866-e7a6999f25548b30.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1948.15f356b15a625597.js +5 -0
- 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/{2977-e38442e86289f1d3.js → 211-9c9090c59e2c5907.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/2116-6429c188eb846ab8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2196-4700653d12a44b40.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/2537-6c289376caa88ca1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2549-89c518a4c8ed4d00.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2699-325cc923ba7b8631.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2957-96ffab3d521fb08f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/311.e685738f92e6e2ec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3194-807b212fb8eae065.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3324-d1d2979821dfa845.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3558-bbb5ab553a0471d3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3857-995ea9f5ef33d71e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3916-3d02dd1fc257dd3c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4144-fad095b48a1862ee.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4565-3d7e3fdeb43686a8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4795-f372490b178583fc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5360-be1f1d8f9fcc9755.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/{4847.56f5bc454fbcc827.js → 6110.1297d4093fdfbb0d.js} +2 -2
- package/apps/pwa/out/_next/static/chunks/6158.af2e93b7adff709b.js +2 -0
- package/apps/pwa/out/_next/static/chunks/6877-74399f69c7da1433.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1470.98a6ab61ccb4131c.js → 726.83532d69452ee24b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/7290-f09c928a06c52e2d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{6062-73e68343a9814f6a.js → 7327-2d69fb321ba73389.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{8870-9614a213b990b10b.js → 7748-6e51bfe927019f5c.js} +3 -3
- package/apps/pwa/out/_next/static/chunks/7797-df5aa0381fdc078f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8060-cc008cc6fedfa818.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8085-d065d0393c1760f5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8401-35e94e9091f5eb8d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8682-7061dd4fa7bd62ed.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8695-846034ce44852c70.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9070.067c106528ec1ae6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9103-60fbef33e66f0a46.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9247-c1753a059e7bac08.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{7647.2fcd30d894d5ee30.js → 935.4ddaa99fc1d1126c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{ae787e4c-e5b1ef00b2dbceca.js → a9f2b54d-33c0689b70ed8cf3.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-c1a96669158471ec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-c855c24de7db74fe.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-85b316bb4235980b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-9b2b26a33cb2cd42.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-c9955fde67b1bcb5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-7a852956c74015a3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-c5a5c621be0e04ae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-4ba4abe096b6c2a8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-575d4a8b4f7becb2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-f5d56ff5f45ad164.js +1 -0
- 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-7c058069c5e7fa63.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-dc5da0b8a2edb8a4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/page-466bf0ee16ad1432.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/page-a23bc71713bcc08f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-c1a96669158471ec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/inside/{page-5b29d9b06ceadade.js → page-ed4a6f799f005d09.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-9cd565005b93aca6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-55f0f40c6db95f15.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-474cb5888ea0e8f3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-35bfec2e631d3eae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-9ca21f9ad3208a61.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-f9ccb1b1adbea14d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/page-ec102c9b8b691e37.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/not-found-a9c2cfa8dda922a5.js +1 -0
- 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-db03ae3589a81c9e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-4a6463b97f0ad94f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-75bffecc9aa1004e.js +1 -0
- 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-31d5b875760bb62d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-c26fa10aac6c21c4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-c1a96669158471ec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-68793c6e10331d66.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-46d5201d2207a511.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-d81c07a7b4f618ae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-be1c4e8697f22360.js +1 -0
- 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-0fd895fab9b62c1b.js +1 -0
- 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-34882c84f84021c5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-0506a1e0e4bdb2f5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8046cadf-211ca3c0e00f2b72.js → f6231284-45726550aba08fca.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{2ea96958.e8c7e112de50b067.js → f6b7c9ba.64a895b8b2fedbdc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/framework-5c169757551134ed.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-59fe95fed40c7a01.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-app-4ca74dbbee7057c6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_app-13a0a886df4d97a1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_error-6af8591cd3a3f038.js +1 -0
- package/apps/pwa/out/_next/static/chunks/webpack-c260571513bf43c2.js +1 -0
- package/apps/pwa/out/_next/static/css/c23d959d3ee15e57.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +23 -23
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +23 -23
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +23 -23
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +22 -22
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +22 -22
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +23 -23
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +22 -22
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +23 -23
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +23 -23
- package/apps/pwa/out/decisions/index.html +1 -1
- package/apps/pwa/out/decisions/index.txt +22 -22
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +23 -23
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +23 -23
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +23 -23
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +23 -23
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +22 -22
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +22 -22
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +23 -23
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +23 -23
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +22 -22
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +22 -22
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +22 -22
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +23 -23
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +23 -23
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +22 -22
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +22 -22
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +22 -22
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +22 -22
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +23 -23
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +23 -23
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +22 -22
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +23 -23
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +23 -23
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +23 -23
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +23 -23
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +23 -23
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +22 -22
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +23 -23
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +23 -23
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +23 -23
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +23 -23
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +23 -23
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +23 -23
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +23 -23
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +23 -23
- package/graphs/demo/editor-recipes.yaml +22 -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/land-global-checks.ts +89 -0
- package/scripts/lib/pch-regress-map.ts +2 -1
- package/scripts/release-loop/gate-node.ts +296 -25
- package/scripts/release-loop/gate-timing.ts +116 -7
- 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 +4 -1
- package/scripts/release-loop/unattended-release.ts +10 -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/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/release-watch.ts +20 -3
- package/src/cli/config-drift.ts +513 -0
- package/src/cli/log-event-names.ts +4 -0
- package/src/cli/pr-cli.ts +20 -0
- package/src/cli/release-cli.ts +20 -5
- package/src/cli/retro-cli.ts +78 -0
- package/src/cli/tasks-cli.ts +94 -8
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +5 -4
- package/src/dashboard/slash-runtime/help-from-registry.ts +57 -31
- package/src/domains/outbound-log-probe.ts +6 -1
- package/src/graph-runner/graph-run-api.ts +205 -0
- package/src/graph-runner/runner.ts +11 -1
- package/src/harness/harness-cli-command.ts +419 -52
- package/src/harness/harness-queue.ts +315 -25
- package/src/harness/harness-salvage-cli.ts +62 -0
- package/src/hq/fence-outcomes.ts +63 -0
- package/src/hq/fence-wrapper.ts +33 -0
- package/src/hq/hq.ts +11 -1
- package/src/index.ts +69 -6
- package/src/maturity/feature-maturity.ts +2 -2
- 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 +38 -3
- package/src/nexus/api/loop-edges.ts +4 -2
- package/src/nexus/api/ops-api.ts +22 -7
- package/src/nexus/api/ops-board-route.ts +48 -0
- package/src/onboarding/entry-hints.ts +5 -0
- package/src/onboarding.ts +24 -6
- package/src/release-loop/feature-store.ts +14 -11
- package/src/release-loop/gate-shards.ts +158 -0
- package/src/release-loop/release-schedule.ts +6 -2
- 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/draft-sweep.ts +371 -16
- package/src/self-dev/draft-triage-rules.ts +19 -4
- package/src/self-dev/pr-preview.ts +84 -16
- package/src/self-implement/blocked-draft-policy.ts +101 -0
- package/src/self-implement/execution-clarification.ts +11 -0
- package/src/self-implement/gate-env-retry.ts +83 -0
- package/src/self-implement/orchestrator.ts +218 -5
- package/src/self-implement/run-stop.ts +123 -0
- package/src/self-implement/run-ttl.ts +258 -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/board.ts +1 -1
- package/src/task-agent/card-evidence.ts +99 -10
- package/src/task-agent/judge.ts +7 -0
- package/src/task-agent/live-moves.ts +120 -20
- package/src/task-agent/mission.ts +1 -1
- package/src/task-agent/shadow.ts +10 -1
- package/src/task-agent/task-hand.ts +7 -2
- package/src/task-orchestrator/surfaces/pod-command-job.ts +19 -5
- 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 +93 -10
- package/src/task-orchestrator/surfaces/pod-pool.ts +147 -34
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +19 -3
- package/src/telegram-commands.ts +41 -0
- package/src/testing/real-cli-spawn.ts +106 -0
- package/src/user-config.ts +56 -3
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1157-cba0e8553d35a980.js +0 -1
- package/apps/pwa/out/_next/static/chunks/171-23a877ed11720de9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2336-e830c777eb6bce04.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2375-e061bb24cd2b5cde.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3094.67339318c7f72a3a.js +0 -2
- package/apps/pwa/out/_next/static/chunks/338-ceb956612dcfaa86.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3395-835e4ceaa6335ba4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +0 -5
- package/apps/pwa/out/_next/static/chunks/3441-be82e3595aef3a20.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3689-89905039bf3e7257.js +0 -1
- package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3822-6f268bc26c6a84ac.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4457-d7df81c90f9d0268.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4551-77dadae76f6cbace.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4978.6fd93b974441e87f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5012-f746f6549b869f91.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5791-bb82743fa6299562.js +0 -1
- package/apps/pwa/out/_next/static/chunks/62-5bfed1e23790b745.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6320-9bc8561fde416e07.js +0 -1
- package/apps/pwa/out/_next/static/chunks/649-ca96d1c5d09e02b8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6726-85a7db59b288c53f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6735-33267c4630d0d963.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7633-141682015873beb6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8067-143791231c99bd02.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8430-a10cfba8ef282d4e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9454-12c2f30dea62291c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/976-4a9b4ebdca7c739a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9989-15cdf568bafa9fc8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-deef5ee191830f14.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-b193da485f9a14b2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-b6e1a79df0e214f4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-e84df4d907f703c7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-a5fb9a5db89ae4ea.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-1499f481d7fc52d6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-c7cf753aeb648790.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-4118469e514e10fd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-e968f7630654332a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-505915052f947022.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-c2f962fb4a3b06c6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-7b5ed0bd8a589ec4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-cf8193250dd42dd4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-cf3f5dfa35dea8e9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-a15e7fcc283e9ca7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-d2915e7b70ff3d0e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-b70cd005d9169e25.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-2595f0eba29f57e1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-c776322454156e36.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-77bbd17f4435aa10.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-049689e611ecc936.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-85954a4fb09c44eb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-e773e552e1068ee6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-5633c4bfa7684b4a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-d829b92bd01b4994.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-061c317fc771becf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-3a45ece8865ed4b6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-0be990618c0d56bd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-bfea4865dc244b73.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-0d6ba8e337741484.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-76a88b55d816482e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-bba1542df59f61ce.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-4185c95689bb372e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-b699e1a8d5d34b8b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-c225e338c31bf274.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-449353f70507ca1f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-90b889714102476d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-f1ee7fbeb7f96ddc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-d59a95e1434dfaa0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-71491a23414d1a47.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-de7112bdb65fa4fe.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-feb4d0d88079660d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f17d887b40dd6b1d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-8da77be2192bcd5d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-72c3b9a5e9582b29.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3afe4c8d0c48c39.js +0 -1
- package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-b9308f2d8a931531.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +0 -1
- package/apps/pwa/out/_next/static/chunks/webpack-6f35f9d892fa630b.js +0 -1
- package/apps/pwa/out/_next/static/css/85f53c6f35d11382.css +0 -1
- /package/apps/pwa/out/_next/static/{J5H7BvqgSI8DUjtRRcA_S → Bam1YziK2iH9bN4K0utGk}/_ssgManifest.js +0 -0
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* - @ssh호스트 원격 클러스터면 그 기계 — 이미지 판 대조·반입(`docker load` ⊕ `k3d image import`)에 쓴다
|
|
10
10
|
* - 상한 이 노드에 동시에 둘 Job 수(기본 2)
|
|
11
11
|
* Job 마다 «빈 자리 비율»이 가장 큰 노드를 고른다(동률은 앞 순위 · 측정 실패 노드는 측정된 노드가 다 찼을 때만). 다 차면 자리가 날 때까지 기다린다.
|
|
12
|
+
* POOL-LITE(10-07): 골 종류를 주면 lite·문서·측정 골은 상한이 가장 작은 노드(node-c), 구현 골은 가장 큰 노드(node-b)를 먼저 쓰고,
|
|
13
|
+
* 그 노드가 차면 빈 자리 비율로 넘친다. 상한 0 노드는 «꺼짐» — 해석에서 빠지고 절대 고르지 않는다.
|
|
12
14
|
* 노드 선택의 진행 중 수는 로컬 프로세스 기준이다. 발사 허가는 별도의 lease 측정으로
|
|
13
15
|
* 다른 호스트의 Running/Pending 및 메모리 예약까지 고려한다.
|
|
14
16
|
* 풀을 안 주면 종전처럼 «현재 컨텍스트» 하나다(동작 불변).
|
|
@@ -21,7 +23,7 @@ import { debug } from '../../debug/log.js';
|
|
|
21
23
|
import { PodLeaseAdmission, type PodLeasePredecessor, type PodLeaseRelease } from '../../pod-lease/admission.js';
|
|
22
24
|
import { predecessorState, type PredecessorState } from '../../pod-lease/dependency-state.js';
|
|
23
25
|
import { HostPoolLease, leaseHasPendingPod } from '../../pod-lease/host-lease.js';
|
|
24
|
-
import { measurePoolLease, recommendConcurrency, type PodLeaseMember, type PoolDnsProbe, type PoolLeaseRecommendation } from './pod-lease.js';
|
|
26
|
+
import { harnessCpuSlotsBesideGate, measurePoolLease, recommendConcurrency, type PodLeaseMember, type PoolDnsProbe, type PoolLeaseRecommendation } from './pod-lease.js';
|
|
25
27
|
|
|
26
28
|
export interface PodPoolMember {
|
|
27
29
|
readonly context: string;
|
|
@@ -111,7 +113,9 @@ export function parsePodPool(spec: string): PodPoolMember[] {
|
|
|
111
113
|
const m = MEMBER.exec(part);
|
|
112
114
|
if (!m) throw new Error(`--pod-pool: 못 읽는 노드 「${part}」 — 형식 컨텍스트[@ssh호스트][:상한][#k3d-레지스트리:포트]`);
|
|
113
115
|
const capacity = m[3] === undefined ? 2 : Number(m[3]);
|
|
114
|
-
|
|
116
|
+
// POOL-LITE (10-07): `:0` means «this member is off» — a `node-c:0` entry once threw here and broke every launch.
|
|
117
|
+
// It is dropped below (never chosen); only a pool with no live member left is an error.
|
|
118
|
+
if (!Number.isInteger(capacity) || capacity < 0) throw new Error(`--pod-pool: 상한은 1 이상 — 「${part}」`);
|
|
115
119
|
if (m[5] && Number(m[5]) > 65535) throw new Error(`--pod-pool: 레지스트리 포트는 65535 이하 — 「${part}」`);
|
|
116
120
|
const context = m[1]!;
|
|
117
121
|
const k3dCluster = context.startsWith('k3d-') ? context.slice(4) : 'elanous-pool';
|
|
@@ -119,7 +123,44 @@ export function parsePodPool(spec: string): PodPoolMember[] {
|
|
|
119
123
|
});
|
|
120
124
|
if (members.length === 0) throw new Error('--pod-pool: 노드가 없다');
|
|
121
125
|
if (new Set(members.map((m) => m.context)).size !== members.length) throw new Error('--pod-pool: 컨텍스트가 겹친다');
|
|
122
|
-
|
|
126
|
+
const live = members.filter((m) => m.capacity > 0);
|
|
127
|
+
if (live.length === 0) throw new Error(`--pod-pool: 상한은 1 이상 — 상한 0 이 아닌 노드가 없다 「${spec}」`);
|
|
128
|
+
if (live.length !== members.length) {
|
|
129
|
+
debug.log('pod.pool', 'member-disabled', { members: members.filter((m) => m.capacity <= 0).map((m) => m.context), reason: 'capacity-0' });
|
|
130
|
+
}
|
|
131
|
+
return live;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* POOL-LITE (10-07) — what kind of Job is being placed. `lite` = lite memory tier or a docs/measurement goal;
|
|
136
|
+
* `standard` = an implementation goal. Absent kind keeps the plain free-share placement unchanged.
|
|
137
|
+
*/
|
|
138
|
+
export type PodPlacementKind = 'lite' | 'standard';
|
|
139
|
+
|
|
140
|
+
export interface PodPlacement {
|
|
141
|
+
readonly member: string;
|
|
142
|
+
readonly kind: PodPlacementKind | null;
|
|
143
|
+
/** `preferred` = landed on the kind's preferred member · `overflow` = preferred member(s) full/skipped · `share` = no preference applies. */
|
|
144
|
+
readonly outcome: 'preferred' | 'overflow' | 'share';
|
|
145
|
+
readonly preferred: readonly string[];
|
|
146
|
+
/** One line for launch output and the run ledger. */
|
|
147
|
+
readonly reason: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Preferred members for a placement kind: lite → the smallest-capacity member(s) (node-c), standard → the largest (node-b).
|
|
152
|
+
* Capacity is the operator's own statement of member size (`pool-node-b@node-b:40,pool-node-c@node-c:6`), so no new spec syntax.
|
|
153
|
+
* When every member has the same capacity there is no small/big member and no preference (empty list).
|
|
154
|
+
*/
|
|
155
|
+
export function preferredMembers(members: readonly PodPoolMember[], kind: PodPlacementKind | null | undefined): string[] {
|
|
156
|
+
const live = members.filter((m) => m.capacity > 0);
|
|
157
|
+
if (!kind || live.length < 2) return [];
|
|
158
|
+
const caps = live.map((m) => m.capacity);
|
|
159
|
+
const min = Math.min(...caps);
|
|
160
|
+
const max = Math.max(...caps);
|
|
161
|
+
if (min === max) return [];
|
|
162
|
+
const target = kind === 'lite' ? min : max;
|
|
163
|
+
return live.filter((m) => m.capacity === target).map((m) => m.context);
|
|
123
164
|
}
|
|
124
165
|
|
|
125
166
|
/** 스펙 해석 순서: 명시 인자 → `ELANOUS_POD_POOL` → 없음(null = 현재 컨텍스트 하나). */
|
|
@@ -178,12 +219,21 @@ export function podPoolHostLease(members: readonly PodPoolMember[]): HostPoolLea
|
|
|
178
219
|
export class PodPoolScheduler {
|
|
179
220
|
private readonly inflight = new Map<string, number>();
|
|
180
221
|
private readonly admission: PodLeaseAdmission;
|
|
222
|
+
/**
|
|
223
|
+
* GATE-ADMIT-EXEMPT — the gate's own shard Jobs (GATE-MEM-ADMIT) queue here: memory admission without the
|
|
224
|
+
* GATE-RESERVE-AUTO CPU bound, which exists to protect the gate from harness Pods, not from itself.
|
|
225
|
+
*/
|
|
226
|
+
private readonly gateAdmission: PodLeaseAdmission;
|
|
181
227
|
private readonly outstanding = new Set<PodLeaseRelease>();
|
|
182
228
|
private lastFree = 0;
|
|
183
229
|
private budget = 0;
|
|
230
|
+
/** GATE-RESERVE-AUTO — local permits whose Pod was observed (Running/terminal): already inside the CPU reading. */
|
|
231
|
+
private seenActive = 0;
|
|
184
232
|
private initialized = false;
|
|
185
233
|
private readonly measure: () => Promise<PoolLeaseRecommendation>;
|
|
186
234
|
private lastRaw: PoolLeaseRecommendation | null = null;
|
|
235
|
+
/** GATE-ADMIT-EXEMPT — the same reading before the gate CPU cap (memory/limitedBy unchanged); gate callers read it. */
|
|
236
|
+
private lastUncapped: PoolLeaseRecommendation | null = null;
|
|
187
237
|
private readonly hostLease: HostPoolLease;
|
|
188
238
|
private readonly pollMs: number;
|
|
189
239
|
private readonly occupancy: () => Record<string, MemberOccupancy> | Promise<Record<string, MemberOccupancy>>;
|
|
@@ -192,19 +242,30 @@ export class PodPoolScheduler {
|
|
|
192
242
|
this.pollMs = options.pollMs ?? 15_000;
|
|
193
243
|
// One lease directory per pool (contexts, not caps) — every CLI process launching into the same clusters shares it.
|
|
194
244
|
this.hostLease = options.hostLease ?? podPoolHostLease(members);
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
245
|
+
// GATE-RESERVE-AUTO: the gate reservation is returned beside the recommendation, never folded into it — the
|
|
246
|
+
// delta budget below must not see a gate start/end as a free-slot change (that made the result order-dependent).
|
|
247
|
+
const measureStatus: () => Promise<{ status: PoolLeaseRecommendation; gateSlots: number | null | undefined }> = options.status
|
|
248
|
+
? async () => ({ status: await options.status!(), gateSlots: undefined })
|
|
249
|
+
: async () => {
|
|
250
|
+
// POD-ADMIT-BY-USAGE: admission reads observed Pod usage (`kubectl top`); tryAcquire's occupancy read does not.
|
|
251
|
+
const measure = measurePoolLease(members, { ...(options.kubectl ? { kubectl: options.kubectl } : {}), ...(options.dns ? { dns: options.dns } : {}), measureUsage: true });
|
|
252
|
+
return { status: recommendConcurrency(measure, { capacity: members.reduce((n, m) => n + m.capacity, 0), accounts: 0, perAccount: 0 }), gateSlots: harnessCpuSlotsBesideGate(measure) };
|
|
253
|
+
};
|
|
254
|
+
/** Cluster-level additional slots for the cross-process host lease: the measured free slots, capped by the gate's CPU. */
|
|
255
|
+
const capped = ({ status, gateSlots }: { status: PoolLeaseRecommendation; gateSlots: number | null | undefined }): PoolLeaseRecommendation =>
|
|
256
|
+
gateSlots === undefined || status.recommended === null ? status
|
|
257
|
+
: gateSlots === null ? { ...status, recommended: null, reason: '측정 불가: gate CPU' }
|
|
258
|
+
: { ...status, recommended: Math.min(status.recommended, gateSlots) };
|
|
259
|
+
this.measure = async () => { const reading = await measureStatus(); this.lastUncapped = reading.status; return capped(reading); };
|
|
201
260
|
// An injected status (tests, callers with their own measurement) without memberMemory never shells out to kubectl here.
|
|
202
261
|
this.memberMemory = options.memberMemory ?? (options.status ? (() => []) : () => measurePoolLease(members, { ...(options.kubectl ? { kubectl: options.kubectl } : {}), ...(options.dns ? { dns: options.dns } : {}) }).members
|
|
203
262
|
.map((m: PodLeaseMember) => ({ context: m.context, allocatableMemoryBytes: m.allocatableMemoryBytes })));
|
|
204
263
|
this.occupancy = options.occupancy ?? (() => measureMemberOccupancy(members, { ...(options.kubectl ? { kubectl: options.kubectl } : {}), ...(options.dns ? { dns: options.dns } : {}) }));
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
264
|
+
const decide = async (gate: boolean): Promise<PoolLeaseRecommendation> => {
|
|
265
|
+
const reading = await measureStatus();
|
|
266
|
+
const measured = reading.status;
|
|
267
|
+
this.lastRaw = capped(reading);
|
|
268
|
+
this.lastUncapped = measured;
|
|
208
269
|
const pendingJobs = measured.pendingJobs ?? [];
|
|
209
270
|
const ownStart = this.hostLease.selfStart();
|
|
210
271
|
const transferId = process.env.ELANOUS_POD_RESERVED_LEASE;
|
|
@@ -220,16 +281,40 @@ export class PodPoolScheduler {
|
|
|
220
281
|
this.budget = Math.max(0, this.budget + decision.recommended - (this.initialized ? this.lastFree : 0));
|
|
221
282
|
this.initialized = true;
|
|
222
283
|
this.lastFree = decision.recommended;
|
|
223
|
-
|
|
224
|
-
|
|
284
|
+
// Each queue's limit is its own permits plus the shared remaining budget (every permit already took one from it).
|
|
285
|
+
const active = (gate ? this.gateAdmission : this.admission).snapshot().active;
|
|
286
|
+
const total = active + this.budget;
|
|
287
|
+
if (reading.gateSlots === undefined) return { ...decision, recommended: total };
|
|
288
|
+
if (gate) {
|
|
289
|
+
debug.log('pod.pool', 'gate-reserve', { gateSlots: reading.gateSlots, active, budget: this.budget, recommended: total, exempt: 'gate-caller' });
|
|
290
|
+
return { ...decision, recommended: total };
|
|
291
|
+
}
|
|
292
|
+
// GATE-RESERVE-AUTO: while a gate shard Job is active, harness admission is also bounded by the CPU left after
|
|
293
|
+
// the gate. The bound is absolute (this reading ⊕ current local permits). Pods already in the reading — observed
|
|
294
|
+
// ones (`seenActive`) and ones measured Pending (own leases with a pending Pod) — are not subtracted again;
|
|
295
|
+
// the remaining local permits and other processes' leases without a measured Pod (`others`) are.
|
|
296
|
+
const ownMeasuredPending = this.hostLease.live().filter((r) => r.pid === process.pid && r.startedAt === ownStart && leaseHasPendingPod(r, pendingJobs)).length;
|
|
297
|
+
const unseen = Math.max(0, active - this.seenActive - ownMeasuredPending);
|
|
298
|
+
const cpuFree = reading.gateSlots === null ? 0 : Math.max(0, reading.gateSlots - others + (transferIsOther ? 1 : 0) - unseen);
|
|
299
|
+
const recommended = Math.min(total, active + cpuFree);
|
|
300
|
+
if (recommended < total) debug.log('pod.pool', 'gate-reserve', { gateSlots: reading.gateSlots, active, unseen, ownMeasuredPending, others, budget: this.budget, recommended });
|
|
301
|
+
return reading.gateSlots === null
|
|
302
|
+
? { ...decision, recommended, reason: '측정 불가: gate CPU' }
|
|
303
|
+
: { ...decision, recommended };
|
|
304
|
+
};
|
|
305
|
+
const dependencyMerged = options.dependencyMerged ?? ((after: PodLeasePredecessor, signal?: AbortSignal) => predecessorState(after, {}, signal));
|
|
306
|
+
this.admission = new PodLeaseAdmission({ status: () => decide(false), ...(options.pollMs ? { pollMs: options.pollMs } : {}), dependencyMerged });
|
|
307
|
+
this.gateAdmission = new PodLeaseAdmission({ status: () => decide(true), ...(options.pollMs ? { pollMs: options.pollMs } : {}), dependencyMerged });
|
|
225
308
|
}
|
|
226
309
|
/** FIFO gate within this process, then a host lease so independent CLI processes cannot over-admit the same pool. */
|
|
227
|
-
async acquireAdmission(signal?: AbortSignal, after?: PodLeasePredecessor): Promise<PodLeaseRelease & { applied: (job: string, context: string, namespace: string) => void; observed: () => void }> {
|
|
228
|
-
const
|
|
310
|
+
async acquireAdmission(signal?: AbortSignal, after?: PodLeasePredecessor, opts: { gate?: boolean } = {}): Promise<PodLeaseRelease & { applied: (job: string, context: string, namespace: string) => void; observed: () => void }> {
|
|
311
|
+
const gate = opts.gate === true;
|
|
312
|
+
const local = await this.acquireLocalAdmission(signal, after, gate);
|
|
229
313
|
try {
|
|
230
314
|
for (;;) {
|
|
231
315
|
if (signal?.aborted) throw new Error('pod lease admission aborted');
|
|
232
|
-
|
|
316
|
+
// GATE-ADMIT-EXEMPT: a gate caller is not bounded by its own gate's CPU reservation (memory still bounds it).
|
|
317
|
+
const free = (gate ? this.lastUncapped : this.lastRaw)?.recommended ?? null;
|
|
233
318
|
// Same check as the local gate, but atomic across processes: other authoring and pre-Running leases hold slots.
|
|
234
319
|
const reservedId = process.env.ELANOUS_POD_RESERVED_LEASE;
|
|
235
320
|
const transferred = reservedId ? this.hostLease.claim(reservedId) : null;
|
|
@@ -242,9 +327,16 @@ export class PodPoolScheduler {
|
|
|
242
327
|
if (lease) {
|
|
243
328
|
let hostReleased = false;
|
|
244
329
|
const releaseHost = () => { if (!hostReleased) { hostReleased = true; lease(); } };
|
|
245
|
-
|
|
330
|
+
// GATE-RESERVE-AUTO: an observed Pod (Running or terminal) is in the CPU reading; until then it is subtracted.
|
|
331
|
+
let seen = false, done = false;
|
|
332
|
+
return Object.assign(() => {
|
|
333
|
+
if (done) return;
|
|
334
|
+
done = true;
|
|
335
|
+
if (seen) this.seenActive--;
|
|
336
|
+
releaseHost(); local();
|
|
337
|
+
}, {
|
|
246
338
|
applied: (job: string, context: string, namespace: string) => lease.applied(job, context, namespace),
|
|
247
|
-
observed: releaseHost,
|
|
339
|
+
observed: () => { if (!seen && !done) { seen = true; this.seenActive++; } releaseHost(); },
|
|
248
340
|
});
|
|
249
341
|
}
|
|
250
342
|
await new Promise<void>((resolve, reject) => {
|
|
@@ -252,15 +344,15 @@ export class PodPoolScheduler {
|
|
|
252
344
|
const onAbort = () => { clearTimeout(timer); reject(new Error('pod lease admission aborted')); };
|
|
253
345
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
254
346
|
});
|
|
255
|
-
try { this.lastRaw = await this.measure(); } catch { this.lastRaw = null; }
|
|
347
|
+
try { this.lastRaw = await this.measure(); } catch { this.lastRaw = null; this.lastUncapped = null; }
|
|
256
348
|
}
|
|
257
349
|
} catch (error) {
|
|
258
350
|
local();
|
|
259
351
|
throw error;
|
|
260
352
|
}
|
|
261
353
|
}
|
|
262
|
-
private acquireLocalAdmission(signal?: AbortSignal, after?: PodLeasePredecessor): Promise<PodLeaseRelease> {
|
|
263
|
-
return this.admission.acquire(signal, after).then((release) => {
|
|
354
|
+
private acquireLocalAdmission(signal?: AbortSignal, after?: PodLeasePredecessor, gate = false): Promise<PodLeaseRelease> {
|
|
355
|
+
return (gate ? this.gateAdmission : this.admission).acquire(signal, after).then((release) => {
|
|
264
356
|
this.budget = Math.max(0, this.budget - 1);
|
|
265
357
|
this.outstanding.add(release);
|
|
266
358
|
return () => {
|
|
@@ -290,8 +382,9 @@ export class PodPoolScheduler {
|
|
|
290
382
|
if (!r) return null;
|
|
291
383
|
return `recommended=${r.recommended ?? 'unknown'} limitedBy=${r.limitedBy ?? '-'} memorySlots=${r.memorySlots ?? '?'}${r.reason ? ` reason=${r.reason}` : ''}`;
|
|
292
384
|
}
|
|
293
|
-
|
|
294
|
-
|
|
385
|
+
/** The harness queue by default; `{ gate: true }` reads the gate-caller queue (GATE-ADMIT-EXEMPT). */
|
|
386
|
+
admissionSnapshot(opts: { gate?: boolean } = {}): ReturnType<PodLeaseAdmission['snapshot']> {
|
|
387
|
+
return (opts.gate ? this.gateAdmission : this.admission).snapshot();
|
|
295
388
|
}
|
|
296
389
|
/**
|
|
297
390
|
* Pick the member with the largest free share: (capacity − measured cluster occupancy − this process's inflight) / capacity.
|
|
@@ -304,8 +397,11 @@ export class PodPoolScheduler {
|
|
|
304
397
|
* member the 0.2.15 gate launched its shards one at a time.
|
|
305
398
|
* Ties keep the configured member order. Returns null when every readable member is full.
|
|
306
399
|
*/
|
|
307
|
-
async tryAcquire(skip?: ReadonlySet<string>, onEmpty?: (reason: string) => void, mode: { selfCapped?: boolean } = {}): Promise<PodPoolMember | null> {
|
|
400
|
+
async tryAcquire(skip?: ReadonlySet<string>, onEmpty?: (reason: string) => void, mode: { selfCapped?: boolean; kind?: PodPlacementKind | null; onPlacement?: (placement: PodPlacement) => void } = {}): Promise<PodPoolMember | null> {
|
|
308
401
|
const selfCapped = mode.selfCapped === true;
|
|
402
|
+
const kind = mode.kind ?? null;
|
|
403
|
+
// POOL-LITE: lite goals try the small member first, implementation goals the big one; full → overflow by free share.
|
|
404
|
+
const preferred = new Set(preferredMembers(this.members, kind));
|
|
309
405
|
let reading: Record<string, MemberOccupancy>;
|
|
310
406
|
// A self-capped caller's own Jobs are its inflight; other workloads' Pods are not its slots (no cluster read).
|
|
311
407
|
if (selfCapped) reading = Object.fromEntries(this.members.map((m) => [m.context, { occupied: 0 }]));
|
|
@@ -316,13 +412,17 @@ export class PodPoolScheduler {
|
|
|
316
412
|
}
|
|
317
413
|
const free: Record<string, number> = {};
|
|
318
414
|
const measured: Record<string, boolean> = {};
|
|
319
|
-
let best: { member: PodPoolMember; slots: number; measured: boolean } | null = null;
|
|
415
|
+
let best: { member: PodPoolMember; slots: number; measured: boolean; preferred: boolean } | null = null;
|
|
320
416
|
// A measured member always beats an unmeasured one (its share would read 100% and draw every Job to a node
|
|
321
417
|
// that did not answer); unmeasured members compete only when no measured member has a free slot.
|
|
322
|
-
|
|
418
|
+
// Among equally measured members a preferred one (POOL-LITE) beats the rest; then the larger free share wins.
|
|
419
|
+
const better = (cand: { member: PodPoolMember; slots: number; measured: boolean; preferred: boolean }) => !best
|
|
323
420
|
|| (cand.measured !== best.measured ? cand.measured
|
|
324
|
-
: cand.
|
|
421
|
+
: cand.preferred !== best.preferred ? cand.preferred
|
|
422
|
+
: cand.slots / cand.member.capacity > best.slots / best.member.capacity);
|
|
325
423
|
for (const m of this.members) {
|
|
424
|
+
// A capacity-0 member is off — never chosen (and no division by zero in the share).
|
|
425
|
+
if (m.capacity <= 0) continue;
|
|
326
426
|
// LAUNCH-STALL: a member this launch cannot fit is skipped for this call only (other launches may fit it).
|
|
327
427
|
if (skip?.has(m.context)) continue;
|
|
328
428
|
const inflight = this.inflight.get(m.context) ?? 0;
|
|
@@ -333,17 +433,30 @@ export class PodPoolScheduler {
|
|
|
333
433
|
const slots = m.capacity - occupied - inflight;
|
|
334
434
|
free[m.context] = slots;
|
|
335
435
|
if (slots <= 0) continue;
|
|
336
|
-
const cand = { member: m, slots, measured: ok };
|
|
436
|
+
const cand = { member: m, slots, measured: ok, preferred: preferred.has(m.context) };
|
|
337
437
|
if (better(cand)) best = cand;
|
|
338
438
|
}
|
|
339
439
|
if (!best) {
|
|
340
|
-
onEmpty?.(`no free slot · free=${JSON.stringify(free)} · measured=${JSON.stringify(measured)}${selfCapped ? ' · selfCapped' : ''}${skip?.size ? ` · skipped=${[...skip].join(',')}` : ''}`);
|
|
440
|
+
onEmpty?.(`no free slot · free=${JSON.stringify(free)} · measured=${JSON.stringify(measured)}${selfCapped ? ' · selfCapped' : ''}${skip?.size ? ` · skipped=${[...skip].join(',')}` : ''}${kind ? ` · kind=${kind}` : ''}`);
|
|
341
441
|
return null;
|
|
342
442
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
443
|
+
const chosen: { member: PodPoolMember; slots: number; measured: boolean; preferred: boolean } = best;
|
|
444
|
+
this.inflight.set(chosen.member.context, (this.inflight.get(chosen.member.context) ?? 0) + 1);
|
|
445
|
+
const share = Object.fromEntries(this.members.map((m) => [m.context, m.capacity > 0 ? Math.round(Math.max(0, free[m.context] ?? 0) / m.capacity * 100) / 100 : 0]));
|
|
446
|
+
const outcome: PodPlacement['outcome'] = preferred.size === 0 ? 'share' : chosen.preferred ? 'preferred' : 'overflow';
|
|
447
|
+
const pref = [...preferred];
|
|
448
|
+
const why = (c: string) => skip?.has(c) ? 'skipped' : measured[c] === false ? 'unmeasured' : 'full';
|
|
449
|
+
const reason = outcome === 'share'
|
|
450
|
+
? `${chosen.member.context} · free share ${share[chosen.member.context]}${kind ? ` · ${kind} 골 · 선호 멤버 없음(${this.members.filter((m) => m.capacity > 0).length < 2 ? '단일 멤버' : '상한이 같다'})` : ''}`
|
|
451
|
+
: outcome === 'preferred'
|
|
452
|
+
? `${chosen.member.context} · ${kind} 골 선호 멤버 · free share ${share[chosen.member.context]}`
|
|
453
|
+
: `${chosen.member.context} · ${kind} 골 넘침 — 선호 ${pref.map((c) => `${c}(${why(c)})`).join(',')} · free share ${share[chosen.member.context]}`;
|
|
454
|
+
const placement: PodPlacement = { member: chosen.member.context, kind, outcome, preferred: pref, reason };
|
|
455
|
+
debug.log('pod.pool', 'member-selected', { member: chosen.member.context, free, measured, share, ...(selfCapped ? { selfCapped: true } : {}), ...(kind ? { kind, outcome, preferred: pref, reason } : {}) });
|
|
456
|
+
// The callback only observes the placement — a throwing observer must not leak the slot it was told about.
|
|
457
|
+
try { mode.onPlacement?.(placement); }
|
|
458
|
+
catch (error) { debug.log('pod.pool', 'placement-observer-failed', { member: chosen.member.context, reason: error instanceof Error ? error.message : String(error) }, { level: 'warn' }); }
|
|
459
|
+
return chosen.member;
|
|
347
460
|
}
|
|
348
461
|
release(member: PodPoolMember): void {
|
|
349
462
|
this.inflight.set(member.context, Math.max(0, (this.inflight.get(member.context) ?? 0) - 1));
|
|
@@ -48,7 +48,7 @@ export const POD_JOB_DEADLINE_SECONDS = 10_800;
|
|
|
48
48
|
// POD-DIET (10-06 03:47~05:18 · node-b goal Pods · 89 one-minute samples · 42 Pods): per-Pod observed max median 4.6Gi ·
|
|
49
49
|
// p75 6.2Gi · p90 11.7Gi — 23/42 went above the old 4Gi request, so the scheduler over-packed. 6Gi ≈ p75; limits stay.
|
|
50
50
|
// Node allocatable ≈ 343Gi ≫ 25 × 6Gi. One-minute samples can miss the true peak (lower bound).
|
|
51
|
-
import type { PodPoolMember, PodPoolScheduler } from './pod-pool.js';
|
|
51
|
+
import type { PodPlacement, PodPlacementKind, PodPoolMember, PodPoolScheduler } from './pod-pool.js';
|
|
52
52
|
export { POD_CHILD_REQUESTS } from './pod-lease.js';
|
|
53
53
|
import { measurePoolLease, recommendConcurrency, POD_CHILD_REQUESTS, POD_HOST_LEASE_ANNOTATION, LEASE_KUBECTL_MAX_BUFFER, memoryQuantityBytes } from './pod-lease.js';
|
|
54
54
|
import { ACTUAL_SUBSTRATE_ENV, RUN_CONTRACT_ENV, carryRunContract, completionFloorFor } from '../../self-implement/graph-run-contract.js';
|
|
@@ -654,6 +654,16 @@ export function parsePodMemoryTier(v: string | undefined): PodMemoryTier | null
|
|
|
654
654
|
return t && (POD_MEMORY_TIERS as readonly string[]).includes(t) ? (t as PodMemoryTier) : null;
|
|
655
655
|
}
|
|
656
656
|
|
|
657
|
+
/**
|
|
658
|
+
* POOL-LITE (10-07): which pool member a Job prefers. Lite tier, or a docs/measurement goal not raised to `high`,
|
|
659
|
+
* goes small-first (node-c); everything else is an implementation Job and goes big-first (node-b).
|
|
660
|
+
*/
|
|
661
|
+
export function podPlacementKind(tier: PodMemoryTier, declared: DeclaredGoalType | null, requestKind: string | null): PodPlacementKind {
|
|
662
|
+
if (tier === 'lite') return 'lite';
|
|
663
|
+
if (tier !== 'high' && (declared === 'document' || declared === 'research' || requestKind === 'docs')) return 'lite';
|
|
664
|
+
return 'standard';
|
|
665
|
+
}
|
|
666
|
+
|
|
657
667
|
function parsePodGoalType(value: string | undefined): DeclaredGoalType | null {
|
|
658
668
|
return value === 'implement' || value === 'research' || value === 'document' || value === 'operate' ? value : null;
|
|
659
669
|
}
|
|
@@ -991,6 +1001,8 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
|
|
|
991
1001
|
// pool-slot reason = what THIS launch saw on its last empty tryAcquire; admission reason = the pool-wide lease measurement (one pool, one reading).
|
|
992
1002
|
const stageReason: { 'pool-slot'?: string | null } = {};
|
|
993
1003
|
const unfitContexts = new Set<string>();
|
|
1004
|
+
// POOL-LITE (10-07): the last placement of this launch — printed at launch and journaled with the Job.
|
|
1005
|
+
let lastPlacement = null as PodPlacement | null;
|
|
994
1006
|
const watchStall = (stage: 'admission' | 'pool-slot'): (() => void) => {
|
|
995
1007
|
const started = Date.now();
|
|
996
1008
|
const timer = setTimeout(() => {
|
|
@@ -1026,9 +1038,12 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
|
|
|
1026
1038
|
for (;;) {
|
|
1027
1039
|
if (input.signal?.aborted) return null;
|
|
1028
1040
|
// The reason comes back inside this call, so a concurrent launch cannot overwrite it.
|
|
1029
|
-
const
|
|
1041
|
+
const kind = podPlacementKind(oomRetried ? retryTier : memoryTier, selectedGoalType, requestKind);
|
|
1042
|
+
const acquired = await options.pool.tryAcquire(unfitContexts, (reason) => { stageReason['pool-slot'] = reason; },
|
|
1043
|
+
{ kind, onPlacement: (placement) => { lastPlacement = placement; } });
|
|
1030
1044
|
if (acquired) {
|
|
1031
|
-
debug.log('self-implement.pod', 'pool-slot', { spaceId: input.spaceId, context: acquired.context, inflight: options.pool.snapshot() });
|
|
1045
|
+
debug.log('self-implement.pod', 'pool-slot', { spaceId: input.spaceId, context: acquired.context, inflight: options.pool.snapshot(), ...(lastPlacement ? { placement: lastPlacement } : {}) });
|
|
1046
|
+
if (lastPlacement) console.warn(`[pod] 배치: ${lastPlacement.reason}`);
|
|
1032
1047
|
return acquired;
|
|
1033
1048
|
}
|
|
1034
1049
|
await sleep(options.pollMs ?? 15_000);
|
|
@@ -1451,6 +1466,7 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
|
|
|
1451
1466
|
}
|
|
1452
1467
|
try {
|
|
1453
1468
|
appendRunLedgerEntry({ runId: ledgerRunId, event: 'pod-child-run', data: { childRunId: liveChildRunId, job: name, attempt: oomRetried ? 2 : 1, ...(goalFile ? { goalFile } : {}) } }, ledgerDir);
|
|
1469
|
+
if (lastPlacement) appendRunLedgerEntry({ runId: ledgerRunId, event: 'pod-placement', data: { job: name, childRunId: liveChildRunId, attempt: oomRetried ? 2 : 1, member: lastPlacement.member, kind: lastPlacement.kind, outcome: lastPlacement.outcome, preferred: lastPlacement.preferred, reason: lastPlacement.reason } }, ledgerDir);
|
|
1454
1470
|
appendRunLedgerEntry({ runId: ledgerRunId, event: 'pod-memory-selected', data: { job: name, childRunId: liveChildRunId, attempt: oomRetried ? 2 : 1, limit: memoryLimit, ...(appliedMemoryRequest ? { request: appliedMemoryRequest } : {}), tier: oomRetried ? retryTier : memoryTier, source: oomRetried ? 'oom-retry' : memorySource, reason: oomRetried ? 'OOMKilled' : memoryReason, declaredGoalType: selectedGoalType } }, ledgerDir);
|
|
1455
1471
|
if (oomRetried && retryFromChildRunId) appendRunLedgerEntry({ runId: ledgerRunId, event: 'pod-oom-retry', data: { attempt: 2, from: retryFromMemoryLimit ?? selectedMemoryLimit, to: memoryLimit, tier: retryTier, reason: 'OOMKilled', job: name, childRunId: liveChildRunId, fromChildRunId: retryFromChildRunId } }, ledgerDir);
|
|
1456
1472
|
} catch (error) {
|
package/src/telegram-commands.ts
CHANGED
|
@@ -55,6 +55,7 @@ import { createWishCard } from './intake-plane/wish-card.js';
|
|
|
55
55
|
import { telegramDecisionOwner } from './decisions/telegram-decision-cards.js';
|
|
56
56
|
import { CardStore } from './task-cards/card-store.js';
|
|
57
57
|
import type { ContextNowDeps } from './context-bus/context-now.js';
|
|
58
|
+
import type { RunningRunsResult } from './self-implement/running-runs.js';
|
|
58
59
|
|
|
59
60
|
/** How many prior user/assistant turns to pass into `executeSkill` as
|
|
60
61
|
* the `## Recent conversation` block. Mirrors the dashboard default
|
|
@@ -305,6 +306,40 @@ function dropChatSessionReply(ctx: TgIncoming): string {
|
|
|
305
306
|
: `_Session \`${sess.id.slice(0, 8)}\` was already gone._`;
|
|
306
307
|
}
|
|
307
308
|
|
|
309
|
+
/** `/runs` — the Telegram twin of TUI `/harness runs`: the same `renderRunningRuns` text over the same
|
|
310
|
+
* production-universe query. The query is injectable so a test can prove the reply equals the renderer
|
|
311
|
+
* output for one data set. Long replies are chunked by the bot's sender, so no run line is dropped here. */
|
|
312
|
+
export async function telegramRunsSlash(args: readonly string[], query?: () => RunningRunsResult,
|
|
313
|
+
runCli: (argv: readonly string[]) => Promise<{ code: number | null; stdout: string }> = runSelfCli): Promise<string> {
|
|
314
|
+
if (args.length > 0) return 'Usage: /runs';
|
|
315
|
+
const { queryRunningRuns, renderRunningRuns } = await import('./self-implement/running-runs.js');
|
|
316
|
+
if (query) return renderRunningRuns(query());
|
|
317
|
+
// The ledger scan is synchronous and took 30–58 s under load (TC 10-07 17:37) — run inside the daemon it would freeze
|
|
318
|
+
// every bot and HTTP reply for that long. Ask the CLI twin (`self running-runs`, same renderer) in a child process,
|
|
319
|
+
// pinned to this daemon's universe; fall back in-process only when the child cannot start.
|
|
320
|
+
const { effectiveInstanceRoot } = await import('./instance/resolve.js');
|
|
321
|
+
try {
|
|
322
|
+
const { code, stdout } = await runCli(['--config-dir', effectiveInstanceRoot(), 'self', 'running-runs']);
|
|
323
|
+
if (code === 0 && stdout.trim()) return stdout.trimEnd();
|
|
324
|
+
} catch { /* fall through */ }
|
|
325
|
+
return renderRunningRuns(queryRunningRuns({ includeTest: false, caller: 'telegram' }));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Run this same elanous entry as a child (`bun <entry> …`) with a hard time limit. */
|
|
329
|
+
async function runSelfCli(argv: readonly string[]): Promise<{ code: number | null; stdout: string }> {
|
|
330
|
+
const { spawn } = await import('node:child_process');
|
|
331
|
+
const entry = process.argv[1];
|
|
332
|
+
if (!entry) throw new Error('no elanous entry');
|
|
333
|
+
return new Promise((resolve, reject) => {
|
|
334
|
+
const child = spawn(process.execPath, [entry, ...argv], { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
335
|
+
let stdout = '';
|
|
336
|
+
const timer = setTimeout(() => { child.kill('SIGTERM'); reject(new Error('timeout')); }, 120_000);
|
|
337
|
+
child.stdout.setEncoding('utf8').on('data', (chunk: string) => { stdout += chunk; });
|
|
338
|
+
child.on('error', (error) => { clearTimeout(timer); reject(error); });
|
|
339
|
+
child.on('close', (code) => { clearTimeout(timer); resolve({ code, stdout }); });
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
308
343
|
/** Build the default command set for a Telegram bot wired via
|
|
309
344
|
* botFromConfig. Returns the array rather than mutating globals so
|
|
310
345
|
* tests can construct a variant set with stubbed handlers. */
|
|
@@ -312,6 +347,7 @@ export function defaultTelegramCommands(
|
|
|
312
347
|
nowDeps?: ContextNowDeps,
|
|
313
348
|
coreCommands: readonly SlashCommand[] = SLASH_COMMANDS,
|
|
314
349
|
maturity: { tuiSlash: Readonly<Record<string, Maturity>>; telegramCommand: Readonly<Record<string, Maturity>>; discordCommand: Readonly<Record<string, Maturity>> } = FEATURE_MATURITY,
|
|
350
|
+
runningRunsQuery?: () => RunningRunsResult,
|
|
315
351
|
): TgSlashCommand[] {
|
|
316
352
|
const handled: TgSlashCommand[] = [
|
|
317
353
|
{
|
|
@@ -335,6 +371,11 @@ export function defaultTelegramCommands(
|
|
|
335
371
|
description: '루프·크론 현황 — 늦음·실패를 먼저 보여줍니다',
|
|
336
372
|
handler: (args) => telegramLoopsStatus(args),
|
|
337
373
|
},
|
|
374
|
+
{
|
|
375
|
+
name: 'runs',
|
|
376
|
+
description: 'Running harness runs — same output as TUI /harness runs',
|
|
377
|
+
handler: async (args) => telegramRunsSlash(args, runningRunsQuery),
|
|
378
|
+
},
|
|
338
379
|
{
|
|
339
380
|
name: 'release',
|
|
340
381
|
description: '발행 현황 — 지금 런·노드·막힘을 한 통으로',
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⏱️ GATE-SPEED ③ — 실물 엔트리 spawn 을 «비동기 ⊕ 상한 있는 동시성»으로 돌리는 테스트 전용 도구.
|
|
3
|
+
*
|
|
4
|
+
* Situation: 배선 시험(`bin/elanous.mjs` 실물 spawn)은 1회 ~3–4초(전 모듈 그래프 콜드 스타트)이고,
|
|
5
|
+
* `spawnSync` 로 줄 세우면 파일 하나가 게이트 임계 경로(수백 초)를 잡는다.
|
|
6
|
+
* Answer: 시험이 문는 값(실물 프로세스의 stdout·stderr·종료 코드·시그널)은 그대로 두고,
|
|
7
|
+
* «기다리는 방식»만 바꾼다 — `describe.concurrent` 안에서 이 함수를 `await` 하면 서로 독립인
|
|
8
|
+
* spawn 들이 `REAL_CLI_SPAWN_CONCURRENCY` 개까지 겹쳐 돈다.
|
|
9
|
+
*
|
|
10
|
+
* ⛔ 반환 모양은 `spawnSync(..., { encoding: 'utf8' })` 와 같다 — 시그널로 죽으면 `status` 는 null 이고
|
|
11
|
+
* `signal` 이 찬다. spawn 자체가 실패하면 `error` 가 찬다(타임아웃 = 0바이트 산출과 구분하려고).
|
|
12
|
+
* ⛔ spawn 의 `timeoutMs` 는 «슬롯을 얻은 뒤»부터 잰다 — 대기열에서 기다린 시간은 실패로 세지 않는다.
|
|
13
|
+
* 그래서 이 도구를 쓰는 시험의 «테스트» 타임아웃은 대기열 몫까지 넉넉히 둔다.
|
|
14
|
+
*/
|
|
15
|
+
import { availableParallelism } from 'node:os';
|
|
16
|
+
|
|
17
|
+
export interface RealCliSpawnResult {
|
|
18
|
+
stdout: string;
|
|
19
|
+
stderr: string;
|
|
20
|
+
status: number | null;
|
|
21
|
+
signal: string | null;
|
|
22
|
+
error?: Error;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RealCliSpawnOptions {
|
|
26
|
+
cwd?: string;
|
|
27
|
+
env?: Record<string, string | undefined>;
|
|
28
|
+
timeoutMs: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resolveConcurrency(): number {
|
|
32
|
+
const fromEnv = Number(process.env.ELANOUS_TEST_REAL_CLI_CONCURRENCY);
|
|
33
|
+
if (Number.isInteger(fromEnv) && fromEnv > 0) return fromEnv;
|
|
34
|
+
return Math.max(2, Math.min(6, Math.floor(availableParallelism() / 3)));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const REAL_CLI_SPAWN_CONCURRENCY = resolveConcurrency();
|
|
38
|
+
|
|
39
|
+
let active = 0;
|
|
40
|
+
const waiters: Array<() => void> = [];
|
|
41
|
+
|
|
42
|
+
async function withSlot<T>(run: () => Promise<T>): Promise<T> {
|
|
43
|
+
while (active >= REAL_CLI_SPAWN_CONCURRENCY) await new Promise<void>((resolve) => waiters.push(resolve));
|
|
44
|
+
active += 1;
|
|
45
|
+
try {
|
|
46
|
+
return await run();
|
|
47
|
+
} finally {
|
|
48
|
+
active -= 1;
|
|
49
|
+
waiters.shift()?.();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function definedEnv(env: Record<string, string | undefined>): Record<string, string> {
|
|
54
|
+
const out: Record<string, string> = {};
|
|
55
|
+
for (const [key, value] of Object.entries(env)) if (value !== undefined) out[key] = value;
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** `spawnSync(cmd[0], cmd.slice(1), { encoding: 'utf8' })` 의 비동기 대응 — 동시성 상한 안에서 돈다. */
|
|
60
|
+
export async function spawnRealCli(cmd: string[], options: RealCliSpawnOptions): Promise<RealCliSpawnResult> {
|
|
61
|
+
const r = await spawnRealCliBytes(cmd, options);
|
|
62
|
+
const decoder = new TextDecoder();
|
|
63
|
+
return {
|
|
64
|
+
stdout: decoder.decode(r.stdout),
|
|
65
|
+
stderr: decoder.decode(r.stderr),
|
|
66
|
+
status: r.exitCode,
|
|
67
|
+
signal: r.signal,
|
|
68
|
+
...(r.error ? { error: r.error } : {}),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface RealCliSpawnBytesResult {
|
|
73
|
+
/** `Bun.spawnSync` 와 같은 이름 — 시그널로 죽으면 null. */
|
|
74
|
+
exitCode: number | null;
|
|
75
|
+
stdout: Uint8Array;
|
|
76
|
+
stderr: Uint8Array;
|
|
77
|
+
signal: string | null;
|
|
78
|
+
error?: Error;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** `Bun.spawnSync({ cmd, stdout: 'pipe', stderr: 'pipe' })` 의 비동기 대응 — 바이트 그대로 돌려준다. */
|
|
82
|
+
export function spawnRealCliBytes(cmd: string[], options: RealCliSpawnOptions): Promise<RealCliSpawnBytesResult> {
|
|
83
|
+
return withSlot(async () => {
|
|
84
|
+
let proc: ReturnType<typeof Bun.spawn>;
|
|
85
|
+
try {
|
|
86
|
+
proc = Bun.spawn(cmd, {
|
|
87
|
+
cwd: options.cwd,
|
|
88
|
+
env: definedEnv(options.env ?? { ...process.env }),
|
|
89
|
+
stdin: 'ignore',
|
|
90
|
+
stdout: 'pipe',
|
|
91
|
+
stderr: 'pipe',
|
|
92
|
+
timeout: options.timeoutMs,
|
|
93
|
+
killSignal: 'SIGKILL',
|
|
94
|
+
});
|
|
95
|
+
} catch (error) {
|
|
96
|
+
return { stdout: new Uint8Array(), stderr: new Uint8Array(), exitCode: null, signal: null, error: error instanceof Error ? error : new Error(String(error)) };
|
|
97
|
+
}
|
|
98
|
+
const [stdout, stderr] = await Promise.all([
|
|
99
|
+
new Response(proc.stdout as ReadableStream).arrayBuffer(),
|
|
100
|
+
new Response(proc.stderr as ReadableStream).arrayBuffer(),
|
|
101
|
+
]);
|
|
102
|
+
await proc.exited;
|
|
103
|
+
const signal = proc.signalCode ?? null;
|
|
104
|
+
return { stdout: new Uint8Array(stdout), stderr: new Uint8Array(stderr), exitCode: signal ? null : proc.exitCode, signal };
|
|
105
|
+
});
|
|
106
|
+
}
|