autonomous-flow-daemon 1.0.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.
@@ -0,0 +1,371 @@
1
+ import { watch } from "chokidar";
2
+ import { mkdirSync, writeFileSync, unlinkSync, readFileSync, existsSync } from "fs";
3
+ import { resolve } from "path";
4
+ import { AFD_DIR, PID_FILE, PORT_FILE, WATCH_TARGETS } from "../constants";
5
+ import { initDb } from "../core/db";
6
+ import { generateHologram } from "../core/hologram";
7
+ import { diagnose } from "../core/immune";
8
+ import type { PatchOp } from "../core/immune";
9
+ import { detectEcosystem } from "../adapters/index";
10
+ import type { DetectionResult } from "../adapters/index";
11
+
12
+ // ── Suppression Safety Constants ──
13
+ const DOUBLE_TAP_WINDOW_MS = 60_000; // 60 seconds
14
+ const MASS_EVENT_THRESHOLD = 3; // >3 unlinks in 1 second
15
+ const MASS_EVENT_WINDOW_MS = 1_000; // 1 second window
16
+
17
+ interface HologramStats {
18
+ totalRequests: number;
19
+ totalOriginalChars: number;
20
+ totalHologramChars: number;
21
+ }
22
+
23
+ interface DaemonState {
24
+ startedAt: number;
25
+ filesDetected: number;
26
+ lastEvent: string | null;
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
+
41
+ const state: DaemonState = {
42
+ startedAt: Date.now(),
43
+ filesDetected: 0,
44
+ lastEvent: null,
45
+ lastEventAt: null,
46
+ watchedFiles: [],
47
+ hologramStats: { totalRequests: 0, totalOriginalChars: 0, totalHologramChars: 0 },
48
+ ecosystems: [],
49
+ autoHealCount: 0,
50
+ autoHealLog: [],
51
+ recentUnlinks: [],
52
+ firstTapTimestamps: new Map(),
53
+ suppressionSkippedCount: 0,
54
+ dormantTransitions: [],
55
+ };
56
+
57
+ function cleanup() {
58
+ try { unlinkSync(PID_FILE); } catch {}
59
+ try { unlinkSync(PORT_FILE); } catch {}
60
+ }
61
+
62
+ function main() {
63
+ // Detect ecosystem at startup
64
+ state.ecosystems = detectEcosystem(process.cwd());
65
+
66
+ const db = initDb();
67
+ const insertEvent = db.prepare("INSERT INTO events (type, path, timestamp) VALUES (?, ?, ?)");
68
+ const insertAntibody = db.prepare(
69
+ "INSERT OR REPLACE INTO antibodies (id, pattern_type, file_target, patch_op, created_at) VALUES (?, ?, ?, ?, datetime('now'))"
70
+ );
71
+ const listAntibodies = db.prepare("SELECT * FROM antibodies ORDER BY created_at DESC");
72
+ const antibodyIds = db.prepare("SELECT id FROM antibodies WHERE dormant = 0");
73
+ const countAntibodies = db.prepare("SELECT COUNT(*) as cnt FROM antibodies");
74
+ const insertUnlinkLog = db.prepare("INSERT INTO unlink_log (file_path, timestamp) VALUES (?, ?)");
75
+ const findAntibodyByFile = db.prepare("SELECT id, dormant FROM antibodies WHERE file_target = ? AND dormant = 0");
76
+ const setAntibodyDormant = db.prepare("UPDATE antibodies SET dormant = 1 WHERE id = ?");
77
+
78
+ // ── Suppression Safety: Helper Functions ──
79
+
80
+ /** Check if we're in a mass-event burst (>3 unlinks within 1 second) */
81
+ function isMassEvent(now: number): boolean {
82
+ // Prune old entries beyond the window
83
+ state.recentUnlinks = state.recentUnlinks.filter(t => now - t < MASS_EVENT_WINDOW_MS);
84
+ return state.recentUnlinks.length > MASS_EVENT_THRESHOLD;
85
+ }
86
+
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
+ function autoHealFile(antibodyId: string, fileTarget: string, patchOp: string) {
94
+ try {
95
+ const patches = JSON.parse(patchOp) as PatchOp[];
96
+ for (const patch of patches) {
97
+ if (patch.op === "add" && patch.value) {
98
+ const targetPath = resolve(patch.path.replace(/^\//, ""));
99
+ writeFileSync(targetPath, patch.value, "utf-8");
100
+ }
101
+ }
102
+ state.autoHealCount++;
103
+ state.autoHealLog.push({ id: antibodyId, at: Date.now() });
104
+ if (state.autoHealLog.length > 100) state.autoHealLog.shift();
105
+ } catch {
106
+ // Crash-only: if healing fails, let it go
107
+ }
108
+ }
109
+
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
116
+ state.recentUnlinks.push(now);
117
+ 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
127
+ const antibody = findAntibodyByFile.get(filePath) as { id: string; dormant: number } | null;
128
+ if (!antibody) return false; // No antibody for this file
129
+
130
+ // Fetch full antibody data for healing
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
137
+ const previousTap = state.firstTapTimestamps.get(filePath);
138
+
139
+ if (previousTap && (now - previousTap) < DOUBLE_TAP_WINDOW_MS) {
140
+ // SECOND TAP within window → user is intentional → make dormant
141
+ setAntibodyDormant.run(antibody.id);
142
+ state.firstTapTimestamps.delete(filePath);
143
+ state.dormantTransitions.push({ antibodyId: antibody.id, at: now });
144
+ return true; // Dormant — do NOT heal
145
+ }
146
+
147
+ // FIRST TAP: record timestamp and auto-heal
148
+ state.firstTapTimestamps.set(filePath, now);
149
+ autoHealFile(fullAntibody.id, fullAntibody.file_target, fullAntibody.patch_op);
150
+ return true;
151
+ }
152
+
153
+ // File watcher
154
+ const watcher = watch(WATCH_TARGETS, {
155
+ ignoreInitial: false,
156
+ persistent: true,
157
+ });
158
+
159
+ watcher.on("all", (event, path) => {
160
+ state.filesDetected++;
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());
167
+
168
+ // Suppression safety: handle unlink events
169
+ if (event === "unlink") {
170
+ handleUnlink(path, Date.now());
171
+ }
172
+ });
173
+
174
+ // HTTP server for IPC
175
+ const server = Bun.serve({
176
+ port: 0,
177
+ async fetch(req) {
178
+ const url = new URL(req.url);
179
+
180
+ if (url.pathname === "/health") {
181
+ return Response.json({ status: "alive", pid: process.pid });
182
+ }
183
+
184
+ if (url.pathname === "/mini-status") {
185
+ const last = state.autoHealLog.length > 0
186
+ ? state.autoHealLog[state.autoHealLog.length - 1].id
187
+ : null;
188
+ return Response.json({
189
+ status: "ON",
190
+ healed_count: state.autoHealCount,
191
+ last_healed: last,
192
+ });
193
+ }
194
+
195
+ if (url.pathname === "/hologram") {
196
+ const file = url.searchParams.get("file");
197
+ if (!file) return Response.json({ error: "?file= required" }, { status: 400 });
198
+ try {
199
+ const absPath = resolve(file);
200
+ const source = readFileSync(absPath, "utf-8");
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
+ }
211
+
212
+ if (url.pathname === "/diagnose") {
213
+ const raw = url.searchParams.get("raw") === "true";
214
+ const known = (antibodyIds.all() as { id: string }[]).map(r => r.id);
215
+ const result = diagnose(known, { raw });
216
+ return Response.json(result);
217
+ }
218
+
219
+ if (url.pathname === "/antibodies") {
220
+ const rows = listAntibodies.all();
221
+ return Response.json({ antibodies: rows });
222
+ }
223
+
224
+ if (url.pathname === "/antibodies/learn" && req.method === "POST") {
225
+ try {
226
+ const body = await req.json() as {
227
+ id: string;
228
+ patternType: string;
229
+ fileTarget: string;
230
+ patches: PatchOp[];
231
+ };
232
+ insertAntibody.run(
233
+ body.id,
234
+ body.patternType,
235
+ body.fileTarget,
236
+ JSON.stringify(body.patches)
237
+ );
238
+ return Response.json({ status: "learned", id: body.id });
239
+ } catch (err: unknown) {
240
+ const msg = err instanceof Error ? err.message : String(err);
241
+ return Response.json({ error: msg }, { status: 400 });
242
+ }
243
+ }
244
+
245
+ if (url.pathname === "/auto-heal/record" && req.method === "POST") {
246
+ try {
247
+ const body = await req.json() as { id: string };
248
+ state.autoHealCount++;
249
+ state.autoHealLog.push({ id: body.id, at: Date.now() });
250
+ // Keep log bounded
251
+ if (state.autoHealLog.length > 100) state.autoHealLog.shift();
252
+ return Response.json({ status: "recorded", total: state.autoHealCount });
253
+ } catch (err: unknown) {
254
+ const msg = err instanceof Error ? err.message : String(err);
255
+ return Response.json({ error: msg }, { status: 400 });
256
+ }
257
+ }
258
+
259
+ if (url.pathname === "/score") {
260
+ const uptime = Math.floor((Date.now() - state.startedAt) / 1000);
261
+ const eventCount = db.query("SELECT COUNT(*) as cnt FROM events").get() as { cnt: number };
262
+ const abCount = countAntibodies.get() as { cnt: number };
263
+ const hs = state.hologramStats;
264
+ const globalSavings = hs.totalOriginalChars > 0
265
+ ? Math.round((hs.totalOriginalChars - hs.totalHologramChars) / hs.totalOriginalChars * 1000) / 10
266
+ : 0;
267
+ return Response.json({
268
+ uptime,
269
+ filesDetected: state.filesDetected,
270
+ totalEvents: eventCount.cnt,
271
+ lastEvent: state.lastEvent,
272
+ lastEventAt: state.lastEventAt,
273
+ watchedFiles: state.watchedFiles,
274
+ watchTargets: WATCH_TARGETS,
275
+ hologram: {
276
+ requests: hs.totalRequests,
277
+ originalChars: hs.totalOriginalChars,
278
+ hologramChars: hs.totalHologramChars,
279
+ savings: globalSavings,
280
+ },
281
+ immune: {
282
+ antibodies: abCount.cnt,
283
+ autoHealed: state.autoHealCount,
284
+ lastAutoHeal: state.autoHealLog.length > 0
285
+ ? state.autoHealLog[state.autoHealLog.length - 1]
286
+ : null,
287
+ },
288
+ ecosystem: {
289
+ detected: state.ecosystems.map(e => ({
290
+ name: e.adapter.name,
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
+ });
302
+ }
303
+
304
+ if (url.pathname === "/sync") {
305
+ const rows = listAntibodies.all() as {
306
+ id: string;
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
+ }
342
+
343
+ if (url.pathname === "/stop") {
344
+ cleanup();
345
+ setTimeout(() => process.exit(0), 100);
346
+ return Response.json({ status: "stopping" });
347
+ }
348
+
349
+ return Response.json({ error: "not found" }, { status: 404 });
350
+ },
351
+ });
352
+
353
+ const port = server.port;
354
+
355
+ mkdirSync(AFD_DIR, { recursive: true });
356
+ writeFileSync(PID_FILE, String(process.pid));
357
+ writeFileSync(PORT_FILE, String(port));
358
+
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
+ });
366
+
367
+ process.on("SIGTERM", () => { cleanup(); process.exit(0); });
368
+ process.on("SIGINT", () => { cleanup(); process.exit(0); });
369
+ }
370
+
371
+ main();