elanous 0.2.17 → 0.2.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/P6YTVgzuZz5oTZhY4Din_/_buildManifest.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1307-aac7332576c4d6fc.js → 1307-30dff22c9e1f3073.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/195-fb1d7690bf30bab4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1986-33e13d648a9df765.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2537-6c289376caa88ca1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2699-325cc923ba7b8631.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3857-4f936cdb30322aa6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4554-5761612531d0146b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4565-3d7e3fdeb43686a8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5977-a11f37254196b659.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{6158.9c0449a9a8d0064b.js → 6158.af2e93b7adff709b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/6877-74399f69c7da1433.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7797-df5aa0381fdc078f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8401-8615a660c84cdb03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8695-846034ce44852c70.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/{page-3143b07a817d1da9.js → page-d92f7ee3994e84c7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-17ba086ef82d339b.js → page-4733c967b56a4277.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/{page-7eb661ff23af7da2.js → page-d22fd849cc22f104.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-bb742f995c6769e7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-6e5c3a867557298b.js → page-02b073a2ac793b83.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-8a9b3594bc23289b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/{page-5e77cd1802567c89.js → page-a0eb02638b1e6f31.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-308111abadf35670.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-4187666132fc98c4.js → page-5d97b2901e0d1ea6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-09328d499162bae4.js → page-c39554c321576982.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-29ff3a8f84f03b6a.js → page-8c3d8e63ae2e1c98.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-80ec7401bc83f511.js → page-9b72a07d424701c6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-fc7d86467291ae38.js → page-a572f58cd3ca5864.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-be5ea965a07fbc41.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/{page-0e2d6985fb42cd2f.js → page-c130b63862437814.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/market/{page-13e4108999f58d13.js → page-fa244bd146a00ae8.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-e8296ee284aa1293.js → page-44d56d6391965a6a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-043a9111e74f6b24.js → page-83a700d4c754d802.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-23a4601ceb7f452e.js → not-found-7b3170f374f77b9d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-10d958605a77b3bd.js → page-a3c58f5c72a5f3f9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/{page-7a1d85e121065cc8.js → page-e8e1ecaea7cd71d2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-12916918988ac97e.js → page-1467047104141297.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-d8a3347a9d0c3f4a.js → page-2da6f9b8a38c6978.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-102310930791197a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-6ed0b73bae8351d8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-1d824510df025d18.js → page-44d5f767491692bf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-43336055451f6e37.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/{page-7f49fe645d5132f7.js → page-3cc47dca0922ab9a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-a77ee355a2304ff0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-27d71f131acd9320.js → page-b17cd46cf0e9b8cf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-61553851aa606146.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/page-a1f585e154e61c59.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-424659bacfb8cdd7.js → page-7bf54c9bb9274af8.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-5150ef17b5383a57.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-5d4fef9ccd7d406b.js → page-134da468538baf48.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{webpack-539cde99554be20f.js → webpack-c2782b330447ffb6.js} +1 -1
- package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +14 -14
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +14 -14
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +14 -14
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +14 -14
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +14 -14
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +14 -14
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +14 -14
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +14 -14
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +14 -14
- package/apps/pwa/out/decisions/index.html +1 -0
- package/apps/pwa/out/decisions/index.txt +30 -0
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +14 -14
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +14 -14
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +14 -14
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +14 -14
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +14 -14
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +14 -14
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +14 -14
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +14 -14
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +14 -14
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +14 -14
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +14 -14
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +14 -14
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +14 -14
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +14 -14
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +14 -14
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +14 -14
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +14 -14
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +15 -15
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +14 -14
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +14 -14
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +14 -14
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +14 -14
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +14 -14
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +14 -14
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +14 -14
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +14 -14
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +14 -14
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +14 -14
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +14 -14
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +14 -14
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +14 -14
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +14 -14
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +14 -14
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +14 -14
- package/package.json +2 -1
- package/samples/workflows/templates/approval-gated-deploy.yaml +30 -0
- package/samples/workflows/templates/chat-driven-research.yaml +35 -0
- package/samples/workflows/templates/discord-listen-and-respond.yaml +29 -0
- package/samples/workflows/templates/http-poll-then-alert.yaml +30 -0
- package/samples/workflows/templates/manual-quickstart.yaml +21 -0
- package/samples/workflows/templates/scheduled-daily-digest.yaml +30 -0
- package/samples/workflows/templates/webhook-deploy-hook.yaml +27 -0
- package/scripts/ci-public-export-import-gate.ts +7 -2
- package/scripts/ci-typecheck-changed.ts +21 -6
- package/scripts/discord-seat-probe.ts +3 -2
- package/scripts/docs/generate-cli-anatomy.ts +162 -0
- package/scripts/goals/authoring-metrics.ts +73 -0
- package/scripts/landing-heal/must-fix-match.ts +7 -7
- package/scripts/landing-heal/run-mission.ts +3 -2
- package/scripts/public-export-test-run.ts +37 -2
- package/scripts/release-loop/checklist-gate-node.ts +13 -11
- package/scripts/release-loop/cut-branch.ts +2 -1
- package/scripts/release-loop/cutoff.ts +4 -2
- package/scripts/release-loop/docs-land-node.ts +8 -1
- package/scripts/release-loop/docs-node.ts +1 -1
- package/scripts/release-loop/gate-node.ts +99 -14
- package/scripts/release-loop/node-verdict.ts +2 -1
- package/scripts/release-loop/npm-publish-node.ts +11 -7
- package/scripts/release-loop/ops-upgrade-node.ts +8 -1
- package/scripts/release-loop/preflight.ts +246 -0
- package/scripts/release-loop/publish-node.ts +8 -1
- package/scripts/release-loop/release-readiness.ts +11 -6
- package/scripts/release-loop/release-version.ts +48 -0
- package/scripts/release-loop/resume-release.ts +115 -0
- package/scripts/release-loop/unattended-release.ts +43 -7
- package/scripts/release-loop/version-node.ts +145 -40
- package/scripts/rhythm/daily.ts +25 -4
- package/scripts/skills/public-check.ts +62 -12
- package/scripts/test-deterministic.ts +51 -33
- package/scripts/test-diet/effectiveness.ts +62 -1
- package/scripts/test-diet/lib.ts +1 -1
- package/scripts/test-diet/node.ts +8 -1
- package/src/agent-mission/review-loop.ts +4 -3
- package/src/agent-substrate/pr-comment-meta.ts +20 -0
- package/src/autopilot/build/llm-conflict-merge.ts +28 -8
- package/src/briefing/brief-items.ts +86 -17
- package/src/cli/autopilot-cli.ts +30 -11
- package/src/cli/brief-cli.ts +36 -2
- package/src/cli/freeze-cli.ts +3 -2
- package/src/cli/intake-cli.ts +73 -1
- package/src/cli/notify-cli.ts +75 -0
- package/src/cli/provider-cli.ts +12 -2
- package/src/cli/release-cli.ts +101 -17
- package/src/cli/tasks-cli.ts +309 -3
- package/src/context-bus/condense.ts +19 -3
- package/src/context-bus/context-day.ts +77 -24
- package/src/context-bus/context-first.ts +33 -0
- package/src/discord-self-message.ts +20 -2
- package/src/discord-slash-wire.ts +1 -0
- package/src/domains/outbound-alert.ts +200 -27
- package/src/domains/outbound-log-probe.ts +16 -1
- package/src/domains/release-tool.ts +2 -2
- package/src/execution-loop/gates.ts +11 -2
- package/src/flow-loop/split.ts +8 -4
- package/src/git-fs/worktree.ts +23 -3
- package/src/harness/harness-cli-command.ts +10 -2
- package/src/harness/harness-queue.ts +388 -33
- package/src/harness/helper-scan.ts +10 -3
- package/src/hq/seats.ts +369 -0
- package/src/index.ts +133 -15
- package/src/intake-plane/adopt-gate.ts +49 -0
- package/src/intake-plane/check.ts +3 -0
- package/src/intake-plane/collect-github.ts +57 -25
- package/src/intake-plane/discord-seat-work.ts +8 -2
- package/src/intake-plane/requirement-funnel.ts +316 -0
- package/src/knowledge/know-find.ts +10 -1
- package/src/loops/budget.ts +36 -0
- package/src/loops/orchestrator/finish-rate.ts +73 -1
- package/src/loops/orchestrator/grid-place.ts +11 -0
- package/src/loops/orchestrator/tick.ts +295 -20
- package/src/maturity/feature-maturity.ts +3 -2
- package/src/nexus/api/decisions-route.ts +11 -1
- package/src/nexus/api/loop-edges.ts +69 -13
- package/src/nexus/api/ops-seats-sources.ts +2 -2
- package/src/nexus/api/workflow-templates.ts +3 -0
- package/src/oauth/codex.ts +38 -0
- package/src/onboarding/first-chat-gate.ts +29 -0
- package/src/release-loop/checklist.ts +104 -6
- package/src/release-loop/feature-store.ts +10 -9
- package/src/release-loop/landing-freeze.ts +6 -4
- package/src/release-loop/placement.ts +13 -3
- package/src/release-loop/rubric.ts +80 -0
- package/src/seat-dispatch/seat-ask.ts +79 -16
- package/src/seat-loop/seat-loop.ts +12 -6
- package/src/self-dev/ask-launch-flow.ts +24 -3
- package/src/self-dev/ask-path-candidates.ts +68 -0
- package/src/self-dev/dev-pipeline.ts +4 -0
- package/src/self-dev/draft-triage-rules.ts +1 -1
- package/src/self-dev/orchestrate-cli.ts +22 -0
- package/src/self-dev/round-pr-supersede.ts +244 -0
- package/src/self-dev/run-supervisor.ts +14 -0
- package/src/self-implement/follow-up-goals.ts +2 -2
- package/src/self-implement/gate-remote.ts +427 -0
- package/src/self-implement/goal-author.ts +8 -3
- package/src/self-implement/l8-merge-queue.ts +29 -5
- package/src/self-implement/orchestrator.ts +59 -12
- package/src/self-implement/prior-draft-findings.ts +4 -3
- package/src/skills/skill-feedback.ts +76 -0
- package/src/task-agent/actions.ts +39 -6
- package/src/task-agent/judge.ts +31 -5
- package/src/task-agent/mission.ts +423 -0
- package/src/task-agent/shadow.ts +90 -0
- package/src/task-agent/task-hand.ts +235 -0
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +10 -0
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +86 -18
- package/src/telegram-loops-command.ts +1 -1
- package/src/telegram.ts +5 -20
- package/src/user-config.ts +63 -3
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4614-21403aac730d4586.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6877-fe94e76a5841ee9b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8401-4ce1c42c8d89903f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8949-ecb4128a0858b911.js +0 -1
- package/apps/pwa/out/_next/static/chunks/899-f41be85e8a680651.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-6b8444914da74ca6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-7110f732f45bf944.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-9a41d65355fc25b9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-41afec0b255ed2bb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-ab93b240f687d69a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-de43a186e5ba5683.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-8b08cafad27759c9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-1ed1324b1dc36754.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-166745e51ac0c7b0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a04260b8ccc968d4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-1f50e3d4e13435f4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-a9117d2cd3093d36.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-5a64cb5208c6a8fd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-1407ec83ea64c1f9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-6e878ebbe1737d00.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-81a4e7f8dbfe74cb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-3ab59d19913b200e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-bf85b2f5bf077925.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-ab32a8f522a60371.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-fc2df261f8a2c7df.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-09461366af82ef13.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ea90a8ac73ce2749.js +0 -1
- package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +0 -1
- package/apps/pwa/out/_next/static/yRA5ur6DcPl0YHSRheH2O/_buildManifest.js +0 -1
- /package/apps/pwa/out/_next/static/{yRA5ur6DcPl0YHSRheH2O → P6YTVgzuZz5oTZhY4Din_}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TASK-AGENT 과제 넘기기 문 — `elanous task hand "<한 줄>"` (RFC-task-agent-any-task-to-completion §A9 표 1행 · R3).
|
|
3
|
+
*
|
|
4
|
+
* - 과제 카드를 실행부와 «같은» 상태 파일(`effectiveInstanceRoot()/task-agent-actions.json`)의 `tasks` 칸에 적는다.
|
|
5
|
+
* 종전 칸(landingDay·landingsToday·failureCounts)은 그대로 두고 덧붙이기만 한다(하위 호환 — 실행부 쓰기는 `...disk` 로 보존).
|
|
6
|
+
* - 첫 수는 «launch» = `harness say [--seat <자리>] --substrate pod --merge-by-host "<원문>"`.
|
|
7
|
+
* - 기본은 SHADOW: 카드와 명령만 남기고 아무것도 띄우지 않는다. `--live` 일 때만 주입된 launcher 로 띄운다.
|
|
8
|
+
* - 관측: `debug.log('task-agent', 'handed', {taskId, seat, checklistId, mode})`.
|
|
9
|
+
*/
|
|
10
|
+
import { randomBytes } from 'node:crypto';
|
|
11
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { dirname, join } from 'node:path';
|
|
13
|
+
import { debug } from '../debug/log.js';
|
|
14
|
+
import { effectiveInstanceRoot } from '../instance/resolve.js';
|
|
15
|
+
import { withFileLockSync } from '../storage/file-lock.js';
|
|
16
|
+
|
|
17
|
+
export const TASK_SEATS = ['OP', 'TC', 'MK', 'UX'] as const;
|
|
18
|
+
export type TaskSeat = typeof TASK_SEATS[number];
|
|
19
|
+
/** 과제 카드 id 접두 — `task show <id>` 가 이 접두로 TOX 태스크와 갈린다. */
|
|
20
|
+
export const TASK_CARD_PREFIX = 'ta-';
|
|
21
|
+
|
|
22
|
+
export interface TaskCardEvent { at: string; event: string; detail?: string }
|
|
23
|
+
export interface TaskCardMove { kind: 'launch' | 'wait'; command?: string[]; reason: string }
|
|
24
|
+
export interface TaskCard {
|
|
25
|
+
id: string;
|
|
26
|
+
text: string;
|
|
27
|
+
seat?: TaskSeat;
|
|
28
|
+
checklistId?: string;
|
|
29
|
+
createdAt: string;
|
|
30
|
+
/** launch-failed = 발사기가 띄우기에 실패했다(사유는 history 마지막 칸) — 다음 수는 다시 발사. */
|
|
31
|
+
status: 'handed' | 'launched' | 'launch-failed';
|
|
32
|
+
history: TaskCardEvent[];
|
|
33
|
+
/** 미션 카드만 — 조각 카드 id(순서대로) · 쪼갠 출처(`mission.ts` splitMission). */
|
|
34
|
+
pieces?: string[];
|
|
35
|
+
splitSource?: string;
|
|
36
|
+
/** 조각 카드만 — 소속 미션 카드 id · 먼저 착지해야 하는 조각 id(선후). */
|
|
37
|
+
mission?: string;
|
|
38
|
+
after?: string[];
|
|
39
|
+
/** 조각 카드만 — 착지 근거(② `tasks advance --pr|--goal`): 이 조각 런이 연 PR 번호 · 골 id(16자 · 런 원장 `pr-opened` 로 PR 을 찾는다). */
|
|
40
|
+
pr?: number;
|
|
41
|
+
goalId?: string;
|
|
42
|
+
/** 조각 카드만 — ② 가 선행 착지 뒤 넘겼다(한 번만 · 같은 잠금 안에서 먼저 적는다). */
|
|
43
|
+
handed?: { at: string; mode: 'shadow' | 'live'; claim?: string; error?: string };
|
|
44
|
+
/** 미션 카드만 — 모든 조각 착지 → 칸 green «제안»(체크리스트는 손대지 않는다 · OP/주인이 뒤집는다). */
|
|
45
|
+
greenProposal?: { at: string; checklistId: string | null; evidence: Record<string, string> };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface TaskAgentStateFile { tasks?: Record<string, TaskCard>; [key: string]: unknown }
|
|
49
|
+
|
|
50
|
+
export function taskAgentStatePath(): string {
|
|
51
|
+
return join(effectiveInstanceRoot(), 'task-agent-actions.json');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 상태 파일을 읽을 수 없거나(ENOENT 제외) 해석할 수 없다 — 덮어쓰기 전에 멈춘다. */
|
|
55
|
+
export class TaskAgentStateError extends Error {
|
|
56
|
+
constructor(path: string, cause: unknown) {
|
|
57
|
+
super(`task-agent 상태 파일을 읽을 수 없다 — 덮어쓰지 않고 멈춘다: ${path} (${cause instanceof Error ? cause.message : String(cause)})`);
|
|
58
|
+
this.name = 'TaskAgentStateError';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 실행부·과제 넘기기 문이 함께 쓰는 상태 파일(`task-agent-actions.json`)의 엄격한 읽기.
|
|
64
|
+
* 파일이 «없음»(ENOENT)만 빈 상태다. 그 밖의 읽기·해석 오류(손상된 JSON · 객체 아님 · 권한)는
|
|
65
|
+
* 던진다 — 빈 상태로 읽고 다시 쓰면 failureCounts·착지 기록·과제 카드가 지워진다.
|
|
66
|
+
*/
|
|
67
|
+
export function readTaskAgentState<T extends object = TaskAgentStateFile>(path: string): T {
|
|
68
|
+
let raw: string;
|
|
69
|
+
try { raw = readFileSync(path, 'utf8'); } catch (error) {
|
|
70
|
+
if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') return {} as T;
|
|
71
|
+
return stateError(path, error);
|
|
72
|
+
}
|
|
73
|
+
let parsed: unknown;
|
|
74
|
+
try { parsed = JSON.parse(raw); } catch (error) { return stateError(path, error); }
|
|
75
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return stateError(path, new Error('최상위가 객체가 아니다'));
|
|
76
|
+
return parsed as T;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function stateError(path: string, cause: unknown): never {
|
|
80
|
+
const error = new TaskAgentStateError(path, cause);
|
|
81
|
+
try { debug.log('task-agent', 'state-unreadable', { path, reason: error.message }); } catch { /* fail-soft */ }
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function readState(path: string): TaskAgentStateFile {
|
|
86
|
+
return readTaskAgentState<TaskAgentStateFile>(path);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 카드 여럿을 한 잠금 안에서 적는다(미션 ⊕ 조각) — 종전 칸은 `...disk` 로 보존. */
|
|
90
|
+
export function writeTaskCards(path: string, cards: readonly TaskCard[]): void {
|
|
91
|
+
withFileLockSync(`${path}.lock`, () => {
|
|
92
|
+
const disk = readState(path);
|
|
93
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
94
|
+
const tasks = { ...(disk.tasks ?? {}) };
|
|
95
|
+
for (const card of cards) tasks[card.id] = card;
|
|
96
|
+
writeFileSync(path, JSON.stringify({ ...disk, tasks }));
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** 카드 하나를 잠금 안에서 읽고 고쳐 쓴다 — `update` 가 undefined 를 돌려주면 쓰지 않는다. 쓴(또는 그대로인) 카드를 돌려준다. */
|
|
101
|
+
export function updateTaskCard(path: string, id: string, update: (card: TaskCard | undefined, tasks: Readonly<Record<string, TaskCard>>) => TaskCard | undefined): TaskCard | undefined {
|
|
102
|
+
return withFileLockSync(`${path}.lock`, () => {
|
|
103
|
+
const disk = readState(path);
|
|
104
|
+
const current = disk.tasks?.[id];
|
|
105
|
+
const next = update(current, disk.tasks ?? {});
|
|
106
|
+
if (!next) return current;
|
|
107
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
108
|
+
writeFileSync(path, JSON.stringify({ ...disk, tasks: { ...(disk.tasks ?? {}), [id]: next } }));
|
|
109
|
+
return next;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function readTaskCard(id: string, path = taskAgentStatePath()): TaskCard | undefined {
|
|
114
|
+
return readState(path).tasks?.[id];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** 첫 수의 elanous 인자(진입점·--config-dir 앞붙임은 launcher 몫). */
|
|
118
|
+
export function launchArgs(card: Pick<TaskCard, 'text' | 'seat'>): string[] {
|
|
119
|
+
return ['harness', 'say', ...(card.seat ? ['--seat', card.seat] : []), '--substrate', 'pod', '--merge-by-host', card.text];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* `landed` = 착지한 조각 id(② 감지가 채운다). 안 주면 «모른다» — 선행은 미착지로 보고(fail-closed)
|
|
124
|
+
* 이유에 «착지 미확인»을 단다(착지했다/안 했다를 단정하지 않는다).
|
|
125
|
+
* 미션 카드는 스스로 발사하지 않는다 · 아직 안 뜬 조각의 선행이 덜 착지했으면 `wait after <id>`.
|
|
126
|
+
*/
|
|
127
|
+
export function nextMoveFor(card: TaskCard, landed?: ReadonlySet<string>): TaskCardMove {
|
|
128
|
+
if (card.pieces) return { kind: 'wait', reason: `미션 — 조각 ${card.pieces.length}장이 선후대로 발사된다` };
|
|
129
|
+
const unmet = (card.after ?? []).filter((id) => !landed?.has(id));
|
|
130
|
+
if (card.status !== 'launched' && unmet.length > 0) return { kind: 'wait', reason: `after ${unmet.join(', ')}${landed ? '' : ' · 착지 미확인'}` };
|
|
131
|
+
if (card.status === 'handed') return { kind: 'launch', command: launchArgs(card), reason: '넘겨받은 과제 — 첫 발사' };
|
|
132
|
+
if (card.status === 'launch-failed') return { kind: 'launch', command: launchArgs(card), reason: '직전 발사 실패 — 다시 발사' };
|
|
133
|
+
return { kind: 'wait', reason: '발사됨 — 런 멈춤을 슈퍼바이저 그림자 판단(task-agent.shadow-move)이 받는다' };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function shellQuote(arg: string): string {
|
|
137
|
+
return /^[A-Za-z0-9_./:=@%+-]+$/.test(arg) ? arg : `'${arg.replaceAll("'", `'\\''`)}'`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export type TaskLauncher = (args: string[]) => void | Promise<void>;
|
|
141
|
+
|
|
142
|
+
export interface HandTaskOptions {
|
|
143
|
+
text: string;
|
|
144
|
+
seat?: TaskSeat;
|
|
145
|
+
checklistId?: string;
|
|
146
|
+
live?: boolean;
|
|
147
|
+
statePath?: string;
|
|
148
|
+
/** `--live` 일 때만 불린다. */
|
|
149
|
+
launcher?: TaskLauncher;
|
|
150
|
+
now?: () => Date;
|
|
151
|
+
id?: string;
|
|
152
|
+
/** 미션 조각으로 넘길 때 — 카드에 소속 미션·선후를 싣는다. */
|
|
153
|
+
mission?: string;
|
|
154
|
+
after?: string[];
|
|
155
|
+
/** 착지한 조각 id — `after` 가 모두 여기 있어야 발사한다(② 의 자리). */
|
|
156
|
+
landed?: ReadonlySet<string>;
|
|
157
|
+
/** 카드에 그대로 싣는 칸(② 의 넘김 표지·착지 근거) — handTask 가 카드를 새로 적을 때 지워지지 않게. */
|
|
158
|
+
cardFields?: Pick<TaskCard, 'pr' | 'goalId' | 'handed'>;
|
|
159
|
+
/**
|
|
160
|
+
* 있으면 카드 쓰기마다 잠금 안에서 디스크 카드를 보고 참일 때만 쓴다(② 의 claim 소유 확인).
|
|
161
|
+
* 거짓이면 쓰지 않고 `TaskCardSupersededError` — 다른 넘김이 그 카드를 가져갔다(발사 전이면 발사하지 않는다).
|
|
162
|
+
*/
|
|
163
|
+
writeGuard?: (current: TaskCard | undefined) => boolean;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** `writeGuard` 가 거짓 — 다른 넘김이 카드를 가져갔다. 카드는 건드리지 않았다. */
|
|
167
|
+
export class TaskCardSupersededError extends Error {
|
|
168
|
+
constructor(id: string) {
|
|
169
|
+
super(`과제 카드를 다른 넘김이 가져갔다 — 쓰지 않았다: ${id}`);
|
|
170
|
+
this.name = 'TaskCardSupersededError';
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** `--live` 발사가 실패했다 — 카드는 launch-failed 로 남았다. */
|
|
175
|
+
export class TaskLaunchError extends Error {
|
|
176
|
+
constructor(card: TaskCard, reason: string) {
|
|
177
|
+
super(`발사 실패 (${card.id} → launch-failed): ${reason}`);
|
|
178
|
+
this.name = 'TaskLaunchError';
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface HandTaskResult { card: TaskCard; move: TaskCardMove; mode: 'shadow' | 'live'; launched: boolean }
|
|
183
|
+
|
|
184
|
+
export async function handTask(opts: HandTaskOptions): Promise<HandTaskResult> {
|
|
185
|
+
const text = opts.text.trim();
|
|
186
|
+
if (!text) throw new Error('과제 한 줄이 비어 있다');
|
|
187
|
+
if (opts.seat !== undefined && !TASK_SEATS.includes(opts.seat)) throw new Error(`자리는 ${TASK_SEATS.join('|')} 중 하나다: ${opts.seat}`);
|
|
188
|
+
const now = (opts.now ?? (() => new Date()))();
|
|
189
|
+
const id = opts.id ?? `${TASK_CARD_PREFIX}${now.toISOString().slice(0, 10).replaceAll('-', '')}-${randomBytes(3).toString('hex')}`;
|
|
190
|
+
const mode = opts.live ? 'live' : 'shadow';
|
|
191
|
+
const path = opts.statePath ?? taskAgentStatePath();
|
|
192
|
+
const card: TaskCard = {
|
|
193
|
+
id, text,
|
|
194
|
+
...(opts.seat ? { seat: opts.seat } : {}),
|
|
195
|
+
...(opts.checklistId ? { checklistId: opts.checklistId } : {}),
|
|
196
|
+
createdAt: now.toISOString(),
|
|
197
|
+
status: 'handed',
|
|
198
|
+
history: [],
|
|
199
|
+
...(opts.mission ? { mission: opts.mission, after: [...(opts.after ?? [])] } : {}),
|
|
200
|
+
...(opts.cardFields ?? {}),
|
|
201
|
+
};
|
|
202
|
+
const move = nextMoveFor(card, opts.landed);
|
|
203
|
+
if (move.kind !== 'launch') throw new Error(`발사할 수 없는 과제다 — ${move.reason}`);
|
|
204
|
+
const guard = opts.writeGuard;
|
|
205
|
+
const writeCard = (target: string, next: TaskCard): void => {
|
|
206
|
+
if (!guard) { writeTaskCards(target, [next]); return; }
|
|
207
|
+
const owned = { ok: false };
|
|
208
|
+
// 착지 근거(pr·goalId)는 `recordPieceRef` 소유 — claim 뒤에 적힌 최신 근거를 디스크에서 이어 받는다.
|
|
209
|
+
updateTaskCard(target, next.id, (current) => {
|
|
210
|
+
if (!guard(current)) return undefined;
|
|
211
|
+
owned.ok = true;
|
|
212
|
+
// next 의 옛 근거는 버리고 디스크의 근거로 «교체»한다 — 정정된 근거 옆에 옛 근거가 되살아나지 않게.
|
|
213
|
+
const { pr: _stalePr, goalId: _staleGoal, ...base } = next;
|
|
214
|
+
return { ...base, ...(current?.pr !== undefined ? { pr: current.pr } : {}), ...(current?.goalId !== undefined ? { goalId: current.goalId } : {}) };
|
|
215
|
+
});
|
|
216
|
+
if (!owned.ok) throw new TaskCardSupersededError(next.id);
|
|
217
|
+
};
|
|
218
|
+
writeCard(path, card);
|
|
219
|
+
try { debug.log('task-agent', 'handed', { taskId: id, seat: card.seat ?? null, checklistId: card.checklistId ?? null, mode }); } catch { /* fail-soft */ }
|
|
220
|
+
if (mode === 'shadow') return { card, move, mode, launched: false };
|
|
221
|
+
if (!opts.launcher) throw new Error('--live 인데 launcher 가 없다');
|
|
222
|
+
try {
|
|
223
|
+
await opts.launcher(move.command!);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
226
|
+
const failed: TaskCard = { ...card, status: 'launch-failed', history: [{ at: (opts.now ?? (() => new Date()))().toISOString(), event: 'launch-failed', detail: reason }] };
|
|
227
|
+
writeCard(path, failed);
|
|
228
|
+
try { debug.log('task-agent', 'launch-failed', { taskId: id, seat: card.seat ?? null, reason }); } catch { /* fail-soft */ }
|
|
229
|
+
throw new TaskLaunchError(failed, reason);
|
|
230
|
+
}
|
|
231
|
+
const launched: TaskCard = { ...card, status: 'launched', history: [{ at: (opts.now ?? (() => new Date()))().toISOString(), event: 'launch', detail: move.command!.join(' ') }] };
|
|
232
|
+
writeCard(path, launched);
|
|
233
|
+
try { debug.log('task-agent', 'launched', { taskId: id, seat: card.seat ?? null }); } catch { /* fail-soft */ }
|
|
234
|
+
return { card: launched, move, mode, launched: true };
|
|
235
|
+
}
|
|
@@ -33,6 +33,8 @@ export function extractPodFailureReason(input: {
|
|
|
33
33
|
containerReason?: string | null;
|
|
34
34
|
jobReason?: string;
|
|
35
35
|
deadlineSeconds?: number;
|
|
36
|
+
/** POD-NORESULT: the stage and PR from the child's parsed result row (disposition) — used only when no log line gives a readable reason. */
|
|
37
|
+
result?: { stage?: string | null; prUrl?: string | null; prNumber?: number | null } | null;
|
|
36
38
|
}): string {
|
|
37
39
|
if (input.jobReason === 'DeadlineExceeded') {
|
|
38
40
|
return `DeadlineExceeded: Job 수명 상한 ${input.deadlineSeconds ?? 'unknown'}초 초과`;
|
|
@@ -72,6 +74,14 @@ export function extractPodFailureReason(input: {
|
|
|
72
74
|
const errorLine = [...meaningful].reverse().find((line) => line.terminal || isError(line.text));
|
|
73
75
|
const reason = errorLine?.text.replace(/\/home\/[^/\s]+\//gu, '~/').slice(0, 240);
|
|
74
76
|
if (reason) return reason;
|
|
77
|
+
// POD-NORESULT: an unconverged child (exit 2 → BackoffLimitExceeded under backoffLimit 0) often leaves a result row
|
|
78
|
+
// with a stage and PR but no error text — report that instead of «unreadable».
|
|
79
|
+
const stage = input.result?.stage?.trim();
|
|
80
|
+
if (stage) {
|
|
81
|
+
const prNumber = input.result?.prNumber;
|
|
82
|
+
const pr = Number.isSafeInteger(prNumber) && prNumber! > 0 ? `PR #${prNumber}` : input.result?.prUrl ? `PR ${input.result.prUrl}` : '';
|
|
83
|
+
return pr ? `수확 가능(${stage}) · ${pr}` : `수렴 못 함(단계 ${stage})`;
|
|
84
|
+
}
|
|
75
85
|
const why = input.logTailReason?.trim() || (input.logs.trim() ? '로그에 읽을 수 있는 오류 줄 없음' : '자식 로그 비어 있음');
|
|
76
86
|
return `사유 못 읽음: ${why.replace(/\/home\/[^/\s]+\//gu, '~/').slice(0, 240)}`;
|
|
77
87
|
}
|
|
@@ -33,6 +33,13 @@ export const POD_CONTROL_INBOX_DIR = '/tmp/elanous-control.inbox';
|
|
|
33
33
|
/** Newest pod log bytes returned when the export passes the 5MB artifact limit (headroom under it). */
|
|
34
34
|
export const POD_LOGS_KEEP_BYTES = 4_500_000;
|
|
35
35
|
|
|
36
|
+
/** Re-emit the child's `{kind:"self", ok, result}` line as one flat `{...result, ok}` line at the end of /tmp/si.out.
|
|
37
|
+
* POD-NORESULT ⓑ (10-07 · 28 failed rc=2 Jobs read back): every child printed its result line, but one
|
|
38
|
+
* (si-task-71f68d57f6b7, image 0.2.15) had stderr lines (`[graph] collect fail`) after it, so the old
|
|
39
|
+
* «last line only» check skipped the flatten and the host kept `ok:false` with no stage or PR number.
|
|
40
|
+
* Scan back to the newest child result line instead; still append exactly one line. */
|
|
41
|
+
const POD_RESULT_FLATTEN = `bun -e 'const fs=require("fs");const p="/tmp/si.out";const lines=fs.readFileSync(p,"utf8").trimEnd().split("\\n");for(let i=lines.length-1;i>=0;i--){const l=lines[i].trim();if(!l.startsWith("{"))continue;let o;try{o=JSON.parse(l)}catch{continue}if(!o||o.kind!=="self")continue;if(o.result&&typeof o.result==="object"&&!Array.isArray(o.result))fs.appendFileSync(p,"\\n"+JSON.stringify({...o.result,...(typeof o.ok==="boolean"?{ok:o.ok}:{})})+"\\n");break}'`;
|
|
42
|
+
|
|
36
43
|
export const POD_JOB_DEADLINE_SECONDS = 10_800;
|
|
37
44
|
|
|
38
45
|
/** 자식 컨테이너 «요청» — 🩸 2026-09-27: limits 만 두면 k8s 가 requests=limits(cpu 4)로 잡아, 32코어 노드에
|
|
@@ -147,6 +154,8 @@ export interface PodSpawnOptions {
|
|
|
147
154
|
credentials?: (account: string) => { elanousAuth: string; codexAuth: string; ghToken: string };
|
|
148
155
|
/** POD-TOKEN-PREREFRESH 시험 심 — «3시간 안 만료» 계정의 호스트 선갱신(기본 refreshCodexAccountHome). */
|
|
149
156
|
codexPrerefresh?: (account: string) => Promise<{ ok: true; beforeH: number | null; afterH: number | null } | { ok: false; kind: string; message: string }>;
|
|
157
|
+
/** PREREFRESH-LOCK 시험 심 — 계정별 갱신 잠금 파일 경로(기본 = 그 계정 codexHome 옆). */
|
|
158
|
+
codexPrerefreshLockPath?: (account: string) => string | null;
|
|
150
159
|
/** 시험 심 — 이 호스트가 본부 임대 보유자인가(기본 HQ 펜스 · kubectl 주입 시험이면 false). */
|
|
151
160
|
hqLeaseHolder?: () => boolean | Promise<boolean>;
|
|
152
161
|
grokCredentials?: () => { grokAuth?: string; grokApiKey?: string; ghToken: string };
|
|
@@ -186,17 +195,29 @@ export class PodCodexExpiringError extends Error {
|
|
|
186
195
|
}
|
|
187
196
|
}
|
|
188
197
|
|
|
189
|
-
export type PodCredentialPrerefreshOutcome =
|
|
198
|
+
export type PodCredentialPrerefreshOutcome =
|
|
199
|
+
| 'refreshed' | 'still-expiring' | 'refresh-failed' | 'no-lease'
|
|
200
|
+
// PREREFRESH-LOCK (10-06): a peer refreshed it while we waited for the lock · a failed refresh's one re-check.
|
|
201
|
+
| 'refresh-skipped-already-fresh' | 'refresh-failed-recheck-ok' | 'refresh-failed-recheck-failed'
|
|
202
|
+
// 갱신을 «시도하지 않았다» — 잠금 자리를 못 구했거나 잠금을 못 잡았다(갱신 실패와 가른다 · reason = no-lock-path|lock-timeout|lock-error).
|
|
203
|
+
| 'lock-unavailable';
|
|
190
204
|
|
|
191
205
|
/** POD-TOKEN-PREREFRESH (10-06) — «3시간 안 만료» 계정은 거부만 하지 말고 선갱신을 한 번 시도한다.
|
|
192
206
|
* 🩸 05:3x team · 09:47 third: 거부만 하고 다음 계정으로 안 넘어가 그 계정으로 뽑힌 Pod 런이 전부 pod-error.
|
|
193
207
|
* ⛔ 갱신은 본부(HQ) 임대 보유자 한 곳에서만 — refresh 토큰은 갱신마다 회전해 두 곳이 갱신하면 로그아웃된다.
|
|
208
|
+
* ⭐ PREREFRESH-LOCK (10-06 · #24449 사후 리뷰): 같은 호스트의 두 런이 같은 계정을 «동시에» 고르면 갱신이 둘이 돌아
|
|
209
|
+
* 뒤엣것이 회전된 refresh 토큰으로 실패하고 그 계정을 버렸다. ⇒ 계정별 파일 잠금 안에서 ⑴ 먼저 다시 읽어
|
|
210
|
+
* 이미 신선하면 갱신하지 않고 ⑵ 갱신이 실패해도 한 번 다시 읽어(동료가 갱신했을 수 있다) 신선하면 쓴다.
|
|
194
211
|
* null = 이 계정은 후보에서 뺀다(호출자가 다음 계정으로). 만료 아닌 다른 자격 오류는 그대로 던진다. */
|
|
195
212
|
export async function podCodexCredentialWithPrerefresh(account: string, deps: {
|
|
196
213
|
credentials: (account: string) => { elanousAuth: string; codexAuth: string; ghToken: string };
|
|
197
214
|
refresh: (account: string) => Promise<{ ok: true; beforeH: number | null; afterH: number | null } | { ok: false; kind: string; message: string }>;
|
|
198
215
|
isLeaseHolder: () => boolean | Promise<boolean>;
|
|
199
216
|
log?: (category: string, event: string, data: Record<string, unknown>) => void;
|
|
217
|
+
/** 계정별 갱신 잠금 파일(기본 = 그 계정 codexHome 옆 · `codexAccountRefreshLockPath`). null = 잠금 없음. */
|
|
218
|
+
lockPath?: (account: string) => string | null;
|
|
219
|
+
/** 잠금 대기 조정(시험 심) — 기본 CODEX_REFRESH_LOCK_OPTS. */
|
|
220
|
+
lockOpts?: { staleMs?: number; retryBusyMs?: number; maxTries?: number };
|
|
200
221
|
}): Promise<{ elanousAuth: string; codexAuth: string; ghToken: string } | null> {
|
|
201
222
|
const log = deps.log ?? ((category, event, data) => debug.log(category, event, data));
|
|
202
223
|
try { return deps.credentials(account); }
|
|
@@ -209,17 +230,56 @@ export async function podCodexCredentialWithPrerefresh(account: string, deps: {
|
|
|
209
230
|
let holder = false;
|
|
210
231
|
try { holder = await deps.isLeaseHolder(); } catch { holder = false; }
|
|
211
232
|
if (!holder) { observe('no-lease', null); return null; }
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
233
|
+
/** One re-read: fresh credential, null when still expiring. Other credential errors throw. */
|
|
234
|
+
const reread = (): { elanousAuth: string; codexAuth: string; ghToken: string } | { expiringH: number } => {
|
|
235
|
+
try { return deps.credentials(account); }
|
|
236
|
+
catch (again) {
|
|
237
|
+
if (!(again instanceof PodCodexExpiringError)) throw again;
|
|
238
|
+
return { expiringH: again.hoursLeft };
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
const codex = await import('../../oauth/codex.js');
|
|
242
|
+
let lockPath: string | null = null;
|
|
243
|
+
let lockUnknown = false;
|
|
244
|
+
try { lockPath = (deps.lockPath ?? ((name: string) => codex.codexAccountRefreshLockPath(name)))(account); } catch { lockUnknown = true; }
|
|
245
|
+
// 이름 있는 계정인데 잠금 자리를 못 구하면 잠금 없이 갱신하지 않는다 — 막으려던 중복 갱신이 그 자리에서 재발한다.
|
|
246
|
+
// 갱신은 건너뛰고 한 번만 다시 읽는다(동료가 이미 갱신했으면 그대로 쓴다).
|
|
247
|
+
if (lockUnknown || (lockPath === null && account !== (await import('../../oauth/codex-account.js')).DEFAULT_CODEX_ACCOUNT)) {
|
|
248
|
+
const late = reread();
|
|
249
|
+
if (!('expiringH' in late)) { observe('refresh-skipped-already-fresh', null, 'no-lock-path'); return late; }
|
|
250
|
+
observe('lock-unavailable', null, 'no-lock-path');
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
const critical = async () => {
|
|
254
|
+
// ⑴ 잠금을 기다리는 사이 동료 런이 갱신했으면 그대로 쓴다 — 두 번째 갱신이 회전된 토큰으로 실패하는 자리.
|
|
255
|
+
const first = reread();
|
|
256
|
+
if (!('expiringH' in first)) { observe('refresh-skipped-already-fresh', null); return first; }
|
|
257
|
+
let refreshed: Awaited<ReturnType<typeof deps.refresh>> | null = null;
|
|
258
|
+
let failReason = 'threw';
|
|
259
|
+
try { refreshed = await deps.refresh(account); }
|
|
260
|
+
catch { refreshed = null; }
|
|
261
|
+
if (refreshed && !refreshed.ok) failReason = refreshed.kind;
|
|
262
|
+
if (!refreshed || !refreshed.ok) {
|
|
263
|
+
observe('refresh-failed', null, failReason);
|
|
264
|
+
// ⑵ 버리기 전에 한 번 다시 읽는다 — 다른 호스트 경로(사람 `account refresh`)가 방금 갱신했을 수 있다.
|
|
265
|
+
let recheck: ReturnType<typeof reread>;
|
|
266
|
+
try { recheck = reread(); } catch { observe('refresh-failed-recheck-failed', null, 'recheck-error'); return null; }
|
|
267
|
+
if (!('expiringH' in recheck)) { observe('refresh-failed-recheck-ok', null); return recheck; }
|
|
268
|
+
observe('refresh-failed-recheck-failed', recheck.expiringH);
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
const after = reread();
|
|
272
|
+
if ('expiringH' in after) { observe('still-expiring', after.expiringH); return null; }
|
|
218
273
|
observe('refreshed', refreshed.afterH);
|
|
219
|
-
return
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
|
|
274
|
+
return after;
|
|
275
|
+
};
|
|
276
|
+
try { return await codex.withCodexAccountRefreshLock(lockPath, critical, deps.lockOpts ?? codex.CODEX_REFRESH_LOCK_OPTS); }
|
|
277
|
+
catch (lockError) {
|
|
278
|
+
if (!(lockError instanceof codex.CodexRefreshLockUnavailableError)) throw lockError;
|
|
279
|
+
// 잠금을 못 잡았다(대기 초과 · 잠금 파일 생성 실패) — 갱신은 하지 않고 한 번만 다시 읽는다.
|
|
280
|
+
const late = reread();
|
|
281
|
+
if (!('expiringH' in late)) { observe('refresh-skipped-already-fresh', null, lockError.reason); return late; }
|
|
282
|
+
observe('lock-unavailable', null, lockError.reason);
|
|
223
283
|
return null;
|
|
224
284
|
}
|
|
225
285
|
}
|
|
@@ -688,11 +748,11 @@ export function podJobManifest(o: { name: string; namespace: string; image: stri
|
|
|
688
748
|
...(o.appCredential ? [podGithubWatchdogScript(o.githubStaleSeconds ?? POD_GH_STALE_SECONDS)] : []),
|
|
689
749
|
// 마지막 줄 JSON 이 «맨 끝»이어야 한다(parseSelfImplementJson) — rollup 은 그 앞에.
|
|
690
750
|
// ⛔ `--author-grade` 는 Pod 안 `elanous`(이미지에 깔린 판)가 모를 수 있어 넘기지 않는다 — 10-06 «unknown option» 즉사(#24445 되돌림).
|
|
691
|
-
//
|
|
751
|
+
// AUTHOR-LITE2-POD — 등급은 환경변수 `ELANOUS_AUTHOR_GRADE` 로 싣는다(옛 Pod elanous 는 모르는 환경변수를 무시하고 full 로 돈다).
|
|
692
752
|
o.authorSentence
|
|
693
|
-
? `echo "ELANOUS_AUTHOR_ON_POD started host=$(hostname) at=$(date -u +%Y-%m-%dT%H:%M:%SZ)"; author_t0=$(date +%s); elanous harness say --substrate local --json${quotedAsk} -- "$(cat /creds/feature)" > /tmp/si.out 2>&1; rc=$?; echo "ELANOUS_AUTHOR_ON_POD finished host=$(hostname) rc=$rc seconds=$(( $(date +%s) - author_t0 ))";
|
|
753
|
+
? `echo "ELANOUS_AUTHOR_ON_POD started host=$(hostname) at=$(date -u +%Y-%m-%dT%H:%M:%SZ)"; author_t0=$(date +%s); ${o.authorGrade === 'lite' || o.authorGrade === 'full' ? `ELANOUS_AUTHOR_GRADE=${o.authorGrade} ` : ''}elanous harness say --substrate local --json${quotedAsk} -- "$(cat /creds/feature)" > /tmp/si.out 2>&1; rc=$?; echo "ELANOUS_AUTHOR_ON_POD finished host=$(hostname) rc=$rc seconds=$(( $(date +%s) - author_t0 ))"; ${POD_RESULT_FLATTEN}`
|
|
694
754
|
: goalPath
|
|
695
|
-
? `elanous harness ask ${goalPath} --json${quotedAsk} > /tmp/si.out 2>&1; rc=$?;
|
|
755
|
+
? `elanous harness ask ${goalPath} --json${quotedAsk} > /tmp/si.out 2>&1; rc=$?; ${POD_RESULT_FLATTEN}`
|
|
696
756
|
: `export ${OLD_DOOR_STAMP_ENV}=self-implement; elanous self implement "$(cat /creds/feature)" --json ${quoted} > /tmp/si.out 2>&1; rc=$?`,
|
|
697
757
|
'cat /tmp/si.out',
|
|
698
758
|
'[ -f scripts/usage-rollup.ts ] && bun scripts/usage-rollup.ts --since 12h || echo "ELANOUS_USAGE_ROLLUP {\"measured\":false,\"reason\":\"no rollup script\"}"',
|
|
@@ -1054,6 +1114,9 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
|
|
|
1054
1114
|
credentials: accountCredentials,
|
|
1055
1115
|
refresh: options.codexPrerefresh ?? (async (name: string) => (await import('../../oauth/codex.js')).refreshCodexAccountHome(name)),
|
|
1056
1116
|
isLeaseHolder: options.hqLeaseHolder ?? (options.kubectl ? () => false : defaultHqLeaseHolder), // kubectl 주입(=시험)이면 본부 임대를 묻지 않는다
|
|
1117
|
+
// PREREFRESH-LOCK: 시험(kubectl 주입)은 정본 스토어를 읽어 잠금 경로를 풀지 않는다 — 임시 디렉터리의 계정별 잠금.
|
|
1118
|
+
// 잠금 자리는 주입 여부와 무관하게 계정 홈 옆 하나 — 시험은 codexPrerefreshLockPath 로 명시한다(같은 계정이 서로 다른 잠금을 쓰지 않게).
|
|
1119
|
+
...(options.codexPrerefreshLockPath ? { lockPath: options.codexPrerefreshLockPath } : {}),
|
|
1057
1120
|
};
|
|
1058
1121
|
const candidates = grok ? [] : plannedCodexAccounts ?? [plannedAccount];
|
|
1059
1122
|
const usable: Array<{ name: string; credential: ReturnType<typeof accountCredentials> }> = [];
|
|
@@ -1552,12 +1615,15 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
|
|
|
1552
1615
|
}
|
|
1553
1616
|
const releaseHold = regate.failures[0]?.step === 'release-path-hold';
|
|
1554
1617
|
const frozen = regate.status === 'frozen';
|
|
1618
|
+
// FREEZE-POD: the host is the only merger of a Pod run, so its freeze check here is the one that holds it.
|
|
1619
|
+
if (frozen) debug.log('self-implement.pod', 'merge-blocked-by-freeze', { job: name, runId: liveChildRunId, pr: prNumber ?? null, reason: 'landing-freeze' });
|
|
1555
1620
|
disposition = { ...disposition, stage: frozen || releaseHold ? 'pr-opened' : regate.passed ? 'merged' : 'host-regate-failed', merged: regate.passed && !frozen, hostRegate: regate, ok: regate.passed || releaseHold || frozen };
|
|
1556
1621
|
}
|
|
1557
1622
|
const childFailure = state === 'failed' && !oomKilled && failedReason !== 'DeadlineExceeded'
|
|
1558
1623
|
? lastPodChildFailure(logs) : null;
|
|
1559
1624
|
let noResultDiagnostic: string | null = null;
|
|
1560
|
-
|
|
1625
|
+
// POD-NORESULT: the Job runs with backoffLimit 0, so any non-zero child exit is «BackoffLimitExceeded» — not a reason to skip the ledger.
|
|
1626
|
+
if (state === 'failed' && containerExitCode !== null && containerExitCode !== 0 && !disposition && !oomKilled && failedReason !== 'DeadlineExceeded') {
|
|
1561
1627
|
try { noResultDiagnostic = podNoResultDiagnostic(readFileSync(runLedgerPath(liveChildRunId, ledgerDir), 'utf8')); }
|
|
1562
1628
|
catch { /* ledger not returned: do not invent a stage */ }
|
|
1563
1629
|
noResultDiagnostic ??= 'child terminal result missing; last ledger stage=unknown; round=unknown; mustFix=unknown';
|
|
@@ -1568,7 +1634,9 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
|
|
|
1568
1634
|
}
|
|
1569
1635
|
debug.log('self-implement.pod', 'job-finished', { job: name, ...(member ? { context: member.context } : {}), state, containerReason, stage: disposition?.stage ?? null, prUrl: disposition?.prUrl ?? null, childRunId: liveChildRunId, ledgerCompleteness,
|
|
1570
1636
|
...(state === 'failed' && !oomKilled && failedReason !== 'DeadlineExceeded'
|
|
1571
|
-
? { ...(childFailure ? { childStage: childFailure.stage } : {
|
|
1637
|
+
? { ...(childFailure ? { childStage: childFailure.stage } : disposition?.stage ? { childStage: disposition.stage } : {}),
|
|
1638
|
+
// POD-NORESULT: a parsed result row without error text is not a missing result line (UX 10-07 01:01: 28 of 28 exit-2 Jobs printed one).
|
|
1639
|
+
childError: childFailure?.error ?? (disposition?.stage ? 'result-without-error' : 'no-result-line') } : {}) },
|
|
1572
1640
|
state === 'failed' && !oomKilled && failedReason !== 'DeadlineExceeded' ? { compact: { stringMax: 500 } } : undefined);
|
|
1573
1641
|
if (oomKilled && reattach && !oomRetried && (!reattachedMemoryLimit || memoryGi(reattachedMemoryLimit) === null)) {
|
|
1574
1642
|
return { exitCode: 1, output: '', error: { code: 'pod-memory-unavailable', message: `Job ${name} 실제 메모리 한도를 확인할 수 없다` } };
|
|
@@ -1639,7 +1707,7 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
|
|
|
1639
1707
|
: disposition;
|
|
1640
1708
|
const podExitCode = state === 'complete' && disposition?.stage !== 'host-regate-failed' && disposition?.ok !== false ? 0 : 1;
|
|
1641
1709
|
// SCHED-CONTRACT: a failed child must say why outside the Pod, not «no error diagnostic».
|
|
1642
|
-
const podReason = podExitCode === 1 ? extractPodFailureReason({ logs, logTailReason, containerReason, jobReason: failedReason || undefined, deadlineSeconds }) : undefined;
|
|
1710
|
+
const podReason = podExitCode === 1 ? extractPodFailureReason({ logs, logTailReason, containerReason, jobReason: failedReason || undefined, deadlineSeconds, result: disposition ? { stage: disposition.stage, prUrl: disposition.prUrl ?? null, prNumber: disposition.prNumber ?? null } : null }) : undefined;
|
|
1643
1711
|
if (podReason) debug.log('self-implement.pod', 'failure-reason', { job: name, reason: podReason });
|
|
1644
1712
|
return {
|
|
1645
1713
|
exitCode: podExitCode,
|
|
@@ -1651,7 +1719,7 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
|
|
|
1651
1719
|
? { error: { code: 'pod-gh-login-failed', message: `Job ${name} GitHub 앱 로그인 실패 (container=${containerReason ?? 'unknown'}/7, attempt=${ghLoginRetried ? 2 : 1}) — 새 토큰으로 ${ghLoginRetried ? '1회 재시도했으나 다시 실패' : '재시도 불가'}` } }
|
|
1652
1720
|
: oomKilled
|
|
1653
1721
|
? { error: { code: 'pod-oom-killed', message: `Job ${name} failed (OOMKilled/${containerExitCode ?? 'unknown'}, memoryLimit=${memoryLimit})${oomRetried || (memoryGi(reattachedMemoryLimit ?? memoryLimit) ?? 0) >= (memoryGi(podMemoryLimitFor(input.feature, { ...env, ELANOUS_POD_MEMORY_TIER: 'high' }).limit) ?? Infinity) ? ` — OOM · ${oomRetried ? retryTier : 'high'} 에서도` : ''}\n${formatLastMemSample(samples.at(-1))}` } }
|
|
1654
|
-
: { error: { code: 'pod-job-failed', message: `Job ${name} failed${failedReason || containerReason ? ` (${[failedReason, containerReason ? `container=${containerReason}/${containerExitCode ?? 'unknown'}` : ''].filter(Boolean).join(', ')})` : ''}${childFailure ? ` — childStage=${childFailure.stage} · childError=${childFailure.error}` : ` — childError=no-result-line · reason=${podReason}`}${noResultDiagnostic ? ` · ${noResultDiagnostic}` : ''}` } }
|
|
1722
|
+
: { error: { code: 'pod-job-failed', message: `Job ${name} failed${failedReason || containerReason ? ` (${[failedReason, containerReason ? `container=${containerReason}/${containerExitCode ?? 'unknown'}` : ''].filter(Boolean).join(', ')})` : ''}${childFailure ? ` — childStage=${childFailure.stage} · childError=${childFailure.error}` : disposition?.stage ? ` — childStage=${disposition.stage} · reason=${podReason}` : ` — childError=no-result-line · reason=${podReason}`}${noResultDiagnostic ? ` · ${noResultDiagnostic}` : ''}` } }
|
|
1655
1723
|
: podReason && !childFailure?.error ? { error: { code: 'pod-child-failed', message: podReason } } : {}),
|
|
1656
1724
|
...(finishedDisposition ? { disposition: finishedDisposition } : {}),
|
|
1657
1725
|
};
|
|
@@ -2,7 +2,7 @@ import { dashboardLoops } from './domains/dashboard-data.js';
|
|
|
2
2
|
import { readSchedulesInventory } from './nexus/api/schedules-read.js';
|
|
3
3
|
import { loadLoopRows, LOOP_SCHEDULES_PATH, LOOPS_PATH } from './loops/status-rows.js';
|
|
4
4
|
|
|
5
|
-
interface LoopsSources {
|
|
5
|
+
export interface LoopsSources {
|
|
6
6
|
schedules: () => unknown;
|
|
7
7
|
loops: () => unknown;
|
|
8
8
|
now: () => number;
|
package/src/telegram.ts
CHANGED
|
@@ -82,6 +82,7 @@ import { attachTelegramProjectButtons } from './telegram-project-command.js';
|
|
|
82
82
|
import { attachTelegramFabricPlan } from './telegram-fabric-plan.js';
|
|
83
83
|
import { contextNow as readContextNow, type ContextNowDeps } from './context-bus/context-now.js';
|
|
84
84
|
import { renderTelegramNow } from './context-bus/context-now-surfaces.js';
|
|
85
|
+
import { createContextFirstGate, renderContextFirst } from './context-bus/context-first.js';
|
|
85
86
|
|
|
86
87
|
export type TgAttachmentKind = 'photo' | 'voice' | 'audio' | 'document';
|
|
87
88
|
|
|
@@ -269,9 +270,6 @@ const EMPTY_ALLOWLIST_NOTICE_GAP_MS = 10 * 60 * 1000;
|
|
|
269
270
|
// Tuned above a normal chat answer but below a real delegation (which
|
|
270
271
|
// pays a multi-second session load before any work).
|
|
271
272
|
const NOTIFY_AS_NEW_MSG_THRESHOLD_MS = 20_000;
|
|
272
|
-
/** 같은 대화에서 이 간격 안이면 context-now 요약을 다시 보내지 않는다. */
|
|
273
|
-
const CONTEXT_FIRST_GAP_MS = 6 * 60 * 60 * 1000;
|
|
274
|
-
const CONTEXT_UNREADABLE = '맥락 못 읽음';
|
|
275
273
|
|
|
276
274
|
interface ApiResponse<T> {
|
|
277
275
|
ok: boolean;
|
|
@@ -365,10 +363,7 @@ export class TelegramBot {
|
|
|
365
363
|
private readonly handleFabricPlan: (ctx: TgIncoming) => Promise<boolean>;
|
|
366
364
|
private readonly readContextNow: typeof readContextNow;
|
|
367
365
|
private readonly contextNowDeps?: ContextNowDeps;
|
|
368
|
-
|
|
369
|
-
private readonly lastUtteranceAt = new Map<string, number>();
|
|
370
|
-
/** 첫 말이거나 6시간 이상 쉰 뒤라 요약이 «밀려 있는» 대화 키 — 다음 일반 답이 한 번 소비한다. */
|
|
371
|
-
private readonly contextFirstPending = new Set<string>();
|
|
366
|
+
private readonly contextFirstGate = createContextFirstGate({ now: () => this.nowImpl() });
|
|
372
367
|
private seatAskTimer: ReturnType<typeof setInterval> | null = null;
|
|
373
368
|
/** 앨범 id → `#현장` 행사 · 답장 디바운스. 태그된 앨범만 들어온다. */
|
|
374
369
|
private readonly fieldGroups = new Map<string, {
|
|
@@ -432,27 +427,17 @@ export class TelegramBot {
|
|
|
432
427
|
|
|
433
428
|
/** 허용된 모든 발화(명령 포함)마다 부른다 — 쉼은 마지막 발화부터 재고, 요약 전송과는 따로 기록한다. */
|
|
434
429
|
private noteUtterance(ctx: TgIncoming): void {
|
|
435
|
-
|
|
436
|
-
const now = this.nowImpl();
|
|
437
|
-
const previous = this.lastUtteranceAt.get(key);
|
|
438
|
-
this.lastUtteranceAt.set(key, now);
|
|
439
|
-
if (previous === undefined || now - previous >= CONTEXT_FIRST_GAP_MS) this.contextFirstPending.add(key);
|
|
430
|
+
this.contextFirstGate.note(`${ctx.chatId}:${ctx.threadId ?? ''}`);
|
|
440
431
|
}
|
|
441
432
|
|
|
442
433
|
/** 처음이거나 6시간 이상 쉰 대화의 첫 일반 답 앞에 context-now 요약 한 메시지를 보낸다. 실패해도 본 답은 막지 않는다. */
|
|
443
434
|
private async maybeSendContextFirst(ctx: TgIncoming): Promise<void> {
|
|
444
435
|
const key = `${ctx.chatId}:${ctx.threadId ?? ''}`;
|
|
445
|
-
if (!this.
|
|
436
|
+
if (!this.contextFirstGate.take(key)) return;
|
|
446
437
|
const cfg = this.slashContext?.userConfig.telegram;
|
|
447
438
|
if (cfg?.contextFirst === false) return;
|
|
448
|
-
let text = CONTEXT_UNREADABLE;
|
|
449
|
-
try {
|
|
450
|
-
text = renderTelegramNow(this.readContextNow({}, this.contextNowDeps));
|
|
451
|
-
} catch {
|
|
452
|
-
text = CONTEXT_UNREADABLE;
|
|
453
|
-
}
|
|
454
439
|
try {
|
|
455
|
-
await this.sendMessage(ctx.chatId,
|
|
440
|
+
await this.sendMessage(ctx.chatId, renderContextFirst(this.readContextNow, this.contextNowDeps, renderTelegramNow), { replyTo: ctx.messageId, threadId: ctx.threadId });
|
|
456
441
|
} catch (err) {
|
|
457
442
|
this.log(`telegram context-first failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
458
443
|
}
|