autonomous-flow-daemon 1.6.0 → 1.9.0
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/CHANGELOG.md +85 -85
- package/LICENSE +21 -21
- package/README-ko.md +282 -0
- package/README.md +282 -266
- package/mcp-config.json +10 -10
- package/package.json +4 -2
- package/src/adapters/index.ts +370 -370
- package/src/cli.ts +162 -127
- package/src/commands/benchmark.ts +187 -187
- package/src/commands/correlate.ts +180 -0
- package/src/commands/dashboard.ts +404 -0
- package/src/commands/evolution.ts +84 -1
- package/src/commands/fix.ts +158 -158
- package/src/commands/lang.ts +41 -41
- package/src/commands/plugin.ts +110 -0
- package/src/commands/restart.ts +14 -14
- package/src/commands/score.ts +276 -276
- package/src/commands/start.ts +155 -155
- package/src/commands/status.ts +157 -157
- package/src/commands/stop.ts +68 -68
- package/src/commands/suggest.ts +211 -0
- package/src/commands/sync.ts +329 -16
- package/src/constants.ts +32 -32
- package/src/core/boast.ts +280 -280
- package/src/core/config.ts +49 -49
- package/src/core/correlation-engine.ts +265 -0
- package/src/core/db.ts +145 -117
- package/src/core/discovery.ts +65 -65
- package/src/core/federation.ts +129 -0
- package/src/core/hologram/engine.ts +71 -71
- package/src/core/hologram/fallback.ts +11 -11
- package/src/core/hologram/go-extractor.ts +203 -0
- package/src/core/hologram/incremental.ts +227 -227
- package/src/core/hologram/py-extractor.ts +132 -132
- package/src/core/hologram/rust-extractor.ts +244 -0
- package/src/core/hologram/ts-extractor.ts +406 -320
- package/src/core/hologram/types.ts +27 -25
- package/src/core/hologram.ts +73 -71
- package/src/core/i18n/messages.ts +309 -309
- package/src/core/locale.ts +88 -88
- package/src/core/log-rotate.ts +33 -33
- package/src/core/log-utils.ts +38 -38
- package/src/core/lru-map.ts +61 -61
- package/src/core/notify.ts +74 -74
- package/src/core/plugin-manager.ts +225 -0
- package/src/core/rule-suggestion.ts +127 -0
- package/src/core/validator-generator.ts +224 -0
- package/src/core/workspace.ts +28 -28
- package/src/daemon/client.ts +78 -65
- package/src/daemon/event-batcher.ts +108 -108
- package/src/daemon/guards.ts +13 -13
- package/src/daemon/http-routes.ts +376 -293
- package/src/daemon/mcp-handler.ts +575 -270
- package/src/daemon/mcp-subscriptions.ts +81 -0
- package/src/daemon/mesh.ts +51 -0
- package/src/daemon/server.ts +655 -590
- package/src/daemon/types.ts +121 -100
- package/src/daemon/workspace-map.ts +104 -92
- package/src/platform.ts +60 -60
- package/src/version.ts +15 -15
- package/README.ko.md +0 -266
|
@@ -1,293 +1,376 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HTTP routes for daemon IPC — extracted from server.ts.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { readFileSync, writeFileSync } from "fs";
|
|
6
|
-
import { resolve } from "path";
|
|
7
|
-
import { generateHologram } from "../core/hologram";
|
|
8
|
-
import { diagnose } from "../core/immune";
|
|
9
|
-
import type { PatchOp } from "../core/immune";
|
|
10
|
-
import { buildShiftSummary } from "../core/boast";
|
|
11
|
-
import { analyzeQuarantine, listQuarantine, evolve } from "../core/evolution";
|
|
12
|
-
import { MAX_SSE_CLIENTS } from "./types";
|
|
13
|
-
import
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (url.pathname === "/
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
1
|
+
/**
|
|
2
|
+
* HTTP routes for daemon IPC — extracted from server.ts.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
6
|
+
import { resolve } from "path";
|
|
7
|
+
import { generateHologram } from "../core/hologram";
|
|
8
|
+
import { diagnose } from "../core/immune";
|
|
9
|
+
import type { PatchOp } from "../core/immune";
|
|
10
|
+
import { buildShiftSummary } from "../core/boast";
|
|
11
|
+
import { analyzeQuarantine, listQuarantine, evolve } from "../core/evolution";
|
|
12
|
+
import { MAX_SSE_CLIENTS } from "./types";
|
|
13
|
+
import { subscriptionManager } from "./mcp-subscriptions";
|
|
14
|
+
import type { DaemonContext } from "./types";
|
|
15
|
+
import { assertInsideWorkspace as _assertWs } from "./guards";
|
|
16
|
+
import { shouldAcceptRemote } from "../core/federation";
|
|
17
|
+
import { listMeshPeers } from "./mesh";
|
|
18
|
+
|
|
19
|
+
/** Create the HTTP fetch handler for Bun.serve */
|
|
20
|
+
export function createHttpHandler(ctx: DaemonContext, cleanup: () => void) {
|
|
21
|
+
return async function fetch(req: Request): Promise<Response> {
|
|
22
|
+
const url = new URL(req.url);
|
|
23
|
+
|
|
24
|
+
if (url.pathname === "/health") {
|
|
25
|
+
return Response.json({ status: "alive", pid: process.pid, workspace: ctx.ws.root, port: ctx.port });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (url.pathname === "/mini-status") {
|
|
29
|
+
const last = ctx.state.autoHealLog.length > 0
|
|
30
|
+
? ctx.state.autoHealLog[ctx.state.autoHealLog.length - 1].id
|
|
31
|
+
: null;
|
|
32
|
+
// Defense reasons from in-memory mistakeCache (not DB query — stays under 200ms)
|
|
33
|
+
const reasonSet = new Set<string>();
|
|
34
|
+
for (const entries of ctx.state.mistakeCache.values()) {
|
|
35
|
+
for (const e of entries) {
|
|
36
|
+
reasonSet.add(e.mistake_type);
|
|
37
|
+
if (reasonSet.size >= 3) break;
|
|
38
|
+
}
|
|
39
|
+
if (reasonSet.size >= 3) break;
|
|
40
|
+
}
|
|
41
|
+
const latestLog = ctx.state.autoHealLog.length > 0
|
|
42
|
+
? ctx.state.autoHealLog[ctx.state.autoHealLog.length - 1]
|
|
43
|
+
: null;
|
|
44
|
+
const latestDefense = latestLog
|
|
45
|
+
? { file: latestLog.file, healMs: latestLog.healMs, at: latestLog.at }
|
|
46
|
+
: null;
|
|
47
|
+
// session_saved_tokens_k: DB의 오늘치 stats 사용
|
|
48
|
+
// (MCP 프로세스와 HTTP 데몬이 별개 프로세스라 in-memory sessionOriginalChars는 항상 0)
|
|
49
|
+
const todayStr = ctx.today();
|
|
50
|
+
const dailyRows = ctx.getDailyAll.all();
|
|
51
|
+
const todayRow = dailyRows.find(r => r.date === todayStr);
|
|
52
|
+
const sessionSavedTokensK = todayRow
|
|
53
|
+
? Math.round(Math.max(0, todayRow.original_chars - todayRow.hologram_chars) / 4 / 100) / 10
|
|
54
|
+
: 0;
|
|
55
|
+
return Response.json({
|
|
56
|
+
status: "ON",
|
|
57
|
+
healed_count: ctx.state.autoHealCount,
|
|
58
|
+
last_healed: last,
|
|
59
|
+
total_defenses: ctx.state.autoHealCount,
|
|
60
|
+
defense_reasons: [...reasonSet],
|
|
61
|
+
latest_defense: latestDefense,
|
|
62
|
+
saved_tokens_k: Math.round(Math.max(0, ctx.state.hologramStats.totalOriginalChars - ctx.state.hologramStats.totalHologramChars) / 4 / 100) / 10,
|
|
63
|
+
session_saved_tokens_k: sessionSavedTokensK,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Track HTTP API calls as telemetry
|
|
68
|
+
const _apiPath = url.pathname.replace(/^\//, "");
|
|
69
|
+
if (["/hologram", "/read", "/diagnose", "/score", "/evolution", "/sync"].includes(url.pathname)) {
|
|
70
|
+
try { ctx.insertTelemetry.run("mcp", `http_${_apiPath}`, null, null, Date.now()); } catch { /* crash-only */ }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (url.pathname === "/hologram") {
|
|
74
|
+
const file = url.searchParams.get("file");
|
|
75
|
+
if (!file) return Response.json({ error: "?file= required" }, { status: 400 });
|
|
76
|
+
try {
|
|
77
|
+
const absPath = resolve(file);
|
|
78
|
+
_assertWs(absPath, ctx.ws.root);
|
|
79
|
+
const source = readFileSync(absPath, "utf-8");
|
|
80
|
+
const contextFile = url.searchParams.get("contextFile");
|
|
81
|
+
const result = await generateHologram(file, source, contextFile ? { contextFile: resolve(contextFile) } : undefined);
|
|
82
|
+
ctx.persistHologramStats(result.originalLength, result.hologramLength);
|
|
83
|
+
return Response.json(result);
|
|
84
|
+
} catch (err: unknown) {
|
|
85
|
+
return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 404 });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (url.pathname === "/workspace-map") {
|
|
90
|
+
const mapText = ctx.getWorkspaceMap();
|
|
91
|
+
const { totalProjectBytes, mapBytes } = ctx.getWorkspaceMapStats();
|
|
92
|
+
if (totalProjectBytes > 0) {
|
|
93
|
+
ctx.persistCtxSavings('wsmap', totalProjectBytes, Math.max(0, totalProjectBytes - mapBytes));
|
|
94
|
+
}
|
|
95
|
+
return new Response(mapText, { headers: { "Content-Type": "text/plain" } });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (url.pathname === "/read") {
|
|
99
|
+
const file = url.searchParams.get("file");
|
|
100
|
+
if (!file) return Response.json({ error: "?file= required" }, { status: 400 });
|
|
101
|
+
try {
|
|
102
|
+
const AFD_READ_THRESHOLD = 10 * 1024;
|
|
103
|
+
const absPath = resolve(file);
|
|
104
|
+
_assertWs(absPath, ctx.ws.root);
|
|
105
|
+
const source = readFileSync(absPath, "utf-8");
|
|
106
|
+
const rawStart = parseInt(url.searchParams.get("startLine") ?? "", 10);
|
|
107
|
+
const rawEnd = parseInt(url.searchParams.get("endLine") ?? "", 10);
|
|
108
|
+
|
|
109
|
+
if (Number.isFinite(rawStart) && Number.isFinite(rawEnd)) {
|
|
110
|
+
const lines = source.split("\n");
|
|
111
|
+
const s = Math.max(1, rawStart) - 1;
|
|
112
|
+
const e = Math.min(lines.length, rawEnd);
|
|
113
|
+
return Response.json({ file, lines: lines.slice(s, e), range: [s + 1, e], totalLines: lines.length });
|
|
114
|
+
}
|
|
115
|
+
if (source.length < AFD_READ_THRESHOLD) {
|
|
116
|
+
return Response.json({ file, content: source, mode: "full" });
|
|
117
|
+
}
|
|
118
|
+
const result = await generateHologram(file, source);
|
|
119
|
+
ctx.persistHologramStats(result.originalLength, result.hologramLength);
|
|
120
|
+
return Response.json({ file, hologram: result.hologram, mode: "hologram", originalSize: source.length, totalLines: source.split("\n").length });
|
|
121
|
+
} catch (err: unknown) {
|
|
122
|
+
return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 404 });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (url.pathname === "/mistake-history") {
|
|
127
|
+
const file = url.searchParams.get("file");
|
|
128
|
+
if (!file) return Response.json({ error: "?file= required" }, { status: 400 });
|
|
129
|
+
const normalizedFile = file.replace(/\\/g, "/");
|
|
130
|
+
const cached = ctx.state.mistakeCache.get(normalizedFile);
|
|
131
|
+
return Response.json({ mistakes: cached ?? [] });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (url.pathname === "/diagnose") {
|
|
135
|
+
const raw = url.searchParams.get("raw") === "true";
|
|
136
|
+
const known = ctx.antibodyIds.all().map(r => r.id);
|
|
137
|
+
const result = diagnose(known, { raw });
|
|
138
|
+
const PROACTIVE_HOLOGRAM_THRESHOLD = 5 * 1024;
|
|
139
|
+
|
|
140
|
+
const enriched = await Promise.all(result.symptoms.map(async (s: { fileTarget: string; [k: string]: unknown }) => {
|
|
141
|
+
const snapshot = ctx.state.fileSnapshots.get(s.fileTarget)
|
|
142
|
+
?? ctx.state.fileSnapshots.get(s.fileTarget.replace(/\//g, "\\"));
|
|
143
|
+
if (!snapshot) return s;
|
|
144
|
+
if (snapshot.length > PROACTIVE_HOLOGRAM_THRESHOLD) {
|
|
145
|
+
const hologram = await ctx.safeHologram(s.fileTarget, snapshot);
|
|
146
|
+
return {
|
|
147
|
+
...s, hologram,
|
|
148
|
+
contextNote: `File is ${(snapshot.length / 1024).toFixed(1)}KB — hologram skeleton provided to save tokens (${Math.round((1 - hologram.length / snapshot.length) * 100)}% reduction).`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return { ...s, context: snapshot };
|
|
152
|
+
}));
|
|
153
|
+
return Response.json({ ...result, symptoms: enriched });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (url.pathname === "/antibodies") {
|
|
157
|
+
return Response.json({ antibodies: ctx.listAntibodies.all() });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (url.pathname === "/antibodies/learn" && req.method === "POST") {
|
|
161
|
+
try {
|
|
162
|
+
const body = await req.json() as {
|
|
163
|
+
id: string; patternType: string; fileTarget: string; patches: PatchOp[];
|
|
164
|
+
scope?: string; version?: number; updatedAt?: string;
|
|
165
|
+
};
|
|
166
|
+
const scope = body.scope ?? "local";
|
|
167
|
+
const incomingVersion = body.version ?? 1;
|
|
168
|
+
const updatedAt = body.updatedAt ?? new Date().toISOString();
|
|
169
|
+
// Non-local antibodies are stored under their fqid to avoid collisions
|
|
170
|
+
const storageId = scope === "local" ? body.id : `${scope}/${body.id}`;
|
|
171
|
+
|
|
172
|
+
const incomingPatch = JSON.stringify(body.patches);
|
|
173
|
+
|
|
174
|
+
type ExistingRow = { ab_version: number; updated_at: string; patch_op: string } | null;
|
|
175
|
+
const existing = ctx.db.prepare(
|
|
176
|
+
"SELECT ab_version, updated_at, patch_op FROM antibodies WHERE id = ?"
|
|
177
|
+
).get(storageId) as ExistingRow;
|
|
178
|
+
|
|
179
|
+
if (existing) {
|
|
180
|
+
const decision = shouldAcceptRemote(
|
|
181
|
+
{ version: incomingVersion, updatedAt, patch: incomingPatch },
|
|
182
|
+
{ version: existing.ab_version, updatedAt: existing.updated_at, patch: existing.patch_op },
|
|
183
|
+
);
|
|
184
|
+
if (!decision.accept) {
|
|
185
|
+
return Response.json({ status: "skipped", reason: decision.reason, id: storageId });
|
|
186
|
+
}
|
|
187
|
+
ctx.db.prepare(
|
|
188
|
+
"UPDATE antibodies SET patch_op = ?, file_target = ?, ab_version = ?, updated_at = ?, scope = ? WHERE id = ?"
|
|
189
|
+
).run(incomingPatch, body.fileTarget, incomingVersion, updatedAt, scope, storageId);
|
|
190
|
+
return Response.json({ status: "updated", reason: decision.reason, id: storageId });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
ctx.insertAntibody.run(storageId, body.patternType, body.fileTarget, JSON.stringify(body.patches));
|
|
194
|
+
subscriptionManager.dispatchResourceUpdated("afd://antibodies");
|
|
195
|
+
ctx.db.prepare(
|
|
196
|
+
"UPDATE antibodies SET scope = ?, ab_version = ?, updated_at = ? WHERE id = ?"
|
|
197
|
+
).run(scope, incomingVersion, updatedAt, storageId);
|
|
198
|
+
return Response.json({ status: "learned", id: storageId });
|
|
199
|
+
} catch (err: unknown) {
|
|
200
|
+
return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 400 });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (url.pathname === "/auto-heal/record" && req.method === "POST") {
|
|
205
|
+
try {
|
|
206
|
+
const body = await req.json() as { id: string; file?: string; healMs?: number };
|
|
207
|
+
ctx.state.autoHealCount++;
|
|
208
|
+
ctx.state.autoHealLog.push({ id: body.id, at: Date.now(), file: body.file ?? body.id, healMs: body.healMs ?? 0 });
|
|
209
|
+
if (ctx.state.autoHealLog.length > 100) ctx.state.autoHealLog.shift();
|
|
210
|
+
try { ctx.insertTelemetry.run("immune", "heal_hit", JSON.stringify({ antibodyId: body.id }), null, Date.now()); } catch { /* crash-only */ }
|
|
211
|
+
return Response.json({ status: "recorded", total: ctx.state.autoHealCount });
|
|
212
|
+
} catch (err: unknown) {
|
|
213
|
+
return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 400 });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (url.pathname === "/score") {
|
|
218
|
+
const uptime = Math.floor((Date.now() - ctx.state.startedAt) / 1000);
|
|
219
|
+
const eventCount = ctx.db.query("SELECT COUNT(*) as cnt FROM events").get() as { cnt: number };
|
|
220
|
+
const abCount = ctx.countAntibodies.get() as { cnt: number };
|
|
221
|
+
const hs = ctx.state.hologramStats;
|
|
222
|
+
const globalSavings = hs.totalOriginalChars > 0
|
|
223
|
+
? Math.round((hs.totalOriginalChars - hs.totalHologramChars) / hs.totalOriginalChars * 1000) / 10
|
|
224
|
+
: 0;
|
|
225
|
+
const dailyRows = ctx.getDailyAll.all() as { date: string; requests: number; original_chars: number; hologram_chars: number }[];
|
|
226
|
+
const todayRow = dailyRows.find(r => r.date === ctx.today());
|
|
227
|
+
const ctxDailyRaw = ctx.getCtxSavingsDaily.all() as { date: string; type: string; requests: number; original_chars: number; saved_chars: number }[];
|
|
228
|
+
const ctxLifetimeRaw = ctx.getCtxSavingsLifetime.all() as { type: string; total_requests: number; total_original_chars: number; total_saved_chars: number }[];
|
|
229
|
+
return Response.json({
|
|
230
|
+
uptime,
|
|
231
|
+
filesDetected: ctx.state.filesDetected,
|
|
232
|
+
totalEvents: eventCount.cnt,
|
|
233
|
+
lastEvent: ctx.state.lastEvent,
|
|
234
|
+
lastEventAt: ctx.state.lastEventAt,
|
|
235
|
+
watchedFiles: [...ctx.state.watchedFiles],
|
|
236
|
+
watchTargets: ctx.discoveryTargets,
|
|
237
|
+
hologram: {
|
|
238
|
+
lifetime: { requests: hs.totalRequests, originalChars: hs.totalOriginalChars, hologramChars: hs.totalHologramChars, savings: globalSavings },
|
|
239
|
+
today: todayRow ? {
|
|
240
|
+
requests: todayRow.requests, originalChars: todayRow.original_chars, hologramChars: todayRow.hologram_chars,
|
|
241
|
+
savings: todayRow.original_chars > 0 ? Math.round((todayRow.original_chars - todayRow.hologram_chars) / todayRow.original_chars * 1000) / 10 : 0,
|
|
242
|
+
} : null,
|
|
243
|
+
daily: dailyRows.map(r => ({ date: r.date, requests: r.requests, originalChars: r.original_chars, hologramChars: r.hologram_chars })),
|
|
244
|
+
},
|
|
245
|
+
ctxSavings: {
|
|
246
|
+
daily: ctxDailyRaw,
|
|
247
|
+
lifetime: ctxLifetimeRaw,
|
|
248
|
+
},
|
|
249
|
+
immune: {
|
|
250
|
+
antibodies: abCount.cnt,
|
|
251
|
+
autoHealed: ctx.state.autoHealCount,
|
|
252
|
+
lastAutoHeal: ctx.state.autoHealLog.length > 0 ? ctx.state.autoHealLog[ctx.state.autoHealLog.length - 1] : null,
|
|
253
|
+
},
|
|
254
|
+
ecosystem: {
|
|
255
|
+
detected: ctx.state.ecosystems.map(e => ({ name: e.adapter.name, confidence: e.confidence, schema: e.adapter.getHarnessSchema() })),
|
|
256
|
+
primary: ctx.state.ecosystems[0]?.adapter.name ?? "Unknown",
|
|
257
|
+
},
|
|
258
|
+
suppression: {
|
|
259
|
+
massEventsSkipped: ctx.state.suppressionSkippedCount,
|
|
260
|
+
dormantTransitions: ctx.state.dormantTransitions.length,
|
|
261
|
+
activeTaps: ctx.state.firstTapTimestamps.size,
|
|
262
|
+
},
|
|
263
|
+
evolution: (() => {
|
|
264
|
+
const q = listQuarantine();
|
|
265
|
+
return { totalQuarantined: q.length, totalLearned: q.filter(e => e.learned).length, pending: q.filter(e => !e.learned).length };
|
|
266
|
+
})(),
|
|
267
|
+
dynamicImmune: {
|
|
268
|
+
activeValidators: ctx.state.customValidators.size,
|
|
269
|
+
validatorNames: [...ctx.state.customValidators.keys()],
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (url.pathname === "/evolution") {
|
|
275
|
+
return Response.json(evolve());
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (url.pathname === "/evolution/status") {
|
|
279
|
+
const q = listQuarantine();
|
|
280
|
+
const stats = analyzeQuarantine();
|
|
281
|
+
return Response.json({
|
|
282
|
+
totalQuarantined: q.length, totalLearned: q.filter(e => e.learned).length, pending: stats.pending,
|
|
283
|
+
lessons: stats.lessons.map(l => ({ file: l.entry.originalPath, type: l.failureType, timestamp: l.entry.timestamp, suggestion: l.suggestion })),
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (url.pathname === "/sync") {
|
|
288
|
+
type AntibodyRow = { id: string; pattern_type: string; file_target: string; patch_op: string; created_at: string; scope?: string; ab_version?: number; updated_at?: string };
|
|
289
|
+
const rows = ctx.listAntibodies.all() as AntibodyRow[];
|
|
290
|
+
const sanitized = rows.flatMap(r => {
|
|
291
|
+
let patches: PatchOp[];
|
|
292
|
+
try { patches = JSON.parse(r.patch_op) as PatchOp[]; } catch { return []; }
|
|
293
|
+
const cleanPatches = patches.map(p => ({
|
|
294
|
+
...p,
|
|
295
|
+
path: p.path.replace(/^[A-Za-z]:/, "").replace(/\\/g, "/"),
|
|
296
|
+
value: p.value?.replace(/[A-Za-z]:\\[^\s"']*/g, "<redacted>"),
|
|
297
|
+
}));
|
|
298
|
+
const scope = r.scope ?? "local";
|
|
299
|
+
const cleanId = r.id.replace(/^[A-Za-z]:/, "").replace(/\\/g, "/");
|
|
300
|
+
// fqid: non-local ids are already stored as "<scope>/<name>", local use "local/<id>"
|
|
301
|
+
const fqid = scope !== "local" ? cleanId : `local/${cleanId}`;
|
|
302
|
+
return [{
|
|
303
|
+
id: cleanId,
|
|
304
|
+
scope,
|
|
305
|
+
fqid,
|
|
306
|
+
patternType: r.pattern_type,
|
|
307
|
+
fileTarget: r.file_target.replace(/^[A-Za-z]:/, "").replace(/\\/g, "/"),
|
|
308
|
+
patches: cleanPatches,
|
|
309
|
+
version: r.ab_version ?? 1,
|
|
310
|
+
updatedAt: r.updated_at ?? r.created_at,
|
|
311
|
+
learnedAt: r.created_at,
|
|
312
|
+
}];
|
|
313
|
+
});
|
|
314
|
+
const payload = {
|
|
315
|
+
version: "1.7", generatedAt: new Date().toISOString(),
|
|
316
|
+
ecosystem: ctx.state.ecosystems[0]?.adapter.name ?? "Unknown",
|
|
317
|
+
scope: "local", // publisher scope — overridden by CLI syncRemotePush
|
|
318
|
+
antibodyCount: sanitized.length, antibodies: sanitized,
|
|
319
|
+
};
|
|
320
|
+
const payloadPath = resolve(ctx.ws.afdDir, "global-vaccine-payload.json");
|
|
321
|
+
writeFileSync(payloadPath, JSON.stringify(payload, null, 2), "utf-8");
|
|
322
|
+
return Response.json({ status: "exported", path: payloadPath, count: sanitized.length });
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (url.pathname === "/shift-summary") {
|
|
326
|
+
const uptime = Math.floor((Date.now() - ctx.state.startedAt) / 1000);
|
|
327
|
+
const eventCount = ctx.db.query("SELECT COUNT(*) as cnt FROM events").get() as { cnt: number };
|
|
328
|
+
const hs = ctx.state.hologramStats;
|
|
329
|
+
const hologramSavedChars = Math.max(0, hs.totalOriginalChars - hs.totalHologramChars);
|
|
330
|
+
return Response.json(buildShiftSummary({
|
|
331
|
+
uptimeSeconds: uptime, totalEvents: eventCount.cnt, healsPerformed: ctx.state.autoHealCount,
|
|
332
|
+
totalFileBytesSaved: ctx.state.totalFileBytesSaved, suppressionsSkipped: ctx.state.suppressionSkippedCount,
|
|
333
|
+
dormantTransitions: ctx.state.dormantTransitions.length, hologramSavedChars,
|
|
334
|
+
}));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (url.pathname === "/events") {
|
|
338
|
+
if (ctx.state.sseClients.size >= MAX_SSE_CLIENTS) {
|
|
339
|
+
return Response.json({ error: "Too many SSE clients" }, { status: 429 });
|
|
340
|
+
}
|
|
341
|
+
let sseController: ReadableStreamDefaultController<Uint8Array> | null = null;
|
|
342
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
343
|
+
start(controller) { sseController = controller; ctx.state.sseClients.add(controller); },
|
|
344
|
+
cancel() { if (sseController) ctx.state.sseClients.delete(sseController); },
|
|
345
|
+
});
|
|
346
|
+
return new Response(stream, {
|
|
347
|
+
headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive" },
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (url.pathname === "/telemetry") {
|
|
352
|
+
const days = parseInt(url.searchParams.get("days") ?? "7", 10) || 7;
|
|
353
|
+
const since = Date.now() - days * 86_400_000;
|
|
354
|
+
try {
|
|
355
|
+
const rows = ctx.db.prepare(
|
|
356
|
+
"SELECT category, action, COUNT(*) as cnt, AVG(duration_ms) as avg_ms FROM telemetry WHERE timestamp >= ? GROUP BY category, action ORDER BY cnt DESC"
|
|
357
|
+
).all(since) as { category: string; action: string; cnt: number; avg_ms: number | null }[];
|
|
358
|
+
return Response.json({ days, rows });
|
|
359
|
+
} catch {
|
|
360
|
+
return Response.json({ days, rows: [] });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (url.pathname === "/mesh/peers") {
|
|
365
|
+
return Response.json(listMeshPeers(ctx.ws.root));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (url.pathname === "/stop") {
|
|
369
|
+
cleanup();
|
|
370
|
+
setTimeout(() => process.exit(0), 100);
|
|
371
|
+
return Response.json({ status: "stopping" });
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return Response.json({ error: "not found" }, { status: 404 });
|
|
375
|
+
};
|
|
376
|
+
}
|