elanous 0.2.5 → 0.2.7
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/.env.example +10 -7
- package/README.md +9 -4
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1377-1efeffbbac0963ff.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1470.98a6ab61ccb4131c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{5391-8793f88b03b30161.js → 2375-cb6debea56cce4fb.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{1751.50b96ea9c37b49b6.js → 2415.ed6428ff334c7ce2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2977-e38442e86289f1d3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2982-2a2df543be3d2e5a.js +25 -0
- package/apps/pwa/out/_next/static/chunks/{4daa2798-1363d725c79f3593.js → 2ea96958-e8c7e112de50b067.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +5 -0
- package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +1 -0
- package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8617.465ca22a319c95b3.js → 4323.c5dfb6f503304775.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/4836-2eba6079f31b88d4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8918-482b30ce2a8ebf4c.js → 5080-1613752891fcadb4.js} +3 -3
- package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{5348-bbf838e119661eee.js → 5791-bb82743fa6299562.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{9704-3221c102623c8d73.js → 6062-73e68343a9814f6a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{4440-18bb160d95819f4e.js → 6090-85a3f05f954e5697.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/6284-69db415193d5f8bd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6320-baac91d68843a9a6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7647.2fcd30d894d5ee30.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{f6f3850d-7f324c6566002113.js → 8046cadf-211ca3c0e00f2b72.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/8067-f74bf14d67ad98f1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8519-f6b5d8a5833a7603.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8894-e92491a164192cc1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9454-a3e0953307216c58.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9651-3cd291a6fa1370d2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9740-67051141115ce775.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9804-64c02c3c127d5014.js +1 -0
- package/apps/pwa/out/_next/static/chunks/ae787e4c-f2998de7f1ba72a4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-d5ebde04844689e6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-f89e9a303515a033.js → page-10ed24f413ed66e9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/{page-4f403cd060442c95.js → page-211810b6c93ab66a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-0ac5d053f0444d21.js → page-624f7a2f89b60c8f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-5a5fb385a4df0ffe.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-80497e93a878ddc5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-7457f52683562161.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-cee86394de407aa6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-4ff8edab44196b8c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-92e4f44428d5e290.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/page-b7dcf5d48a854b76.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/page-51480757e7d68b92.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/page-2a05dfe80c8bdf16.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-d9979633aa82f8e6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-30533117beb53625.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-f7857826c2f97ad7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-07267f1391b96aba.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-bccce0babceae30f.js → page-55d50e9b0006a91d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-9699b1786be6171d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-43dd772aee8af75f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-a7e37af7d0399cb8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-973ab7af59c718b7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a9f8d3865b91e436.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-15e5a15db84c56aa.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-a2e572e4511f2a16.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-dd107bf1c7d52a90.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-d42b99cdfb9f27c6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-2764ff40dad5a995.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-c5f1152db116ff96.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-f933bca81ea38a6f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-c1c95739c0bff3b8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-a7aaffcf771662d8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-71d18eec33b9c667.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-56adbe1a1f1379bd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-2c4e59079ee357ac.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-a81f113f3ef89d72.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-5d422f38c052967e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-fc27b00e20733146.js +1 -0
- package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-app-6870f30ea99f8346.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{webpack-2bf59c720fe9542c.js → webpack-e5bea36f183b25e6.js} +1 -1
- package/apps/pwa/out/_next/static/css/f537bdb19c69306a.css +1 -0
- package/apps/pwa/out/_next/static/keajW7DDyghEs1y8oJe6T/_buildManifest.js +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +22 -22
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +22 -22
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +22 -22
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +21 -21
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +21 -21
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +21 -21
- package/apps/pwa/out/consult/index.html +1 -0
- package/apps/pwa/out/consult/index.txt +32 -0
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +22 -22
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +22 -22
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +22 -22
- package/apps/pwa/out/exec/index.html +1 -0
- package/apps/pwa/out/exec/index.txt +32 -0
- package/apps/pwa/out/field/index.html +1 -0
- package/apps/pwa/out/field/index.txt +32 -0
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/icons/elanous-16.png +0 -0
- package/apps/pwa/out/icons/elanous-180.png +0 -0
- package/apps/pwa/out/icons/elanous-192.png +0 -0
- package/apps/pwa/out/icons/elanous-32.png +0 -0
- package/apps/pwa/out/icons/elanous-512.png +0 -0
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +21 -21
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +22 -22
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +22 -22
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +21 -21
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +21 -21
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +22 -22
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +22 -22
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +22 -22
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +22 -22
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +21 -21
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +22 -22
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +22 -22
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +22 -22
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +22 -22
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +22 -22
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +21 -21
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +22 -22
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +22 -22
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +22 -22
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +22 -22
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +22 -22
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +22 -22
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +22 -22
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +22 -22
- package/bin/elanous.cjs +107 -0
- package/graphs/field/field-feed.yaml +24 -0
- package/graphs/field/recipes.yaml +12 -0
- package/graphs/landing/landing-heal.yaml +3 -1
- package/graphs/landing/recipes.yaml +3 -0
- package/graphs/release/recipes.yaml +13 -0
- package/graphs/release/release-loop.yaml +13 -4
- package/package.json +5 -5
- package/plugins/card-followup/README.md +42 -0
- package/plugins/card-followup/examples/card-sample.json +7 -0
- package/plugins/card-followup/graphs/card-followup.yaml +18 -0
- package/plugins/card-followup/graphs/recipes.yaml +16 -0
- package/plugins/card-followup/graphs/run-step.ts +276 -0
- package/plugins/card-followup/graphs/sales-strategy.ts +173 -0
- package/plugins/card-followup/plugin.json +25 -0
- package/plugins/doc-draft/README.md +13 -0
- package/plugins/doc-draft/examples/brief-sample.json +8 -0
- package/plugins/doc-draft/graphs/doc-draft.yaml +14 -0
- package/plugins/doc-draft/graphs/recipes.yaml +9 -0
- package/plugins/doc-draft/graphs/run-step.ts +113 -0
- package/plugins/doc-draft/plugin.json +24 -0
- package/plugins/geo-check/README.md +15 -0
- package/plugins/geo-check/examples/brand-sample.json +8 -0
- package/plugins/geo-check/graphs/geo-check.yaml +20 -0
- package/plugins/geo-check/graphs/recipes.yaml +18 -0
- package/plugins/geo-check/graphs/run-step.ts +198 -0
- package/plugins/geo-check/plugin.json +24 -0
- package/plugins/job-coach/.codex-plugin/plugin.json +1 -1
- package/plugins/job-coach/README.md +31 -1
- package/plugins/job-coach/graphs/run-step.ts +2 -2
- package/plugins/job-coach/plugin.json +1 -1
- package/plugins/job-coach/plugin.ts +1 -1
- package/scripts/affinity/endcard.sh +3 -2
- package/scripts/affinity/reframe.sh +3 -1
- package/scripts/ci-public-export-leak-gate.ts +48 -0
- package/scripts/codex-quota-alert.ts +122 -258
- package/scripts/coord-tracks.json +4 -4
- package/scripts/daemon-port-baseline.txt +1 -2
- package/scripts/field-feed/deliver-node.ts +52 -0
- package/scripts/field-feed/feed-node.ts +67 -0
- package/scripts/field-feed/lib.ts +162 -0
- package/scripts/field-feed/reel-node.ts +24 -0
- package/scripts/field-feed/start.ts +62 -0
- package/scripts/fixtures/vflow-sample.ndjson +6 -0
- package/scripts/install.sh +92 -13
- package/scripts/landing-heal/clean-checkout.ts +114 -0
- package/scripts/landing-heal/run-mission.ts +38 -3
- package/scripts/lib/android-test-device.ts +32 -0
- package/scripts/lib/pwa-scenarios/scenarios.ts +2 -1
- package/scripts/public-export.ts +8 -4
- package/scripts/release-loop/baseline-from-logs.ts +92 -0
- package/scripts/release-loop/checklist-gate-node.ts +35 -0
- package/scripts/release-loop/env-known-failures.ts +31 -0
- package/scripts/release-loop/gate-diff.ts +29 -0
- package/scripts/release-loop/gate-node.ts +124 -29
- package/scripts/release-loop/known-issues-node.ts +159 -0
- package/scripts/release-loop/npm-publish-node.ts +118 -0
- package/scripts/release-loop/ops-upgrade-node.ts +87 -0
- package/scripts/release-loop/release-readiness.ts +58 -0
- package/scripts/release-loop/shard-plan.ts +47 -0
- package/scripts/release-loop/unattended-release.ts +93 -0
- package/scripts/ux/measure-first-text.ts +102 -0
- package/scripts/ux/record-page.ts +4 -1
- package/scripts/vflow-collect.ts +20 -7
- package/scripts/vflow-search.ts +73 -0
- package/scripts/video-free-line-test-walker.ts +36 -0
- package/scripts/video-pipeline.ts +2 -2
- package/skills/explainer-video/SKILL.md +24 -3
- package/skills/explainer-video/engine/brand/elanous-mark-on-dark.svg +1 -1
- package/skills/explainer-video/engine/build.mjs +2 -2
- package/skills/explainer-video/engine/reel.mjs +235 -0
- package/skills/explainer-video/engine/reel.sh +9 -0
- package/skills/explainer-video/engine/vo.ts +58 -16
- package/skills/explainer-video/examples/elanous-0.2.5/script.json +1 -1
- package/skills/grill-me/SKILL.md +1 -1
- package/skills/omni-digest/docs/diagrams/noncode-terminal-promotion-path-2026-08-13.mmd +1 -1
- package/src/a2a/server.ts +2 -0
- package/src/acp/elanous-aware-peers.ts +30 -0
- package/src/acp/server.ts +91 -15
- package/src/agent/monad-agent-turn.ts +25 -6
- package/src/agent/shared-app-tools.ts +5 -3
- package/src/agent-substrate/review-intent.ts +6 -2
- package/src/agent-substrate/shard-boundary-intent.ts +70 -0
- package/src/auth/github-app-token.ts +4 -1
- package/src/autopilot/mission-origin.ts +20 -4
- package/src/autopilot/mission-phase-track.ts +11 -1
- package/src/autopilot/track-agent-shadow.ts +151 -0
- package/src/autopilot/track-agent.ts +26 -6
- package/src/autopilot/track-courier.ts +2 -2
- package/src/boot/acp-server-help.ts +16 -0
- package/src/boot/daemon-prompt-turn.ts +31 -10
- package/src/boot/daemon-tools/index.ts +7 -1
- package/src/budget/codex-credit-pace.ts +84 -0
- package/src/budget/codex-credit-plan.ts +154 -0
- package/src/budget/codex-reset-credit-state.ts +2 -0
- package/src/chat/chat-cli.ts +4 -1
- package/src/chat/short-question.ts +40 -0
- package/src/cli/agent-cli.ts +24 -15
- package/src/cli/ask-bare.ts +42 -0
- package/src/cli/cli-user-error.ts +25 -0
- package/src/cli/codex-credit-plan-view.ts +25 -0
- package/src/cli/dependence-gauge.ts +237 -0
- package/src/cli/doctor-cli.ts +5 -0
- package/src/cli/doctor-modes.ts +48 -0
- package/src/cli/intake-cli.ts +12 -2
- package/src/cli/intake-source-cli.ts +58 -0
- package/src/cli/machine-cli.ts +23 -0
- package/src/cli/msg-cli.ts +137 -0
- package/src/cli/nexus-ios-bind.ts +25 -8
- package/src/cli/phone-link.ts +117 -0
- package/src/cli/plugin-cli.ts +52 -0
- package/src/cli/pr-cli.ts +36 -0
- package/src/cli/private-ledger-commands.ts +31 -0
- package/src/cli/provider-cli.ts +5 -0
- package/src/cli/release-cli.ts +56 -12
- package/src/cli/release-run-if-ready.ts +48 -0
- package/src/cli/setup-cli.ts +27 -6
- package/src/cli/setup-recommend.ts +8 -0
- package/src/cli/usage-cli.ts +11 -1
- package/src/codex/setup.ts +27 -8
- package/src/dashboard/index.ts +1 -1
- package/src/discord.ts +56 -6
- package/src/domains/core-tools.ts +37 -14
- package/src/domains/outbound-alert.ts +55 -16
- package/src/exec-requests/planner.ts +140 -0
- package/src/exec-requests/runner.ts +338 -0
- package/src/exec-requests/store.ts +76 -0
- package/src/field/field-media.ts +250 -0
- package/src/field/field-reel.ts +196 -0
- package/src/grok/credential.ts +7 -5
- package/src/harness/aside-run-status.ts +33 -0
- package/src/harness/browser-verify.ts +48 -17
- package/src/harness/harness-cli-command.ts +25 -3
- package/src/harness/image-load-state.ts +21 -0
- package/src/harness/process-garbage.ts +45 -0
- package/src/index.ts +104 -38
- package/src/instance/leader.ts +13 -1
- package/src/intake-plane/collect-github.ts +5 -3
- package/src/intake-plane/collect-telegram-saved.ts +15 -3
- package/src/intake-plane/discord-seat-work.ts +56 -0
- package/src/intake-plane/intake-sources.ts +202 -0
- package/src/intake-plane/items.ts +28 -12
- package/src/intake-plane/seat-doc-route.ts +207 -0
- package/src/intake-plane/submit-intake-work.ts +9 -1
- package/src/intake-plane/telegram-seat-work.ts +72 -0
- package/src/llm/model-defaults.ts +24 -1
- package/src/llm.ts +5 -4
- package/src/machines/machine-ledger.ts +127 -0
- package/src/market/pack-internal-refs.ts +28 -0
- package/src/market/publish.ts +31 -4
- package/src/maturity/feature-maturity.ts +65 -0
- package/src/model-tier/turn-tier.ts +24 -0
- package/src/msg/msg-store.ts +149 -0
- package/src/native-tool-catalog.ts +2 -1
- package/src/nexus/api/chat-fast-path-config.ts +28 -0
- package/src/nexus/api/consult-requests.ts +117 -0
- package/src/nexus/api/discord-trigger-bot.ts +3 -0
- package/src/nexus/api/exec-requests.ts +51 -0
- package/src/nexus/api/field-uploads.ts +180 -0
- package/src/nexus/api/graph-approvals-feed.ts +140 -0
- package/src/nexus/api/graph-approvals.ts +120 -4
- package/src/nexus/api/harness-api.ts +219 -5
- package/src/nexus/api/http-server.ts +68 -5
- package/src/nexus/api/seat-requests.ts +254 -0
- package/src/nexus/api/trace-run-scan.ts +37 -0
- package/src/nexus/api/trace.ts +19 -3
- package/src/nexus/index.ts +3 -0
- package/src/nexus/setup-status.ts +4 -5
- package/src/notify/llm-budget-alert.ts +125 -0
- package/src/oauth/codex-account-rotation.ts +26 -4
- package/src/oauth/codex-account-store.ts +20 -2
- package/src/oauth/codex-browser-login.ts +173 -0
- package/src/oauth/codex.ts +4 -1
- package/src/oauth/fallback-chain.ts +5 -5
- package/src/oauth/store.ts +42 -5
- package/src/onboarding/continue-here.ts +29 -0
- package/src/onboarding/entry-hints.ts +8 -0
- package/src/onboarding/full-screen-io.ts +35 -3
- package/src/onboarding/help/skills.md +1 -1
- package/src/onboarding/non-interactive.ts +1 -0
- package/src/onboarding/subscription-hint.ts +27 -0
- package/src/onboarding/web-first.ts +83 -0
- package/src/onboarding.ts +230 -23
- package/src/plugins/maker/node-install.ts +92 -0
- package/src/plugins/maker/node-maker.ts +139 -0
- package/src/plugins/maker/plugin-maker.ts +220 -0
- package/src/policy/llm-policy.ts +268 -0
- package/src/prompt-library/session-guidance.ts +7 -1
- package/src/prompt-library/universal-preamble.ts +2 -2
- package/src/relay/channel.ts +184 -0
- package/src/relay/mux.ts +182 -0
- package/src/relay/relay-cli.ts +20 -0
- package/src/relay/relay-server.ts +220 -0
- package/src/relay/test-vectors.json +29 -0
- package/src/release-loop/checklist.ts +36 -9
- package/src/seat-address/seat-address.ts +34 -0
- package/src/self-dev/decompose.ts +1 -0
- package/src/self-dev/dev-pipeline.ts +6 -3
- package/src/self-dev/run-record-mirror.ts +44 -0
- package/src/self-dev/run-store.ts +12 -3
- package/src/self-implement/harness-policy-gates.ts +4 -1
- package/src/self-implement/report-origin.ts +12 -1
- package/src/self-implement/run-ledger-compact.ts +1 -1
- package/src/session/chat.ts +17 -5
- package/src/session/index.ts +23 -10
- package/src/skills/shared-agent-skill-roots.ts +25 -0
- package/src/steward/failure-streak.ts +78 -0
- package/src/steward/steward-cards.ts +24 -0
- package/src/steward/triage-budget.ts +127 -0
- package/src/steward/triage-plan.ts +75 -0
- package/src/steward/triage.ts +244 -42
- package/src/task-cards/card-store.ts +1 -1
- package/src/task-orchestrator/surfaces/pod-bun-cache.ts +18 -0
- package/src/task-orchestrator/surfaces/pod-command-job.ts +12 -3
- package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +11 -5
- package/src/task-orchestrator/surfaces/pod-ledger-events.ts +68 -0
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +78 -4
- package/src/telegram.ts +145 -2
- package/src/tool-runtime/elanous-skills-list-runtime.ts +3 -3
- package/src/tool-runtime/self-cognition-runtimes.ts +160 -2
- package/src/user-config.ts +84 -4
- package/src/version/packed-revision.json +1 -1
- package/src/video-pipeline/recipes/character.ts +42 -5
- package/src/video-pipeline/recipes/explainer.ts +213 -0
- package/src/video-pipeline/recipes/ffmpeg.ts +1 -1
- package/src/video-pipeline/recipes/free-line.ts +16 -3
- package/src/video-pipeline/recipes/remote.ts +7 -1
- package/src/video-pipeline/vflow-reference.ts +109 -0
- package/src/video-pipeline/walk-line.ts +3 -2
- package/apps/pwa/out/_next/static/chunks/06ef2eba-d1b99ac6d2ea663c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1413-7179a49332db5a80.js +0 -1
- package/apps/pwa/out/_next/static/chunks/150-25c277137a9638ab.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1640-bc99ec23bbe29d12.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2300-de059d0ab2a16630.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2684-05b0a81e75298c16.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3026-2ed7e317d9b02102.js +0 -25
- package/apps/pwa/out/_next/static/chunks/3611-77496ada38cd6358.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3717-68957c4dc9ceec90.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3929-46ba9af6f9d895cc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4015-62c5a3744698a8da.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4217-3c4317a7905f9eeb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4240-3230f230fae2d7d5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4271-e53e075ebd48593b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4377.3b125fef79576dfd.js +0 -5
- package/apps/pwa/out/_next/static/chunks/4853-35fdbe6bdbcdc0fc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5007-6a3b30e23ff68a6e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5244-12b424c42070f5ff.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5611-df00af3e4e4a61bb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5890-dca2321773be829a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6123-37c9dc57ddaa7ab0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6588-b7975304a302ab91.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6604-c03961962b6273b6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7109-cf8c87e968e83a65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7178-30f77877525a2efb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7262-871563fc9663730b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7294-f0eeebb92aa93459.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7429-881af44deed6958f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7527.943753b6ed829f09.js +0 -1
- package/apps/pwa/out/_next/static/chunks/764-a01e858525a190c0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7868-e35d5f7985ecd3f1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8320-51484af7b01fdfa5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9131-52f5fad51cceca04.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9238.2cd5f47f4bf7a2c3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-70c404c7bb6d7f0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-3ffd80166b1050aa.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-4bcc63e272ae8205.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-880734bd07d62882.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-72bd384898d88955.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-1018bbc9586ecae5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-de86f8c38076dd7b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-70c404c7bb6d7f0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-cd6447196d8bafe6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-e733630c47fa102b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-d1c1a8357b161875.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-60c514128e81d787.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-8b637b7e325e13c1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-63301d11dbe73836.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-a8cf0dc472060db2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-02fba54881ccfa49.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-d5e8e90f0038f8de.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-b1ef6824f494466a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-d589bb0f233b1009.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-c9bbaebef4cda059.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-3639ce7916b92fed.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-7450ae7478955653.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-70c404c7bb6d7f0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-b41ca4abfd618452.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-b333dd79ebee3fa9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-8be1227492793655.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-6e03e9371b552e0d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-911411e9fca18d03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-792170e649b37088.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-c40463466937bc91.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-509c798b80384536.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-1cac2aa21d77c417.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-9626799d9b4a2cd1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-84cd419494e16b0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/framework-361487d2da603917.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-607f5beb94cb2e07.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-d0222d219e29dddf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_app-a26980a31126decc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_error-edb2b37decf23e4d.js +0 -1
- package/apps/pwa/out/_next/static/css/eaa74e8bb2f62269.css +0 -1
- package/apps/pwa/out/_next/static/ktMqfrBbf8BMGac0jqLrb/_buildManifest.js +0 -1
- /package/apps/pwa/out/_next/static/{ktMqfrBbf8BMGac0jqLrb → keajW7DDyghEs1y8oJe6T}/_ssgManifest.js +0 -0
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
import * as childProcess from 'node:child_process';
|
|
8
8
|
import { existsSync, readFileSync, appendFileSync, mkdirSync, unlinkSync } from 'node:fs';
|
|
9
|
-
import { dirname, join } from 'node:path';
|
|
9
|
+
import { dirname, join, relative } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
10
11
|
import { getElanousConfigDir } from '../elanous-config-dir.js';
|
|
11
12
|
import { conatusEnv } from './conatus-env.js';
|
|
12
13
|
import { spillLongContent } from '../storage/content-spill.js';
|
|
@@ -54,7 +55,7 @@ function recentSendBurst(): { recentCount: number; burst: boolean } {
|
|
|
54
55
|
|
|
55
56
|
/** 발송 1건을 logs.db 에 관측(fail-open) — 발송 시각·mode·kind·밀림 판정. */
|
|
56
57
|
function logSend(
|
|
57
|
-
mode: 'realtime' | '
|
|
58
|
+
mode: 'realtime' | 'quiet-bypass' | 'flush',
|
|
58
59
|
kind: string,
|
|
59
60
|
extra: Record<string, unknown> = {},
|
|
60
61
|
opts?: { level?: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'critical' },
|
|
@@ -111,15 +112,16 @@ export function userRecentlyActive(now: Date = new Date()): boolean {
|
|
|
111
112
|
|
|
112
113
|
/** 야간 보류 적재 (jsonl append — 크론 동시 실행에 안전). origin 있으면 함께 적재 —
|
|
113
114
|
* 아침 flush 가 그 origin(발신 채널)으로 되돌려 발송(없으면 report 묶음). */
|
|
114
|
-
function deferOutbound(text: string, kind: string, origin?: MissionOrigin | null, path = DEFERRED_PATH):
|
|
115
|
+
function deferOutbound(text: string, kind: string, origin?: MissionOrigin | null, path = DEFERRED_PATH): boolean {
|
|
115
116
|
try {
|
|
116
117
|
mkdirSync(dirname(path), { recursive: true });
|
|
117
118
|
const rec = { ts: new Date().toISOString(), kind, text, ...(origin ? { origin } : {}) };
|
|
118
119
|
appendFileSync(path, JSON.stringify(rec) + '\n');
|
|
119
|
-
console.
|
|
120
|
-
|
|
120
|
+
console.error(`[outbound] 야간 무음(00:00~06:30 KST) — 보류 적재 (${kind})`);
|
|
121
|
+
return true;
|
|
121
122
|
} catch (e) {
|
|
122
|
-
console.
|
|
123
|
+
console.error(`[outbound] 보류 적재 실패 — 콘솔 출력\n${text}`, e instanceof Error ? e.message : '');
|
|
124
|
+
return false;
|
|
123
125
|
}
|
|
124
126
|
}
|
|
125
127
|
|
|
@@ -139,8 +141,29 @@ function resolveBotToken(botId?: string): string | null {
|
|
|
139
141
|
} catch { return null; }
|
|
140
142
|
}
|
|
141
143
|
|
|
142
|
-
/** origin(발신 채널)으로 직접
|
|
144
|
+
/** origin(발신 채널)으로 직접 발송. 실패하면 호출자가 report 폴백/재시도한다. */
|
|
143
145
|
function deliverToOrigin(origin: MissionOrigin, text: string): boolean {
|
|
146
|
+
if (origin.channel === 'discord') {
|
|
147
|
+
if (!origin.channelId) return false;
|
|
148
|
+
try {
|
|
149
|
+
const token = resolveChannelBotToken('discord', getUserConfig())?.token;
|
|
150
|
+
if (!token) return false;
|
|
151
|
+
const channelId = origin.discordThreadId || origin.channelId;
|
|
152
|
+
const out = spillLongContent(text).text;
|
|
153
|
+
for (let start = 0; start < out.length;) {
|
|
154
|
+
let end = Math.min(start + 2000, out.length);
|
|
155
|
+
if (end < out.length && /[\uD800-\uDBFF]/.test(out[end - 1]!)) end--;
|
|
156
|
+
const response = curlPost(
|
|
157
|
+
`https://discord.com/api/v10/channels/${channelId}/messages`,
|
|
158
|
+
JSON.stringify({ content: out.slice(start, end) }),
|
|
159
|
+
[`Authorization: Bot ${token}`, 'Content-Type: application/json'],
|
|
160
|
+
);
|
|
161
|
+
if (typeof response?.id !== 'string' || !response.id) return false;
|
|
162
|
+
start = end;
|
|
163
|
+
}
|
|
164
|
+
return out.length > 0;
|
|
165
|
+
} catch { return false; }
|
|
166
|
+
}
|
|
144
167
|
if (origin.channel !== 'telegram' || origin.chatId == null) return false;
|
|
145
168
|
const token = resolveBotToken(origin.botId);
|
|
146
169
|
if (!token) return false;
|
|
@@ -189,11 +212,13 @@ export function flushDeferred(path = DEFERRED_PATH): number {
|
|
|
189
212
|
// origin 없는 것 = report 묶음(기존 동작·finance/크론 알림).
|
|
190
213
|
const noOrigin = items.filter(i => !i.origin);
|
|
191
214
|
if (noOrigin.length && deliver(fmt(noOrigin), 'report')) delivered += noOrigin.length;
|
|
192
|
-
//
|
|
215
|
+
// origin 있는 것 = 발신 채널·스레드별로 묶어 서로 다른 수신자에게 섞이지 않게 발송.
|
|
193
216
|
const groups = new Map<string, { origin: MissionOrigin; list: Item[] }>();
|
|
194
217
|
for (const it of items) {
|
|
195
218
|
if (!it.origin) continue;
|
|
196
|
-
const key =
|
|
219
|
+
const key = it.origin.channel === 'discord'
|
|
220
|
+
? JSON.stringify(['discord', it.origin.channelId, it.origin.discordThreadId ?? null])
|
|
221
|
+
: JSON.stringify([it.origin.channel, it.origin.botId ?? null, it.origin.chatId ?? null, it.origin.threadId ?? null]);
|
|
197
222
|
if (!groups.has(key)) groups.set(key, { origin: it.origin, list: [] });
|
|
198
223
|
groups.get(key)!.list.push(it);
|
|
199
224
|
}
|
|
@@ -228,31 +253,45 @@ export function recordOutbound(text: string, kind: string): void {
|
|
|
228
253
|
/** 발송 진입점 — 야간 무음 게이트(00:00~06:30 KST 보류) + 보류분 자동 플러시.
|
|
229
254
|
* origin(발신 채널) 이 주어지면 그 채널(메인 Q&A 봇 등)로 되돌려 발송 — 무음이면 origin 을
|
|
230
255
|
* 함께 보류했다가 아침에 그 채널로 flush. origin 없거나 발송 실패 시 report 폴백(기존 동작). */
|
|
231
|
-
export function sendOutbound(text: string, kind = 'alert', origin?: MissionOrigin | null): boolean {
|
|
256
|
+
export function sendOutbound(text: string, kind = 'alert', origin?: (MissionOrigin & { surface?: string }) | null): boolean {
|
|
257
|
+
// Prefer the producer's surface; otherwise keep the caller's repository-relative path
|
|
258
|
+
// so two producers named index.ts cannot collapse into one sender.
|
|
259
|
+
const caller = new Error().stack?.split('\n')[2] ?? '';
|
|
260
|
+
const callerPath = caller.match(/(?:\(|\s)(file:\/\/[^\s()]+|\/[^\s()]+\.[cm]?[jt]s):\d+:\d+\)?/)?.[1];
|
|
261
|
+
const repoRoot = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
|
|
262
|
+
const source = origin?.surface || (callerPath
|
|
263
|
+
? relative(repoRoot, callerPath.startsWith('file://') ? fileURLToPath(callerPath) : callerPath).replace(/\\/g, '/')
|
|
264
|
+
: 'unknown');
|
|
265
|
+
const logOutcome = (event: 'sent' | 'deferred' | 'failed'): void => {
|
|
266
|
+
try { debug.log('outbound.send', event, { kind, source, chars: text.length }); } catch { /* fail-open */ }
|
|
267
|
+
};
|
|
232
268
|
// ★ 무음 우회(대표 2026-07-14) — 사용자가 최근(기본 30분) genuine 인텐트(타이핑/버튼탭)를 냈으면
|
|
233
269
|
// 깨어있으므로 야간 무음이어도 즉시 발송(사용자 발원 흐름의 결과물이 아침까지 묶이지 않게).
|
|
234
270
|
// 우회 시 flushDeferred 로 그간 보류분도 함께 전달(사용자가 지금 볼 수 있음).
|
|
235
271
|
if (inQuietHours() && !userRecentlyActive()) {
|
|
236
|
-
deferOutbound(text, kind, origin);
|
|
272
|
+
const queued = deferOutbound(text, kind, origin);
|
|
237
273
|
recordOutbound(text, kind); // 보류도 논리적 발송 — 회상 대상(데몬 미경유라 클라 기록)
|
|
238
|
-
|
|
274
|
+
logOutcome(queued ? 'deferred' : 'failed');
|
|
275
|
+
return true; // 보류 시 수락 — 적재 오류에서도 기존 호출측 재시도 계약 유지
|
|
239
276
|
}
|
|
240
277
|
const bypass = inQuietHours();
|
|
241
|
-
if (bypass) console.
|
|
278
|
+
if (bypass) console.error('[outbound] 야간 무음 우회 — 최근 사용자 활동(깨어있음) → 즉시 발송 + 보류분 flush');
|
|
242
279
|
try { flushDeferred(); } catch { /* fail-soft */ }
|
|
243
280
|
// ★ 발송 관측(대표 지시) — 발송 시각·mode·밀림(burst) 판정을 logs.db 에. burst=최근 2분 5건+
|
|
244
281
|
// (몰려 나가는 중 = 밀림 의심). `elanous logs --category outbound.send` 로 실시간/밀림 구분.
|
|
245
282
|
const { recentCount, burst } = recentSendBurst();
|
|
246
283
|
logSend(bypass ? 'quiet-bypass' : 'realtime', kind, { burst, recentCount, ...(burst ? { backlog: true } : {}) });
|
|
247
284
|
// ★ origin 되돌림(무음 밖) — 발신 채널로 직접 발송. 성공 시 종료, 실패면 report 폴백.
|
|
248
|
-
if (origin &&
|
|
285
|
+
if (origin && deliverToOrigin(origin, text)) {
|
|
249
286
|
recordOutbound(text, kind);
|
|
287
|
+
logOutcome('sent');
|
|
250
288
|
return true;
|
|
251
289
|
}
|
|
252
290
|
const path = deliver(text, kind);
|
|
253
291
|
// 원장 기록은 정확히 1회. 데몬 경유(daemon)면 /v1/outbound 핸들러(outbound-report.ts)
|
|
254
292
|
// 가 기록하므로 클라는 중복 금지 — 직접 폴백(direct·데몬 다운)만 클라가 기록.
|
|
255
293
|
if (path === 'direct') recordOutbound(text, kind);
|
|
294
|
+
logOutcome(path === false ? 'failed' : 'sent');
|
|
256
295
|
return path !== false;
|
|
257
296
|
}
|
|
258
297
|
|
|
@@ -275,7 +314,7 @@ export function classifyDaemonResponse(j: unknown): DaemonPathClass {
|
|
|
275
314
|
function logDaemonPath(classification: DaemonPathClass, kind: string, extra: Record<string, unknown> = {}): void {
|
|
276
315
|
try { debug.log('outbound.send', 'daemon-path', { classification, kind, ...extra }); } catch { /* fail-soft */ }
|
|
277
316
|
if (classification === 'ok') return;
|
|
278
|
-
try { console.
|
|
317
|
+
try { console.error(`[outbound] daemon-path ${classification}`); } catch { /* fail-soft */ }
|
|
279
318
|
}
|
|
280
319
|
|
|
281
320
|
/** elanous `/v1/outbound` 우선 → 실패 시 텔레그램 직접. 성공 경로 반환(원장 중복방지용). */
|
|
@@ -334,6 +373,6 @@ function sendTelegramDirect(text: string, kind?: string): boolean {
|
|
|
334
373
|
tok = tok || env.TELEGRAM_BOT_TOKEN || '';
|
|
335
374
|
chat = chat || env.TELEGRAM_CHAT_ID || '';
|
|
336
375
|
}
|
|
337
|
-
if (!tok || !chat) { console.
|
|
376
|
+
if (!tok || !chat) { console.error('[outbound] 토큰/chat 미설정 — 콘솔 출력\n' + text); return false; }
|
|
338
377
|
return sendTelegramRaw(tok, chat, text);
|
|
339
378
|
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { parse as parseYaml } from 'yaml';
|
|
5
|
+
import { elanousStateRoot } from '../autopilot/state-paths.js';
|
|
6
|
+
import { defaultGraphsDir } from '../self-implement/graph-templates.js';
|
|
7
|
+
import { handleGraphsGet } from '../nexus/api/graphs-api.js';
|
|
8
|
+
import { debug } from '../debug/log.js';
|
|
9
|
+
|
|
10
|
+
export interface InstalledGraph {
|
|
11
|
+
id: string;
|
|
12
|
+
title: string;
|
|
13
|
+
description: string;
|
|
14
|
+
path: string;
|
|
15
|
+
/** 플러그인 그래프가 받는 입력 키(`examples/*.json` 표본에서) — COO 가 inputs 를 채울 수 있게. */
|
|
16
|
+
inputKeys?: string[];
|
|
17
|
+
}
|
|
18
|
+
export interface ExecPlanItem {
|
|
19
|
+
seat: string;
|
|
20
|
+
title: string;
|
|
21
|
+
graphId: string;
|
|
22
|
+
inputs: Record<string, unknown>;
|
|
23
|
+
reason?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 설치된 플러그인의 실행 그래프 — `<state>/plugins/<market>/<name>/<version>/graphs/{recipes.yaml, <id>.yaml}`
|
|
27
|
+
* (`src/plugins/install/plugin-install.ts` 설치 경로). 이름마다 가장 새 버전 하나 · 제목·설명은 그래프 `loop` 가 없으면
|
|
28
|
+
* `plugin.json` 의 name·description(COO 가 «무엇을 하는 그래프인지» 알게). 2026-10-01 실측: 이것을 안 봐서
|
|
29
|
+
* 한 줄이 자리로 나뉘어도 «맞는 설치 그래프 없음»이었다. */
|
|
30
|
+
export function installedPluginGraphs(pluginsRoot = join(elanousStateRoot(), 'plugins')): { graphs: InstalledGraph[]; errors: string[] } {
|
|
31
|
+
const graphs: InstalledGraph[] = [];
|
|
32
|
+
const errors: string[] = [];
|
|
33
|
+
const dirs = (path: string): string[] => {
|
|
34
|
+
try { return readdirSync(path, { withFileTypes: true }).filter(e => e.isDirectory() && !e.name.startsWith('.')).map(e => e.name); }
|
|
35
|
+
catch { return []; }
|
|
36
|
+
};
|
|
37
|
+
for (const market of dirs(pluginsRoot)) {
|
|
38
|
+
for (const name of dirs(join(pluginsRoot, market))) {
|
|
39
|
+
const versions = dirs(join(pluginsRoot, market, name)).sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
|
|
40
|
+
const version = versions[0];
|
|
41
|
+
if (!version) continue;
|
|
42
|
+
const root = join(pluginsRoot, market, name, version);
|
|
43
|
+
const graphsDir = join(root, 'graphs');
|
|
44
|
+
if (!existsSync(join(graphsDir, 'recipes.yaml'))) continue;
|
|
45
|
+
let meta: { name?: string; description?: string } = {};
|
|
46
|
+
try { meta = JSON.parse(readFileSync(join(root, 'plugin.json'), 'utf8')) as typeof meta; } catch { errors.push(`${market}/${name}@${version}: plugin.json unreadable`); }
|
|
47
|
+
// 입력 키 — 플러그인이 입력 스키마를 따로 선언하지 않아 `examples/*.json` 첫 표본의 키를 쓴다(값은 가상이라 안 싣는다).
|
|
48
|
+
// 2026-10-01 실측: 키를 몰라 COO 가 `inputs:{}` 로 맡겼고 geo-check 가 «brand is required» 로 첫 노드에서 떨어졌다.
|
|
49
|
+
let inputKeys: string[] | undefined;
|
|
50
|
+
try {
|
|
51
|
+
const sample = readdirSync(join(root, 'examples')).filter(f => f.endsWith('.json')).sort()[0];
|
|
52
|
+
if (sample) {
|
|
53
|
+
const parsed = JSON.parse(readFileSync(join(root, 'examples', sample), 'utf8')) as unknown;
|
|
54
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) inputKeys = Object.keys(parsed);
|
|
55
|
+
}
|
|
56
|
+
} catch { /* 표본 없음 · 망가짐 = 키 모름 */ }
|
|
57
|
+
let files: string[] = [];
|
|
58
|
+
try { files = readdirSync(graphsDir).filter(f => /\.ya?ml$/.test(f) && f !== 'recipes.yaml'); } catch { errors.push(`${market}/${name}@${version}: graphs unreadable`); }
|
|
59
|
+
for (const file of files) {
|
|
60
|
+
const path = join(graphsDir, file);
|
|
61
|
+
try {
|
|
62
|
+
const doc = parseYaml(readFileSync(path, 'utf8')) as { graph_id?: string; loop?: { title?: string; description?: string } };
|
|
63
|
+
const id = doc?.graph_id;
|
|
64
|
+
if (!id || !/^[a-z0-9-]+$/.test(id)) continue;
|
|
65
|
+
graphs.push({ id, title: doc.loop?.title ?? meta.name ?? id, description: doc.loop?.description ?? meta.description ?? '', path, ...(inputKeys ? { inputKeys } : {}) });
|
|
66
|
+
} catch { errors.push(`${market}/${name}@${version}: ${file} malformed`); }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { graphs, errors };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function installedGraphs(coreDir = defaultGraphsDir(), mineDir = join(elanousStateRoot(), 'graphs')): Promise<InstalledGraph[]> {
|
|
74
|
+
const base = await coreAndMineGraphs(coreDir, mineDir);
|
|
75
|
+
// 플러그인은 mine 과 같은 상태 뿌리 아래(<root>/graphs ↔ <root>/plugins) — 시험의 임시 mine 은 운영 플러그인을 안 본다.
|
|
76
|
+
const plugins = installedPluginGraphs(join(dirname(mineDir), 'plugins'));
|
|
77
|
+
const seen = new Set(base.map(g => g.id));
|
|
78
|
+
const extra = plugins.graphs.filter(g => !seen.has(g.id));
|
|
79
|
+
debug.log('exec-requests', 'catalog', { coreAndMine: base.length, plugins: extra.length, pluginErrors: plugins.errors.slice(0, 5) });
|
|
80
|
+
return [...base, ...extra];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function coreAndMineGraphs(coreDir: string, mineDir: string): Promise<InstalledGraph[]> {
|
|
84
|
+
const catalog = handleGraphsGet('/v1/graphs', coreDir, { coreDir, mineDir });
|
|
85
|
+
if (!catalog.ok) throw new Error('installed graph catalog unavailable');
|
|
86
|
+
const { graphs } = await catalog.json() as { graphs: Array<{ id: string; source: 'core' | 'mine' }> };
|
|
87
|
+
return graphs.flatMap(({ id, source }) => {
|
|
88
|
+
const dir = source === 'core' ? coreDir : mineDir;
|
|
89
|
+
if (!/^[a-z0-9-]+$/.test(id) || !existsSync(join(dir, 'recipes.yaml'))) return [];
|
|
90
|
+
let names: string[];
|
|
91
|
+
try { names = readdirSync(dir).filter(name => /\.ya?ml$/.test(name)); }
|
|
92
|
+
catch { return []; }
|
|
93
|
+
for (const name of names) {
|
|
94
|
+
const path = join(dir, name);
|
|
95
|
+
try {
|
|
96
|
+
const doc = parseYaml(readFileSync(path, 'utf8')) as { graph_id?: string; loop?: { title?: string; description?: string } };
|
|
97
|
+
if (doc?.graph_id === id) return [{ id, title: doc.loop?.title ?? id, description: doc.loop?.description ?? '', path }];
|
|
98
|
+
} catch { /* A malformed file is not an executable graph. */ }
|
|
99
|
+
}
|
|
100
|
+
return [];
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function seatTitles(path = fileURLToPath(new URL('../../scripts/coord-tracks.json', import.meta.url))): string[] {
|
|
105
|
+
const data = JSON.parse(readFileSync(path, 'utf8')) as { tracks: Array<{ title?: string }> };
|
|
106
|
+
return data.tracks.flatMap(track => track.title ? [track.title] : []);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function judgeExecPlan(text: string, graphs: readonly InstalledGraph[], seats: readonly string[]): Promise<unknown> {
|
|
110
|
+
const prompt = `COO 역할. 요청을 실행 가능한 자리별 그래프로 나눠 맡겨라. JSON 배열만 출력: [{"seat":"자리","title":"할 일","graphId":"설치된 그래프 ID 또는 빈 문자열","inputs":{}}]. 자리 이름은 주어진 목록에서만, 그래프 ID는 설치된 목록에서만 고른다. 맞는 그래프가 없으면 graphId=""로 표시한다. 그래프에 inputKeys 가 있으면 inputs 를 그 키로만 채우되 한 줄에서 알 수 있는 값만 넣는다(모르는 값은 넣지 않는다). 그 그래프가 꼭 받아야 할 값(예: 브랜드 이름 · 사진 파일)을 한 줄에서 얻을 수 없으면 graphId=""로 두고 title 끝에 « — <무엇> 필요»를 적는다. 없는 그래프나 산출을 지어내지 마라. 바깥 게시·발행·광고·결제는 승인 노드를 거치기 전 실행하면 안 된다.\n자리: ${JSON.stringify(seats)}\n실행 가능한 그래프: ${JSON.stringify(graphs.map(({ id, title, description, inputKeys }) => ({ id, title, description, ...(inputKeys ? { inputKeys } : {}) })))}\n한 줄: ${JSON.stringify(text)}`;
|
|
111
|
+
const { streamLLM } = await import('../llm.js');
|
|
112
|
+
const { tierModel } = await import('../llm/model-defaults.js');
|
|
113
|
+
const raw = await streamLLM([{ role: 'user', content: prompt }], () => {}, { model: tierModel('best') });
|
|
114
|
+
return JSON.parse(raw.replace(/^```(?:json)?\s*|\s*```$/g, '').trim()) as unknown;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function planExecRequest(text: string, deps: {
|
|
118
|
+
graphs?: () => Promise<InstalledGraph[]>;
|
|
119
|
+
seats?: () => string[];
|
|
120
|
+
judge?: typeof judgeExecPlan;
|
|
121
|
+
} = {}): Promise<ExecPlanItem[]> {
|
|
122
|
+
const graphs = await (deps.graphs ?? installedGraphs)();
|
|
123
|
+
const seats = (deps.seats ?? seatTitles)();
|
|
124
|
+
const raw = await (deps.judge ?? judgeExecPlan)(text, graphs, seats);
|
|
125
|
+
if (!Array.isArray(raw) || raw.length === 0) throw new Error('COO 계획이 비었거나 배열이 아닙니다');
|
|
126
|
+
const known = new Map(graphs.map(graph => [graph.id, graph]));
|
|
127
|
+
return raw.map((item: unknown) => {
|
|
128
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) throw new Error('COO 계획 항목 형식 오류');
|
|
129
|
+
const row = item as Record<string, unknown>;
|
|
130
|
+
if (typeof row.seat !== 'string' || !seats.includes(row.seat) || typeof row.title !== 'string' || !row.title.trim()
|
|
131
|
+
|| typeof row.graphId !== 'string' || !row.inputs || typeof row.inputs !== 'object' || Array.isArray(row.inputs)) {
|
|
132
|
+
throw new Error('COO 계획 항목 형식 오류');
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
seat: row.seat, title: row.title.trim(), graphId: row.graphId,
|
|
136
|
+
inputs: row.inputs as Record<string, unknown>,
|
|
137
|
+
...(!known.has(row.graphId) ? { reason: `${row.seat}: 요청에 맞는 설치된 실행 그래프가 없습니다` } : {}),
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, isAbsolute, join, resolve, sep } from 'node:path';
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
4
|
+
import { debug } from '../debug/log.js';
|
|
5
|
+
import { effectiveInstanceRoot } from '../instance/resolve.js';
|
|
6
|
+
import { lastJsonObject, runGraph, type GraphRunState } from '../graph-runner/runner.js';
|
|
7
|
+
import { installedGraphs, planExecRequest, type ExecPlanItem, type InstalledGraph } from './planner.js';
|
|
8
|
+
import { ExecRequestStore, type ExecRequest, type ExecResult, type ExecSeat } from './store.js';
|
|
9
|
+
|
|
10
|
+
export interface ExecRunnerDeps {
|
|
11
|
+
store?: ExecRequestStore;
|
|
12
|
+
graphs?: () => Promise<InstalledGraph[]>;
|
|
13
|
+
plan?: (text: string) => Promise<ExecPlanItem[]>;
|
|
14
|
+
run?: typeof runGraph;
|
|
15
|
+
root?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const active = new Set<string>();
|
|
19
|
+
const SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
20
|
+
const kinds: Record<string, ExecResult['kind']> = {
|
|
21
|
+
'.pdf': 'pdf', '.mp4': 'video', '.mov': 'video', '.webm': 'video', '.png': 'image', '.jpg': 'image',
|
|
22
|
+
'.jpeg': 'image', '.webp': 'image', '.md': 'report', '.txt': 'text',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function safeFile(path: string, root: string, graphId: string, runId: string): string | null {
|
|
26
|
+
if (!SEGMENT.test(graphId) || !SEGMENT.test(runId)) return null;
|
|
27
|
+
// Only run-prefixed files or the run artifact directory, never a sibling run's output.
|
|
28
|
+
const allowed = join(root, 'graph-runs', graphId);
|
|
29
|
+
const candidate = isAbsolute(path) ? path : resolve(allowed, path);
|
|
30
|
+
try {
|
|
31
|
+
const real = realpathSync(candidate);
|
|
32
|
+
const base = realpathSync(allowed);
|
|
33
|
+
const runPrefix = `${runId}.json.`;
|
|
34
|
+
const local = dirname(real) === base && basename(real).startsWith(runPrefix)
|
|
35
|
+
&& !/\.(?:decision\.json|tmp)$/.test(real);
|
|
36
|
+
const artifactsDir = join(allowed, `${runId}.json.artifacts`);
|
|
37
|
+
const realArtifactsDir = existsSync(artifactsDir) && statSync(artifactsDir).isDirectory()
|
|
38
|
+
? realpathSync(artifactsDir) : null;
|
|
39
|
+
const inRunDirectory = realArtifactsDir === join(base, `${runId}.json.artifacts`)
|
|
40
|
+
&& real.startsWith(`${realArtifactsDir}${sep}`);
|
|
41
|
+
if ((!local && !inRunDirectory) || !statSync(real).isFile()) return null;
|
|
42
|
+
return real;
|
|
43
|
+
} catch { return null; }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function publicLink(value: unknown): string | null {
|
|
47
|
+
if (typeof value !== 'string') return null;
|
|
48
|
+
try {
|
|
49
|
+
const url = new URL(value);
|
|
50
|
+
return (url.protocol === 'https:' || url.protocol === 'http:') && url.hostname && !url.username && !url.password ? value : null;
|
|
51
|
+
} catch { return null; }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function fileName(path: string, runId: string): string {
|
|
55
|
+
const digest = createHash('sha256').update(path).digest('hex');
|
|
56
|
+
return `${runId}--${digest}--${basename(path)}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function resultOf(output: Record<string, unknown>, seat: ExecSeat, id: string, root: string): ExecResult | null {
|
|
60
|
+
const candidate = typeof output.file === 'string' ? output.file : typeof output.path === 'string' ? output.path : null;
|
|
61
|
+
let url: string;
|
|
62
|
+
let kind: ExecResult['kind'];
|
|
63
|
+
let name: string;
|
|
64
|
+
if (candidate !== null) {
|
|
65
|
+
const path = safeFile(candidate, root, seat.graphId, seat.runId);
|
|
66
|
+
if (!path) return null;
|
|
67
|
+
const fileKind = kinds[basename(path).slice(basename(path).lastIndexOf('.')).toLowerCase()];
|
|
68
|
+
if (!fileKind) return null;
|
|
69
|
+
kind = fileKind;
|
|
70
|
+
name = basename(path);
|
|
71
|
+
url = `/v1/exec-requests/${id}/files/${encodeURIComponent(fileName(path, seat.runId))}`;
|
|
72
|
+
} else {
|
|
73
|
+
const link = publicLink(output.url);
|
|
74
|
+
if (!link) return null;
|
|
75
|
+
kind = 'link';
|
|
76
|
+
name = link;
|
|
77
|
+
url = link;
|
|
78
|
+
}
|
|
79
|
+
const sources = Array.isArray(output.sources) ? output.sources.flatMap((source: unknown) => {
|
|
80
|
+
if (!source || typeof source !== 'object') return [];
|
|
81
|
+
const entry = source as { title?: unknown; url?: unknown };
|
|
82
|
+
const sourceUrl = publicLink(entry.url);
|
|
83
|
+
return typeof entry.title === 'string' && sourceUrl ? [{ title: entry.title, url: sourceUrl }] : [];
|
|
84
|
+
}) : [];
|
|
85
|
+
return { seat: seat.seat, kind, title: typeof output.title === 'string' ? output.title : name,
|
|
86
|
+
url, ...(sources.length ? { sources } : {}) };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function syncRun(item: ExecRequest, seat: ExecSeat, state: GraphRunState, root: string): void {
|
|
90
|
+
const previous = seat.status;
|
|
91
|
+
seat.status = state.status === 'done' ? 'done' : state.status === 'failed' || state.status === 'budget-exceeded' ? 'failed' : 'running';
|
|
92
|
+
if (previous !== seat.status) debug.log('exec-requests', 'seat-status', { id: item.id, seat: seat.seat, from: previous, to: seat.status, graphId: seat.graphId, runId: seat.runId });
|
|
93
|
+
const claim = state.status === 'awaiting-approval' && state.pending
|
|
94
|
+
? `${join(root, 'graph-runs', seat.graphId, `${seat.runId}.json`)}.${state.path.length}.decision.json` : '';
|
|
95
|
+
if (state.status === 'awaiting-approval' && state.pending && !state.pending.decision && !existsSync(claim)) {
|
|
96
|
+
if (!item.approvals.some(a => a.graphId === seat.graphId && a.runId === seat.runId)) {
|
|
97
|
+
item.approvals.push({ graphId: seat.graphId, runId: seat.runId, message: state.pending.message });
|
|
98
|
+
}
|
|
99
|
+
} else item.approvals = item.approvals.filter(a => a.graphId !== seat.graphId || a.runId !== seat.runId);
|
|
100
|
+
for (const node of state.nodes ?? []) {
|
|
101
|
+
const output = lastJsonObject(node.output);
|
|
102
|
+
if (!output) continue;
|
|
103
|
+
const entries = Array.isArray(output.artifacts) ? output.artifacts : [output];
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue;
|
|
106
|
+
const result = resultOf(entry as Record<string, unknown>, seat, item.id, root);
|
|
107
|
+
if (result && !item.results.some(existing => existing.seat === result.seat && existing.url === result.url)) item.results.push(result);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const summary = [...(state.nodes ?? [])].reverse().map(node => lastJsonObject(node.output)?.summary).find(value => typeof value === 'string');
|
|
111
|
+
if (typeof summary === 'string') item.summary = summary;
|
|
112
|
+
if (seat.status === 'failed') seat.reason = state.nodes?.at(-1)?.error ?? `그래프 런 ${state.status}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function aggregate(item: ExecRequest): void {
|
|
116
|
+
if (item.status === 'planning' || item.seats.length === 0) return;
|
|
117
|
+
if (item.seats.some(seat => seat.status === 'waiting' || seat.status === 'running')) item.status = 'running';
|
|
118
|
+
else item.status = item.seats.some(seat => seat.status === 'failed') ? 'failed' : 'done';
|
|
119
|
+
// 실패면 요약은 «실패 사유»가 이긴다 — 먼저 끝난 자리의 산출 설명이 남아 있어도 덮는다(혼합 결과에서 성공처럼 보이지 않게).
|
|
120
|
+
if (item.status === 'failed') {
|
|
121
|
+
const reasons = item.seats.filter(seat => seat.status === 'failed').map(seat => seat.reason ?? `${seat.seat}: 그래프 실행 실패`);
|
|
122
|
+
if (reasons.length > 0) item.summary = reasons.join('; ');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export class ExecRequestRunner {
|
|
127
|
+
readonly store: ExecRequestStore;
|
|
128
|
+
private readonly root: string;
|
|
129
|
+
constructor(private readonly deps: ExecRunnerDeps = {}) {
|
|
130
|
+
this.store = deps.store ?? new ExecRequestStore();
|
|
131
|
+
this.root = deps.root ?? effectiveInstanceRoot();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** 데몬 «시작» 때 한 번만 부른다(`startNexusHttpServer`). 생성자에서 부르면 모듈을 import 만 한 다른 프로세스
|
|
135
|
+
* (시험·CLI·동시 서버)가 살아 있는 요청을 «재시작 중단»으로 실패 처리한다(리뷰 3라운드 must-fix). */
|
|
136
|
+
reconcileInterrupted(): void {
|
|
137
|
+
for (const item of this.store.list()) {
|
|
138
|
+
if (item.status !== 'planning' && item.status !== 'running') continue;
|
|
139
|
+
if (item.status === 'planning') {
|
|
140
|
+
item.status = 'failed';
|
|
141
|
+
item.summary = '데몬 재시작으로 COO 계획이 중단됐습니다';
|
|
142
|
+
} else {
|
|
143
|
+
if (item.seats.length === 0) {
|
|
144
|
+
item.status = 'failed';
|
|
145
|
+
item.summary = '데몬 재시작으로 자리 배정이 중단됐습니다';
|
|
146
|
+
}
|
|
147
|
+
for (const seat of item.seats) {
|
|
148
|
+
if (seat.status === 'done' || seat.status === 'failed') continue;
|
|
149
|
+
const reason = '데몬 재시작으로 그래프 실행이 중단됐습니다';
|
|
150
|
+
if (SEGMENT.test(seat.graphId) && SEGMENT.test(seat.runId)) {
|
|
151
|
+
try {
|
|
152
|
+
const file = join(this.root, 'graph-runs', seat.graphId, `${seat.runId}.json`);
|
|
153
|
+
const state = JSON.parse(readFileSync(file, 'utf8')) as GraphRunState;
|
|
154
|
+
if (state.graphId === seat.graphId && state.runId === seat.runId) {
|
|
155
|
+
if (state.status === 'done' || state.status === 'failed' || state.status === 'budget-exceeded'
|
|
156
|
+
|| (state.status === 'awaiting-approval' && state.pending)) {
|
|
157
|
+
syncRun(item, seat, state, this.root);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} catch { /* No trustworthy completed or approval-pending run to recover. */ }
|
|
162
|
+
}
|
|
163
|
+
const previous = seat.status;
|
|
164
|
+
seat.status = 'failed';
|
|
165
|
+
seat.reason = reason;
|
|
166
|
+
item.approvals = item.approvals.filter(a => a.graphId !== seat.graphId || a.runId !== seat.runId);
|
|
167
|
+
debug.log('exec-requests', 'seat-status', { id: item.id, seat: seat.seat, from: previous, to: 'failed', reason });
|
|
168
|
+
}
|
|
169
|
+
aggregate(item);
|
|
170
|
+
if (item.status === 'failed' && item.seats.length > 0) item.summary = item.seats.filter(seat => seat.status === 'failed')
|
|
171
|
+
.map(seat => seat.reason ?? `${seat.seat}: 그래프 실행 실패`).join('; ');
|
|
172
|
+
}
|
|
173
|
+
this.store.save(item);
|
|
174
|
+
if (item.status === 'failed') debug.log('exec-requests', 'failed', { id: item.id, reason: item.summary });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
submit(text: string): ExecRequest {
|
|
179
|
+
const item = this.store.create(text);
|
|
180
|
+
void this.execute(item.id).catch(error => {
|
|
181
|
+
const current = this.store.get(item.id);
|
|
182
|
+
if (!current) return;
|
|
183
|
+
current.status = 'failed';
|
|
184
|
+
current.summary = error instanceof Error ? error.message : String(error);
|
|
185
|
+
this.store.save(current);
|
|
186
|
+
debug.log('exec-requests', 'failed', { id: item.id, reason: current.summary });
|
|
187
|
+
});
|
|
188
|
+
return item;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private async execute(id: string): Promise<void> {
|
|
192
|
+
const item = this.store.get(id);
|
|
193
|
+
if (!item) return;
|
|
194
|
+
const graphs = await (this.deps.graphs ?? installedGraphs)();
|
|
195
|
+
const plans = await (this.deps.plan ?? ((text: string) => planExecRequest(text, { graphs: async () => graphs })))(item.text);
|
|
196
|
+
const byId = new Map(graphs.map(graph => [graph.id, graph]));
|
|
197
|
+
item.seats = plans.map(plan => ({ seat: plan.seat, title: plan.title, status: plan.reason || !byId.has(plan.graphId) ? 'failed' : 'waiting', graphId: plan.graphId,
|
|
198
|
+
runId: plan.reason || !byId.has(plan.graphId) ? '' : randomUUID(), inputs: plan.inputs,
|
|
199
|
+
...(plan.reason || !byId.has(plan.graphId) ? { reason: plan.reason ?? `${plan.seat}: 요청에 맞는 설치된 실행 그래프가 없습니다` } : {}) }));
|
|
200
|
+
item.status = 'running';
|
|
201
|
+
debug.log('exec-requests', 'planned', { id, seats: item.seats.map(seat => ({ seat: seat.seat, graphId: seat.graphId, status: seat.status })) });
|
|
202
|
+
this.store.save(item);
|
|
203
|
+
await Promise.all(item.seats.map(async (seat, index) => {
|
|
204
|
+
const graph = byId.get(seat.graphId);
|
|
205
|
+
if (!graph || seat.status === 'failed') return;
|
|
206
|
+
const starting = this.store.get(id)!;
|
|
207
|
+
starting.seats[index]!.status = 'running';
|
|
208
|
+
this.store.save(starting);
|
|
209
|
+
debug.log('exec-requests', 'started', { id, seat: seat.seat, graphId: seat.graphId, runId: seat.runId });
|
|
210
|
+
debug.log('exec-requests', 'seat-status', { id, seat: seat.seat, from: 'waiting', to: 'running' });
|
|
211
|
+
try {
|
|
212
|
+
const state = await (this.deps.run ?? runGraph)(graph.path, { input: seat.inputs, runId: seat.runId, deps: { root: this.root } });
|
|
213
|
+
const current = this.store.get(id)!;
|
|
214
|
+
syncRun(current, current.seats[index]!, state, this.root);
|
|
215
|
+
aggregate(current);
|
|
216
|
+
this.store.save(current);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
const current = this.store.get(id)!;
|
|
219
|
+
current.seats[index]!.status = 'failed';
|
|
220
|
+
current.seats[index]!.reason = error instanceof Error ? error.message : String(error);
|
|
221
|
+
debug.log('exec-requests', 'seat-status', { id, seat: seat.seat, to: 'failed', reason: current.seats[index]!.reason });
|
|
222
|
+
aggregate(current);
|
|
223
|
+
this.store.save(current);
|
|
224
|
+
}
|
|
225
|
+
}));
|
|
226
|
+
const finished = this.store.get(id)!;
|
|
227
|
+
aggregate(finished);
|
|
228
|
+
this.store.save(finished);
|
|
229
|
+
if (finished.status === 'done' || finished.status === 'failed') debug.log('exec-requests', finished.status, { id, results: finished.results.length });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
get(id: string): ExecRequest | null {
|
|
233
|
+
const item = this.store.get(id);
|
|
234
|
+
if (!item) return null;
|
|
235
|
+
if (item.status === 'planning' || item.status === 'done' || item.status === 'failed') return item;
|
|
236
|
+
for (const seat of item.seats) {
|
|
237
|
+
// An approved decision triggers a resume; failures are terminal and never retried on reads.
|
|
238
|
+
// A terminal seat is never re-synced from a stale awaiting-approval snapshot.
|
|
239
|
+
if (seat.status === 'done' || seat.status === 'failed' || !seat.runId || !SEGMENT.test(seat.graphId) || !SEGMENT.test(seat.runId)) continue;
|
|
240
|
+
const file = join(this.root, 'graph-runs', seat.graphId, `${seat.runId}.json`);
|
|
241
|
+
if (!existsSync(file)) continue;
|
|
242
|
+
try {
|
|
243
|
+
const state = JSON.parse(readFileSync(file, 'utf8')) as GraphRunState;
|
|
244
|
+
if (state.graphId !== seat.graphId || state.runId !== seat.runId) continue;
|
|
245
|
+
syncRun(item, seat, state, this.root);
|
|
246
|
+
if (state.status === 'awaiting-approval' && state.pending && !state.pending.decision) {
|
|
247
|
+
const decision = `${file}.${state.path.length}.decision.json`;
|
|
248
|
+
if (existsSync(decision)) {
|
|
249
|
+
item.approvals = item.approvals.filter(a => a.runId !== seat.runId);
|
|
250
|
+
const key = `${seat.graphId}/${seat.runId}`;
|
|
251
|
+
if (!active.has(key)) {
|
|
252
|
+
active.add(key);
|
|
253
|
+
const launch = async () => {
|
|
254
|
+
try {
|
|
255
|
+
const graphs = await (this.deps.graphs ?? installedGraphs)();
|
|
256
|
+
const graph = graphs.find(candidate => candidate.id === seat.graphId);
|
|
257
|
+
if (!graph) throw new Error('설치된 그래프가 없습니다');
|
|
258
|
+
const resumed = await (this.deps.run ?? runGraph)(graph.path, { resumeRunId: seat.runId, deps: { root: this.root } });
|
|
259
|
+
const current = this.store.get(id);
|
|
260
|
+
const target = current?.seats.find(s => s.runId === seat.runId);
|
|
261
|
+
if (current && target) { syncRun(current, target, resumed, this.root); aggregate(current); this.store.save(current); }
|
|
262
|
+
} catch (error) {
|
|
263
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
264
|
+
const current = this.store.get(id);
|
|
265
|
+
const target = current?.seats.find(s => s.graphId === seat.graphId && s.runId === seat.runId);
|
|
266
|
+
if (current && target && target.status !== 'done') {
|
|
267
|
+
target.status = 'failed';
|
|
268
|
+
target.reason = `그래프 재개 실패: ${reason}`;
|
|
269
|
+
current.approvals = current.approvals.filter(a => a.graphId !== seat.graphId || a.runId !== seat.runId);
|
|
270
|
+
aggregate(current);
|
|
271
|
+
this.store.save(current);
|
|
272
|
+
debug.log('exec-requests', 'seat-status', { id, seat: seat.seat, to: 'failed', reason: target.reason });
|
|
273
|
+
}
|
|
274
|
+
debug.log('exec-requests', 'failed', { id, seat: seat.seat, reason });
|
|
275
|
+
} finally { active.delete(key); }
|
|
276
|
+
};
|
|
277
|
+
void launch();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
} catch (error) { debug.log('exec-requests', 'failed', { id, seat: seat.seat, reason: String(error) }); }
|
|
282
|
+
}
|
|
283
|
+
const current = this.store.get(id) ?? item;
|
|
284
|
+
for (const seat of item.seats) {
|
|
285
|
+
const target = current.seats.find(candidate => candidate.runId && candidate.runId === seat.runId);
|
|
286
|
+
if (target && target.status !== 'done' && target.status !== 'failed') {
|
|
287
|
+
target.status = seat.status;
|
|
288
|
+
if (seat.reason) target.reason = seat.reason;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const approvals = new Map(current.approvals.map(approval => [`${approval.graphId}/${approval.runId}`, approval]));
|
|
292
|
+
for (const approval of item.approvals) approvals.set(`${approval.graphId}/${approval.runId}`, approval);
|
|
293
|
+
current.approvals = [...approvals.values()].filter(approval => {
|
|
294
|
+
const seat = current.seats.find(candidate => candidate.graphId === approval.graphId && candidate.runId === approval.runId);
|
|
295
|
+
if (!seat || seat.status === 'done' || seat.status === 'failed') return false;
|
|
296
|
+
const file = join(this.root, 'graph-runs', approval.graphId, `${approval.runId}.json`);
|
|
297
|
+
try {
|
|
298
|
+
const state = JSON.parse(readFileSync(file, 'utf8')) as GraphRunState;
|
|
299
|
+
return state.status === 'awaiting-approval' && !!state.pending && !state.pending.decision
|
|
300
|
+
&& !existsSync(`${file}.${state.path.length}.decision.json`);
|
|
301
|
+
} catch { return false; }
|
|
302
|
+
});
|
|
303
|
+
for (const result of item.results) {
|
|
304
|
+
if (!current.results.some(existing => existing.url === result.url)) current.results.push(result);
|
|
305
|
+
}
|
|
306
|
+
if (item.summary && !current.summary) current.summary = item.summary;
|
|
307
|
+
aggregate(current);
|
|
308
|
+
this.store.save(current);
|
|
309
|
+
return current;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
file(id: string, name: string): string | null {
|
|
313
|
+
// Artifact names may contain spaces and Unicode; only a single safe path component is accepted.
|
|
314
|
+
if (!name || name === '.' || name === '..' || name !== basename(name) || /[\\/\x00-\x1f\x7f]/.test(name)) return null;
|
|
315
|
+
const item = this.get(id);
|
|
316
|
+
const result = item?.results.find(entry => entry.url === `/v1/exec-requests/${id}/files/${encodeURIComponent(name)}`);
|
|
317
|
+
if (!result) return null;
|
|
318
|
+
const seat = item!.seats.find(entry => name.startsWith(`${entry.runId}--`) && entry.seat === result.seat);
|
|
319
|
+
if (!seat) return null;
|
|
320
|
+
try {
|
|
321
|
+
const state = JSON.parse(readFileSync(join(this.root, 'graph-runs', seat.graphId, `${seat.runId}.json`), 'utf8')) as GraphRunState;
|
|
322
|
+
if (state.graphId !== seat.graphId || state.runId !== seat.runId) return null;
|
|
323
|
+
for (const node of state.nodes ?? []) {
|
|
324
|
+
const output = lastJsonObject(node.output);
|
|
325
|
+
if (!output) continue;
|
|
326
|
+
for (const entry of Array.isArray(output.artifacts) ? output.artifacts : [output]) {
|
|
327
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue;
|
|
328
|
+
const artifact = entry as Record<string, unknown>;
|
|
329
|
+
const candidate = typeof artifact.file === 'string' ? artifact.file : artifact.path;
|
|
330
|
+
if (typeof candidate !== 'string') continue;
|
|
331
|
+
const path = safeFile(candidate, this.root, seat.graphId, seat.runId);
|
|
332
|
+
if (path && fileName(path, seat.runId) === name) return path;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
} catch { return null; }
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
}
|