agentxchain 2.119.0 → 2.120.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.119.0",
3
+ "version": "2.120.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -201,10 +201,10 @@ export async function executeGovernedRun(context, opts = {}) {
201
201
  const onSigint = () => {
202
202
  sigintCount++;
203
203
  if (sigintCount >= 2) {
204
+ controller.abort();
204
205
  process.exit(130);
205
206
  }
206
207
  aborted = true;
207
- controller.abort();
208
208
  log(chalk.yellow('\nSIGINT received — finishing current turn, then stopping.'));
209
209
  };
210
210
  process.on('SIGINT', onSigint);
@@ -234,10 +234,12 @@ async function runDueSchedules(context, opts = {}) {
234
234
 
235
235
  const nowIso = opts.at || new Date().toISOString();
236
236
  const results = [];
237
+ const excludedSchedules = new Set(opts.excludeSchedules || []);
237
238
 
238
239
  for (const entry of resolved.entries) {
239
- // Skip entries handled by the continuous session manager
240
- if (opts.excludeSchedule && entry.id === opts.excludeSchedule) {
240
+ // Skip entries handled by the continuous session manager.
241
+ if ((opts.excludeSchedule && entry.id === opts.excludeSchedule)
242
+ || excludedSchedules.has(entry.id)) {
241
243
  continue;
242
244
  }
243
245
  if (!entry.enabled) {
@@ -332,6 +334,12 @@ function isSessionTerminal(session) {
332
334
  return ['completed', 'idle_exit', 'failed', 'stopped'].includes(session?.status);
333
335
  }
334
336
 
337
+ function getContinuousEnabledScheduleIds(config) {
338
+ return Object.entries(config?.schedules || {})
339
+ .filter(([, schedule]) => schedule?.continuous?.enabled === true)
340
+ .map(([id]) => id);
341
+ }
342
+
335
343
  export function selectContinuousScheduleEntry(root, config, opts = {}) {
336
344
  const entries = listSchedules(root, config, { at: opts.at });
337
345
  const continuousEntries = entries.filter((entry) => config?.schedules?.[entry.id]?.continuous?.enabled === true);
@@ -653,6 +661,7 @@ export async function scheduleDaemonCommand(opts) {
653
661
  while (true) {
654
662
  cycle += 1;
655
663
  daemonState.last_cycle_started_at = new Date().toISOString();
664
+ const continuousScheduleIds = getContinuousEnabledScheduleIds(context.config);
656
665
 
657
666
  // Check for continuous schedule entries first
658
667
  const contEntry = selectContinuousScheduleEntry(context.root, context.config, {
@@ -686,7 +695,7 @@ export async function scheduleDaemonCommand(opts) {
686
695
  ...opts,
687
696
  continueActiveScheduleRuns: true,
688
697
  tolerateBlockedRun: true,
689
- excludeSchedule: contEntry.id,
698
+ excludeSchedules: continuousScheduleIds,
690
699
  });
691
700
 
692
701
  // Merge results
@@ -711,6 +720,7 @@ export async function scheduleDaemonCommand(opts) {
711
720
  ...opts,
712
721
  continueActiveScheduleRuns: true,
713
722
  tolerateBlockedRun: true,
723
+ excludeSchedules: continuousScheduleIds,
714
724
  });
715
725
  }
716
726
 
@@ -21,6 +21,7 @@ import {
21
21
  startIntent,
22
22
  resolveIntent,
23
23
  } from './intake.js';
24
+ import { loadProjectState } from './config.js';
24
25
  import { safeWriteJson } from './safe-write.js';
25
26
 
26
27
  const CONTINUOUS_SESSION_PATH = '.agentxchain/continuous-session.json';
@@ -79,12 +80,27 @@ function describeContinuousTerminalStep(step, contOpts) {
79
80
  if (step.action === 'session_budget_exhausted') {
80
81
  return 'Session budget exhausted. Stopping.';
81
82
  }
83
+ if (step.action === 'operator_stopped') {
84
+ return 'Continuous loop stopped by operator.';
85
+ }
82
86
  if (step.status === 'idle_exit') {
83
87
  return `All vision goals appear addressed (${contOpts.maxIdleCycles} consecutive idle cycles). Stopping.`;
84
88
  }
85
89
  return null;
86
90
  }
87
91
 
92
+ function getExecutionRunSpentUsd(execution) {
93
+ return execution?.result?.state?.budget_status?.spent_usd || 0;
94
+ }
95
+
96
+ function isBlockedContinuousExecution(execution) {
97
+ const stopReason = execution?.result?.stop_reason || null;
98
+ const stateStatus = execution?.result?.state?.status || null;
99
+ return stateStatus === 'blocked'
100
+ || stopReason === 'blocked'
101
+ || stopReason === 'reject_exhausted';
102
+ }
103
+
88
104
  // ---------------------------------------------------------------------------
89
105
  // Intake queue check
90
106
  // ---------------------------------------------------------------------------
@@ -339,6 +355,56 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
339
355
  return { ok: true, status: 'completed', action: 'session_budget_exhausted', stop_reason: 'session_budget' };
340
356
  }
341
357
 
358
+ // Paused-session guard: if session is paused (blocked run awaiting unblock),
359
+ // check governed state before attempting to advance. Without this guard, the
360
+ // loop would try to startIntent() on a blocked project, hit the blocked-state
361
+ // rejection, and permanently fail the session instead of staying paused.
362
+ if (session.status === 'paused') {
363
+ const governedState = loadProjectState(root, context.config);
364
+ if (governedState?.status === 'blocked') {
365
+ // Still blocked — stay paused, do not attempt new work
366
+ writeContinuousSession(root, session);
367
+ return { ok: true, status: 'blocked', action: 'still_blocked', run_id: session.current_run_id };
368
+ }
369
+ // Unblocked — resume by continuing the existing governed run directly.
370
+ // Skip the intake pipeline: the run is already in progress, and startIntent
371
+ // would reject because the governed state is active.
372
+ session.status = 'running';
373
+ log('Blocked run resolved — resuming continuous session.');
374
+ writeContinuousSession(root, session);
375
+
376
+ let execution;
377
+ try {
378
+ execution = await executeGovernedRun(context, { autoApprove: true, report: true, log });
379
+ } catch (err) {
380
+ session.status = 'failed';
381
+ writeContinuousSession(root, session);
382
+ return { ok: false, status: 'failed', action: 'run_failed', stop_reason: err.message, run_id: session.current_run_id };
383
+ }
384
+
385
+ session.cumulative_spent_usd = (session.cumulative_spent_usd || 0) + getExecutionRunSpentUsd(execution);
386
+ const resumeStopReason = execution.result?.stop_reason;
387
+
388
+ if (isBlockedContinuousExecution(execution)) {
389
+ session.status = 'paused';
390
+ log('Resumed run blocked again — continuous loop re-paused.');
391
+ writeContinuousSession(root, session);
392
+ return { ok: true, status: 'blocked', action: 'run_blocked', run_id: session.current_run_id };
393
+ }
394
+
395
+ if (execution.exitCode !== 0 || !execution.result) {
396
+ session.status = 'failed';
397
+ writeContinuousSession(root, session);
398
+ return { ok: false, status: 'failed', action: 'run_failed', stop_reason: resumeStopReason || `exit_code_${execution.exitCode}`, run_id: session.current_run_id };
399
+ }
400
+
401
+ session.runs_completed += 1;
402
+ session.current_run_id = execution.result?.state?.run_id || session.current_run_id;
403
+ log(`Resumed run completed (${session.runs_completed}/${contOpts.maxRuns}): ${resumeStopReason || 'completed'}`);
404
+ writeContinuousSession(root, session);
405
+ return { ok: true, status: 'running', action: 'resumed_after_unblock', run_id: session.current_run_id };
406
+ }
407
+
342
408
  // Validate vision file
343
409
  if (!existsSync(absVisionPath)) {
344
410
  session.status = 'failed';
@@ -408,42 +474,94 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
408
474
  session.status = 'running';
409
475
  writeContinuousSession(root, session);
410
476
 
411
- const execution = await executeGovernedRun(context, {
412
- autoApprove: true,
413
- report: true,
414
- log,
415
- });
416
-
417
- session.runs_completed += 1;
418
- session.current_run_id = execution.result?.state?.run_id || null;
477
+ let execution;
478
+ try {
479
+ execution = await executeGovernedRun(context, {
480
+ autoApprove: true,
481
+ report: true,
482
+ log,
483
+ });
484
+ } catch (err) {
485
+ session.status = 'failed';
486
+ writeContinuousSession(root, session);
487
+ log(`Governed run threw during continuous execution: ${err.message}`);
488
+ return {
489
+ ok: false,
490
+ status: 'failed',
491
+ action: 'run_failed',
492
+ stop_reason: err.message,
493
+ run_id: preparedIntent.runId || null,
494
+ intent_id: targetIntentId,
495
+ };
496
+ }
419
497
 
420
- // Accumulate cost from this run into the session total
421
- const runSpentUsd = execution.result?.state?.budget_status?.spent_usd || 0;
422
- session.cumulative_spent_usd = (session.cumulative_spent_usd || 0) + runSpentUsd;
498
+ session.current_run_id = execution.result?.state?.run_id || preparedIntent.runId || null;
499
+ session.cumulative_spent_usd = (session.cumulative_spent_usd || 0) + getExecutionRunSpentUsd(execution);
423
500
 
424
501
  const stopReason = execution.result?.stop_reason;
425
- log(`Run ${session.runs_completed}/${contOpts.maxRuns} completed: ${stopReason || 'unknown'}`);
426
502
 
427
- // Resolve the consumed intent
428
- const resolved = resolveIntent(root, targetIntentId);
429
- if (!resolved.ok) {
430
- log(`Continuous resolve error: ${resolved.error}`);
431
- session.status = 'failed';
503
+ if (stopReason === 'priority_preempted') {
504
+ log('Priority preemption detected consuming injected work next cycle.');
432
505
  writeContinuousSession(root, session);
433
- return { ok: false, status: 'failed', action: 'resolve_failed', stop_reason: resolved.error, intent_id: targetIntentId };
506
+ return {
507
+ ok: true,
508
+ status: 'running',
509
+ action: 'consumed_injected_priority',
510
+ run_id: session.current_run_id,
511
+ intent_id: targetIntentId,
512
+ };
434
513
  }
435
514
 
436
- if (stopReason === 'blocked') {
515
+ if (isBlockedContinuousExecution(execution)) {
516
+ const resolved = resolveIntent(root, targetIntentId);
517
+ if (!resolved.ok) {
518
+ log(`Continuous resolve error: ${resolved.error}`);
519
+ session.status = 'failed';
520
+ writeContinuousSession(root, session);
521
+ return { ok: false, status: 'failed', action: 'resolve_failed', stop_reason: resolved.error, intent_id: targetIntentId };
522
+ }
437
523
  session.status = 'paused';
438
524
  log('Run blocked — continuous loop paused. Use `agentxchain unblock <id>` to resume.');
439
525
  writeContinuousSession(root, session);
440
526
  return { ok: true, status: 'blocked', action: 'run_blocked', run_id: session.current_run_id, intent_id: targetIntentId };
441
527
  }
442
528
 
443
- if (stopReason === 'priority_preempted') {
444
- log('Priority preemption detected — consuming injected work next cycle.');
529
+ if (stopReason === 'caller_stopped') {
530
+ session.status = 'stopped';
531
+ writeContinuousSession(root, session);
532
+ return {
533
+ ok: true,
534
+ status: 'stopped',
535
+ action: 'operator_stopped',
536
+ run_id: session.current_run_id,
537
+ intent_id: targetIntentId,
538
+ };
539
+ }
540
+
541
+ if (execution.exitCode !== 0 || !execution.result) {
542
+ session.status = 'failed';
543
+ writeContinuousSession(root, session);
544
+ log(`Governed run failed during continuous execution: ${stopReason || `exit_code_${execution.exitCode}`}.`);
545
+ return {
546
+ ok: false,
547
+ status: 'failed',
548
+ action: 'run_failed',
549
+ stop_reason: stopReason || `exit_code_${execution.exitCode}`,
550
+ run_id: session.current_run_id,
551
+ intent_id: targetIntentId,
552
+ };
553
+ }
554
+
555
+ session.runs_completed += 1;
556
+ log(`Run ${session.runs_completed}/${contOpts.maxRuns} completed: ${stopReason || 'unknown'}`);
557
+
558
+ // Resolve the consumed intent
559
+ const resolved = resolveIntent(root, targetIntentId);
560
+ if (!resolved.ok) {
561
+ log(`Continuous resolve error: ${resolved.error}`);
562
+ session.status = 'failed';
445
563
  writeContinuousSession(root, session);
446
- return { ok: true, status: 'running', action: 'consumed_injected_priority', run_id: session.current_run_id, intent_id: targetIntentId };
564
+ return { ok: false, status: 'failed', action: 'resolve_failed', stop_reason: resolved.error, intent_id: targetIntentId };
447
565
  }
448
566
 
449
567
  writeContinuousSession(root, session);
@@ -496,7 +614,7 @@ export async function executeContinuousRun(context, contOpts, executeGovernedRun
496
614
  const step = await advanceContinuousRunOnce(context, session, contOpts, executeGovernedRun, log);
497
615
 
498
616
  // Terminal states
499
- if (step.status === 'completed' || step.status === 'idle_exit' || step.status === 'failed' || step.status === 'blocked') {
617
+ if (step.status === 'completed' || step.status === 'idle_exit' || step.status === 'failed' || step.status === 'blocked' || step.status === 'stopped') {
500
618
  const terminalMessage = describeContinuousTerminalStep(step, contOpts);
501
619
  if (terminalMessage) {
502
620
  log(terminalMessage);