agentxchain 2.118.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/bin/agentxchain.js +1 -0
- package/package.json +1 -1
- package/src/commands/run.js +8 -1
- package/src/commands/schedule.js +21 -4
- package/src/commands/status.js +11 -0
- package/src/lib/continuous-run.js +173 -25
- package/src/lib/normalized-config.js +10 -0
package/bin/agentxchain.js
CHANGED
|
@@ -676,6 +676,7 @@ program
|
|
|
676
676
|
.option('--max-runs <n>', 'Maximum consecutive governed runs in continuous mode (default: 100)', parseInt)
|
|
677
677
|
.option('--poll-seconds <n>', 'Seconds between idle-detection cycles in continuous mode (default: 30)', parseInt)
|
|
678
678
|
.option('--max-idle-cycles <n>', 'Stop after N consecutive idle cycles with no derivable work (default: 3)', parseInt)
|
|
679
|
+
.option('--session-budget <usd>', 'Cumulative session-level budget cap in USD for continuous mode', parseFloat)
|
|
679
680
|
.action(runCommand);
|
|
680
681
|
|
|
681
682
|
program
|
package/package.json
CHANGED
package/src/commands/run.js
CHANGED
|
@@ -57,10 +57,17 @@ export async function runCommand(opts) {
|
|
|
57
57
|
// Continuous vision-driven mode
|
|
58
58
|
const contOpts = resolveContinuousOptions(opts, context.config);
|
|
59
59
|
if (contOpts.enabled) {
|
|
60
|
+
if (contOpts.perSessionMaxUsd != null && (!Number.isFinite(contOpts.perSessionMaxUsd) || contOpts.perSessionMaxUsd <= 0)) {
|
|
61
|
+
console.log(chalk.red('--session-budget must be a finite number greater than 0'));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
60
64
|
console.log(chalk.cyan.bold('agentxchain run --continuous'));
|
|
61
65
|
console.log(chalk.dim(` Vision: ${contOpts.visionPath}`));
|
|
62
66
|
console.log(chalk.dim(` Max runs: ${contOpts.maxRuns}, Poll: ${contOpts.pollSeconds}s, Idle limit: ${contOpts.maxIdleCycles}`));
|
|
63
67
|
console.log(chalk.dim(` Triage approval: ${contOpts.triageApproval}`));
|
|
68
|
+
if (contOpts.perSessionMaxUsd != null) {
|
|
69
|
+
console.log(chalk.dim(` Session budget: $${contOpts.perSessionMaxUsd.toFixed(2)}`));
|
|
70
|
+
}
|
|
64
71
|
console.log('');
|
|
65
72
|
const { exitCode } = await executeContinuousRun(context, contOpts, executeGovernedRun);
|
|
66
73
|
process.exit(exitCode);
|
|
@@ -194,10 +201,10 @@ export async function executeGovernedRun(context, opts = {}) {
|
|
|
194
201
|
const onSigint = () => {
|
|
195
202
|
sigintCount++;
|
|
196
203
|
if (sigintCount >= 2) {
|
|
204
|
+
controller.abort();
|
|
197
205
|
process.exit(130);
|
|
198
206
|
}
|
|
199
207
|
aborted = true;
|
|
200
|
-
controller.abort();
|
|
201
208
|
log(chalk.yellow('\nSIGINT received — finishing current turn, then stopping.'));
|
|
202
209
|
};
|
|
203
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);
|
|
@@ -381,6 +389,9 @@ function createScheduleOwnedSession(schedule, scheduleId) {
|
|
|
381
389
|
status: 'running',
|
|
382
390
|
owner_type: 'schedule',
|
|
383
391
|
owner_id: scheduleId,
|
|
392
|
+
per_session_max_usd: schedule.continuous.per_session_max_usd || null,
|
|
393
|
+
cumulative_spent_usd: 0,
|
|
394
|
+
budget_exhausted: false,
|
|
384
395
|
};
|
|
385
396
|
}
|
|
386
397
|
|
|
@@ -443,6 +454,7 @@ async function advanceScheduleContinuousSession(context, entry, opts = {}) {
|
|
|
443
454
|
maxRuns: contConfig.max_runs,
|
|
444
455
|
maxIdleCycles: contConfig.max_idle_cycles,
|
|
445
456
|
triageApproval: contConfig.triage_approval,
|
|
457
|
+
perSessionMaxUsd: contConfig.per_session_max_usd || null,
|
|
446
458
|
};
|
|
447
459
|
|
|
448
460
|
// Advance one step
|
|
@@ -456,7 +468,10 @@ async function advanceScheduleContinuousSession(context, entry, opts = {}) {
|
|
|
456
468
|
blocked: 'continuous_blocked',
|
|
457
469
|
running: 'continuous_running',
|
|
458
470
|
};
|
|
459
|
-
|
|
471
|
+
let schedStatus = statusMap[step.status] || 'continuous_running';
|
|
472
|
+
if (step.action === 'session_budget_exhausted') {
|
|
473
|
+
schedStatus = 'continuous_session_budget_exhausted';
|
|
474
|
+
}
|
|
460
475
|
|
|
461
476
|
updateScheduleState(root, config, scheduleId, (record) => ({
|
|
462
477
|
...record,
|
|
@@ -646,6 +661,7 @@ export async function scheduleDaemonCommand(opts) {
|
|
|
646
661
|
while (true) {
|
|
647
662
|
cycle += 1;
|
|
648
663
|
daemonState.last_cycle_started_at = new Date().toISOString();
|
|
664
|
+
const continuousScheduleIds = getContinuousEnabledScheduleIds(context.config);
|
|
649
665
|
|
|
650
666
|
// Check for continuous schedule entries first
|
|
651
667
|
const contEntry = selectContinuousScheduleEntry(context.root, context.config, {
|
|
@@ -679,7 +695,7 @@ export async function scheduleDaemonCommand(opts) {
|
|
|
679
695
|
...opts,
|
|
680
696
|
continueActiveScheduleRuns: true,
|
|
681
697
|
tolerateBlockedRun: true,
|
|
682
|
-
|
|
698
|
+
excludeSchedules: continuousScheduleIds,
|
|
683
699
|
});
|
|
684
700
|
|
|
685
701
|
// Merge results
|
|
@@ -704,6 +720,7 @@ export async function scheduleDaemonCommand(opts) {
|
|
|
704
720
|
...opts,
|
|
705
721
|
continueActiveScheduleRuns: true,
|
|
706
722
|
tolerateBlockedRun: true,
|
|
723
|
+
excludeSchedules: continuousScheduleIds,
|
|
707
724
|
});
|
|
708
725
|
}
|
|
709
726
|
|
package/src/commands/status.js
CHANGED
|
@@ -215,6 +215,17 @@ function renderGovernedStatus(context, opts) {
|
|
|
215
215
|
if (continuousSession.idle_cycles > 0) {
|
|
216
216
|
console.log(chalk.dim(` Idle cycles: ${continuousSession.idle_cycles}/${continuousSession.max_idle_cycles}`));
|
|
217
217
|
}
|
|
218
|
+
if (continuousSession.per_session_max_usd != null) {
|
|
219
|
+
const spent = (continuousSession.cumulative_spent_usd || 0).toFixed(2);
|
|
220
|
+
const limit = continuousSession.per_session_max_usd.toFixed(2);
|
|
221
|
+
const pct = continuousSession.per_session_max_usd > 0
|
|
222
|
+
? ((continuousSession.cumulative_spent_usd || 0) / continuousSession.per_session_max_usd * 100).toFixed(1)
|
|
223
|
+
: '0.0';
|
|
224
|
+
const budgetStr = continuousSession.budget_exhausted
|
|
225
|
+
? chalk.red(`$${spent} / $${limit} (${pct}%) [EXHAUSTED]`)
|
|
226
|
+
: `$${spent} / $${limit} (${pct}%)`;
|
|
227
|
+
console.log(` Budget: ${budgetStr}`);
|
|
228
|
+
}
|
|
218
229
|
console.log(chalk.dim(' ' + '─'.repeat(44)));
|
|
219
230
|
console.log('');
|
|
220
231
|
}
|
|
@@ -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';
|
|
@@ -54,7 +55,7 @@ export function removeContinuousSession(root) {
|
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
function createSession(visionPath, maxRuns, maxIdleCycles) {
|
|
58
|
+
function createSession(visionPath, maxRuns, maxIdleCycles, perSessionMaxUsd) {
|
|
58
59
|
return {
|
|
59
60
|
session_id: `cont-${randomUUID().slice(0, 8)}`,
|
|
60
61
|
started_at: new Date().toISOString(),
|
|
@@ -66,9 +67,40 @@ function createSession(visionPath, maxRuns, maxIdleCycles) {
|
|
|
66
67
|
current_run_id: null,
|
|
67
68
|
current_vision_objective: null,
|
|
68
69
|
status: 'running',
|
|
70
|
+
per_session_max_usd: perSessionMaxUsd || null,
|
|
71
|
+
cumulative_spent_usd: 0,
|
|
72
|
+
budget_exhausted: false,
|
|
69
73
|
};
|
|
70
74
|
}
|
|
71
75
|
|
|
76
|
+
function describeContinuousTerminalStep(step, contOpts) {
|
|
77
|
+
if (step.action === 'max_runs_reached') {
|
|
78
|
+
return `Max runs reached (${contOpts.maxRuns}). Stopping.`;
|
|
79
|
+
}
|
|
80
|
+
if (step.action === 'session_budget_exhausted') {
|
|
81
|
+
return 'Session budget exhausted. Stopping.';
|
|
82
|
+
}
|
|
83
|
+
if (step.action === 'operator_stopped') {
|
|
84
|
+
return 'Continuous loop stopped by operator.';
|
|
85
|
+
}
|
|
86
|
+
if (step.status === 'idle_exit') {
|
|
87
|
+
return `All vision goals appear addressed (${contOpts.maxIdleCycles} consecutive idle cycles). Stopping.`;
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
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
|
+
|
|
72
104
|
// ---------------------------------------------------------------------------
|
|
73
105
|
// Intake queue check
|
|
74
106
|
// ---------------------------------------------------------------------------
|
|
@@ -274,6 +306,7 @@ export function resolveContinuousOptions(opts, config) {
|
|
|
274
306
|
maxIdleCycles: opts.maxIdleCycles ?? configCont.max_idle_cycles ?? 3,
|
|
275
307
|
triageApproval: configCont.triage_approval ?? 'auto',
|
|
276
308
|
cooldownSeconds: opts.cooldownSeconds ?? configCont.cooldown_seconds ?? 5,
|
|
309
|
+
perSessionMaxUsd: opts.sessionBudget ?? configCont.per_session_max_usd ?? null,
|
|
277
310
|
};
|
|
278
311
|
}
|
|
279
312
|
|
|
@@ -312,6 +345,66 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
|
|
|
312
345
|
return { ok: true, status: 'idle_exit', action: 'max_idle_reached', stop_reason: 'idle_exit' };
|
|
313
346
|
}
|
|
314
347
|
|
|
348
|
+
// Session budget check (cumulative spend across all runs)
|
|
349
|
+
const sessionBudget = session.per_session_max_usd ?? contOpts.perSessionMaxUsd ?? null;
|
|
350
|
+
if (sessionBudget != null && (session.cumulative_spent_usd || 0) >= sessionBudget) {
|
|
351
|
+
session.status = 'completed';
|
|
352
|
+
session.budget_exhausted = true;
|
|
353
|
+
writeContinuousSession(root, session);
|
|
354
|
+
log(`Session budget exhausted: $${(session.cumulative_spent_usd || 0).toFixed(2)} spent of $${sessionBudget.toFixed(2)} limit.`);
|
|
355
|
+
return { ok: true, status: 'completed', action: 'session_budget_exhausted', stop_reason: 'session_budget' };
|
|
356
|
+
}
|
|
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
|
+
|
|
315
408
|
// Validate vision file
|
|
316
409
|
if (!existsSync(absVisionPath)) {
|
|
317
410
|
session.status = 'failed';
|
|
@@ -381,38 +474,94 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
|
|
|
381
474
|
session.status = 'running';
|
|
382
475
|
writeContinuousSession(root, session);
|
|
383
476
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
+
}
|
|
389
497
|
|
|
390
|
-
session.
|
|
391
|
-
session.
|
|
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);
|
|
392
500
|
|
|
393
501
|
const stopReason = execution.result?.stop_reason;
|
|
394
|
-
log(`Run ${session.runs_completed}/${contOpts.maxRuns} completed: ${stopReason || 'unknown'}`);
|
|
395
502
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
if (!resolved.ok) {
|
|
399
|
-
log(`Continuous resolve error: ${resolved.error}`);
|
|
400
|
-
session.status = 'failed';
|
|
503
|
+
if (stopReason === 'priority_preempted') {
|
|
504
|
+
log('Priority preemption detected — consuming injected work next cycle.');
|
|
401
505
|
writeContinuousSession(root, session);
|
|
402
|
-
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
|
+
};
|
|
403
513
|
}
|
|
404
514
|
|
|
405
|
-
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
|
+
}
|
|
406
523
|
session.status = 'paused';
|
|
407
524
|
log('Run blocked — continuous loop paused. Use `agentxchain unblock <id>` to resume.');
|
|
408
525
|
writeContinuousSession(root, session);
|
|
409
526
|
return { ok: true, status: 'blocked', action: 'run_blocked', run_id: session.current_run_id, intent_id: targetIntentId };
|
|
410
527
|
}
|
|
411
528
|
|
|
412
|
-
if (stopReason === '
|
|
413
|
-
|
|
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';
|
|
414
563
|
writeContinuousSession(root, session);
|
|
415
|
-
return { ok:
|
|
564
|
+
return { ok: false, status: 'failed', action: 'resolve_failed', stop_reason: resolved.error, intent_id: targetIntentId };
|
|
416
565
|
}
|
|
417
566
|
|
|
418
567
|
writeContinuousSession(root, session);
|
|
@@ -449,7 +598,7 @@ export async function executeContinuousRun(context, contOpts, executeGovernedRun
|
|
|
449
598
|
return { exitCode: 1, session: null };
|
|
450
599
|
}
|
|
451
600
|
|
|
452
|
-
const session = createSession(contOpts.visionPath, contOpts.maxRuns, contOpts.maxIdleCycles);
|
|
601
|
+
const session = createSession(contOpts.visionPath, contOpts.maxRuns, contOpts.maxIdleCycles, contOpts.perSessionMaxUsd);
|
|
453
602
|
writeContinuousSession(root, session);
|
|
454
603
|
|
|
455
604
|
// SIGINT handler
|
|
@@ -465,11 +614,10 @@ export async function executeContinuousRun(context, contOpts, executeGovernedRun
|
|
|
465
614
|
const step = await advanceContinuousRunOnce(context, session, contOpts, executeGovernedRun, log);
|
|
466
615
|
|
|
467
616
|
// Terminal states
|
|
468
|
-
if (step.status === 'completed' || step.status === 'idle_exit' || step.status === 'failed' || step.status === 'blocked') {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
log(`All vision goals appear addressed (${contOpts.maxIdleCycles} consecutive idle cycles). Stopping.`);
|
|
617
|
+
if (step.status === 'completed' || step.status === 'idle_exit' || step.status === 'failed' || step.status === 'blocked' || step.status === 'stopped') {
|
|
618
|
+
const terminalMessage = describeContinuousTerminalStep(step, contOpts);
|
|
619
|
+
if (terminalMessage) {
|
|
620
|
+
log(terminalMessage);
|
|
473
621
|
}
|
|
474
622
|
return { exitCode: step.ok ? 0 : 1, session };
|
|
475
623
|
}
|
|
@@ -711,6 +711,13 @@ export function validateSchedulesConfig(schedules, roles) {
|
|
|
711
711
|
if ('triage_approval' in cont && cont.triage_approval !== 'auto' && cont.triage_approval !== 'human') {
|
|
712
712
|
errors.push(`Schedule "${scheduleId}": continuous.triage_approval must be "auto" or "human"`);
|
|
713
713
|
}
|
|
714
|
+
if ('per_session_max_usd' in cont && cont.per_session_max_usd != null) {
|
|
715
|
+
if (typeof cont.per_session_max_usd !== 'number' || !Number.isFinite(cont.per_session_max_usd)) {
|
|
716
|
+
errors.push(`Schedule "${scheduleId}": continuous.per_session_max_usd must be a finite number when provided`);
|
|
717
|
+
} else if (cont.per_session_max_usd <= 0) {
|
|
718
|
+
errors.push(`Schedule "${scheduleId}": continuous.per_session_max_usd must be greater than 0 when provided`);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
714
721
|
}
|
|
715
722
|
}
|
|
716
723
|
}
|
|
@@ -1153,6 +1160,9 @@ function normalizeContinuousConfig(raw) {
|
|
|
1153
1160
|
max_runs: Number.isInteger(raw.max_runs) && raw.max_runs >= 1 ? raw.max_runs : 50,
|
|
1154
1161
|
max_idle_cycles: Number.isInteger(raw.max_idle_cycles) && raw.max_idle_cycles >= 1 ? raw.max_idle_cycles : 5,
|
|
1155
1162
|
triage_approval: raw.triage_approval === 'human' ? 'human' : 'auto',
|
|
1163
|
+
per_session_max_usd: Number.isFinite(raw.per_session_max_usd) && raw.per_session_max_usd > 0
|
|
1164
|
+
? raw.per_session_max_usd
|
|
1165
|
+
: null,
|
|
1156
1166
|
};
|
|
1157
1167
|
}
|
|
1158
1168
|
|