@mndrk/agx 1.4.3 → 1.4.4

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.
Files changed (2) hide show
  1. package/index.js +79 -48
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -333,6 +333,7 @@ function createSubtasks(splits, memInfo) {
333
333
 
334
334
  const DAEMON_PID_FILE = path.join(process.env.HOME || process.env.USERPROFILE, '.agx', 'daemon.pid');
335
335
  const DAEMON_LOG_FILE = path.join(process.env.HOME || process.env.USERPROFILE, '.agx', 'daemon.log');
336
+ const DAEMON_STATE_FILE = path.join(process.env.HOME || process.env.USERPROFILE, '.agx', 'daemon-state.json');
336
337
 
337
338
  function isDaemonRunning() {
338
339
  try {
@@ -414,82 +415,112 @@ async function runDaemon() {
414
415
  console.log(`[${new Date().toISOString()}] Daemon starting...`);
415
416
 
416
417
  const DEFAULT_WAKE_INTERVAL = 15 * 60 * 1000; // 15 minutes fallback
418
+ const TICK_INTERVAL = 60 * 1000; // Check every 1 minute
417
419
  const memDir = path.join(process.env.HOME || process.env.USERPROFILE, '.mem');
418
420
 
419
- // Read wake interval from mem state (use shortest interval across all tasks)
420
- function getWakeInterval() {
421
+ // Load/save daemon state (last run times per task)
422
+ function loadState() {
421
423
  try {
422
- const indexFile = path.join(memDir, 'index.json');
423
- if (!fs.existsSync(indexFile)) return DEFAULT_WAKE_INTERVAL;
424
-
425
- const index = JSON.parse(fs.readFileSync(indexFile, 'utf8'));
426
- let minInterval = DEFAULT_WAKE_INTERVAL;
427
-
428
- for (const taskBranch of Object.values(index)) {
429
- try {
430
- execSync(`git checkout ${taskBranch}`, { cwd: memDir, stdio: 'ignore' });
431
- const state = fs.readFileSync(path.join(memDir, 'state.md'), 'utf8');
432
- const wakeMatch = state.match(/^wake:\s*(.+)$/m);
433
- if (wakeMatch) {
434
- const interval = parseWakeInterval(wakeMatch[1]);
435
- if (interval && interval < minInterval) minInterval = interval;
436
- }
437
- } catch {}
424
+ if (fs.existsSync(DAEMON_STATE_FILE)) {
425
+ return JSON.parse(fs.readFileSync(DAEMON_STATE_FILE, 'utf8'));
438
426
  }
439
- return minInterval;
440
- } catch {
441
- return DEFAULT_WAKE_INTERVAL;
442
- }
427
+ } catch {}
428
+ return { lastRun: {} };
429
+ }
430
+
431
+ function saveState(state) {
432
+ const dir = path.dirname(DAEMON_STATE_FILE);
433
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
434
+ fs.writeFileSync(DAEMON_STATE_FILE, JSON.stringify(state, null, 2));
435
+ }
436
+
437
+ // Get wake interval for a specific task
438
+ function getTaskWakeInterval(projectDir) {
439
+ try {
440
+ const wake = execSync('mem wake', { cwd: projectDir, encoding: 'utf8' });
441
+ const wakeMatch = wake.match(/Wake:\s*(.+)/);
442
+ if (wakeMatch) {
443
+ const interval = parseWakeInterval(wakeMatch[1]);
444
+ if (interval) return interval;
445
+ }
446
+ } catch {}
447
+ return DEFAULT_WAKE_INTERVAL;
443
448
  }
444
449
 
445
450
  const tick = async () => {
446
- console.log(`[${new Date().toISOString()}] Daemon tick - checking tasks...`);
451
+ const now = Date.now();
452
+ const state = loadState();
447
453
 
454
+ // Get task list from mem index
448
455
  const indexFile = path.join(memDir, 'index.json');
449
-
450
- if (!fs.existsSync(indexFile)) {
451
- console.log(`[${new Date().toISOString()}] No tasks found`);
452
- return;
453
- }
456
+ if (!fs.existsSync(indexFile)) return;
454
457
 
455
458
  const index = JSON.parse(fs.readFileSync(indexFile, 'utf8'));
459
+ let ranAny = false;
456
460
 
457
461
  for (const [projectDir, taskBranch] of Object.entries(index)) {
458
462
  if (!fs.existsSync(projectDir)) continue;
459
463
 
460
- // Check if task is active
461
464
  try {
462
- execSync(`git checkout ${taskBranch}`, { cwd: memDir, stdio: 'ignore' });
463
- const state = fs.readFileSync(path.join(memDir, 'state.md'), 'utf8');
465
+ // Check if task is due based on its wake interval
466
+ const wakeInterval = getTaskWakeInterval(projectDir);
467
+ const lastRun = state.lastRun[taskBranch] || 0;
468
+ const elapsed = now - lastRun;
464
469
 
465
- if (state.includes('status: active')) {
466
- console.log(`[${new Date().toISOString()}] Continuing task: ${taskBranch} in ${projectDir}`);
467
-
468
- // Run agx continue
469
- try {
470
- execSync(`agx claude -y -p "continue"`, {
471
- cwd: projectDir,
472
- stdio: 'inherit',
473
- timeout: 10 * 60 * 1000 // 10 min timeout
474
- });
475
- } catch (err) {
476
- console.log(`[${new Date().toISOString()}] Task ${taskBranch} error: ${err.message}`);
477
- }
470
+ if (elapsed < wakeInterval) {
471
+ continue; // Not due yet
472
+ }
473
+
474
+ // Use mem to switch and check status
475
+ execSync(`mem switch ${taskBranch.replace('task/', '')}`, { cwd: projectDir, stdio: 'ignore' });
476
+ const status = execSync('mem status', { cwd: projectDir, encoding: 'utf8' });
477
+
478
+ // Check if task is active (not done/blocked)
479
+ if (status.includes('status: done') || status.includes('status: blocked')) {
480
+ continue; // Skip inactive tasks
478
481
  }
482
+
483
+ console.log(`[${new Date().toISOString()}] Running task: ${taskBranch} (due after ${Math.round(wakeInterval/60000)}m)`);
484
+ ranAny = true;
485
+
486
+ // Update last run time before executing (in case it takes a while)
487
+ state.lastRun[taskBranch] = now;
488
+ saveState(state);
489
+
490
+ // Run agx continue
491
+ try {
492
+ execSync(`agx claude -y -p "continue"`, {
493
+ cwd: projectDir,
494
+ stdio: 'inherit',
495
+ timeout: 10 * 60 * 1000 // 10 min timeout
496
+ });
497
+ } catch (err) {
498
+ console.log(`[${new Date().toISOString()}] Task ${taskBranch} error: ${err.message}`);
499
+ }
500
+
479
501
  } catch (err) {
480
502
  console.log(`[${new Date().toISOString()}] Error checking ${taskBranch}: ${err.message}`);
481
503
  }
482
504
  }
505
+
506
+ if (!ranAny) {
507
+ // Only log periodically to avoid spam
508
+ const lastLog = state.lastLog || 0;
509
+ if (now - lastLog > 5 * 60 * 1000) { // Every 5 min
510
+ console.log(`[${new Date().toISOString()}] Daemon tick - no tasks due`);
511
+ state.lastLog = now;
512
+ saveState(state);
513
+ }
514
+ }
483
515
  };
484
516
 
485
517
  // Initial tick
486
518
  await tick();
487
519
 
488
- // Get interval from mem (with fallback) and schedule recurring ticks
489
- const wakeInterval = getWakeInterval();
490
- setInterval(tick, wakeInterval);
520
+ // Check every minute, but only run tasks when their interval is due
521
+ setInterval(tick, TICK_INTERVAL);
491
522
 
492
- console.log(`[${new Date().toISOString()}] Daemon running, wake interval: ${wakeInterval / 1000 / 60}m`);
523
+ console.log(`[${new Date().toISOString()}] Daemon running, checking every ${TICK_INTERVAL / 1000}s`);
493
524
  }
494
525
 
495
526
  // Handle approval prompts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mndrk/agx",
3
- "version": "1.4.3",
3
+ "version": "1.4.4",
4
4
  "description": "Unified AI Agent Wrapper for Gemini, Claude, and Ollama",
5
5
  "main": "index.js",
6
6
  "bin": {