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