agents-library 0.1.0 → 0.1.2
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/README.md +178 -0
- package/dist/base-agent.d.ts +10 -8
- package/dist/base-agent.d.ts.map +1 -1
- package/dist/base-agent.js +30 -26
- package/dist/base-agent.js.map +1 -1
- package/dist/base-bot.d.ts +0 -0
- package/dist/base-bot.d.ts.map +0 -0
- package/dist/base-bot.js +5 -5
- package/dist/base-bot.js.map +1 -1
- package/dist/common/result.d.ts +0 -0
- package/dist/common/result.d.ts.map +0 -0
- package/dist/common/result.js +0 -0
- package/dist/common/result.js.map +0 -0
- package/dist/common/types.d.ts +0 -0
- package/dist/common/types.d.ts.map +0 -0
- package/dist/common/types.js +0 -0
- package/dist/common/types.js.map +0 -0
- package/dist/index.d.ts +12 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/kadi-event-publisher.d.ts +0 -0
- package/dist/kadi-event-publisher.d.ts.map +0 -0
- package/dist/kadi-event-publisher.js +1 -1
- package/dist/kadi-event-publisher.js.map +1 -1
- package/dist/memory/arcadedb-adapter.d.ts +8 -0
- package/dist/memory/arcadedb-adapter.d.ts.map +1 -1
- package/dist/memory/arcadedb-adapter.js +8 -0
- package/dist/memory/arcadedb-adapter.js.map +1 -1
- package/dist/memory/entity-extractor.d.ts +110 -0
- package/dist/memory/entity-extractor.d.ts.map +1 -0
- package/dist/memory/entity-extractor.js +259 -0
- package/dist/memory/entity-extractor.js.map +1 -0
- package/dist/memory/file-storage-adapter.d.ts +0 -0
- package/dist/memory/file-storage-adapter.d.ts.map +0 -0
- package/dist/memory/file-storage-adapter.js +0 -0
- package/dist/memory/file-storage-adapter.js.map +0 -0
- package/dist/memory/memory-service.d.ts +123 -13
- package/dist/memory/memory-service.d.ts.map +1 -1
- package/dist/memory/memory-service.js +428 -72
- package/dist/memory/memory-service.js.map +1 -1
- package/dist/memory/types.d.ts +0 -0
- package/dist/memory/types.d.ts.map +0 -0
- package/dist/memory/types.js +0 -0
- package/dist/memory/types.js.map +0 -0
- package/dist/producer-tool-utils.d.ts +0 -0
- package/dist/producer-tool-utils.d.ts.map +0 -0
- package/dist/producer-tool-utils.js +16 -16
- package/dist/producer-tool-utils.js.map +1 -1
- package/dist/providers/anthropic-provider.d.ts +0 -0
- package/dist/providers/anthropic-provider.d.ts.map +0 -0
- package/dist/providers/anthropic-provider.js +0 -0
- package/dist/providers/anthropic-provider.js.map +0 -0
- package/dist/providers/model-manager-provider.d.ts +0 -0
- package/dist/providers/model-manager-provider.d.ts.map +0 -0
- package/dist/providers/model-manager-provider.js +0 -0
- package/dist/providers/model-manager-provider.js.map +0 -0
- package/dist/providers/provider-manager.d.ts +0 -0
- package/dist/providers/provider-manager.d.ts.map +1 -1
- package/dist/providers/provider-manager.js +6 -2
- package/dist/providers/provider-manager.js.map +1 -1
- package/dist/providers/types.d.ts +0 -0
- package/dist/providers/types.d.ts.map +0 -0
- package/dist/providers/types.js +0 -0
- package/dist/providers/types.js.map +0 -0
- package/dist/shadow-agent-factory.d.ts +23 -97
- package/dist/shadow-agent-factory.d.ts.map +1 -1
- package/dist/shadow-agent-factory.js +116 -306
- package/dist/shadow-agent-factory.js.map +1 -1
- package/dist/types/agent-config.d.ts +62 -1
- package/dist/types/agent-config.d.ts.map +1 -1
- package/dist/types/agent-config.js +0 -0
- package/dist/types/agent-config.js.map +0 -0
- package/dist/types/event-schemas.d.ts +194 -0
- package/dist/types/event-schemas.d.ts.map +1 -1
- package/dist/types/event-schemas.js +77 -2
- package/dist/types/event-schemas.js.map +1 -1
- package/dist/types/tool-schemas.d.ts +0 -0
- package/dist/types/tool-schemas.d.ts.map +0 -0
- package/dist/types/tool-schemas.js +0 -0
- package/dist/types/tool-schemas.js.map +0 -0
- package/dist/utils/config.d.ts +48 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +163 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/logger.d.ts +11 -1
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +26 -1
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/path-utils.d.ts +22 -0
- package/dist/utils/path-utils.d.ts.map +1 -0
- package/dist/utils/path-utils.js +51 -0
- package/dist/utils/path-utils.js.map +1 -0
- package/dist/utils/read-config.d.ts +43 -0
- package/dist/utils/read-config.d.ts.map +1 -0
- package/dist/utils/read-config.js +97 -0
- package/dist/utils/read-config.js.map +1 -0
- package/dist/utils/timer.d.ts +0 -0
- package/dist/utils/timer.d.ts.map +0 -0
- package/dist/utils/timer.js +0 -0
- package/dist/utils/timer.js.map +0 -0
- package/dist/utils/vault.d.ts +29 -0
- package/dist/utils/vault.d.ts.map +1 -0
- package/dist/utils/vault.js +79 -0
- package/dist/utils/vault.js.map +1 -0
- package/dist/worker-agent-factory.d.ts +37 -38
- package/dist/worker-agent-factory.d.ts.map +1 -1
- package/dist/worker-agent-factory.js +355 -212
- package/dist/worker-agent-factory.js.map +1 -1
- package/package.json +5 -3
|
@@ -25,7 +25,6 @@
|
|
|
25
25
|
* @module shadow-agent-factory
|
|
26
26
|
*/
|
|
27
27
|
import { KadiClient, z } from '@kadi.build/core';
|
|
28
|
-
import chokidar from 'chokidar';
|
|
29
28
|
import fs from 'fs';
|
|
30
29
|
import path from 'path';
|
|
31
30
|
import { execSync } from 'child_process';
|
|
@@ -182,28 +181,15 @@ export class BaseShadowAgent {
|
|
|
182
181
|
*/
|
|
183
182
|
usesBaseAgent;
|
|
184
183
|
/**
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
* Monitors file operations (create, modify, delete) in worker worktree.
|
|
188
|
-
* Null until start() is called.
|
|
184
|
+
* Handler reference for broker file.changed events.
|
|
185
|
+
* Stored so we can unsubscribe on stop().
|
|
189
186
|
*/
|
|
190
|
-
|
|
187
|
+
fileEventHandler = null;
|
|
191
188
|
/**
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
* Watches .git/refs/heads/{workerBranch} file for commit SHA changes.
|
|
195
|
-
* Uses fs.watch (not chokidar) for lightweight ref monitoring.
|
|
196
|
-
* Null until start() is called.
|
|
197
|
-
*/
|
|
198
|
-
refWatcher = null;
|
|
199
|
-
/**
|
|
200
|
-
* Previous commit SHA from worker branch
|
|
201
|
-
*
|
|
202
|
-
* Stores last known commit SHA to detect actual commit changes.
|
|
203
|
-
* Used to differentiate real commits from other ref updates.
|
|
204
|
-
* Null until first commit is detected.
|
|
189
|
+
* Native ability-file-local instance for in-process file operations.
|
|
190
|
+
* Set via setNativeFileLocal() after loadNative in the consuming agent.
|
|
205
191
|
*/
|
|
206
|
-
|
|
192
|
+
nativeFileLocal = null;
|
|
207
193
|
/**
|
|
208
194
|
* Set of changed file paths awaiting backup processing
|
|
209
195
|
*
|
|
@@ -214,13 +200,6 @@ export class BaseShadowAgent {
|
|
|
214
200
|
* Value: Debounce timeout handle
|
|
215
201
|
*/
|
|
216
202
|
debounceMap = new Map();
|
|
217
|
-
/**
|
|
218
|
-
* Debounce timeout for git ref watcher
|
|
219
|
-
*
|
|
220
|
-
* Stores timeout handle for debouncing ref change events.
|
|
221
|
-
* Prevents processing rapid ref updates.
|
|
222
|
-
*/
|
|
223
|
-
refDebounceTimeout = null;
|
|
224
203
|
/**
|
|
225
204
|
* Create a new BaseShadowAgent instance
|
|
226
205
|
*
|
|
@@ -259,7 +238,7 @@ export class BaseShadowAgent {
|
|
|
259
238
|
if (baseAgent) {
|
|
260
239
|
this.client = baseAgent.client;
|
|
261
240
|
this.usesBaseAgent = true;
|
|
262
|
-
logger.info(MODULE_AGENT, '
|
|
241
|
+
logger.info(MODULE_AGENT, ' Using BaseAgent client (connection managed externally)', timer.elapsed('shadow-factory'));
|
|
263
242
|
}
|
|
264
243
|
else {
|
|
265
244
|
this.client = new KadiClient({
|
|
@@ -272,13 +251,20 @@ export class BaseShadowAgent {
|
|
|
272
251
|
});
|
|
273
252
|
this.usesBaseAgent = false;
|
|
274
253
|
}
|
|
275
|
-
logger.info(MODULE_AGENT,
|
|
254
|
+
logger.info(MODULE_AGENT, `BaseShadowAgent initialized for role: ${this.role}`, timer.elapsed('shadow-factory'));
|
|
276
255
|
logger.info(MODULE_AGENT, ` Worker worktree: ${this.workerWorktreePath}`, timer.elapsed('shadow-factory'));
|
|
277
256
|
logger.info(MODULE_AGENT, ` Shadow worktree: ${this.shadowWorktreePath}`, timer.elapsed('shadow-factory'));
|
|
278
257
|
logger.info(MODULE_AGENT, ` Worker branch: ${this.workerBranch}`, timer.elapsed('shadow-factory'));
|
|
279
258
|
logger.info(MODULE_AGENT, ` Shadow branch: ${this.shadowBranch}`, timer.elapsed('shadow-factory'));
|
|
280
259
|
logger.info(MODULE_AGENT, ` Debounce delay: ${this.debounceMs}ms`, timer.elapsed('shadow-factory'));
|
|
281
260
|
}
|
|
261
|
+
/**
|
|
262
|
+
* Set native ability-file-local for in-process file operations.
|
|
263
|
+
* Call this after loadNative('ability-file-local') in the consuming agent.
|
|
264
|
+
*/
|
|
265
|
+
setNativeFileLocal(native) {
|
|
266
|
+
this.nativeFileLocal = native;
|
|
267
|
+
}
|
|
282
268
|
/**
|
|
283
269
|
* Start the shadow agent
|
|
284
270
|
*
|
|
@@ -302,29 +288,26 @@ export class BaseShadowAgent {
|
|
|
302
288
|
* ```
|
|
303
289
|
*/
|
|
304
290
|
async start() {
|
|
305
|
-
logger.info(MODULE_AGENT,
|
|
291
|
+
logger.info(MODULE_AGENT, `Starting shadow agent for role: ${this.role}`, timer.elapsed('shadow-factory'));
|
|
306
292
|
// Connect to KĀDI broker (skip if BaseAgent manages connection)
|
|
307
293
|
if (this.usesBaseAgent) {
|
|
308
|
-
logger.info(MODULE_AGENT, '
|
|
294
|
+
logger.info(MODULE_AGENT, ' Broker connection managed by BaseAgent (skipping)', timer.elapsed('shadow-factory'));
|
|
309
295
|
}
|
|
310
296
|
else {
|
|
311
297
|
logger.info(MODULE_AGENT, ' → Connecting to KĀDI broker...', timer.elapsed('shadow-factory'));
|
|
312
298
|
try {
|
|
313
299
|
await this.client.connect();
|
|
314
|
-
logger.info(MODULE_AGENT, '
|
|
300
|
+
logger.info(MODULE_AGENT, ' Connected to KĀDI broker', timer.elapsed('shadow-factory'));
|
|
315
301
|
}
|
|
316
302
|
catch (error) {
|
|
317
|
-
logger.error(MODULE_AGENT, '
|
|
303
|
+
logger.error(MODULE_AGENT, 'Broker connection error', timer.elapsed('shadow-factory'), error);
|
|
318
304
|
process.exit(1);
|
|
319
305
|
}
|
|
320
306
|
}
|
|
321
|
-
//
|
|
322
|
-
await this.
|
|
323
|
-
logger.info(MODULE_AGENT, '
|
|
324
|
-
|
|
325
|
-
await this.setupGitRefWatcher();
|
|
326
|
-
logger.info(MODULE_AGENT, '✅ Git ref watcher initialized', timer.elapsed('shadow-factory'));
|
|
327
|
-
logger.info(MODULE_AGENT, '✅ Shadow agent started and monitoring', timer.elapsed('shadow-factory'));
|
|
307
|
+
// Subscribe to file.changed broker events (emitted by ability-file-local)
|
|
308
|
+
await this.setupBrokerFileEventSubscription();
|
|
309
|
+
logger.info(MODULE_AGENT, 'Broker file event subscription active', timer.elapsed('shadow-factory'));
|
|
310
|
+
logger.info(MODULE_AGENT, 'Shadow agent started and monitoring via broker events', timer.elapsed('shadow-factory'));
|
|
328
311
|
}
|
|
329
312
|
/**
|
|
330
313
|
* Stop the shadow agent
|
|
@@ -346,284 +329,111 @@ export class BaseShadowAgent {
|
|
|
346
329
|
* ```
|
|
347
330
|
*/
|
|
348
331
|
async stop() {
|
|
349
|
-
logger.info(MODULE_AGENT,
|
|
350
|
-
//
|
|
351
|
-
if (this.
|
|
352
|
-
logger.info(MODULE_AGENT, '
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
this.refWatcher = null;
|
|
362
|
-
logger.info(MODULE_AGENT, '✅ Git ref watcher stopped', timer.elapsed('shadow-factory'));
|
|
363
|
-
}
|
|
364
|
-
// Clear ref debounce timeout
|
|
365
|
-
if (this.refDebounceTimeout) {
|
|
366
|
-
logger.info(MODULE_AGENT, '🛑 Clearing ref debounce timeout...', timer.elapsed('shadow-factory'));
|
|
367
|
-
clearTimeout(this.refDebounceTimeout);
|
|
368
|
-
this.refDebounceTimeout = null;
|
|
369
|
-
logger.info(MODULE_AGENT, '✅ Ref debounce timeout cleared', timer.elapsed('shadow-factory'));
|
|
332
|
+
logger.info(MODULE_AGENT, `Stopping shadow agent for role: ${this.role}`, timer.elapsed('shadow-factory'));
|
|
333
|
+
// Unsubscribe from broker file events
|
|
334
|
+
if (this.fileEventHandler) {
|
|
335
|
+
logger.info(MODULE_AGENT, 'Unsubscribing from file.changed events...', timer.elapsed('shadow-factory'));
|
|
336
|
+
try {
|
|
337
|
+
await this.client.unsubscribe('file.changed', this.fileEventHandler);
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
// Best-effort unsubscribe — may not be supported in all KadiClient versions
|
|
341
|
+
}
|
|
342
|
+
this.fileEventHandler = null;
|
|
343
|
+
logger.info(MODULE_AGENT, 'File event subscription removed', timer.elapsed('shadow-factory'));
|
|
370
344
|
}
|
|
371
345
|
// Clear all pending debounce timers
|
|
372
346
|
if (this.debounceMap.size > 0) {
|
|
373
|
-
logger.info(MODULE_AGENT,
|
|
347
|
+
logger.info(MODULE_AGENT, `Clearing ${this.debounceMap.size} pending debounce timers...`, timer.elapsed('shadow-factory'));
|
|
374
348
|
for (const timeout of this.debounceMap.values()) {
|
|
375
349
|
clearTimeout(timeout);
|
|
376
350
|
}
|
|
377
351
|
this.debounceMap.clear();
|
|
378
|
-
logger.info(MODULE_AGENT, '
|
|
352
|
+
logger.info(MODULE_AGENT, 'Debounce timers cleared', timer.elapsed('shadow-factory'));
|
|
379
353
|
}
|
|
380
354
|
// Disconnect KĀDI client (skip if BaseAgent manages connection)
|
|
381
355
|
if (this.usesBaseAgent) {
|
|
382
|
-
logger.info(MODULE_AGENT, '
|
|
356
|
+
logger.info(MODULE_AGENT, ' Broker disconnection managed by BaseAgent (skipping)', timer.elapsed('shadow-factory'));
|
|
383
357
|
}
|
|
384
358
|
else {
|
|
385
359
|
logger.info(MODULE_AGENT, ' → Disconnecting from KĀDI broker...', timer.elapsed('shadow-factory'));
|
|
386
360
|
await this.client.disconnect();
|
|
387
|
-
logger.info(MODULE_AGENT, '
|
|
361
|
+
logger.info(MODULE_AGENT, ' Disconnected from KĀDI broker', timer.elapsed('shadow-factory'));
|
|
388
362
|
}
|
|
389
|
-
logger.info(MODULE_AGENT, '
|
|
363
|
+
logger.info(MODULE_AGENT, 'Shadow agent stopped', timer.elapsed('shadow-factory'));
|
|
390
364
|
}
|
|
391
365
|
/**
|
|
392
|
-
*
|
|
393
|
-
*
|
|
394
|
-
* Monitors worker worktree for file operations (create, modify, delete) using chokidar.
|
|
395
|
-
* File changes are debounced and stored for batch backup processing.
|
|
396
|
-
*
|
|
397
|
-
* Configuration:
|
|
398
|
-
* - Watches: config.workerWorktreePath
|
|
399
|
-
* - Excludes: .git directory, node_modules, .env files
|
|
400
|
-
* - Debounce: config.debounceMs (default: 1000ms)
|
|
401
|
-
* - Stability threshold: Waits for file writes to complete
|
|
366
|
+
* Subscribe to file.changed broker events from ability-file-local
|
|
402
367
|
*
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
* - 'unlink': File deleted from worktree
|
|
407
|
-
* - 'error': Watcher errors (logged but non-fatal)
|
|
408
|
-
* - 'ready': Watcher initialization complete
|
|
368
|
+
* Instead of using chokidar directly (unreliable on /mnt/c/ in WSL containers),
|
|
369
|
+
* the shadow agent subscribes to `file.changed` events published by ability-file-local
|
|
370
|
+
* which runs on the host where filesystem events work natively.
|
|
409
371
|
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
* - Clears previous timeout if file changes again before debounce completes
|
|
413
|
-
* - Only processes file after debounceMs of inactivity
|
|
414
|
-
* - Prevents rapid-fire commits for the same file
|
|
372
|
+
* Event payload from ability-file-local:
|
|
373
|
+
* { watchId: string, event: 'add'|'change'|'unlink', path: string }
|
|
415
374
|
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
* @example
|
|
419
|
-
* ```typescript
|
|
420
|
-
* await this.setupFilesystemWatcher();
|
|
421
|
-
* // Watcher is now monitoring worker worktree for file changes
|
|
422
|
-
* ```
|
|
375
|
+
* The shadow agent filters events to only process those matching its workerWorktreePath,
|
|
376
|
+
* then debounces and creates shadow backup commits.
|
|
423
377
|
*/
|
|
424
|
-
async
|
|
425
|
-
logger.info(MODULE_AGENT,
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
'
|
|
435
|
-
'
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
pollInterval: 100
|
|
440
|
-
}
|
|
441
|
-
});
|
|
442
|
-
// Event: File created
|
|
443
|
-
this.fsWatcher.on('add', (filePath) => {
|
|
444
|
-
logger.info(MODULE_AGENT, `➕ File created: ${filePath}`, timer.elapsed('shadow-factory'));
|
|
445
|
-
// Debounce to avoid rapid-fire commits
|
|
446
|
-
if (this.debounceMap.has(filePath)) {
|
|
447
|
-
clearTimeout(this.debounceMap.get(filePath));
|
|
448
|
-
}
|
|
449
|
-
const timeout = setTimeout(async () => {
|
|
450
|
-
logger.info(MODULE_AGENT, `📝 Processing created file: ${filePath}`, timer.elapsed('shadow-factory'));
|
|
451
|
-
const relativePath = path.relative(this.workerWorktreePath, filePath);
|
|
452
|
-
await this.createShadowBackup('Created', relativePath);
|
|
453
|
-
this.debounceMap.delete(filePath);
|
|
454
|
-
}, this.debounceMs);
|
|
455
|
-
this.debounceMap.set(filePath, timeout);
|
|
456
|
-
});
|
|
457
|
-
// Event: File modified
|
|
458
|
-
this.fsWatcher.on('change', (filePath) => {
|
|
459
|
-
logger.info(MODULE_AGENT, `✏️ File modified: ${filePath}`, timer.elapsed('shadow-factory'));
|
|
460
|
-
// Debounce to avoid rapid-fire commits
|
|
461
|
-
if (this.debounceMap.has(filePath)) {
|
|
462
|
-
clearTimeout(this.debounceMap.get(filePath));
|
|
463
|
-
}
|
|
464
|
-
const timeout = setTimeout(async () => {
|
|
465
|
-
logger.info(MODULE_AGENT, `📝 Processing modified file: ${filePath}`, timer.elapsed('shadow-factory'));
|
|
466
|
-
const relativePath = path.relative(this.workerWorktreePath, filePath);
|
|
467
|
-
await this.createShadowBackup('Modified', relativePath);
|
|
468
|
-
this.debounceMap.delete(filePath);
|
|
469
|
-
}, this.debounceMs);
|
|
470
|
-
this.debounceMap.set(filePath, timeout);
|
|
471
|
-
});
|
|
472
|
-
// Event: File deleted
|
|
473
|
-
this.fsWatcher.on('unlink', (filePath) => {
|
|
474
|
-
logger.info(MODULE_AGENT, `🗑️ File deleted: ${filePath}`, timer.elapsed('shadow-factory'));
|
|
475
|
-
// Debounce to avoid rapid-fire commits
|
|
476
|
-
if (this.debounceMap.has(filePath)) {
|
|
477
|
-
clearTimeout(this.debounceMap.get(filePath));
|
|
478
|
-
}
|
|
479
|
-
const timeout = setTimeout(async () => {
|
|
480
|
-
logger.info(MODULE_AGENT, `📝 Processing deleted file: ${filePath}`, timer.elapsed('shadow-factory'));
|
|
378
|
+
async setupBrokerFileEventSubscription() {
|
|
379
|
+
logger.info(MODULE_AGENT, `Subscribing to file.changed broker events for: ${this.workerWorktreePath}`, timer.elapsed('shadow-factory'));
|
|
380
|
+
const handler = async (event) => {
|
|
381
|
+
try {
|
|
382
|
+
const data = (event?.data || event);
|
|
383
|
+
if (!data.path || !data.event)
|
|
384
|
+
return;
|
|
385
|
+
const filePath = data.path;
|
|
386
|
+
// Filter: only process events for our worker worktree
|
|
387
|
+
// Normalize both paths for comparison (handle Windows/Unix path differences)
|
|
388
|
+
const normalizedFile = filePath.replace(/\\/g, '/').toLowerCase();
|
|
389
|
+
const normalizedWorktree = this.workerWorktreePath.replace(/\\/g, '/').toLowerCase();
|
|
390
|
+
if (!normalizedFile.startsWith(normalizedWorktree))
|
|
391
|
+
return;
|
|
392
|
+
// Skip .git, node_modules, .env files
|
|
481
393
|
const relativePath = path.relative(this.workerWorktreePath, filePath);
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
this.debounceMap.set(filePath, timeout);
|
|
486
|
-
});
|
|
487
|
-
// Event: Watcher error
|
|
488
|
-
this.fsWatcher.on('error', (error) => {
|
|
489
|
-
logger.error(MODULE_AGENT, '❌ Filesystem watcher error', timer.elapsed('shadow-factory'), error);
|
|
490
|
-
// Non-fatal - watcher continues operating
|
|
491
|
-
});
|
|
492
|
-
// Event: Watcher ready
|
|
493
|
-
this.fsWatcher.on('ready', () => {
|
|
494
|
-
logger.info(MODULE_AGENT, '✅ Filesystem watcher ready', timer.elapsed('shadow-factory'));
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
/**
|
|
498
|
-
* Setup git ref watcher for worker branch commits
|
|
499
|
-
*
|
|
500
|
-
* Monitors worker branch ref file (.git/refs/heads/{workerBranch}) for commit SHA changes
|
|
501
|
-
* using fs.watch. Detects new commits and triggers createShadowBackup after debounce period.
|
|
502
|
-
*
|
|
503
|
-
* Architecture:
|
|
504
|
-
* - Uses fs.watch (not chokidar) for lightweight ref monitoring
|
|
505
|
-
* - Reads commit SHA from ref file on each change
|
|
506
|
-
* - Compares with previousCommitSha to detect actual commits
|
|
507
|
-
* - Debounces to handle rapid ref updates (e.g., during rebase)
|
|
508
|
-
* - Triggers backup only for real commit changes
|
|
509
|
-
*
|
|
510
|
-
* Ref File Location:
|
|
511
|
-
* - {workerWorktreePath}/.git/refs/heads/{workerBranch}
|
|
512
|
-
* - Contains commit SHA as plain text (40 hex characters)
|
|
513
|
-
* - Updated by git on each commit to branch
|
|
514
|
-
*
|
|
515
|
-
* Change Detection Strategy:
|
|
516
|
-
* 1. fs.watch fires on any ref file modification
|
|
517
|
-
* 2. Read current SHA from ref file
|
|
518
|
-
* 3. Compare with previousCommitSha
|
|
519
|
-
* 4. If different, debounce and trigger backup
|
|
520
|
-
* 5. Update previousCommitSha for next comparison
|
|
521
|
-
*
|
|
522
|
-
* Debouncing:
|
|
523
|
-
* - Uses config.debounceMs delay (default: 1000ms)
|
|
524
|
-
* - Clears previous timeout if ref changes again
|
|
525
|
-
* - Prevents multiple backups during rapid commits
|
|
526
|
-
*
|
|
527
|
-
* @throws {Error} If ref file doesn't exist or can't be watched
|
|
528
|
-
*
|
|
529
|
-
* @example
|
|
530
|
-
* ```typescript
|
|
531
|
-
* await this.setupGitRefWatcher();
|
|
532
|
-
* // Watcher is now monitoring worker branch for commits
|
|
533
|
-
* ```
|
|
534
|
-
*/
|
|
535
|
-
async setupGitRefWatcher() {
|
|
536
|
-
// Construct path to worker branch ref file
|
|
537
|
-
// Handle both regular repos and git worktrees
|
|
538
|
-
let gitDir = path.join(this.workerWorktreePath, '.git');
|
|
539
|
-
// Check if .git is a file (worktree) or directory (regular repo)
|
|
540
|
-
if (fs.existsSync(gitDir)) {
|
|
541
|
-
const gitStat = fs.statSync(gitDir);
|
|
542
|
-
if (gitStat.isFile()) {
|
|
543
|
-
// This is a worktree - read the .git file to get actual git directory
|
|
544
|
-
const gitFileContent = fs.readFileSync(gitDir, 'utf-8').trim();
|
|
545
|
-
const match = gitFileContent.match(/^gitdir:\s*(.+)$/);
|
|
546
|
-
if (match) {
|
|
547
|
-
gitDir = match[1].trim();
|
|
548
|
-
logger.info(MODULE_AGENT, `📁 Detected git worktree, actual git dir: ${gitDir}`, timer.elapsed('shadow-factory'));
|
|
549
|
-
// For worktrees, refs are stored in the common (main) git directory
|
|
550
|
-
// Read the commondir file to get the path to the main git directory
|
|
551
|
-
const commondirPath = path.join(gitDir, 'commondir');
|
|
552
|
-
if (fs.existsSync(commondirPath)) {
|
|
553
|
-
const commondirContent = fs.readFileSync(commondirPath, 'utf-8').trim();
|
|
554
|
-
// commondir contains a relative path to the main git directory
|
|
555
|
-
const mainGitDir = path.resolve(gitDir, commondirContent);
|
|
556
|
-
logger.info(MODULE_AGENT, `📁 Worktree refs stored in common dir: ${mainGitDir}`, timer.elapsed('shadow-factory'));
|
|
557
|
-
gitDir = mainGitDir;
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
const refFilePath = path.join(gitDir, 'refs/heads', this.workerBranch);
|
|
563
|
-
logger.info(MODULE_AGENT, `👁️ Setting up git ref watcher: ${refFilePath}`, timer.elapsed('shadow-factory'));
|
|
564
|
-
// Verify ref file exists before watching
|
|
565
|
-
if (!fs.existsSync(refFilePath)) {
|
|
566
|
-
logger.warn(MODULE_AGENT, `⚠️ Ref file not found: ${refFilePath}`, timer.elapsed('shadow-factory'));
|
|
567
|
-
logger.warn(MODULE_AGENT, ` Worker branch may not exist yet. Skipping ref watcher setup.`, timer.elapsed('shadow-factory'));
|
|
568
|
-
return;
|
|
569
|
-
}
|
|
570
|
-
// Read initial commit SHA
|
|
571
|
-
try {
|
|
572
|
-
this.previousCommitSha = fs.readFileSync(refFilePath, 'utf-8').trim();
|
|
573
|
-
logger.info(MODULE_AGENT, `📋 Initial commit SHA: ${this.previousCommitSha.substring(0, 7)}`, timer.elapsed('shadow-factory'));
|
|
574
|
-
}
|
|
575
|
-
catch (error) {
|
|
576
|
-
logger.error(MODULE_AGENT, `❌ Failed to read initial commit SHA: ${error.message}`, timer.elapsed('shadow-factory'), error);
|
|
577
|
-
this.previousCommitSha = null;
|
|
578
|
-
}
|
|
579
|
-
// Setup fs.watch for ref file
|
|
580
|
-
try {
|
|
581
|
-
this.refWatcher = fs.watch(refFilePath, (eventType, _filename) => {
|
|
582
|
-
// Handle 'change' and 'rename' events (rename can occur during git operations)
|
|
583
|
-
if (eventType !== 'change' && eventType !== 'rename') {
|
|
394
|
+
if (relativePath.startsWith('.git') ||
|
|
395
|
+
relativePath.includes('node_modules') ||
|
|
396
|
+
relativePath.startsWith('.env'))
|
|
584
397
|
return;
|
|
398
|
+
const eventType = data.event; // 'add' | 'change' | 'unlink'
|
|
399
|
+
const operation = eventType === 'add' ? 'Created'
|
|
400
|
+
: eventType === 'unlink' ? 'Deleted'
|
|
401
|
+
: 'Modified';
|
|
402
|
+
logger.info(MODULE_AGENT, `📁 Broker file event: ${operation} ${relativePath}`, timer.elapsed('shadow-factory'));
|
|
403
|
+
// Debounce to avoid rapid-fire commits
|
|
404
|
+
if (this.debounceMap.has(filePath)) {
|
|
405
|
+
clearTimeout(this.debounceMap.get(filePath));
|
|
585
406
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
}
|
|
591
|
-
// Debounce to handle rapid ref updates
|
|
592
|
-
this.refDebounceTimeout = setTimeout(async () => {
|
|
593
|
-
try {
|
|
594
|
-
// Read current commit SHA from ref file
|
|
595
|
-
const currentSha = fs.readFileSync(refFilePath, 'utf-8').trim();
|
|
596
|
-
// Check if SHA actually changed (ignore non-commit ref updates)
|
|
597
|
-
if (currentSha === this.previousCommitSha) {
|
|
598
|
-
logger.info(MODULE_AGENT, `ℹ️ Ref updated but SHA unchanged - skipping`, timer.elapsed('shadow-factory'));
|
|
599
|
-
return;
|
|
600
|
-
}
|
|
601
|
-
logger.info(MODULE_AGENT, `🔄 Worker commit detected on ${this.workerBranch}`, timer.elapsed('shadow-factory'));
|
|
602
|
-
logger.info(MODULE_AGENT, ` Previous SHA: ${this.previousCommitSha?.substring(0, 7) || 'none'}`, timer.elapsed('shadow-factory'));
|
|
603
|
-
logger.info(MODULE_AGENT, ` Current SHA: ${currentSha.substring(0, 7)}`, timer.elapsed('shadow-factory'));
|
|
604
|
-
// Update tracked SHA
|
|
605
|
-
this.previousCommitSha = currentSha;
|
|
606
|
-
// Trigger shadow backup for commit
|
|
607
|
-
await this.createShadowBackup('COMMIT', `Commit ${currentSha.substring(0, 7)}`);
|
|
608
|
-
}
|
|
609
|
-
catch (error) {
|
|
610
|
-
logger.error(MODULE_AGENT, `❌ Failed to process ref change: ${error.message}`, timer.elapsed('shadow-factory'), error);
|
|
611
|
-
// Non-fatal - watcher continues operating
|
|
612
|
-
}
|
|
407
|
+
const timeout = setTimeout(async () => {
|
|
408
|
+
logger.info(MODULE_AGENT, `Processing ${operation.toLowerCase()} file: ${relativePath}`, timer.elapsed('shadow-factory'));
|
|
409
|
+
await this.createShadowBackup(operation, relativePath);
|
|
410
|
+
this.debounceMap.delete(filePath);
|
|
613
411
|
}, this.debounceMs);
|
|
614
|
-
|
|
615
|
-
|
|
412
|
+
this.debounceMap.set(filePath, timeout);
|
|
413
|
+
}
|
|
414
|
+
catch (err) {
|
|
415
|
+
logger.error(MODULE_AGENT, `Error processing file.changed event: ${err.message}`, timer.elapsed('shadow-factory'), err);
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
this.fileEventHandler = handler;
|
|
419
|
+
await this.client.subscribe('file.changed', handler);
|
|
420
|
+
logger.info(MODULE_AGENT, 'Subscribed to file.changed broker events', timer.elapsed('shadow-factory'));
|
|
421
|
+
// Invoke ability-file-local's watch_folder to start emitting file.changed events
|
|
422
|
+
if (this.nativeFileLocal) {
|
|
423
|
+
try {
|
|
424
|
+
await this.nativeFileLocal.invoke('watch_folder', {
|
|
425
|
+
folderPath: this.workerWorktreePath,
|
|
426
|
+
watchId: `shadow-${this.role}`,
|
|
427
|
+
});
|
|
428
|
+
logger.info(MODULE_AGENT, `Started file watcher via native ability-file-local for: ${this.workerWorktreePath}`, timer.elapsed('shadow-factory'));
|
|
429
|
+
}
|
|
430
|
+
catch (err) {
|
|
431
|
+
logger.warn(MODULE_AGENT, `Could not start native file watcher: ${err.message}`, timer.elapsed('shadow-factory'));
|
|
432
|
+
}
|
|
616
433
|
}
|
|
617
|
-
|
|
618
|
-
logger.
|
|
619
|
-
this.refWatcher = null;
|
|
620
|
-
// Non-fatal - agent continues with filesystem watching only
|
|
434
|
+
else {
|
|
435
|
+
logger.warn(MODULE_AGENT, 'ability-file-local not loaded natively — file watcher disabled (broker fallback not available)', timer.elapsed('shadow-factory'));
|
|
621
436
|
}
|
|
622
|
-
// Handle watcher errors
|
|
623
|
-
this.refWatcher?.on('error', (error) => {
|
|
624
|
-
logger.error(MODULE_AGENT, '❌ Git ref watcher error', timer.elapsed('shadow-factory'), error);
|
|
625
|
-
// Non-fatal - watcher may auto-recover
|
|
626
|
-
});
|
|
627
437
|
}
|
|
628
438
|
/**
|
|
629
439
|
* Create shadow backup commit
|
|
@@ -650,13 +460,13 @@ export class BaseShadowAgent {
|
|
|
650
460
|
logger.info(MODULE_AGENT, `📦 Creating shadow backup: ${operation} - ${fileName}`, timer.elapsed('shadow-factory'));
|
|
651
461
|
// Check circuit breaker state before attempting git operations
|
|
652
462
|
if (this.checkCircuitBreaker()) {
|
|
653
|
-
logger.warn(MODULE_AGENT,
|
|
463
|
+
logger.warn(MODULE_AGENT, `Circuit breaker open - skipping backup operation`, timer.elapsed('shadow-factory'));
|
|
654
464
|
return;
|
|
655
465
|
}
|
|
656
466
|
try {
|
|
657
467
|
// For COMMIT operations, parse worker commit and copy changed files
|
|
658
468
|
if (operation === 'COMMIT') {
|
|
659
|
-
logger.info(MODULE_AGENT,
|
|
469
|
+
logger.info(MODULE_AGENT, `Processing worker commit mirror...`, timer.elapsed('shadow-factory'));
|
|
660
470
|
// Step 1: Get latest commit hash from worker worktree
|
|
661
471
|
const commitHash = execSync('git log -1 --format=%H', {
|
|
662
472
|
cwd: this.workerWorktreePath,
|
|
@@ -709,7 +519,7 @@ export class BaseShadowAgent {
|
|
|
709
519
|
}
|
|
710
520
|
catch (copyError) {
|
|
711
521
|
// File may have been deleted - that's ok, git will handle it
|
|
712
|
-
logger.info(MODULE_AGENT, `
|
|
522
|
+
logger.info(MODULE_AGENT, ` Could not copy ${file}: ${copyError.message}`, timer.elapsed('shadow-factory'));
|
|
713
523
|
}
|
|
714
524
|
}
|
|
715
525
|
// Step 5: Stage all changes in shadow worktree
|
|
@@ -721,7 +531,7 @@ export class BaseShadowAgent {
|
|
|
721
531
|
try {
|
|
722
532
|
execSync('git diff --cached --quiet', { cwd: this.shadowWorktreePath });
|
|
723
533
|
// Exit code 0 = nothing staged — FS watcher already backed up this change
|
|
724
|
-
logger.info(MODULE_AGENT,
|
|
534
|
+
logger.info(MODULE_AGENT, `No new changes to commit (already backed up by filesystem watcher)`, timer.elapsed('shadow-factory'));
|
|
725
535
|
this.recordGitSuccess();
|
|
726
536
|
await this.publishBackupStatus(true, changedFiles, 'mirror-commit-skipped');
|
|
727
537
|
return;
|
|
@@ -740,7 +550,7 @@ export class BaseShadowAgent {
|
|
|
740
550
|
cwd: this.shadowWorktreePath,
|
|
741
551
|
encoding: 'utf-8'
|
|
742
552
|
}).trim();
|
|
743
|
-
logger.info(MODULE_AGENT,
|
|
553
|
+
logger.info(MODULE_AGENT, `Shadow commit created: ${shadowCommitHash.substring(0, 7)}`, timer.elapsed('shadow-factory'));
|
|
744
554
|
// Record success and reset failure count
|
|
745
555
|
this.recordGitSuccess();
|
|
746
556
|
// Publish backup success event using standardized method
|
|
@@ -748,7 +558,7 @@ export class BaseShadowAgent {
|
|
|
748
558
|
}
|
|
749
559
|
else {
|
|
750
560
|
// For file operations (Created, Modified, Deleted), handle individual file
|
|
751
|
-
logger.info(MODULE_AGENT,
|
|
561
|
+
logger.info(MODULE_AGENT, `Processing file operation: ${operation} - ${fileName}`, timer.elapsed('shadow-factory'));
|
|
752
562
|
const srcPath = path.join(this.workerWorktreePath, fileName);
|
|
753
563
|
const destPath = path.join(this.shadowWorktreePath, fileName);
|
|
754
564
|
// Create destination directory if needed
|
|
@@ -783,7 +593,7 @@ export class BaseShadowAgent {
|
|
|
783
593
|
try {
|
|
784
594
|
execSync('git diff --cached --quiet', { cwd: this.shadowWorktreePath });
|
|
785
595
|
// Exit code 0 = nothing staged — content is identical to HEAD
|
|
786
|
-
logger.info(MODULE_AGENT,
|
|
596
|
+
logger.info(MODULE_AGENT, `No new changes to commit (file unchanged)`, timer.elapsed('shadow-factory'));
|
|
787
597
|
this.recordGitSuccess();
|
|
788
598
|
await this.publishBackupStatus(true, [fileName], `file-${operation.toLowerCase()}-skipped`);
|
|
789
599
|
return;
|
|
@@ -802,7 +612,7 @@ export class BaseShadowAgent {
|
|
|
802
612
|
cwd: this.shadowWorktreePath,
|
|
803
613
|
encoding: 'utf-8'
|
|
804
614
|
}).trim();
|
|
805
|
-
logger.info(MODULE_AGENT,
|
|
615
|
+
logger.info(MODULE_AGENT, `Shadow backup commit created: ${commitHash.substring(0, 7)}`, timer.elapsed('shadow-factory'));
|
|
806
616
|
// Record success and reset failure count
|
|
807
617
|
this.recordGitSuccess();
|
|
808
618
|
// Publish backup success event using standardized method
|
|
@@ -810,7 +620,7 @@ export class BaseShadowAgent {
|
|
|
810
620
|
}
|
|
811
621
|
}
|
|
812
622
|
catch (error) {
|
|
813
|
-
logger.error(MODULE_AGENT,
|
|
623
|
+
logger.error(MODULE_AGENT, `Shadow backup failed: ${error.message}`, timer.elapsed('shadow-factory'), error);
|
|
814
624
|
// Record failure and potentially open circuit breaker
|
|
815
625
|
this.recordGitFailure('createShadowBackup', error);
|
|
816
626
|
// Only publish failure event if circuit is not open (avoid spam)
|
|
@@ -860,7 +670,7 @@ export class BaseShadowAgent {
|
|
|
860
670
|
}
|
|
861
671
|
// Publish event using KadiClient
|
|
862
672
|
await this.client.publish(topic, payload, { broker: 'default', network: 'global' });
|
|
863
|
-
logger.info(MODULE_AGENT,
|
|
673
|
+
logger.info(MODULE_AGENT, `Published backup ${success ? 'success' : 'failure'} event to ${topic}`, timer.elapsed('shadow-factory'));
|
|
864
674
|
}
|
|
865
675
|
/**
|
|
866
676
|
* Check circuit breaker state for git operations
|
|
@@ -892,7 +702,7 @@ export class BaseShadowAgent {
|
|
|
892
702
|
*/
|
|
893
703
|
recordGitFailure(operation, error) {
|
|
894
704
|
this.gitFailureCount++;
|
|
895
|
-
logger.error(MODULE_AGENT,
|
|
705
|
+
logger.error(MODULE_AGENT, `Git operation failed (${this.gitFailureCount}/${this.MAX_GIT_FAILURES}): ${operation}`, timer.elapsed('shadow-factory'), error);
|
|
896
706
|
if (this.gitFailureCount >= this.MAX_GIT_FAILURES) {
|
|
897
707
|
this.gitCircuitOpen = true;
|
|
898
708
|
logger.error(MODULE_AGENT, `🚨 Circuit breaker opened - too many git failures`, timer.elapsed('shadow-factory'));
|
|
@@ -900,7 +710,7 @@ export class BaseShadowAgent {
|
|
|
900
710
|
setTimeout(() => {
|
|
901
711
|
this.gitCircuitOpen = false;
|
|
902
712
|
this.gitFailureCount = 0;
|
|
903
|
-
logger.info(MODULE_AGENT,
|
|
713
|
+
logger.info(MODULE_AGENT, `Circuit breaker reset - retrying git operations`, timer.elapsed('shadow-factory'));
|
|
904
714
|
}, this.CIRCUIT_RESET_TIME);
|
|
905
715
|
}
|
|
906
716
|
}
|
|
@@ -947,7 +757,7 @@ export class BaseShadowAgent {
|
|
|
947
757
|
* debounceMs: 2000 // optional
|
|
948
758
|
* };
|
|
949
759
|
*
|
|
950
|
-
* ShadowAgentConfigSchema.parse(validConfig); //
|
|
760
|
+
* ShadowAgentConfigSchema.parse(validConfig); // Passes validation
|
|
951
761
|
* ```
|
|
952
762
|
*/
|
|
953
763
|
export const ShadowAgentConfigSchema = z.object({
|
|
@@ -1015,13 +825,13 @@ export const ShadowAgentConfigSchema = z.object({
|
|
|
1015
825
|
* ```typescript
|
|
1016
826
|
* try {
|
|
1017
827
|
* const agent = ShadowAgentFactory.createAgent({
|
|
1018
|
-
* role: '', //
|
|
828
|
+
* role: '', // Empty role will fail validation
|
|
1019
829
|
* workerWorktreePath: 'C:/p4/Personal/SD/agent-playground-artist',
|
|
1020
830
|
* shadowWorktreePath: 'C:/p4/Personal/SD/shadow-artist-backup',
|
|
1021
831
|
* workerBranch: 'main',
|
|
1022
832
|
* shadowBranch: 'shadow-main',
|
|
1023
833
|
* brokerUrl: 'ws://localhost:8080/kadi',
|
|
1024
|
-
* networks: [] //
|
|
834
|
+
* networks: [] // Empty networks array will fail validation
|
|
1025
835
|
* });
|
|
1026
836
|
* } catch (error) {
|
|
1027
837
|
* console.error('Configuration validation failed:', error.message);
|