autonomous-flow-daemon 1.1.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 +124 -164
- package/README.md +99 -170
- package/package.json +11 -5
- package/src/adapters/index.ts +246 -35
- package/src/cli.ts +71 -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/mcp.ts +129 -0
- package/src/commands/restart.ts +14 -0
- package/src/commands/score.ts +164 -96
- package/src/commands/start.ts +74 -15
- package/src/commands/stats.ts +103 -0
- package/src/commands/status.ts +157 -0
- package/src/commands/stop.ts +23 -4
- package/src/commands/sync.ts +253 -20
- package/src/commands/vaccine.ts +177 -0
- package/src/constants.ts +25 -1
- package/src/core/boast.ts +27 -12
- package/src/core/db.ts +74 -3
- 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 +43 -0
- package/src/core/immune.ts +8 -123
- 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 +27 -19
- 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 +439 -353
- package/src/daemon/types.ts +100 -0
- package/src/daemon/workspace-map.ts +92 -0
- package/src/platform.ts +23 -2
- package/src/version.ts +15 -0
package/src/daemon/server.ts
CHANGED
|
@@ -1,53 +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
|
-
import { calcHealMetrics, maybeHealBoast, formatHealLog, formatDormantLog, buildShiftSummary } from "../core/boast";
|
|
20
|
+
import { calcHealMetrics, maybeHealBoast, formatHealLog, formatDormantLog } from "../core/boast";
|
|
12
21
|
import { discoverWatchTargets } from "../core/discovery";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
lastEvent: string | null;
|
|
29
|
-
lastEventAt: number | null;
|
|
30
|
-
watchedFiles: string[];
|
|
31
|
-
hologramStats: HologramStats;
|
|
32
|
-
ecosystems: DetectionResult[];
|
|
33
|
-
autoHealCount: number;
|
|
34
|
-
autoHealLog: { id: string; at: number }[];
|
|
35
|
-
// Suppression safety: recent unlink timestamps for mass-event detection
|
|
36
|
-
recentUnlinks: number[];
|
|
37
|
-
// Suppression safety: per-file first-tap timestamps for double-tap detection
|
|
38
|
-
firstTapTimestamps: Map<string, number>;
|
|
39
|
-
suppressionSkippedCount: number;
|
|
40
|
-
dormantTransitions: { antibodyId: string; at: number }[];
|
|
41
|
-
totalFileBytesSaved: number;
|
|
42
|
-
}
|
|
43
|
-
|
|
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 ──
|
|
44
37
|
const state: DaemonState = {
|
|
45
38
|
startedAt: Date.now(),
|
|
46
39
|
filesDetected: 0,
|
|
47
40
|
lastEvent: null,
|
|
48
41
|
lastEventAt: null,
|
|
49
|
-
watchedFiles:
|
|
50
|
-
hologramStats: { totalRequests: 0, totalOriginalChars: 0, totalHologramChars: 0 },
|
|
42
|
+
watchedFiles: new Set(),
|
|
43
|
+
hologramStats: { totalRequests: 0, totalOriginalChars: 0, totalHologramChars: 0, sessionOriginalChars: 0, sessionHologramChars: 0 },
|
|
51
44
|
ecosystems: [],
|
|
52
45
|
autoHealCount: 0,
|
|
53
46
|
autoHealLog: [],
|
|
@@ -56,22 +49,68 @@ const state: DaemonState = {
|
|
|
56
49
|
suppressionSkippedCount: 0,
|
|
57
50
|
dormantTransitions: [],
|
|
58
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(),
|
|
59
57
|
};
|
|
60
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
|
+
|
|
61
70
|
function cleanup() {
|
|
62
|
-
try {
|
|
63
|
-
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 {}
|
|
64
79
|
}
|
|
65
80
|
|
|
66
|
-
|
|
67
|
-
|
|
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
|
+
};
|
|
68
104
|
}
|
|
69
105
|
|
|
106
|
+
// ══════════════════════════════════════════════════════════
|
|
70
107
|
export function main(options: DaemonOptions = {}) {
|
|
71
|
-
// Detect ecosystem at startup
|
|
72
108
|
state.ecosystems = detectEcosystem(process.cwd());
|
|
73
109
|
|
|
74
110
|
const db = initDb();
|
|
111
|
+
_cleanupResources.db = db;
|
|
112
|
+
|
|
113
|
+
// ── Prepared statements ──
|
|
75
114
|
const insertEvent = db.prepare("INSERT INTO events (type, path, timestamp) VALUES (?, ?, ?)");
|
|
76
115
|
const insertAntibody = db.prepare(
|
|
77
116
|
"INSERT OR REPLACE INTO antibodies (id, pattern_type, file_target, patch_op, created_at) VALUES (?, ?, ?, ?, datetime('now'))"
|
|
@@ -81,49 +120,219 @@ export function main(options: DaemonOptions = {}) {
|
|
|
81
120
|
const countAntibodies = db.prepare("SELECT COUNT(*) as cnt FROM antibodies");
|
|
82
121
|
const insertUnlinkLog = db.prepare("INSERT INTO unlink_log (file_path, timestamp) VALUES (?, ?)");
|
|
83
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 = ?");
|
|
84
124
|
const setAntibodyDormant = db.prepare("UPDATE antibodies SET dormant = 1 WHERE id = ?");
|
|
85
125
|
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
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 */ }
|
|
89
146
|
}
|
|
90
147
|
|
|
91
|
-
// ──
|
|
92
|
-
|
|
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 ──
|
|
93
244
|
function seedAntibodies() {
|
|
94
245
|
const immuneFiles = [
|
|
95
246
|
{ id: "IMM-001", path: ".claudeignore" },
|
|
96
247
|
{ id: "IMM-002", path: ".claude/hooks.json" },
|
|
97
248
|
{ id: "IMM-003", path: "CLAUDE.md" },
|
|
98
249
|
];
|
|
99
|
-
|
|
100
250
|
for (const { id, path: filePath } of immuneFiles) {
|
|
101
251
|
if (existsSync(filePath)) {
|
|
102
252
|
const content = readFileSync(filePath, "utf-8");
|
|
103
253
|
const patches: PatchOp[] = [{ op: "add", path: `/${filePath}`, value: content }];
|
|
104
254
|
insertAntibody.run(id, "auto-seed", filePath, JSON.stringify(patches));
|
|
255
|
+
state.fileSnapshots.set(filePath, content);
|
|
105
256
|
seam("Adapt", `Antibody ${id} seeded for ${filePath} (${content.length} bytes)`);
|
|
106
257
|
}
|
|
107
258
|
}
|
|
108
259
|
}
|
|
109
260
|
|
|
261
|
+
const selfWrites = new Set<string>();
|
|
110
262
|
seedAntibodies();
|
|
111
263
|
|
|
112
|
-
// ──
|
|
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
|
+
}
|
|
113
306
|
|
|
114
|
-
|
|
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 ──
|
|
115
331
|
function isMassEvent(now: number): boolean {
|
|
116
|
-
// Prune old entries beyond the window
|
|
117
332
|
state.recentUnlinks = state.recentUnlinks.filter(t => now - t < MASS_EVENT_WINDOW_MS);
|
|
118
333
|
return state.recentUnlinks.length > MASS_EVENT_THRESHOLD;
|
|
119
334
|
}
|
|
120
335
|
|
|
121
|
-
/** Clear first-tap state when mass event is detected (bulk ops are not intentional user deletes) */
|
|
122
|
-
function clearTapsOnMassEvent() {
|
|
123
|
-
state.firstTapTimestamps.clear();
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** Auto-heal: re-apply patches for a given antibody, with metrics & boast */
|
|
127
336
|
function autoHealFile(antibodyId: string, fileTarget: string, patchOp: string) {
|
|
128
337
|
const t0 = performance.now();
|
|
129
338
|
try {
|
|
@@ -133,6 +342,7 @@ export function main(options: DaemonOptions = {}) {
|
|
|
133
342
|
for (const patch of patches) {
|
|
134
343
|
if (patch.op === "add" && patch.value) {
|
|
135
344
|
const targetPath = resolve(patch.path.replace(/^\//, ""));
|
|
345
|
+
assertInsideWorkspace(targetPath, _ws.root);
|
|
136
346
|
writeFileSync(targetPath, patch.value, "utf-8");
|
|
137
347
|
bytesWritten += patch.value.length;
|
|
138
348
|
}
|
|
@@ -141,364 +351,240 @@ export function main(options: DaemonOptions = {}) {
|
|
|
141
351
|
state.autoHealCount++;
|
|
142
352
|
state.totalFileBytesSaved += bytesWritten;
|
|
143
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);
|
|
144
356
|
if (state.autoHealLog.length > 100) state.autoHealLog.shift();
|
|
145
|
-
|
|
146
|
-
// Delightful logging: metrics + occasional boast
|
|
147
357
|
const metrics = calcHealMetrics(bytesWritten, healMs);
|
|
148
|
-
const boast = maybeHealBoast(5);
|
|
358
|
+
const boast = maybeHealBoast(5);
|
|
149
359
|
const fileName = fileTarget.split("/").pop() ?? fileTarget;
|
|
150
|
-
console.log(formatHealLog(fileName, metrics, boast));
|
|
360
|
+
(options.mcp ? console.error : console.log)(formatHealLog(fileName, metrics, boast));
|
|
151
361
|
} catch (err) {
|
|
152
362
|
seam("Mutate", `FAILED to restore ${fileTarget}: ${err instanceof Error ? err.message : String(err)}`);
|
|
153
363
|
}
|
|
154
364
|
}
|
|
155
365
|
|
|
156
|
-
/**
|
|
157
|
-
* Handle unlink event with Double-Tap and Mass-Event heuristics.
|
|
158
|
-
* Returns true if the event was handled (healed or made dormant).
|
|
159
|
-
*/
|
|
160
|
-
/**
|
|
161
|
-
* Handle unlink event with Double-Tap and Mass-Event heuristics.
|
|
162
|
-
* Returns: "healed" | "dormant" | null
|
|
163
|
-
*/
|
|
164
366
|
function handleUnlink(filePath: string, now: number): "healed" | "dormant" | null {
|
|
165
|
-
// Record for mass-event detection
|
|
166
367
|
state.recentUnlinks.push(now);
|
|
167
368
|
insertUnlinkLog.run(filePath, now);
|
|
168
|
-
|
|
169
|
-
// Mass-event check: if >3 unlinks in 1s, skip ALL suppression logic
|
|
170
|
-
if (isMassEvent(now)) {
|
|
171
|
-
state.suppressionSkippedCount++;
|
|
172
|
-
clearTapsOnMassEvent();
|
|
173
|
-
return null; // Do nothing — likely git checkout or bulk operation
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// 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; }
|
|
177
370
|
const antibody = findAntibodyByFile.get(filePath) as { id: string; dormant: number } | null;
|
|
178
|
-
if (!antibody) return null;
|
|
179
|
-
|
|
180
|
-
// Fetch full antibody data for healing
|
|
181
|
-
const fullAntibody = db.prepare("SELECT * FROM antibodies WHERE id = ?").get(antibody.id) as {
|
|
182
|
-
id: string; patch_op: string; file_target: string;
|
|
183
|
-
} | null;
|
|
371
|
+
if (!antibody) return null;
|
|
372
|
+
const fullAntibody = findAntibodyById.get(antibody.id) as { id: string; patch_op: string; file_target: string } | null;
|
|
184
373
|
if (!fullAntibody) return null;
|
|
185
|
-
|
|
186
|
-
// Double-Tap detection
|
|
187
374
|
const previousTap = state.firstTapTimestamps.get(filePath);
|
|
188
|
-
|
|
189
375
|
if (previousTap && (now - previousTap) < DOUBLE_TAP_WINDOW_MS) {
|
|
190
|
-
// SECOND TAP within window → user is intentional → make dormant
|
|
191
376
|
setAntibodyDormant.run(antibody.id);
|
|
192
377
|
state.firstTapTimestamps.delete(filePath);
|
|
193
378
|
state.dormantTransitions.push({ antibodyId: antibody.id, at: now });
|
|
194
|
-
|
|
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));
|
|
195
382
|
return "dormant";
|
|
196
383
|
}
|
|
197
|
-
|
|
198
|
-
// FIRST TAP: record timestamp and auto-heal
|
|
199
384
|
state.firstTapTimestamps.set(filePath, now);
|
|
385
|
+
quarantineFile(filePath, null);
|
|
200
386
|
autoHealFile(fullAntibody.id, fullAntibody.file_target, fullAntibody.patch_op);
|
|
201
387
|
return "healed";
|
|
202
388
|
}
|
|
203
389
|
|
|
204
|
-
// ── Smart Discovery
|
|
390
|
+
// ── Smart Discovery + Watcher ──
|
|
205
391
|
const discovery = discoverWatchTargets(WATCH_TARGETS);
|
|
206
392
|
seam("Sense", `Smart Discovery: ${discovery.targets.length} targets found in ${discovery.elapsedMs}ms`);
|
|
207
393
|
if (discovery.discoveredCount > 0) {
|
|
208
394
|
seam("Sense", `Discovered ${discovery.discoveredCount} extra: ${discovery.targets.filter(t => !WATCH_TARGETS.includes(t)).join(", ")}`);
|
|
209
395
|
}
|
|
210
396
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
397
|
+
const watcher = watch(discovery.targets, { ignoreInitial: false, persistent: true, atomic: 100 });
|
|
398
|
+
_cleanupResources.watcher = watcher;
|
|
399
|
+
|
|
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>();
|
|
402
|
+
|
|
403
|
+
function isInternalPath(p: string): boolean {
|
|
404
|
+
const normalized = p.replace(/\\/g, "/");
|
|
405
|
+
return normalized.startsWith(".afd/") || normalized.includes("/.afd/");
|
|
406
|
+
}
|
|
407
|
+
|
|
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
|
+
}
|
|
419
|
+
|
|
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`);
|
|
431
|
+
}
|
|
432
|
+
for (const e of events) handleFileEvent(e.event, e.path);
|
|
433
|
+
},
|
|
217
434
|
});
|
|
435
|
+
_cleanupResources.eventBatcher = eventBatcher;
|
|
218
436
|
|
|
437
|
+
// ── Watcher Event Handler (S.E.A.M) ──
|
|
219
438
|
watcher.on("all", (event, path) => {
|
|
439
|
+
if (isInternalPath(path)) return;
|
|
440
|
+
if (selfWrites.has(path)) return;
|
|
441
|
+
eventBatcher.push(event, path);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
function handleFileEvent(event: string, path: string) {
|
|
445
|
+
const _seamStart = performance.now();
|
|
446
|
+
|
|
220
447
|
state.filesDetected++;
|
|
221
448
|
state.lastEvent = `${event}:${path}`;
|
|
222
449
|
state.lastEventAt = Date.now();
|
|
223
|
-
|
|
224
|
-
state.watchedFiles.push(path);
|
|
225
|
-
}
|
|
450
|
+
state.watchedFiles.add(path);
|
|
226
451
|
insertEvent.run(event, path, Date.now());
|
|
227
452
|
|
|
228
|
-
|
|
229
|
-
seam("Sense", `${event} → ${path}`);
|
|
453
|
+
if (event === "add" || event === "addDir") { seam("Sense", `${event} → ${path}`); snapshotFile(path); trackEvent("seam", "sense", null, Math.round(performance.now() - _seamStart)); return; }
|
|
230
454
|
|
|
231
455
|
if (event === "unlink") {
|
|
232
|
-
seam("
|
|
456
|
+
seam("Sense", `unlink → ${path}`);
|
|
457
|
+
state.fileSnapshots.delete(path);
|
|
458
|
+
state.watchedFiles.delete(path);
|
|
233
459
|
const result = handleUnlink(path, Date.now());
|
|
234
460
|
if (result === "healed") {
|
|
235
|
-
|
|
236
|
-
|
|
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);
|
|
237
466
|
watcher.add(path);
|
|
238
467
|
} else if (result === "dormant") {
|
|
239
|
-
seam("Adapt", `Double-tap confirmed — ${path}
|
|
240
|
-
} else {
|
|
241
|
-
seam("Extract", `No antibody found for ${path} — skipped`);
|
|
242
|
-
}
|
|
243
|
-
} else if (event === "change") {
|
|
244
|
-
// Re-seed antibody on file change so restore always has latest content
|
|
245
|
-
const immuneMap: Record<string, string> = {
|
|
246
|
-
".claudeignore": "IMM-001",
|
|
247
|
-
".claude/hooks.json": "IMM-002",
|
|
248
|
-
"CLAUDE.md": "IMM-003",
|
|
249
|
-
};
|
|
250
|
-
const abId = immuneMap[path];
|
|
251
|
-
if (abId && existsSync(path)) {
|
|
252
|
-
const content = readFileSync(path, "utf-8");
|
|
253
|
-
const patches: PatchOp[] = [{ op: "add", path: `/${path}`, value: content }];
|
|
254
|
-
insertAntibody.run(abId, "auto-seed", path, JSON.stringify(patches));
|
|
255
|
-
seam("Adapt", `Antibody ${abId} refreshed for ${path}`);
|
|
468
|
+
seam("Adapt", `Double-tap confirmed — user intentionally deleted ${path}, antibody deactivated`);
|
|
256
469
|
}
|
|
470
|
+
trackEvent("seam", event, path, Math.round(performance.now() - _seamStart));
|
|
471
|
+
return;
|
|
257
472
|
}
|
|
258
|
-
});
|
|
259
473
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
};
|
|
281
|
-
process.stdout.write(JSON.stringify(response) + "\n");
|
|
282
|
-
} catch {
|
|
283
|
-
// Crash-only: malformed input is ignored
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
})();
|
|
287
|
-
return; // file watcher + stdin loop keep process alive
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// HTTP server for IPC
|
|
291
|
-
const server = Bun.serve({
|
|
292
|
-
port: 0,
|
|
293
|
-
async fetch(req) {
|
|
294
|
-
const url = new URL(req.url);
|
|
295
|
-
|
|
296
|
-
if (url.pathname === "/health") {
|
|
297
|
-
return Response.json({ status: "alive", pid: process.pid });
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
if (url.pathname === "/mini-status") {
|
|
301
|
-
const last = state.autoHealLog.length > 0
|
|
302
|
-
? state.autoHealLog[state.autoHealLog.length - 1].id
|
|
303
|
-
: null;
|
|
304
|
-
return Response.json({
|
|
305
|
-
status: "ON",
|
|
306
|
-
healed_count: state.autoHealCount,
|
|
307
|
-
last_healed: last,
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
if (url.pathname === "/hologram") {
|
|
312
|
-
const file = url.searchParams.get("file");
|
|
313
|
-
if (!file) return Response.json({ error: "?file= required" }, { status: 400 });
|
|
314
|
-
try {
|
|
315
|
-
const absPath = resolve(file);
|
|
316
|
-
const source = readFileSync(absPath, "utf-8");
|
|
317
|
-
const result = generateHologram(file, source);
|
|
318
|
-
state.hologramStats.totalRequests++;
|
|
319
|
-
state.hologramStats.totalOriginalChars += result.originalLength;
|
|
320
|
-
state.hologramStats.totalHologramChars += result.hologramLength;
|
|
321
|
-
return Response.json(result);
|
|
322
|
-
} catch (err: unknown) {
|
|
323
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
324
|
-
return Response.json({ error: msg }, { status: 404 });
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
if (url.pathname === "/diagnose") {
|
|
329
|
-
const raw = url.searchParams.get("raw") === "true";
|
|
330
|
-
const known = (antibodyIds.all() as { id: string }[]).map(r => r.id);
|
|
331
|
-
const result = diagnose(known, { raw });
|
|
332
|
-
return Response.json(result);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
if (url.pathname === "/antibodies") {
|
|
336
|
-
const rows = listAntibodies.all();
|
|
337
|
-
return Response.json({ antibodies: rows });
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
if (url.pathname === "/antibodies/learn" && req.method === "POST") {
|
|
341
|
-
try {
|
|
342
|
-
const body = await req.json() as {
|
|
343
|
-
id: string;
|
|
344
|
-
patternType: string;
|
|
345
|
-
fileTarget: string;
|
|
346
|
-
patches: PatchOp[];
|
|
347
|
-
};
|
|
348
|
-
insertAntibody.run(
|
|
349
|
-
body.id,
|
|
350
|
-
body.patternType,
|
|
351
|
-
body.fileTarget,
|
|
352
|
-
JSON.stringify(body.patches)
|
|
353
|
-
);
|
|
354
|
-
return Response.json({ status: "learned", id: body.id });
|
|
355
|
-
} catch (err: unknown) {
|
|
356
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
357
|
-
return Response.json({ error: msg }, { status: 400 });
|
|
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)`);
|
|
358
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)`);
|
|
359
500
|
}
|
|
360
501
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
if (
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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`);
|
|
372
531
|
}
|
|
532
|
+
} else {
|
|
533
|
+
state.fileSnapshots.set(path, newContent);
|
|
373
534
|
}
|
|
535
|
+
trackEvent("seam", "change", path, Math.round(performance.now() - _seamStart));
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
374
538
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
const abCount = countAntibodies.get() as { cnt: number };
|
|
379
|
-
const hs = state.hologramStats;
|
|
380
|
-
const globalSavings = hs.totalOriginalChars > 0
|
|
381
|
-
? Math.round((hs.totalOriginalChars - hs.totalHologramChars) / hs.totalOriginalChars * 1000) / 10
|
|
382
|
-
: 0;
|
|
383
|
-
return Response.json({
|
|
384
|
-
uptime,
|
|
385
|
-
filesDetected: state.filesDetected,
|
|
386
|
-
totalEvents: eventCount.cnt,
|
|
387
|
-
lastEvent: state.lastEvent,
|
|
388
|
-
lastEventAt: state.lastEventAt,
|
|
389
|
-
watchedFiles: state.watchedFiles,
|
|
390
|
-
watchTargets: discovery.targets,
|
|
391
|
-
hologram: {
|
|
392
|
-
requests: hs.totalRequests,
|
|
393
|
-
originalChars: hs.totalOriginalChars,
|
|
394
|
-
hologramChars: hs.totalHologramChars,
|
|
395
|
-
savings: globalSavings,
|
|
396
|
-
},
|
|
397
|
-
immune: {
|
|
398
|
-
antibodies: abCount.cnt,
|
|
399
|
-
autoHealed: state.autoHealCount,
|
|
400
|
-
lastAutoHeal: state.autoHealLog.length > 0
|
|
401
|
-
? state.autoHealLog[state.autoHealLog.length - 1]
|
|
402
|
-
: null,
|
|
403
|
-
},
|
|
404
|
-
ecosystem: {
|
|
405
|
-
detected: state.ecosystems.map(e => ({
|
|
406
|
-
name: e.adapter.name,
|
|
407
|
-
confidence: e.confidence,
|
|
408
|
-
schema: e.adapter.getHarnessSchema(),
|
|
409
|
-
})),
|
|
410
|
-
primary: state.ecosystems[0]?.adapter.name ?? "Unknown",
|
|
411
|
-
},
|
|
412
|
-
suppression: {
|
|
413
|
-
massEventsSkipped: state.suppressionSkippedCount,
|
|
414
|
-
dormantTransitions: state.dormantTransitions.length,
|
|
415
|
-
activeTaps: state.firstTapTimestamps.size,
|
|
416
|
-
},
|
|
417
|
-
});
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
if (url.pathname === "/sync") {
|
|
421
|
-
const rows = listAntibodies.all() as {
|
|
422
|
-
id: string;
|
|
423
|
-
pattern_type: string;
|
|
424
|
-
file_target: string;
|
|
425
|
-
patch_op: string;
|
|
426
|
-
created_at: string;
|
|
427
|
-
}[];
|
|
428
|
-
// Sanitize: strip absolute paths, keep only relative patterns
|
|
429
|
-
const sanitized = rows.map(r => {
|
|
430
|
-
const patches = JSON.parse(r.patch_op) as PatchOp[];
|
|
431
|
-
const cleanPatches = patches.map(p => ({
|
|
432
|
-
...p,
|
|
433
|
-
// Ensure paths are relative (strip any leading drive/abs prefix)
|
|
434
|
-
path: p.path.replace(/^[A-Za-z]:/, "").replace(/\\/g, "/"),
|
|
435
|
-
// Strip absolute paths from values
|
|
436
|
-
value: p.value?.replace(/[A-Za-z]:\\[^\s"']*/g, "<redacted>"),
|
|
437
|
-
}));
|
|
438
|
-
return {
|
|
439
|
-
id: r.id,
|
|
440
|
-
patternType: r.pattern_type,
|
|
441
|
-
fileTarget: r.file_target.replace(/^[A-Za-z]:/, "").replace(/\\/g, "/"),
|
|
442
|
-
patches: cleanPatches,
|
|
443
|
-
learnedAt: r.created_at,
|
|
444
|
-
};
|
|
445
|
-
});
|
|
446
|
-
const payload = {
|
|
447
|
-
version: "0.1.0",
|
|
448
|
-
generatedAt: new Date().toISOString(),
|
|
449
|
-
ecosystem: state.ecosystems[0]?.adapter.name ?? "Unknown",
|
|
450
|
-
antibodyCount: sanitized.length,
|
|
451
|
-
antibodies: sanitized,
|
|
452
|
-
};
|
|
453
|
-
// Write payload to disk
|
|
454
|
-
const payloadPath = resolve(AFD_DIR, "global-vaccine-payload.json");
|
|
455
|
-
writeFileSync(payloadPath, JSON.stringify(payload, null, 2), "utf-8");
|
|
456
|
-
return Response.json({ status: "exported", path: payloadPath, count: sanitized.length });
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
if (url.pathname === "/shift-summary") {
|
|
460
|
-
const uptime = Math.floor((Date.now() - state.startedAt) / 1000);
|
|
461
|
-
const eventCount = db.query("SELECT COUNT(*) as cnt FROM events").get() as { cnt: number };
|
|
462
|
-
const summary = buildShiftSummary({
|
|
463
|
-
uptimeSeconds: uptime,
|
|
464
|
-
totalEvents: eventCount.cnt,
|
|
465
|
-
healsPerformed: state.autoHealCount,
|
|
466
|
-
totalFileBytesSaved: state.totalFileBytesSaved,
|
|
467
|
-
suppressionsSkipped: state.suppressionSkippedCount,
|
|
468
|
-
dormantTransitions: state.dormantTransitions.length,
|
|
469
|
-
});
|
|
470
|
-
return Response.json(summary);
|
|
471
|
-
}
|
|
539
|
+
seam("Sense", `${event} → ${path}`);
|
|
540
|
+
trackEvent("seam", event, path, Math.round(performance.now() - _seamStart));
|
|
541
|
+
}
|
|
472
542
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
+
};
|
|
478
564
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
565
|
+
// ── MCP Mode ──
|
|
566
|
+
if (options.mcp) {
|
|
567
|
+
startMcpStdio(ctx);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
482
570
|
|
|
571
|
+
// ── HTTP Server ──
|
|
572
|
+
const server = Bun.serve({ port: 0, fetch: createHttpHandler(ctx, cleanup) });
|
|
483
573
|
const port = server.port;
|
|
574
|
+
ctx.port = port;
|
|
484
575
|
|
|
485
|
-
mkdirSync(
|
|
486
|
-
writeFileSync(
|
|
487
|
-
writeFileSync(
|
|
488
|
-
|
|
489
|
-
console.log(`[afd daemon] pid=${process.pid} port=${port}`);
|
|
576
|
+
mkdirSync(_ws.afdDir, { recursive: true });
|
|
577
|
+
writeFileSync(_ws.pidFile, String(process.pid));
|
|
578
|
+
writeFileSync(_ws.portFile, String(port));
|
|
490
579
|
|
|
491
|
-
|
|
492
|
-
console.error("[afd daemon] FATAL:", err.message);
|
|
493
|
-
cleanup();
|
|
494
|
-
process.exit(1);
|
|
495
|
-
});
|
|
580
|
+
console.log(`[afd daemon] pid=${process.pid} port=${port} workspace=${_ws.root}`);
|
|
496
581
|
|
|
582
|
+
process.on("uncaughtException", (err) => { console.error("[afd daemon] FATAL:", err.message); cleanup(); process.exit(1); });
|
|
497
583
|
process.on("SIGTERM", () => { cleanup(); process.exit(0); });
|
|
498
584
|
process.on("SIGINT", () => { cleanup(); process.exit(0); });
|
|
499
585
|
}
|
|
500
586
|
|
|
501
|
-
// Auto-execute when run directly (not imported)
|
|
502
587
|
if (import.meta.main) {
|
|
503
|
-
|
|
588
|
+
const mcp = process.argv.includes("--mcp") || process.env.AFD_MCP === "1";
|
|
589
|
+
main({ mcp });
|
|
504
590
|
}
|