elanous 0.2.18 → 0.2.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/chunks/{1307-30dff22c9e1f3073.js → 1307-77fad80cf54477c6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/1612-f60c0c93ec773f81.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2116-6429c188eb846ab8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/224-e7be8f762fe74986.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2489-ff55a35b202c0f94.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2549-89c518a4c8ed4d00.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3857-995ea9f5ef33d71e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4144-fad095b48a1862ee.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5755-ea7ed88eb956f47b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5977-94ed6f78e9814b2b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7748-6e51bfe927019f5c.js +25 -0
- package/apps/pwa/out/_next/static/chunks/8401-35e94e9091f5eb8d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9247-c1753a059e7bac08.js +1 -0
- package/apps/pwa/out/_next/static/chunks/a9f2b54d-33c0689b70ed8cf3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/{page-d92f7ee3994e84c7.js → page-c855c24de7db74fe.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-4733c967b56a4277.js → page-85b316bb4235980b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/{page-d22fd849cc22f104.js → page-9b2b26a33cb2cd42.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-bb742f995c6769e7.js → page-c9955fde67b1bcb5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-02b073a2ac793b83.js → page-7a852956c74015a3.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-c5a5c621be0e04ae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-8a9b3594bc23289b.js → page-4ba4abe096b6c2a8.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-575d4a8b4f7becb2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/{page-308111abadf35670.js → page-f5d56ff5f45ad164.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-2260e9eef0357421.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-5d97b2901e0d1ea6.js → page-7c058069c5e7fa63.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-dc5da0b8a2edb8a4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-8c3d8e63ae2e1c98.js → page-466bf0ee16ad1432.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-9b72a07d424701c6.js → page-a23bc71713bcc08f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-ed4a6f799f005d09.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-a572f58cd3ca5864.js → page-9cd565005b93aca6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-55f0f40c6db95f15.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/{page-be5ea965a07fbc41.js → page-474cb5888ea0e8f3.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-35bfec2e631d3eae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/{page-fa244bd146a00ae8.js → page-9ca21f9ad3208a61.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-44d56d6391965a6a.js → page-f9ccb1b1adbea14d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-83a700d4c754d802.js → page-ec102c9b8b691e37.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-7b3170f374f77b9d.js → not-found-a9c2cfa8dda922a5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-6da35144878feb6d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-f6055495e9ed7ea8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-25204694fa9984a5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a60e035f151d84d3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-189c587f36874538.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-c56634bde705a65d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-1467047104141297.js → page-db03ae3589a81c9e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-4a6463b97f0ad94f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-102310930791197a.js → page-75bffecc9aa1004e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-a8d2fe0c669e0de4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-6ed0b73bae8351d8.js → page-31d5b875760bb62d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-c26fa10aac6c21c4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-43336055451f6e37.js → page-68793c6e10331d66.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/{page-3cc47dca0922ab9a.js → page-46d5201d2207a511.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/{page-a77ee355a2304ff0.js → page-d81c07a7b4f618ae.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-b17cd46cf0e9b8cf.js → page-be1c4e8697f22360.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-cf0a4b4abead71ea.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/page-a4717e2c4bf7a28f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-7bf54c9bb9274af8.js → page-0fd895fab9b62c1b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-8b6c1454a8e8f306.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-c6e4364d9de8f717.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-0acc8e198f7b0dfb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-134da468538baf48.js → page-34882c84f84021c5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-0506a1e0e4bdb2f5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{webpack-c2782b330447ffb6.js → webpack-c260571513bf43c2.js} +1 -1
- package/apps/pwa/out/_next/static/css/{e99e3cff94ced78a.css → 599369d853c61df7.css} +1 -1
- package/apps/pwa/out/_next/static/css/c23d959d3ee15e57.css +1 -0
- package/apps/pwa/out/_next/static/css/d3427996da11e480.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +14 -14
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +14 -14
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +14 -14
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +14 -14
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +14 -14
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +14 -14
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +14 -14
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +14 -14
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +14 -14
- package/apps/pwa/out/decisions/index.html +1 -1
- package/apps/pwa/out/decisions/index.txt +14 -14
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +14 -14
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +16 -15
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +14 -14
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +14 -14
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +14 -14
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +16 -15
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +14 -14
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +14 -14
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +24 -23
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +14 -14
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +14 -14
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +14 -14
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +14 -14
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +14 -14
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +14 -14
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +14 -14
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +14 -14
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +15 -15
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +14 -14
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +14 -14
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +14 -14
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +14 -14
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +14 -14
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +14 -14
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +14 -14
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +14 -14
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +14 -14
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +14 -14
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +14 -14
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +14 -14
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +14 -14
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +14 -14
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +14 -14
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +14 -14
- package/graphs/demo/editor-recipes.yaml +22 -0
- package/graphs/ops/guardian.yaml +26 -0
- package/graphs/ops/recipes.yaml +3 -0
- package/graphs/retro/daily.yaml +25 -0
- package/graphs/retro/recipes.yaml +24 -0
- package/package.json +1 -1
- package/scripts/ask-marker-check.ts +3 -1
- package/scripts/coord-channel-watch.sh +2 -2
- package/scripts/discord-seat-probe.ts +3 -1
- package/scripts/land-global-checks.ts +89 -0
- package/scripts/lib/pch-regress-map.ts +24 -0
- package/scripts/morning-report.ts +18 -4
- package/scripts/pch-regress.ts +88 -0
- package/scripts/release-loop/gate-diff.ts +18 -0
- package/scripts/release-loop/gate-node.ts +628 -65
- package/scripts/release-loop/gate-partial.ts +106 -0
- package/scripts/release-loop/gate-timing.ts +229 -0
- package/scripts/release-loop/preflight.ts +6 -4
- package/scripts/release-loop/publish-window.ts +141 -0
- package/scripts/release-loop/release-docs.ts +1 -4
- package/scripts/release-loop/release-universe.ts +38 -0
- package/scripts/release-loop/resume-release.ts +67 -5
- package/scripts/release-loop/shard-plan.ts +65 -0
- package/scripts/release-loop/unattended-release.ts +12 -2
- package/scripts/release-story/draft.ts +5 -1
- package/scripts/retro/shadow-compare.ts +71 -0
- package/scripts/rhythm/daily.ts +6 -1
- package/scripts/video/scene-table.ts +159 -0
- package/src/acp/core-turn-bridge.ts +39 -2
- package/src/agent-mission/driver.ts +34 -6
- package/src/autopilot/build/llm-conflict-merge.ts +132 -7
- package/src/autopilot/build/merge-intent.ts +135 -7
- package/src/away/presence.ts +40 -0
- package/src/away/release-watch.ts +272 -0
- package/src/chat/index.ts +2 -1
- package/src/chat/short-question.ts +3 -40
- package/src/cli/away-cli.ts +68 -0
- package/src/cli/config-drift.ts +513 -0
- package/src/cli/guardian-cli.ts +49 -0
- package/src/cli/intake-cli.ts +13 -7
- package/src/cli/log-event-names.ts +5 -0
- package/src/cli/pod-cli.ts +5 -1
- package/src/cli/pr-cli.ts +20 -0
- package/src/cli/pty-drive-cli.ts +14 -3
- package/src/cli/release-cli.ts +52 -12
- package/src/cli/retro-cli.ts +78 -0
- package/src/cli/setup-cli.ts +31 -1
- package/src/cli/tasks-cli.ts +193 -14
- package/src/context-bus/context-now-public.ts +1 -1
- package/src/context-bus/context-now-surfaces.ts +16 -6
- package/src/context-bus/context-now.ts +97 -3
- package/src/core-turn/short-question.ts +100 -0
- package/src/dashboard/dashboard-status-lines.ts +24 -4
- package/src/dashboard/index.ts +50 -5
- package/src/dashboard/input/slash-executor.ts +6 -2
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +68 -40
- package/src/dashboard/slash-runtime/decide-slash.ts +60 -0
- package/src/dashboard/slash-runtime/help-from-registry.ts +62 -23
- package/src/dashboard/slash-runtime/help-groups.ts +44 -0
- package/src/dashboard/slash-runtime/model-choices.ts +125 -0
- package/src/domains/guardian-tick.ts +138 -0
- package/src/domains/outbound-alert.ts +44 -13
- package/src/domains/outbound-log-probe.ts +6 -1
- package/src/domains/schedule-registry.ts +23 -0
- package/src/domains/telegram-kind-route.ts +39 -0
- package/src/domains/watch-default-scheduler.ts +95 -0
- package/src/esc-abort-gate.ts +44 -0
- package/src/graph-runner/graph-run-api.ts +205 -0
- package/src/graph-runner/runner.ts +36 -6
- package/src/harness/harness-cli-command.ts +420 -51
- package/src/harness/harness-queue.ts +315 -25
- package/src/harness/harness-salvage-cli.ts +140 -0
- package/src/harness/harness-worktree-add.ts +68 -0
- package/src/harness/ledger-write-guard.ts +6 -16
- package/src/hq/fence-outcomes.ts +63 -0
- package/src/hq/fence-wrapper.ts +33 -0
- package/src/hq/hq.ts +17 -2
- package/src/index.ts +118 -6
- package/src/instance/test-write-guard-spy-first.fixture.ts +13 -0
- package/src/instance/test-write-guard.ts +200 -0
- package/src/intake-plane/digest.ts +39 -15
- package/src/intake-plane/harness-target.ts +4 -3
- package/src/intake-plane/intake-sources.ts +1 -0
- package/src/intake-plane/items.ts +1 -0
- package/src/loops/orchestrator/degradation.ts +57 -0
- package/src/loops/orchestrator/finish-rate.ts +33 -3
- package/src/loops/orchestrator/tick.ts +166 -45
- package/src/maturity/feature-maturity.ts +6 -4
- package/src/maturity/tui-slash-maturity.ts +1 -0
- package/src/mss/logging/log-store.ts +3 -0
- package/src/nexus/api/graph-kinds.ts +10 -4
- package/src/nexus/api/graphs-api.ts +178 -6
- package/src/nexus/api/http-server.ts +54 -8
- package/src/nexus/api/loop-edges.ts +292 -17
- package/src/nexus/api/ops-api.ts +112 -29
- package/src/nexus/api/ops-board-route.ts +48 -0
- package/src/nexus/index.ts +7 -0
- package/src/onboarding/entry-hints.ts +5 -0
- package/src/onboarding.ts +24 -6
- package/src/release-loop/checklist.ts +2 -0
- package/src/release-loop/feature-store.ts +68 -18
- package/src/release-loop/gate-shards.ts +158 -0
- package/src/release-loop/light-rc.ts +173 -0
- package/src/release-loop/placement.ts +51 -17
- package/src/release-loop/release-schedule.ts +6 -2
- package/src/release-loop/rubric.ts +57 -1
- package/src/retro/daily.ts +372 -0
- package/src/seat-loop/op-decide.ts +173 -0
- package/src/seat-loop/seat-loop.ts +60 -3
- package/src/seat-loop/seat-report.ts +58 -3
- package/src/self-dev/ask-launch-flow.ts +5 -4
- package/src/self-dev/ask-launch-io.ts +49 -1
- package/src/self-dev/ask-path-candidates.ts +40 -2
- package/src/self-dev/dev-cli.ts +36 -0
- package/src/self-dev/draft-sweep.ts +371 -16
- package/src/self-dev/draft-triage-rules.ts +19 -4
- package/src/self-dev/orchestrate.ts +1 -0
- package/src/self-dev/pr-preview.ts +247 -0
- package/src/self-dev/shard-run.ts +23 -7
- package/src/self-implement/blocked-draft-policy.ts +101 -0
- package/src/self-implement/execution-clarification.ts +11 -0
- package/src/self-implement/gate-cli.ts +29 -8
- package/src/self-implement/gate-env-retry.ts +83 -0
- package/src/self-implement/goal-author-clarification.ts +88 -13
- package/src/self-implement/goal-clarification-escalation.ts +2 -0
- package/src/self-implement/orchestrator.ts +220 -6
- package/src/self-implement/run-stop.ts +123 -0
- package/src/self-implement/run-ttl.ts +258 -0
- package/src/self-implement/salvage-branches.ts +254 -0
- package/src/self-implement/salvage-retention.ts +123 -0
- package/src/self-implement/seams.ts +4 -0
- package/src/self-implement/self-implement-cli.ts +2 -0
- package/src/self-implement/sibling-resync.ts +223 -0
- package/src/self-implement/stop-autoheal.ts +204 -0
- package/src/task-agent/actions.ts +63 -2
- package/src/task-agent/board.ts +132 -0
- package/src/task-agent/card-evidence.ts +292 -0
- package/src/task-agent/completion-evidence.ts +126 -0
- package/src/task-agent/judge.ts +25 -0
- package/src/task-agent/live-moves.ts +276 -0
- package/src/task-agent/mission.ts +33 -7
- package/src/task-agent/shadow.ts +192 -15
- package/src/task-agent/task-hand.ts +124 -17
- package/src/task-orchestrator/surfaces/pod-command-job.ts +82 -8
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +4 -0
- package/src/task-orchestrator/surfaces/pod-install-slots.ts +89 -0
- package/src/task-orchestrator/surfaces/pod-lease.ts +233 -21
- package/src/task-orchestrator/surfaces/pod-pool.ts +155 -34
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +21 -5
- package/src/telegram-away-command.ts +38 -0
- package/src/telegram-commands.ts +52 -0
- package/src/telegram-report-log-probe.ts +21 -0
- package/src/telegram-report.ts +27 -3
- package/src/testing/real-cli-spawn.ts +106 -0
- package/src/tui-client/dashboard-session.ts +1 -0
- package/src/user-config.ts +190 -5
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3857-4f936cdb30322aa6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4554-5761612531d0146b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5977-a11f37254196b659.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8401-8615a660c84cdb03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +0 -25
- package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/a9f2b54d-7865aba0f4d2ee49.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-a0eb02638b1e6f31.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-c39554c321576982.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-c130b63862437814.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-a3c58f5c72a5f3f9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-e8e1ecaea7cd71d2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-2da6f9b8a38c6978.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-44d5f767491692bf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-61553851aa606146.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-a1f585e154e61c59.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-5150ef17b5383a57.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +0 -1
- package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.css +0 -1
- /package/apps/pwa/out/_next/static/{P6YTVgzuZz5oTZhY4Din_ → Bam1YziK2iH9bN4K0utGk}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{P6YTVgzuZz5oTZhY4Din_ → Bam1YziK2iH9bN4K0utGk}/_ssgManifest.js +0 -0
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* - @ssh호스트 원격 클러스터면 그 기계 — 이미지 판 대조·반입(`docker load` ⊕ `k3d image import`)에 쓴다
|
|
10
10
|
* - 상한 이 노드에 동시에 둘 Job 수(기본 2)
|
|
11
11
|
* Job 마다 «빈 자리 비율»이 가장 큰 노드를 고른다(동률은 앞 순위 · 측정 실패 노드는 측정된 노드가 다 찼을 때만). 다 차면 자리가 날 때까지 기다린다.
|
|
12
|
+
* POOL-LITE(10-07): 골 종류를 주면 lite·문서·측정 골은 상한이 가장 작은 노드(node-c), 구현 골은 가장 큰 노드(node-b)를 먼저 쓰고,
|
|
13
|
+
* 그 노드가 차면 빈 자리 비율로 넘친다. 상한 0 노드는 «꺼짐» — 해석에서 빠지고 절대 고르지 않는다.
|
|
12
14
|
* 노드 선택의 진행 중 수는 로컬 프로세스 기준이다. 발사 허가는 별도의 lease 측정으로
|
|
13
15
|
* 다른 호스트의 Running/Pending 및 메모리 예약까지 고려한다.
|
|
14
16
|
* 풀을 안 주면 종전처럼 «현재 컨텍스트» 하나다(동작 불변).
|
|
@@ -21,7 +23,7 @@ import { debug } from '../../debug/log.js';
|
|
|
21
23
|
import { PodLeaseAdmission, type PodLeasePredecessor, type PodLeaseRelease } from '../../pod-lease/admission.js';
|
|
22
24
|
import { predecessorState, type PredecessorState } from '../../pod-lease/dependency-state.js';
|
|
23
25
|
import { HostPoolLease, leaseHasPendingPod } from '../../pod-lease/host-lease.js';
|
|
24
|
-
import { measurePoolLease, recommendConcurrency, type PodLeaseMember, type PoolDnsProbe, type PoolLeaseRecommendation } from './pod-lease.js';
|
|
26
|
+
import { harnessCpuSlotsBesideGate, measurePoolLease, recommendConcurrency, type PodLeaseMember, type PoolDnsProbe, type PoolLeaseRecommendation } from './pod-lease.js';
|
|
25
27
|
|
|
26
28
|
export interface PodPoolMember {
|
|
27
29
|
readonly context: string;
|
|
@@ -111,7 +113,9 @@ export function parsePodPool(spec: string): PodPoolMember[] {
|
|
|
111
113
|
const m = MEMBER.exec(part);
|
|
112
114
|
if (!m) throw new Error(`--pod-pool: 못 읽는 노드 「${part}」 — 형식 컨텍스트[@ssh호스트][:상한][#k3d-레지스트리:포트]`);
|
|
113
115
|
const capacity = m[3] === undefined ? 2 : Number(m[3]);
|
|
114
|
-
|
|
116
|
+
// POOL-LITE (10-07): `:0` means «this member is off» — a `node-c:0` entry once threw here and broke every launch.
|
|
117
|
+
// It is dropped below (never chosen); only a pool with no live member left is an error.
|
|
118
|
+
if (!Number.isInteger(capacity) || capacity < 0) throw new Error(`--pod-pool: 상한은 1 이상 — 「${part}」`);
|
|
115
119
|
if (m[5] && Number(m[5]) > 65535) throw new Error(`--pod-pool: 레지스트리 포트는 65535 이하 — 「${part}」`);
|
|
116
120
|
const context = m[1]!;
|
|
117
121
|
const k3dCluster = context.startsWith('k3d-') ? context.slice(4) : 'elanous-pool';
|
|
@@ -119,7 +123,44 @@ export function parsePodPool(spec: string): PodPoolMember[] {
|
|
|
119
123
|
});
|
|
120
124
|
if (members.length === 0) throw new Error('--pod-pool: 노드가 없다');
|
|
121
125
|
if (new Set(members.map((m) => m.context)).size !== members.length) throw new Error('--pod-pool: 컨텍스트가 겹친다');
|
|
122
|
-
|
|
126
|
+
const live = members.filter((m) => m.capacity > 0);
|
|
127
|
+
if (live.length === 0) throw new Error(`--pod-pool: 상한은 1 이상 — 상한 0 이 아닌 노드가 없다 「${spec}」`);
|
|
128
|
+
if (live.length !== members.length) {
|
|
129
|
+
debug.log('pod.pool', 'member-disabled', { members: members.filter((m) => m.capacity <= 0).map((m) => m.context), reason: 'capacity-0' });
|
|
130
|
+
}
|
|
131
|
+
return live;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* POOL-LITE (10-07) — what kind of Job is being placed. `lite` = lite memory tier or a docs/measurement goal;
|
|
136
|
+
* `standard` = an implementation goal. Absent kind keeps the plain free-share placement unchanged.
|
|
137
|
+
*/
|
|
138
|
+
export type PodPlacementKind = 'lite' | 'standard';
|
|
139
|
+
|
|
140
|
+
export interface PodPlacement {
|
|
141
|
+
readonly member: string;
|
|
142
|
+
readonly kind: PodPlacementKind | null;
|
|
143
|
+
/** `preferred` = landed on the kind's preferred member · `overflow` = preferred member(s) full/skipped · `share` = no preference applies. */
|
|
144
|
+
readonly outcome: 'preferred' | 'overflow' | 'share';
|
|
145
|
+
readonly preferred: readonly string[];
|
|
146
|
+
/** One line for launch output and the run ledger. */
|
|
147
|
+
readonly reason: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Preferred members for a placement kind: lite → the smallest-capacity member(s) (node-c), standard → the largest (node-b).
|
|
152
|
+
* Capacity is the operator's own statement of member size (`pool-node-b@node-b:40,pool-node-c@node-c:6`), so no new spec syntax.
|
|
153
|
+
* When every member has the same capacity there is no small/big member and no preference (empty list).
|
|
154
|
+
*/
|
|
155
|
+
export function preferredMembers(members: readonly PodPoolMember[], kind: PodPlacementKind | null | undefined): string[] {
|
|
156
|
+
const live = members.filter((m) => m.capacity > 0);
|
|
157
|
+
if (!kind || live.length < 2) return [];
|
|
158
|
+
const caps = live.map((m) => m.capacity);
|
|
159
|
+
const min = Math.min(...caps);
|
|
160
|
+
const max = Math.max(...caps);
|
|
161
|
+
if (min === max) return [];
|
|
162
|
+
const target = kind === 'lite' ? min : max;
|
|
163
|
+
return live.filter((m) => m.capacity === target).map((m) => m.context);
|
|
123
164
|
}
|
|
124
165
|
|
|
125
166
|
/** 스펙 해석 순서: 명시 인자 → `ELANOUS_POD_POOL` → 없음(null = 현재 컨텍스트 하나). */
|
|
@@ -178,12 +219,21 @@ export function podPoolHostLease(members: readonly PodPoolMember[]): HostPoolLea
|
|
|
178
219
|
export class PodPoolScheduler {
|
|
179
220
|
private readonly inflight = new Map<string, number>();
|
|
180
221
|
private readonly admission: PodLeaseAdmission;
|
|
222
|
+
/**
|
|
223
|
+
* GATE-ADMIT-EXEMPT — the gate's own shard Jobs (GATE-MEM-ADMIT) queue here: memory admission without the
|
|
224
|
+
* GATE-RESERVE-AUTO CPU bound, which exists to protect the gate from harness Pods, not from itself.
|
|
225
|
+
*/
|
|
226
|
+
private readonly gateAdmission: PodLeaseAdmission;
|
|
181
227
|
private readonly outstanding = new Set<PodLeaseRelease>();
|
|
182
228
|
private lastFree = 0;
|
|
183
229
|
private budget = 0;
|
|
230
|
+
/** GATE-RESERVE-AUTO — local permits whose Pod was observed (Running/terminal): already inside the CPU reading. */
|
|
231
|
+
private seenActive = 0;
|
|
184
232
|
private initialized = false;
|
|
185
233
|
private readonly measure: () => Promise<PoolLeaseRecommendation>;
|
|
186
234
|
private lastRaw: PoolLeaseRecommendation | null = null;
|
|
235
|
+
/** GATE-ADMIT-EXEMPT — the same reading before the gate CPU cap (memory/limitedBy unchanged); gate callers read it. */
|
|
236
|
+
private lastUncapped: PoolLeaseRecommendation | null = null;
|
|
187
237
|
private readonly hostLease: HostPoolLease;
|
|
188
238
|
private readonly pollMs: number;
|
|
189
239
|
private readonly occupancy: () => Record<string, MemberOccupancy> | Promise<Record<string, MemberOccupancy>>;
|
|
@@ -192,18 +242,30 @@ export class PodPoolScheduler {
|
|
|
192
242
|
this.pollMs = options.pollMs ?? 15_000;
|
|
193
243
|
// One lease directory per pool (contexts, not caps) — every CLI process launching into the same clusters shares it.
|
|
194
244
|
this.hostLease = options.hostLease ?? podPoolHostLease(members);
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
245
|
+
// GATE-RESERVE-AUTO: the gate reservation is returned beside the recommendation, never folded into it — the
|
|
246
|
+
// delta budget below must not see a gate start/end as a free-slot change (that made the result order-dependent).
|
|
247
|
+
const measureStatus: () => Promise<{ status: PoolLeaseRecommendation; gateSlots: number | null | undefined }> = options.status
|
|
248
|
+
? async () => ({ status: await options.status!(), gateSlots: undefined })
|
|
249
|
+
: async () => {
|
|
250
|
+
// POD-ADMIT-BY-USAGE: admission reads observed Pod usage (`kubectl top`); tryAcquire's occupancy read does not.
|
|
251
|
+
const measure = measurePoolLease(members, { ...(options.kubectl ? { kubectl: options.kubectl } : {}), ...(options.dns ? { dns: options.dns } : {}), measureUsage: true });
|
|
252
|
+
return { status: recommendConcurrency(measure, { capacity: members.reduce((n, m) => n + m.capacity, 0), accounts: 0, perAccount: 0 }), gateSlots: harnessCpuSlotsBesideGate(measure) };
|
|
253
|
+
};
|
|
254
|
+
/** Cluster-level additional slots for the cross-process host lease: the measured free slots, capped by the gate's CPU. */
|
|
255
|
+
const capped = ({ status, gateSlots }: { status: PoolLeaseRecommendation; gateSlots: number | null | undefined }): PoolLeaseRecommendation =>
|
|
256
|
+
gateSlots === undefined || status.recommended === null ? status
|
|
257
|
+
: gateSlots === null ? { ...status, recommended: null, reason: '측정 불가: gate CPU' }
|
|
258
|
+
: { ...status, recommended: Math.min(status.recommended, gateSlots) };
|
|
259
|
+
this.measure = async () => { const reading = await measureStatus(); this.lastUncapped = reading.status; return capped(reading); };
|
|
200
260
|
// An injected status (tests, callers with their own measurement) without memberMemory never shells out to kubectl here.
|
|
201
261
|
this.memberMemory = options.memberMemory ?? (options.status ? (() => []) : () => measurePoolLease(members, { ...(options.kubectl ? { kubectl: options.kubectl } : {}), ...(options.dns ? { dns: options.dns } : {}) }).members
|
|
202
262
|
.map((m: PodLeaseMember) => ({ context: m.context, allocatableMemoryBytes: m.allocatableMemoryBytes })));
|
|
203
263
|
this.occupancy = options.occupancy ?? (() => measureMemberOccupancy(members, { ...(options.kubectl ? { kubectl: options.kubectl } : {}), ...(options.dns ? { dns: options.dns } : {}) }));
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
264
|
+
const decide = async (gate: boolean): Promise<PoolLeaseRecommendation> => {
|
|
265
|
+
const reading = await measureStatus();
|
|
266
|
+
const measured = reading.status;
|
|
267
|
+
this.lastRaw = capped(reading);
|
|
268
|
+
this.lastUncapped = measured;
|
|
207
269
|
const pendingJobs = measured.pendingJobs ?? [];
|
|
208
270
|
const ownStart = this.hostLease.selfStart();
|
|
209
271
|
const transferId = process.env.ELANOUS_POD_RESERVED_LEASE;
|
|
@@ -219,16 +281,40 @@ export class PodPoolScheduler {
|
|
|
219
281
|
this.budget = Math.max(0, this.budget + decision.recommended - (this.initialized ? this.lastFree : 0));
|
|
220
282
|
this.initialized = true;
|
|
221
283
|
this.lastFree = decision.recommended;
|
|
222
|
-
|
|
223
|
-
|
|
284
|
+
// Each queue's limit is its own permits plus the shared remaining budget (every permit already took one from it).
|
|
285
|
+
const active = (gate ? this.gateAdmission : this.admission).snapshot().active;
|
|
286
|
+
const total = active + this.budget;
|
|
287
|
+
if (reading.gateSlots === undefined) return { ...decision, recommended: total };
|
|
288
|
+
if (gate) {
|
|
289
|
+
debug.log('pod.pool', 'gate-reserve', { gateSlots: reading.gateSlots, active, budget: this.budget, recommended: total, exempt: 'gate-caller' });
|
|
290
|
+
return { ...decision, recommended: total };
|
|
291
|
+
}
|
|
292
|
+
// GATE-RESERVE-AUTO: while a gate shard Job is active, harness admission is also bounded by the CPU left after
|
|
293
|
+
// the gate. The bound is absolute (this reading ⊕ current local permits). Pods already in the reading — observed
|
|
294
|
+
// ones (`seenActive`) and ones measured Pending (own leases with a pending Pod) — are not subtracted again;
|
|
295
|
+
// the remaining local permits and other processes' leases without a measured Pod (`others`) are.
|
|
296
|
+
const ownMeasuredPending = this.hostLease.live().filter((r) => r.pid === process.pid && r.startedAt === ownStart && leaseHasPendingPod(r, pendingJobs)).length;
|
|
297
|
+
const unseen = Math.max(0, active - this.seenActive - ownMeasuredPending);
|
|
298
|
+
const cpuFree = reading.gateSlots === null ? 0 : Math.max(0, reading.gateSlots - others + (transferIsOther ? 1 : 0) - unseen);
|
|
299
|
+
const recommended = Math.min(total, active + cpuFree);
|
|
300
|
+
if (recommended < total) debug.log('pod.pool', 'gate-reserve', { gateSlots: reading.gateSlots, active, unseen, ownMeasuredPending, others, budget: this.budget, recommended });
|
|
301
|
+
return reading.gateSlots === null
|
|
302
|
+
? { ...decision, recommended, reason: '측정 불가: gate CPU' }
|
|
303
|
+
: { ...decision, recommended };
|
|
304
|
+
};
|
|
305
|
+
const dependencyMerged = options.dependencyMerged ?? ((after: PodLeasePredecessor, signal?: AbortSignal) => predecessorState(after, {}, signal));
|
|
306
|
+
this.admission = new PodLeaseAdmission({ status: () => decide(false), ...(options.pollMs ? { pollMs: options.pollMs } : {}), dependencyMerged });
|
|
307
|
+
this.gateAdmission = new PodLeaseAdmission({ status: () => decide(true), ...(options.pollMs ? { pollMs: options.pollMs } : {}), dependencyMerged });
|
|
224
308
|
}
|
|
225
309
|
/** FIFO gate within this process, then a host lease so independent CLI processes cannot over-admit the same pool. */
|
|
226
|
-
async acquireAdmission(signal?: AbortSignal, after?: PodLeasePredecessor): Promise<PodLeaseRelease & { applied: (job: string, context: string, namespace: string) => void; observed: () => void }> {
|
|
227
|
-
const
|
|
310
|
+
async acquireAdmission(signal?: AbortSignal, after?: PodLeasePredecessor, opts: { gate?: boolean } = {}): Promise<PodLeaseRelease & { applied: (job: string, context: string, namespace: string) => void; observed: () => void }> {
|
|
311
|
+
const gate = opts.gate === true;
|
|
312
|
+
const local = await this.acquireLocalAdmission(signal, after, gate);
|
|
228
313
|
try {
|
|
229
314
|
for (;;) {
|
|
230
315
|
if (signal?.aborted) throw new Error('pod lease admission aborted');
|
|
231
|
-
|
|
316
|
+
// GATE-ADMIT-EXEMPT: a gate caller is not bounded by its own gate's CPU reservation (memory still bounds it).
|
|
317
|
+
const free = (gate ? this.lastUncapped : this.lastRaw)?.recommended ?? null;
|
|
232
318
|
// Same check as the local gate, but atomic across processes: other authoring and pre-Running leases hold slots.
|
|
233
319
|
const reservedId = process.env.ELANOUS_POD_RESERVED_LEASE;
|
|
234
320
|
const transferred = reservedId ? this.hostLease.claim(reservedId) : null;
|
|
@@ -241,9 +327,16 @@ export class PodPoolScheduler {
|
|
|
241
327
|
if (lease) {
|
|
242
328
|
let hostReleased = false;
|
|
243
329
|
const releaseHost = () => { if (!hostReleased) { hostReleased = true; lease(); } };
|
|
244
|
-
|
|
330
|
+
// GATE-RESERVE-AUTO: an observed Pod (Running or terminal) is in the CPU reading; until then it is subtracted.
|
|
331
|
+
let seen = false, done = false;
|
|
332
|
+
return Object.assign(() => {
|
|
333
|
+
if (done) return;
|
|
334
|
+
done = true;
|
|
335
|
+
if (seen) this.seenActive--;
|
|
336
|
+
releaseHost(); local();
|
|
337
|
+
}, {
|
|
245
338
|
applied: (job: string, context: string, namespace: string) => lease.applied(job, context, namespace),
|
|
246
|
-
observed: releaseHost,
|
|
339
|
+
observed: () => { if (!seen && !done) { seen = true; this.seenActive++; } releaseHost(); },
|
|
247
340
|
});
|
|
248
341
|
}
|
|
249
342
|
await new Promise<void>((resolve, reject) => {
|
|
@@ -251,15 +344,15 @@ export class PodPoolScheduler {
|
|
|
251
344
|
const onAbort = () => { clearTimeout(timer); reject(new Error('pod lease admission aborted')); };
|
|
252
345
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
253
346
|
});
|
|
254
|
-
try { this.lastRaw = await this.measure(); } catch { this.lastRaw = null; }
|
|
347
|
+
try { this.lastRaw = await this.measure(); } catch { this.lastRaw = null; this.lastUncapped = null; }
|
|
255
348
|
}
|
|
256
349
|
} catch (error) {
|
|
257
350
|
local();
|
|
258
351
|
throw error;
|
|
259
352
|
}
|
|
260
353
|
}
|
|
261
|
-
private acquireLocalAdmission(signal?: AbortSignal, after?: PodLeasePredecessor): Promise<PodLeaseRelease> {
|
|
262
|
-
return this.admission.acquire(signal, after).then((release) => {
|
|
354
|
+
private acquireLocalAdmission(signal?: AbortSignal, after?: PodLeasePredecessor, gate = false): Promise<PodLeaseRelease> {
|
|
355
|
+
return (gate ? this.gateAdmission : this.admission).acquire(signal, after).then((release) => {
|
|
263
356
|
this.budget = Math.max(0, this.budget - 1);
|
|
264
357
|
this.outstanding.add(release);
|
|
265
358
|
return () => {
|
|
@@ -289,32 +382,47 @@ export class PodPoolScheduler {
|
|
|
289
382
|
if (!r) return null;
|
|
290
383
|
return `recommended=${r.recommended ?? 'unknown'} limitedBy=${r.limitedBy ?? '-'} memorySlots=${r.memorySlots ?? '?'}${r.reason ? ` reason=${r.reason}` : ''}`;
|
|
291
384
|
}
|
|
292
|
-
|
|
293
|
-
|
|
385
|
+
/** The harness queue by default; `{ gate: true }` reads the gate-caller queue (GATE-ADMIT-EXEMPT). */
|
|
386
|
+
admissionSnapshot(opts: { gate?: boolean } = {}): ReturnType<PodLeaseAdmission['snapshot']> {
|
|
387
|
+
return (opts.gate ? this.gateAdmission : this.admission).snapshot();
|
|
294
388
|
}
|
|
295
389
|
/**
|
|
296
390
|
* Pick the member with the largest free share: (capacity − measured cluster occupancy − this process's inflight) / capacity.
|
|
297
391
|
* POOL-SPREAD (10-06): comparing absolute free slots always picked the big member (node-b 25 vs node-c 4), so the small
|
|
298
392
|
* member idled until the big one was full. Comparing the free share fills members in proportion to their capacity.
|
|
299
393
|
* A member whose occupancy could not be read falls back to inflight-only (never treated as zero occupied).
|
|
394
|
+
* `selfCapped` (POOLGLOBAL-REG 10-07): a caller with its own concurrency cap (the release gate) counts only its own Jobs
|
|
395
|
+
* — free = capacity − this scheduler's inflight, summed over members as the pool total — and reads no cluster occupancy.
|
|
396
|
+
* #24253 made every acquire subtract all Running+Pending harness goal Pods, so with ~24 goal Pods on the gate's 25-slot
|
|
397
|
+
* member the 0.2.15 gate launched its shards one at a time.
|
|
300
398
|
* Ties keep the configured member order. Returns null when every readable member is full.
|
|
301
399
|
*/
|
|
302
|
-
async tryAcquire(skip?: ReadonlySet<string>, onEmpty?: (reason: string) => void): Promise<PodPoolMember | null> {
|
|
400
|
+
async tryAcquire(skip?: ReadonlySet<string>, onEmpty?: (reason: string) => void, mode: { selfCapped?: boolean; kind?: PodPlacementKind | null; onPlacement?: (placement: PodPlacement) => void } = {}): Promise<PodPoolMember | null> {
|
|
401
|
+
const selfCapped = mode.selfCapped === true;
|
|
402
|
+
const kind = mode.kind ?? null;
|
|
403
|
+
// POOL-LITE: lite goals try the small member first, implementation goals the big one; full → overflow by free share.
|
|
404
|
+
const preferred = new Set(preferredMembers(this.members, kind));
|
|
303
405
|
let reading: Record<string, MemberOccupancy>;
|
|
304
|
-
|
|
406
|
+
// A self-capped caller's own Jobs are its inflight; other workloads' Pods are not its slots (no cluster read).
|
|
407
|
+
if (selfCapped) reading = Object.fromEntries(this.members.map((m) => [m.context, { occupied: 0 }]));
|
|
408
|
+
else try { reading = await this.occupancy(); }
|
|
305
409
|
catch (error) {
|
|
306
410
|
const reason = error instanceof Error ? error.message : String(error);
|
|
307
411
|
reading = Object.fromEntries(this.members.map((m) => [m.context, { occupied: null, reason }]));
|
|
308
412
|
}
|
|
309
413
|
const free: Record<string, number> = {};
|
|
310
414
|
const measured: Record<string, boolean> = {};
|
|
311
|
-
let best: { member: PodPoolMember; slots: number; measured: boolean } | null = null;
|
|
415
|
+
let best: { member: PodPoolMember; slots: number; measured: boolean; preferred: boolean } | null = null;
|
|
312
416
|
// A measured member always beats an unmeasured one (its share would read 100% and draw every Job to a node
|
|
313
417
|
// that did not answer); unmeasured members compete only when no measured member has a free slot.
|
|
314
|
-
|
|
418
|
+
// Among equally measured members a preferred one (POOL-LITE) beats the rest; then the larger free share wins.
|
|
419
|
+
const better = (cand: { member: PodPoolMember; slots: number; measured: boolean; preferred: boolean }) => !best
|
|
315
420
|
|| (cand.measured !== best.measured ? cand.measured
|
|
316
|
-
: cand.
|
|
421
|
+
: cand.preferred !== best.preferred ? cand.preferred
|
|
422
|
+
: cand.slots / cand.member.capacity > best.slots / best.member.capacity);
|
|
317
423
|
for (const m of this.members) {
|
|
424
|
+
// A capacity-0 member is off — never chosen (and no division by zero in the share).
|
|
425
|
+
if (m.capacity <= 0) continue;
|
|
318
426
|
// LAUNCH-STALL: a member this launch cannot fit is skipped for this call only (other launches may fit it).
|
|
319
427
|
if (skip?.has(m.context)) continue;
|
|
320
428
|
const inflight = this.inflight.get(m.context) ?? 0;
|
|
@@ -325,17 +433,30 @@ export class PodPoolScheduler {
|
|
|
325
433
|
const slots = m.capacity - occupied - inflight;
|
|
326
434
|
free[m.context] = slots;
|
|
327
435
|
if (slots <= 0) continue;
|
|
328
|
-
const cand = { member: m, slots, measured: ok };
|
|
436
|
+
const cand = { member: m, slots, measured: ok, preferred: preferred.has(m.context) };
|
|
329
437
|
if (better(cand)) best = cand;
|
|
330
438
|
}
|
|
331
439
|
if (!best) {
|
|
332
|
-
onEmpty?.(`no free slot · free=${JSON.stringify(free)} · measured=${JSON.stringify(measured)}${skip?.size ? ` · skipped=${[...skip].join(',')}` : ''}`);
|
|
440
|
+
onEmpty?.(`no free slot · free=${JSON.stringify(free)} · measured=${JSON.stringify(measured)}${selfCapped ? ' · selfCapped' : ''}${skip?.size ? ` · skipped=${[...skip].join(',')}` : ''}${kind ? ` · kind=${kind}` : ''}`);
|
|
333
441
|
return null;
|
|
334
442
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
443
|
+
const chosen: { member: PodPoolMember; slots: number; measured: boolean; preferred: boolean } = best;
|
|
444
|
+
this.inflight.set(chosen.member.context, (this.inflight.get(chosen.member.context) ?? 0) + 1);
|
|
445
|
+
const share = Object.fromEntries(this.members.map((m) => [m.context, m.capacity > 0 ? Math.round(Math.max(0, free[m.context] ?? 0) / m.capacity * 100) / 100 : 0]));
|
|
446
|
+
const outcome: PodPlacement['outcome'] = preferred.size === 0 ? 'share' : chosen.preferred ? 'preferred' : 'overflow';
|
|
447
|
+
const pref = [...preferred];
|
|
448
|
+
const why = (c: string) => skip?.has(c) ? 'skipped' : measured[c] === false ? 'unmeasured' : 'full';
|
|
449
|
+
const reason = outcome === 'share'
|
|
450
|
+
? `${chosen.member.context} · free share ${share[chosen.member.context]}${kind ? ` · ${kind} 골 · 선호 멤버 없음(${this.members.filter((m) => m.capacity > 0).length < 2 ? '단일 멤버' : '상한이 같다'})` : ''}`
|
|
451
|
+
: outcome === 'preferred'
|
|
452
|
+
? `${chosen.member.context} · ${kind} 골 선호 멤버 · free share ${share[chosen.member.context]}`
|
|
453
|
+
: `${chosen.member.context} · ${kind} 골 넘침 — 선호 ${pref.map((c) => `${c}(${why(c)})`).join(',')} · free share ${share[chosen.member.context]}`;
|
|
454
|
+
const placement: PodPlacement = { member: chosen.member.context, kind, outcome, preferred: pref, reason };
|
|
455
|
+
debug.log('pod.pool', 'member-selected', { member: chosen.member.context, free, measured, share, ...(selfCapped ? { selfCapped: true } : {}), ...(kind ? { kind, outcome, preferred: pref, reason } : {}) });
|
|
456
|
+
// The callback only observes the placement — a throwing observer must not leak the slot it was told about.
|
|
457
|
+
try { mode.onPlacement?.(placement); }
|
|
458
|
+
catch (error) { debug.log('pod.pool', 'placement-observer-failed', { member: chosen.member.context, reason: error instanceof Error ? error.message : String(error) }, { level: 'warn' }); }
|
|
459
|
+
return chosen.member;
|
|
339
460
|
}
|
|
340
461
|
release(member: PodPoolMember): void {
|
|
341
462
|
this.inflight.set(member.context, Math.max(0, (this.inflight.get(member.context) ?? 0) - 1));
|
|
@@ -48,9 +48,9 @@ export const POD_JOB_DEADLINE_SECONDS = 10_800;
|
|
|
48
48
|
// POD-DIET (10-06 03:47~05:18 · node-b goal Pods · 89 one-minute samples · 42 Pods): per-Pod observed max median 4.6Gi ·
|
|
49
49
|
// p75 6.2Gi · p90 11.7Gi — 23/42 went above the old 4Gi request, so the scheduler over-packed. 6Gi ≈ p75; limits stay.
|
|
50
50
|
// Node allocatable ≈ 343Gi ≫ 25 × 6Gi. One-minute samples can miss the true peak (lower bound).
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
import { measurePoolLease, recommendConcurrency, POD_HOST_LEASE_ANNOTATION, LEASE_KUBECTL_MAX_BUFFER, memoryQuantityBytes } from './pod-lease.js';
|
|
51
|
+
import type { PodPlacement, PodPlacementKind, PodPoolMember, PodPoolScheduler } from './pod-pool.js';
|
|
52
|
+
export { POD_CHILD_REQUESTS } from './pod-lease.js';
|
|
53
|
+
import { measurePoolLease, recommendConcurrency, POD_CHILD_REQUESTS, POD_HOST_LEASE_ANNOTATION, LEASE_KUBECTL_MAX_BUFFER, memoryQuantityBytes } from './pod-lease.js';
|
|
54
54
|
import { ACTUAL_SUBSTRATE_ENV, RUN_CONTRACT_ENV, carryRunContract, completionFloorFor } from '../../self-implement/graph-run-contract.js';
|
|
55
55
|
import { spawnSync } from 'node:child_process';
|
|
56
56
|
import { createHash } from 'node:crypto';
|
|
@@ -654,6 +654,16 @@ export function parsePodMemoryTier(v: string | undefined): PodMemoryTier | null
|
|
|
654
654
|
return t && (POD_MEMORY_TIERS as readonly string[]).includes(t) ? (t as PodMemoryTier) : null;
|
|
655
655
|
}
|
|
656
656
|
|
|
657
|
+
/**
|
|
658
|
+
* POOL-LITE (10-07): which pool member a Job prefers. Lite tier, or a docs/measurement goal not raised to `high`,
|
|
659
|
+
* goes small-first (node-c); everything else is an implementation Job and goes big-first (node-b).
|
|
660
|
+
*/
|
|
661
|
+
export function podPlacementKind(tier: PodMemoryTier, declared: DeclaredGoalType | null, requestKind: string | null): PodPlacementKind {
|
|
662
|
+
if (tier === 'lite') return 'lite';
|
|
663
|
+
if (tier !== 'high' && (declared === 'document' || declared === 'research' || requestKind === 'docs')) return 'lite';
|
|
664
|
+
return 'standard';
|
|
665
|
+
}
|
|
666
|
+
|
|
657
667
|
function parsePodGoalType(value: string | undefined): DeclaredGoalType | null {
|
|
658
668
|
return value === 'implement' || value === 'research' || value === 'document' || value === 'operate' ? value : null;
|
|
659
669
|
}
|
|
@@ -991,6 +1001,8 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
|
|
|
991
1001
|
// pool-slot reason = what THIS launch saw on its last empty tryAcquire; admission reason = the pool-wide lease measurement (one pool, one reading).
|
|
992
1002
|
const stageReason: { 'pool-slot'?: string | null } = {};
|
|
993
1003
|
const unfitContexts = new Set<string>();
|
|
1004
|
+
// POOL-LITE (10-07): the last placement of this launch — printed at launch and journaled with the Job.
|
|
1005
|
+
let lastPlacement = null as PodPlacement | null;
|
|
994
1006
|
const watchStall = (stage: 'admission' | 'pool-slot'): (() => void) => {
|
|
995
1007
|
const started = Date.now();
|
|
996
1008
|
const timer = setTimeout(() => {
|
|
@@ -1026,9 +1038,12 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
|
|
|
1026
1038
|
for (;;) {
|
|
1027
1039
|
if (input.signal?.aborted) return null;
|
|
1028
1040
|
// The reason comes back inside this call, so a concurrent launch cannot overwrite it.
|
|
1029
|
-
const
|
|
1041
|
+
const kind = podPlacementKind(oomRetried ? retryTier : memoryTier, selectedGoalType, requestKind);
|
|
1042
|
+
const acquired = await options.pool.tryAcquire(unfitContexts, (reason) => { stageReason['pool-slot'] = reason; },
|
|
1043
|
+
{ kind, onPlacement: (placement) => { lastPlacement = placement; } });
|
|
1030
1044
|
if (acquired) {
|
|
1031
|
-
debug.log('self-implement.pod', 'pool-slot', { spaceId: input.spaceId, context: acquired.context, inflight: options.pool.snapshot() });
|
|
1045
|
+
debug.log('self-implement.pod', 'pool-slot', { spaceId: input.spaceId, context: acquired.context, inflight: options.pool.snapshot(), ...(lastPlacement ? { placement: lastPlacement } : {}) });
|
|
1046
|
+
if (lastPlacement) console.warn(`[pod] 배치: ${lastPlacement.reason}`);
|
|
1032
1047
|
return acquired;
|
|
1033
1048
|
}
|
|
1034
1049
|
await sleep(options.pollMs ?? 15_000);
|
|
@@ -1451,6 +1466,7 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
|
|
|
1451
1466
|
}
|
|
1452
1467
|
try {
|
|
1453
1468
|
appendRunLedgerEntry({ runId: ledgerRunId, event: 'pod-child-run', data: { childRunId: liveChildRunId, job: name, attempt: oomRetried ? 2 : 1, ...(goalFile ? { goalFile } : {}) } }, ledgerDir);
|
|
1469
|
+
if (lastPlacement) appendRunLedgerEntry({ runId: ledgerRunId, event: 'pod-placement', data: { job: name, childRunId: liveChildRunId, attempt: oomRetried ? 2 : 1, member: lastPlacement.member, kind: lastPlacement.kind, outcome: lastPlacement.outcome, preferred: lastPlacement.preferred, reason: lastPlacement.reason } }, ledgerDir);
|
|
1454
1470
|
appendRunLedgerEntry({ runId: ledgerRunId, event: 'pod-memory-selected', data: { job: name, childRunId: liveChildRunId, attempt: oomRetried ? 2 : 1, limit: memoryLimit, ...(appliedMemoryRequest ? { request: appliedMemoryRequest } : {}), tier: oomRetried ? retryTier : memoryTier, source: oomRetried ? 'oom-retry' : memorySource, reason: oomRetried ? 'OOMKilled' : memoryReason, declaredGoalType: selectedGoalType } }, ledgerDir);
|
|
1455
1471
|
if (oomRetried && retryFromChildRunId) appendRunLedgerEntry({ runId: ledgerRunId, event: 'pod-oom-retry', data: { attempt: 2, from: retryFromMemoryLimit ?? selectedMemoryLimit, to: memoryLimit, tier: retryTier, reason: 'OOMKilled', job: name, childRunId: liveChildRunId, fromChildRunId: retryFromChildRunId } }, ledgerDir);
|
|
1456
1472
|
} catch (error) {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { readPresence, writePresence, type Presence } from './away/presence.js';
|
|
2
|
+
import { releaseNowText, releaseWatchTick } from './away/release-watch.js';
|
|
3
|
+
import { readReleaseRuns, type ReleaseRunView } from './nexus/api/ops-api.js';
|
|
4
|
+
|
|
5
|
+
/** AWAY-MODE-1 ③ — 텔레그램 /release: PWA /ops/release 와 같은 원장을 읽어 지금 런·노드를 한 통으로. */
|
|
6
|
+
export function telegramReleaseStatus(args: string[], runs: () => ReleaseRunView[] | 'unavailable' = () => readReleaseRuns(null, undefined, { facts: true }), now: () => number = Date.now): string {
|
|
7
|
+
if (args.length) return '사용법: /release';
|
|
8
|
+
return releaseNowText(runs(), now());
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface AwaySlashDeps {
|
|
12
|
+
read?: () => Presence;
|
|
13
|
+
write?: (away: boolean, by: string) => Presence;
|
|
14
|
+
tick?: () => unknown;
|
|
15
|
+
release?: () => string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** AWAY-MODE-1 ① — 텔레그램 /away on|off|status. 답은 이 대화로 돌아오므로 따로 발송하지 않는다. */
|
|
19
|
+
export function telegramAwaySlash(args: string[], deps: AwaySlashDeps = {}): string {
|
|
20
|
+
const read = deps.read ?? (() => readPresence());
|
|
21
|
+
const write = deps.write ?? ((away: boolean, by: string) => writePresence(away, by));
|
|
22
|
+
const tick = deps.tick ?? (() => releaseWatchTick());
|
|
23
|
+
const release = deps.release ?? (() => telegramReleaseStatus([]));
|
|
24
|
+
const sub = (args[0] ?? 'status').toLowerCase();
|
|
25
|
+
if (args.length > 1 || !['on', 'off', 'status'].includes(sub)) return '사용법: /away on | off | status';
|
|
26
|
+
if (sub === 'on') {
|
|
27
|
+
write(true, 'telegram');
|
|
28
|
+
tick();
|
|
29
|
+
return `🚶 외출 모드 켬 — 발행 노드 전이·막힘·60분 생존 줄을 여기로 보낸다(끄기: /away off)\n${release()}`;
|
|
30
|
+
}
|
|
31
|
+
if (sub === 'off') {
|
|
32
|
+
write(false, 'telegram');
|
|
33
|
+
tick();
|
|
34
|
+
return '🏠 외출 모드 끔 — 발행 알림은 평소대로';
|
|
35
|
+
}
|
|
36
|
+
const presence = read();
|
|
37
|
+
return `${presence.away ? '🚶 외출 중' : '🏠 자리에 있음'}${presence.since ? ` · ${presence.since}` : ''}\n${release()}`;
|
|
38
|
+
}
|
package/src/telegram-commands.ts
CHANGED
|
@@ -50,10 +50,12 @@ import { createAdPipelineDeps, runAdPipeline } from './ad-pipeline/run.js';
|
|
|
50
50
|
import { projectCommand } from './telegram-project-command.js';
|
|
51
51
|
import { telegramNowSlash } from './context-bus/context-now-surfaces.js';
|
|
52
52
|
import { telegramLoopsStatus } from './telegram-loops-command.js';
|
|
53
|
+
import { telegramAwaySlash, telegramReleaseStatus } from './telegram-away-command.js';
|
|
53
54
|
import { createWishCard } from './intake-plane/wish-card.js';
|
|
54
55
|
import { telegramDecisionOwner } from './decisions/telegram-decision-cards.js';
|
|
55
56
|
import { CardStore } from './task-cards/card-store.js';
|
|
56
57
|
import type { ContextNowDeps } from './context-bus/context-now.js';
|
|
58
|
+
import type { RunningRunsResult } from './self-implement/running-runs.js';
|
|
57
59
|
|
|
58
60
|
/** How many prior user/assistant turns to pass into `executeSkill` as
|
|
59
61
|
* the `## Recent conversation` block. Mirrors the dashboard default
|
|
@@ -304,6 +306,40 @@ function dropChatSessionReply(ctx: TgIncoming): string {
|
|
|
304
306
|
: `_Session \`${sess.id.slice(0, 8)}\` was already gone._`;
|
|
305
307
|
}
|
|
306
308
|
|
|
309
|
+
/** `/runs` — the Telegram twin of TUI `/harness runs`: the same `renderRunningRuns` text over the same
|
|
310
|
+
* production-universe query. The query is injectable so a test can prove the reply equals the renderer
|
|
311
|
+
* output for one data set. Long replies are chunked by the bot's sender, so no run line is dropped here. */
|
|
312
|
+
export async function telegramRunsSlash(args: readonly string[], query?: () => RunningRunsResult,
|
|
313
|
+
runCli: (argv: readonly string[]) => Promise<{ code: number | null; stdout: string }> = runSelfCli): Promise<string> {
|
|
314
|
+
if (args.length > 0) return 'Usage: /runs';
|
|
315
|
+
const { queryRunningRuns, renderRunningRuns } = await import('./self-implement/running-runs.js');
|
|
316
|
+
if (query) return renderRunningRuns(query());
|
|
317
|
+
// The ledger scan is synchronous and took 30–58 s under load (TC 10-07 17:37) — run inside the daemon it would freeze
|
|
318
|
+
// every bot and HTTP reply for that long. Ask the CLI twin (`self running-runs`, same renderer) in a child process,
|
|
319
|
+
// pinned to this daemon's universe; fall back in-process only when the child cannot start.
|
|
320
|
+
const { effectiveInstanceRoot } = await import('./instance/resolve.js');
|
|
321
|
+
try {
|
|
322
|
+
const { code, stdout } = await runCli(['--config-dir', effectiveInstanceRoot(), 'self', 'running-runs']);
|
|
323
|
+
if (code === 0 && stdout.trim()) return stdout.trimEnd();
|
|
324
|
+
} catch { /* fall through */ }
|
|
325
|
+
return renderRunningRuns(queryRunningRuns({ includeTest: false, caller: 'telegram' }));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Run this same elanous entry as a child (`bun <entry> …`) with a hard time limit. */
|
|
329
|
+
async function runSelfCli(argv: readonly string[]): Promise<{ code: number | null; stdout: string }> {
|
|
330
|
+
const { spawn } = await import('node:child_process');
|
|
331
|
+
const entry = process.argv[1];
|
|
332
|
+
if (!entry) throw new Error('no elanous entry');
|
|
333
|
+
return new Promise((resolve, reject) => {
|
|
334
|
+
const child = spawn(process.execPath, [entry, ...argv], { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
335
|
+
let stdout = '';
|
|
336
|
+
const timer = setTimeout(() => { child.kill('SIGTERM'); reject(new Error('timeout')); }, 120_000);
|
|
337
|
+
child.stdout.setEncoding('utf8').on('data', (chunk: string) => { stdout += chunk; });
|
|
338
|
+
child.on('error', (error) => { clearTimeout(timer); reject(error); });
|
|
339
|
+
child.on('close', (code) => { clearTimeout(timer); resolve({ code, stdout }); });
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
307
343
|
/** Build the default command set for a Telegram bot wired via
|
|
308
344
|
* botFromConfig. Returns the array rather than mutating globals so
|
|
309
345
|
* tests can construct a variant set with stubbed handlers. */
|
|
@@ -311,6 +347,7 @@ export function defaultTelegramCommands(
|
|
|
311
347
|
nowDeps?: ContextNowDeps,
|
|
312
348
|
coreCommands: readonly SlashCommand[] = SLASH_COMMANDS,
|
|
313
349
|
maturity: { tuiSlash: Readonly<Record<string, Maturity>>; telegramCommand: Readonly<Record<string, Maturity>>; discordCommand: Readonly<Record<string, Maturity>> } = FEATURE_MATURITY,
|
|
350
|
+
runningRunsQuery?: () => RunningRunsResult,
|
|
314
351
|
): TgSlashCommand[] {
|
|
315
352
|
const handled: TgSlashCommand[] = [
|
|
316
353
|
{
|
|
@@ -334,6 +371,21 @@ export function defaultTelegramCommands(
|
|
|
334
371
|
description: '루프·크론 현황 — 늦음·실패를 먼저 보여줍니다',
|
|
335
372
|
handler: (args) => telegramLoopsStatus(args),
|
|
336
373
|
},
|
|
374
|
+
{
|
|
375
|
+
name: 'runs',
|
|
376
|
+
description: 'Running harness runs — same output as TUI /harness runs',
|
|
377
|
+
handler: async (args) => telegramRunsSlash(args, runningRunsQuery),
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
name: 'release',
|
|
381
|
+
description: '발행 현황 — 지금 런·노드·막힘을 한 통으로',
|
|
382
|
+
handler: async (args) => telegramReleaseStatus(args),
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
name: 'away',
|
|
386
|
+
description: '외출 모드 — /away on|off|status (켜면 발행 전이를 여기로)',
|
|
387
|
+
handler: async (args) => telegramAwaySlash(args),
|
|
388
|
+
},
|
|
337
389
|
{
|
|
338
390
|
name: 'status',
|
|
339
391
|
description: 'Show bot identity, current session, provider + model',
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { sendTelegramReport } from './telegram-report.js';
|
|
2
|
+
import { getUserConfig, setUserConfigOverlay } from './user-config.js';
|
|
3
|
+
import { registerStandaloneLogSink } from './domains/standalone-log-sink.js';
|
|
4
|
+
|
|
5
|
+
// Focused subprocess fixture (BRIEF-DELIVERY-1007): the production shape — a channels table whose
|
|
6
|
+
// trading channel lacks the `report` role while the legacy reportChannel points at that bot.
|
|
7
|
+
setUserConfigOverlay(cfg => ({
|
|
8
|
+
...cfg,
|
|
9
|
+
telegram: {
|
|
10
|
+
...cfg.telegram,
|
|
11
|
+
channels: [
|
|
12
|
+
{ name: 'main', botToken: '111111:private-secret', chatId: 98765, interactive: true, roles: ['qa', 'default', 'system'] },
|
|
13
|
+
{ name: 'conatus', botToken: '222222:private-secret', chatId: 98766, interactive: false, roles: ['investment', 'finance'] },
|
|
14
|
+
],
|
|
15
|
+
reportChannel: { botToken: '222222:private-secret', chatId: 98766 },
|
|
16
|
+
},
|
|
17
|
+
}));
|
|
18
|
+
// The entry point attaches logs.db, exactly as cron morning-report does.
|
|
19
|
+
if (process.env.NODE_ENV !== 'test') await registerStandaloneLogSink('telegram-report-probe');
|
|
20
|
+
console.log(`probe-start ${new Date().toISOString()}`);
|
|
21
|
+
if (await sendTelegramReport(getUserConfig(), 'SECRET-BODY-DO-NOT-LOG', { kind: 'report' })) process.exitCode = 1;
|
package/src/telegram-report.ts
CHANGED
|
@@ -11,11 +11,25 @@
|
|
|
11
11
|
// the report channel while interactive Q&A stays on the main bot.
|
|
12
12
|
|
|
13
13
|
import { TelegramBot } from './telegram.js';
|
|
14
|
-
import { isTradingKind, kindRouteTarget, logKindRouteFallback, mainHomeTarget } from './domains/telegram-kind-route.js';
|
|
14
|
+
import { explainReportRoute, isTradingKind, kindRouteTarget, logKindRouteFallback, mainHomeTarget } from './domains/telegram-kind-route.js';
|
|
15
|
+
export { explainReportRoute } from './domains/telegram-kind-route.js';
|
|
15
16
|
export { DEFAULT_KIND_ROLES, roleForKind } from './domains/telegram-kind-route.js';
|
|
16
17
|
import { resolveChannelBotToken } from './channel-bot-token.js';
|
|
17
18
|
import type { UserConfig } from './user-config.js';
|
|
18
19
|
import { findSessionByTelegramChat, createSession, appendMessage } from './session/index.js';
|
|
20
|
+
import { debug } from './debug/log.js';
|
|
21
|
+
// Observations go through debug.log only: a standalone caller (cron morning-report) registers its own
|
|
22
|
+
// logs.db sink at its entry point (registerStandaloneLogSink) — registering here would duplicate every
|
|
23
|
+
// row inside the daemon, which already has one (review r4).
|
|
24
|
+
function logReport(event: 'sent' | 'unconfirmed' | 'unrouted', data: Record<string, unknown>): void {
|
|
25
|
+
try { debug.log('telegram.report', event, data); } catch { /* fail-soft */ }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Public numeric bot id only — never the token's secret suffix. */
|
|
29
|
+
function botId(token: string): string {
|
|
30
|
+
const id = token.split(':', 1)[0] ?? '';
|
|
31
|
+
return /^\d+$/.test(id) ? id : 'configured';
|
|
32
|
+
}
|
|
19
33
|
|
|
20
34
|
/** Cross-surface memory — mirror an outbound report/alert into the
|
|
21
35
|
* RECEIVING channel's session transcript so a follow-up question in that
|
|
@@ -95,14 +109,24 @@ export async function sendTelegramReport(
|
|
|
95
109
|
opts: SendReportOpts = {},
|
|
96
110
|
): Promise<boolean> {
|
|
97
111
|
const target = resolveReportTarget(cfg, opts.kind);
|
|
98
|
-
if (!
|
|
112
|
+
if (!text) return false;
|
|
113
|
+
if (!target) {
|
|
114
|
+
const why = explainReportRoute(cfg, opts.kind);
|
|
115
|
+
logReport('unrouted', { kind: opts.kind ?? null, role: why.role, reason: why.reason });
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
99
118
|
const bot = new TelegramBot({
|
|
100
119
|
token: target.botToken,
|
|
101
120
|
allowedUsers: [],
|
|
102
121
|
onMessage: async () => undefined,
|
|
103
122
|
...(opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}),
|
|
104
123
|
});
|
|
105
|
-
await bot.sendMessage(target.chatId, text, { markdown: opts.markdown ?? true });
|
|
124
|
+
const sent = await bot.sendMessage(target.chatId, text, { markdown: opts.markdown ?? true });
|
|
125
|
+
// Delivered only with Telegram's message id back — an empty answer is unconfirmed and returns false (review r4).
|
|
126
|
+
const confirmed = Number.isSafeInteger(sent?.messageId) && (sent?.messageId ?? 0) > 0;
|
|
127
|
+
logReport(confirmed ? 'sent' : 'unconfirmed',
|
|
128
|
+
{ kind: opts.kind ?? null, role: explainReportRoute(cfg, opts.kind).role, bot: botId(target.botToken), chars: text.length });
|
|
129
|
+
if (!confirmed) return false;
|
|
106
130
|
// Mirror into the report channel's session so a follow-up question in
|
|
107
131
|
// that chat can recall this alert (Fix — was surface_events-only).
|
|
108
132
|
mirrorOutboundToSession(target, text);
|