autonomous-flow-daemon 1.1.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +85 -46
- package/LICENSE +21 -21
- package/README-ko.md +282 -0
- package/README.md +282 -337
- package/mcp-config.json +10 -10
- package/package.json +14 -6
- package/src/adapters/index.ts +370 -159
- package/src/cli.ts +162 -57
- package/src/commands/benchmark.ts +187 -0
- package/src/commands/correlate.ts +180 -0
- package/src/commands/dashboard.ts +404 -0
- package/src/commands/diagnose.ts +56 -14
- package/src/commands/doctor.ts +243 -0
- package/src/commands/evolution.ts +190 -0
- package/src/commands/fix.ts +158 -138
- package/src/commands/hooks.ts +136 -0
- package/src/commands/lang.ts +41 -41
- package/src/commands/mcp.ts +129 -0
- package/src/commands/plugin.ts +110 -0
- package/src/commands/restart.ts +14 -0
- package/src/commands/score.ts +276 -208
- package/src/commands/start.ts +155 -96
- package/src/commands/stats.ts +103 -0
- package/src/commands/status.ts +157 -0
- package/src/commands/stop.ts +68 -49
- package/src/commands/suggest.ts +211 -0
- package/src/commands/sync.ts +567 -21
- package/src/commands/vaccine.ts +177 -0
- package/src/constants.ts +32 -8
- package/src/core/boast.ts +280 -265
- package/src/core/config.ts +49 -49
- package/src/core/correlation-engine.ts +265 -0
- package/src/core/db.ts +145 -46
- package/src/core/discovery.ts +65 -65
- package/src/core/evolution.ts +215 -0
- package/src/core/federation.ts +129 -0
- package/src/core/hologram/engine.ts +71 -0
- package/src/core/hologram/fallback.ts +11 -0
- package/src/core/hologram/go-extractor.ts +203 -0
- package/src/core/hologram/incremental.ts +227 -0
- package/src/core/hologram/py-extractor.ts +132 -0
- package/src/core/hologram/rust-extractor.ts +244 -0
- package/src/core/hologram/ts-extractor.ts +406 -0
- package/src/core/hologram/types.ts +27 -0
- package/src/core/hologram.ts +73 -243
- package/src/core/hook-manager.ts +259 -0
- package/src/core/i18n/messages.ts +309 -266
- package/src/core/immune.ts +8 -123
- package/src/core/locale.ts +88 -88
- 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 +74 -66
- package/src/core/plugin-manager.ts +225 -0
- package/src/core/rule-engine.ts +287 -0
- package/src/core/rule-suggestion.ts +127 -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/validator-generator.ts +224 -0
- package/src/core/workspace.ts +28 -0
- package/src/core/yaml-minimal.ts +176 -0
- package/src/daemon/client.ts +78 -37
- package/src/daemon/event-batcher.ts +108 -0
- package/src/daemon/guards.ts +13 -0
- package/src/daemon/http-routes.ts +376 -0
- package/src/daemon/mcp-handler.ts +575 -0
- package/src/daemon/mcp-subscriptions.ts +81 -0
- package/src/daemon/mesh.ts +51 -0
- package/src/daemon/server.ts +655 -504
- package/src/daemon/types.ts +121 -0
- package/src/daemon/workspace-map.ts +104 -0
- package/src/platform.ts +60 -39
- package/src/version.ts +15 -0
- package/README.ko.md +0 -306
package/src/daemon/server.ts
CHANGED
|
@@ -1,504 +1,655 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
{
|
|
96
|
-
{
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
}
|
|
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
|
+
|
|
11
|
+
import { watch } from "chokidar";
|
|
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";
|
|
15
|
+
import { initDb } from "../core/db";
|
|
16
|
+
import { generateHologram } from "../core/hologram";
|
|
17
|
+
import { EventBatcher } from "./event-batcher";
|
|
18
|
+
import type { PatchOp } from "../core/immune";
|
|
19
|
+
import { detectEcosystem } from "../adapters/index";
|
|
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, SeamEventEntry, QuarantineLogEntry } 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
|
+
import { registerMesh, deregisterMesh } from "./mesh";
|
|
36
|
+
import { subscriptionManager } from "./mcp-subscriptions";
|
|
37
|
+
|
|
38
|
+
// ── State ──
|
|
39
|
+
const state: DaemonState = {
|
|
40
|
+
startedAt: Date.now(),
|
|
41
|
+
filesDetected: 0,
|
|
42
|
+
lastEvent: null,
|
|
43
|
+
lastEventAt: null,
|
|
44
|
+
watchedFiles: new Set(),
|
|
45
|
+
hologramStats: { totalRequests: 0, totalOriginalChars: 0, totalHologramChars: 0, sessionOriginalChars: 0, sessionHologramChars: 0 },
|
|
46
|
+
ecosystems: [],
|
|
47
|
+
autoHealCount: 0,
|
|
48
|
+
autoHealLog: [],
|
|
49
|
+
recentUnlinks: [],
|
|
50
|
+
firstTapTimestamps: new Map(),
|
|
51
|
+
suppressionSkippedCount: 0,
|
|
52
|
+
dormantTransitions: [],
|
|
53
|
+
totalFileBytesSaved: 0,
|
|
54
|
+
totalSavedTokens: 0,
|
|
55
|
+
fileSnapshots: new LruStringMap(10 * 1024 * 1024),
|
|
56
|
+
sseClients: new Set(),
|
|
57
|
+
customValidators: new Map(),
|
|
58
|
+
mistakeCache: new Map(),
|
|
59
|
+
seamEventLog: [],
|
|
60
|
+
quarantineLog: [],
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const _ws = resolveWorkspacePaths();
|
|
64
|
+
|
|
65
|
+
let _cleanupResources: {
|
|
66
|
+
watcher?: ReturnType<typeof watch>;
|
|
67
|
+
interval?: ReturnType<typeof setInterval>;
|
|
68
|
+
wsMapGetTimer?: () => ReturnType<typeof setTimeout> | null;
|
|
69
|
+
validatorWatcher?: ReturnType<typeof fsWatch>;
|
|
70
|
+
db?: { close(): void };
|
|
71
|
+
eventBatcher?: EventBatcher;
|
|
72
|
+
} = {};
|
|
73
|
+
|
|
74
|
+
function cleanup() {
|
|
75
|
+
try { _cleanupResources.eventBatcher?.destroy(); } catch {}
|
|
76
|
+
try { _cleanupResources.interval && clearInterval(_cleanupResources.interval); } catch {}
|
|
77
|
+
try { const mt = _cleanupResources.wsMapGetTimer?.(); mt && clearTimeout(mt); } catch {}
|
|
78
|
+
try { _cleanupResources.watcher?.close(); } catch {}
|
|
79
|
+
try { _cleanupResources.validatorWatcher?.close(); } catch {}
|
|
80
|
+
try { _cleanupResources.db?.close(); } catch {}
|
|
81
|
+
try { unlinkSync(_ws.pidFile); } catch {}
|
|
82
|
+
try { unlinkSync(_ws.portFile); } catch {}
|
|
83
|
+
try { deregisterMesh(_ws.root); } catch {}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── S.E.A.M Logger ──
|
|
87
|
+
const GUARD_LINE = "========== GUARDED ==========";
|
|
88
|
+
const GUARD_PHASES = new Set(["Mutate", "Quarantine"]);
|
|
89
|
+
|
|
90
|
+
function createSeamLogger(mcp: boolean) {
|
|
91
|
+
const log = mcp ? console.error.bind(console) : console.log.bind(console);
|
|
92
|
+
return function seam(phase: string, msg: string) {
|
|
93
|
+
if (GUARD_PHASES.has(phase)) {
|
|
94
|
+
log(`\n${GUARD_LINE}`);
|
|
95
|
+
log(`[${formatTimestamp()}] [afd] [${phase}] ${msg}`);
|
|
96
|
+
log(`${GUARD_LINE}\n`);
|
|
97
|
+
} else {
|
|
98
|
+
log(`[${formatTimestamp()}] [afd] [${phase}] ${msg}`);
|
|
99
|
+
}
|
|
100
|
+
const ts = Date.now();
|
|
101
|
+
const payload = JSON.stringify({ phase, msg, ts });
|
|
102
|
+
// SSE 브로드캐스트
|
|
103
|
+
const encoder = new TextEncoder();
|
|
104
|
+
const sseData = encoder.encode(`data: ${payload}\n\n`);
|
|
105
|
+
const dead: ReadableStreamDefaultController<Uint8Array>[] = [];
|
|
106
|
+
for (const controller of state.sseClients) {
|
|
107
|
+
try { controller.enqueue(sseData); } catch { dead.push(controller); }
|
|
108
|
+
}
|
|
109
|
+
for (const c of dead) state.sseClients.delete(c);
|
|
110
|
+
// v1.9.0: SEAM 이벤트 링 버퍼 (최근 200개 유지)
|
|
111
|
+
state.seamEventLog.push({ phase, msg, ts } as SeamEventEntry);
|
|
112
|
+
if (state.seamEventLog.length > 200) state.seamEventLog.shift();
|
|
113
|
+
// MCP 구독자에게 afd://events 업데이트 알림
|
|
114
|
+
subscriptionManager.dispatchResourceUpdated("afd://events");
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ══════════════════════════════════════════════════════════
|
|
119
|
+
export function main(options: DaemonOptions = {}) {
|
|
120
|
+
state.ecosystems = detectEcosystem(process.cwd());
|
|
121
|
+
|
|
122
|
+
const db = initDb();
|
|
123
|
+
_cleanupResources.db = db;
|
|
124
|
+
|
|
125
|
+
// ── Prepared statements ──
|
|
126
|
+
const insertEvent = db.prepare("INSERT INTO events (type, path, timestamp) VALUES (?, ?, ?)");
|
|
127
|
+
const insertAntibody = db.prepare(
|
|
128
|
+
"INSERT OR REPLACE INTO antibodies (id, pattern_type, file_target, patch_op, created_at) VALUES (?, ?, ?, ?, datetime('now'))"
|
|
129
|
+
);
|
|
130
|
+
// v1.9.0: insertAntibody 래퍼 — DB 삽입 후 MCP afd://antibodies 알림 발송
|
|
131
|
+
function insertAntibodyAndNotify(...args: unknown[]) {
|
|
132
|
+
insertAntibody.run(...args);
|
|
133
|
+
subscriptionManager.dispatchResourceUpdated("afd://antibodies");
|
|
134
|
+
}
|
|
135
|
+
const listAntibodies = db.prepare("SELECT * FROM antibodies ORDER BY created_at DESC");
|
|
136
|
+
const antibodyIds = db.prepare("SELECT id FROM antibodies WHERE dormant = 0");
|
|
137
|
+
const countAntibodies = db.prepare("SELECT COUNT(*) as cnt FROM antibodies");
|
|
138
|
+
const insertUnlinkLog = db.prepare("INSERT INTO unlink_log (file_path, timestamp) VALUES (?, ?)");
|
|
139
|
+
const findAntibodyByFile = db.prepare("SELECT id, dormant FROM antibodies WHERE file_target = ? AND dormant = 0");
|
|
140
|
+
const findAntibodyById = db.prepare("SELECT * FROM antibodies WHERE id = ?");
|
|
141
|
+
const setAntibodyDormant = db.prepare("UPDATE antibodies SET dormant = 1 WHERE id = ?");
|
|
142
|
+
|
|
143
|
+
// Hologram stats
|
|
144
|
+
const getLifetime = db.prepare("SELECT total_requests, total_original_chars, total_hologram_chars FROM hologram_lifetime WHERE id = 1");
|
|
145
|
+
const updateLifetime = db.prepare(
|
|
146
|
+
"UPDATE hologram_lifetime SET total_requests = ?, total_original_chars = ?, total_hologram_chars = ? WHERE id = 1"
|
|
147
|
+
);
|
|
148
|
+
const upsertDaily = db.prepare(`
|
|
149
|
+
INSERT INTO hologram_daily (date, requests, original_chars, hologram_chars) VALUES (?, ?, ?, ?)
|
|
150
|
+
ON CONFLICT(date) DO UPDATE SET requests = requests + excluded.requests,
|
|
151
|
+
original_chars = original_chars + excluded.original_chars,
|
|
152
|
+
hologram_chars = hologram_chars + excluded.hologram_chars
|
|
153
|
+
`);
|
|
154
|
+
const getDailyAll = db.prepare("SELECT date, requests, original_chars, hologram_chars FROM hologram_daily ORDER BY date DESC LIMIT 7");
|
|
155
|
+
const purgeOldDaily = db.prepare("DELETE FROM hologram_daily WHERE date < date('now', '-7 days')");
|
|
156
|
+
|
|
157
|
+
// ── Context Savings (wsmap + pinpoint) ──
|
|
158
|
+
const upsertCtxDaily = db.prepare(`
|
|
159
|
+
INSERT INTO ctx_savings_daily (date, type, requests, original_chars, saved_chars) VALUES (?, ?, 1, ?, ?)
|
|
160
|
+
ON CONFLICT(date, type) DO UPDATE SET
|
|
161
|
+
requests = requests + 1,
|
|
162
|
+
original_chars = original_chars + excluded.original_chars,
|
|
163
|
+
saved_chars = saved_chars + excluded.saved_chars
|
|
164
|
+
`);
|
|
165
|
+
const updateCtxLifetime = db.prepare(`
|
|
166
|
+
UPDATE ctx_savings_lifetime SET
|
|
167
|
+
total_requests = total_requests + 1,
|
|
168
|
+
total_original_chars = total_original_chars + ?,
|
|
169
|
+
total_saved_chars = total_saved_chars + ?
|
|
170
|
+
WHERE type = ?
|
|
171
|
+
`);
|
|
172
|
+
const getCtxSavingsDaily = db.prepare(
|
|
173
|
+
"SELECT date, type, requests, original_chars, saved_chars FROM ctx_savings_daily ORDER BY date DESC, type"
|
|
174
|
+
);
|
|
175
|
+
const getCtxSavingsLifetime = db.prepare(
|
|
176
|
+
"SELECT type, total_requests, total_original_chars, total_saved_chars FROM ctx_savings_lifetime"
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
function persistCtxSavings(type: 'wsmap' | 'pinpoint', originalChars: number, savedChars: number) {
|
|
180
|
+
if (savedChars <= 0) return;
|
|
181
|
+
try {
|
|
182
|
+
upsertCtxDaily.run(today(), type, originalChars, savedChars);
|
|
183
|
+
updateCtxLifetime.run(originalChars, savedChars, type);
|
|
184
|
+
} catch (err) {
|
|
185
|
+
console.error(`[afd] Failed to persist ctx savings:`, err instanceof Error ? err.message : err);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── Telemetry ──
|
|
190
|
+
const insertTelemetry = db.prepare(
|
|
191
|
+
"INSERT INTO telemetry (category, action, detail, duration_ms, timestamp) VALUES (?, ?, ?, ?, ?)"
|
|
192
|
+
);
|
|
193
|
+
function trackEvent(category: string, action: string, detail?: string, durationMs?: number) {
|
|
194
|
+
try { insertTelemetry.run(category, action, detail ?? null, durationMs ?? null, Date.now()); } catch { /* crash-only */ }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ── Mistake History (Passive Defense) ──
|
|
198
|
+
const insertMistakeHistory = db.prepare(
|
|
199
|
+
"INSERT INTO mistake_history (file_path, mistake_type, description, antibody_id, timestamp) VALUES (?, ?, ?, ?, ?)"
|
|
200
|
+
);
|
|
201
|
+
const queryMistakesByFile = db.prepare(
|
|
202
|
+
"SELECT mistake_type, description, timestamp FROM mistake_history WHERE file_path = ? ORDER BY timestamp DESC LIMIT 5"
|
|
203
|
+
);
|
|
204
|
+
const deleteMistakeOverflow = db.prepare(
|
|
205
|
+
"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)"
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
function recordMistake(filePath: string, mistakeType: string, description: string, antibodyId?: string) {
|
|
209
|
+
try {
|
|
210
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
211
|
+
const truncatedDesc = description.slice(0, 200);
|
|
212
|
+
insertMistakeHistory.run(normalizedPath, mistakeType, truncatedDesc, antibodyId ?? null, Date.now());
|
|
213
|
+
deleteMistakeOverflow.run(normalizedPath, normalizedPath);
|
|
214
|
+
const cached = state.mistakeCache.get(normalizedPath) ?? [];
|
|
215
|
+
cached.unshift({ mistake_type: mistakeType, description: truncatedDesc, timestamp: Date.now() });
|
|
216
|
+
if (cached.length > 5) cached.length = 5;
|
|
217
|
+
state.mistakeCache.set(normalizedPath, cached);
|
|
218
|
+
} catch { /* crash-only */ }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
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 }[];
|
|
223
|
+
for (const row of allMistakes) {
|
|
224
|
+
const normalizedPath = row.file_path.replace(/\\/g, "/");
|
|
225
|
+
const cached = state.mistakeCache.get(normalizedPath) ?? [];
|
|
226
|
+
if (cached.length < 5) {
|
|
227
|
+
cached.push({ mistake_type: row.mistake_type, description: row.description, timestamp: row.timestamp });
|
|
228
|
+
state.mistakeCache.set(normalizedPath, cached);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
} catch { /* crash-only — empty cache is fine */ }
|
|
232
|
+
|
|
233
|
+
function today(): string { return new Date().toISOString().slice(0, 10); }
|
|
234
|
+
|
|
235
|
+
// Load persisted hologram stats
|
|
236
|
+
const persisted = getLifetime.get() as { total_requests: number; total_original_chars: number; total_hologram_chars: number } | null;
|
|
237
|
+
if (persisted) {
|
|
238
|
+
state.hologramStats.totalRequests = persisted.total_requests;
|
|
239
|
+
state.hologramStats.totalOriginalChars = persisted.total_original_chars;
|
|
240
|
+
state.hologramStats.totalHologramChars = persisted.total_hologram_chars;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const seam = createSeamLogger(!!options.mcp);
|
|
244
|
+
|
|
245
|
+
function persistHologramStats(originalChars: number, hologramChars: number) {
|
|
246
|
+
state.hologramStats.totalRequests++;
|
|
247
|
+
state.hologramStats.totalOriginalChars += originalChars;
|
|
248
|
+
state.totalSavedTokens += Math.max(0, Math.floor((originalChars - hologramChars) / 4));
|
|
249
|
+
state.hologramStats.totalHologramChars += hologramChars;
|
|
250
|
+
state.hologramStats.sessionOriginalChars += originalChars;
|
|
251
|
+
state.hologramStats.sessionHologramChars += hologramChars;
|
|
252
|
+
try {
|
|
253
|
+
const hs = state.hologramStats;
|
|
254
|
+
updateLifetime.run(hs.totalRequests, hs.totalOriginalChars, hs.totalHologramChars);
|
|
255
|
+
upsertDaily.run(today(), 1, originalChars, hologramChars);
|
|
256
|
+
purgeOldDaily.run();
|
|
257
|
+
} catch (err) {
|
|
258
|
+
console.error("[afd] Failed to persist hologram stats:", err instanceof Error ? err.message : String(err));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function snapshotFile(filePath: string) {
|
|
263
|
+
try {
|
|
264
|
+
if (existsSync(filePath)) state.fileSnapshots.set(filePath, readFileSync(filePath, "utf-8"));
|
|
265
|
+
} catch { /* ignore */ }
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function safeHologram(filePath: string, source: string): Promise<string> {
|
|
269
|
+
try {
|
|
270
|
+
const result = await generateHologram(filePath, source);
|
|
271
|
+
persistHologramStats(result.originalLength, result.hologramLength);
|
|
272
|
+
return result.hologram;
|
|
273
|
+
} catch {
|
|
274
|
+
const lines = source.split("\n");
|
|
275
|
+
return lines.slice(0, 50).join("\n") + (lines.length > 50 ? "\n// … (truncated, AST parse failed)" : "");
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function quarantineFile(originalPath: string, corruptedContent: string | null): void {
|
|
280
|
+
try {
|
|
281
|
+
mkdirSync(QUARANTINE_DIR, { recursive: true });
|
|
282
|
+
const now = new Date();
|
|
283
|
+
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")}`;
|
|
284
|
+
const baseName = originalPath.replace(/[\\/]/g, "_").replace(/^_+/, "");
|
|
285
|
+
const quarantinePath = resolve(QUARANTINE_DIR, `${ts}_${baseName}`);
|
|
286
|
+
writeFileSync(quarantinePath, corruptedContent ?? "DELETED", "utf-8");
|
|
287
|
+
seam("Quarantine", `Saved corrupted state → .afd/quarantine/${ts}_${baseName}`);
|
|
288
|
+
// v1.9.0: 격리 로그 업데이트 + MCP 알림
|
|
289
|
+
state.quarantineLog.push({ path: originalPath, ts: Date.now() } as QuarantineLogEntry);
|
|
290
|
+
if (state.quarantineLog.length > 100) state.quarantineLog.shift();
|
|
291
|
+
subscriptionManager.dispatchResourceUpdated("afd://quarantine");
|
|
292
|
+
} catch (err) {
|
|
293
|
+
seam("Quarantine", `FAILED to quarantine ${originalPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ── Auto-Seed Antibodies ──
|
|
298
|
+
function seedAntibodies() {
|
|
299
|
+
const immuneFiles = [
|
|
300
|
+
{ id: "IMM-001", path: ".claudeignore" },
|
|
301
|
+
{ id: "IMM-002", path: ".claude/hooks.json" },
|
|
302
|
+
{ id: "IMM-003", path: "CLAUDE.md" },
|
|
303
|
+
];
|
|
304
|
+
for (const { id, path: filePath } of immuneFiles) {
|
|
305
|
+
if (existsSync(filePath)) {
|
|
306
|
+
const content = readFileSync(filePath, "utf-8");
|
|
307
|
+
const patches: PatchOp[] = [{ op: "add", path: `/${filePath}`, value: content }];
|
|
308
|
+
insertAntibodyAndNotify(id, "auto-seed", filePath, JSON.stringify(patches));
|
|
309
|
+
state.fileSnapshots.set(filePath, content);
|
|
310
|
+
seam("Adapt", `Antibody ${id} seeded for ${filePath} (${content.length} bytes)`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const selfWrites = new Set<string>();
|
|
316
|
+
seedAntibodies();
|
|
317
|
+
|
|
318
|
+
// ── Dynamic Immune Synthesis ──
|
|
319
|
+
const validatorsDir = join(process.cwd(), VALIDATORS_DIR);
|
|
320
|
+
|
|
321
|
+
async function loadValidators() {
|
|
322
|
+
state.customValidators.clear();
|
|
323
|
+
if (!existsSync(validatorsDir)) return;
|
|
324
|
+
let files: string[];
|
|
325
|
+
try { files = readdirSync(validatorsDir).filter(f => f.endsWith(".js")); } catch { return; }
|
|
326
|
+
for (const file of files) {
|
|
327
|
+
const absPath = resolve(validatorsDir, file);
|
|
328
|
+
try {
|
|
329
|
+
const mod = await import(absPath);
|
|
330
|
+
const fn = mod.default ?? mod;
|
|
331
|
+
if (typeof fn === "function") {
|
|
332
|
+
state.customValidators.set(file, fn as ValidatorFn);
|
|
333
|
+
seam("Adapt", `🧬 Validator loaded: ${file}`);
|
|
334
|
+
} else {
|
|
335
|
+
seam("Adapt", `⚠️ Validator ${file} does not export a function — skipped`);
|
|
336
|
+
}
|
|
337
|
+
} catch (err) {
|
|
338
|
+
seam("Adapt", `⚠️ Failed to load validator ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (state.customValidators.size > 0) {
|
|
342
|
+
seam("Adapt", `Dynamic Immune Synthesis: ${state.customValidators.size} active validator(s)`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
loadValidators();
|
|
347
|
+
|
|
348
|
+
try {
|
|
349
|
+
mkdirSync(validatorsDir, { recursive: true });
|
|
350
|
+
let reloadTimer: ReturnType<typeof setTimeout> | null = null;
|
|
351
|
+
const validatorWatcher = fsWatch(validatorsDir, (_eventType, filename) => {
|
|
352
|
+
if (!filename?.endsWith(".js")) return;
|
|
353
|
+
if (reloadTimer) clearTimeout(reloadTimer);
|
|
354
|
+
reloadTimer = setTimeout(() => { seam("Sense", `Validator change detected: ${filename} — reloading...`); loadValidators(); reloadTimer = null; }, 200);
|
|
355
|
+
});
|
|
356
|
+
_cleanupResources.validatorWatcher = validatorWatcher;
|
|
357
|
+
} catch (err) {
|
|
358
|
+
seam("Adapt", `⚠️ Cannot watch ${VALIDATORS_DIR}: ${err instanceof Error ? err.message : String(err)}`);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function runCustomValidators(newContent: string, filePath: string): boolean {
|
|
362
|
+
for (const [name, fn] of state.customValidators) {
|
|
363
|
+
try {
|
|
364
|
+
const t0 = performance.now();
|
|
365
|
+
const result = fn(newContent, filePath);
|
|
366
|
+
const elapsed = performance.now() - t0;
|
|
367
|
+
if (elapsed > VALIDATOR_TIMEOUT_MS) seam("Adapt", `⚠️ Validator ${name} took ${Math.round(elapsed)}ms (>${VALIDATOR_TIMEOUT_MS}ms)`);
|
|
368
|
+
if (result === true) { trackEvent("validator", name, filePath); seam("Adapt", `🛡️ Custom validator ${name} flagged corruption in ${filePath}`); return true; }
|
|
369
|
+
} catch (err) {
|
|
370
|
+
seam("Adapt", `⚠️ Validator ${name} threw error — ignored: ${err instanceof Error ? err.message : String(err)}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ── Periodic cleanup ──
|
|
377
|
+
const tapCleanupInterval = setInterval(() => {
|
|
378
|
+
const now = Date.now();
|
|
379
|
+
for (const [file, ts] of state.firstTapTimestamps) { if (now - ts > DOUBLE_TAP_WINDOW_MS) state.firstTapTimestamps.delete(file); }
|
|
380
|
+
for (const [file, ts] of corruptionTaps) { if (now - ts > DOUBLE_TAP_WINDOW_MS) corruptionTaps.delete(file); }
|
|
381
|
+
}, TAP_CLEANUP_INTERVAL_MS);
|
|
382
|
+
_cleanupResources.interval = tapCleanupInterval;
|
|
383
|
+
|
|
384
|
+
// ── Suppression Safety ──
|
|
385
|
+
function isMassEvent(now: number): boolean {
|
|
386
|
+
state.recentUnlinks = state.recentUnlinks.filter(t => now - t < MASS_EVENT_WINDOW_MS);
|
|
387
|
+
return state.recentUnlinks.length > MASS_EVENT_THRESHOLD;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function autoHealFile(antibodyId: string, fileTarget: string, patchOp: string) {
|
|
391
|
+
const t0 = performance.now();
|
|
392
|
+
try {
|
|
393
|
+
seam("Mutate", `Restoring ${fileTarget} via antibody ${antibodyId}...`);
|
|
394
|
+
const patches = JSON.parse(patchOp) as PatchOp[];
|
|
395
|
+
let bytesWritten = 0;
|
|
396
|
+
for (const patch of patches) {
|
|
397
|
+
if (patch.op === "add" && patch.value) {
|
|
398
|
+
const targetPath = resolve(patch.path.replace(/^\//, ""));
|
|
399
|
+
assertInsideWorkspace(targetPath, _ws.root);
|
|
400
|
+
writeFileSync(targetPath, patch.value, "utf-8");
|
|
401
|
+
bytesWritten += patch.value.length;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const healMs = Math.round(performance.now() - t0);
|
|
405
|
+
state.autoHealCount++;
|
|
406
|
+
state.totalFileBytesSaved += bytesWritten;
|
|
407
|
+
state.autoHealLog.push({ id: antibodyId, at: Date.now(), file: fileTarget.split("/").pop() ?? fileTarget, healMs });
|
|
408
|
+
trackEvent("immune", "heal_hit", JSON.stringify({ antibodyId, fileTarget, bytesWritten, healMs }));
|
|
409
|
+
recordMistake(fileTarget, "file-deleted", `File deleted and restored via antibody ${antibodyId}`, antibodyId);
|
|
410
|
+
if (state.autoHealLog.length > 100) state.autoHealLog.shift();
|
|
411
|
+
const metrics = calcHealMetrics(bytesWritten, healMs);
|
|
412
|
+
const boast = maybeHealBoast(5);
|
|
413
|
+
const fileName = fileTarget.split("/").pop() ?? fileTarget;
|
|
414
|
+
(options.mcp ? console.error : console.log)(formatHealLog(fileName, metrics, boast));
|
|
415
|
+
// v1.9.0: 치유 완료 MCP 알림 (notifications/message)
|
|
416
|
+
subscriptionManager.dispatchMessage("warning", `[afd] ${fileTarget} 파일의 자가 치유가 완료되었습니다`);
|
|
417
|
+
} catch (err) {
|
|
418
|
+
seam("Mutate", `FAILED to restore ${fileTarget}: ${err instanceof Error ? err.message : String(err)}`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function handleUnlink(filePath: string, now: number): "healed" | "dormant" | null {
|
|
423
|
+
state.recentUnlinks.push(now);
|
|
424
|
+
insertUnlinkLog.run(filePath, now);
|
|
425
|
+
if (isMassEvent(now)) { state.suppressionSkippedCount++; trackEvent("immune", "suppression", JSON.stringify({ filePath })); state.firstTapTimestamps.clear(); return null; }
|
|
426
|
+
const antibody = findAntibodyByFile.get(filePath) as { id: string; dormant: number } | null;
|
|
427
|
+
if (!antibody) return null;
|
|
428
|
+
const fullAntibody = findAntibodyById.get(antibody.id) as { id: string; patch_op: string; file_target: string } | null;
|
|
429
|
+
if (!fullAntibody) return null;
|
|
430
|
+
const previousTap = state.firstTapTimestamps.get(filePath);
|
|
431
|
+
if (previousTap && (now - previousTap) < DOUBLE_TAP_WINDOW_MS) {
|
|
432
|
+
setAntibodyDormant.run(antibody.id);
|
|
433
|
+
state.firstTapTimestamps.delete(filePath);
|
|
434
|
+
state.dormantTransitions.push({ antibodyId: antibody.id, at: now });
|
|
435
|
+
if (state.dormantTransitions.length > 100) state.dormantTransitions.shift();
|
|
436
|
+
trackEvent("immune", "dormant", JSON.stringify({ antibodyId: antibody.id }));
|
|
437
|
+
(options.mcp ? console.error : console.log)(formatDormantLog(antibody.id));
|
|
438
|
+
return "dormant";
|
|
439
|
+
}
|
|
440
|
+
state.firstTapTimestamps.set(filePath, now);
|
|
441
|
+
quarantineFile(filePath, null);
|
|
442
|
+
autoHealFile(fullAntibody.id, fullAntibody.file_target, fullAntibody.patch_op);
|
|
443
|
+
return "healed";
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ── Smart Discovery + Watcher ──
|
|
447
|
+
const discovery = discoverWatchTargets(WATCH_TARGETS);
|
|
448
|
+
seam("Sense", `Smart Discovery: ${discovery.targets.length} targets found in ${discovery.elapsedMs}ms`);
|
|
449
|
+
if (discovery.discoveredCount > 0) {
|
|
450
|
+
seam("Sense", `Discovered ${discovery.discoveredCount} extra: ${discovery.targets.filter(t => !WATCH_TARGETS.includes(t)).join(", ")}`);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const watcher = watch(discovery.targets, { ignoreInitial: false, persistent: true, atomic: 100 });
|
|
454
|
+
_cleanupResources.watcher = watcher;
|
|
455
|
+
|
|
456
|
+
const immuneMap: Record<string, string> = { ".claudeignore": "IMM-001", ".claude/hooks.json": "IMM-002", "CLAUDE.md": "IMM-003" };
|
|
457
|
+
const corruptionTaps = new Map<string, number>();
|
|
458
|
+
|
|
459
|
+
function isInternalPath(p: string): boolean {
|
|
460
|
+
const normalized = p.replace(/\\/g, "/");
|
|
461
|
+
return normalized.startsWith(".afd/") || normalized.includes("/.afd/");
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function isCorrupted(oldContent: string, newContent: string, filePath: string): boolean {
|
|
465
|
+
if (runCustomValidators(newContent, filePath)) return true;
|
|
466
|
+
const trimmed = newContent.trim();
|
|
467
|
+
if (trimmed.length === 0) return true;
|
|
468
|
+
if (filePath.endsWith(".json")) {
|
|
469
|
+
if (trimmed === "{}" || trimmed === "[]") return true;
|
|
470
|
+
try { JSON.parse(newContent); } catch { return true; }
|
|
471
|
+
}
|
|
472
|
+
if (oldContent.length > 50 && newContent.length < oldContent.length * 0.1) return true;
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// ── Event Batcher (Adaptive Debounce) ──
|
|
477
|
+
const eventBatcher = new EventBatcher({
|
|
478
|
+
debounceMs: 300,
|
|
479
|
+
isImmunePath: (p: string) => {
|
|
480
|
+
const normalized = p.replace(/\\/g, "/");
|
|
481
|
+
return normalized in immuneMap;
|
|
482
|
+
},
|
|
483
|
+
onImmediate: (event: string, path: string) => handleFileEvent(event, path),
|
|
484
|
+
onBatch: (events) => {
|
|
485
|
+
if (events.length > 1) {
|
|
486
|
+
seam("Sense", `[Batch] Processing ${events.length} events as single batch`);
|
|
487
|
+
}
|
|
488
|
+
for (const e of events) handleFileEvent(e.event, e.path);
|
|
489
|
+
},
|
|
490
|
+
});
|
|
491
|
+
_cleanupResources.eventBatcher = eventBatcher;
|
|
492
|
+
|
|
493
|
+
// ── Watcher Event Handler (S.E.A.M) ──
|
|
494
|
+
watcher.on("all", (event, path) => {
|
|
495
|
+
if (isInternalPath(path)) return;
|
|
496
|
+
if (selfWrites.has(path)) return;
|
|
497
|
+
eventBatcher.push(event, path);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
function handleFileEvent(event: string, path: string) {
|
|
501
|
+
const _seamStart = performance.now();
|
|
502
|
+
|
|
503
|
+
state.filesDetected++;
|
|
504
|
+
state.lastEvent = `${event}:${path}`;
|
|
505
|
+
state.lastEventAt = Date.now();
|
|
506
|
+
state.watchedFiles.add(path);
|
|
507
|
+
insertEvent.run(event, path, Date.now());
|
|
508
|
+
// v1.9.0: 구독된 afd://history/{path} 리소스에 업데이트 알림
|
|
509
|
+
{
|
|
510
|
+
const normPath = path.replace(/\\/g, "/");
|
|
511
|
+
subscriptionManager.dispatchResourceUpdated(`afd://history/${normPath}`);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (event === "add" || event === "addDir") { seam("Sense", `${event} → ${path}`); snapshotFile(path); trackEvent("seam", "sense", null, Math.round(performance.now() - _seamStart)); return; }
|
|
515
|
+
|
|
516
|
+
if (event === "unlink") {
|
|
517
|
+
seam("Sense", `unlink → ${path}`);
|
|
518
|
+
state.fileSnapshots.delete(path);
|
|
519
|
+
state.watchedFiles.delete(path);
|
|
520
|
+
const result = handleUnlink(path, Date.now());
|
|
521
|
+
if (result === "healed") {
|
|
522
|
+
selfWrites.add(path);
|
|
523
|
+
setTimeout(() => selfWrites.delete(path), SELF_WRITE_DEBOUNCE_MS);
|
|
524
|
+
seam("Mutate", `Restored ${path} from antibody snapshot`);
|
|
525
|
+
seam("Extract", `💡 Tip: Use the MCP tool 'afd_hologram' on ${path} to safely inspect the file's structure before attempting another edit.`);
|
|
526
|
+
snapshotFile(path);
|
|
527
|
+
watcher.add(path);
|
|
528
|
+
} else if (result === "dormant") {
|
|
529
|
+
seam("Adapt", `Double-tap confirmed — user intentionally deleted ${path}, antibody deactivated`);
|
|
530
|
+
}
|
|
531
|
+
trackEvent("seam", event, path, Math.round(performance.now() - _seamStart));
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (event === "change") {
|
|
536
|
+
if (!existsSync(path)) return;
|
|
537
|
+
let newContent: string;
|
|
538
|
+
try { newContent = readFileSync(path, "utf-8"); } catch { return; }
|
|
539
|
+
const oldContent = state.fileSnapshots.get(path);
|
|
540
|
+
const newSize = newContent.length;
|
|
541
|
+
|
|
542
|
+
if (oldContent !== undefined && oldContent !== newContent) {
|
|
543
|
+
if (isAstSupported(path)) {
|
|
544
|
+
try {
|
|
545
|
+
const sdiff = semanticDiff(path, oldContent, newContent);
|
|
546
|
+
const breakingTag = sdiff.hasBreakingChanges ? " ⚠️ BREAKING" : "";
|
|
547
|
+
seam("Sense", `change → ${path} (${newSize} bytes)${breakingTag}\n [semantic] ${sdiff.summary}`);
|
|
548
|
+
} catch {
|
|
549
|
+
const diffs = lineDiff(oldContent, newContent);
|
|
550
|
+
seam("Sense", `change → ${path} (${newSize} bytes)\n${diffs.join("\n")}`);
|
|
551
|
+
}
|
|
552
|
+
} else {
|
|
553
|
+
const diffs = lineDiff(oldContent, newContent);
|
|
554
|
+
if (diffs.length > 0) seam("Sense", `change → ${path} (${newSize} bytes)\n${diffs.join("\n")}`);
|
|
555
|
+
else seam("Sense", `change → ${path} (${newSize} bytes, whitespace-only diff)`);
|
|
556
|
+
}
|
|
557
|
+
} else if (oldContent === undefined) {
|
|
558
|
+
seam("Sense", `change → ${path} (${newSize} bytes, no previous snapshot)`);
|
|
559
|
+
} else {
|
|
560
|
+
seam("Sense", `change → ${path} (${newSize} bytes, content identical — touch or metadata)`);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const normalizedPath = path.replace(/\\/g, "/");
|
|
564
|
+
const abId = immuneMap[normalizedPath];
|
|
565
|
+
if (abId && oldContent !== undefined) {
|
|
566
|
+
if (isCorrupted(oldContent, newContent, path)) {
|
|
567
|
+
const prevCorruption = corruptionTaps.get(path);
|
|
568
|
+
const now = Date.now();
|
|
569
|
+
if (prevCorruption && (now - prevCorruption) < DOUBLE_TAP_WINDOW_MS) {
|
|
570
|
+
corruptionTaps.delete(path);
|
|
571
|
+
state.fileSnapshots.set(path, newContent);
|
|
572
|
+
insertAntibodyAndNotify(abId, "auto-seed", path, JSON.stringify([{ op: "add", path: `/${path}`, value: newContent }]));
|
|
573
|
+
trackEvent("immune", "heal_false_positive", JSON.stringify({ filePath: path, abId }));
|
|
574
|
+
seam("Adapt", `Corruption double-tap on ${path} — standing down, accepting new content`);
|
|
575
|
+
} else {
|
|
576
|
+
corruptionTaps.set(path, now);
|
|
577
|
+
quarantineFile(path, newContent);
|
|
578
|
+
selfWrites.add(path);
|
|
579
|
+
setTimeout(() => selfWrites.delete(path), SELF_WRITE_DEBOUNCE_MS);
|
|
580
|
+
writeFileSync(resolve(path), oldContent, "utf-8");
|
|
581
|
+
trackEvent("immune", "heal_hit", JSON.stringify({ filePath: path, abId }));
|
|
582
|
+
recordMistake(path, "corruption", `Silent corruption detected (${oldContent.length} → ${newSize} bytes) — restored`, abId);
|
|
583
|
+
seam("Mutate", `Silent corruption detected in ${path} (${oldContent.length} → ${newSize} bytes) — restored from snapshot`);
|
|
584
|
+
seam("Extract", `💡 Tip: Use the MCP tool 'afd_hologram' on ${path} to safely inspect the file's structure before attempting another edit.`);
|
|
585
|
+
}
|
|
586
|
+
} else {
|
|
587
|
+
state.fileSnapshots.set(path, newContent);
|
|
588
|
+
const patches: PatchOp[] = [{ op: "add", path: `/${path}`, value: newContent }];
|
|
589
|
+
insertAntibodyAndNotify(abId, "auto-seed", path, JSON.stringify(patches));
|
|
590
|
+
trackEvent("immune", "heal_pass", JSON.stringify({ filePath: path, abId }));
|
|
591
|
+
seam("Adapt", `Antibody ${abId} updated: stored latest ${path} (${newSize} bytes) for auto-restore`);
|
|
592
|
+
}
|
|
593
|
+
} else {
|
|
594
|
+
state.fileSnapshots.set(path, newContent);
|
|
595
|
+
}
|
|
596
|
+
trackEvent("seam", "change", path, Math.round(performance.now() - _seamStart));
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
seam("Sense", `${event} → ${path}`);
|
|
601
|
+
trackEvent("seam", event, path, Math.round(performance.now() - _seamStart));
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// ── Workspace Map ──
|
|
605
|
+
const wsMap = createWorkspaceMap();
|
|
606
|
+
_cleanupResources.wsMapGetTimer = wsMap.getTimer;
|
|
607
|
+
watcher.on("add", () => wsMap.markDirty());
|
|
608
|
+
watcher.on("unlink", () => wsMap.markDirty());
|
|
609
|
+
wsMap.get(); // initial build
|
|
610
|
+
|
|
611
|
+
// ── Build DaemonContext ──
|
|
612
|
+
const ctx: DaemonContext = {
|
|
613
|
+
state, db: db as unknown as DaemonContext["db"], ws: _ws, options,
|
|
614
|
+
insertEvent, insertAntibody, listAntibodies, antibodyIds: antibodyIds as unknown as DaemonContext["antibodyIds"],
|
|
615
|
+
countAntibodies: countAntibodies as unknown as DaemonContext["countAntibodies"],
|
|
616
|
+
getDailyAll: getDailyAll as unknown as DaemonContext["getDailyAll"],
|
|
617
|
+
insertTelemetry: insertTelemetry as unknown as DaemonContext["insertTelemetry"],
|
|
618
|
+
insertMistakeHistory: insertMistakeHistory as unknown as DaemonContext["insertMistakeHistory"],
|
|
619
|
+
queryMistakesByFile: queryMistakesByFile as unknown as DaemonContext["queryMistakesByFile"],
|
|
620
|
+
deleteMistakeOverflow: deleteMistakeOverflow as unknown as DaemonContext["deleteMistakeOverflow"],
|
|
621
|
+
seam, persistHologramStats, persistCtxSavings, safeHologram,
|
|
622
|
+
getWorkspaceMap: wsMap.get, getWorkspaceMapStats: wsMap.getLastBuildStats,
|
|
623
|
+
today, discoveryTargets: discovery.targets,
|
|
624
|
+
getCtxSavingsDaily: getCtxSavingsDaily as unknown as DaemonContext["getCtxSavingsDaily"],
|
|
625
|
+
getCtxSavingsLifetime: getCtxSavingsLifetime as unknown as DaemonContext["getCtxSavingsLifetime"],
|
|
626
|
+
port: 0,
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
// ── MCP Mode ──
|
|
630
|
+
if (options.mcp) {
|
|
631
|
+
startMcpStdio(ctx);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// ── HTTP Server ──
|
|
636
|
+
const server = Bun.serve({ port: 0, fetch: createHttpHandler(ctx, cleanup) });
|
|
637
|
+
const port = server.port;
|
|
638
|
+
ctx.port = port;
|
|
639
|
+
|
|
640
|
+
mkdirSync(_ws.afdDir, { recursive: true });
|
|
641
|
+
writeFileSync(_ws.pidFile, String(process.pid));
|
|
642
|
+
writeFileSync(_ws.portFile, String(port));
|
|
643
|
+
try { registerMesh(_ws.root, port, process.pid); } catch {}
|
|
644
|
+
|
|
645
|
+
console.log(`[afd daemon] pid=${process.pid} port=${port} workspace=${_ws.root}`);
|
|
646
|
+
|
|
647
|
+
process.on("uncaughtException", (err) => { console.error("[afd daemon] FATAL:", err.message); cleanup(); process.exit(1); });
|
|
648
|
+
process.on("SIGTERM", () => { cleanup(); process.exit(0); });
|
|
649
|
+
process.on("SIGINT", () => { cleanup(); process.exit(0); });
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (import.meta.main) {
|
|
653
|
+
const mcp = process.argv.includes("--mcp") || process.env.AFD_MCP === "1";
|
|
654
|
+
main({ mcp });
|
|
655
|
+
}
|