elanous 0.2.7 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/chunks/4836-3084c58c1725a40b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7032-f4ef698518fba61c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8067-d976b441a104ab74.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8519-5632377cce994f86.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8894-7317998f6cb97d8c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9740-8f5ff96c522b5f55.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-9aa47b0bc6d570ff.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f4d2ecbbae6088d6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-2053e49930c82748.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-624f7a2f89b60c8f.js → page-d4b6df905f185055.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-5a5fb385a4df0ffe.js → page-a70ba27c33003dd0.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-186fc6952f027acc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/{page-7457f52683562161.js → page-18166f885d831d3e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-1c9d6cfe68b121c2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-cbbe03c0da7b081a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-c108ff1fdb665c97.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-b7dcf5d48a854b76.js → page-147a328753d983e2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-51480757e7d68b92.js → page-799395a9df7d95aa.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-6a8158b71d1fe9d7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-9567560270857531.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/{page-30533117beb53625.js → page-e7ed3d010153d818.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-c0c0d19d51342d3e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-07267f1391b96aba.js → page-2e002ad8faa43291.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-55d50e9b0006a91d.js → page-b672e02e4bec8deb.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-1ba4a5d857276334.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-9db2226f38c542fb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/{page-a7e37af7d0399cb8.js → page-70ee65f5f7a151dc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-0bb8e43203efb23b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-092e1bcf27d751cc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-15e5a15db84c56aa.js → page-eb52580c4b75b905.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-a2e572e4511f2a16.js → page-7464c1987dd76e6d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-dd107bf1c7d52a90.js → page-e573437b0a1c728f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-294300625e49e4d3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-085847b088675978.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-9fe9d38c36999eb4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-d8ab4d1489223459.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-6141f67f531d8b6a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-295a02b1026b8255.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-1eb9a0e1e054209d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-56adbe1a1f1379bd.js → page-82efa177a30f3741.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d0be51cec4f62606.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-2e367075ffdbc062.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-5d422f38c052967e.js → page-647ed2b66c1d49f1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ba058c56e38f2792.js +1 -0
- package/apps/pwa/out/_next/static/css/5ef72a618a8994f3.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +13 -13
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +13 -13
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +13 -13
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +13 -13
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +13 -13
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +13 -13
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +13 -13
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +13 -13
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +13 -13
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +13 -13
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +13 -13
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +13 -13
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +13 -13
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +13 -13
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +13 -13
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +13 -13
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +13 -13
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +13 -13
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +13 -13
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +14 -14
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +13 -13
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +13 -13
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +13 -13
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +13 -13
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +13 -13
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +13 -13
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +13 -13
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +13 -13
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +13 -13
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +13 -13
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +13 -13
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +13 -13
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +13 -13
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +13 -13
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +13 -13
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +13 -13
- package/graphs/test-diet/recipes.yaml +9 -0
- package/graphs/test-diet/test-diet.yaml +22 -0
- package/package.json +2 -1
- package/persona-presets/_index.yaml +10 -0
- package/persona-presets/_schema.json +146 -0
- package/persona-presets/dana.yaml +26 -0
- package/persona-presets/haru.yaml +27 -0
- package/persona-presets/kai.yaml +27 -0
- package/persona-presets/leo.yaml +27 -0
- package/persona-presets/mira.yaml +30 -0
- package/persona-presets/nari.yaml +27 -0
- package/persona-presets/sora.yaml +27 -0
- package/persona-presets/yuna.yaml +27 -0
- package/plugins/geo-check/graphs/run-step.ts +34 -10
- package/scripts/field-feed/deliver-node.ts +8 -3
- package/scripts/field-feed/lib.ts +2 -0
- package/scripts/install.sh +20 -11
- package/scripts/persona-presets/lint.ts +79 -0
- package/scripts/release-loop/checklist-gate-node.ts +2 -2
- package/scripts/release-loop/gate-node.ts +27 -3
- package/scripts/release-loop/npm-publish-node.ts +10 -4
- package/scripts/release-loop/ops-upgrade-node.ts +11 -3
- package/scripts/release-loop/publish-node.ts +23 -1
- package/scripts/release-loop/shard-plan.ts +17 -0
- package/scripts/storyboard/storyboard.ts +167 -0
- package/scripts/test-diet/lib.ts +88 -0
- package/scripts/test-diet/node.ts +102 -0
- package/src/acp/auth-methods.ts +30 -0
- package/src/acp/server.ts +5 -5
- package/src/channel-import/detect.ts +191 -0
- package/src/channel-import/run.ts +111 -0
- package/src/cli/config-test-sync.ts +4 -0
- package/src/cli/decisions-cli.ts +13 -3
- package/src/cli/doctor-bundle.ts +128 -0
- package/src/cli/doctor-cli.ts +36 -1
- package/src/cli/doctor-local-llm.ts +95 -0
- package/src/cli/import-cli.ts +60 -0
- package/src/cli/mcp-login.ts +13 -4
- package/src/cli/persona-cli.ts +116 -0
- package/src/cli/release-cli.ts +8 -7
- package/src/cli/skills-cli.ts +173 -0
- package/src/cli/start-cli.ts +13 -4
- package/src/codex/setup.ts +15 -13
- package/src/dashboard/index.ts +17 -1
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +2 -2
- package/src/dashboard/slash-runtime/help-from-registry.ts +4 -2
- package/src/decisions/decision-cards.ts +291 -0
- package/src/decisions/decision-ledger.ts +5 -2
- package/src/decisions/discord-decision-cards.ts +133 -0
- package/src/decisions/telegram-decision-cards.ts +100 -0
- package/src/directives/version-at.ts +77 -0
- package/src/discord.ts +17 -0
- package/src/domains/outbound-alert.ts +44 -1
- package/src/domains/repeated-failure.ts +1 -1
- package/src/error-report/report.ts +200 -0
- package/src/exec-requests/planner.ts +8 -2
- package/src/exec-requests/runner.ts +64 -24
- package/src/exec-requests/store.ts +2 -0
- package/src/field/field-feed-auto.ts +48 -0
- package/src/field/field-reel.ts +8 -1
- package/src/hooks/error-report.ts +210 -0
- package/src/hooks/receiver.ts +41 -1
- package/src/import/apply.ts +128 -0
- package/src/import/plan.ts +197 -0
- package/src/index.ts +82 -7
- package/src/input-core/text-entry.ts +13 -5
- package/src/intake-plane/seat-answer.ts +96 -0
- package/src/intake-plane/seat-doc-route.ts +17 -3
- package/src/nexus/api/consult-requests.ts +19 -8
- package/src/nexus/api/error-reports.ts +108 -0
- package/src/nexus/api/http-server.ts +21 -1
- package/src/nexus/api/outbound-report.ts +7 -0
- package/src/nexus/api/running-runs-background.ts +68 -0
- package/src/nexus/api/running-runs-child.ts +6 -0
- package/src/nexus/api/share-captures.ts +91 -0
- package/src/nexus/api/skill-problems.ts +56 -0
- package/src/nexus/api/terminals.ts +24 -8
- package/src/nexus/index.ts +25 -1
- package/src/nexus/setup-status.ts +7 -2
- package/src/oauth/browser-availability.ts +14 -0
- package/src/oauth/codex-quota-policy.ts +16 -0
- package/src/onboarding/continue-here.ts +13 -8
- package/src/onboarding/web-first.ts +8 -1
- package/src/onboarding.ts +66 -12
- package/src/persona/presets.ts +105 -0
- package/src/release-loop/checklist.ts +28 -5
- package/src/self-dev/orchestrate.ts +18 -11
- package/src/shell-env-bootstrap.ts +44 -7
- package/src/skills/connect-claude-plugin.ts +142 -0
- package/src/skills/connect.ts +70 -0
- package/src/skills/index.ts +29 -1
- package/src/skills/repair.ts +124 -0
- package/src/skills/runner.ts +11 -2
- package/src/skills/trigger-extract.ts +1 -1
- package/src/task-orchestrator/surfaces/pod-command-job.ts +16 -8
- package/src/telegram-commands.ts +13 -0
- package/src/telegram.ts +34 -0
- package/src/user-config.ts +111 -13
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/4836-2eba6079f31b88d4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8067-f74bf14d67ad98f1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8519-f6b5d8a5833a7603.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8894-e92491a164192cc1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9740-67051141115ce775.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9804-64c02c3c127d5014.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-d5ebde04844689e6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-10ed24f413ed66e9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-211810b6c93ab66a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-80497e93a878ddc5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-cee86394de407aa6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-4ff8edab44196b8c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-92e4f44428d5e290.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-2a05dfe80c8bdf16.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-d9979633aa82f8e6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-f7857826c2f97ad7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-9699b1786be6171d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-43dd772aee8af75f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-973ab7af59c718b7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a9f8d3865b91e436.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-d42b99cdfb9f27c6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-2764ff40dad5a995.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-c5f1152db116ff96.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-f933bca81ea38a6f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-c1c95739c0bff3b8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-a7aaffcf771662d8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-71d18eec33b9c667.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-2c4e59079ee357ac.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-a81f113f3ef89d72.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-fc27b00e20733146.js +0 -1
- package/apps/pwa/out/_next/static/css/f537bdb19c69306a.css +0 -1
- /package/apps/pwa/out/_next/static/{keajW7DDyghEs1y8oJe6T → lywc8m_HDzH9MKSDQhyEj}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{keajW7DDyghEs1y8oJe6T → lywc8m_HDzH9MKSDQhyEj}/_ssgManifest.js +0 -0
|
@@ -10,6 +10,7 @@ import { getElanousConfigDir } from '../elanous-config-dir.js';
|
|
|
10
10
|
import type { ExecRequest } from '../exec-requests/store.js';
|
|
11
11
|
import { resolveDaemonEndpoint } from '../nexus/daemon-endpoint.js';
|
|
12
12
|
import { parseSeatAddress, resolveSeat } from '../seat-address/seat-address.js';
|
|
13
|
+
import { answerAsSeat, isNoGraphFailure } from './seat-answer.js';
|
|
13
14
|
|
|
14
15
|
const POLL_MS = 5_000;
|
|
15
16
|
const TIMEOUT_MS = 30 * 60_000;
|
|
@@ -34,7 +35,9 @@ export interface SeatDocDeps {
|
|
|
34
35
|
sleep?: (ms: number) => Promise<void>;
|
|
35
36
|
now?: () => number;
|
|
36
37
|
pwaUrl?: () => string | undefined;
|
|
37
|
-
log?: (event: 'submitted' | 'research' | 'settled' | 'delivered' | 'delivery-failed', data: Record<string, unknown>) => void;
|
|
38
|
+
log?: (event: 'submitted' | 'research' | 'settled' | 'delivered' | 'delivery-failed' | 'seat-answer', data: Record<string, unknown>) => void;
|
|
39
|
+
/** EV12d — the seat's own answer when no installed graph fits (default = role file ⊕ owned checklist ⊕ recent, one LLM call). */
|
|
40
|
+
seatAnswer?: (seatAddress: string, question: string) => Promise<{ title: string; text: string } | null>;
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
export function isSeatDocRequest(text: string): boolean {
|
|
@@ -194,9 +197,20 @@ export async function submitSeatDocRequest({ text, reportTo, deps = {} }: {
|
|
|
194
197
|
}
|
|
195
198
|
log('settled', { id, status: result?.status ?? (lookupAvailable ? 'timeout' : 'lookup-unavailable'), lastConfirmedStatus });
|
|
196
199
|
const links = result?.status === 'done' ? await fileLinks(id, result, deps) : new Map<string, string>();
|
|
200
|
+
let reply = message(id, result, withoutResearch, lookupAvailable, accessiblePwaUrl((deps.pwaUrl ?? chatPwaUrl)()), links);
|
|
201
|
+
if (isNoGraphFailure(result)) {
|
|
202
|
+
// EV12d — a question with no fitting graph gets the seat's own answer, not «문서 요청 실패».
|
|
203
|
+
const seat = result!.seats![0]!.seat;
|
|
204
|
+
try {
|
|
205
|
+
const answer = await (deps.seatAnswer ?? answerAsSeat)(seat, body);
|
|
206
|
+
if (answer) reply = `${answer.title} 답 (접수번호 ${id})\n${answer.text}`;
|
|
207
|
+
log('seat-answer', { id, seat, answered: !!answer });
|
|
208
|
+
} catch (error) {
|
|
209
|
+
log('seat-answer', { id, seat, answered: false, reason: String(error).slice(0, 200) });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
197
212
|
try {
|
|
198
|
-
const sent = await (deps.sendOutbound ?? sendOutbound)(
|
|
199
|
-
accessiblePwaUrl((deps.pwaUrl ?? chatPwaUrl)()), links), 'report', reportTo);
|
|
213
|
+
const sent = await (deps.sendOutbound ?? sendOutbound)(reply, 'report', reportTo);
|
|
200
214
|
log(sent ? 'delivered' : 'delivery-failed', { id });
|
|
201
215
|
} catch (error) {
|
|
202
216
|
log('delivery-failed', { id, reason: String(error).slice(0, 200) });
|
|
@@ -2,7 +2,8 @@ import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, re
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { debug } from '../../debug/log.js';
|
|
5
|
-
import {
|
|
5
|
+
import { routeOutbound } from '../outbound/router.js';
|
|
6
|
+
import { getUserConfig } from '../../user-config.js';
|
|
6
7
|
import { effectiveInstanceRoot } from '../../instance/resolve.js';
|
|
7
8
|
import { jsonResponse } from './json-response.js';
|
|
8
9
|
|
|
@@ -22,7 +23,10 @@ interface ConsultRequest {
|
|
|
22
23
|
export interface ConsultRequestsDeps {
|
|
23
24
|
root?: () => string;
|
|
24
25
|
now?: () => string;
|
|
25
|
-
|
|
26
|
+
/** Owner alert. Default = the daemon's own in-process router (the same one `/v1/outbound` uses).
|
|
27
|
+
* ⛔ Never `sendOutbound` here: it is a synchronous curl to this very daemon's `/v1/outbound`, so inside the
|
|
28
|
+
* daemon it blocks the event loop until it times out (~25 s · `unreachable`) and the alert is lost (CS1 · 10-01). */
|
|
29
|
+
send?: (text: string, kind: string) => unknown;
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
function file(root: string): string {
|
|
@@ -107,11 +111,18 @@ export async function handleConsultRequests(req: Request, deps: ConsultRequestsD
|
|
|
107
111
|
return jsonResponse({ error: 'journal-unavailable' }, 503);
|
|
108
112
|
}
|
|
109
113
|
debug.log('consult.request', 'accepted', { receiptId: entry.receiptId });
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
);
|
|
115
|
-
} catch { /* the local receipt remains accepted if delivery is unavailable */ }
|
|
114
|
+
const alert = `📮 상담 문의 ${entry.receiptId} · ${entry.name}(${entry.kind === 'personal' ? '개인' : (entry.org ?? '회사')}) · 관심 ${entry.interest}\n앱에서 보기`;
|
|
115
|
+
// Fire and forget — the receipt is already on disk; the answer must not wait for the channel.
|
|
116
|
+
void Promise.resolve()
|
|
117
|
+
.then(() => (deps.send ?? notifyInProcess)(alert, 'alert'))
|
|
118
|
+
.catch((error: unknown) => { debug.log('consult.request', 'notify-failed', { receiptId: entry.receiptId, reason: error instanceof Error ? error.message : String(error) }); });
|
|
116
119
|
return jsonResponse({ receiptId: entry.receiptId, receivedAt: entry.receivedAt }, 202);
|
|
117
120
|
}
|
|
121
|
+
|
|
122
|
+
async function notifyInProcess(text: string, kind: string): Promise<void> {
|
|
123
|
+
const result = await routeOutbound(getUserConfig(), { text, markdown: false, kind });
|
|
124
|
+
debug.log('consult.request', result.delivered ? 'notified' : 'notify-failed', {
|
|
125
|
+
delivered: result.delivered,
|
|
126
|
+
channels: result.channels.map((channel) => `${channel.type}:${channel.ok ? 'ok' : 'fail'}`),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// ER2 — the Primary side of error reports. The bot VM forwards each accepted report here over the tailnet
|
|
2
|
+
// (ingest token). It is stored under <instance>/error-reports/YYYY/MM/DD/<reportId>.json for 30 days, and the
|
|
3
|
+
// operator gets one Telegram line per distinct error per hour — never the message, stack or identity.
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { debug } from '../../debug/log.js';
|
|
7
|
+
import { effectiveInstanceRoot } from '../../instance/resolve.js';
|
|
8
|
+
import type { ReceivedReport } from '../../hooks/error-report.js';
|
|
9
|
+
import { validateErrorReport } from '../../hooks/error-report.js';
|
|
10
|
+
import { jsonResponse } from './json-response.js';
|
|
11
|
+
import { routeOutboundInProcess } from './outbound-report.js';
|
|
12
|
+
|
|
13
|
+
export const ERROR_REPORT_KEEP_DAYS = 30;
|
|
14
|
+
const DAY_MS = 86_400_000;
|
|
15
|
+
|
|
16
|
+
export interface ErrorReportIngestDeps {
|
|
17
|
+
root?: () => string;
|
|
18
|
+
now?: () => number;
|
|
19
|
+
/** Operator alert. Default = the daemon's in-process router (never a self-call · OB8). */
|
|
20
|
+
alert?: (text: string, kind: string) => Promise<boolean>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function storeDir(root: string): string { return join(root, 'error-reports'); }
|
|
24
|
+
|
|
25
|
+
/** Removes day folders older than the keep window. Returns how many days were removed. */
|
|
26
|
+
export function pruneErrorReports(root: string, now: number): number {
|
|
27
|
+
const base = storeDir(root);
|
|
28
|
+
if (!existsSync(base)) return 0;
|
|
29
|
+
let removed = 0;
|
|
30
|
+
for (const year of readdirSync(base).filter((n) => /^\d{4}$/.test(n))) {
|
|
31
|
+
for (const month of readdirSync(join(base, year)).filter((n) => /^\d{2}$/.test(n))) {
|
|
32
|
+
for (const day of readdirSync(join(base, year, month)).filter((n) => /^\d{2}$/.test(n))) {
|
|
33
|
+
const at = Date.parse(`${year}-${month}-${day}T00:00:00Z`);
|
|
34
|
+
if (Number.isFinite(at) && now - at > ERROR_REPORT_KEEP_DAYS * DAY_MS + DAY_MS) {
|
|
35
|
+
rmSync(join(base, year, month, day), { recursive: true, force: true });
|
|
36
|
+
removed++;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return removed;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** One alert per dedupe per hour, remembered on disk so a restart does not repeat it. */
|
|
45
|
+
function shouldAlert(root: string, dedupe: string, now: number): boolean {
|
|
46
|
+
const path = join(storeDir(root), 'alerts.json');
|
|
47
|
+
let seen: Record<string, number> = {};
|
|
48
|
+
try { seen = JSON.parse(readFileSync(path, 'utf8')) as Record<string, number>; } catch { /* first alert */ }
|
|
49
|
+
for (const [k, t] of Object.entries(seen)) if (now - t > DAY_MS) delete seen[k];
|
|
50
|
+
const last = seen[dedupe];
|
|
51
|
+
if (last !== undefined && now - last < 3600_000) return false;
|
|
52
|
+
seen[dedupe] = now;
|
|
53
|
+
mkdirSync(storeDir(root), { recursive: true, mode: 0o700 });
|
|
54
|
+
writeFileSync(path, JSON.stringify(seen), { mode: 0o600 });
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function errorReportAlertText(item: ReceivedReport, count24h: number): string {
|
|
59
|
+
const r = item.report;
|
|
60
|
+
return `🧯 오류 보고 ${r.code} · v${r.app.version} · ${r.app.surface} · 같은 오류 24h ${count24h}회 · ${item.reportId}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Same dedupe stored in the last 24 h, counted from the store itself. */
|
|
64
|
+
function count24h(root: string, dedupe: string, now: number): number {
|
|
65
|
+
let n = 0;
|
|
66
|
+
for (const offset of [0, 1]) {
|
|
67
|
+
const d = new Date(now - offset * DAY_MS);
|
|
68
|
+
const dir = join(storeDir(root), String(d.getUTCFullYear()), String(d.getUTCMonth() + 1).padStart(2, '0'), String(d.getUTCDate()).padStart(2, '0'));
|
|
69
|
+
if (!existsSync(dir)) continue;
|
|
70
|
+
for (const name of readdirSync(dir).filter((f) => f.endsWith('.json'))) {
|
|
71
|
+
try {
|
|
72
|
+
const item = JSON.parse(readFileSync(join(dir, name), 'utf8')) as ReceivedReport;
|
|
73
|
+
if (item.dedupe === dedupe && now - Date.parse(item.receivedAt) < DAY_MS) n++;
|
|
74
|
+
} catch { /* a broken file is not counted */ }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return n;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** POST /v1/reports/ingest — the caller (router) has already checked the ingest token. */
|
|
81
|
+
export async function handleErrorReportIngest(req: Request, deps: ErrorReportIngestDeps = {}): Promise<Response> {
|
|
82
|
+
const root = (deps.root ?? effectiveInstanceRoot)();
|
|
83
|
+
const now = (deps.now ?? Date.now)();
|
|
84
|
+
let body: unknown;
|
|
85
|
+
try { body = await req.json(); } catch { return jsonResponse({ error: 'invalid', field: 'body' }, 400); }
|
|
86
|
+
const item = body as Partial<ReceivedReport>;
|
|
87
|
+
if (typeof item.reportId !== 'string' || !/^er_[0-9a-z]{26}$/.test(item.reportId)) return jsonResponse({ error: 'invalid', field: 'reportId' }, 400);
|
|
88
|
+
if (typeof item.dedupe !== 'string' || !/^[0-9a-f]{12}$/.test(item.dedupe)) return jsonResponse({ error: 'invalid', field: 'dedupe' }, 400);
|
|
89
|
+
if (typeof item.receivedAt !== 'string' || !Number.isFinite(Date.parse(item.receivedAt))) return jsonResponse({ error: 'invalid', field: 'receivedAt' }, 400);
|
|
90
|
+
const checked = validateErrorReport(item.report);
|
|
91
|
+
if (!checked.ok) return jsonResponse({ error: 'invalid', field: `report.${checked.field}` }, 400);
|
|
92
|
+
const stored: ReceivedReport = { reportId: item.reportId, dedupe: item.dedupe, receivedAt: item.receivedAt, report: checked.report };
|
|
93
|
+
const at = new Date(Date.parse(stored.receivedAt));
|
|
94
|
+
const dir = join(storeDir(root), String(at.getUTCFullYear()), String(at.getUTCMonth() + 1).padStart(2, '0'), String(at.getUTCDate()).padStart(2, '0'));
|
|
95
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
96
|
+
writeFileSync(join(dir, `${stored.reportId}.json`), JSON.stringify(stored), { mode: 0o600 });
|
|
97
|
+
let pruned = 0;
|
|
98
|
+
try { pruned = pruneErrorReports(root, now); } catch { /* pruning never blocks a receipt */ }
|
|
99
|
+
const count = count24h(root, stored.dedupe, now);
|
|
100
|
+
const alerted = shouldAlert(root, stored.dedupe, now);
|
|
101
|
+
debug.log('error-report.ingest', 'stored', { code: stored.report.code, surface: stored.report.app.surface, count24h: count, alerted, pruned });
|
|
102
|
+
if (alerted) {
|
|
103
|
+
const alert = deps.alert ?? routeOutboundInProcess;
|
|
104
|
+
void Promise.resolve().then(() => alert(errorReportAlertText(stored, count), 'alert'))
|
|
105
|
+
.catch((error: unknown) => debug.log('error-report.ingest', 'alert-failed', { reason: error instanceof Error ? error.message : String(error) }));
|
|
106
|
+
}
|
|
107
|
+
return jsonResponse({ stored: true, reportId: stored.reportId, count }, 202);
|
|
108
|
+
}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
// Port policy (N1-2 decision): auto-pick from 31415 upward. Actual port
|
|
16
16
|
// is recorded in runtime.json + printed in boot banner.
|
|
17
17
|
|
|
18
|
+
import { setInProcessOutbound } from '../../domains/outbound-alert.js';
|
|
18
19
|
import { getUserConfig } from '../../user-config.js';
|
|
19
20
|
import type { TelegramEvent } from '../../workflow-runtime/triggers/telegram-source.js';
|
|
20
21
|
import { spawnSync } from 'node:child_process';
|
|
@@ -385,6 +386,8 @@ import {
|
|
|
385
386
|
import { handleSessionTurnControl, SESSION_TURN_CONTROL_PATH } from '../../session/session-turn-control.js';
|
|
386
387
|
import { authenticatePodCredential, handlePodGrokCredential, handlePodGithubCredential, POD_CREDENTIAL_GROK_PATH, POD_CREDENTIAL_GITHUB_PATH } from './pod-credential-api.js';
|
|
387
388
|
import { handleOutboundReport } from './outbound-report.js';
|
|
389
|
+
import { handleErrorReportIngest } from './error-reports.js';
|
|
390
|
+
import { routeOutboundInProcess } from './outbound-report.js';
|
|
388
391
|
import { handleSelfEvent } from './self-event.js';
|
|
389
392
|
/** 매매 «실행»은 애드온이다(대표 09-20 결정 ③ · 별도 상용 저장소 · release/trading-export.yaml) — 공개 코어엔 없다.
|
|
390
393
|
* 있으면 그 경로를 켜고, 없으면 501. 경로를 변수로 둬 공개본 타입 검사가 «모듈 없음»으로 막히지 않게 한다. */
|
|
@@ -757,6 +760,8 @@ export function startNexusHttpServer(opts: NexusHttpServerOpts): NexusHttpServer
|
|
|
757
760
|
...(websocketHandler ? { websocket: websocketHandler } : {}),
|
|
758
761
|
} as unknown as Parameters<typeof Bun.serve>[0];
|
|
759
762
|
server = Bun.serve(fetchOpts);
|
|
763
|
+
// OB8 — this process now answers /v1/outbound itself; its own sendOutbound calls must not curl it.
|
|
764
|
+
setInProcessOutbound(routeOutboundInProcess);
|
|
760
765
|
// 데몬이 «실제로» 떴을 때만 재시작 복구(맡긴 일) — 주입된 러너(시험)는 건드리지 않는다.
|
|
761
766
|
if (!opts.execRequests) { try { execRequestRunner().reconcileInterrupted(); } catch { /* 복구 실패가 서버 기동을 막지 않는다 */ } }
|
|
762
767
|
resolvedPort = port;
|
|
@@ -781,6 +786,7 @@ export function startNexusHttpServer(opts: NexusHttpServerOpts): NexusHttpServer
|
|
|
781
786
|
url,
|
|
782
787
|
stop() {
|
|
783
788
|
try { server!.stop(true); } catch { /* idempotent */ }
|
|
789
|
+
setInProcessOutbound(null);
|
|
784
790
|
},
|
|
785
791
|
};
|
|
786
792
|
}
|
|
@@ -876,7 +882,7 @@ export async function routeRequest(
|
|
|
876
882
|
const fallbackAuthorized = fallbackToken !== undefined && offered?.startsWith('Bearer ') === true
|
|
877
883
|
&& compareTokenConstTime(offered.slice('Bearer '.length).trim(), fallbackToken);
|
|
878
884
|
if (!(opts.metaApi ? checkAuth(req, opts.metaApi) : fallbackAuthorized)) {
|
|
879
|
-
const name = method === 'POST' && pathname === '/v1/tasks' && opts.metaApi
|
|
885
|
+
const name = method === 'POST' && (pathname === '/v1/tasks' || pathname === '/v1/reports/ingest') && opts.metaApi
|
|
880
886
|
? matchIngestAuthorization(req)?.name
|
|
881
887
|
: undefined;
|
|
882
888
|
if (name === undefined) {
|
|
@@ -1304,6 +1310,8 @@ export async function routeRequest(
|
|
|
1304
1310
|
if (!opts.metaApi) return jsonResponse({ error: 'meta-api-not-wired' }, 503);
|
|
1305
1311
|
return handleTaskCreatePost(req, opts.metaApi);
|
|
1306
1312
|
}
|
|
1313
|
+
// ER2 — error reports forwarded by the bot VM (ingest token · same as /v1/tasks).
|
|
1314
|
+
if (method === 'POST' && pathname === '/v1/reports/ingest') return handleErrorReportIngest(req);
|
|
1307
1315
|
if (method === 'POST' && pathname.startsWith('/v1/tasks/') && pathname.endsWith('/approve')) {
|
|
1308
1316
|
if (!opts.metaApi) return jsonResponse({ error: 'meta-api-not-wired' }, 503);
|
|
1309
1317
|
let taskId: string;
|
|
@@ -1969,6 +1977,18 @@ export async function routeRequest(
|
|
|
1969
1977
|
}
|
|
1970
1978
|
// HANDOFF §4.2 follow-up — skill-side mirror of the workflow
|
|
1971
1979
|
// router. Same regex → LLM cascade, applied to the skill index.
|
|
1980
|
+
// SK2 (PWA) — skills the index could not read: list ⊕ confirmed repair.
|
|
1981
|
+
if (pathname === '/v1/skills/problems' || pathname === '/v1/skills/problems/repair') {
|
|
1982
|
+
if (!opts.metaApi) return jsonResponse({ error: 'meta-api-not-wired' }, 503);
|
|
1983
|
+
const { handleSkillProblems } = await import('./skill-problems.js');
|
|
1984
|
+
return handleSkillProblems(req, opts.metaApi);
|
|
1985
|
+
}
|
|
1986
|
+
// SC1 (PWA · beta) — «공유용 캡처»: masked screen PNG in, metadata chunks stripped, stored under <configDir>/captures.
|
|
1987
|
+
if (pathname === '/v1/captures' || pathname.startsWith('/v1/captures/')) {
|
|
1988
|
+
if (!opts.metaApi) return jsonResponse({ error: 'meta-api-not-wired' }, 503);
|
|
1989
|
+
const { handleShareCaptures } = await import('./share-captures.js');
|
|
1990
|
+
return handleShareCaptures(req, opts.metaApi);
|
|
1991
|
+
}
|
|
1972
1992
|
if (pathname === '/v1/skills/route' && method === 'POST') {
|
|
1973
1993
|
if (!opts.metaApi) return jsonResponse({ error: 'meta-api-not-wired' }, 503);
|
|
1974
1994
|
return handleSkillRoute(req, opts.metaApi);
|
|
@@ -44,6 +44,13 @@ function json(body: unknown, status: number): Response {
|
|
|
44
44
|
});
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/** The daemon's own sendOutbound path (OB8) — what POST /v1/outbound does, without the HTTP self-call. */
|
|
48
|
+
export async function routeOutboundInProcess(text: string, kind: string): Promise<boolean> {
|
|
49
|
+
const result = await routeOutbound(getUserConfig(), { text, markdown: false, kind });
|
|
50
|
+
if (result.delivered) recordOutboundEvent(text, kind);
|
|
51
|
+
return result.delivered;
|
|
52
|
+
}
|
|
53
|
+
|
|
47
54
|
/** POST /v1/outbound — fan one message out to the routed channels. */
|
|
48
55
|
export async function handleOutboundReport(req: Request, metaApi: MetaApiOpts): Promise<Response> {
|
|
49
56
|
if (!checkAuth(req, metaApi)) return json({ error: 'unauthorized' }, 401);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// The terminals list must answer at once and must never share the event loop with a ledger scan: the running-runs query
|
|
2
|
+
// reads every run ledger synchronously (~5k files · 0.9–1.8 s on 10-02), which froze typing in the web terminal while the
|
|
3
|
+
// PWA polled the list. This keeps the last result as a snapshot and refreshes it in a separate bun process, on demand.
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { debug } from '../../debug/log.js';
|
|
6
|
+
import type { RunningRunsResult } from '../../self-implement/running-runs.js';
|
|
7
|
+
|
|
8
|
+
export const RUNNING_RUNS_REFRESH_MS = 15_000;
|
|
9
|
+
/** A refresh that has not answered by then is abandoned; the next request starts another. */
|
|
10
|
+
export const RUNNING_RUNS_REFRESH_TIMEOUT_MS = 60_000;
|
|
11
|
+
|
|
12
|
+
export interface RunningRunsSnapshot {
|
|
13
|
+
/** Last result, or null before the first refresh finished (subjects then keep unknown assessments). */
|
|
14
|
+
readonly result: RunningRunsResult | null;
|
|
15
|
+
/** Milliseconds since `result` was computed; null when there is none. */
|
|
16
|
+
readonly ageMs: number | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Runs the query somewhere other than this event loop and resolves with its JSON output. */
|
|
20
|
+
export type RunningRunsRunner = (includeTest: boolean) => Promise<RunningRunsResult>;
|
|
21
|
+
|
|
22
|
+
export const RUNNING_RUNS_CHILD_SCRIPT = join(import.meta.dir, 'running-runs-child.ts');
|
|
23
|
+
|
|
24
|
+
/** Default runner: `bun running-runs-child.ts [--include-test]` — the child does the ledger I/O, we only parse its stdout. */
|
|
25
|
+
export const childProcessRunner: RunningRunsRunner = async (includeTest) => {
|
|
26
|
+
const proc = Bun.spawn([process.execPath, RUNNING_RUNS_CHILD_SCRIPT, ...(includeTest ? ['--include-test'] : [])], {
|
|
27
|
+
stdout: 'pipe', stderr: 'ignore', stdin: 'ignore', env: process.env,
|
|
28
|
+
});
|
|
29
|
+
const timer = setTimeout(() => proc.kill(), RUNNING_RUNS_REFRESH_TIMEOUT_MS);
|
|
30
|
+
try {
|
|
31
|
+
const [out, code] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
|
32
|
+
if (code !== 0) throw new Error(`running-runs child exit ${code}`);
|
|
33
|
+
return JSON.parse(out) as RunningRunsResult;
|
|
34
|
+
} finally { clearTimeout(timer); }
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export interface RunningRunsBackground {
|
|
38
|
+
/** Never blocks: returns the last snapshot and starts a refresh when it is stale and none is in flight. */
|
|
39
|
+
snapshot(options: { includeTest?: boolean }): RunningRunsSnapshot;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createRunningRunsBackground(runner: RunningRunsRunner = childProcessRunner, now: () => number = Date.now, refreshMs = RUNNING_RUNS_REFRESH_MS): RunningRunsBackground {
|
|
43
|
+
const slots = new Map<boolean, { at: number; result: RunningRunsResult }>();
|
|
44
|
+
const flights = new Set<boolean>();
|
|
45
|
+
const refresh = (includeTest: boolean): void => {
|
|
46
|
+
if (flights.has(includeTest)) return;
|
|
47
|
+
flights.add(includeTest);
|
|
48
|
+
const started = now();
|
|
49
|
+
void runner(includeTest)
|
|
50
|
+
.then((result) => {
|
|
51
|
+
slots.set(includeTest, { at: now(), result });
|
|
52
|
+
debug.log('nexus.terminals', 'running-runs-refreshed', { includeTest, ms: now() - started });
|
|
53
|
+
})
|
|
54
|
+
.catch((error: unknown) => {
|
|
55
|
+
debug.log('nexus.terminals', 'running-runs-refresh-failed', { includeTest, reason: error instanceof Error ? error.message.slice(0, 80) : 'unknown' }, { level: 'warn' });
|
|
56
|
+
})
|
|
57
|
+
.finally(() => { flights.delete(includeTest); });
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
snapshot(options) {
|
|
61
|
+
const includeTest = options.includeTest === true;
|
|
62
|
+
const slot = slots.get(includeTest);
|
|
63
|
+
const t = now();
|
|
64
|
+
if (!slot || t - slot.at >= refreshMs) refresh(includeTest);
|
|
65
|
+
return slot ? { result: slot.result, ageMs: t - slot.at } : { result: null, ageMs: null };
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// Child of running-runs-background.ts: runs the running-runs query (all the ledger I/O) and prints the result as JSON.
|
|
3
|
+
import { queryRunningRuns } from '../../self-implement/running-runs.js';
|
|
4
|
+
|
|
5
|
+
const result = queryRunningRuns({ includeTest: process.argv.includes('--include-test'), caller: 'nexus.terminals' });
|
|
6
|
+
process.stdout.write(JSON.stringify(result));
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// SC1 — 앱 안 «공유용 캡처»(beta) 받는 쪽. PWA 가 화면을 가린 뒤 PNG 로 올린다(티저·현장·보고용).
|
|
2
|
+
// POST /v1/captures body = image/png(≤ 8 MiB) → { id, bytes, droppedChunks }
|
|
3
|
+
// GET /v1/captures → { items: [{ id, bytes, at }] } (최신 먼저 · 50개)
|
|
4
|
+
// GET /v1/captures/<id> → image/png
|
|
5
|
+
// ⛔ 두 번째 방어: 받은 PNG 에서 «그림 아닌» 덩어리(tEXt·iTXt·zTXt·eXIf·iCCP·tIME 등 메타)를 전부 걷고 저장한다 —
|
|
6
|
+
// 화면 쪽 가림이 놓친 것이 파일 메타로 새지 않게. 남기는 것은 그림을 그리는 데 필요한 덩어리뿐이다.
|
|
7
|
+
import { randomBytes } from 'node:crypto';
|
|
8
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { debug } from '../../debug/log.js';
|
|
11
|
+
import { getElanousConfigDir } from '../../elanous-config-dir.js';
|
|
12
|
+
import { jsonResponse } from './json-response.js';
|
|
13
|
+
import { checkAuth, type MetaApiOpts } from './meta-api.js';
|
|
14
|
+
|
|
15
|
+
export const SHARE_CAPTURES_PATH = '/v1/captures';
|
|
16
|
+
const MAX_BYTES = 8 * 1024 * 1024;
|
|
17
|
+
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
18
|
+
/** Chunks needed to draw the picture. Everything else (text, EXIF, ICC profile, time…) is dropped. */
|
|
19
|
+
const KEEP = new Set(['IHDR', 'PLTE', 'IDAT', 'IEND', 'tRNS', 'gAMA', 'cHRM', 'sRGB', 'pHYs']);
|
|
20
|
+
const ID = /^share-\d{8}-\d{6}-[0-9a-f]{6}$/;
|
|
21
|
+
|
|
22
|
+
export interface ShareCapturesDeps { configDir?: string; now?: () => Date }
|
|
23
|
+
|
|
24
|
+
/** Rebuild a PNG with only drawing chunks. Returns null when it is not a well-formed PNG. */
|
|
25
|
+
export function stripPngMetadata(input: Uint8Array): { png: Buffer; dropped: string[] } | null {
|
|
26
|
+
const buf = Buffer.from(input);
|
|
27
|
+
if (buf.length < 8 + 12 || !buf.subarray(0, 8).equals(PNG_SIGNATURE)) return null;
|
|
28
|
+
const parts: Buffer[] = [PNG_SIGNATURE];
|
|
29
|
+
const dropped: string[] = [];
|
|
30
|
+
let offset = 8;
|
|
31
|
+
let sawHeader = false;
|
|
32
|
+
let sawEnd = false;
|
|
33
|
+
while (offset + 12 <= buf.length) {
|
|
34
|
+
const length = buf.readUInt32BE(offset);
|
|
35
|
+
const type = buf.subarray(offset + 4, offset + 8).toString('latin1');
|
|
36
|
+
const end = offset + 12 + length;
|
|
37
|
+
if (end > buf.length || !/^[A-Za-z]{4}$/.test(type)) return null;
|
|
38
|
+
if (!sawHeader && type !== 'IHDR') return null;
|
|
39
|
+
sawHeader = true;
|
|
40
|
+
if (KEEP.has(type)) parts.push(buf.subarray(offset, end)); else dropped.push(type);
|
|
41
|
+
offset = end;
|
|
42
|
+
if (type === 'IEND') { sawEnd = true; break; }
|
|
43
|
+
}
|
|
44
|
+
if (!sawEnd) return null;
|
|
45
|
+
return { png: Buffer.concat(parts), dropped };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function capturesDir(deps: ShareCapturesDeps): string {
|
|
49
|
+
return join(deps.configDir ?? getElanousConfigDir(), 'captures');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function newId(now: Date): string {
|
|
53
|
+
const p = (n: number) => String(n).padStart(2, '0');
|
|
54
|
+
return `share-${now.getFullYear()}${p(now.getMonth() + 1)}${p(now.getDate())}-${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}-${randomBytes(3).toString('hex')}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function handleShareCaptures(req: Request, metaApi: MetaApiOpts, deps: ShareCapturesDeps = {}): Promise<Response> {
|
|
58
|
+
if (!checkAuth(req, metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
59
|
+
const pathname = new URL(req.url).pathname;
|
|
60
|
+
const dir = capturesDir(deps);
|
|
61
|
+
|
|
62
|
+
if (pathname === SHARE_CAPTURES_PATH && req.method === 'POST') {
|
|
63
|
+
const declared = Number(req.headers.get('content-length') ?? 0);
|
|
64
|
+
if (declared > MAX_BYTES) return jsonResponse({ error: 'too-large', maxBytes: MAX_BYTES }, 413);
|
|
65
|
+
const body = new Uint8Array(await req.arrayBuffer());
|
|
66
|
+
if (body.byteLength > MAX_BYTES) return jsonResponse({ error: 'too-large', maxBytes: MAX_BYTES }, 413);
|
|
67
|
+
const cleaned = stripPngMetadata(body);
|
|
68
|
+
if (!cleaned) return jsonResponse({ error: 'not-png' }, 400);
|
|
69
|
+
const id = newId((deps.now ?? (() => new Date()))());
|
|
70
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
71
|
+
writeFileSync(join(dir, `${id}.png`), cleaned.png, { mode: 0o600, flag: 'wx' });
|
|
72
|
+
debug.log('share-capture', 'stored', { id, bytes: cleaned.png.length, dropped: cleaned.dropped });
|
|
73
|
+
return jsonResponse({ id, bytes: cleaned.png.length, droppedChunks: cleaned.dropped }, 201);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (pathname === SHARE_CAPTURES_PATH && req.method === 'GET') {
|
|
77
|
+
const items = existsSync(dir)
|
|
78
|
+
? readdirSync(dir).filter((f) => f.endsWith('.png') && ID.test(f.slice(0, -4))).sort().reverse().slice(0, 50)
|
|
79
|
+
.map((f) => { const s = statSync(join(dir, f)); return { id: f.slice(0, -4), bytes: s.size, at: s.mtime.toISOString() }; })
|
|
80
|
+
: [];
|
|
81
|
+
return jsonResponse({ items });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const id = pathname.slice(SHARE_CAPTURES_PATH.length + 1);
|
|
85
|
+
if (req.method === 'GET' && ID.test(id)) {
|
|
86
|
+
const file = join(dir, `${id}.png`);
|
|
87
|
+
if (!existsSync(file)) return jsonResponse({ error: 'not-found' }, 404);
|
|
88
|
+
return new Response(readFileSync(file), { headers: { 'content-type': 'image/png', 'cache-control': 'no-store' } });
|
|
89
|
+
}
|
|
90
|
+
return jsonResponse({ error: 'not-found' }, 404);
|
|
91
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// SK2 (PWA) — 색인이 못 읽은 스킬을 PWA 에도 «안내 ⊕ 확인 뒤 수리»로.
|
|
2
|
+
// GET /v1/skills/problems → { items: [{ id, name, code, fixable, hint? }] } — 경로 대신 해시 id 만 내보낸다
|
|
3
|
+
// POST /v1/skills/problems/repair → body { id } · 고칠 수 있는 것만 고친다(남의 도구 파일 — PWA 가 먼저 확인을 받는다)
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { debug } from '../../debug/log.js';
|
|
6
|
+
import { getSkillIndex, skillIndexProblems, type SkillIndexProblem } from '../../skills/index.js';
|
|
7
|
+
import { applySkillRepair, planSkillRepair } from '../../skills/repair.js';
|
|
8
|
+
import { jsonResponse } from './json-response.js';
|
|
9
|
+
import { checkAuth, type MetaApiOpts } from './meta-api.js';
|
|
10
|
+
|
|
11
|
+
export const SKILL_PROBLEMS_PATH = '/v1/skills/problems';
|
|
12
|
+
|
|
13
|
+
export interface SkillProblemsDeps {
|
|
14
|
+
problems?: () => SkillIndexProblem[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function problemId(p: SkillIndexProblem): string {
|
|
18
|
+
return createHash('sha256').update(`${p.dir}\u0000${p.name}`).digest('hex').slice(0, 16);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function current(deps: SkillProblemsDeps): SkillIndexProblem[] {
|
|
22
|
+
if (deps.problems) return deps.problems();
|
|
23
|
+
getSkillIndex();
|
|
24
|
+
return skillIndexProblems();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function handleSkillProblems(req: Request, metaApi: MetaApiOpts, deps: SkillProblemsDeps = {}): Promise<Response> {
|
|
28
|
+
if (!checkAuth(req, metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
29
|
+
const pathname = new URL(req.url).pathname;
|
|
30
|
+
if (pathname === SKILL_PROBLEMS_PATH) {
|
|
31
|
+
if (req.method !== 'GET') return jsonResponse({ error: 'method-not-allowed' }, 405);
|
|
32
|
+
const items = current(deps).map((p) => {
|
|
33
|
+
const plan = planSkillRepair(p);
|
|
34
|
+
return {
|
|
35
|
+
id: problemId(p), name: p.name, code: p.code,
|
|
36
|
+
fixable: plan.kind === 'fixable' || plan.kind === 'fixable-permission',
|
|
37
|
+
...(plan.kind === 'permission' || plan.kind === 'manual' ? { hint: plan.hint.replace(p.dir, '…') } : {}),
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
return jsonResponse({ items });
|
|
41
|
+
}
|
|
42
|
+
if (pathname === `${SKILL_PROBLEMS_PATH}/repair`) {
|
|
43
|
+
if (req.method !== 'POST') return jsonResponse({ error: 'method-not-allowed' }, 405);
|
|
44
|
+
let body: unknown;
|
|
45
|
+
try { body = await req.json(); } catch { return jsonResponse({ error: 'bad_request' }, 400); }
|
|
46
|
+
const id = body && typeof body === 'object' && typeof (body as { id?: unknown }).id === 'string' ? (body as { id: string }).id : '';
|
|
47
|
+
const problem = current(deps).find((p) => problemId(p) === id);
|
|
48
|
+
if (!problem) return jsonResponse({ error: 'not-found' }, 404);
|
|
49
|
+
const plan = planSkillRepair(problem);
|
|
50
|
+
if (plan.kind !== 'fixable' && plan.kind !== 'fixable-permission') return jsonResponse({ error: 'not-fixable', hint: plan.hint.replace(problem.dir, '…') }, 409);
|
|
51
|
+
const result = applySkillRepair(plan);
|
|
52
|
+
debug.log('skills.repair', 'pwa-request', { name: problem.name, ok: result.ok });
|
|
53
|
+
return result.ok ? jsonResponse({ ok: true, name: problem.name }) : jsonResponse({ ok: false, reason: result.reason }, 409);
|
|
54
|
+
}
|
|
55
|
+
return jsonResponse({ error: 'not-found' }, 404);
|
|
56
|
+
}
|
|
@@ -34,6 +34,7 @@ import { classifyPtyOwnerRunUsage, joinPtyLineage, resolveRunTermination, type P
|
|
|
34
34
|
import { loadRunLedger, resolveFederatedRunLedgerDirectories, runLedgerDir } from '../../self-implement/run-ledger.js';
|
|
35
35
|
import { requestRemotePtyControl, type PtyControlAction, type PtyControlPayload, type PtyControlRequestOptions, type PtyControlResult } from '../../pty-shell/pty-control-ipc.js';
|
|
36
36
|
import { encodeSgrMouse, type PtyMouseInput } from '../../pty-shell/pty-mouse.js';
|
|
37
|
+
import { createRunningRunsBackground, type RunningRunsBackground } from './running-runs-background.js';
|
|
37
38
|
import { queryRunningRuns, type RunningRunAssessment, type RunningRunPresence, type RunningRunStatus, type RunningRunsResult } from '../../self-implement/running-runs.js';
|
|
38
39
|
// ⭐P4 §4-1 — 표시 폭(wide-char/CJK/emoji 인지) SSOT. PNG 캔버스 cols 파생용.
|
|
39
40
|
import { cellWidth } from '../../ui/printer.js';
|
|
@@ -494,6 +495,12 @@ function runningRunsCacheFor(
|
|
|
494
495
|
return binding.cache;
|
|
495
496
|
}
|
|
496
497
|
|
|
498
|
+
let liveBackground: RunningRunsBackground | null = null;
|
|
499
|
+
function liveRunningRunsBackground(): RunningRunsBackground {
|
|
500
|
+
liveBackground ??= createRunningRunsBackground();
|
|
501
|
+
return liveBackground;
|
|
502
|
+
}
|
|
503
|
+
|
|
497
504
|
const liveTerminalsListDeps: TerminalsListDeps = {
|
|
498
505
|
ptyManifestTargets,
|
|
499
506
|
listPtyManifestRows,
|
|
@@ -1054,17 +1061,26 @@ export function handleTerminalsList(req: Request, opts: MetaApiOpts, deps: Termi
|
|
|
1054
1061
|
const resolveRunTerminationForRow = (runId: string): RunTermination => deps.runTerminated
|
|
1055
1062
|
? deps.runTerminated(runId, { includeTest })
|
|
1056
1063
|
: 'ledger-indeterminate';
|
|
1057
|
-
const runningRunsCache = runningRunsCacheFor(getRunningRuns, deps.now ?? Date.now);
|
|
1058
1064
|
let runningRuns: RunningRunsResult | null = null;
|
|
1059
1065
|
let runningRunsAgeMs = 0;
|
|
1060
1066
|
let runningRunsQueryFailed = false;
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1067
|
+
if (getRunningRuns === queryRunningRuns) {
|
|
1068
|
+
// Live daemon: never scan ledgers on this event loop (it froze web-terminal typing) — use the last snapshot a
|
|
1069
|
+
// separate process refreshed. Before the first one lands, subjects keep unknown assessments and the age is null.
|
|
1070
|
+
const snap = liveRunningRunsBackground().snapshot({ includeTest });
|
|
1071
|
+
runningRuns = snap.result;
|
|
1072
|
+
if (snap.ageMs === null) runningRunsQueryFailed = true;
|
|
1073
|
+
else runningRunsAgeMs = snap.ageMs;
|
|
1074
|
+
} else {
|
|
1075
|
+
const runningRunsCache = runningRunsCacheFor(getRunningRuns, deps.now ?? Date.now);
|
|
1076
|
+
try {
|
|
1077
|
+
const cached = runningRunsCache({ includeTest });
|
|
1078
|
+
runningRuns = cached.result;
|
|
1079
|
+
runningRunsAgeMs = cached.ageMs;
|
|
1080
|
+
} catch {
|
|
1081
|
+
// Fail-soft and do not cache: subjects keep unknown assessments.
|
|
1082
|
+
runningRunsQueryFailed = true;
|
|
1083
|
+
}
|
|
1068
1084
|
}
|
|
1069
1085
|
const ptyLessSubAgentCollection = collectPtyLessSubAgents(
|
|
1070
1086
|
getLogStore,
|
package/src/nexus/index.ts
CHANGED
|
@@ -2609,10 +2609,16 @@ export async function runNexus(opts: RunNexusOptions = {}): Promise<RunNexusHand
|
|
|
2609
2609
|
runTurnImpl,
|
|
2610
2610
|
getBot: () => getBotRef.current,
|
|
2611
2611
|
});
|
|
2612
|
+
// DEC-TG(대표 10-02) — decision cards in the owner's Discord DM. Filled once the bot exists (below).
|
|
2613
|
+
let decisionWire: import('../decisions/discord-decision-cards.js').DiscordDecisionWire | null = null;
|
|
2612
2614
|
const composedOnMessage = async (
|
|
2613
2615
|
ctx: import('../discord.js').DcIncoming,
|
|
2614
2616
|
streamer?: import('../discord.js').DcMessageStreamer,
|
|
2615
2617
|
): Promise<string | void> => {
|
|
2618
|
+
if (decisionWire && ctx.text.trim() === '/decisions') {
|
|
2619
|
+
const { discordDecisionsCommand } = await import('../decisions/discord-decision-cards.js');
|
|
2620
|
+
return discordDecisionsCommand(decisionWire.service, ctx);
|
|
2621
|
+
}
|
|
2616
2622
|
const voiceReply = await voiceWire.dispatchVoiceCommand(ctx);
|
|
2617
2623
|
if (voiceReply !== null) return voiceReply;
|
|
2618
2624
|
return selfOnMessage(ctx, streamer as never);
|
|
@@ -2635,7 +2641,8 @@ export async function runNexus(opts: RunNexusOptions = {}): Promise<RunNexusHand
|
|
|
2635
2641
|
dispatch: (event) => workflowDaemon!.dispatchDiscord(event),
|
|
2636
2642
|
onMessage: composedOnMessage,
|
|
2637
2643
|
onInteraction: async (raw) => {
|
|
2638
|
-
// C1 버튼 탭
|
|
2644
|
+
// DEC-TG 결정 카드(dec:/decmemo:) → C1 버튼 탭(elanous-q: 소비) → 아니면 C3 슬래시.
|
|
2645
|
+
if (decisionWire && await decisionWire.onInteraction(raw)) return;
|
|
2639
2646
|
if (await questionRuntime.handleComponentInteraction(raw)) return;
|
|
2640
2647
|
await slashWire.onInteraction(raw);
|
|
2641
2648
|
},
|
|
@@ -2644,6 +2651,14 @@ export async function runNexus(opts: RunNexusOptions = {}): Promise<RunNexusHand
|
|
|
2644
2651
|
if (handle) {
|
|
2645
2652
|
getBotRef.current = handle.bot;
|
|
2646
2653
|
workflowDiscordBot = handle;
|
|
2654
|
+
if ((cfg.raw?.decisions as { cards?: unknown } | undefined)?.cards !== false) {
|
|
2655
|
+
try {
|
|
2656
|
+
const { attachDiscordDecisionCards } = await import('../decisions/discord-decision-cards.js');
|
|
2657
|
+
decisionWire = attachDiscordDecisionCards(handle.bot, cfg);
|
|
2658
|
+
} catch (err) {
|
|
2659
|
+
console.warn(`[nexus] decision cards (discord) not attached: ${err instanceof Error ? err.message : String(err)}`);
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2647
2662
|
void slashWire.registerCommands().catch((err: unknown) => {
|
|
2648
2663
|
console.warn(`[nexus] slash registration failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2649
2664
|
});
|
|
@@ -2919,6 +2934,15 @@ export async function runNexus(opts: RunNexusOptions = {}): Promise<RunNexusHand
|
|
|
2919
2934
|
: readyCount === 1 ? readyServerIds[0] : undefined,
|
|
2920
2935
|
readyCount,
|
|
2921
2936
|
});
|
|
2937
|
+
// TERM1 ④ — the first web-terminal spawn used to pay the login-shell env capture (spawnSync · ~3 s with the
|
|
2938
|
+
// daemon frozen) and the terminal module import (~0.9 s). Warm both off the event loop shortly after boot.
|
|
2939
|
+
if (!opts.startNexusHttpServerFn) {
|
|
2940
|
+
const warm = setTimeout(() => {
|
|
2941
|
+
void import('../shell-env-bootstrap.js').then(({ warmCapturedEnv }) => warmCapturedEnv()).catch(() => undefined);
|
|
2942
|
+
void import('../web-terminal/preview-tap-registry.js').then(() => import('../preview/terminal.js')).catch(() => undefined);
|
|
2943
|
+
}, 1_500);
|
|
2944
|
+
(warm as { unref?: () => void }).unref?.();
|
|
2945
|
+
}
|
|
2922
2946
|
httpServer = startHttpServer({
|
|
2923
2947
|
state,
|
|
2924
2948
|
registry,
|