elanous 0.2.16 → 0.2.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/P6YTVgzuZz5oTZhY4Din_/_buildManifest.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1307-b46d45b705e8c9ae.js → 1307-30dff22c9e1f3073.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/195-fb1d7690bf30bab4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1986-33e13d648a9df765.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2537-6c289376caa88ca1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2699-325cc923ba7b8631.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3857-4f936cdb30322aa6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4554-5761612531d0146b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4565-3d7e3fdeb43686a8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5977-a11f37254196b659.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{6158.9c0449a9a8d0064b.js → 6158.af2e93b7adff709b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/6877-74399f69c7da1433.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7797-df5aa0381fdc078f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8401-8615a660c84cdb03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8695-846034ce44852c70.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-d92f7ee3994e84c7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-3cb52e5ea8fc2fa5.js → page-4733c967b56a4277.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-d22fd849cc22f104.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-c1e20d7d338713f1.js → page-bb742f995c6769e7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-482c5dbde699947c.js → page-02b073a2ac793b83.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-8a9b3594bc23289b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-a0eb02638b1e6f31.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/{page-a0e2a6d1bfb52fc5.js → page-308111abadf35670.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-c53598cfce6156f2.js → page-5d97b2901e0d1ea6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-13a63953f53c6a23.js → page-c39554c321576982.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-5d3c8429d04f3831.js → page-8c3d8e63ae2e1c98.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-3193bdc0bbab047a.js → page-9b72a07d424701c6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-6107ca8ddd5e3bdd.js → page-a572f58cd3ca5864.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-be5ea965a07fbc41.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-c130b63862437814.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-fa244bd146a00ae8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-44d56d6391965a6a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-4167348049166852.js → page-83a700d4c754d802.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-de52e8fdf9095f7a.js → not-found-7b3170f374f77b9d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-ca3952cd78a83ca2.js → page-a3c58f5c72a5f3f9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-e8e1ecaea7cd71d2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-1ebfe049c640e171.js → page-1467047104141297.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-2da6f9b8a38c6978.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-102310930791197a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-6ed0b73bae8351d8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-abe8e3b74bea1ffc.js → page-44d5f767491692bf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-43336055451f6e37.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/{page-d28573d6792b4275.js → page-3cc47dca0922ab9a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-a77ee355a2304ff0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-d751de5f8062ff5e.js → page-b17cd46cf0e9b8cf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/{page-fe1759f94003b32b.js → page-61553851aa606146.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/{page-d64c984e855c8c68.js → page-a1f585e154e61c59.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-7bf54c9bb9274af8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-9afdee66b820c76e.js → page-5150ef17b5383a57.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-3544f154c87b43ee.js → page-134da468538baf48.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{webpack-539cde99554be20f.js → webpack-c2782b330447ffb6.js} +1 -1
- package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +14 -14
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +14 -14
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +14 -14
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +14 -14
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +14 -14
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +14 -14
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +14 -14
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +14 -14
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +14 -14
- package/apps/pwa/out/decisions/index.html +1 -0
- package/apps/pwa/out/decisions/index.txt +30 -0
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +14 -14
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +14 -14
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +14 -14
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +14 -14
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +14 -14
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +14 -14
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +14 -14
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +14 -14
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +14 -14
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +14 -14
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +14 -14
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +14 -14
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +14 -14
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +14 -14
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +14 -14
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +14 -14
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +14 -14
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +15 -15
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +14 -14
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +14 -14
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +14 -14
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +14 -14
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +14 -14
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +14 -14
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +14 -14
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +14 -14
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +14 -14
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +14 -14
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +14 -14
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +14 -14
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +14 -14
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +14 -14
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +14 -14
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +14 -14
- package/graphs/intake/intake-daily.yaml +9 -2
- package/graphs/intake/news-sources.yaml +4 -0
- package/graphs/intake/recipes.yaml +6 -1
- package/package.json +2 -1
- package/samples/workflows/templates/approval-gated-deploy.yaml +30 -0
- package/samples/workflows/templates/chat-driven-research.yaml +35 -0
- package/samples/workflows/templates/discord-listen-and-respond.yaml +29 -0
- package/samples/workflows/templates/http-poll-then-alert.yaml +30 -0
- package/samples/workflows/templates/manual-quickstart.yaml +21 -0
- package/samples/workflows/templates/scheduled-daily-digest.yaml +30 -0
- package/samples/workflows/templates/webhook-deploy-hook.yaml +27 -0
- package/scripts/__fixtures__/intake-news.xml +7 -0
- package/scripts/ci-public-export-import-gate.ts +7 -2
- package/scripts/ci-typecheck-changed.ts +21 -6
- package/scripts/discord-seat-probe.ts +3 -2
- package/scripts/docs/generate-cli-anatomy.ts +162 -0
- package/scripts/goals/authoring-metrics.ts +73 -0
- package/scripts/intake-news.ts +169 -0
- package/scripts/landing-heal/must-fix-match.ts +7 -7
- package/scripts/landing-heal/run-mission.ts +3 -2
- package/scripts/public-export-test-run.ts +37 -2
- package/scripts/release-loop/checklist-gate-node.ts +13 -11
- package/scripts/release-loop/cut-branch.ts +2 -1
- package/scripts/release-loop/cutoff.ts +4 -2
- package/scripts/release-loop/docs-land-node.ts +77 -11
- package/scripts/release-loop/docs-node.ts +1 -1
- package/scripts/release-loop/gate-node.ts +99 -14
- package/scripts/release-loop/node-verdict.ts +2 -1
- package/scripts/release-loop/npm-publish-node.ts +11 -7
- package/scripts/release-loop/ops-upgrade-node.ts +8 -1
- package/scripts/release-loop/preflight.ts +246 -0
- package/scripts/release-loop/publish-node.ts +10 -3
- package/scripts/release-loop/release-docs.ts +134 -0
- package/scripts/release-loop/release-readiness.ts +11 -6
- package/scripts/release-loop/release-version.ts +48 -0
- package/scripts/release-loop/resume-release.ts +115 -0
- package/scripts/release-loop/unattended-release.ts +43 -7
- package/scripts/release-loop/version-node.ts +149 -41
- package/scripts/release-story/draft.ts +42 -14
- package/scripts/rhythm/daily.ts +25 -4
- package/scripts/skills/public-check.ts +62 -12
- package/scripts/test-deterministic.ts +51 -33
- package/scripts/test-diet/effectiveness.ts +62 -1
- package/scripts/test-diet/lib.ts +1 -1
- package/scripts/test-diet/node.ts +8 -1
- package/src/agent-mission/driver.ts +53 -10
- package/src/agent-mission/review-loop.ts +4 -3
- package/src/agent-substrate/pr-comment-meta.ts +20 -0
- package/src/autopilot/build/llm-conflict-merge.ts +28 -8
- package/src/autopilot/track-agent.ts +5 -3
- package/src/briefing/brief-items.ts +86 -17
- package/src/cli/autopilot-cli.ts +86 -1
- package/src/cli/brief-cli.ts +36 -2
- package/src/cli/decisions-cli.ts +4 -1
- package/src/cli/freeze-cli.ts +3 -2
- package/src/cli/intake-cli.ts +74 -2
- package/src/cli/log-event-names.ts +1 -0
- package/src/cli/notify-cli.ts +75 -0
- package/src/cli/pr-cli.ts +12 -1
- package/src/cli/provider-cli.ts +24 -0
- package/src/cli/release-cli.ts +101 -17
- package/src/cli/self-update-canary.ts +46 -0
- package/src/cli/self-update.ts +200 -1
- package/src/cli/tasks-cli.ts +309 -3
- package/src/context-bus/condense.ts +19 -3
- package/src/context-bus/context-day.ts +77 -24
- package/src/context-bus/context-first.ts +33 -0
- package/src/context-card/index.ts +45 -0
- package/src/context-card/task.ts +76 -0
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +2 -0
- package/src/decisions/decision-ledger.ts +10 -1
- package/src/discord-self-message.ts +20 -2
- package/src/discord-slash-wire.ts +1 -0
- package/src/domains/outbound-alert.ts +200 -27
- package/src/domains/outbound-log-probe.ts +16 -1
- package/src/domains/release-tool.ts +2 -2
- package/src/execution-loop/gates.ts +11 -2
- package/src/flow-loop/split.ts +8 -4
- package/src/git-fs/worktree.ts +23 -3
- package/src/harness/harness-cli-command.ts +151 -11
- package/src/harness/harness-pod-dispatch.ts +11 -0
- package/src/harness/harness-queue.ts +466 -32
- package/src/harness/helper-scan.ts +10 -3
- package/src/harness/nested-elanous-policy.ts +23 -4
- package/src/hq/fence-wrapper.ts +123 -0
- package/src/hq/hq.ts +8 -0
- package/src/hq/seats.ts +369 -0
- package/src/index.ts +190 -23
- package/src/intake-plane/adopt-gate.ts +49 -0
- package/src/intake-plane/check.ts +3 -0
- package/src/intake-plane/collect-github.ts +57 -25
- package/src/intake-plane/digest.ts +39 -4
- package/src/intake-plane/discord-seat-work.ts +8 -2
- package/src/intake-plane/requirement-funnel.ts +316 -0
- package/src/knowledge/know-find.ts +10 -1
- package/src/loops/budget.ts +36 -0
- package/src/loops/orchestrator/finish-rate.ts +73 -1
- package/src/loops/orchestrator/grid-place.ts +11 -0
- package/src/loops/orchestrator/tick.ts +295 -20
- package/src/maturity/feature-maturity.ts +3 -2
- package/src/nexus/api/decisions-route.ts +11 -1
- package/src/nexus/api/grid.ts +61 -0
- package/src/nexus/api/http-server.ts +19 -1
- package/src/nexus/api/loop-edges.ts +69 -13
- package/src/nexus/api/ops-seats-sources.ts +2 -2
- package/src/nexus/api/task-cards-api.ts +19 -0
- package/src/nexus/api/workflow-templates.ts +3 -0
- package/src/oauth/codex.ts +120 -2
- package/src/onboarding/first-chat-gate.ts +29 -0
- package/src/release-loop/checklist.ts +104 -6
- package/src/release-loop/feature-store.ts +10 -9
- package/src/release-loop/landing-freeze.ts +24 -14
- package/src/release-loop/placement.ts +13 -3
- package/src/release-loop/rubric.ts +80 -0
- package/src/seat-dispatch/seat-ask.ts +79 -16
- package/src/seat-loop/seat-loop.ts +107 -37
- package/src/self-dev/ask-launch-flow.ts +50 -10
- package/src/self-dev/ask-path-candidates.ts +68 -0
- package/src/self-dev/dev-pipeline.ts +4 -0
- package/src/self-dev/draft-triage-rules.ts +1 -1
- package/src/self-dev/orchestrate-cli.ts +22 -0
- package/src/self-dev/round-pr-supersede.ts +244 -0
- package/src/self-dev/run-store.ts +5 -1
- package/src/self-dev/run-supervisor.ts +21 -1
- package/src/self-dev/supervisor-verdict-edges.ts +46 -0
- package/src/self-implement/follow-up-goals.ts +2 -2
- package/src/self-implement/frozen-merges.ts +25 -20
- package/src/self-implement/gate-remote.ts +427 -0
- package/src/self-implement/goal-author-clarification.ts +81 -0
- package/src/self-implement/goal-author-cli.ts +33 -5
- package/src/self-implement/goal-author.ts +93 -12
- package/src/self-implement/goal-clarification-escalation.ts +37 -1
- package/src/self-implement/goal-document-write.ts +28 -0
- package/src/self-implement/heal-intake.ts +4 -0
- package/src/self-implement/host-regate.ts +3 -1
- package/src/self-implement/l8-merge-queue.ts +29 -5
- package/src/self-implement/orchestrator.ts +78 -13
- package/src/self-implement/prior-draft-findings.ts +4 -3
- package/src/skills/skill-feedback.ts +76 -0
- package/src/task-agent/actions.ts +151 -0
- package/src/task-agent/judge.ts +122 -0
- package/src/task-agent/mission.ts +423 -0
- package/src/task-agent/shadow.ts +90 -0
- package/src/task-agent/task-hand.ts +235 -0
- package/src/task-cards/card-store.ts +4 -1
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +43 -4
- package/src/task-orchestrator/surfaces/pod-lease.ts +5 -2
- package/src/task-orchestrator/surfaces/pod-pool.ts +81 -13
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +225 -35
- package/src/telegram-commands.ts +1 -1
- package/src/telegram-loops-command.ts +22 -15
- package/src/telegram.ts +5 -20
- package/src/user-config.ts +86 -4
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4614-21403aac730d4586.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6877-7c6208705c284124.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8827-e2f5b09a8d37e83b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8949-ecb4128a0858b911.js +0 -1
- package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9247-29d889b809288e65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-1e962b31d1f11081.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-8c2d9526164f5d09.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-76270efaee638f17.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-dce686e582c23937.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-d3d53b146416e422.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-1a61c7ca1ceb3ae7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-51f811fa119308b6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-dacff46049e1557e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-36a7d994f4ca0299.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-5b042736aa113756.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-acb749f1e432bcf3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-57924393e3e4e539.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-31fa1ce4c4aff9ae.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-0f5cd57ca928f42b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-59f03fda76c03d22.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d4aef67abf264874.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-1ea64acadd704ace.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-30c2073bfc21c004.js +0 -1
- package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +0 -1
- package/apps/pwa/out/_next/static/rv9cierV0jSOvDBm1g0XB/_buildManifest.js +0 -1
- /package/apps/pwa/out/_next/static/{rv9cierV0jSOvDBm1g0XB → P6YTVgzuZz5oTZhY4Din_}/_ssgManifest.js +0 -0
|
@@ -33,6 +33,13 @@ export const POD_CONTROL_INBOX_DIR = '/tmp/elanous-control.inbox';
|
|
|
33
33
|
/** Newest pod log bytes returned when the export passes the 5MB artifact limit (headroom under it). */
|
|
34
34
|
export const POD_LOGS_KEEP_BYTES = 4_500_000;
|
|
35
35
|
|
|
36
|
+
/** Re-emit the child's `{kind:"self", ok, result}` line as one flat `{...result, ok}` line at the end of /tmp/si.out.
|
|
37
|
+
* POD-NORESULT ⓑ (10-07 · 28 failed rc=2 Jobs read back): every child printed its result line, but one
|
|
38
|
+
* (si-task-71f68d57f6b7, image 0.2.15) had stderr lines (`[graph] collect fail`) after it, so the old
|
|
39
|
+
* «last line only» check skipped the flatten and the host kept `ok:false` with no stage or PR number.
|
|
40
|
+
* Scan back to the newest child result line instead; still append exactly one line. */
|
|
41
|
+
const POD_RESULT_FLATTEN = `bun -e 'const fs=require("fs");const p="/tmp/si.out";const lines=fs.readFileSync(p,"utf8").trimEnd().split("\\n");for(let i=lines.length-1;i>=0;i--){const l=lines[i].trim();if(!l.startsWith("{"))continue;let o;try{o=JSON.parse(l)}catch{continue}if(!o||o.kind!=="self")continue;if(o.result&&typeof o.result==="object"&&!Array.isArray(o.result))fs.appendFileSync(p,"\\n"+JSON.stringify({...o.result,...(typeof o.ok==="boolean"?{ok:o.ok}:{})})+"\\n");break}'`;
|
|
42
|
+
|
|
36
43
|
export const POD_JOB_DEADLINE_SECONDS = 10_800;
|
|
37
44
|
|
|
38
45
|
/** 자식 컨테이너 «요청» — 🩸 2026-09-27: limits 만 두면 k8s 가 requests=limits(cpu 4)로 잡아, 32코어 노드에
|
|
@@ -43,7 +50,7 @@ export const POD_JOB_DEADLINE_SECONDS = 10_800;
|
|
|
43
50
|
// Node allocatable ≈ 343Gi ≫ 25 × 6Gi. One-minute samples can miss the true peak (lower bound).
|
|
44
51
|
export const POD_CHILD_REQUESTS = { cpu: '1', memory: '6Gi' } as const;
|
|
45
52
|
import type { PodPoolMember, PodPoolScheduler } from './pod-pool.js';
|
|
46
|
-
import { measurePoolLease, recommendConcurrency, POD_HOST_LEASE_ANNOTATION, LEASE_KUBECTL_MAX_BUFFER } from './pod-lease.js';
|
|
53
|
+
import { measurePoolLease, recommendConcurrency, POD_HOST_LEASE_ANNOTATION, LEASE_KUBECTL_MAX_BUFFER, memoryQuantityBytes } from './pod-lease.js';
|
|
47
54
|
import { ACTUAL_SUBSTRATE_ENV, RUN_CONTRACT_ENV, carryRunContract, completionFloorFor } from '../../self-implement/graph-run-contract.js';
|
|
48
55
|
import { spawnSync } from 'node:child_process';
|
|
49
56
|
import { createHash } from 'node:crypto';
|
|
@@ -65,7 +72,7 @@ import { goalTypeOf, type GoalType } from '../../../scripts/measure-pod-memory-b
|
|
|
65
72
|
import { declaredGoalType, type GoalType as DeclaredGoalType } from '../../self-implement/goal-author.js';
|
|
66
73
|
import { readPodMemoryAdvice, type PodMemoryAdvice } from '../../cli/pod-memory-advice.js';
|
|
67
74
|
import { getUserConfig } from '../../user-config.js';
|
|
68
|
-
import { extractPodFailureReason } from './pod-failure-reason.js';
|
|
75
|
+
import { extractPodFailureReason, podTerminalRow } from './pod-failure-reason.js';
|
|
69
76
|
import { OLD_DOOR_STAMP_ENV } from '../../self-dev/old-door.js';
|
|
70
77
|
|
|
71
78
|
export type Kubectl = (args: readonly string[], input?: string) => { status: number | null; stdout: string; stderr: string };
|
|
@@ -119,6 +126,8 @@ export interface PodSpawnOptions {
|
|
|
119
126
|
/** Job 수명 상한(초). */
|
|
120
127
|
deadlineSeconds?: number;
|
|
121
128
|
pollMs?: number;
|
|
129
|
+
/** LAUNCH-STALL: ms a launch may wait in one stage before `launch-stalled` (test seam; default harness.launchStallMinutes · 15 min). */
|
|
130
|
+
launchStallMs?: number;
|
|
122
131
|
kubectl?: Kubectl;
|
|
123
132
|
/** Host-side regate (injected for Pod tests). */
|
|
124
133
|
hostRegate?: (input: { prNumber: number; headCommit: string; repoRoot: string; goalFile?: string }) => Promise<HostRegateResult>;
|
|
@@ -143,6 +152,12 @@ export interface PodSpawnOptions {
|
|
|
143
152
|
now?: () => number;
|
|
144
153
|
sleep?: (ms: number) => Promise<void>;
|
|
145
154
|
credentials?: (account: string) => { elanousAuth: string; codexAuth: string; ghToken: string };
|
|
155
|
+
/** POD-TOKEN-PREREFRESH 시험 심 — «3시간 안 만료» 계정의 호스트 선갱신(기본 refreshCodexAccountHome). */
|
|
156
|
+
codexPrerefresh?: (account: string) => Promise<{ ok: true; beforeH: number | null; afterH: number | null } | { ok: false; kind: string; message: string }>;
|
|
157
|
+
/** PREREFRESH-LOCK 시험 심 — 계정별 갱신 잠금 파일 경로(기본 = 그 계정 codexHome 옆). */
|
|
158
|
+
codexPrerefreshLockPath?: (account: string) => string | null;
|
|
159
|
+
/** 시험 심 — 이 호스트가 본부 임대 보유자인가(기본 HQ 펜스 · kubectl 주입 시험이면 false). */
|
|
160
|
+
hqLeaseHolder?: () => boolean | Promise<boolean>;
|
|
146
161
|
grokCredentials?: () => { grokAuth?: string; grokApiKey?: string; ghToken: string };
|
|
147
162
|
/** 호스트 키 캐시(`~/.cache/<소문자 이름>`)에서 키를 읽는다(시험 주입) — env 에 없을 때. */
|
|
148
163
|
readKeyCache?: (name: string) => string | undefined;
|
|
@@ -173,6 +188,109 @@ export function podJobName(spaceId: string): string {
|
|
|
173
188
|
return `si-${slug || 'job'}-${hash}`.replace(/-+/g, '-').slice(0, 58);
|
|
174
189
|
}
|
|
175
190
|
|
|
191
|
+
/** «3시간 안 만료» — 선갱신 대상이라는 표지(다른 자격 오류와 구분한다). */
|
|
192
|
+
export class PodCodexExpiringError extends Error {
|
|
193
|
+
constructor(readonly account: string, readonly hoursLeft: number) {
|
|
194
|
+
super(`openai-codex:${account} access token 이 3시간 안에 만료 — 호스트에서 먼저 갱신(컨테이너는 갱신 못 한다)`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export type PodCredentialPrerefreshOutcome =
|
|
199
|
+
| 'refreshed' | 'still-expiring' | 'refresh-failed' | 'no-lease'
|
|
200
|
+
// PREREFRESH-LOCK (10-06): a peer refreshed it while we waited for the lock · a failed refresh's one re-check.
|
|
201
|
+
| 'refresh-skipped-already-fresh' | 'refresh-failed-recheck-ok' | 'refresh-failed-recheck-failed'
|
|
202
|
+
// 갱신을 «시도하지 않았다» — 잠금 자리를 못 구했거나 잠금을 못 잡았다(갱신 실패와 가른다 · reason = no-lock-path|lock-timeout|lock-error).
|
|
203
|
+
| 'lock-unavailable';
|
|
204
|
+
|
|
205
|
+
/** POD-TOKEN-PREREFRESH (10-06) — «3시간 안 만료» 계정은 거부만 하지 말고 선갱신을 한 번 시도한다.
|
|
206
|
+
* 🩸 05:3x team · 09:47 third: 거부만 하고 다음 계정으로 안 넘어가 그 계정으로 뽑힌 Pod 런이 전부 pod-error.
|
|
207
|
+
* ⛔ 갱신은 본부(HQ) 임대 보유자 한 곳에서만 — refresh 토큰은 갱신마다 회전해 두 곳이 갱신하면 로그아웃된다.
|
|
208
|
+
* ⭐ PREREFRESH-LOCK (10-06 · #24449 사후 리뷰): 같은 호스트의 두 런이 같은 계정을 «동시에» 고르면 갱신이 둘이 돌아
|
|
209
|
+
* 뒤엣것이 회전된 refresh 토큰으로 실패하고 그 계정을 버렸다. ⇒ 계정별 파일 잠금 안에서 ⑴ 먼저 다시 읽어
|
|
210
|
+
* 이미 신선하면 갱신하지 않고 ⑵ 갱신이 실패해도 한 번 다시 읽어(동료가 갱신했을 수 있다) 신선하면 쓴다.
|
|
211
|
+
* null = 이 계정은 후보에서 뺀다(호출자가 다음 계정으로). 만료 아닌 다른 자격 오류는 그대로 던진다. */
|
|
212
|
+
export async function podCodexCredentialWithPrerefresh(account: string, deps: {
|
|
213
|
+
credentials: (account: string) => { elanousAuth: string; codexAuth: string; ghToken: string };
|
|
214
|
+
refresh: (account: string) => Promise<{ ok: true; beforeH: number | null; afterH: number | null } | { ok: false; kind: string; message: string }>;
|
|
215
|
+
isLeaseHolder: () => boolean | Promise<boolean>;
|
|
216
|
+
log?: (category: string, event: string, data: Record<string, unknown>) => void;
|
|
217
|
+
/** 계정별 갱신 잠금 파일(기본 = 그 계정 codexHome 옆 · `codexAccountRefreshLockPath`). null = 잠금 없음. */
|
|
218
|
+
lockPath?: (account: string) => string | null;
|
|
219
|
+
/** 잠금 대기 조정(시험 심) — 기본 CODEX_REFRESH_LOCK_OPTS. */
|
|
220
|
+
lockOpts?: { staleMs?: number; retryBusyMs?: number; maxTries?: number };
|
|
221
|
+
}): Promise<{ elanousAuth: string; codexAuth: string; ghToken: string } | null> {
|
|
222
|
+
const log = deps.log ?? ((category, event, data) => debug.log(category, event, data));
|
|
223
|
+
try { return deps.credentials(account); }
|
|
224
|
+
catch (error) {
|
|
225
|
+
if (!(error instanceof PodCodexExpiringError)) throw error;
|
|
226
|
+
const beforeH = error.hoursLeft;
|
|
227
|
+
const observe = (outcome: PodCredentialPrerefreshOutcome, afterH: number | null, reason?: string) => {
|
|
228
|
+
try { log('pod.credential-prerefresh', outcome, { account, beforeH, afterH, outcome, ...(reason ? { reason } : {}) }); } catch { /* observation is fail-soft */ }
|
|
229
|
+
};
|
|
230
|
+
let holder = false;
|
|
231
|
+
try { holder = await deps.isLeaseHolder(); } catch { holder = false; }
|
|
232
|
+
if (!holder) { observe('no-lease', null); return null; }
|
|
233
|
+
/** One re-read: fresh credential, null when still expiring. Other credential errors throw. */
|
|
234
|
+
const reread = (): { elanousAuth: string; codexAuth: string; ghToken: string } | { expiringH: number } => {
|
|
235
|
+
try { return deps.credentials(account); }
|
|
236
|
+
catch (again) {
|
|
237
|
+
if (!(again instanceof PodCodexExpiringError)) throw again;
|
|
238
|
+
return { expiringH: again.hoursLeft };
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
const codex = await import('../../oauth/codex.js');
|
|
242
|
+
let lockPath: string | null = null;
|
|
243
|
+
let lockUnknown = false;
|
|
244
|
+
try { lockPath = (deps.lockPath ?? ((name: string) => codex.codexAccountRefreshLockPath(name)))(account); } catch { lockUnknown = true; }
|
|
245
|
+
// 이름 있는 계정인데 잠금 자리를 못 구하면 잠금 없이 갱신하지 않는다 — 막으려던 중복 갱신이 그 자리에서 재발한다.
|
|
246
|
+
// 갱신은 건너뛰고 한 번만 다시 읽는다(동료가 이미 갱신했으면 그대로 쓴다).
|
|
247
|
+
if (lockUnknown || (lockPath === null && account !== (await import('../../oauth/codex-account.js')).DEFAULT_CODEX_ACCOUNT)) {
|
|
248
|
+
const late = reread();
|
|
249
|
+
if (!('expiringH' in late)) { observe('refresh-skipped-already-fresh', null, 'no-lock-path'); return late; }
|
|
250
|
+
observe('lock-unavailable', null, 'no-lock-path');
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
const critical = async () => {
|
|
254
|
+
// ⑴ 잠금을 기다리는 사이 동료 런이 갱신했으면 그대로 쓴다 — 두 번째 갱신이 회전된 토큰으로 실패하는 자리.
|
|
255
|
+
const first = reread();
|
|
256
|
+
if (!('expiringH' in first)) { observe('refresh-skipped-already-fresh', null); return first; }
|
|
257
|
+
let refreshed: Awaited<ReturnType<typeof deps.refresh>> | null = null;
|
|
258
|
+
let failReason = 'threw';
|
|
259
|
+
try { refreshed = await deps.refresh(account); }
|
|
260
|
+
catch { refreshed = null; }
|
|
261
|
+
if (refreshed && !refreshed.ok) failReason = refreshed.kind;
|
|
262
|
+
if (!refreshed || !refreshed.ok) {
|
|
263
|
+
observe('refresh-failed', null, failReason);
|
|
264
|
+
// ⑵ 버리기 전에 한 번 다시 읽는다 — 다른 호스트 경로(사람 `account refresh`)가 방금 갱신했을 수 있다.
|
|
265
|
+
let recheck: ReturnType<typeof reread>;
|
|
266
|
+
try { recheck = reread(); } catch { observe('refresh-failed-recheck-failed', null, 'recheck-error'); return null; }
|
|
267
|
+
if (!('expiringH' in recheck)) { observe('refresh-failed-recheck-ok', null); return recheck; }
|
|
268
|
+
observe('refresh-failed-recheck-failed', recheck.expiringH);
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
const after = reread();
|
|
272
|
+
if ('expiringH' in after) { observe('still-expiring', after.expiringH); return null; }
|
|
273
|
+
observe('refreshed', refreshed.afterH);
|
|
274
|
+
return after;
|
|
275
|
+
};
|
|
276
|
+
try { return await codex.withCodexAccountRefreshLock(lockPath, critical, deps.lockOpts ?? codex.CODEX_REFRESH_LOCK_OPTS); }
|
|
277
|
+
catch (lockError) {
|
|
278
|
+
if (!(lockError instanceof codex.CodexRefreshLockUnavailableError)) throw lockError;
|
|
279
|
+
// 잠금을 못 잡았다(대기 초과 · 잠금 파일 생성 실패) — 갱신은 하지 않고 한 번만 다시 읽는다.
|
|
280
|
+
const late = reread();
|
|
281
|
+
if (!('expiringH' in late)) { observe('refresh-skipped-already-fresh', null, lockError.reason); return late; }
|
|
282
|
+
observe('lock-unavailable', null, lockError.reason);
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** 기본 임대 판정 — 기존 HQ 펜스(`ledger-cli` 역할 · fail-open 없음)를 그대로 쓴다. */
|
|
289
|
+
async function defaultHqLeaseHolder(): Promise<boolean> {
|
|
290
|
+
const { hqFenceDecision } = await import('../../hq/hq.js');
|
|
291
|
+
return hqFenceDecision('ledger-cli').run;
|
|
292
|
+
}
|
|
293
|
+
|
|
176
294
|
/** 호스트 계정 → refresh 없는 사본(elanous 저장소 ⊕ codex auth.json) ⊕ gh 토큰. */
|
|
177
295
|
export function hostCredentials(account: string, storePath: string = authStorePath(), ghToken: () => string = defaultGhToken): { elanousAuth: string; codexAuth: string; ghToken: string } {
|
|
178
296
|
// 경로는 해석기로(격리 게이트 · 2026-09-25) — 손으로 `~/.elanous/auth.json` 을 조립하면 test↔prod 격리가 새는 자리가 된다.
|
|
@@ -185,7 +303,7 @@ export function hostCredentials(account: string, storePath: string = authStorePa
|
|
|
185
303
|
const codex = JSON.parse(readFileSync(join(codexHome, 'auth.json'), 'utf8')) as { tokens: Record<string, unknown> };
|
|
186
304
|
const access = String(codex.tokens.access_token ?? '');
|
|
187
305
|
const exp = Number(JSON.parse(Buffer.from(access.split('.')[1] ?? '', 'base64url').toString('utf8') || '{}').exp ?? 0);
|
|
188
|
-
if (exp * 1000 - Date.now() < 3 * 3600_000) throw new
|
|
306
|
+
if (exp * 1000 - Date.now() < 3 * 3600_000) throw new PodCodexExpiringError(account, Math.round(((exp * 1000 - Date.now()) / 3_600_000) * 10) / 10);
|
|
189
307
|
const elanousAuth = JSON.stringify({
|
|
190
308
|
version: store.version ?? 1,
|
|
191
309
|
// ⭐ P4(2026-09-26): elanous 사본도 «방금 검사한» codex 홈의 토큰으로 싣는다 — 두 저장소는 따로 갱신된다.
|
|
@@ -600,7 +718,7 @@ export function podChildLlmArgs(options: Pick<PodSpawnOptions, 'provider' | 'chi
|
|
|
600
718
|
return ['--child-llm-provider', provider, ...(model ? ['--child-llm-model', model] : [])];
|
|
601
719
|
}
|
|
602
720
|
|
|
603
|
-
export function podJobManifest(o: { name: string; namespace: string; image: string; /** 레지스트리 이미지면 IfNotPresent(노드가 pull) · 반입 이미지면 Never. */ imagePullPolicy?: 'Never' | 'IfNotPresent'; repoUrl: string; source?: PodSource; hostMirror?: string; args: readonly string[]; passEnv: readonly string[]; deadlineSeconds: number; runId?: string; parentRunId?: string; armEnv?: Readonly<Record<string, string>>; hostId?: string; imageCommit?: string | null; skillEnvs?: readonly string[]; memoryLimit?: string; memoryRequest?: string; goalDoc?: string; /** AUTHOR-POD2 — run `harness say` on /creds/feature inside the Pod (authoring happens off the host). */ authorSentence?: boolean; grokCredential?: 'subscription' | 'api_key'; codexAccounts?: readonly string[]; appCredential?: boolean; /** Test seam — default POD_GH_STALE_SECONDS. */ githubStaleSeconds?: number; /** Which goal execution and attempt this Job is — a resumed host verifies it before following the Job (POD9). */ execution?: { key: string; attempt: number }; hostLeaseAdmitted?: boolean }): Record<string, unknown> {
|
|
721
|
+
export function podJobManifest(o: { name: string; namespace: string; image: string; /** 레지스트리 이미지면 IfNotPresent(노드가 pull) · 반입 이미지면 Never. */ imagePullPolicy?: 'Never' | 'IfNotPresent'; repoUrl: string; source?: PodSource; hostMirror?: string; args: readonly string[]; passEnv: readonly string[]; deadlineSeconds: number; runId?: string; parentRunId?: string; armEnv?: Readonly<Record<string, string>>; hostId?: string; imageCommit?: string | null; skillEnvs?: readonly string[]; memoryLimit?: string; memoryRequest?: string; goalDoc?: string; /** AUTHOR-POD2 — run `harness say` on /creds/feature inside the Pod (authoring happens off the host). */ authorSentence?: boolean; authorGrade?: 'full' | 'lite'; grokCredential?: 'subscription' | 'api_key'; codexAccounts?: readonly string[]; appCredential?: boolean; /** Test seam — default POD_GH_STALE_SECONDS. */ githubStaleSeconds?: number; /** Which goal execution and attempt this Job is — a resumed host verifies it before following the Job (POD9). */ execution?: { key: string; attempt: number }; hostLeaseAdmitted?: boolean }): Record<string, unknown> {
|
|
604
722
|
const quoted = o.args.map((a) => `'${a.replace(/'/g, `'\\''`)}'`).join(' ');
|
|
605
723
|
const goalPath = o.goalDoc ? `'${(o.goalDoc.startsWith('-') ? `./${o.goalDoc}` : o.goalDoc).replace(/'/g, `'\\''`)}'` : undefined;
|
|
606
724
|
const delegated = Boolean(o.goalDoc) || o.authorSentence === true;
|
|
@@ -629,10 +747,12 @@ export function podJobManifest(o: { name: string; namespace: string; image: stri
|
|
|
629
747
|
`(while :; do { mem=$(if [ -r /sys/fs/cgroup/memory.current ]; then cat /sys/fs/cgroup/memory.current; elif [ -r /sys/fs/cgroup/memory/memory.usage_in_bytes ]; then cat /sys/fs/cgroup/memory/memory.usage_in_bytes; else printf -- -; fi); top=$(ps -eo rss=,comm=,args= --sort=-rss | head -5 | awk 'function encode(s) { gsub(/%/, "%25", s); gsub(/:/, "%3A", s); gsub(/ /, "%20", s); gsub(/\\t/, "%09", s); return s } { rss=$1; name=$2; sub(/^[[:space:]]*[0-9]+[[:space:]]+[^[:space:]]+[[:space:]]*/, ""); n=split($0, a, /[[:space:]]+/); cmd="<redacted>"; if (name=="sleep" && a[2]=="30") cmd="sleep 30" (n>2 ? " <redacted>" : ""); else if (name=="bun") { cmd="bun <redacted>"; if (a[2]=="test") cmd="bun test <redacted>"; else if (a[2]=="run") cmd="bun run <redacted>"; else if ((a[2]=="x" || a[2]=="exec") && a[3]=="tsc") cmd="bun x tsc <redacted>" } else if (name=="tsc") cmd="tsc <redacted>"; else if (name=="elanous") { cmd="elanous <redacted>"; if (a[2]=="self") cmd="elanous self <redacted>"; else if (a[2]=="harness") cmd="elanous harness <redacted>" } else if (name=="node") cmd="node <redacted>"; if (name=="sleep" && cmd=="<redacted>") cmd="sleep <redacted>"; else if (cmd=="<redacted>" && name!="bash" && name!="sh") name="other"; printf " %s:%s:%s", rss, encode(name), encode(substr(cmd,1,120)) }'); printf "ELANOUS_MEM %s %s%s\\n" "$(date +%s)" "$mem" "$top"; } || true; sleep 15 || break; done) & mem_sampler_pid=$!`,
|
|
630
748
|
...(o.appCredential ? [podGithubWatchdogScript(o.githubStaleSeconds ?? POD_GH_STALE_SECONDS)] : []),
|
|
631
749
|
// 마지막 줄 JSON 이 «맨 끝»이어야 한다(parseSelfImplementJson) — rollup 은 그 앞에.
|
|
750
|
+
// ⛔ `--author-grade` 는 Pod 안 `elanous`(이미지에 깔린 판)가 모를 수 있어 넘기지 않는다 — 10-06 «unknown option» 즉사(#24445 되돌림).
|
|
751
|
+
// AUTHOR-LITE2-POD — 등급은 환경변수 `ELANOUS_AUTHOR_GRADE` 로 싣는다(옛 Pod elanous 는 모르는 환경변수를 무시하고 full 로 돈다).
|
|
632
752
|
o.authorSentence
|
|
633
|
-
? `echo "ELANOUS_AUTHOR_ON_POD started host=$(hostname) at=$(date -u +%Y-%m-%dT%H:%M:%SZ)"; author_t0=$(date +%s); elanous harness say --substrate local --json${quotedAsk} -- "$(cat /creds/feature)" > /tmp/si.out 2>&1; rc=$?; echo "ELANOUS_AUTHOR_ON_POD finished host=$(hostname) rc=$rc seconds=$(( $(date +%s) - author_t0 ))";
|
|
753
|
+
? `echo "ELANOUS_AUTHOR_ON_POD started host=$(hostname) at=$(date -u +%Y-%m-%dT%H:%M:%SZ)"; author_t0=$(date +%s); ${o.authorGrade === 'lite' || o.authorGrade === 'full' ? `ELANOUS_AUTHOR_GRADE=${o.authorGrade} ` : ''}elanous harness say --substrate local --json${quotedAsk} -- "$(cat /creds/feature)" > /tmp/si.out 2>&1; rc=$?; echo "ELANOUS_AUTHOR_ON_POD finished host=$(hostname) rc=$rc seconds=$(( $(date +%s) - author_t0 ))"; ${POD_RESULT_FLATTEN}`
|
|
634
754
|
: goalPath
|
|
635
|
-
? `elanous harness ask ${goalPath} --json${quotedAsk} > /tmp/si.out 2>&1; rc=$?;
|
|
755
|
+
? `elanous harness ask ${goalPath} --json${quotedAsk} > /tmp/si.out 2>&1; rc=$?; ${POD_RESULT_FLATTEN}`
|
|
636
756
|
: `export ${OLD_DOOR_STAMP_ENV}=self-implement; elanous self implement "$(cat /creds/feature)" --json ${quoted} > /tmp/si.out 2>&1; rc=$?`,
|
|
637
757
|
'cat /tmp/si.out',
|
|
638
758
|
'[ -f scripts/usage-rollup.ts ] && bun scripts/usage-rollup.ts --since 12h || echo "ELANOUS_USAGE_ROLLUP {\"measured\":false,\"reason\":\"no rollup script\"}"',
|
|
@@ -865,31 +985,72 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
|
|
|
865
985
|
let releaseAdmission: ((() => void) & { applied?: (job: string, context: string, namespace: string) => void; observed?: () => void }) | undefined;
|
|
866
986
|
try {
|
|
867
987
|
// Every attempt, including an OOM retry, must acquire a pool slot before applying a Job.
|
|
988
|
+
const stallMs = options.launchStallMs ?? (getUserConfig().harness?.launchStallMinutes ?? 15) * 60_000;
|
|
989
|
+
// LAUNCH-STALL (10-06): a stage that waits past the cap says so once — today Pods sat 22 min to 5 h with no event.
|
|
990
|
+
const stalledStages = new Set<string>();
|
|
991
|
+
// pool-slot reason = what THIS launch saw on its last empty tryAcquire; admission reason = the pool-wide lease measurement (one pool, one reading).
|
|
992
|
+
const stageReason: { 'pool-slot'?: string | null } = {};
|
|
993
|
+
const unfitContexts = new Set<string>();
|
|
994
|
+
const watchStall = (stage: 'admission' | 'pool-slot'): (() => void) => {
|
|
995
|
+
const started = Date.now();
|
|
996
|
+
const timer = setTimeout(() => {
|
|
997
|
+
if (stalledStages.has(stage)) return;
|
|
998
|
+
stalledStages.add(stage);
|
|
999
|
+
const pool = options.pool as { waitReason?: () => string | null } | undefined;
|
|
1000
|
+
debug.log('harness.launch', 'launch-stalled', { spaceId: input.spaceId, runId: childRunId, stage, waitedSec: Math.round((Date.now() - started) / 1000),
|
|
1001
|
+
reason: (stage === 'pool-slot' ? stageReason['pool-slot'] : (typeof pool?.waitReason === 'function' ? pool.waitReason() : null)) ?? 'not-measured-yet' }, { level: 'warn' });
|
|
1002
|
+
}, stallMs);
|
|
1003
|
+
(timer as { unref?: () => void }).unref?.();
|
|
1004
|
+
return () => clearTimeout(timer);
|
|
1005
|
+
};
|
|
1006
|
+
// Members that can never hold this Job's memory limit are skipped with a reason; all unfit → fail fast instead of waiting forever.
|
|
1007
|
+
const unfitFailure = async (): Promise<SelfImplementJobDone | null> => {
|
|
1008
|
+
const pool = options.pool as { unfitMembers?: (b: number) => Promise<Array<{ context: string; allocatableMemoryBytes: number }>>; members?: readonly PodPoolMember[] } | undefined;
|
|
1009
|
+
if (!pool || typeof pool.unfitMembers !== 'function' || !Array.isArray(pool.members)) return null;
|
|
1010
|
+
const limitBytes = memoryQuantityBytes(memoryLimit);
|
|
1011
|
+
if (limitBytes === null) return null;
|
|
1012
|
+
const unfit = await pool.unfitMembers(limitBytes);
|
|
1013
|
+
for (const u of unfit) {
|
|
1014
|
+
debug.log('harness.launch', 'launch-member-skipped', { spaceId: input.spaceId, runId: childRunId, member: u.context, reason: 'memory-never-fits', allocatableMemoryBytes: u.allocatableMemoryBytes, memoryLimit }, { level: 'warn' });
|
|
1015
|
+
}
|
|
1016
|
+
unfitContexts.clear();
|
|
1017
|
+
for (const u of unfit) unfitContexts.add(u.context);
|
|
1018
|
+
if (!unfit.length || !pool.members.every((m) => unfitContexts.has(m.context))) return null;
|
|
1019
|
+
const detail = unfit.map((u) => `${u.context} ${Math.round(u.allocatableMemoryBytes / 2 ** 30 * 10) / 10}GiB`).join(' · ');
|
|
1020
|
+
return { exitCode: 1, output: '', error: { code: 'pod-pool-unfit', message: `모든 풀 멤버의 할당 가능 메모리가 이 Job 의 한도 ${memoryLimit} 보다 작다 — 영원히 배치될 수 없다(${detail}) · --pod-memory 를 낮추거나 다른 풀로` } };
|
|
1021
|
+
};
|
|
868
1022
|
const acquireSlot = async (): Promise<PodPoolMember | null> => {
|
|
869
1023
|
if (!options.pool) return null;
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
1024
|
+
const stop = watchStall('pool-slot');
|
|
1025
|
+
try {
|
|
1026
|
+
for (;;) {
|
|
1027
|
+
if (input.signal?.aborted) return null;
|
|
1028
|
+
// The reason comes back inside this call, so a concurrent launch cannot overwrite it.
|
|
1029
|
+
const acquired = await options.pool.tryAcquire(unfitContexts, (reason) => { stageReason['pool-slot'] = reason; });
|
|
1030
|
+
if (acquired) {
|
|
1031
|
+
debug.log('self-implement.pod', 'pool-slot', { spaceId: input.spaceId, context: acquired.context, inflight: options.pool.snapshot() });
|
|
1032
|
+
return acquired;
|
|
1033
|
+
}
|
|
1034
|
+
await sleep(options.pollMs ?? 15_000);
|
|
876
1035
|
}
|
|
877
|
-
|
|
878
|
-
}
|
|
1036
|
+
} finally { stop(); }
|
|
879
1037
|
};
|
|
880
1038
|
const acquireAdmission = async (): Promise<boolean> => {
|
|
881
1039
|
if (!options.pool || typeof options.pool.acquireAdmission !== 'function') return true;
|
|
1040
|
+
const stop = watchStall('admission');
|
|
882
1041
|
try { releaseAdmission = await options.pool.acquireAdmission(input.signal, after); return true; }
|
|
883
1042
|
catch (error) {
|
|
884
1043
|
// A predecessor closed without merging is not a cancellation: surface it so the queue owner is notified.
|
|
885
1044
|
if (error instanceof Error && error.message.startsWith('pod lease predecessor blocked')) admissionBlocked = error.message;
|
|
886
1045
|
return false;
|
|
887
|
-
}
|
|
1046
|
+
} finally { stop(); }
|
|
888
1047
|
};
|
|
889
1048
|
let admissionBlocked: string | undefined;
|
|
890
1049
|
const admissionFailure = (): SelfImplementJobDone => admissionBlocked
|
|
891
1050
|
? { exitCode: 1, output: '', error: { code: 'pod-predecessor-blocked', message: admissionBlocked } }
|
|
892
1051
|
: { exitCode: null, output: '', error: { code: 'aborted', message: 'aborted before a pool slot opened' } };
|
|
1052
|
+
const unfitDone = await unfitFailure();
|
|
1053
|
+
if (unfitDone) return unfitDone;
|
|
893
1054
|
if (!await acquireAdmission()) return admissionFailure();
|
|
894
1055
|
let member: PodPoolMember | null = await acquireSlot();
|
|
895
1056
|
if (options.pool && !member) return { exitCode: null, output: '', error: { code: 'aborted', message: 'aborted before a pool slot opened' } };
|
|
@@ -941,25 +1102,43 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
|
|
|
941
1102
|
for (;;) {
|
|
942
1103
|
try {
|
|
943
1104
|
const grok = options.provider === 'grok';
|
|
944
|
-
const
|
|
945
|
-
debug.log('self-implement.pod', 'account', { spaceId: input.spaceId, account, brokered: Boolean(options.accountBroker) });
|
|
946
|
-
const
|
|
947
|
-
if (
|
|
948
|
-
if (
|
|
1105
|
+
const plannedAccount = retryAccount ?? (grok ? 'grok' : options.accountBroker?.() ?? options.account ?? 'team');
|
|
1106
|
+
debug.log('self-implement.pod', 'account', { spaceId: input.spaceId, account: plannedAccount, brokered: Boolean(options.accountBroker) });
|
|
1107
|
+
const plannedCodexAccounts = grok || !options.accountBroker || !options.rotationAccounts ? undefined : [plannedAccount, ...options.rotationAccounts.filter((candidate) => candidate !== plannedAccount && candidate !== failedAccount)];
|
|
1108
|
+
if (plannedCodexAccounts && !options.rotationAccounts!.includes(plannedAccount)) throw new Error(`pod: 배분 계정 ${plannedAccount} 이 회전 계획에 없다`);
|
|
1109
|
+
if (plannedCodexAccounts && new Set(options.rotationAccounts).size !== options.rotationAccounts!.length) throw new Error('pod: 유효하고 서로 다른 codex 계정이 필요하다');
|
|
949
1110
|
const accountCredentials = options.credentials ?? hostCredentials;
|
|
950
|
-
if (
|
|
951
|
-
|
|
952
|
-
|
|
1111
|
+
if (plannedCodexAccounts) podCodexAccountScript(plannedCodexAccounts);
|
|
1112
|
+
// POD-TOKEN-PREREFRESH: «3시간 안 만료» 후보는 선갱신 → 실패면 후보에서 빼고 다음 계정으로.
|
|
1113
|
+
const prerefreshDeps = {
|
|
1114
|
+
credentials: accountCredentials,
|
|
1115
|
+
refresh: options.codexPrerefresh ?? (async (name: string) => (await import('../../oauth/codex.js')).refreshCodexAccountHome(name)),
|
|
1116
|
+
isLeaseHolder: options.hqLeaseHolder ?? (options.kubectl ? () => false : defaultHqLeaseHolder), // kubectl 주입(=시험)이면 본부 임대를 묻지 않는다
|
|
1117
|
+
// PREREFRESH-LOCK: 시험(kubectl 주입)은 정본 스토어를 읽어 잠금 경로를 풀지 않는다 — 임시 디렉터리의 계정별 잠금.
|
|
1118
|
+
// 잠금 자리는 주입 여부와 무관하게 계정 홈 옆 하나 — 시험은 codexPrerefreshLockPath 로 명시한다(같은 계정이 서로 다른 잠금을 쓰지 않게).
|
|
1119
|
+
...(options.codexPrerefreshLockPath ? { lockPath: options.codexPrerefreshLockPath } : {}),
|
|
1120
|
+
};
|
|
1121
|
+
const candidates = grok ? [] : plannedCodexAccounts ?? [plannedAccount];
|
|
1122
|
+
const usable: Array<{ name: string; credential: ReturnType<typeof accountCredentials> }> = [];
|
|
1123
|
+
for (const candidate of candidates) {
|
|
1124
|
+
const credential = await podCodexCredentialWithPrerefresh(candidate, prerefreshDeps);
|
|
1125
|
+
if (credential) usable.push({ name: candidate, credential });
|
|
1126
|
+
}
|
|
1127
|
+
if (!grok && usable.length === 0) throw new Error(`openai-codex 후보(${candidates.join(', ')}) access token 이 전부 3시간 안에 만료 — 선갱신도 못 했다(본부 임대 없음 또는 갱신 실패 · pod.credential-prerefresh 를 보라)`);
|
|
1128
|
+
const account = grok ? plannedAccount : usable[0]!.name;
|
|
1129
|
+
if (account !== plannedAccount) debug.log('self-implement.pod', 'account-skipped-expiring', { spaceId: input.spaceId, from: plannedAccount, to: account });
|
|
1130
|
+
const codexAccounts = plannedCodexAccounts ? usable.map((entry) => entry.name) : undefined;
|
|
1131
|
+
const codexCredentials = codexAccounts ? usable.map(({ name: candidate, credential }) => {
|
|
953
1132
|
const elanous = JSON.parse(credential.elanousAuth) as { providers?: Record<string, { tokens?: { refreshToken?: unknown } }> };
|
|
954
1133
|
const codex = JSON.parse(credential.codexAuth) as { tokens?: { refresh_token?: unknown } };
|
|
955
1134
|
if (!elanous.providers?.['openai-codex']?.tokens || elanous.providers['openai-codex'].tokens.refreshToken || !codex.tokens || codex.tokens.refresh_token) {
|
|
956
1135
|
throw new Error(`pod: ${candidate} 자격에 refresh 토큰이 있거나 인증 파일이 없다`);
|
|
957
1136
|
}
|
|
958
1137
|
return credential;
|
|
959
|
-
});
|
|
1138
|
+
}) : undefined;
|
|
960
1139
|
const creds = grok
|
|
961
1140
|
? (options.grokCredentials ?? (() => hostGrokCredentials({ env, apiKeyOptIn: options.grokApiKeyOptIn })))()
|
|
962
|
-
: codexCredentials?.[0] ??
|
|
1141
|
+
: codexCredentials?.[0] ?? usable[0]!.credential;
|
|
963
1142
|
if (grok && !('grokAuth' in creds && creds.grokAuth) && !('grokApiKey' in creds && creds.grokApiKey && options.grokApiKeyOptIn === true)) {
|
|
964
1143
|
throw new Error('grok: 구독 자격 없음 · API 키 opt-in 꺼짐 또는 키 없음');
|
|
965
1144
|
}
|
|
@@ -1211,12 +1390,15 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
|
|
|
1211
1390
|
? String(detail.until) : undefined;
|
|
1212
1391
|
const armEnv = Object.fromEntries(Object.entries(options.armEnv ?? {}).filter(([key]) => key !== 'ELANOUS_LIVE_DETAIL_UNTIL' && key !== POD_GITHUB_CREDENTIAL_TOKEN_ENV && key !== POD_GITHUB_CREDENTIAL_URL_ENV && (!hostRefresh || !['GH_TOKEN', 'GITHUB_TOKEN', 'GH_CONFIG_DIR'].includes(key))));
|
|
1213
1392
|
const seat = env.ELANOUS_HARNESS_SEAT;
|
|
1214
|
-
const jobArmEnv = grounding || credentialRelay || githubRelay || env.ELANOUS_DISPATCH_RECORDED === '1' || options.armEnv || detailUntil || seat
|
|
1393
|
+
const jobArmEnv = grounding || credentialRelay || githubRelay || env.ELANOUS_DISPATCH_RECORDED === '1' || options.armEnv || detailUntil || seat || env.ELANOUS_POD_AUTHOR_ON_POD === '1'
|
|
1215
1394
|
? {
|
|
1216
1395
|
...armEnv,
|
|
1217
1396
|
...(detailUntil ? { ELANOUS_LIVE_DETAIL_UNTIL: detailUntil } : {}),
|
|
1218
1397
|
...(env.ELANOUS_DISPATCH_RECORDED === '1' ? { ELANOUS_DISPATCH_RECORDED: '1' } : {}),
|
|
1219
1398
|
...(seat === 'OP' || seat === 'TC' || seat === 'MK' || seat === 'UX' ? { ELANOUS_HARNESS_SEAT: seat } : {}),
|
|
1399
|
+
...(env.ELANOUS_POD_AUTHOR_ON_POD === '1'
|
|
1400
|
+
? { ELANOUS_POD_AUTHOR_ON_POD: '1', ...(env.ELANOUS_POD_AUTHOR_GRADE_SOURCE === 'flag' || env.ELANOUS_POD_AUTHOR_GRADE_SOURCE === 'config' || env.ELANOUS_POD_AUTHOR_GRADE_SOURCE === 'default'
|
|
1401
|
+
? { ELANOUS_POD_AUTHOR_GRADE_SOURCE: env.ELANOUS_POD_AUTHOR_GRADE_SOURCE, ...(env.ELANOUS_POD_AUTHOR_GRADE === 'lite' || env.ELANOUS_POD_AUTHOR_GRADE === 'full' ? { ELANOUS_POD_AUTHOR_GRADE: env.ELANOUS_POD_AUTHOR_GRADE } : {}) } : {}) } : {}),
|
|
1220
1402
|
...(grounding ? { [GROUNDING_URL_ENV]: groundingUrl! } : {}),
|
|
1221
1403
|
...(credentialRelay ? { [POD_CREDENTIAL_URL_ENV]: `${new URL(groundingUrl!).origin}${POD_CREDENTIAL_GROK_PATH}` } : {}),
|
|
1222
1404
|
...(githubRelay ? { [POD_GITHUB_CREDENTIAL_URL_ENV]: `${new URL(groundingUrl!).origin}${POD_CREDENTIAL_GITHUB_PATH}` } : {}),
|
|
@@ -1233,7 +1415,7 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
|
|
|
1233
1415
|
debug.log('self-implement.pod', 'memory-request', { spaceId: input.spaceId, job: name,
|
|
1234
1416
|
tier: oomRetried ? retryTier : memoryTier, reason: oomRetried ? 'OOMKilled' : memoryReason,
|
|
1235
1417
|
memoryLimit, memoryRequest });
|
|
1236
|
-
const job = podJobManifest({ name, namespace, image: jobImage, ...(member?.imageRef ? { imagePullPolicy: 'IfNotPresent' as const } : {}), repoUrl, ...(options.source ? { source: options.source } : {}), ...(hostMirror ? { hostMirror } : {}), args, passEnv: jobPassEnv, deadlineSeconds: options.deadlineSeconds ?? POD_JOB_DEADLINE_SECONDS, ...(goalDoc ? { goalDoc } : env.ELANOUS_POD_AUTHOR_ON_POD === '1' && !shard ? { authorSentence: true } : {}), runId: launchRunId, ...(parentRunId ? { parentRunId } : {}), ...(jobArmEnv ? { armEnv: jobArmEnv } : {}), hostId: resolveHostId(env), skillEnvs: Object.keys(skillEnvs), memoryLimit, memoryRequest, imageCommit: options.imageCommit !== undefined ? options.imageCommit : options.kubectl ? null : podImageFreshness({ image }).imageCommit, ...(grok ? { grokCredential: 'grokAuth' in creds && creds.grokAuth ? 'subscription' as const : 'api_key' as const } : {}), ...(codexAccounts ? { codexAccounts } : {}), ...(hostRefresh ? { appCredential: true } : {}), execution: { key: executionKey, attempt: oomRetried ? 2 : 1 }, hostLeaseAdmitted: !!releaseAdmission }); // kubectl 주입(=시험)이면 docker 를 부르지 않는다
|
|
1418
|
+
const job = podJobManifest({ name, namespace, image: jobImage, ...(member?.imageRef ? { imagePullPolicy: 'IfNotPresent' as const } : {}), repoUrl, ...(options.source ? { source: options.source } : {}), ...(hostMirror ? { hostMirror } : {}), args, passEnv: jobPassEnv, deadlineSeconds: options.deadlineSeconds ?? POD_JOB_DEADLINE_SECONDS, ...(goalDoc ? { goalDoc } : env.ELANOUS_POD_AUTHOR_ON_POD === '1' && !shard ? { authorSentence: true, ...(env.ELANOUS_POD_AUTHOR_GRADE === 'lite' || env.ELANOUS_POD_AUTHOR_GRADE === 'full' ? { authorGrade: env.ELANOUS_POD_AUTHOR_GRADE } : {}) } : {}), runId: launchRunId, ...(parentRunId ? { parentRunId } : {}), ...(jobArmEnv ? { armEnv: jobArmEnv } : {}), hostId: resolveHostId(env), skillEnvs: Object.keys(skillEnvs), memoryLimit, memoryRequest, imageCommit: options.imageCommit !== undefined ? options.imageCommit : options.kubectl ? null : podImageFreshness({ image }).imageCommit, ...(grok ? { grokCredential: 'grokAuth' in creds && creds.grokAuth ? 'subscription' as const : 'api_key' as const } : {}), ...(codexAccounts ? { codexAccounts } : {}), ...(hostRefresh ? { appCredential: true } : {}), execution: { key: executionKey, attempt: oomRetried ? 2 : 1 }, hostLeaseAdmitted: !!releaseAdmission }); // kubectl 주입(=시험)이면 docker 를 부르지 않는다
|
|
1237
1419
|
const a = kubectl(['apply', '-f', '-'], JSON.stringify(job));
|
|
1238
1420
|
if (a.status !== 0) { cleanupSecret(); return { exitCode: 1, output: a.stderr, error: { code: 'pod-apply', message: a.stderr.trim() } }; }
|
|
1239
1421
|
releaseAdmission?.applied?.(name, context, namespace);
|
|
@@ -1433,12 +1615,15 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
|
|
|
1433
1615
|
}
|
|
1434
1616
|
const releaseHold = regate.failures[0]?.step === 'release-path-hold';
|
|
1435
1617
|
const frozen = regate.status === 'frozen';
|
|
1618
|
+
// FREEZE-POD: the host is the only merger of a Pod run, so its freeze check here is the one that holds it.
|
|
1619
|
+
if (frozen) debug.log('self-implement.pod', 'merge-blocked-by-freeze', { job: name, runId: liveChildRunId, pr: prNumber ?? null, reason: 'landing-freeze' });
|
|
1436
1620
|
disposition = { ...disposition, stage: frozen || releaseHold ? 'pr-opened' : regate.passed ? 'merged' : 'host-regate-failed', merged: regate.passed && !frozen, hostRegate: regate, ok: regate.passed || releaseHold || frozen };
|
|
1437
1621
|
}
|
|
1438
1622
|
const childFailure = state === 'failed' && !oomKilled && failedReason !== 'DeadlineExceeded'
|
|
1439
1623
|
? lastPodChildFailure(logs) : null;
|
|
1440
1624
|
let noResultDiagnostic: string | null = null;
|
|
1441
|
-
|
|
1625
|
+
// POD-NORESULT: the Job runs with backoffLimit 0, so any non-zero child exit is «BackoffLimitExceeded» — not a reason to skip the ledger.
|
|
1626
|
+
if (state === 'failed' && containerExitCode !== null && containerExitCode !== 0 && !disposition && !oomKilled && failedReason !== 'DeadlineExceeded') {
|
|
1442
1627
|
try { noResultDiagnostic = podNoResultDiagnostic(readFileSync(runLedgerPath(liveChildRunId, ledgerDir), 'utf8')); }
|
|
1443
1628
|
catch { /* ledger not returned: do not invent a stage */ }
|
|
1444
1629
|
noResultDiagnostic ??= 'child terminal result missing; last ledger stage=unknown; round=unknown; mustFix=unknown';
|
|
@@ -1449,7 +1634,9 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
|
|
|
1449
1634
|
}
|
|
1450
1635
|
debug.log('self-implement.pod', 'job-finished', { job: name, ...(member ? { context: member.context } : {}), state, containerReason, stage: disposition?.stage ?? null, prUrl: disposition?.prUrl ?? null, childRunId: liveChildRunId, ledgerCompleteness,
|
|
1451
1636
|
...(state === 'failed' && !oomKilled && failedReason !== 'DeadlineExceeded'
|
|
1452
|
-
? { ...(childFailure ? { childStage: childFailure.stage } : {
|
|
1637
|
+
? { ...(childFailure ? { childStage: childFailure.stage } : disposition?.stage ? { childStage: disposition.stage } : {}),
|
|
1638
|
+
// POD-NORESULT: a parsed result row without error text is not a missing result line (UX 10-07 01:01: 28 of 28 exit-2 Jobs printed one).
|
|
1639
|
+
childError: childFailure?.error ?? (disposition?.stage ? 'result-without-error' : 'no-result-line') } : {}) },
|
|
1453
1640
|
state === 'failed' && !oomKilled && failedReason !== 'DeadlineExceeded' ? { compact: { stringMax: 500 } } : undefined);
|
|
1454
1641
|
if (oomKilled && reattach && !oomRetried && (!reattachedMemoryLimit || memoryGi(reattachedMemoryLimit) === null)) {
|
|
1455
1642
|
return { exitCode: 1, output: '', error: { code: 'pod-memory-unavailable', message: `Job ${name} 실제 메모리 한도를 확인할 수 없다` } };
|
|
@@ -1477,6 +1664,8 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
|
|
|
1477
1664
|
if (member) { options.pool!.release(member); member = null; }
|
|
1478
1665
|
releaseAdmission?.();
|
|
1479
1666
|
releaseAdmission = undefined;
|
|
1667
|
+
const unfitRetry = await unfitFailure();
|
|
1668
|
+
if (unfitRetry) return unfitRetry;
|
|
1480
1669
|
if (!await acquireAdmission()) return admissionFailure();
|
|
1481
1670
|
member = await acquireSlot();
|
|
1482
1671
|
if (options.pool && !member) return { exitCode: null, output: '', error: { code: 'aborted', message: 'aborted before a pool slot opened' } };
|
|
@@ -1518,7 +1707,7 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
|
|
|
1518
1707
|
: disposition;
|
|
1519
1708
|
const podExitCode = state === 'complete' && disposition?.stage !== 'host-regate-failed' && disposition?.ok !== false ? 0 : 1;
|
|
1520
1709
|
// SCHED-CONTRACT: a failed child must say why outside the Pod, not «no error diagnostic».
|
|
1521
|
-
const podReason = podExitCode === 1 ? extractPodFailureReason({ logs, logTailReason, containerReason, jobReason: failedReason || undefined, deadlineSeconds }) : undefined;
|
|
1710
|
+
const podReason = podExitCode === 1 ? extractPodFailureReason({ logs, logTailReason, containerReason, jobReason: failedReason || undefined, deadlineSeconds, result: disposition ? { stage: disposition.stage, prUrl: disposition.prUrl ?? null, prNumber: disposition.prNumber ?? null } : null }) : undefined;
|
|
1522
1711
|
if (podReason) debug.log('self-implement.pod', 'failure-reason', { job: name, reason: podReason });
|
|
1523
1712
|
return {
|
|
1524
1713
|
exitCode: podExitCode,
|
|
@@ -1530,7 +1719,7 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
|
|
|
1530
1719
|
? { error: { code: 'pod-gh-login-failed', message: `Job ${name} GitHub 앱 로그인 실패 (container=${containerReason ?? 'unknown'}/7, attempt=${ghLoginRetried ? 2 : 1}) — 새 토큰으로 ${ghLoginRetried ? '1회 재시도했으나 다시 실패' : '재시도 불가'}` } }
|
|
1531
1720
|
: oomKilled
|
|
1532
1721
|
? { error: { code: 'pod-oom-killed', message: `Job ${name} failed (OOMKilled/${containerExitCode ?? 'unknown'}, memoryLimit=${memoryLimit})${oomRetried || (memoryGi(reattachedMemoryLimit ?? memoryLimit) ?? 0) >= (memoryGi(podMemoryLimitFor(input.feature, { ...env, ELANOUS_POD_MEMORY_TIER: 'high' }).limit) ?? Infinity) ? ` — OOM · ${oomRetried ? retryTier : 'high'} 에서도` : ''}\n${formatLastMemSample(samples.at(-1))}` } }
|
|
1533
|
-
: { error: { code: 'pod-job-failed', message: `Job ${name} failed${failedReason || containerReason ? ` (${[failedReason, containerReason ? `container=${containerReason}/${containerExitCode ?? 'unknown'}` : ''].filter(Boolean).join(', ')})` : ''}${childFailure ? ` — childStage=${childFailure.stage} · childError=${childFailure.error}` : ` — childError=no-result-line · reason=${podReason}`}${noResultDiagnostic ? ` · ${noResultDiagnostic}` : ''}` } }
|
|
1722
|
+
: { error: { code: 'pod-job-failed', message: `Job ${name} failed${failedReason || containerReason ? ` (${[failedReason, containerReason ? `container=${containerReason}/${containerExitCode ?? 'unknown'}` : ''].filter(Boolean).join(', ')})` : ''}${childFailure ? ` — childStage=${childFailure.stage} · childError=${childFailure.error}` : disposition?.stage ? ` — childStage=${disposition.stage} · reason=${podReason}` : ` — childError=no-result-line · reason=${podReason}`}${noResultDiagnostic ? ` · ${noResultDiagnostic}` : ''}` } }
|
|
1534
1723
|
: podReason && !childFailure?.error ? { error: { code: 'pod-child-failed', message: podReason } } : {}),
|
|
1535
1724
|
...(finishedDisposition ? { disposition: finishedDisposition } : {}),
|
|
1536
1725
|
};
|
|
@@ -1602,12 +1791,13 @@ function lastPodChildFailure(logs: string): { stage: string; error: string } | n
|
|
|
1602
1791
|
try { result = JSON.parse(line); } catch { continue; }
|
|
1603
1792
|
if (!result || typeof result !== 'object' || Array.isArray(result)) continue;
|
|
1604
1793
|
const row = result as Record<string, unknown>;
|
|
1605
|
-
|
|
1606
|
-
if (
|
|
1607
|
-
|
|
1794
|
+
const terminal = podTerminalRow(row);
|
|
1795
|
+
if (!terminal) continue;
|
|
1796
|
+
if (terminal.ok !== false || typeof terminal.error !== 'string') return null;
|
|
1797
|
+
const lines = terminal.error.replace(/\/home\/[^/\s]+\//g, '~/').split(/\r\n|\n|\r/);
|
|
1608
1798
|
const first = lines[0]!.slice(0, 240);
|
|
1609
1799
|
const last = lines.at(-1)!.slice(0, 240);
|
|
1610
|
-
return { stage:
|
|
1800
|
+
return { stage: terminal.stage, error: first === last ? first : `${first}\n${last}` };
|
|
1611
1801
|
}
|
|
1612
1802
|
return null;
|
|
1613
1803
|
}
|
package/src/telegram-commands.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { dashboardLoops } from './domains/dashboard-data.js';
|
|
2
2
|
import { readSchedulesInventory } from './nexus/api/schedules-read.js';
|
|
3
|
-
import { loadLoopRows, LOOP_SCHEDULES_PATH, LOOPS_PATH
|
|
3
|
+
import { loadLoopRows, LOOP_SCHEDULES_PATH, LOOPS_PATH } from './loops/status-rows.js';
|
|
4
4
|
|
|
5
|
-
interface LoopsSources {
|
|
5
|
+
export interface LoopsSources {
|
|
6
6
|
schedules: () => unknown;
|
|
7
7
|
loops: () => unknown;
|
|
8
8
|
now: () => number;
|
|
@@ -25,20 +25,27 @@ export async function telegramLoopsStatus(args: string[], sources: LoopsSources
|
|
|
25
25
|
if (path === LOOPS_PATH) return sources.loops();
|
|
26
26
|
throw new Error('unknown inventory');
|
|
27
27
|
}, sources.now());
|
|
28
|
-
if (!rows.length) return '
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
28
|
+
if (!rows.length) return '모두 정상';
|
|
29
|
+
const problems = rows.filter(row => row.verdict === '늦음' || row.verdict === '실패');
|
|
30
|
+
const ordered = [
|
|
31
|
+
...problems.filter(row => row.verdict === '늦음'),
|
|
32
|
+
...problems.filter(row => row.verdict === '실패'),
|
|
33
|
+
];
|
|
34
|
+
const summary = (['살아 있음', '꺼짐', '판정 불가'] as const)
|
|
35
|
+
.map(verdict => `${verdict} ${rows.filter(row => row.verdict === verdict).length}`).join(' · ');
|
|
36
|
+
const lines = ['루프·크론 현황'];
|
|
37
|
+
let shown = 0;
|
|
38
|
+
for (const row of ordered.slice(0, 10)) {
|
|
39
|
+
const line = `• ${row.verdict === '늦음' ? '🔴' : '❌'} ${row.verdict} · ${clean(row.name)} (${clean(row.layer)}) · ${clean(row.mode)} · 마지막 ${row.lastRun ?? '기록 없음'} · ${clean(row.id)}`;
|
|
40
|
+
// Leave room for the summary and an omission count under Telegram's 4096-character cap.
|
|
41
|
+
if (lines.join('\n').length + line.length + summary.length + 60 > 4000) break;
|
|
42
|
+
lines.push(line);
|
|
43
|
+
shown++;
|
|
39
44
|
}
|
|
40
|
-
|
|
45
|
+
if (shown < ordered.length) lines.push(`… ${ordered.length - shown}개 더`);
|
|
46
|
+
lines.push(summary);
|
|
47
|
+
return lines.join('\n');
|
|
41
48
|
} catch {
|
|
42
|
-
return '루프
|
|
49
|
+
return '루프 현황 못 읽음';
|
|
43
50
|
}
|
|
44
51
|
}
|
package/src/telegram.ts
CHANGED
|
@@ -82,6 +82,7 @@ import { attachTelegramProjectButtons } from './telegram-project-command.js';
|
|
|
82
82
|
import { attachTelegramFabricPlan } from './telegram-fabric-plan.js';
|
|
83
83
|
import { contextNow as readContextNow, type ContextNowDeps } from './context-bus/context-now.js';
|
|
84
84
|
import { renderTelegramNow } from './context-bus/context-now-surfaces.js';
|
|
85
|
+
import { createContextFirstGate, renderContextFirst } from './context-bus/context-first.js';
|
|
85
86
|
|
|
86
87
|
export type TgAttachmentKind = 'photo' | 'voice' | 'audio' | 'document';
|
|
87
88
|
|
|
@@ -269,9 +270,6 @@ const EMPTY_ALLOWLIST_NOTICE_GAP_MS = 10 * 60 * 1000;
|
|
|
269
270
|
// Tuned above a normal chat answer but below a real delegation (which
|
|
270
271
|
// pays a multi-second session load before any work).
|
|
271
272
|
const NOTIFY_AS_NEW_MSG_THRESHOLD_MS = 20_000;
|
|
272
|
-
/** 같은 대화에서 이 간격 안이면 context-now 요약을 다시 보내지 않는다. */
|
|
273
|
-
const CONTEXT_FIRST_GAP_MS = 6 * 60 * 60 * 1000;
|
|
274
|
-
const CONTEXT_UNREADABLE = '맥락 못 읽음';
|
|
275
273
|
|
|
276
274
|
interface ApiResponse<T> {
|
|
277
275
|
ok: boolean;
|
|
@@ -365,10 +363,7 @@ export class TelegramBot {
|
|
|
365
363
|
private readonly handleFabricPlan: (ctx: TgIncoming) => Promise<boolean>;
|
|
366
364
|
private readonly readContextNow: typeof readContextNow;
|
|
367
365
|
private readonly contextNowDeps?: ContextNowDeps;
|
|
368
|
-
|
|
369
|
-
private readonly lastUtteranceAt = new Map<string, number>();
|
|
370
|
-
/** 첫 말이거나 6시간 이상 쉰 뒤라 요약이 «밀려 있는» 대화 키 — 다음 일반 답이 한 번 소비한다. */
|
|
371
|
-
private readonly contextFirstPending = new Set<string>();
|
|
366
|
+
private readonly contextFirstGate = createContextFirstGate({ now: () => this.nowImpl() });
|
|
372
367
|
private seatAskTimer: ReturnType<typeof setInterval> | null = null;
|
|
373
368
|
/** 앨범 id → `#현장` 행사 · 답장 디바운스. 태그된 앨범만 들어온다. */
|
|
374
369
|
private readonly fieldGroups = new Map<string, {
|
|
@@ -432,27 +427,17 @@ export class TelegramBot {
|
|
|
432
427
|
|
|
433
428
|
/** 허용된 모든 발화(명령 포함)마다 부른다 — 쉼은 마지막 발화부터 재고, 요약 전송과는 따로 기록한다. */
|
|
434
429
|
private noteUtterance(ctx: TgIncoming): void {
|
|
435
|
-
|
|
436
|
-
const now = this.nowImpl();
|
|
437
|
-
const previous = this.lastUtteranceAt.get(key);
|
|
438
|
-
this.lastUtteranceAt.set(key, now);
|
|
439
|
-
if (previous === undefined || now - previous >= CONTEXT_FIRST_GAP_MS) this.contextFirstPending.add(key);
|
|
430
|
+
this.contextFirstGate.note(`${ctx.chatId}:${ctx.threadId ?? ''}`);
|
|
440
431
|
}
|
|
441
432
|
|
|
442
433
|
/** 처음이거나 6시간 이상 쉰 대화의 첫 일반 답 앞에 context-now 요약 한 메시지를 보낸다. 실패해도 본 답은 막지 않는다. */
|
|
443
434
|
private async maybeSendContextFirst(ctx: TgIncoming): Promise<void> {
|
|
444
435
|
const key = `${ctx.chatId}:${ctx.threadId ?? ''}`;
|
|
445
|
-
if (!this.
|
|
436
|
+
if (!this.contextFirstGate.take(key)) return;
|
|
446
437
|
const cfg = this.slashContext?.userConfig.telegram;
|
|
447
438
|
if (cfg?.contextFirst === false) return;
|
|
448
|
-
let text = CONTEXT_UNREADABLE;
|
|
449
|
-
try {
|
|
450
|
-
text = renderTelegramNow(this.readContextNow({}, this.contextNowDeps));
|
|
451
|
-
} catch {
|
|
452
|
-
text = CONTEXT_UNREADABLE;
|
|
453
|
-
}
|
|
454
439
|
try {
|
|
455
|
-
await this.sendMessage(ctx.chatId,
|
|
440
|
+
await this.sendMessage(ctx.chatId, renderContextFirst(this.readContextNow, this.contextNowDeps, renderTelegramNow), { replyTo: ctx.messageId, threadId: ctx.threadId });
|
|
456
441
|
} catch (err) {
|
|
457
442
|
this.log(`telegram context-first failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
458
443
|
}
|