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 +1 -1
- package/src/commands/run.js +1 -1
- package/src/commands/schedule.js +13 -3
- package/src/lib/continuous-run.js +141 -23
package/package.json
CHANGED
package/src/commands/run.js
CHANGED
|
@@ -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);
|
package/src/commands/schedule.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
-
|
|
421
|
-
|
|
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
|
-
|
|
428
|
-
|
|
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 {
|
|
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 (
|
|
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 === '
|
|
444
|
-
|
|
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:
|
|
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);
|