elanous 0.2.14 → 0.2.15
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/LZcstaO9_NXypPFmujolv/_buildManifest.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1307-6e7e0377c1db0bdc.js → 1307-33ffcc49ab45123b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +1 -0
- package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/{page-74383f890f075511.js → page-0e2c57fe6add4b2a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-7b2b4f378354869a.js → page-e05a77df7e005b04.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-5491c115b1f9a923.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-1512d91c7491ed62.js → page-b8c49f6b4fa2df91.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-a627db94ec6904a0.js → page-976813c91d7394ea.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/{page-fb260b9283dbf646.js → page-28b9448091776c06.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-1d4901eb7c2d8822.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-72d3e7ff35a8c954.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-fed69f9759d248d0.js → page-60c7a8e1a4fe62ce.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-15374909d8f1aebd.js → page-2a52b316cb143d9c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-012a742f81218b01.js → page-c3ce483c8d5db370.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-99effd9d893a43dc.js → page-96f074f6538e8a2c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{layout-938de57ae0315e46.js → layout-e97be5334d83a9f5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/{page-629e1ede482ec6eb.js → page-955ec2da3be4c560.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-9f88fbc15d9058d1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-27e99b4e54a849d4.js → page-c1343f9499d86f3f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-8739a2e0ae793130.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-c9fc5706b8abaa11.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-32cde713be47cddb.js → page-d23b12bb92242558.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-c7fea475488201a8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/{page-04250f0e2c0d03df.js → page-e2ee2896811e98d0.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-e64caff084f7ce21.js → page-5efbf81124c474ae.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-db07eaffba0df7e3.js → page-85f1a1fc0a82738c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-8ac8d71c2adf9c8a.js → page-da6c08aae0f92768.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-06ee266953d9c879.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-86f91034bd5cabe5.js → page-153c2b3bcc89e8de.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-94dc383d3d54db1a.js → page-87a11e6a05f15199.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-b3759cc14da80908.js → page-7d4182964407c09c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/{page-ce4d8a1215ce53a3.js → page-0d6d23b40263a7ea.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/{page-274703bc41b71333.js → page-f5597e894c02cd33.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-0337fdf7fb5e37b6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-35bdeae2e2c17147.js → page-73735b64bfe1ae67.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-3b7e821dc492bdfd.js → page-8fcec4c9ea2e7f70.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f7e631e9b4daf66e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-2f810b18d89ecba6.js → page-9817c7f9007466cc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-134c1bc762aee62c.js → page-97f9f43172a3a90e.js} +1 -1
- package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.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 -0
- package/apps/pwa/out/ceo/index.txt +33 -0
- 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/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +14 -14
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +14 -14
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +14 -14
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +14 -14
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +14 -14
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +14 -14
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +14 -14
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +14 -14
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +14 -14
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +14 -14
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +14 -14
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +14 -14
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +14 -14
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +14 -14
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +14 -14
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +14 -14
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +14 -14
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +15 -15
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +14 -14
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +14 -14
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +14 -14
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +14 -14
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +14 -14
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +14 -14
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +14 -14
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +14 -14
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +14 -14
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +14 -14
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +14 -14
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +14 -14
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +14 -14
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +14 -14
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +14 -14
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +14 -14
- package/graphs/heal/heal-loop.yaml +1 -1
- package/graphs/implement-loop.yaml +1 -1
- package/graphs/release/recipes.yaml +3 -0
- package/graphs/release/release-loop.yaml +4 -2
- package/package.json +1 -1
- package/scripts/brand/check.ts +7 -5
- package/scripts/ci-typecheck-changed.ts +50 -18
- package/scripts/docs/generate-graph-anatomy.ts +315 -0
- package/scripts/expose-rubric-suggest.ts +18 -12
- package/scripts/lecture-note/node.ts +31 -11
- package/scripts/lessons/handbook-page.ts +30 -3
- package/scripts/marketing/generate-ax-brief.ts +140 -0
- package/scripts/marketing/generate-feature-map.ts +118 -0
- package/scripts/release-loop/checklist-gate-node.ts +53 -14
- package/scripts/release-loop/gate-node.ts +80 -14
- package/scripts/release-loop/vault-note-node.ts +240 -8
- package/scripts/release-story/draft.ts +57 -9
- package/scripts/rhythm/daily.ts +263 -76
- package/scripts/rhythm/weekly.ts +30 -9
- package/scripts/smart-merge/resync-shadow.ts +147 -0
- package/src/agent-env/profile.ts +161 -0
- package/src/agent-substrate/pr-reviewer.ts +2 -0
- package/src/ask-user-question/pending-questions.ts +5 -0
- package/src/auth/github-app-token.ts +112 -13
- package/src/autopilot/build/llm-conflict-merge.ts +32 -6
- package/src/briefing/brief-items.ts +121 -0
- package/src/claims/claims-render.ts +14 -5
- package/src/cli/agent-env-cli.ts +21 -0
- package/src/cli/decisions-cli.ts +9 -0
- package/src/cli/doctor-cli.ts +23 -1
- package/src/cli/doctor-node-modules.ts +147 -0
- package/src/cli/intake-cli.ts +46 -17
- package/src/cli/pod-cli.ts +2 -0
- package/src/cli/release-cli.ts +61 -27
- package/src/cli/seat-requests-cli.ts +52 -0
- package/src/context-bus/context-now-public.ts +16 -6
- package/src/context-bus/context-now.ts +3 -1
- package/src/context-bus/coord-events.ts +6 -4
- package/src/decisions/decision-cards.ts +33 -7
- package/src/decisions/proact-meter.ts +286 -0
- package/src/decisions/proact1-lite.ts +86 -0
- package/src/decisions/web-push-decision-cards.ts +1 -1
- package/src/domains/alert-audit.ts +271 -0
- package/src/domains/core-tools.ts +4 -3
- package/src/domains/ops-facts-tool.ts +31 -1
- package/src/harness/harness-cli-command.ts +111 -15
- package/src/harness/harness-incidents.ts +2 -0
- package/src/harness/harness-pod-dispatch.ts +100 -11
- package/src/harness/harness-queue.ts +380 -50
- package/src/harness/helper-repair.ts +87 -0
- package/src/harness/helper-scan.ts +16 -4
- package/src/harness/launch-stamp.ts +236 -0
- package/src/harness/ledger-write-guard-child.fixture.ts +12 -0
- package/src/harness/ledger-write-guard.ts +35 -0
- package/src/harness/pod-memory-policy.ts +15 -0
- package/src/harness/repository-name.ts +55 -0
- package/src/hq/fence-audit.ts +90 -0
- package/src/hq/hq.ts +62 -4
- package/src/hq/lease.ts +4 -4
- package/src/hq/move-plan.ts +126 -0
- package/src/hq/promote.ts +31 -0
- package/src/index.ts +77 -7
- package/src/intake-plane/check.ts +63 -0
- package/src/intake-plane/digest.ts +37 -15
- package/src/intake-plane/front-classifier.ts +6 -0
- package/src/intake-plane/items.ts +3 -2
- package/src/intake-plane/lens.ts +177 -0
- package/src/intake-plane/route.ts +145 -1
- package/src/launch-head/seat-goal-queue-lock.fixture.ts +15 -0
- package/src/launch-head/seat-goal-queue.ts +198 -0
- package/src/loops/budget.ts +36 -5
- package/src/loops/checker.ts +2 -2
- package/src/loops/orchestrator/author-depth.ts +65 -9
- package/src/loops/orchestrator/author-input-check.ts +8 -3
- package/src/loops/orchestrator/author-ledger.ts +3 -1
- package/src/loops/orchestrator/seat-share.ts +56 -0
- package/src/loops/orchestrator/tick.ts +11 -4
- package/src/loops/orchestrator/traffic.ts +117 -11
- package/src/loops/registry.ts +142 -61
- package/src/maturity/feature-maturity.ts +2 -1
- package/src/maturity/tui-slash-availability.ts +31 -0
- package/src/mss/logging/log-store.ts +35 -4
- package/src/nexus/api/http-server.ts +28 -14
- package/src/nexus/api/loop-edges.ts +173 -0
- package/src/nexus/api/schedules-read.ts +13 -0
- package/src/nexus/api/seat-requests.ts +79 -56
- package/src/prompt-enhance/enhance.ts +6 -1
- package/src/release-loop/auto-start.ts +6 -3
- package/src/release-loop/checklist.ts +13 -4
- package/src/release-loop/feature-store.ts +58 -31
- package/src/release-loop/merged-pr-checklist.ts +125 -0
- package/src/release-loop/placement.ts +86 -26
- package/src/seat-dispatch/seat-request-eligibility.ts +19 -0
- package/src/seat-dispatch/seat-request-ledger.ts +133 -0
- package/src/seat-loop/seat-loop.ts +126 -10
- package/src/self-dev/dev-pipeline.ts +8 -4
- package/src/self-dev/draft-sweep.ts +90 -10
- package/src/self-dev/draft-triage-rules.ts +25 -8
- package/src/self-dev/orchestrate.ts +15 -1
- package/src/self-dev/review-provider-fallback.ts +6 -1
- package/src/self-dev/run-supervisor.ts +49 -2
- package/src/self-implement/frozen-merges.ts +3 -2
- package/src/self-implement/goal-author.ts +43 -5
- package/src/self-implement/goal-clarification-escalation.ts +5 -0
- package/src/self-implement/heal-intake.ts +120 -2
- package/src/self-implement/host-regate.ts +9 -2
- package/src/self-implement/l8-merge-queue.ts +132 -0
- package/src/self-implement/orchestrator.ts +187 -40
- package/src/self-implement/pipeline-shape.ts +1 -1
- package/src/self-implement/revert-guard.ts +42 -7
- package/src/self-implement/run-ledger.ts +2 -0
- package/src/self-implement/run-status-mapping.ts +2 -0
- package/src/self-implement/seams.ts +1 -1
- package/src/self-implement/self-implement-cli.ts +1 -0
- package/src/self-implement/speed-claims.ts +338 -0
- package/src/steward/triage.ts +74 -3
- package/src/steward/working-backwards.ts +4 -1
- package/src/task-orchestrator/surfaces/pod-artifact-return.ts +63 -48
- package/src/task-orchestrator/surfaces/pod-command-job.ts +84 -9
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +48 -0
- package/src/task-orchestrator/surfaces/pod-pool.ts +60 -8
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +147 -36
- package/src/task-orchestrator/surfaces/self-implement.ts +5 -1
- package/src/telegram.ts +25 -4
- package/src/user-config.ts +29 -5
- package/src/version/packed-revision.json +1 -1
- package/src/workflow-runtime/nodes/bash.ts +9 -1
- package/apps/pwa/out/_next/static/chunks/4562-071d8bac44a01069.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5977-2df489622a52a33a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-c496bb16da85385f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-1abfe3aec3be6bdc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-e6b84ab07d3567d5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-61ec1d2e92bdd50a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-fd7e200d4d83fbd7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-d47266766766d227.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-160089123a40dd91.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-2ce5471926761fdd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-e995d30f6800b6bf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-39f89c53038a51d4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e7e10cd3386222e5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-5a753bb4ec0cc511.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-341480ed8e3c1ea8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-e1e47d8d940aba3d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-58043db78ba519ae.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-357750dfb91bf960.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-194dce4a83143fce.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-16bef3da05a900c3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-f8da5dc6f3752276.js +0 -1
- package/apps/pwa/out/_next/static/css/2e33713c444d5b39.css +0 -1
- package/apps/pwa/out/_next/static/x6mjI6vqOkVVu0jFuDkz1/_buildManifest.js +0 -1
- /package/apps/pwa/out/_next/static/{x6mjI6vqOkVVu0jFuDkz1 → LZcstaO9_NXypPFmujolv}/_ssgManifest.js +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { lstatSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { lstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
3
3
|
import { gunzipSync } from 'node:zlib';
|
|
4
4
|
import { debug } from '../../debug/log.js';
|
|
5
5
|
import { getDefaultLogStore, type LogStore } from '../../mss/logging/log-store.js';
|
|
6
|
-
import type
|
|
6
|
+
import { fromJsonl, type LogRecord } from '../../mss/logging/record.js';
|
|
7
7
|
|
|
8
8
|
const FILE_LIMIT = 5 * 1024 * 1024;
|
|
9
9
|
const TOTAL_LIMIT = 20 * 1024 * 1024;
|
|
@@ -83,50 +83,58 @@ export function parsePodArtifactChunks(logs: string): ParseResult {
|
|
|
83
83
|
|
|
84
84
|
const POD_LOGS_PATH = 'pod-logs/logs.jsonl';
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
category: 'harness.decision',
|
|
106
|
-
event: row.event,
|
|
107
|
-
data,
|
|
108
|
-
...(typeof row.level === 'string' ? { level: row.level as LogRecord['level'] } : {}),
|
|
109
|
-
...(typeof row.trace_id === 'string' ? { trace_id: row.trace_id } : {}),
|
|
110
|
-
},
|
|
111
|
-
surface: typeof row.surface === 'string' && row.surface ? row.surface : 'pod',
|
|
112
|
-
};
|
|
86
|
+
/** Last measured child ledger context when a failed Pod emitted no terminal JSON. */
|
|
87
|
+
export function podNoResultDiagnostic(ledger: string): string | null {
|
|
88
|
+
let stage: string | undefined;
|
|
89
|
+
let round: number | undefined;
|
|
90
|
+
let mustFix: string | undefined;
|
|
91
|
+
for (const line of ledger.split(/\r?\n/)) {
|
|
92
|
+
try {
|
|
93
|
+
const entry = JSON.parse(line) as { event?: unknown; data?: Record<string, unknown> };
|
|
94
|
+
if (!entry || typeof entry !== 'object' || !entry.data || typeof entry.data !== 'object' || typeof entry.event !== 'string' || entry.event === 'pod-ledger-incomplete') continue;
|
|
95
|
+
if (typeof entry.data.round === 'number' && Number.isSafeInteger(entry.data.round)) round = entry.data.round;
|
|
96
|
+
if (entry.event !== 'run-origin') stage = entry.event;
|
|
97
|
+
// `reviewed` carries pointers only (count + finding ids); the text stays in the review artifact.
|
|
98
|
+
if (entry.event === 'reviewed' && typeof entry.data.mustFix === 'number') {
|
|
99
|
+
const ids = Array.isArray(entry.data.findingIds) ? entry.data.findingIds.filter((id): id is string => typeof id === 'string') : [];
|
|
100
|
+
mustFix = `${entry.data.mustFix}${ids.length ? ` ${JSON.stringify(ids)}` : ''}`;
|
|
101
|
+
}
|
|
102
|
+
} catch { /* incomplete ledger line */ }
|
|
103
|
+
}
|
|
104
|
+
return stage ? `child terminal result missing; last ledger stage=${stage}; round=${round ?? 'unknown'}; mustFix=${mustFix ?? 'unknown'}` : null;
|
|
113
105
|
}
|
|
114
106
|
|
|
115
|
-
/** Import
|
|
116
|
-
export function
|
|
107
|
+
/** Import valid JSONL log records into the launching host store, with durable per-job line receipts. */
|
|
108
|
+
export function importPodLogs(bytes: Buffer, job: string, store: LogStore | null = getDefaultLogStore()): { imported: number; skipped: number } {
|
|
117
109
|
if (!store) return { imported: 0, skipped: 0 };
|
|
118
|
-
const
|
|
119
|
-
const rows: Array<{ rec: LogRecord; surface: string }> = [];
|
|
110
|
+
const rows: Array<{ rec: LogRecord; surface: string; line: number }> = [];
|
|
120
111
|
let skipped = 0;
|
|
121
|
-
for (const line of
|
|
112
|
+
for (const [lineNumber, line] of bytes.toString('utf8').split(/\r?\n/).entries()) {
|
|
122
113
|
if (!line) continue;
|
|
123
|
-
const
|
|
124
|
-
if (!
|
|
125
|
-
|
|
126
|
-
|
|
114
|
+
const rec = fromJsonl(line);
|
|
115
|
+
if (!rec || !Number.isFinite(Date.parse(rec.ts))) { skipped++; continue; }
|
|
116
|
+
const data = rec.data && typeof rec.data === 'object' && !Array.isArray(rec.data)
|
|
117
|
+
? rec.data as Record<string, unknown>
|
|
118
|
+
: rec.data === undefined ? {} : { originalData: rec.data };
|
|
119
|
+
const surface = (rec as LogRecord & { surface?: unknown }).surface;
|
|
120
|
+
rows.push({
|
|
121
|
+
rec: { ...rec, data: { ...data, origin: 'pod', podJob: job } },
|
|
122
|
+
surface: typeof surface === 'string' && surface ? surface : 'pod',
|
|
123
|
+
line: lineNumber,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return { imported: store.insertPodBatch(job, rows), skipped };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function importReturnedLogs(bytes: Buffer, job: string, store: LogStore | null,
|
|
130
|
+
log: (category: string, event: string, data: Record<string, unknown>) => void): void {
|
|
131
|
+
try {
|
|
132
|
+
if (!store) throw new Error('host log store unavailable');
|
|
133
|
+
const imported = importPodLogs(bytes, job, store);
|
|
134
|
+
log('self-implement.pod', 'pod-logs-imported', { job, imported: imported.imported, skipped: imported.skipped });
|
|
135
|
+
} catch (error) {
|
|
136
|
+
debug.log('self-implement.pod', 'pod-logs-import-failed', { job, reason: error instanceof Error ? error.message : String(error) });
|
|
127
137
|
}
|
|
128
|
-
if (rows.length > 0) store.insertBatch(rows);
|
|
129
|
-
return { imported: rows.length, skipped };
|
|
130
138
|
}
|
|
131
139
|
|
|
132
140
|
/** Reject symlink parents as well as lexical traversal; never replace a host file. */
|
|
@@ -176,17 +184,24 @@ export function collectPodArtifacts(logs: string, { dir, job, log = (c, e, d) =>
|
|
|
176
184
|
podLogsReturned = true;
|
|
177
185
|
const lines = bytes.length ? bytes.toString('utf8').split('\n').length - (bytes.at(-1) === 10 ? 1 : 0) : 0;
|
|
178
186
|
log('self-implement.pod', 'pod-logs-returned', { job, path: destination, lines });
|
|
179
|
-
|
|
180
|
-
const imported = importPodDecisionLogs(bytes, job, logStore);
|
|
181
|
-
log('self-implement.pod', 'pod-decisions-imported', { job, imported: imported.imported, skipped: imported.skipped });
|
|
182
|
-
} catch (error) {
|
|
183
|
-
log('self-implement.pod', 'pod-logs-import-failed', { job, reason: error instanceof Error ? error.message : String(error) });
|
|
184
|
-
}
|
|
187
|
+
importReturnedLogs(bytes, job, logStore, log);
|
|
185
188
|
}
|
|
186
189
|
} catch (e) {
|
|
187
190
|
const reason = (e as NodeJS.ErrnoException).code === 'EEXIST' ? 'exists' : e instanceof Error ? e.message : String(e);
|
|
188
|
-
if ((e as NodeJS.ErrnoException).code === 'EEXIST')
|
|
189
|
-
|
|
191
|
+
if ((e as NodeJS.ErrnoException).code === 'EEXIST') {
|
|
192
|
+
log('self-implement.pod', 'artifact-collect-skipped', { job, path, reason });
|
|
193
|
+
if (path === podLogsPath) {
|
|
194
|
+
try {
|
|
195
|
+
const existing = resolve(dir, job, path);
|
|
196
|
+
if (!lstatSync(existing).isSymbolicLink() && readFileSync(existing).equals(bytes)) {
|
|
197
|
+
podLogsReturned = true;
|
|
198
|
+
importReturnedLogs(bytes, job, logStore, log);
|
|
199
|
+
}
|
|
200
|
+
} catch (error) {
|
|
201
|
+
debug.log('self-implement.pod', 'pod-logs-import-failed', { job, reason: error instanceof Error ? error.message : String(error) });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
} else log('self-implement.pod', 'artifact-collect-incomplete', { job, path, reason });
|
|
190
205
|
if (path === podLogsPath) podLogsMissingReason = reason;
|
|
191
206
|
}
|
|
192
207
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { randomBytes } from 'node:crypto';
|
|
9
9
|
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
10
11
|
import { isAbsolute, resolve } from 'node:path';
|
|
11
12
|
import { getUserConfig } from '../../user-config.js';
|
|
12
13
|
import { debug } from '../../debug/log.js';
|
|
@@ -35,6 +36,14 @@ export const POD_LITE_IMAGE = 'elanous-harness-lite:local';
|
|
|
35
36
|
|
|
36
37
|
export const POD_LITE_MEMORY_LIMIT = '2Gi';
|
|
37
38
|
|
|
39
|
+
/** 요청은 한도를 넘을 수 없다(쿠버네티스가 Job 을 거부한다) — lite 2Gi 처럼 한도가 기본 요청보다 작으면 한도로 맞춘다. */
|
|
40
|
+
export function memoryRequestWithin(limit: string | undefined): string {
|
|
41
|
+
const gi = (value: string | undefined) => { const m = /^(\d+)Gi$/.exec(value ?? ''); return m ? Number(m[1]) : null; };
|
|
42
|
+
const request = gi(POD_CHILD_REQUESTS.memory);
|
|
43
|
+
const max = gi(limit);
|
|
44
|
+
return request !== null && max !== null && request > max ? limit! : POD_CHILD_REQUESTS.memory;
|
|
45
|
+
}
|
|
46
|
+
|
|
38
47
|
/** POD7 must-fix: the skill list alone can't tell a network-only job from one that needs ffmpeg/browser — so lite is
|
|
39
48
|
* only ever chosen when the caller says so (`lite`), and then only for network skills without a clone. Anything we
|
|
40
49
|
* can't judge keeps the full image. A lite request that doesn't fit is refused with the reason, not silently widened. */
|
|
@@ -50,6 +59,26 @@ export function podCommandImageFor(skills: readonly string[], clone: boolean, li
|
|
|
50
59
|
|
|
51
60
|
const COMMAND_LOG_CHARS = 80;
|
|
52
61
|
|
|
62
|
+
/** 자식 로그 전문 상한(바이트) — 넘으면 끝부분을 남긴다. config `pod.childLogMaxBytes` 가 양의 정수일 때만 그 값. */
|
|
63
|
+
export const POD_CHILD_LOG_MAX_BYTES_DEFAULT = 1_048_576;
|
|
64
|
+
|
|
65
|
+
export function podChildLogMaxBytes(read: () => number | undefined = () => getUserConfig().pod?.childLogMaxBytes): number {
|
|
66
|
+
const configured = read();
|
|
67
|
+
return typeof configured === 'number' && Number.isSafeInteger(configured) && configured > 0
|
|
68
|
+
? configured
|
|
69
|
+
: POD_CHILD_LOG_MAX_BYTES_DEFAULT;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 크기 상한을 넘으면 끝부분을 남긴다(잘린 앞은 표시). */
|
|
73
|
+
export function tailBytes(text: string, maxBytes: number): string {
|
|
74
|
+
const bytes = Buffer.from(text);
|
|
75
|
+
if (bytes.length <= maxBytes) return text;
|
|
76
|
+
const cut = bytes.subarray(bytes.length - maxBytes);
|
|
77
|
+
let start = 0;
|
|
78
|
+
while (start < cut.length && (cut[start]! & 0xc0) === 0x80) start += 1;
|
|
79
|
+
return `…[앞 ${bytes.length - maxBytes}바이트 생략]\n${cut.subarray(start).toString('utf8')}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
53
82
|
const GATE = `ok=0
|
|
54
83
|
for i in $(seq 1 60); do
|
|
55
84
|
if curl -s -m 1 -o /dev/null http://host.orb.internal:31415/health || curl -s -m 1 -o /dev/null http://core.elanous-prod:8080/; then ok=0; else ok=$((ok+1)); fi
|
|
@@ -156,7 +185,7 @@ export function podCommandJobManifest(o: PodCommandJobInput): Record<string, unk
|
|
|
156
185
|
initContainers: [{ name: 'isolation-gate', image: o.image, imagePullPolicy: o.imagePullPolicy ?? 'Never', command: ['bash', '-c'], args: [GATE] }],
|
|
157
186
|
containers: [{
|
|
158
187
|
name: 'child', image: o.image, imagePullPolicy: o.imagePullPolicy ?? 'Never',
|
|
159
|
-
resources: { requests: { ...POD_CHILD_REQUESTS }, limits: { memory: o.memoryLimit ?? '16Gi', cpu: '4' } },
|
|
188
|
+
resources: { requests: { ...POD_CHILD_REQUESTS, memory: memoryRequestWithin(o.memoryLimit) }, limits: { memory: o.memoryLimit ?? '16Gi', cpu: '4' } },
|
|
160
189
|
command: ['bash', '-c'], args: [script],
|
|
161
190
|
env: [
|
|
162
191
|
...(o.runId ? [{ name: 'ELANOUS_RUN_ID', value: o.runId }] : []),
|
|
@@ -214,12 +243,23 @@ export function podCommandSecretKeys(secret: Record<string, unknown> | null): st
|
|
|
214
243
|
return Object.keys((secret.stringData as Record<string, string> | undefined) ?? {});
|
|
215
244
|
}
|
|
216
245
|
|
|
246
|
+
export interface PodArtifactCollection {
|
|
247
|
+
/** 회수한 산출 파일 수(child.log 제외). 로그 조회 실패면 null. */
|
|
248
|
+
files: number | null;
|
|
249
|
+
/** 회수한 산출 파일의 상대 경로. */
|
|
250
|
+
names: string[];
|
|
251
|
+
/** 로그 조회 자체가 실패했을 때의 사유. 성공이면 null. */
|
|
252
|
+
error: string | null;
|
|
253
|
+
}
|
|
254
|
+
|
|
217
255
|
export interface PodCommandResult {
|
|
218
256
|
exitCode: number;
|
|
219
257
|
artifactsDir: string;
|
|
220
258
|
job: string;
|
|
221
259
|
/** 이 런이 실제로 쓴 이미지(레지스트리 판 태그 또는 로컬 이름). */
|
|
222
260
|
image?: string;
|
|
261
|
+
/** collectPodArtifacts 가 회수한 파일. 로그 조회 실패면 files=null · error=사유. runPodCommand 는 항상 채운다. */
|
|
262
|
+
artifacts?: PodArtifactCollection;
|
|
223
263
|
}
|
|
224
264
|
|
|
225
265
|
export interface RunPodCommandOptions {
|
|
@@ -255,6 +295,8 @@ export interface RunPodCommandOptions {
|
|
|
255
295
|
log?: (category: string, event: string, data: Record<string, unknown>) => void;
|
|
256
296
|
env?: NodeJS.ProcessEnv;
|
|
257
297
|
configHostMirror?: () => string | undefined;
|
|
298
|
+
/** 자식 로그 전문 상한(바이트). 생략 시 config `pod.childLogMaxBytes`, 없으면 1MiB. 넘으면 끝부분 우선. */
|
|
299
|
+
childLogMaxBytes?: number;
|
|
258
300
|
}
|
|
259
301
|
|
|
260
302
|
function jobFinished(types: string): 'complete' | 'failed' | null {
|
|
@@ -263,6 +305,19 @@ function jobFinished(types: string): 'complete' | 'failed' | null {
|
|
|
263
305
|
return null;
|
|
264
306
|
}
|
|
265
307
|
|
|
308
|
+
function artifactNames(logs: string): string[] {
|
|
309
|
+
const names = new Set<string>();
|
|
310
|
+
for (const line of logs.split(/\r?\n/)) {
|
|
311
|
+
const match = /^ELANOUS_POD_ARTIFACT (\S+) \d+\/\d+ /.exec(line);
|
|
312
|
+
if (!match) continue;
|
|
313
|
+
try {
|
|
314
|
+
const path = Buffer.from(match[1]!, 'base64url').toString('utf8');
|
|
315
|
+
if (path && !path.split('/').some((part) => part === '' || part === '.' || part === '..')) names.add(path);
|
|
316
|
+
} catch { /* 회수 쪽(collectPodArtifacts)이 불완전 사유를 남긴다 */ }
|
|
317
|
+
}
|
|
318
|
+
return [...names];
|
|
319
|
+
}
|
|
320
|
+
|
|
266
321
|
function exitCodeFromPod(kubectl: Kubectl, namespace: string, job: string, state: 'complete' | 'failed'): number {
|
|
267
322
|
const got = kubectl(['-n', namespace, 'get', 'pods', '-l', `job-name=${job}`, '-o', 'jsonpath={.items[0].status.containerStatuses[?(@.name=="child")].state.terminated.exitCode}']);
|
|
268
323
|
const code = Number(got.stdout.trim());
|
|
@@ -316,7 +371,7 @@ export async function runPodCommand(options: RunPodCommandOptions): Promise<PodC
|
|
|
316
371
|
}
|
|
317
372
|
if (pool) {
|
|
318
373
|
for (;;) {
|
|
319
|
-
member = pool.tryAcquire();
|
|
374
|
+
member = await pool.tryAcquire();
|
|
320
375
|
if (member) break;
|
|
321
376
|
await sleep(options.pollMs ?? 15_000);
|
|
322
377
|
}
|
|
@@ -387,12 +442,29 @@ export async function runPodCommand(options: RunPodCommandOptions): Promise<PodC
|
|
|
387
442
|
if (finished) { state = finished; break; }
|
|
388
443
|
await sleep(options.pollMs ?? 15_000);
|
|
389
444
|
}
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
445
|
+
const logsResult = kubectl(['-n', namespace, 'logs', `job/${name}`, '-c', 'child']);
|
|
446
|
+
const logsFailed = logsResult.status !== 0;
|
|
447
|
+
const logs = logsFailed ? '' : logsResult.stdout;
|
|
448
|
+
const artifactDir = options.artifactsRoot ?? `${effectiveInstanceRoot()}/pod-artifacts`;
|
|
449
|
+
let artifacts: PodArtifactCollection;
|
|
450
|
+
if (logsFailed) {
|
|
451
|
+
const reason = logsResult.stderr.trim() || `kubectl logs exit ${logsResult.status}`;
|
|
452
|
+
artifacts = { files: null, names: [], error: reason };
|
|
453
|
+
log('pod.command', 'artifact-collect-failed', { job: name, reason });
|
|
454
|
+
} else {
|
|
455
|
+
collectPodArtifacts(logs, {
|
|
456
|
+
dir: artifactDir,
|
|
457
|
+
job: name,
|
|
458
|
+
log: (c, e, d) => log(c, e, d),
|
|
459
|
+
});
|
|
460
|
+
const names = artifactNames(logs);
|
|
461
|
+
artifacts = { files: names.length, names, error: null };
|
|
462
|
+
}
|
|
463
|
+
// 표지가 없어도 알리는 경로는 실제로 존재해야 한다. 자식 로그 전문은 child.log (상한 넘으면 끝부분).
|
|
464
|
+
mkdirSync(artifactsDir, { recursive: true });
|
|
465
|
+
const maxBytes = options.childLogMaxBytes ?? podChildLogMaxBytes();
|
|
466
|
+
const childLog = logsFailed ? `산출 회수 못 함: ${artifacts.error}` : tailBytes(logs, maxBytes);
|
|
467
|
+
writeFileSync(resolve(artifactsDir, 'child.log'), childLog);
|
|
396
468
|
const exitCode = exitCodeFromPod(kubectl, namespace, name, state);
|
|
397
469
|
cleanupSecret();
|
|
398
470
|
log('pod.command', 'job-finished', {
|
|
@@ -401,8 +473,11 @@ export async function runPodCommand(options: RunPodCommandOptions): Promise<PodC
|
|
|
401
473
|
skills,
|
|
402
474
|
llm: llm ?? null,
|
|
403
475
|
command: command.join(' ').slice(0, COMMAND_LOG_CHARS),
|
|
476
|
+
artifactsDir,
|
|
477
|
+
artifacts,
|
|
478
|
+
...(logsFailed ? { artifactError: `산출 회수 못 함: ${artifacts.error}` } : {}),
|
|
404
479
|
});
|
|
405
|
-
return { exitCode, artifactsDir, job: name, image: imageRef ?? image };
|
|
480
|
+
return { exitCode, artifactsDir, job: name, image: imageRef ?? image, artifacts };
|
|
406
481
|
} catch (err) {
|
|
407
482
|
cleanupSecret();
|
|
408
483
|
throw err;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Extract a human-readable reason from a failed child Pod without treating transfer markers as child errors. */
|
|
2
|
+
export function extractPodFailureReason(input: {
|
|
3
|
+
logs: string;
|
|
4
|
+
logTailReason?: string;
|
|
5
|
+
containerReason?: string | null;
|
|
6
|
+
jobReason?: string;
|
|
7
|
+
deadlineSeconds?: number;
|
|
8
|
+
}): string {
|
|
9
|
+
if (input.jobReason === 'DeadlineExceeded') {
|
|
10
|
+
return `DeadlineExceeded: Job 수명 상한 ${input.deadlineSeconds ?? 'unknown'}초 초과`;
|
|
11
|
+
}
|
|
12
|
+
if (input.containerReason === 'OOMKilled') return 'OOMKilled: 컨테이너 메모리 한도 초과';
|
|
13
|
+
|
|
14
|
+
const isBookkeeping = (line: string): boolean =>
|
|
15
|
+
/^ELANOUS_(?:MEM|RUN_LEDGER|USAGE_ROLLUP|POD_)/u.test(line) || /^(?:at\s+\S|\.\.\.\s+\d+\s+more\b|cleanup\s+(?:complete|done|finished)\b)/iu.test(line);
|
|
16
|
+
const isError = (line: string): boolean =>
|
|
17
|
+
/\berror\b|\bfail(?:ed|ure)?\b|exception|fatal|denied|timed?\s*out|exceeded|overloaded|unavailable|exhausted|오류|실패/iu.test(line);
|
|
18
|
+
const meaningful: Array<{ text: string; terminal: boolean }> = [];
|
|
19
|
+
for (const line of input.logs.split(/\r\n|\n|\r/u)) {
|
|
20
|
+
const trimmed = line.trim();
|
|
21
|
+
if (!trimmed || isBookkeeping(trimmed)) continue;
|
|
22
|
+
if (trimmed.startsWith('{')) {
|
|
23
|
+
let parsed: unknown;
|
|
24
|
+
try { parsed = JSON.parse(trimmed); } catch { /* An ordinary line may begin with a brace. */ }
|
|
25
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
26
|
+
const row = parsed as Record<string, unknown>;
|
|
27
|
+
if (typeof row.ok === 'boolean' && typeof row.stage === 'string') {
|
|
28
|
+
meaningful.length = 0;
|
|
29
|
+
if (row.ok === false && typeof row.error === 'string') {
|
|
30
|
+
const errorLines = row.error.split(/\r\n|\n|\r/u).map((part) => part.trim())
|
|
31
|
+
.filter((part) => part && !isBookkeeping(part));
|
|
32
|
+
const errorLine = [...errorLines].reverse().find(isError) ?? errorLines.at(-1);
|
|
33
|
+
if (errorLine) meaningful.push({ text: errorLine, terminal: true });
|
|
34
|
+
}
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
// A JSON transfer/observation is not a plain-text failure reason.
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
meaningful.push({ text: trimmed, terminal: false });
|
|
42
|
+
}
|
|
43
|
+
const errorLine = [...meaningful].reverse().find((line) => line.terminal || isError(line.text));
|
|
44
|
+
const reason = errorLine?.text.replace(/\/home\/[^/\s]+\//gu, '~/').slice(0, 240);
|
|
45
|
+
if (reason) return reason;
|
|
46
|
+
const why = input.logTailReason?.trim() || (input.logs.trim() ? '로그에 읽을 수 있는 오류 줄 없음' : '자식 로그 비어 있음');
|
|
47
|
+
return `사유 못 읽음: ${why.replace(/\/home\/[^/\s]+\//gu, '~/').slice(0, 240)}`;
|
|
48
|
+
}
|
|
@@ -20,7 +20,7 @@ import { debug } from '../../debug/log.js';
|
|
|
20
20
|
import { PodLeaseAdmission, type PodLeasePredecessor, type PodLeaseRelease } from '../../pod-lease/admission.js';
|
|
21
21
|
import { predecessorState, type PredecessorState } from '../../pod-lease/dependency-state.js';
|
|
22
22
|
import { HostPoolLease, leaseHasPendingPod } from '../../pod-lease/host-lease.js';
|
|
23
|
-
import { measurePoolLease, recommendConcurrency, type PoolDnsProbe, type PoolLeaseRecommendation } from './pod-lease.js';
|
|
23
|
+
import { measurePoolLease, recommendConcurrency, type PodLeaseMember, type PoolDnsProbe, type PoolLeaseRecommendation } from './pod-lease.js';
|
|
24
24
|
|
|
25
25
|
export interface PodPoolMember {
|
|
26
26
|
readonly context: string;
|
|
@@ -142,7 +142,34 @@ export function genericConfigPodPool(config: { pod?: { pool?: string }; harness?
|
|
|
142
142
|
return config.pod?.pool;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
/**
|
|
145
|
+
/**
|
|
146
|
+
* Cluster occupancy of one pool member: Running + Pending harness goal Pods.
|
|
147
|
+
* `null` means the read failed — callers must not treat that as zero.
|
|
148
|
+
*/
|
|
149
|
+
export type MemberOccupancy = { occupied: number } | { occupied: null; reason: string };
|
|
150
|
+
|
|
151
|
+
/** Read each member's cluster occupancy (Running + Pending harness goal Pods). A failed member is `occupied: null`, never 0. */
|
|
152
|
+
export function measureMemberOccupancy(members: readonly PodPoolMember[], deps: { kubectl?: PoolKubectl; dns?: (context: string) => PoolDnsProbe; measure?: (members: readonly PodPoolMember[]) => { members: PodLeaseMember[] } } = {}): Record<string, MemberOccupancy> {
|
|
153
|
+
let measured: { members: PodLeaseMember[] };
|
|
154
|
+
try {
|
|
155
|
+
measured = deps.measure
|
|
156
|
+
? deps.measure(members)
|
|
157
|
+
: measurePoolLease(members, { ...(deps.kubectl ? { kubectl: deps.kubectl } : {}), ...(deps.dns ? { dns: deps.dns } : {}) });
|
|
158
|
+
} catch (error) {
|
|
159
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
160
|
+
return Object.fromEntries(members.map((m) => [m.context, { occupied: null, reason }]));
|
|
161
|
+
}
|
|
162
|
+
const byContext = new Map(measured.members.map((m) => [m.context, m]));
|
|
163
|
+
return Object.fromEntries(members.map((member) => {
|
|
164
|
+
const row = byContext.get(member.context);
|
|
165
|
+
if (!row || row.running === null || row.pending === null) {
|
|
166
|
+
return [member.context, { occupied: null, reason: row?.reason ?? '측정 불가' }];
|
|
167
|
+
}
|
|
168
|
+
return [member.context, { occupied: row.running + row.pending }];
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** 노드 자리 배분 — 클러스터 실측 점유가 있으면 남은 칸이 가장 큰 멤버. 측정이 없으면 우선순위 순 첫 빈 자리. */
|
|
146
173
|
export function podPoolHostLease(members: readonly PodPoolMember[]): HostPoolLease {
|
|
147
174
|
return new HostPoolLease([...members.map((m) => `${m.context}@${m.sshHost ?? ''}`)].sort().join(','));
|
|
148
175
|
}
|
|
@@ -158,7 +185,8 @@ export class PodPoolScheduler {
|
|
|
158
185
|
private lastRaw: PoolLeaseRecommendation | null = null;
|
|
159
186
|
private readonly hostLease: HostPoolLease;
|
|
160
187
|
private readonly pollMs: number;
|
|
161
|
-
|
|
188
|
+
private readonly occupancy: () => Record<string, MemberOccupancy> | Promise<Record<string, MemberOccupancy>>;
|
|
189
|
+
constructor(readonly members: readonly PodPoolMember[], options: { status?: () => PoolLeaseRecommendation | Promise<PoolLeaseRecommendation>; kubectl?: PoolKubectl; dns?: (context: string) => PoolDnsProbe; pollMs?: number; hostLease?: HostPoolLease; occupancy?: () => Record<string, MemberOccupancy> | Promise<Record<string, MemberOccupancy>>; dependencyMerged?: (after: PodLeasePredecessor, signal?: AbortSignal) => PredecessorState | boolean | Promise<PredecessorState | boolean> } = {}) {
|
|
162
190
|
this.pollMs = options.pollMs ?? 15_000;
|
|
163
191
|
// One lease directory per pool (contexts, not caps) — every CLI process launching into the same clusters shares it.
|
|
164
192
|
this.hostLease = options.hostLease ?? podPoolHostLease(members);
|
|
@@ -167,6 +195,7 @@ export class PodPoolScheduler {
|
|
|
167
195
|
return recommendConcurrency(measure, { capacity: members.reduce((n, m) => n + m.capacity, 0), accounts: 0, perAccount: 0 });
|
|
168
196
|
});
|
|
169
197
|
this.measure = async () => measureStatus();
|
|
198
|
+
this.occupancy = options.occupancy ?? (() => measureMemberOccupancy(members, { ...(options.kubectl ? { kubectl: options.kubectl } : {}), ...(options.dns ? { dns: options.dns } : {}) }));
|
|
170
199
|
this.admission = new PodLeaseAdmission({ status: async () => {
|
|
171
200
|
const measured = await measureStatus();
|
|
172
201
|
this.lastRaw = measured;
|
|
@@ -241,13 +270,36 @@ export class PodPoolScheduler {
|
|
|
241
270
|
admissionSnapshot(): ReturnType<PodLeaseAdmission['snapshot']> {
|
|
242
271
|
return this.admission.snapshot();
|
|
243
272
|
}
|
|
244
|
-
/**
|
|
245
|
-
|
|
273
|
+
/**
|
|
274
|
+
* Pick the member with the most free slots: capacity − measured cluster occupancy − this process's inflight.
|
|
275
|
+
* A member whose occupancy could not be read falls back to inflight-only (never treated as zero occupied).
|
|
276
|
+
* Ties keep the configured member order. Returns null when every readable member is full.
|
|
277
|
+
*/
|
|
278
|
+
async tryAcquire(): Promise<PodPoolMember | null> {
|
|
279
|
+
let reading: Record<string, MemberOccupancy>;
|
|
280
|
+
try { reading = await this.occupancy(); }
|
|
281
|
+
catch (error) {
|
|
282
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
283
|
+
reading = Object.fromEntries(this.members.map((m) => [m.context, { occupied: null, reason }]));
|
|
284
|
+
}
|
|
285
|
+
const free: Record<string, number> = {};
|
|
286
|
+
const measured: Record<string, boolean> = {};
|
|
287
|
+
let best: { member: PodPoolMember; slots: number } | null = null;
|
|
246
288
|
for (const m of this.members) {
|
|
247
|
-
const
|
|
248
|
-
|
|
289
|
+
const inflight = this.inflight.get(m.context) ?? 0;
|
|
290
|
+
const row = reading[m.context];
|
|
291
|
+
const ok = !!row && row.occupied !== null;
|
|
292
|
+
measured[m.context] = ok;
|
|
293
|
+
const occupied = ok ? row.occupied! : 0;
|
|
294
|
+
const slots = m.capacity - occupied - inflight;
|
|
295
|
+
free[m.context] = slots;
|
|
296
|
+
if (slots <= 0) continue;
|
|
297
|
+
if (!best || slots > best.slots) best = { member: m, slots };
|
|
249
298
|
}
|
|
250
|
-
return null;
|
|
299
|
+
if (!best) return null;
|
|
300
|
+
this.inflight.set(best.member.context, (this.inflight.get(best.member.context) ?? 0) + 1);
|
|
301
|
+
debug.log('pod.pool', 'member-selected', { member: best.member.context, free, measured });
|
|
302
|
+
return best.member;
|
|
251
303
|
}
|
|
252
304
|
release(member: PodPoolMember): void {
|
|
253
305
|
this.inflight.set(member.context, Math.max(0, (this.inflight.get(member.context) ?? 0) - 1));
|