autonomous-flow-daemon 1.0.0 → 1.6.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 +39 -0
- package/README.ko.md +142 -125
- package/README.md +119 -134
- package/package.json +11 -5
- package/src/adapters/index.ts +247 -35
- package/src/cli.ts +79 -1
- package/src/commands/benchmark.ts +187 -0
- package/src/commands/diagnose.ts +56 -14
- package/src/commands/doctor.ts +243 -0
- package/src/commands/evolution.ts +107 -0
- package/src/commands/fix.ts +22 -2
- package/src/commands/hooks.ts +136 -0
- package/src/commands/lang.ts +41 -0
- package/src/commands/mcp.ts +129 -0
- package/src/commands/restart.ts +14 -0
- package/src/commands/score.ts +192 -64
- package/src/commands/start.ts +137 -37
- package/src/commands/stats.ts +103 -0
- package/src/commands/status.ts +157 -0
- package/src/commands/stop.ts +42 -9
- package/src/commands/sync.ts +253 -20
- package/src/commands/vaccine.ts +177 -0
- package/src/constants.ts +26 -1
- package/src/core/boast.ts +280 -0
- package/src/core/config.ts +49 -0
- package/src/core/db.ts +74 -3
- package/src/core/discovery.ts +65 -0
- package/src/core/evolution.ts +215 -0
- package/src/core/hologram/engine.ts +71 -0
- package/src/core/hologram/fallback.ts +11 -0
- package/src/core/hologram/incremental.ts +227 -0
- package/src/core/hologram/py-extractor.ts +132 -0
- package/src/core/hologram/ts-extractor.ts +320 -0
- package/src/core/hologram/types.ts +25 -0
- package/src/core/hologram.ts +64 -236
- package/src/core/hook-manager.ts +259 -0
- package/src/core/i18n/messages.ts +309 -0
- package/src/core/immune.ts +8 -123
- package/src/core/locale.ts +88 -0
- package/src/core/log-rotate.ts +33 -0
- package/src/core/log-utils.ts +38 -0
- package/src/core/lru-map.ts +61 -0
- package/src/core/notify.ts +53 -14
- package/src/core/rule-engine.ts +287 -0
- package/src/core/semantic-diff.ts +432 -0
- package/src/core/telemetry.ts +94 -0
- package/src/core/vaccine-registry.ts +212 -0
- package/src/core/workspace.ts +28 -0
- package/src/core/yaml-minimal.ts +176 -0
- package/src/daemon/client.ts +34 -6
- package/src/daemon/event-batcher.ts +108 -0
- package/src/daemon/guards.ts +13 -0
- package/src/daemon/http-routes.ts +293 -0
- package/src/daemon/mcp-handler.ts +270 -0
- package/src/daemon/server.ts +492 -273
- package/src/daemon/types.ts +100 -0
- package/src/daemon/workspace-map.ts +92 -0
- package/src/platform.ts +60 -0
- package/src/version.ts +15 -0
package/src/daemon/server.ts
CHANGED
|
@@ -1,50 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* afd daemon — S.E.A.M engine + module orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* Modules:
|
|
5
|
+
* types.ts — shared types and constants
|
|
6
|
+
* workspace-map.ts — project structure cache
|
|
7
|
+
* mcp-handler.ts — MCP stdio JSON-RPC dispatcher
|
|
8
|
+
* http-routes.ts — HTTP IPC endpoints
|
|
9
|
+
*/
|
|
10
|
+
|
|
1
11
|
import { watch } from "chokidar";
|
|
2
|
-
import { mkdirSync, writeFileSync, unlinkSync, readFileSync, existsSync } from "fs";
|
|
3
|
-
import { resolve } from "path";
|
|
4
|
-
import {
|
|
12
|
+
import { mkdirSync, writeFileSync, unlinkSync, readFileSync, existsSync, watch as fsWatch, readdirSync } from "fs";
|
|
13
|
+
import { resolve, join } from "path";
|
|
14
|
+
import { QUARANTINE_DIR, WATCH_TARGETS, resolveWorkspacePaths } from "../constants";
|
|
5
15
|
import { initDb } from "../core/db";
|
|
6
16
|
import { generateHologram } from "../core/hologram";
|
|
7
|
-
import {
|
|
17
|
+
import { EventBatcher } from "./event-batcher";
|
|
8
18
|
import type { PatchOp } from "../core/immune";
|
|
9
19
|
import { detectEcosystem } from "../adapters/index";
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
lastEventAt: number | null;
|
|
28
|
-
watchedFiles: string[];
|
|
29
|
-
hologramStats: HologramStats;
|
|
30
|
-
ecosystems: DetectionResult[];
|
|
31
|
-
autoHealCount: number;
|
|
32
|
-
autoHealLog: { id: string; at: number }[];
|
|
33
|
-
// Suppression safety: recent unlink timestamps for mass-event detection
|
|
34
|
-
recentUnlinks: number[];
|
|
35
|
-
// Suppression safety: per-file first-tap timestamps for double-tap detection
|
|
36
|
-
firstTapTimestamps: Map<string, number>;
|
|
37
|
-
suppressionSkippedCount: number;
|
|
38
|
-
dormantTransitions: { antibodyId: string; at: number }[];
|
|
39
|
-
}
|
|
40
|
-
|
|
20
|
+
import { calcHealMetrics, maybeHealBoast, formatHealLog, formatDormantLog } from "../core/boast";
|
|
21
|
+
import { discoverWatchTargets } from "../core/discovery";
|
|
22
|
+
import { formatTimestamp, lineDiff } from "../core/log-utils";
|
|
23
|
+
import { semanticDiff, isAstSupported } from "../core/semantic-diff";
|
|
24
|
+
import { LruStringMap } from "../core/lru-map";
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
DOUBLE_TAP_WINDOW_MS, MASS_EVENT_THRESHOLD, MASS_EVENT_WINDOW_MS,
|
|
28
|
+
TAP_CLEANUP_INTERVAL_MS, SELF_WRITE_DEBOUNCE_MS, VALIDATOR_TIMEOUT_MS, VALIDATORS_DIR,
|
|
29
|
+
} from "./types";
|
|
30
|
+
import type { DaemonState, DaemonContext, DaemonOptions, ValidatorFn } from "./types";
|
|
31
|
+
import { createWorkspaceMap } from "./workspace-map";
|
|
32
|
+
import { startMcpStdio } from "./mcp-handler";
|
|
33
|
+
import { createHttpHandler } from "./http-routes";
|
|
34
|
+
import { assertInsideWorkspace } from "./guards";
|
|
35
|
+
|
|
36
|
+
// ── State ──
|
|
41
37
|
const state: DaemonState = {
|
|
42
38
|
startedAt: Date.now(),
|
|
43
39
|
filesDetected: 0,
|
|
44
40
|
lastEvent: null,
|
|
45
41
|
lastEventAt: null,
|
|
46
|
-
watchedFiles:
|
|
47
|
-
hologramStats: { totalRequests: 0, totalOriginalChars: 0, totalHologramChars: 0 },
|
|
42
|
+
watchedFiles: new Set(),
|
|
43
|
+
hologramStats: { totalRequests: 0, totalOriginalChars: 0, totalHologramChars: 0, sessionOriginalChars: 0, sessionHologramChars: 0 },
|
|
48
44
|
ecosystems: [],
|
|
49
45
|
autoHealCount: 0,
|
|
50
46
|
autoHealLog: [],
|
|
@@ -52,18 +48,69 @@ const state: DaemonState = {
|
|
|
52
48
|
firstTapTimestamps: new Map(),
|
|
53
49
|
suppressionSkippedCount: 0,
|
|
54
50
|
dormantTransitions: [],
|
|
51
|
+
totalFileBytesSaved: 0,
|
|
52
|
+
totalSavedTokens: 0,
|
|
53
|
+
fileSnapshots: new LruStringMap(10 * 1024 * 1024),
|
|
54
|
+
sseClients: new Set(),
|
|
55
|
+
customValidators: new Map(),
|
|
56
|
+
mistakeCache: new Map(),
|
|
55
57
|
};
|
|
56
58
|
|
|
59
|
+
const _ws = resolveWorkspacePaths();
|
|
60
|
+
|
|
61
|
+
let _cleanupResources: {
|
|
62
|
+
watcher?: ReturnType<typeof watch>;
|
|
63
|
+
interval?: ReturnType<typeof setInterval>;
|
|
64
|
+
wsMapGetTimer?: () => ReturnType<typeof setTimeout> | null;
|
|
65
|
+
validatorWatcher?: ReturnType<typeof fsWatch>;
|
|
66
|
+
db?: { close(): void };
|
|
67
|
+
eventBatcher?: EventBatcher;
|
|
68
|
+
} = {};
|
|
69
|
+
|
|
57
70
|
function cleanup() {
|
|
58
|
-
try {
|
|
59
|
-
try {
|
|
71
|
+
try { _cleanupResources.eventBatcher?.destroy(); } catch {}
|
|
72
|
+
try { _cleanupResources.interval && clearInterval(_cleanupResources.interval); } catch {}
|
|
73
|
+
try { const mt = _cleanupResources.wsMapGetTimer?.(); mt && clearTimeout(mt); } catch {}
|
|
74
|
+
try { _cleanupResources.watcher?.close(); } catch {}
|
|
75
|
+
try { _cleanupResources.validatorWatcher?.close(); } catch {}
|
|
76
|
+
try { _cleanupResources.db?.close(); } catch {}
|
|
77
|
+
try { unlinkSync(_ws.pidFile); } catch {}
|
|
78
|
+
try { unlinkSync(_ws.portFile); } catch {}
|
|
60
79
|
}
|
|
61
80
|
|
|
62
|
-
|
|
63
|
-
|
|
81
|
+
// ── S.E.A.M Logger ──
|
|
82
|
+
const GUARD_LINE = "========== GUARDED ==========";
|
|
83
|
+
const GUARD_PHASES = new Set(["Mutate", "Quarantine"]);
|
|
84
|
+
|
|
85
|
+
function createSeamLogger(mcp: boolean) {
|
|
86
|
+
const log = mcp ? console.error.bind(console) : console.log.bind(console);
|
|
87
|
+
return function seam(phase: string, msg: string) {
|
|
88
|
+
if (GUARD_PHASES.has(phase)) {
|
|
89
|
+
log(`\n${GUARD_LINE}`);
|
|
90
|
+
log(`[${formatTimestamp()}] [afd] [${phase}] ${msg}`);
|
|
91
|
+
log(`${GUARD_LINE}\n`);
|
|
92
|
+
} else {
|
|
93
|
+
log(`[${formatTimestamp()}] [afd] [${phase}] ${msg}`);
|
|
94
|
+
}
|
|
95
|
+
const encoder = new TextEncoder();
|
|
96
|
+
const payload = JSON.stringify({ phase, msg, ts: Date.now() });
|
|
97
|
+
const sseData = encoder.encode(`data: ${payload}\n\n`);
|
|
98
|
+
const dead: ReadableStreamDefaultController<Uint8Array>[] = [];
|
|
99
|
+
for (const controller of state.sseClients) {
|
|
100
|
+
try { controller.enqueue(sseData); } catch { dead.push(controller); }
|
|
101
|
+
}
|
|
102
|
+
for (const c of dead) state.sseClients.delete(c);
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ══════════════════════════════════════════════════════════
|
|
107
|
+
export function main(options: DaemonOptions = {}) {
|
|
64
108
|
state.ecosystems = detectEcosystem(process.cwd());
|
|
65
109
|
|
|
66
110
|
const db = initDb();
|
|
111
|
+
_cleanupResources.db = db;
|
|
112
|
+
|
|
113
|
+
// ── Prepared statements ──
|
|
67
114
|
const insertEvent = db.prepare("INSERT INTO events (type, path, timestamp) VALUES (?, ?, ?)");
|
|
68
115
|
const insertAntibody = db.prepare(
|
|
69
116
|
"INSERT OR REPLACE INTO antibodies (id, pattern_type, file_target, patch_op, created_at) VALUES (?, ?, ?, ?, datetime('now'))"
|
|
@@ -73,299 +120,471 @@ function main() {
|
|
|
73
120
|
const countAntibodies = db.prepare("SELECT COUNT(*) as cnt FROM antibodies");
|
|
74
121
|
const insertUnlinkLog = db.prepare("INSERT INTO unlink_log (file_path, timestamp) VALUES (?, ?)");
|
|
75
122
|
const findAntibodyByFile = db.prepare("SELECT id, dormant FROM antibodies WHERE file_target = ? AND dormant = 0");
|
|
123
|
+
const findAntibodyById = db.prepare("SELECT * FROM antibodies WHERE id = ?");
|
|
76
124
|
const setAntibodyDormant = db.prepare("UPDATE antibodies SET dormant = 1 WHERE id = ?");
|
|
77
125
|
|
|
78
|
-
//
|
|
126
|
+
// Hologram stats
|
|
127
|
+
const getLifetime = db.prepare("SELECT total_requests, total_original_chars, total_hologram_chars FROM hologram_lifetime WHERE id = 1");
|
|
128
|
+
const updateLifetime = db.prepare(
|
|
129
|
+
"UPDATE hologram_lifetime SET total_requests = ?, total_original_chars = ?, total_hologram_chars = ? WHERE id = 1"
|
|
130
|
+
);
|
|
131
|
+
const upsertDaily = db.prepare(`
|
|
132
|
+
INSERT INTO hologram_daily (date, requests, original_chars, hologram_chars) VALUES (?, ?, ?, ?)
|
|
133
|
+
ON CONFLICT(date) DO UPDATE SET requests = requests + excluded.requests,
|
|
134
|
+
original_chars = original_chars + excluded.original_chars,
|
|
135
|
+
hologram_chars = hologram_chars + excluded.hologram_chars
|
|
136
|
+
`);
|
|
137
|
+
const getDailyAll = db.prepare("SELECT date, requests, original_chars, hologram_chars FROM hologram_daily ORDER BY date DESC LIMIT 7");
|
|
138
|
+
const purgeOldDaily = db.prepare("DELETE FROM hologram_daily WHERE date < date('now', '-7 days')");
|
|
139
|
+
|
|
140
|
+
// ── Telemetry ──
|
|
141
|
+
const insertTelemetry = db.prepare(
|
|
142
|
+
"INSERT INTO telemetry (category, action, detail, duration_ms, timestamp) VALUES (?, ?, ?, ?, ?)"
|
|
143
|
+
);
|
|
144
|
+
function trackEvent(category: string, action: string, detail?: string, durationMs?: number) {
|
|
145
|
+
try { insertTelemetry.run(category, action, detail ?? null, durationMs ?? null, Date.now()); } catch { /* crash-only */ }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── Mistake History (Passive Defense) ──
|
|
149
|
+
const insertMistakeHistory = db.prepare(
|
|
150
|
+
"INSERT INTO mistake_history (file_path, mistake_type, description, antibody_id, timestamp) VALUES (?, ?, ?, ?, ?)"
|
|
151
|
+
);
|
|
152
|
+
const queryMistakesByFile = db.prepare(
|
|
153
|
+
"SELECT mistake_type, description, timestamp FROM mistake_history WHERE file_path = ? ORDER BY timestamp DESC LIMIT 5"
|
|
154
|
+
);
|
|
155
|
+
const deleteMistakeOverflow = db.prepare(
|
|
156
|
+
"DELETE FROM mistake_history WHERE file_path = ? AND id NOT IN (SELECT id FROM mistake_history WHERE file_path = ? ORDER BY timestamp DESC LIMIT 5)"
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
function recordMistake(filePath: string, mistakeType: string, description: string, antibodyId?: string) {
|
|
160
|
+
try {
|
|
161
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
162
|
+
const truncatedDesc = description.slice(0, 200);
|
|
163
|
+
insertMistakeHistory.run(normalizedPath, mistakeType, truncatedDesc, antibodyId ?? null, Date.now());
|
|
164
|
+
deleteMistakeOverflow.run(normalizedPath, normalizedPath);
|
|
165
|
+
const cached = state.mistakeCache.get(normalizedPath) ?? [];
|
|
166
|
+
cached.unshift({ mistake_type: mistakeType, description: truncatedDesc, timestamp: Date.now() });
|
|
167
|
+
if (cached.length > 5) cached.length = 5;
|
|
168
|
+
state.mistakeCache.set(normalizedPath, cached);
|
|
169
|
+
} catch { /* crash-only */ }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const allMistakes = db.prepare("SELECT file_path, mistake_type, description, timestamp FROM mistake_history ORDER BY timestamp DESC").all() as { file_path: string; mistake_type: string; description: string; timestamp: number }[];
|
|
174
|
+
for (const row of allMistakes) {
|
|
175
|
+
const cached = state.mistakeCache.get(row.file_path) ?? [];
|
|
176
|
+
if (cached.length < 5) {
|
|
177
|
+
cached.push({ mistake_type: row.mistake_type, description: row.description, timestamp: row.timestamp });
|
|
178
|
+
state.mistakeCache.set(row.file_path, cached);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} catch { /* crash-only — empty cache is fine */ }
|
|
182
|
+
|
|
183
|
+
function today(): string { return new Date().toISOString().slice(0, 10); }
|
|
184
|
+
|
|
185
|
+
// Load persisted hologram stats
|
|
186
|
+
const persisted = getLifetime.get() as { total_requests: number; total_original_chars: number; total_hologram_chars: number } | null;
|
|
187
|
+
if (persisted) {
|
|
188
|
+
state.hologramStats.totalRequests = persisted.total_requests;
|
|
189
|
+
state.hologramStats.totalOriginalChars = persisted.total_original_chars;
|
|
190
|
+
state.hologramStats.totalHologramChars = persisted.total_hologram_chars;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const seam = createSeamLogger(!!options.mcp);
|
|
194
|
+
|
|
195
|
+
function persistHologramStats(originalChars: number, hologramChars: number) {
|
|
196
|
+
state.hologramStats.totalRequests++;
|
|
197
|
+
state.hologramStats.totalOriginalChars += originalChars;
|
|
198
|
+
state.totalSavedTokens += Math.max(0, Math.floor((originalChars - hologramChars) / 4));
|
|
199
|
+
state.hologramStats.totalHologramChars += hologramChars;
|
|
200
|
+
state.hologramStats.sessionOriginalChars += originalChars;
|
|
201
|
+
state.hologramStats.sessionHologramChars += hologramChars;
|
|
202
|
+
try {
|
|
203
|
+
const hs = state.hologramStats;
|
|
204
|
+
updateLifetime.run(hs.totalRequests, hs.totalOriginalChars, hs.totalHologramChars);
|
|
205
|
+
upsertDaily.run(today(), 1, originalChars, hologramChars);
|
|
206
|
+
purgeOldDaily.run();
|
|
207
|
+
} catch (err) {
|
|
208
|
+
console.error("[afd] Failed to persist hologram stats:", err instanceof Error ? err.message : String(err));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function snapshotFile(filePath: string) {
|
|
213
|
+
try {
|
|
214
|
+
if (existsSync(filePath)) state.fileSnapshots.set(filePath, readFileSync(filePath, "utf-8"));
|
|
215
|
+
} catch { /* ignore */ }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function safeHologram(filePath: string, source: string): Promise<string> {
|
|
219
|
+
try {
|
|
220
|
+
const result = await generateHologram(filePath, source);
|
|
221
|
+
persistHologramStats(result.originalLength, result.hologramLength);
|
|
222
|
+
return result.hologram;
|
|
223
|
+
} catch {
|
|
224
|
+
const lines = source.split("\n");
|
|
225
|
+
return lines.slice(0, 50).join("\n") + (lines.length > 50 ? "\n// … (truncated, AST parse failed)" : "");
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function quarantineFile(originalPath: string, corruptedContent: string | null): void {
|
|
230
|
+
try {
|
|
231
|
+
mkdirSync(QUARANTINE_DIR, { recursive: true });
|
|
232
|
+
const now = new Date();
|
|
233
|
+
const ts = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}${String(now.getDate()).padStart(2, "0")}_${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}${String(now.getSeconds()).padStart(2, "0")}`;
|
|
234
|
+
const baseName = originalPath.replace(/[\\/]/g, "_").replace(/^_+/, "");
|
|
235
|
+
const quarantinePath = resolve(QUARANTINE_DIR, `${ts}_${baseName}`);
|
|
236
|
+
writeFileSync(quarantinePath, corruptedContent ?? "DELETED", "utf-8");
|
|
237
|
+
seam("Quarantine", `Saved corrupted state → .afd/quarantine/${ts}_${baseName}`);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
seam("Quarantine", `FAILED to quarantine ${originalPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ── Auto-Seed Antibodies ──
|
|
244
|
+
function seedAntibodies() {
|
|
245
|
+
const immuneFiles = [
|
|
246
|
+
{ id: "IMM-001", path: ".claudeignore" },
|
|
247
|
+
{ id: "IMM-002", path: ".claude/hooks.json" },
|
|
248
|
+
{ id: "IMM-003", path: "CLAUDE.md" },
|
|
249
|
+
];
|
|
250
|
+
for (const { id, path: filePath } of immuneFiles) {
|
|
251
|
+
if (existsSync(filePath)) {
|
|
252
|
+
const content = readFileSync(filePath, "utf-8");
|
|
253
|
+
const patches: PatchOp[] = [{ op: "add", path: `/${filePath}`, value: content }];
|
|
254
|
+
insertAntibody.run(id, "auto-seed", filePath, JSON.stringify(patches));
|
|
255
|
+
state.fileSnapshots.set(filePath, content);
|
|
256
|
+
seam("Adapt", `Antibody ${id} seeded for ${filePath} (${content.length} bytes)`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
79
260
|
|
|
80
|
-
|
|
261
|
+
const selfWrites = new Set<string>();
|
|
262
|
+
seedAntibodies();
|
|
263
|
+
|
|
264
|
+
// ── Dynamic Immune Synthesis ──
|
|
265
|
+
const validatorsDir = join(process.cwd(), VALIDATORS_DIR);
|
|
266
|
+
|
|
267
|
+
async function loadValidators() {
|
|
268
|
+
state.customValidators.clear();
|
|
269
|
+
if (!existsSync(validatorsDir)) return;
|
|
270
|
+
let files: string[];
|
|
271
|
+
try { files = readdirSync(validatorsDir).filter(f => f.endsWith(".js")); } catch { return; }
|
|
272
|
+
for (const file of files) {
|
|
273
|
+
const absPath = resolve(validatorsDir, file);
|
|
274
|
+
try {
|
|
275
|
+
const mod = await import(absPath);
|
|
276
|
+
const fn = mod.default ?? mod;
|
|
277
|
+
if (typeof fn === "function") {
|
|
278
|
+
state.customValidators.set(file, fn as ValidatorFn);
|
|
279
|
+
seam("Adapt", `🧬 Validator loaded: ${file}`);
|
|
280
|
+
} else {
|
|
281
|
+
seam("Adapt", `⚠️ Validator ${file} does not export a function — skipped`);
|
|
282
|
+
}
|
|
283
|
+
} catch (err) {
|
|
284
|
+
seam("Adapt", `⚠️ Failed to load validator ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (state.customValidators.size > 0) {
|
|
288
|
+
seam("Adapt", `Dynamic Immune Synthesis: ${state.customValidators.size} active validator(s)`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
loadValidators();
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
mkdirSync(validatorsDir, { recursive: true });
|
|
296
|
+
let reloadTimer: ReturnType<typeof setTimeout> | null = null;
|
|
297
|
+
const validatorWatcher = fsWatch(validatorsDir, (_eventType, filename) => {
|
|
298
|
+
if (!filename?.endsWith(".js")) return;
|
|
299
|
+
if (reloadTimer) clearTimeout(reloadTimer);
|
|
300
|
+
reloadTimer = setTimeout(() => { seam("Sense", `Validator change detected: ${filename} — reloading...`); loadValidators(); reloadTimer = null; }, 200);
|
|
301
|
+
});
|
|
302
|
+
_cleanupResources.validatorWatcher = validatorWatcher;
|
|
303
|
+
} catch (err) {
|
|
304
|
+
seam("Adapt", `⚠️ Cannot watch ${VALIDATORS_DIR}: ${err instanceof Error ? err.message : String(err)}`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function runCustomValidators(newContent: string, filePath: string): boolean {
|
|
308
|
+
for (const [name, fn] of state.customValidators) {
|
|
309
|
+
try {
|
|
310
|
+
const t0 = performance.now();
|
|
311
|
+
const result = fn(newContent, filePath);
|
|
312
|
+
const elapsed = performance.now() - t0;
|
|
313
|
+
if (elapsed > VALIDATOR_TIMEOUT_MS) seam("Adapt", `⚠️ Validator ${name} took ${Math.round(elapsed)}ms (>${VALIDATOR_TIMEOUT_MS}ms)`);
|
|
314
|
+
if (result === true) { trackEvent("validator", name, filePath); seam("Adapt", `🛡️ Custom validator ${name} flagged corruption in ${filePath}`); return true; }
|
|
315
|
+
} catch (err) {
|
|
316
|
+
seam("Adapt", `⚠️ Validator ${name} threw error — ignored: ${err instanceof Error ? err.message : String(err)}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ── Periodic cleanup ──
|
|
323
|
+
const tapCleanupInterval = setInterval(() => {
|
|
324
|
+
const now = Date.now();
|
|
325
|
+
for (const [file, ts] of state.firstTapTimestamps) { if (now - ts > DOUBLE_TAP_WINDOW_MS) state.firstTapTimestamps.delete(file); }
|
|
326
|
+
for (const [file, ts] of corruptionTaps) { if (now - ts > DOUBLE_TAP_WINDOW_MS) corruptionTaps.delete(file); }
|
|
327
|
+
}, TAP_CLEANUP_INTERVAL_MS);
|
|
328
|
+
_cleanupResources.interval = tapCleanupInterval;
|
|
329
|
+
|
|
330
|
+
// ── Suppression Safety ──
|
|
81
331
|
function isMassEvent(now: number): boolean {
|
|
82
|
-
// Prune old entries beyond the window
|
|
83
332
|
state.recentUnlinks = state.recentUnlinks.filter(t => now - t < MASS_EVENT_WINDOW_MS);
|
|
84
333
|
return state.recentUnlinks.length > MASS_EVENT_THRESHOLD;
|
|
85
334
|
}
|
|
86
335
|
|
|
87
|
-
/** Clear first-tap state when mass event is detected (bulk ops are not intentional user deletes) */
|
|
88
|
-
function clearTapsOnMassEvent() {
|
|
89
|
-
state.firstTapTimestamps.clear();
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** Auto-heal: re-apply patches for a given antibody */
|
|
93
336
|
function autoHealFile(antibodyId: string, fileTarget: string, patchOp: string) {
|
|
337
|
+
const t0 = performance.now();
|
|
94
338
|
try {
|
|
339
|
+
seam("Mutate", `Restoring ${fileTarget} via antibody ${antibodyId}...`);
|
|
95
340
|
const patches = JSON.parse(patchOp) as PatchOp[];
|
|
341
|
+
let bytesWritten = 0;
|
|
96
342
|
for (const patch of patches) {
|
|
97
343
|
if (patch.op === "add" && patch.value) {
|
|
98
344
|
const targetPath = resolve(patch.path.replace(/^\//, ""));
|
|
345
|
+
assertInsideWorkspace(targetPath, _ws.root);
|
|
99
346
|
writeFileSync(targetPath, patch.value, "utf-8");
|
|
347
|
+
bytesWritten += patch.value.length;
|
|
100
348
|
}
|
|
101
349
|
}
|
|
350
|
+
const healMs = Math.round(performance.now() - t0);
|
|
102
351
|
state.autoHealCount++;
|
|
352
|
+
state.totalFileBytesSaved += bytesWritten;
|
|
103
353
|
state.autoHealLog.push({ id: antibodyId, at: Date.now() });
|
|
354
|
+
trackEvent("immune", "heal_hit", JSON.stringify({ antibodyId, fileTarget, bytesWritten, healMs }));
|
|
355
|
+
recordMistake(fileTarget, "file-deleted", `File deleted and restored via antibody ${antibodyId}`, antibodyId);
|
|
104
356
|
if (state.autoHealLog.length > 100) state.autoHealLog.shift();
|
|
105
|
-
|
|
106
|
-
|
|
357
|
+
const metrics = calcHealMetrics(bytesWritten, healMs);
|
|
358
|
+
const boast = maybeHealBoast(5);
|
|
359
|
+
const fileName = fileTarget.split("/").pop() ?? fileTarget;
|
|
360
|
+
(options.mcp ? console.error : console.log)(formatHealLog(fileName, metrics, boast));
|
|
361
|
+
} catch (err) {
|
|
362
|
+
seam("Mutate", `FAILED to restore ${fileTarget}: ${err instanceof Error ? err.message : String(err)}`);
|
|
107
363
|
}
|
|
108
364
|
}
|
|
109
365
|
|
|
110
|
-
|
|
111
|
-
* Handle unlink event with Double-Tap and Mass-Event heuristics.
|
|
112
|
-
* Returns true if the event was handled (healed or made dormant).
|
|
113
|
-
*/
|
|
114
|
-
function handleUnlink(filePath: string, now: number): boolean {
|
|
115
|
-
// Record for mass-event detection
|
|
366
|
+
function handleUnlink(filePath: string, now: number): "healed" | "dormant" | null {
|
|
116
367
|
state.recentUnlinks.push(now);
|
|
117
368
|
insertUnlinkLog.run(filePath, now);
|
|
118
|
-
|
|
119
|
-
// Mass-event check: if >3 unlinks in 1s, skip ALL suppression logic
|
|
120
|
-
if (isMassEvent(now)) {
|
|
121
|
-
state.suppressionSkippedCount++;
|
|
122
|
-
clearTapsOnMassEvent();
|
|
123
|
-
return false; // Do nothing — likely git checkout or bulk operation
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Find active (non-dormant) antibody protecting this file
|
|
369
|
+
if (isMassEvent(now)) { state.suppressionSkippedCount++; trackEvent("immune", "suppression", JSON.stringify({ filePath })); state.firstTapTimestamps.clear(); return null; }
|
|
127
370
|
const antibody = findAntibodyByFile.get(filePath) as { id: string; dormant: number } | null;
|
|
128
|
-
if (!antibody) return
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const fullAntibody = db.prepare("SELECT * FROM antibodies WHERE id = ?").get(antibody.id) as {
|
|
132
|
-
id: string; patch_op: string; file_target: string;
|
|
133
|
-
} | null;
|
|
134
|
-
if (!fullAntibody) return false;
|
|
135
|
-
|
|
136
|
-
// Double-Tap detection
|
|
371
|
+
if (!antibody) return null;
|
|
372
|
+
const fullAntibody = findAntibodyById.get(antibody.id) as { id: string; patch_op: string; file_target: string } | null;
|
|
373
|
+
if (!fullAntibody) return null;
|
|
137
374
|
const previousTap = state.firstTapTimestamps.get(filePath);
|
|
138
|
-
|
|
139
375
|
if (previousTap && (now - previousTap) < DOUBLE_TAP_WINDOW_MS) {
|
|
140
|
-
// SECOND TAP within window → user is intentional → make dormant
|
|
141
376
|
setAntibodyDormant.run(antibody.id);
|
|
142
377
|
state.firstTapTimestamps.delete(filePath);
|
|
143
378
|
state.dormantTransitions.push({ antibodyId: antibody.id, at: now });
|
|
144
|
-
|
|
379
|
+
if (state.dormantTransitions.length > 100) state.dormantTransitions.shift();
|
|
380
|
+
trackEvent("immune", "dormant", JSON.stringify({ antibodyId: antibody.id }));
|
|
381
|
+
(options.mcp ? console.error : console.log)(formatDormantLog(antibody.id));
|
|
382
|
+
return "dormant";
|
|
145
383
|
}
|
|
146
|
-
|
|
147
|
-
// FIRST TAP: record timestamp and auto-heal
|
|
148
384
|
state.firstTapTimestamps.set(filePath, now);
|
|
385
|
+
quarantineFile(filePath, null);
|
|
149
386
|
autoHealFile(fullAntibody.id, fullAntibody.file_target, fullAntibody.patch_op);
|
|
150
|
-
return
|
|
387
|
+
return "healed";
|
|
151
388
|
}
|
|
152
389
|
|
|
153
|
-
//
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
390
|
+
// ── Smart Discovery + Watcher ──
|
|
391
|
+
const discovery = discoverWatchTargets(WATCH_TARGETS);
|
|
392
|
+
seam("Sense", `Smart Discovery: ${discovery.targets.length} targets found in ${discovery.elapsedMs}ms`);
|
|
393
|
+
if (discovery.discoveredCount > 0) {
|
|
394
|
+
seam("Sense", `Discovered ${discovery.discoveredCount} extra: ${discovery.targets.filter(t => !WATCH_TARGETS.includes(t)).join(", ")}`);
|
|
395
|
+
}
|
|
158
396
|
|
|
159
|
-
watcher.
|
|
160
|
-
|
|
161
|
-
state.lastEvent = `${event}:${path}`;
|
|
162
|
-
state.lastEventAt = Date.now();
|
|
163
|
-
if (!state.watchedFiles.includes(path)) {
|
|
164
|
-
state.watchedFiles.push(path);
|
|
165
|
-
}
|
|
166
|
-
insertEvent.run(event, path, Date.now());
|
|
397
|
+
const watcher = watch(discovery.targets, { ignoreInitial: false, persistent: true, atomic: 100 });
|
|
398
|
+
_cleanupResources.watcher = watcher;
|
|
167
399
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
handleUnlink(path, Date.now());
|
|
171
|
-
}
|
|
172
|
-
});
|
|
400
|
+
const immuneMap: Record<string, string> = { ".claudeignore": "IMM-001", ".claude/hooks.json": "IMM-002", "CLAUDE.md": "IMM-003" };
|
|
401
|
+
const corruptionTaps = new Map<string, number>();
|
|
173
402
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const url = new URL(req.url);
|
|
403
|
+
function isInternalPath(p: string): boolean {
|
|
404
|
+
const normalized = p.replace(/\\/g, "/");
|
|
405
|
+
return normalized.startsWith(".afd/") || normalized.includes("/.afd/");
|
|
406
|
+
}
|
|
179
407
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
408
|
+
function isCorrupted(oldContent: string, newContent: string, filePath: string): boolean {
|
|
409
|
+
if (runCustomValidators(newContent, filePath)) return true;
|
|
410
|
+
const trimmed = newContent.trim();
|
|
411
|
+
if (trimmed.length === 0) return true;
|
|
412
|
+
if (filePath.endsWith(".json")) {
|
|
413
|
+
if (trimmed === "{}" || trimmed === "[]") return true;
|
|
414
|
+
try { JSON.parse(newContent); } catch { return true; }
|
|
415
|
+
}
|
|
416
|
+
if (oldContent.length > 50 && newContent.length < oldContent.length * 0.1) return true;
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
183
419
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
420
|
+
// ── Event Batcher (Adaptive Debounce) ──
|
|
421
|
+
const eventBatcher = new EventBatcher({
|
|
422
|
+
debounceMs: 300,
|
|
423
|
+
isImmunePath: (p: string) => {
|
|
424
|
+
const normalized = p.replace(/\\/g, "/");
|
|
425
|
+
return normalized in immuneMap;
|
|
426
|
+
},
|
|
427
|
+
onImmediate: (event: string, path: string) => handleFileEvent(event, path),
|
|
428
|
+
onBatch: (events) => {
|
|
429
|
+
if (events.length > 1) {
|
|
430
|
+
seam("Sense", `[Batch] Processing ${events.length} events as single batch`);
|
|
193
431
|
}
|
|
432
|
+
for (const e of events) handleFileEvent(e.event, e.path);
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
_cleanupResources.eventBatcher = eventBatcher;
|
|
194
436
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const result = generateHologram(file, source);
|
|
202
|
-
state.hologramStats.totalRequests++;
|
|
203
|
-
state.hologramStats.totalOriginalChars += result.originalLength;
|
|
204
|
-
state.hologramStats.totalHologramChars += result.hologramLength;
|
|
205
|
-
return Response.json(result);
|
|
206
|
-
} catch (err: unknown) {
|
|
207
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
208
|
-
return Response.json({ error: msg }, { status: 404 });
|
|
209
|
-
}
|
|
210
|
-
}
|
|
437
|
+
// ── Watcher Event Handler (S.E.A.M) ──
|
|
438
|
+
watcher.on("all", (event, path) => {
|
|
439
|
+
if (isInternalPath(path)) return;
|
|
440
|
+
if (selfWrites.has(path)) return;
|
|
441
|
+
eventBatcher.push(event, path);
|
|
442
|
+
});
|
|
211
443
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const known = (antibodyIds.all() as { id: string }[]).map(r => r.id);
|
|
215
|
-
const result = diagnose(known, { raw });
|
|
216
|
-
return Response.json(result);
|
|
217
|
-
}
|
|
444
|
+
function handleFileEvent(event: string, path: string) {
|
|
445
|
+
const _seamStart = performance.now();
|
|
218
446
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
447
|
+
state.filesDetected++;
|
|
448
|
+
state.lastEvent = `${event}:${path}`;
|
|
449
|
+
state.lastEventAt = Date.now();
|
|
450
|
+
state.watchedFiles.add(path);
|
|
451
|
+
insertEvent.run(event, path, Date.now());
|
|
223
452
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
241
|
-
return Response.json({ error: msg }, { status: 400 });
|
|
242
|
-
}
|
|
453
|
+
if (event === "add" || event === "addDir") { seam("Sense", `${event} → ${path}`); snapshotFile(path); trackEvent("seam", "sense", null, Math.round(performance.now() - _seamStart)); return; }
|
|
454
|
+
|
|
455
|
+
if (event === "unlink") {
|
|
456
|
+
seam("Sense", `unlink → ${path}`);
|
|
457
|
+
state.fileSnapshots.delete(path);
|
|
458
|
+
state.watchedFiles.delete(path);
|
|
459
|
+
const result = handleUnlink(path, Date.now());
|
|
460
|
+
if (result === "healed") {
|
|
461
|
+
selfWrites.add(path);
|
|
462
|
+
setTimeout(() => selfWrites.delete(path), SELF_WRITE_DEBOUNCE_MS);
|
|
463
|
+
seam("Mutate", `Restored ${path} from antibody snapshot`);
|
|
464
|
+
seam("Extract", `💡 Tip: Use the MCP tool 'afd_hologram' on ${path} to safely inspect the file's structure before attempting another edit.`);
|
|
465
|
+
snapshotFile(path);
|
|
466
|
+
watcher.add(path);
|
|
467
|
+
} else if (result === "dormant") {
|
|
468
|
+
seam("Adapt", `Double-tap confirmed — user intentionally deleted ${path}, antibody deactivated`);
|
|
243
469
|
}
|
|
470
|
+
trackEvent("seam", event, path, Math.round(performance.now() - _seamStart));
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
244
473
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
474
|
+
if (event === "change") {
|
|
475
|
+
if (!existsSync(path)) return;
|
|
476
|
+
let newContent: string;
|
|
477
|
+
try { newContent = readFileSync(path, "utf-8"); } catch { return; }
|
|
478
|
+
const oldContent = state.fileSnapshots.get(path);
|
|
479
|
+
const newSize = newContent.length;
|
|
480
|
+
|
|
481
|
+
if (oldContent !== undefined && oldContent !== newContent) {
|
|
482
|
+
if (isAstSupported(path)) {
|
|
483
|
+
try {
|
|
484
|
+
const sdiff = semanticDiff(path, oldContent, newContent);
|
|
485
|
+
const breakingTag = sdiff.hasBreakingChanges ? " ⚠️ BREAKING" : "";
|
|
486
|
+
seam("Sense", `change → ${path} (${newSize} bytes)${breakingTag}\n [semantic] ${sdiff.summary}`);
|
|
487
|
+
} catch {
|
|
488
|
+
const diffs = lineDiff(oldContent, newContent);
|
|
489
|
+
seam("Sense", `change → ${path} (${newSize} bytes)\n${diffs.join("\n")}`);
|
|
490
|
+
}
|
|
491
|
+
} else {
|
|
492
|
+
const diffs = lineDiff(oldContent, newContent);
|
|
493
|
+
if (diffs.length > 0) seam("Sense", `change → ${path} (${newSize} bytes)\n${diffs.join("\n")}`);
|
|
494
|
+
else seam("Sense", `change → ${path} (${newSize} bytes, whitespace-only diff)`);
|
|
256
495
|
}
|
|
496
|
+
} else if (oldContent === undefined) {
|
|
497
|
+
seam("Sense", `change → ${path} (${newSize} bytes, no previous snapshot)`);
|
|
498
|
+
} else {
|
|
499
|
+
seam("Sense", `change → ${path} (${newSize} bytes, content identical — touch or metadata)`);
|
|
257
500
|
}
|
|
258
501
|
|
|
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
|
-
confidence: e.confidence,
|
|
292
|
-
schema: e.adapter.getHarnessSchema(),
|
|
293
|
-
})),
|
|
294
|
-
primary: state.ecosystems[0]?.adapter.name ?? "Unknown",
|
|
295
|
-
},
|
|
296
|
-
suppression: {
|
|
297
|
-
massEventsSkipped: state.suppressionSkippedCount,
|
|
298
|
-
dormantTransitions: state.dormantTransitions.length,
|
|
299
|
-
activeTaps: state.firstTapTimestamps.size,
|
|
300
|
-
},
|
|
301
|
-
});
|
|
502
|
+
const normalizedPath = path.replace(/\\/g, "/");
|
|
503
|
+
const abId = immuneMap[normalizedPath];
|
|
504
|
+
if (abId && oldContent !== undefined) {
|
|
505
|
+
if (isCorrupted(oldContent, newContent, path)) {
|
|
506
|
+
const prevCorruption = corruptionTaps.get(path);
|
|
507
|
+
const now = Date.now();
|
|
508
|
+
if (prevCorruption && (now - prevCorruption) < DOUBLE_TAP_WINDOW_MS) {
|
|
509
|
+
corruptionTaps.delete(path);
|
|
510
|
+
state.fileSnapshots.set(path, newContent);
|
|
511
|
+
insertAntibody.run(abId, "auto-seed", path, JSON.stringify([{ op: "add", path: `/${path}`, value: newContent }]));
|
|
512
|
+
trackEvent("immune", "heal_false_positive", JSON.stringify({ filePath: path, abId }));
|
|
513
|
+
seam("Adapt", `Corruption double-tap on ${path} — standing down, accepting new content`);
|
|
514
|
+
} else {
|
|
515
|
+
corruptionTaps.set(path, now);
|
|
516
|
+
quarantineFile(path, newContent);
|
|
517
|
+
selfWrites.add(path);
|
|
518
|
+
setTimeout(() => selfWrites.delete(path), SELF_WRITE_DEBOUNCE_MS);
|
|
519
|
+
writeFileSync(resolve(path), oldContent, "utf-8");
|
|
520
|
+
trackEvent("immune", "heal_hit", JSON.stringify({ filePath: path, abId }));
|
|
521
|
+
recordMistake(path, "corruption", `Silent corruption detected (${oldContent.length} → ${newSize} bytes) — restored`, abId);
|
|
522
|
+
seam("Mutate", `Silent corruption detected in ${path} (${oldContent.length} → ${newSize} bytes) — restored from snapshot`);
|
|
523
|
+
seam("Extract", `💡 Tip: Use the MCP tool 'afd_hologram' on ${path} to safely inspect the file's structure before attempting another edit.`);
|
|
524
|
+
}
|
|
525
|
+
} else {
|
|
526
|
+
state.fileSnapshots.set(path, newContent);
|
|
527
|
+
const patches: PatchOp[] = [{ op: "add", path: `/${path}`, value: newContent }];
|
|
528
|
+
insertAntibody.run(abId, "auto-seed", path, JSON.stringify(patches));
|
|
529
|
+
trackEvent("immune", "heal_pass", JSON.stringify({ filePath: path, abId }));
|
|
530
|
+
seam("Adapt", `Antibody ${abId} updated: stored latest ${path} (${newSize} bytes) for auto-restore`);
|
|
531
|
+
}
|
|
532
|
+
} else {
|
|
533
|
+
state.fileSnapshots.set(path, newContent);
|
|
302
534
|
}
|
|
535
|
+
trackEvent("seam", "change", path, Math.round(performance.now() - _seamStart));
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
303
538
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
pattern_type: string;
|
|
308
|
-
file_target: string;
|
|
309
|
-
patch_op: string;
|
|
310
|
-
created_at: string;
|
|
311
|
-
}[];
|
|
312
|
-
// Sanitize: strip absolute paths, keep only relative patterns
|
|
313
|
-
const sanitized = rows.map(r => {
|
|
314
|
-
const patches = JSON.parse(r.patch_op) as PatchOp[];
|
|
315
|
-
const cleanPatches = patches.map(p => ({
|
|
316
|
-
...p,
|
|
317
|
-
// Ensure paths are relative (strip any leading drive/abs prefix)
|
|
318
|
-
path: p.path.replace(/^[A-Za-z]:/, "").replace(/\\/g, "/"),
|
|
319
|
-
// Strip absolute paths from values
|
|
320
|
-
value: p.value?.replace(/[A-Za-z]:\\[^\s"']*/g, "<redacted>"),
|
|
321
|
-
}));
|
|
322
|
-
return {
|
|
323
|
-
id: r.id,
|
|
324
|
-
patternType: r.pattern_type,
|
|
325
|
-
fileTarget: r.file_target.replace(/^[A-Za-z]:/, "").replace(/\\/g, "/"),
|
|
326
|
-
patches: cleanPatches,
|
|
327
|
-
learnedAt: r.created_at,
|
|
328
|
-
};
|
|
329
|
-
});
|
|
330
|
-
const payload = {
|
|
331
|
-
version: "0.1.0",
|
|
332
|
-
generatedAt: new Date().toISOString(),
|
|
333
|
-
ecosystem: state.ecosystems[0]?.adapter.name ?? "Unknown",
|
|
334
|
-
antibodyCount: sanitized.length,
|
|
335
|
-
antibodies: sanitized,
|
|
336
|
-
};
|
|
337
|
-
// Write payload to disk
|
|
338
|
-
const payloadPath = resolve(AFD_DIR, "global-vaccine-payload.json");
|
|
339
|
-
writeFileSync(payloadPath, JSON.stringify(payload, null, 2), "utf-8");
|
|
340
|
-
return Response.json({ status: "exported", path: payloadPath, count: sanitized.length });
|
|
341
|
-
}
|
|
539
|
+
seam("Sense", `${event} → ${path}`);
|
|
540
|
+
trackEvent("seam", event, path, Math.round(performance.now() - _seamStart));
|
|
541
|
+
}
|
|
342
542
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
543
|
+
// ── Workspace Map ──
|
|
544
|
+
const wsMap = createWorkspaceMap();
|
|
545
|
+
_cleanupResources.wsMapGetTimer = wsMap.getTimer;
|
|
546
|
+
watcher.on("add", () => wsMap.markDirty());
|
|
547
|
+
watcher.on("unlink", () => wsMap.markDirty());
|
|
548
|
+
wsMap.get(); // initial build
|
|
549
|
+
|
|
550
|
+
// ── Build DaemonContext ──
|
|
551
|
+
const ctx: DaemonContext = {
|
|
552
|
+
state, db: db as unknown as DaemonContext["db"], ws: _ws, options,
|
|
553
|
+
insertEvent, insertAntibody, listAntibodies, antibodyIds: antibodyIds as unknown as DaemonContext["antibodyIds"],
|
|
554
|
+
countAntibodies: countAntibodies as unknown as DaemonContext["countAntibodies"],
|
|
555
|
+
getDailyAll: getDailyAll as unknown as DaemonContext["getDailyAll"],
|
|
556
|
+
insertTelemetry: insertTelemetry as unknown as DaemonContext["insertTelemetry"],
|
|
557
|
+
insertMistakeHistory: insertMistakeHistory as unknown as DaemonContext["insertMistakeHistory"],
|
|
558
|
+
queryMistakesByFile: queryMistakesByFile as unknown as DaemonContext["queryMistakesByFile"],
|
|
559
|
+
deleteMistakeOverflow: deleteMistakeOverflow as unknown as DaemonContext["deleteMistakeOverflow"],
|
|
560
|
+
seam, persistHologramStats, safeHologram,
|
|
561
|
+
getWorkspaceMap: wsMap.get, today, discoveryTargets: discovery.targets,
|
|
562
|
+
port: 0,
|
|
563
|
+
};
|
|
348
564
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
565
|
+
// ── MCP Mode ──
|
|
566
|
+
if (options.mcp) {
|
|
567
|
+
startMcpStdio(ctx);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
352
570
|
|
|
571
|
+
// ── HTTP Server ──
|
|
572
|
+
const server = Bun.serve({ port: 0, fetch: createHttpHandler(ctx, cleanup) });
|
|
353
573
|
const port = server.port;
|
|
574
|
+
ctx.port = port;
|
|
354
575
|
|
|
355
|
-
mkdirSync(
|
|
356
|
-
writeFileSync(
|
|
357
|
-
writeFileSync(
|
|
576
|
+
mkdirSync(_ws.afdDir, { recursive: true });
|
|
577
|
+
writeFileSync(_ws.pidFile, String(process.pid));
|
|
578
|
+
writeFileSync(_ws.portFile, String(port));
|
|
358
579
|
|
|
359
|
-
console.log(`[afd daemon] pid=${process.pid} port=${port}`);
|
|
360
|
-
|
|
361
|
-
process.on("uncaughtException", (err) => {
|
|
362
|
-
console.error("[afd daemon] FATAL:", err.message);
|
|
363
|
-
cleanup();
|
|
364
|
-
process.exit(1);
|
|
365
|
-
});
|
|
580
|
+
console.log(`[afd daemon] pid=${process.pid} port=${port} workspace=${_ws.root}`);
|
|
366
581
|
|
|
582
|
+
process.on("uncaughtException", (err) => { console.error("[afd daemon] FATAL:", err.message); cleanup(); process.exit(1); });
|
|
367
583
|
process.on("SIGTERM", () => { cleanup(); process.exit(0); });
|
|
368
584
|
process.on("SIGINT", () => { cleanup(); process.exit(0); });
|
|
369
585
|
}
|
|
370
586
|
|
|
371
|
-
main
|
|
587
|
+
if (import.meta.main) {
|
|
588
|
+
const mcp = process.argv.includes("--mcp") || process.env.AFD_MCP === "1";
|
|
589
|
+
main({ mcp });
|
|
590
|
+
}
|