elanous 0.2.23 → 0.2.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/chunks/{272-b23d2e92161d11fd.js → 272-0eed0fb0aab716b6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{3961-fbef064c213b2b18.js → 3961-aaae04ed1d32622f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/{page-d5341ac96203a983.js → page-3b9fc39362473ae7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f2b62c19ca68fef8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/{page-55c631c032dcc47e.js → page-d7602d489edf5f37.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-ad3bd8084048bee4.js → page-24282bf9f90031a5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-8b7c5ab6b5bc8d64.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ceo/{page-5c5d0e370f495ef6.js → page-4eb84d454f8c3415.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-c325e21543cd48a8.js → page-b2e494bdad57fcb5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/{page-6168a7c43e6ef8f5.js → page-c0ab229673a8b0eb.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-3e2b45f3c36dbac4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-c22aa511b28743ad.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-26345242e4049f55.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-25bb425afdb27541.js → page-bf88c0f558d8ef54.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-32d72fdef1afe11a.js → page-52fec523e5b5d3ad.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-67723dfdcde92b31.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/inside/{page-2bd9ce9c020d02d4.js → page-22725e2570f1126b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-a34096245e63b9f2.js → page-1891335950d68b70.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-3267af1ebdc73cf5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/{page-4cc66d22ca4ec502.js → page-c314b75c11234998.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live-run/{page-83b14c729fcea837.js → page-9f9c85415f61782b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-c1d4276059085587.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-833aab587f26ad4c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-ccdb83ed40c8d724.js → page-8526c17ae38e6a50.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-7ad3de8fa06fdea9.js → page-f998637a8402115c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-b7bb53828b17b335.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-1b1850de0257e46d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-786111f30b6f0969.js → page-6f23159fedb41f67.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-48fb31810693c357.js → page-1d809a87139dd024.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-4e17cce5e2ed0fee.js → page-da9e570234d481fd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/{page-cb154acadb025414.js → page-b92caddb5c715f30.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/page-8ce3a38e210b810a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-c2dd2b19c93f90ab.js → page-57ffeeb6019440fd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-abbb417bf514630b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-d3c007c8a0ddd7ee.js → page-2c2b3f29cec1efa4.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-7837ce2aacb97b04.js → page-9369279f72fc7b51.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-64861f14039499c3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-ddc4c93efd8ab94b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-901a1391b81053bc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-68d9bbaac9841a58.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/{page-119be9a0444d2482.js → page-b5c0c996f329371e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-fa806c018833e332.js → page-89881ccbcf728978.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-0e87a803b3d7165e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/{page-d431fbdea6ad93dd.js → page-58d4e5cb30ef4871.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-02ce4c3b5f39e4d2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-e620d1bb8eadcec2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-847c3de6ca8a4416.js → page-acf868c64d77f950.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-54fa605fa6dba457.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-74e69ba2c8e970d4.js → page-bc79f0833533fa84.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-70038f41ae7cf931.js → page-d5c4398eea03d8f3.js} +1 -1
- package/apps/pwa/out/_next/static/css/72becfd42807b50d.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +14 -14
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +14 -14
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +14 -14
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +14 -14
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +14 -14
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +14 -14
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +14 -14
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +14 -14
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +14 -14
- package/apps/pwa/out/decisions/index.html +1 -1
- package/apps/pwa/out/decisions/index.txt +14 -14
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +14 -14
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +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/live-run/index.html +1 -1
- package/apps/pwa/out/live-run/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/catalog/models/openrouter/deepseek-v4-pro-0813.yaml +39 -0
- package/catalog/models/openrouter/deepseek-v4.1-flash.yaml +38 -0
- package/catalog/models/openrouter/glm-5.3-flash.yaml +35 -0
- package/catalog/providers/openrouter.yaml +2 -0
- package/graphs/implement-loop.yaml +31 -0
- package/graphs/ops/harness-gc.yaml +17 -0
- package/graphs/ops/recipes.yaml +3 -0
- package/graphs/overlays/README.md +1 -1
- package/graphs/overlays/stop-heal-ladder.yaml +105 -0
- package/package.json +1 -1
- package/scripts/add-api-key.sh +1 -1
- package/scripts/ci-typecheck-changed.ts +71 -14
- package/scripts/fleet/bedrock-pod-secret.ts +122 -0
- package/scripts/fleet/node-setup-linux.sh +97 -0
- package/scripts/fleet/node-setup.sh +11 -0
- package/scripts/fleet/zai-pod-secret.ts +86 -0
- package/scripts/land-global-checks.ts +3 -0
- package/scripts/measure-log-diet-2.ts +213 -0
- package/scripts/measure-tsc-affected-roots.ts +289 -0
- package/scripts/release-loop/gate-executor.ts +28 -0
- package/scripts/release-loop/gate-node.ts +262 -26
- package/scripts/release-loop/resume-release.ts +9 -3
- package/scripts/release-loop/resume.ts +41 -0
- package/scripts/release-loop/unattended-release.ts +22 -7
- package/scripts/tsc-baseline-cache.ts +147 -0
- package/scripts/usage-rollup.ts +58 -6
- package/src/agent/run-context.ts +10 -0
- package/src/agent-mission/driver.ts +18 -3
- package/src/agent-mission/resource-ladder.ts +8 -3
- package/src/agent-mission/review-loop.ts +8 -2
- package/src/agent-substrate/acp-implementer.ts +139 -0
- package/src/agent-substrate/pr-reviewer.ts +31 -4
- package/src/agent-substrate/self-review-cli.ts +48 -9
- package/src/autopilot/mission-critique.ts +18 -5
- package/src/autopilot/mission-feasibility-negotiate.ts +11 -10
- package/src/autopilot/mission-trace.ts +12 -1
- package/src/autopilot/system-lookback-run.ts +16 -10
- package/src/bench/bench.ts +183 -0
- package/src/budget/fetchers/grok.ts +15 -5
- package/src/budget/llm-cost.ts +37 -5
- package/src/budget/types.ts +8 -2
- package/src/budget/unified-usage.ts +88 -6
- package/src/cli/bench-cli.ts +34 -0
- package/src/cli/guardian-cli.ts +26 -1
- package/src/cli/image-cli.ts +101 -0
- package/src/cli/log-event-names.ts +4 -0
- package/src/cli/pr-cli.ts +62 -5
- package/src/cli/pr-land-overlap-evidence.ts +160 -12
- package/src/cli/release-cli.ts +18 -9
- package/src/cli/rfc-status.ts +52 -2
- package/src/cli/self-update.ts +104 -0
- package/src/cli/tasks-cli.ts +109 -4
- package/src/cli/usage-cli.ts +91 -8
- package/src/core-turn/run-goal-loop.ts +5 -4
- package/src/dashboard/ad-run-setup-bindings.ts +279 -0
- package/src/dashboard/index.ts +18 -276
- package/src/dashboard/stale-install-check.ts +11 -0
- package/src/debug/log-diet.ts +38 -7
- package/src/debug/log.ts +19 -1
- package/src/domains/release-tool.ts +39 -0
- package/src/error-report/report.ts +21 -4
- package/src/github/pr-labels.ts +1 -0
- package/src/graph-runner/graph-cli.ts +42 -1
- package/src/graph-runner/runner.ts +167 -6
- package/src/grok/usage.ts +23 -2
- package/src/grounding/ref-index.ts +253 -0
- package/src/harness/dispatch-backpressure.ts +10 -2
- package/src/harness/harness-clean.ts +199 -78
- package/src/harness/harness-cli-command.ts +122 -7
- package/src/harness/harness-gc.ts +331 -0
- package/src/harness/harness-stop.ts +4 -2
- package/src/harness/harness-substrate-default.ts +142 -7
- package/src/harness/launch-registry.ts +6 -0
- package/src/harness/worktree-branch-prefix.ts +9 -0
- package/src/hq/fence.ts +187 -0
- package/src/hq/seats.ts +38 -22
- package/src/images/image-ledger.ts +179 -0
- package/src/index.ts +93 -11
- package/src/instance/child-scope.ts +32 -1
- package/src/instance/dev-project.ts +133 -0
- package/src/instance/resolve.ts +24 -0
- package/src/instance/spawn-elanous.ts +45 -0
- package/src/intake-plane/ref-prefetch.ts +110 -0
- package/src/intelligence-map/model-catalog.ts +72 -0
- package/src/llm/bedrock-eventstream.ts +215 -0
- package/src/llm/bedrock-provider.ts +114 -41
- package/src/llm/bedrock.ts +174 -28
- package/src/llm/openrouter-byok-cost.ts +96 -0
- package/src/llm/openrouter-deepseek.ts +151 -0
- package/src/llm/openrouter-upstream-pins.config.ts +25 -0
- package/src/llm/zai-coding-plan.ts +700 -0
- package/src/llm.ts +87 -16
- package/src/loops/checker.ts +148 -9
- package/src/loops/orchestrator/resync-shadow.ts +34 -7
- package/src/maturity/feature-maturity.ts +2 -2
- package/src/model-tier/llm-tier-map.ts +9 -5
- package/src/models/prompts.ts +2 -1
- package/src/mss/logging/log-store.ts +58 -11
- package/src/nexus/api/missions.ts +73 -2
- package/src/nexus/api/ops-api.ts +18 -0
- package/src/nexus/index.ts +7 -75
- package/src/nexus/tools-kind.ts +78 -0
- package/src/oauth/codex-account-rotation.ts +39 -2
- package/src/oauth/codex-account-store.ts +323 -24
- package/src/oauth/fallback-chain.ts +53 -15
- package/src/onboarding/first-run.ts +37 -18
- package/src/onboarding/web-first.ts +20 -2
- package/src/ops/machine-heartbeat.ts +185 -0
- package/src/ops/overview-cache.ts +175 -0
- package/src/ops/overview.ts +8 -0
- package/src/prompt-cache/openai.ts +3 -0
- package/src/prompt-cache/types.ts +5 -0
- package/src/prompt-enhance/enhance.ts +23 -0
- package/src/release-loop/placement.ts +16 -1
- package/src/requirements/requirement-ledger.ts +536 -0
- package/src/retro/daily.ts +5 -5
- package/src/seat-loop/seat-loop.ts +32 -10
- package/src/self-dev/dev-cli.ts +44 -10
- package/src/self-dev/dev-pipeline.ts +7 -2
- package/src/self-dev/draft-residue.ts +14 -2
- package/src/self-dev/draft-sweep.ts +103 -0
- package/src/self-dev/graph-journey-route.ts +6 -0
- package/src/self-dev/orchestrate-cli.ts +25 -3
- package/src/self-dev/orchestrate.ts +41 -6
- package/src/self-dev/run-supervisor.ts +188 -15
- package/src/self-dev/run-types.ts +13 -0
- package/src/self-dev/supervisor-verdict-edges.ts +1 -0
- package/src/self-implement/budget-gate.ts +53 -13
- package/src/self-implement/child-context-composition.ts +139 -0
- package/src/self-implement/child-llm-preference.ts +13 -5
- package/src/self-implement/claude-acp-implement.ts +67 -0
- package/src/self-implement/flex-signals.ts +59 -0
- package/src/self-implement/frozen-merges.ts +8 -3
- package/src/self-implement/goal-author-decompose.ts +10 -2
- package/src/self-implement/goal-author.ts +24 -9
- package/src/self-implement/graph-overlay-condition.ts +28 -0
- package/src/self-implement/graph-overlay-yaml.ts +41 -7
- package/src/self-implement/graph-templates.ts +33 -1
- package/src/self-implement/graph-yaml.ts +56 -0
- package/src/self-implement/heal-intake.ts +5 -1
- package/src/self-implement/orchestrator.ts +144 -15
- package/src/self-implement/running-runs.ts +75 -5
- package/src/self-implement/seams.ts +47 -1
- package/src/self-implement/self-implement-cli.ts +9 -4
- package/src/self-implement/stop-autoheal.ts +69 -2
- package/src/session-runtime/retry-policy.ts +2 -1
- package/src/steward/launch.ts +5 -4
- package/src/steward/triage-budget.ts +62 -0
- package/src/steward/triage.ts +83 -22
- package/src/task-agent/actions.ts +62 -5
- package/src/task-agent/completion-evidence.ts +5 -0
- package/src/task-agent/cover.ts +101 -3
- package/src/task-agent/judge.ts +16 -0
- package/src/task-agent/land-order-ledger.ts +95 -0
- package/src/task-agent/live-move-history.ts +87 -1
- package/src/task-agent/live-moves.ts +350 -20
- package/src/task-agent/rejudge-on-head.ts +75 -20
- package/src/task-agent/review-prior-fix.ts +146 -0
- package/src/task-agent/run-adopt.ts +136 -0
- package/src/task-agent/shadow.ts +53 -9
- package/src/task-agent/task-hand.ts +54 -7
- package/src/task-orchestrator/surfaces/pod-account-broker.ts +38 -5
- package/src/task-orchestrator/surfaces/pod-bedrock-child.ts +74 -0
- package/src/task-orchestrator/surfaces/pod-child-default.ts +50 -0
- package/src/task-orchestrator/surfaces/pod-child-providers.ts +14 -1
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +79 -6
- package/src/task-orchestrator/surfaces/pod-lease.ts +17 -5
- package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +11 -5
- package/src/task-orchestrator/surfaces/pod-pool.ts +20 -4
- package/src/task-orchestrator/surfaces/pod-zai-child.ts +101 -0
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +213 -23
- package/src/task-orchestrator/surfaces/self-implement.ts +19 -1
- package/src/typecheck-ratchet.ts +1 -0
- package/src/user-config.ts +51 -11
- package/src/version/packed-revision.json +1 -1
- package/src/workflow-runtime/triggers/registry.ts +59 -0
- package/src/workflow-runtime/triggers/webhook-source.ts +35 -5
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-0160ab83c8685a12.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-e6784759a2531031.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-31417d3604154a6d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-e9606dc0bdec9305.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-a648bc464c9dc754.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-d27b794c8ba76bbb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-22170f4f00a15496.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-6e2781c523dad761.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-26dd415c8f13f341.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-015c02c4fec4023e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-6b0ae5b5c42f6d2f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-16258e956ea93fc2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-85f35dd1dd96ae87.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-7350ee93f4fe1033.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-00723458575f2b19.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-745d9b6c015076f8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-736d33c7414a8a1a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-c782041b5f5199ba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-0c056abff82b9891.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-242d668e4fbf72c4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-81dd0d8ae923ed72.js +0 -1
- package/apps/pwa/out/_next/static/css/5619d59e87eddf66.css +0 -1
- /package/apps/pwa/out/_next/static/{rfkMm4movqkqMebBrCXiR → Q-7iofoxOEv1WX18S35of}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{rfkMm4movqkqMebBrCXiR → Q-7iofoxOEv1WX18S35of}/_ssgManifest.js +0 -0
|
@@ -21,8 +21,14 @@
|
|
|
21
21
|
import type { RotationCandidate } from './codex-account-rotation.js';
|
|
22
22
|
import type { ClaudePtyMaturity } from './claude-pty-maturity.js';
|
|
23
23
|
|
|
24
|
-
/** 체인에 놓을 수 있는 칸. ⛔ 모르는 이름은 «조용히 건너뛰지 않고» 거부한다(오타 방어).
|
|
25
|
-
|
|
24
|
+
/** 체인에 놓을 수 있는 칸. ⛔ 모르는 이름은 «조용히 건너뛰지 않고» 거부한다(오타 방어).
|
|
25
|
+
*
|
|
26
|
+
* ⭐ `codex-credits`(CODEX-CREDITS-STEP · 2026-10-10) — «마지막 수단»으로 codex 를 «선불 크레딧»으로 쓴다.
|
|
27
|
+
* 한도 정책이 `fallback`(크레딧 ✗)이면 회전은 찬 계정에 «머물지» 않고(`no-candidate`) 체인의 다음 칸(grok)으로 간다.
|
|
28
|
+
* 그 grok 마저 못 쓰면 갈 곳이 없었다 — 이 칸을 grok «뒤»에 두면 그때만 크레딧으로 돌아온다.
|
|
29
|
+
* ⛔ 첫 칸의 크레딧 금지(`codexQuotaPolicy`)는 그대로다 — 이 칸은 «체인이 여기까지 왔을 때»만 돈을 쓴다.
|
|
30
|
+
* ⛔ 기본 체인에는 «없다» — 돈이라 사람이 config 에 «명시»할 때만 켜진다. */
|
|
31
|
+
export const FALLBACK_STEPS = ['codex-rotate', 'grok', 'claude-pty', 'openrouter', 'codex-credits'] as const;
|
|
26
32
|
export type FallbackStep = (typeof FALLBACK_STEPS)[number];
|
|
27
33
|
|
|
28
34
|
/** 기본 체인 — ⛔⭐ **2026-08-20 에 `grok` 이 «기본»으로 들어왔다**(대표 지시).
|
|
@@ -80,6 +86,14 @@ export type FallbackDecision =
|
|
|
80
86
|
| { readonly action: 'switch-backend'; readonly backend: 'grok' | 'claude' }
|
|
81
87
|
/** 명시한 유료 OpenRouter 모델로 전환한다. 호출부가 모델을 바꾸지 못하면 실행하지 않는다. */
|
|
82
88
|
| { readonly action: 'switch-backend'; readonly backend: 'openrouter'; readonly model: string }
|
|
89
|
+
/** codex 로 «돌아와» 선불 크레딧으로 계속한다 — 체인의 `codex-credits` 칸(마지막 수단 · 유료).
|
|
90
|
+
* `skipped` 는 «왜 앞 칸들을 못 썼나»(관측용 · 비밀 없음). */
|
|
91
|
+
| {
|
|
92
|
+
readonly action: 'codex-credits';
|
|
93
|
+
readonly skipped: readonly { readonly step: FallbackStep; readonly why: StayReason }[];
|
|
94
|
+
/** 크레딧으로 쓸 계정(잔액 많은 순 첫 계정) — 순수 판정은 비우고, 수집기(`resolveRunFallback`)가 채운다. */
|
|
95
|
+
readonly to?: RotationCandidate;
|
|
96
|
+
}
|
|
83
97
|
/** 아무것도 안 한다 — 이유를 «값으로» 남긴다. */
|
|
84
98
|
| { readonly action: 'stay'; readonly why: StayReason };
|
|
85
99
|
|
|
@@ -103,7 +117,9 @@ export type StayReason =
|
|
|
103
117
|
/** 유료 OpenRouter 모델 또는 키가 없다. */
|
|
104
118
|
| 'openrouter-unavailable'
|
|
105
119
|
/** claude PTY 의 최근 표본이 성숙 관문을 통과하지 못했거나 읽히지 않는다. */
|
|
106
|
-
| 'claude-pty-immature'
|
|
120
|
+
| 'claude-pty-immature'
|
|
121
|
+
/** 체인에 `codex-credits` 가 있는데 크레딧 잔액이 «확인된» codex 계정이 없다(모름 포함 — 돈이라 fail-closed). */
|
|
122
|
+
| 'codex-credits-unavailable';
|
|
107
123
|
|
|
108
124
|
type FallbackRunPosition =
|
|
109
125
|
| { readonly currentCredentialRateLimited?: false; readonly currentStep?: FallbackStep }
|
|
@@ -140,6 +156,11 @@ export type FallbackInput = {
|
|
|
140
156
|
* ⛔ 이 함수는 config 를 읽지 않는다 — 호출자가 이 칸을 준다.
|
|
141
157
|
*/
|
|
142
158
|
readonly stayOnResetCreditAvailable?: boolean;
|
|
159
|
+
/**
|
|
160
|
+
* 크레딧 잔액이 «확인된»(잔액 > 0 · 보유 ≠ false) codex 계정이 하나라도 있나 — `codex-credits` 칸만 읽는다.
|
|
161
|
+
* ⛔ 생략·false 는 그 칸을 «건너뛴다»(돈이라 「모른다」를 통과로 접지 않는다 — grok 의 unknown 통과와 «반대»).
|
|
162
|
+
*/
|
|
163
|
+
readonly codexCreditsAvailable?: boolean;
|
|
143
164
|
} & FallbackRunPosition;
|
|
144
165
|
|
|
145
166
|
/**
|
|
@@ -162,9 +183,9 @@ export function decideFallback(input: FallbackInput): FallbackDecision {
|
|
|
162
183
|
const {
|
|
163
184
|
rotation, chain, currentStep, currentCredentialRateLimited = false,
|
|
164
185
|
grokAvailable, grokQuota = 'unknown', claudePtyMaturity, stayOnResetCreditAvailable = false,
|
|
165
|
-
openrouterFallbackModel, openrouterAvailable = false,
|
|
186
|
+
openrouterFallbackModel, openrouterAvailable = false, codexCreditsAvailable = false,
|
|
166
187
|
} = input;
|
|
167
|
-
const openrouter = { model: openrouterFallbackModel, available: openrouterAvailable };
|
|
188
|
+
const openrouter = { model: openrouterFallbackModel, available: openrouterAvailable, codexCreditsAvailable };
|
|
168
189
|
|
|
169
190
|
if (currentCredentialRateLimited) {
|
|
170
191
|
if (currentStep === undefined) return { action: 'stay', why: 'chain-exhausted' };
|
|
@@ -202,12 +223,15 @@ export function stayReasonWhenCodexAxisEnds(
|
|
|
202
223
|
return 'chain-exhausted';
|
|
203
224
|
}
|
|
204
225
|
|
|
226
|
+
/** 칸 판정에 필요한 «나머지» 재료 — OpenRouter 모델·키, codex 크레딧 잔액 확인. */
|
|
227
|
+
type StepExtras = { readonly model?: string; readonly available: boolean; readonly codexCreditsAvailable?: boolean };
|
|
228
|
+
|
|
205
229
|
function stepAfterCodex(
|
|
206
230
|
chain: readonly FallbackStep[],
|
|
207
231
|
grokAvailable: boolean,
|
|
208
232
|
grokQuota: 'usable' | 'exhausted' | 'unknown',
|
|
209
233
|
claudePtyMaturity: ClaudePtyMaturity | undefined,
|
|
210
|
-
openrouter:
|
|
234
|
+
openrouter: StepExtras,
|
|
211
235
|
whyIfNone: StayReason,
|
|
212
236
|
): FallbackDecision {
|
|
213
237
|
return stepAfter(chain, 'codex-rotate', grokAvailable, grokQuota, claudePtyMaturity, openrouter, whyIfNone);
|
|
@@ -219,31 +243,39 @@ function stepAfter(
|
|
|
219
243
|
grokAvailable: boolean,
|
|
220
244
|
grokQuota: 'usable' | 'exhausted' | 'unknown',
|
|
221
245
|
claudePtyMaturity: ClaudePtyMaturity | undefined,
|
|
222
|
-
openrouter:
|
|
246
|
+
openrouter: StepExtras,
|
|
223
247
|
whyIfNone: StayReason,
|
|
224
248
|
): FallbackDecision {
|
|
225
249
|
const idx = currentStep === undefined ? -1 : chain.indexOf(currentStep);
|
|
226
250
|
const rest = idx === -1 ? chain : chain.slice(idx + 1);
|
|
227
251
|
let unavailableWhy: StayReason | undefined;
|
|
252
|
+
// `codex-credits` 칸이 «왜 앞 칸을 못 썼나»를 값으로 남긴다(관측 — 비밀 없음).
|
|
253
|
+
const skipped: { step: FallbackStep; why: StayReason }[] = [];
|
|
254
|
+
const skip = (step: FallbackStep, why: StayReason): void => { unavailableWhy = why; skipped.push({ step, why }); };
|
|
228
255
|
for (const step of rest) {
|
|
256
|
+
if (step === 'codex-credits') {
|
|
257
|
+
// ⛔ 크레딧은 돈이다 — 잔액이 «확인된» 계정이 없으면 건너뛴다(「모른다」는 통과가 «아니다»).
|
|
258
|
+
if (openrouter.codexCreditsAvailable !== true) { skip(step, 'codex-credits-unavailable'); continue; }
|
|
259
|
+
return { action: 'codex-credits', skipped };
|
|
260
|
+
}
|
|
229
261
|
if (step === 'openrouter') {
|
|
230
262
|
const model = openrouter.model?.trim();
|
|
231
263
|
if (!openrouter.available || !model || !/^openrouter\/[^/\s]+\/[^\s]+$/.test(model)) {
|
|
232
|
-
|
|
264
|
+
skip(step, 'openrouter-unavailable');
|
|
233
265
|
continue;
|
|
234
266
|
}
|
|
235
267
|
return { action: 'switch-backend', backend: 'openrouter', model };
|
|
236
268
|
}
|
|
237
269
|
if (step === 'grok') {
|
|
238
270
|
// 자격 부재와 소진은 다른 값이다. 다음 칸이 있으면 그 칸을 살핀다.
|
|
239
|
-
if (!grokAvailable) {
|
|
271
|
+
if (!grokAvailable) { skip(step, 'grok-unavailable'); continue; }
|
|
240
272
|
// 「모른다」는 통과시킨다 — 못 읽었다고 갈 곳을 없애지 않는다.
|
|
241
|
-
if (grokQuota === 'exhausted') {
|
|
273
|
+
if (grokQuota === 'exhausted') { skip(step, 'grok-exhausted'); continue; }
|
|
242
274
|
return { action: 'switch-backend', backend: 'grok' };
|
|
243
275
|
}
|
|
244
276
|
if (step === 'claude-pty') {
|
|
245
277
|
// 미성숙·못 읽음은 «건너뛴다» — 뒤에 다른 칸이 있으면 그 칸을 본다(없으면 이 사유로 머문다).
|
|
246
|
-
if (claudePtyMaturity?.mature !== true) {
|
|
278
|
+
if (claudePtyMaturity?.mature !== true) { skip(step, 'claude-pty-immature'); continue; }
|
|
247
279
|
return { action: 'switch-backend', backend: 'claude' };
|
|
248
280
|
}
|
|
249
281
|
}
|
|
@@ -259,6 +291,8 @@ export function describeFallback(decision: FallbackDecision): string {
|
|
|
259
291
|
return decision.backend === 'openrouter'
|
|
260
292
|
? `백엔드 전환 → openrouter (${decision.model} · API 과금)`
|
|
261
293
|
: `백엔드 전환 → ${decision.backend} (구독)`;
|
|
294
|
+
case 'codex-credits':
|
|
295
|
+
return `codex 크레딧으로 계속 (마지막 칸 · 선불 크레딧 과금${decision.skipped.length ? ` · 건너뜀 ${decision.skipped.map((s) => `${s.step}:${s.why}`).join(', ')}` : ''})`;
|
|
262
296
|
case 'stay':
|
|
263
297
|
return `전환 없음 (${decision.why})`;
|
|
264
298
|
}
|
|
@@ -295,24 +329,28 @@ export function grokQuotaFromCreditAxis(axis: {
|
|
|
295
329
|
/**
|
|
296
330
|
* budget 스냅샷 → 폴백 판정이 쓰는 세 값.
|
|
297
331
|
*
|
|
298
|
-
* ⭐ 공급자가 「리밋이 찼다」고 «말한» 것을
|
|
332
|
+
* ⭐ 공급자가 「리밋이 찼다」고 «말한» 것을 weekly window 산술보다 «먼저» 본다
|
|
299
333
|
* (`UsageSnapshot.rateLimitReached` 주석의 그 권위를 그대로 따른다).
|
|
300
334
|
* ⛔ 스냅샷이 없거나 판단할 재료가 없으면 `'unknown'` 이다 — 「소진」으로 접지 «않는다».
|
|
301
335
|
* ⛔ 이 함수는 «네트워크를 안 친다» — 이미 채워진 캐시를 받는다.
|
|
302
336
|
*/
|
|
303
337
|
export function grokQuotaFromUsageSnapshot(snapshot: {
|
|
304
338
|
readonly rateLimitReached?: string;
|
|
339
|
+
readonly windows?: readonly { readonly kind: string; readonly remainingPercent: number }[];
|
|
305
340
|
readonly credits?: { readonly hasCredits?: boolean; readonly unlimited?: boolean };
|
|
306
341
|
} | null | undefined): 'usable' | 'exhausted' | 'unknown' {
|
|
307
342
|
if (!snapshot) return 'unknown';
|
|
308
|
-
// ⛔ 공급자 판정이 이긴다 —
|
|
343
|
+
// ⛔ 공급자 판정이 이긴다 — 주간 창 판정보다 앞선다.
|
|
309
344
|
if (typeof snapshot.rateLimitReached === 'string' && snapshot.rateLimitReached.length > 0) return 'exhausted';
|
|
345
|
+
const weekly = snapshot.windows?.find((window) => window.kind === 'weekly');
|
|
346
|
+
if (weekly && Number.isFinite(weekly.remainingPercent) && weekly.remainingPercent >= 0 && weekly.remainingPercent <= 100) {
|
|
347
|
+
return weekly.remainingPercent === 0 ? 'exhausted' : 'usable';
|
|
348
|
+
}
|
|
310
349
|
const credits = snapshot.credits;
|
|
311
350
|
if (!credits) return 'unknown';
|
|
312
351
|
if (credits.unlimited === true) return 'usable';
|
|
313
352
|
// ⛔⭐ `hasCredits` 는 브랜드마다 «다른 것»을 뜻할 수 있다 — grok 은 「선불 잔액이 있나」다.
|
|
314
|
-
// ⇒
|
|
315
|
-
// 그러면 hasCredits=false 하나로 「소진」을 단정하지 «않는다» — 「모른다」가 정직하다.
|
|
353
|
+
// ⇒ 주간 quota 창이 없으면 hasCredits=false 하나로 「소진」을 단정하지 «않는다» — 「모른다」가 정직하다.
|
|
316
354
|
if (credits.hasCredits === true) return 'usable';
|
|
317
355
|
return 'unknown';
|
|
318
356
|
}
|
|
@@ -13,21 +13,34 @@ export interface FirstRunDeps {
|
|
|
13
13
|
path?: string;
|
|
14
14
|
detectProviders?: () => Promise<DetectedProvider[]>;
|
|
15
15
|
isTTY?: boolean;
|
|
16
|
-
chooseProvider?: () => Promise<LLMProviderName | null>;
|
|
16
|
+
chooseProvider?: (detected: readonly LLMProviderName[]) => Promise<LLMProviderName | null>;
|
|
17
17
|
print?: (message: string) => void;
|
|
18
18
|
saveConfig?: (config: UserConfig, path: string) => void;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
const CHOICE_BASE: readonly LLMProviderName[] = ['openai-codex', 'grok', 'anthropic'];
|
|
22
|
+
const CHOICE_LABEL: Partial<Record<LLMProviderName, string>> = { 'openai-codex': 'Codex', grok: 'Grok', anthropic: 'Claude' };
|
|
23
|
+
const SUBSCRIPTION_PRIORITY = ['openai-codex', 'grok', 'anthropic', 'gemini', 'openai', 'openrouter'] as const;
|
|
24
|
+
|
|
25
|
+
/** Menu order: detected subscriptions first (in priority order), then the remaining base choices. */
|
|
26
|
+
export function orderProviderChoices(detected: readonly LLMProviderName[] = []): LLMProviderName[] {
|
|
27
|
+
return [...detected, ...CHOICE_BASE.filter((name) => !detected.includes(name))];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function chooseProvider(detected: readonly LLMProviderName[] = []): Promise<LLMProviderName | null> {
|
|
31
|
+
const choices = orderProviderChoices(detected);
|
|
32
|
+
const menu = choices.map((name, i) => `${i + 1} ${CHOICE_LABEL[name] ?? name}`).join(', ');
|
|
22
33
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
23
34
|
try {
|
|
24
|
-
const answer = (await rl.question(
|
|
25
|
-
return (
|
|
35
|
+
const answer = (await rl.question(`Choose LLM (${menu}; Enter to cancel): `)).trim();
|
|
36
|
+
return choices[Number(answer) - 1] ?? null;
|
|
26
37
|
} finally {
|
|
27
38
|
rl.close();
|
|
28
39
|
}
|
|
29
40
|
}
|
|
30
41
|
|
|
42
|
+
const detectionLabel = (provider: string) => `${provider === 'openai-codex' ? 'codex' : provider} 구독`;
|
|
43
|
+
|
|
31
44
|
export async function runFirstRun(deps: FirstRunDeps = {}): Promise<{
|
|
32
45
|
outcome: 'ready' | 'needs-llm' | 'cancelled'; provider?: string; inputs: number;
|
|
33
46
|
}> {
|
|
@@ -50,21 +63,31 @@ export async function runFirstRun(deps: FirstRunDeps = {}): Promise<{
|
|
|
50
63
|
: undefined;
|
|
51
64
|
const configured = existing && existing.source !== 'none'
|
|
52
65
|
? cfg.llm.provider : undefined;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
66
|
+
// claude-code is detected but has no subscription path in elanous: mapping it to the 'anthropic'
|
|
67
|
+
// API-key provider would save a provider whose first call fails for a subscription-only user.
|
|
68
|
+
const usable = SUBSCRIPTION_PRIORITY.filter((name) => providers.includes(name));
|
|
69
|
+
const detectionLine = (outcome: string) => `감지: ${providers.map(detectionLabel).join(' · ')} → ${outcome}`;
|
|
70
|
+
const needsLlm = (inputs: number, extra: string[] = []) => {
|
|
71
|
+
print('eln setup llm 으로 고르세요');
|
|
72
|
+
for (const line of extra) print(line);
|
|
73
|
+
observe('needs-llm', inputs);
|
|
74
|
+
return { outcome: 'needs-llm' as const, inputs };
|
|
75
|
+
};
|
|
76
|
+
let provider: LLMProviderName | null | undefined = configured ?? (usable.length === 1 ? usable[0] : undefined);
|
|
77
|
+
if (!configured && provider) print(detectionLine(provider));
|
|
57
78
|
let inputs = 0;
|
|
58
79
|
if (!provider) {
|
|
80
|
+
if (providers.length > 0) {
|
|
81
|
+
print(detectionLine(usable.length === 0
|
|
82
|
+
? 'claude-code 구독은 구독으로 쓸 수 있는 경로가 없어 자동 선택하지 않음'
|
|
83
|
+
: '둘 이상 감지되어 고르세요'));
|
|
84
|
+
}
|
|
59
85
|
if ((deps.isTTY ?? process.stdin.isTTY) !== true) {
|
|
60
|
-
print('eln setup llm 으로 고르세요');
|
|
61
86
|
// FIRST-CHAT-NONTTY — bare `elanous` with no TTY gets the same one-line next step as the other entrances.
|
|
62
|
-
|
|
63
|
-
observe('needs-llm', 0);
|
|
64
|
-
return { outcome: 'needs-llm', inputs: 0 };
|
|
87
|
+
return needsLlm(0, [unattendedNextStepLine()]);
|
|
65
88
|
}
|
|
66
89
|
inputs = 1;
|
|
67
|
-
provider = await (deps.chooseProvider ?? chooseProvider)();
|
|
90
|
+
provider = await (deps.chooseProvider ?? chooseProvider)(usable);
|
|
68
91
|
if (!provider) return { outcome: 'cancelled', inputs };
|
|
69
92
|
observe('chosen', inputs);
|
|
70
93
|
}
|
|
@@ -74,11 +97,7 @@ export async function runFirstRun(deps: FirstRunDeps = {}): Promise<{
|
|
|
74
97
|
baseProvider: cfg.llm.provider, baseApiKey: cfg.llm.apiKey,
|
|
75
98
|
})
|
|
76
99
|
: undefined;
|
|
77
|
-
if (inputs === 1 && credential?.source === 'none')
|
|
78
|
-
print('eln setup llm 으로 고르세요');
|
|
79
|
-
observe('needs-llm', inputs);
|
|
80
|
-
return { outcome: 'needs-llm', inputs };
|
|
81
|
-
}
|
|
100
|
+
if (inputs === 1 && credential?.source === 'none') return needsLlm(inputs);
|
|
82
101
|
(deps.saveConfig ?? saveUserConfig)({
|
|
83
102
|
...cfg,
|
|
84
103
|
llm: { ...cfg.llm, provider,
|
|
@@ -15,6 +15,8 @@ export interface WebFirstSetupDeps {
|
|
|
15
15
|
/** Whether a person is at a terminal (stdin TTY). Without one the web path only uses a daemon that is already up. */
|
|
16
16
|
isTty?: boolean;
|
|
17
17
|
terminal?: boolean;
|
|
18
|
+
/** Bare first run never starts or waits for a missing daemon; explicit setup retains web launch. */
|
|
19
|
+
startIfMissing?: boolean;
|
|
18
20
|
startedAt?: number;
|
|
19
21
|
showNexus?: () => Promise<NexusShowResult>;
|
|
20
22
|
startDaemon?: () => Promise<BgLaunchResult>;
|
|
@@ -67,9 +69,13 @@ export async function runWebFirstSetup(deps: WebFirstSetupDeps): Promise<'link-s
|
|
|
67
69
|
let shown: NexusShowResult | undefined;
|
|
68
70
|
try { shown = await show(); } catch { /* A missing daemon may make inspection fail. */ }
|
|
69
71
|
const running = shown?.status === 'unregistered' || (shown?.status === 'registered' && shown.instance?.alive === true);
|
|
72
|
+
if (deps.startIfMissing === false && !running) {
|
|
73
|
+
print('오프라인 모드');
|
|
74
|
+
observe('fallback', 'offline');
|
|
75
|
+
return 'fallback';
|
|
76
|
+
}
|
|
70
77
|
// A script, pipe or CI run never starts a daemon on its own: without a terminal the web path
|
|
71
|
-
// only uses a daemon that already answers health once.
|
|
72
|
-
// stdin on /dev/tty, so the install → web path still starts one.)
|
|
78
|
+
// only uses a daemon that already answers health once.
|
|
73
79
|
if ((deps.isTty ?? (process.stdin.isTTY === true)) !== true) {
|
|
74
80
|
const rest = running ? shown?.urls?.rest.loopback : undefined;
|
|
75
81
|
const up = rest && shown?.urls?.pwa.loopback
|
|
@@ -78,6 +84,12 @@ export async function runWebFirstSetup(deps: WebFirstSetupDeps): Promise<'link-s
|
|
|
78
84
|
}
|
|
79
85
|
const deadline = Date.now() + STARTUP_TIMEOUT_MS;
|
|
80
86
|
if (!running || !shown?.urls?.pwa.loopback) {
|
|
87
|
+
if (deps.startIfMissing === false) {
|
|
88
|
+
print('오프라인 모드');
|
|
89
|
+
observe('fallback', 'offline');
|
|
90
|
+
return 'fallback';
|
|
91
|
+
}
|
|
92
|
+
print('웹 셋업을 위해 데몬을 시작합니다.');
|
|
81
93
|
let launch: BgLaunchResult;
|
|
82
94
|
try {
|
|
83
95
|
launch = await (deps.startDaemon ?? (() => runBgLaunch({ out: { log: () => {}, error: () => {} } })))();
|
|
@@ -98,9 +110,15 @@ export async function runWebFirstSetup(deps: WebFirstSetupDeps): Promise<'link-s
|
|
|
98
110
|
}
|
|
99
111
|
}
|
|
100
112
|
} catch { /* A daemon may not have published its URLs yet. */ }
|
|
113
|
+
if (deps.startIfMissing === false) break;
|
|
101
114
|
const remaining = deadline - Date.now();
|
|
102
115
|
if (remaining > 0) await Bun.sleep(Math.min(200, remaining));
|
|
103
116
|
}
|
|
117
|
+
if (!healthy && deps.startIfMissing === false) {
|
|
118
|
+
print('오프라인 모드');
|
|
119
|
+
observe('fallback', 'offline');
|
|
120
|
+
return 'fallback';
|
|
121
|
+
}
|
|
104
122
|
if (!healthy) return fallback(running ? 'daemon-health-unavailable' : 'daemon-health-timeout');
|
|
105
123
|
|
|
106
124
|
if (!shown?.urls?.pwa.loopback) return fallback('daemon-unavailable');
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { writeFileSync, mkdirSync, existsSync, readFileSync, renameSync } from 'node:fs';
|
|
2
|
+
import { basename, join } from 'node:path';
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
4
|
+
import { CardStore } from '../task-cards/card-store.js';
|
|
5
|
+
import { debug, redactSecretText } from '../debug/log.js';
|
|
6
|
+
import { effectiveInstanceRoot } from '../instance/resolve.js';
|
|
7
|
+
import { recordRunExit, readRunExits } from '../harness/harness-incidents.js';
|
|
8
|
+
import { notifyOwners } from '../loops/checker.js';
|
|
9
|
+
import { bunVersion, cliFingerprint, gcTools, orphanCandidates, type FenceGcDeps } from '../hq/fence.js';
|
|
10
|
+
|
|
11
|
+
export interface MachineHeartbeatDeps extends FenceGcDeps {
|
|
12
|
+
root?: string;
|
|
13
|
+
now?: Date;
|
|
14
|
+
record?: typeof recordRunExit;
|
|
15
|
+
log?: typeof debug.log;
|
|
16
|
+
publish?: typeof notifyOwners;
|
|
17
|
+
card?: (command: string, day: string, evidencePaths: string[], root: string) => void;
|
|
18
|
+
version?: (pid: number, run: (binary: string, args: string[]) => string) => string;
|
|
19
|
+
/** How long the same tree must stay at the same CPU across guardian ticks before it is called stalled. */
|
|
20
|
+
stallMs?: number;
|
|
21
|
+
/** off = collect nothing · shadow (default) = candidates, evidence file and observation only · on = evidence, then SIGTERM. */
|
|
22
|
+
mode?: OrphanGcMode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type OrphanGcMode = 'off' | 'shadow' | 'on';
|
|
26
|
+
|
|
27
|
+
/** A tree is «stalled» only when an earlier tick saw the very same tree at the very same CPU this long ago.
|
|
28
|
+
* One in-tick second is not enough: Linux `ps time` has whole-second resolution and I/O waits burn no CPU. */
|
|
29
|
+
export const ORPHAN_STALL_MS = 10 * 60_000;
|
|
30
|
+
/** `shadowEvidenced`: shadow already wrote this tree's evidence once — later shadow ticks skip it (no lsof/sample, no new file). */
|
|
31
|
+
type StallWatch = Record<string, { since: string; cpu: number; tree: string; shadowEvidenced?: true }>;
|
|
32
|
+
|
|
33
|
+
function readStallWatch(path: string): StallWatch {
|
|
34
|
+
if (!existsSync(path)) return {};
|
|
35
|
+
const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'));
|
|
36
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as StallWatch : {};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** One cause card per command/day, with incident files as attachments in the incidents section. */
|
|
40
|
+
export function publishOrphanCauseCard(command: string, day: string, paths: string[], root: string): void {
|
|
41
|
+
const store = new CardStore(root);
|
|
42
|
+
try {
|
|
43
|
+
const hash = createHash('sha256').update(command).digest('hex').slice(0, 24);
|
|
44
|
+
const card = store.createCard({ goalId: `orphan-cli-${day}-${hash}`, title: `TC 원인 조사: ${redactSecretText(command)}` });
|
|
45
|
+
for (const path of paths) store.appendSection(card.id, { key: `incidents:${basename(path).replaceAll('.', '-')}`, owner: 'TC',
|
|
46
|
+
content: `증거: incidents/${basename(path)}\n가설(미확정): bun fs.watch 등 상주 핸들이 CLI 종료를 막음` });
|
|
47
|
+
} finally { store.close(); }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Run on the local guardian tick; an observation failure never authorizes termination. */
|
|
51
|
+
export async function runMachineHeartbeat(schedules: readonly { command: string; cron: string }[], deps: MachineHeartbeatDeps = {}): Promise<{ collected: number; terminated: number; cards: number }> {
|
|
52
|
+
const mode = deps.mode ?? 'shadow';
|
|
53
|
+
if (mode === 'off') return { collected: 0, terminated: 0, cards: 0 };
|
|
54
|
+
const tools = gcTools(deps);
|
|
55
|
+
const now = deps.now ?? new Date();
|
|
56
|
+
const root = deps.root ?? effectiveInstanceRoot();
|
|
57
|
+
const timeZone = deps.timeZone;
|
|
58
|
+
const stallMs = deps.stallMs ?? ORPHAN_STALL_MS;
|
|
59
|
+
const watchPath = join(root, 'ops', 'orphan-cli-watch.json');
|
|
60
|
+
// An unreadable watch never authorizes termination: start over and only observe this tick.
|
|
61
|
+
let watch: StallWatch;
|
|
62
|
+
try { watch = readStallWatch(watchPath); } catch { watch = {}; }
|
|
63
|
+
const nextWatch: StallWatch = {};
|
|
64
|
+
const stalled: ReturnType<typeof orphanCandidates> = [];
|
|
65
|
+
const first = tools.snapshot();
|
|
66
|
+
await tools.sleep(1_000);
|
|
67
|
+
const second = tools.snapshot();
|
|
68
|
+
let collected = 0;
|
|
69
|
+
let terminated = 0;
|
|
70
|
+
let cards = 0;
|
|
71
|
+
let alreadyEvidenced = 0;
|
|
72
|
+
const logDecision = (): void => {
|
|
73
|
+
try { (deps.log ?? debug.log.bind(debug))('ops.orphan-cli', 'decision', { mode, candidates: stalled.length, collected, alreadyEvidenced, killed: terminated }); } catch { /* observe only */ }
|
|
74
|
+
};
|
|
75
|
+
for (const candidate of orphanCandidates(first, second, schedules, now, timeZone)) {
|
|
76
|
+
const key = `${candidate.process.pid}:${candidate.process.ppid}:${candidate.process.command}`;
|
|
77
|
+
const cpu = Math.round(candidate.tree.reduce((total, row) => total + row.cpuSeconds, 0) * 100) / 100;
|
|
78
|
+
const tree = candidate.tree.map(row => `${row.pid}:${row.cpuSeconds}`).join(',');
|
|
79
|
+
const seen = watch[key];
|
|
80
|
+
const elapsed = seen ? now.getTime() - Date.parse(seen.since) : NaN;
|
|
81
|
+
// Same tree, same CPU, and the process is at least as old as the watch (not a reused PID).
|
|
82
|
+
if (!seen || seen.cpu !== cpu || seen.tree !== tree || !(elapsed >= 0) || candidate.process.ageSeconds * 1000 + 2_000 < elapsed) {
|
|
83
|
+
nextWatch[key] = { since: now.toISOString(), cpu, tree };
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
nextWatch[key] = seen;
|
|
87
|
+
if (elapsed < stallMs) continue;
|
|
88
|
+
// Shadow never ends the tree, so the same hang stays stalled: evidence it once, not on every tick.
|
|
89
|
+
if (mode !== 'on' && seen.shadowEvidenced) { alreadyEvidenced++; continue; }
|
|
90
|
+
stalled.push(candidate);
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
mkdirSync(join(root, 'ops'), { recursive: true });
|
|
94
|
+
const tmp = `${watchPath}.${process.pid}.tmp`;
|
|
95
|
+
writeFileSync(tmp, JSON.stringify(nextWatch), { mode: 0o600 });
|
|
96
|
+
renameSync(tmp, watchPath);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
// Without a durable watch the next tick cannot prove a stall either — observe only.
|
|
99
|
+
try { (deps.log ?? debug.log.bind(debug))('ops.orphan-cli', 'observation-failed', { stage: 'stall-watch', error: redactSecretText(String(error)) }); } catch { /* observe only */ }
|
|
100
|
+
logDecision();
|
|
101
|
+
return { collected, terminated, cards };
|
|
102
|
+
}
|
|
103
|
+
const writeWatch = (): void => {
|
|
104
|
+
mkdirSync(join(root, 'ops'), { recursive: true });
|
|
105
|
+
const tmp = `${watchPath}.${process.pid}.tmp`;
|
|
106
|
+
writeFileSync(tmp, JSON.stringify(nextWatch), { mode: 0o600 });
|
|
107
|
+
renameSync(tmp, watchPath);
|
|
108
|
+
};
|
|
109
|
+
let shadowMarked = false;
|
|
110
|
+
for (const candidate of stalled) {
|
|
111
|
+
// Recheck the whole tree after slow diagnostics. A new child or CPU progress invalidates the decision.
|
|
112
|
+
let evidence: { command: string; ageSeconds: number; periodMinutes: number; pid: number; tree: number[]; lsof: string; sample: string; bunVersion: string; at: string };
|
|
113
|
+
try {
|
|
114
|
+
evidence = {
|
|
115
|
+
command: candidate.process.command, ageSeconds: candidate.process.ageSeconds, periodMinutes: candidate.periodMinutes, pid: candidate.process.pid,
|
|
116
|
+
tree: candidate.tree.map(row => row.pid),
|
|
117
|
+
lsof: tools.run('lsof', ['-p', candidate.tree.map(row => row.pid).join(',')]),
|
|
118
|
+
sample: tools.run('sample', [String(candidate.process.pid), '1']),
|
|
119
|
+
bunVersion: (deps.version ?? bunVersion)(candidate.process.pid, tools.run), at: now.toISOString(),
|
|
120
|
+
};
|
|
121
|
+
const latest = tools.snapshot();
|
|
122
|
+
const found = orphanCandidates(second, latest, schedules, now, timeZone).find(row => row.process.pid === candidate.process.pid);
|
|
123
|
+
if (!found || found.tree.length !== candidate.tree.length || found.tree.some(row => !candidate.tree.some(old => old.pid === row.pid))) continue;
|
|
124
|
+
// File + existing incident ledger + logs are committed before any signal is delivered.
|
|
125
|
+
const path = join(root, 'incidents', `orphan-cli-${now.getTime()}-${candidate.process.pid}-${randomUUID().slice(0, 8)}.json`);
|
|
126
|
+
mkdirSync(join(root, 'incidents'), { recursive: true });
|
|
127
|
+
const safe = JSON.stringify(evidence, (_key, value) => typeof value === 'string' ? redactSecretText(value) : value);
|
|
128
|
+
writeFileSync(path, safe, { flag: 'wx', mode: 0o600 });
|
|
129
|
+
const fingerprint = redactSecretText(cliFingerprint(candidate.process.command));
|
|
130
|
+
(deps.log ?? debug.log.bind(debug))('ops.orphan-cli', mode === 'on' ? 'evidence-before-termination' : 'shadow-evidence', {
|
|
131
|
+
path, command: redactSecretText(evidence.command), ageSeconds: evidence.ageSeconds, periodMinutes: evidence.periodMinutes, pid: evidence.pid,
|
|
132
|
+
tree: evidence.tree, lsof: redactSecretText(evidence.lsof), sample: redactSecretText(evidence.sample), bunVersion: evidence.bunVersion,
|
|
133
|
+
});
|
|
134
|
+
collected++;
|
|
135
|
+
if (mode !== 'on') {
|
|
136
|
+
const key = `${candidate.process.pid}:${candidate.process.ppid}:${candidate.process.command}`;
|
|
137
|
+
if (nextWatch[key]) { nextWatch[key] = { ...nextWatch[key]!, shadowEvidenced: true }; shadowMarked = true; }
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
// Children first: do not leave the stuck parent holding resources while descendants outlive it.
|
|
141
|
+
const signalFailures: Array<{ pid: number; error: string }> = [];
|
|
142
|
+
for (const row of [...found.tree].reverse()) {
|
|
143
|
+
try { tools.kill(row.pid); } catch (error) { signalFailures.push({ pid: row.pid, error: redactSecretText(String(error)) }); }
|
|
144
|
+
}
|
|
145
|
+
// The run-exit row records what the signals actually did, written after they were sent: SIGTERM only when every
|
|
146
|
+
// signal landed, otherwise no signal and the failures beside the evidence path. The evidence file and log above
|
|
147
|
+
// were committed before any signal. Either way the hang counts toward the cause card below.
|
|
148
|
+
(deps.record ?? recordRunExit)({ runId: `orphan-cli-${candidate.process.pid}-${randomUUID()}`, reason: `orphan-cli:${fingerprint}`, status: null,
|
|
149
|
+
signal: signalFailures.length === 0 ? 'SIGTERM' : null, at: now.toISOString(), pid: candidate.process.pid, entrance: 'guardian-tick',
|
|
150
|
+
lastLines: [path, ...signalFailures.map((failure) => `termination-failed pid=${failure.pid}: ${failure.error}`)] }, root);
|
|
151
|
+
if (signalFailures.length > 0) {
|
|
152
|
+
(deps.log ?? debug.log.bind(debug))('ops.orphan-cli', 'termination-failed', { path, pid: candidate.process.pid, failures: signalFailures });
|
|
153
|
+
} else terminated++;
|
|
154
|
+
const day = new Date(now.getTime() + 9 * 3_600_000).toISOString().slice(0, 10);
|
|
155
|
+
// One hang is one process: the same PID recorded on several ticks (its SIGTERM did not take) counts once.
|
|
156
|
+
const byPid = new Map<number, ReturnType<typeof readRunExits>[number]>();
|
|
157
|
+
for (const row of readRunExits(root)) {
|
|
158
|
+
if (row.reason !== `orphan-cli:${fingerprint}` || typeof row.pid !== 'number') continue;
|
|
159
|
+
if (new Date(Date.parse(row.at) + 9 * 3_600_000).toISOString().slice(0, 10) !== day) continue;
|
|
160
|
+
if (!byPid.has(row.pid)) byPid.set(row.pid, row);
|
|
161
|
+
}
|
|
162
|
+
const same = [...byPid.values()];
|
|
163
|
+
if (same.length >= 3) {
|
|
164
|
+
const paths = same.flatMap(row => row.lastLines ?? []).filter(file => existsSync(file));
|
|
165
|
+
if (paths.length < 3) {
|
|
166
|
+
(deps.log ?? debug.log.bind(debug))('ops.orphan-cli', 'cause-card-evidence-missing', { command: redactSecretText(candidate.process.command), count: same.length, evidence: paths.length });
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const reason = `원인 카드: ${fingerprint} · 하루 ${same.length}회 매달림 · 증거 ${paths.join(' , ')} · 가설(미확정): bun fs.watch 등 상주 핸들이 CLI 종료를 막음`;
|
|
170
|
+
(deps.card ?? publishOrphanCauseCard)(fingerprint, day, paths, root);
|
|
171
|
+
cards += (deps.publish ?? notifyOwners)([{ id: `orphan-cli:${fingerprint}`, owner: 'TC', enabled: true, registered: true, state: 'failing', reason }], { root, now });
|
|
172
|
+
}
|
|
173
|
+
} catch (error) {
|
|
174
|
+
try { (deps.log ?? debug.log.bind(debug))('ops.orphan-cli', 'observation-failed', { pid: candidate.process.pid, error: redactSecretText(String(error)) }); } catch { /* log failure must not kill a process */ }
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (shadowMarked) {
|
|
178
|
+
// A lost mark only means one more evidence file next tick — never a signal.
|
|
179
|
+
try { writeWatch(); } catch (error) {
|
|
180
|
+
try { (deps.log ?? debug.log.bind(debug))('ops.orphan-cli', 'observation-failed', { stage: 'shadow-mark', error: redactSecretText(String(error)) }); } catch { /* observe only */ }
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
logDecision();
|
|
184
|
+
return { collected, terminated, cards };
|
|
185
|
+
}
|