agentxchain 2.117.0 → 2.118.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
|
@@ -70,7 +70,7 @@ function extractAggregateEvidenceLine(text) {
|
|
|
70
70
|
return best;
|
|
71
71
|
}, null);
|
|
72
72
|
|
|
73
|
-
return aggregate.line.replace(/\*\*/g, '').replace(/`/g, '').trim();
|
|
73
|
+
return aggregate.line.replace(/\*\*/g, '').replace(/`/g, '').replace(/,/g, '').trim();
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
function getPreviousVersionTag(repoRoot, version) {
|
package/src/commands/schedule.js
CHANGED
|
@@ -15,6 +15,15 @@ import {
|
|
|
15
15
|
} from '../lib/run-schedule.js';
|
|
16
16
|
import { consumePreemptionMarker } from '../lib/intake.js';
|
|
17
17
|
import { executeGovernedRun } from './run.js';
|
|
18
|
+
import {
|
|
19
|
+
readContinuousSession,
|
|
20
|
+
writeContinuousSession,
|
|
21
|
+
advanceContinuousRunOnce,
|
|
22
|
+
resolveContinuousOptions,
|
|
23
|
+
} from '../lib/continuous-run.js';
|
|
24
|
+
import { resolveVisionPath } from '../lib/vision-reader.js';
|
|
25
|
+
import { existsSync } from 'node:fs';
|
|
26
|
+
import { randomUUID } from 'node:crypto';
|
|
18
27
|
|
|
19
28
|
function loadScheduleContext() {
|
|
20
29
|
const context = loadProjectContext();
|
|
@@ -227,6 +236,10 @@ async function runDueSchedules(context, opts = {}) {
|
|
|
227
236
|
const results = [];
|
|
228
237
|
|
|
229
238
|
for (const entry of resolved.entries) {
|
|
239
|
+
// Skip entries handled by the continuous session manager
|
|
240
|
+
if (opts.excludeSchedule && entry.id === opts.excludeSchedule) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
230
243
|
if (!entry.enabled) {
|
|
231
244
|
results.push({ id: entry.id, action: 'disabled' });
|
|
232
245
|
continue;
|
|
@@ -311,6 +324,159 @@ async function runDueSchedules(context, opts = {}) {
|
|
|
311
324
|
return { ok: true, exitCode: 0, results };
|
|
312
325
|
}
|
|
313
326
|
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
// Schedule-owned continuous session management
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
|
|
331
|
+
function isSessionTerminal(session) {
|
|
332
|
+
return ['completed', 'idle_exit', 'failed', 'stopped'].includes(session?.status);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function selectContinuousScheduleEntry(root, config, opts = {}) {
|
|
336
|
+
const entries = listSchedules(root, config, { at: opts.at });
|
|
337
|
+
const continuousEntries = entries.filter((entry) => config?.schedules?.[entry.id]?.continuous?.enabled === true);
|
|
338
|
+
|
|
339
|
+
if (continuousEntries.length === 0) {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (opts.scheduleId) {
|
|
344
|
+
const selected = continuousEntries.find((entry) => entry.id === opts.scheduleId);
|
|
345
|
+
return selected
|
|
346
|
+
? { id: selected.id, schedule: config.schedules[selected.id], due: selected.due }
|
|
347
|
+
: null;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const activeSession = readContinuousSession(root);
|
|
351
|
+
if (activeSession && !isSessionTerminal(activeSession) && activeSession.owner_type === 'schedule') {
|
|
352
|
+
const ownerEntry = continuousEntries.find((entry) => entry.id === activeSession.owner_id);
|
|
353
|
+
if (!ownerEntry) {
|
|
354
|
+
return {
|
|
355
|
+
id: activeSession.owner_id,
|
|
356
|
+
error: `active continuous session owned by unknown schedule "${activeSession.owner_id}"`,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
return { id: ownerEntry.id, schedule: config.schedules[ownerEntry.id], due: ownerEntry.due };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const dueEntry = continuousEntries.find((entry) => entry.due);
|
|
363
|
+
if (!dueEntry) {
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return { id: dueEntry.id, schedule: config.schedules[dueEntry.id], due: dueEntry.due };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function createScheduleOwnedSession(schedule, scheduleId) {
|
|
371
|
+
return {
|
|
372
|
+
session_id: `cont-${randomUUID().slice(0, 8)}`,
|
|
373
|
+
started_at: new Date().toISOString(),
|
|
374
|
+
vision_path: schedule.continuous.vision_path,
|
|
375
|
+
runs_completed: 0,
|
|
376
|
+
max_runs: schedule.continuous.max_runs,
|
|
377
|
+
idle_cycles: 0,
|
|
378
|
+
max_idle_cycles: schedule.continuous.max_idle_cycles,
|
|
379
|
+
current_run_id: null,
|
|
380
|
+
current_vision_objective: null,
|
|
381
|
+
status: 'running',
|
|
382
|
+
owner_type: 'schedule',
|
|
383
|
+
owner_id: scheduleId,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async function advanceScheduleContinuousSession(context, entry, opts = {}) {
|
|
388
|
+
const { root, config } = context;
|
|
389
|
+
const scheduleId = entry.id;
|
|
390
|
+
const schedule = entry.schedule;
|
|
391
|
+
const contConfig = schedule.continuous;
|
|
392
|
+
const log = opts.json ? () => {} : console.log;
|
|
393
|
+
|
|
394
|
+
// Read existing session
|
|
395
|
+
let session = readContinuousSession(root);
|
|
396
|
+
|
|
397
|
+
// If there's an active session owned by a different schedule, fail closed
|
|
398
|
+
if (session && !isSessionTerminal(session) && session.owner_type === 'schedule' && session.owner_id !== scheduleId) {
|
|
399
|
+
return {
|
|
400
|
+
ok: false,
|
|
401
|
+
action: 'skipped',
|
|
402
|
+
reason: `continuous session owned by schedule "${session.owner_id}"`,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Determine if we need a new session
|
|
407
|
+
const needsNewSession = !session || isSessionTerminal(session) || session.owner_id !== scheduleId;
|
|
408
|
+
|
|
409
|
+
if (needsNewSession) {
|
|
410
|
+
// Only start a new session if the schedule is due
|
|
411
|
+
if (!opts.isDue) {
|
|
412
|
+
return { ok: true, action: 'not_due', reason: 'waiting_interval' };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Check launch eligibility
|
|
416
|
+
const eligibility = evaluateScheduleLaunchEligibility(root, config);
|
|
417
|
+
if (!eligibility.ok) {
|
|
418
|
+
return { ok: false, action: 'skipped', reason: eligibility.reason };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Validate vision path
|
|
422
|
+
const absVision = resolveVisionPath(root, contConfig.vision_path);
|
|
423
|
+
if (!existsSync(absVision)) {
|
|
424
|
+
return { ok: false, action: 'failed', reason: `VISION.md not found at ${absVision}` };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
session = createScheduleOwnedSession(schedule, scheduleId);
|
|
428
|
+
writeContinuousSession(root, session);
|
|
429
|
+
log(chalk.cyan(`Started schedule-owned continuous session: ${session.session_id} (schedule: ${scheduleId})`));
|
|
430
|
+
|
|
431
|
+
// Record schedule start
|
|
432
|
+
updateScheduleState(root, config, scheduleId, (record) => ({
|
|
433
|
+
...record,
|
|
434
|
+
last_started_at: new Date().toISOString(),
|
|
435
|
+
last_status: 'continuous_running',
|
|
436
|
+
last_continuous_session_id: session.session_id,
|
|
437
|
+
}));
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Build contOpts from schedule continuous config
|
|
441
|
+
const contOpts = {
|
|
442
|
+
visionPath: contConfig.vision_path,
|
|
443
|
+
maxRuns: contConfig.max_runs,
|
|
444
|
+
maxIdleCycles: contConfig.max_idle_cycles,
|
|
445
|
+
triageApproval: contConfig.triage_approval,
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
// Advance one step
|
|
449
|
+
const step = await advanceContinuousRunOnce(context, session, contOpts, executeGovernedRun, log);
|
|
450
|
+
|
|
451
|
+
// Update schedule state based on step result
|
|
452
|
+
const statusMap = {
|
|
453
|
+
completed: 'continuous_completed',
|
|
454
|
+
idle_exit: 'continuous_idle_exit',
|
|
455
|
+
failed: 'continuous_failed',
|
|
456
|
+
blocked: 'continuous_blocked',
|
|
457
|
+
running: 'continuous_running',
|
|
458
|
+
};
|
|
459
|
+
const schedStatus = statusMap[step.status] || 'continuous_running';
|
|
460
|
+
|
|
461
|
+
updateScheduleState(root, config, scheduleId, (record) => ({
|
|
462
|
+
...record,
|
|
463
|
+
last_finished_at: new Date().toISOString(),
|
|
464
|
+
last_status: schedStatus,
|
|
465
|
+
last_run_id: step.run_id || record.last_run_id,
|
|
466
|
+
last_continuous_session_id: session.session_id,
|
|
467
|
+
}));
|
|
468
|
+
|
|
469
|
+
return {
|
|
470
|
+
ok: step.ok,
|
|
471
|
+
action: step.action,
|
|
472
|
+
status: step.status,
|
|
473
|
+
session_id: session.session_id,
|
|
474
|
+
run_id: step.run_id || null,
|
|
475
|
+
intent_id: step.intent_id || null,
|
|
476
|
+
runs_completed: session.runs_completed,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
314
480
|
export async function scheduleListCommand(opts) {
|
|
315
481
|
const context = loadScheduleContext();
|
|
316
482
|
if (!context) return;
|
|
@@ -480,11 +646,66 @@ export async function scheduleDaemonCommand(opts) {
|
|
|
480
646
|
while (true) {
|
|
481
647
|
cycle += 1;
|
|
482
648
|
daemonState.last_cycle_started_at = new Date().toISOString();
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
649
|
+
|
|
650
|
+
// Check for continuous schedule entries first
|
|
651
|
+
const contEntry = selectContinuousScheduleEntry(context.root, context.config, {
|
|
652
|
+
scheduleId: opts.schedule || null,
|
|
653
|
+
at: opts.at,
|
|
487
654
|
});
|
|
655
|
+
let result;
|
|
656
|
+
|
|
657
|
+
if (contEntry?.error) {
|
|
658
|
+
result = {
|
|
659
|
+
ok: false,
|
|
660
|
+
exitCode: 1,
|
|
661
|
+
results: [{
|
|
662
|
+
id: contEntry.id,
|
|
663
|
+
action: 'failed',
|
|
664
|
+
continuous: true,
|
|
665
|
+
reason: contEntry.error,
|
|
666
|
+
}],
|
|
667
|
+
};
|
|
668
|
+
} else if (contEntry) {
|
|
669
|
+
const isDue = contEntry.due ?? false;
|
|
670
|
+
|
|
671
|
+
const contResult = await advanceScheduleContinuousSession(context, contEntry, {
|
|
672
|
+
isDue,
|
|
673
|
+
json: opts.json,
|
|
674
|
+
at: opts.at,
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
// Run non-continuous schedules normally alongside
|
|
678
|
+
const nonContResult = await runDueSchedules(context, {
|
|
679
|
+
...opts,
|
|
680
|
+
continueActiveScheduleRuns: true,
|
|
681
|
+
tolerateBlockedRun: true,
|
|
682
|
+
excludeSchedule: contEntry.id,
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
// Merge results
|
|
686
|
+
const contResultEntry = {
|
|
687
|
+
id: contEntry.id,
|
|
688
|
+
action: contResult.action,
|
|
689
|
+
continuous: true,
|
|
690
|
+
session_id: contResult.session_id || null,
|
|
691
|
+
status: contResult.status || null,
|
|
692
|
+
run_id: contResult.run_id || null,
|
|
693
|
+
runs_completed: contResult.runs_completed ?? null,
|
|
694
|
+
};
|
|
695
|
+
if (contResult.reason) contResultEntry.reason = contResult.reason;
|
|
696
|
+
|
|
697
|
+
result = {
|
|
698
|
+
ok: contResult.ok !== false && nonContResult.ok,
|
|
699
|
+
exitCode: (contResult.ok === false || !nonContResult.ok) ? 1 : 0,
|
|
700
|
+
results: [contResultEntry, ...nonContResult.results],
|
|
701
|
+
};
|
|
702
|
+
} else {
|
|
703
|
+
result = await runDueSchedules(context, {
|
|
704
|
+
...opts,
|
|
705
|
+
continueActiveScheduleRuns: true,
|
|
706
|
+
tolerateBlockedRun: true,
|
|
707
|
+
});
|
|
708
|
+
}
|
|
488
709
|
|
|
489
710
|
updateDaemonHeartbeat(context.root, daemonState, result);
|
|
490
711
|
|
package/src/commands/status.js
CHANGED
|
@@ -206,6 +206,9 @@ function renderGovernedStatus(context, opts) {
|
|
|
206
206
|
console.log(chalk.dim(` Vision: ${continuousSession.vision_path}`));
|
|
207
207
|
console.log(` Status: ${chalk.cyan(continuousSession.status || 'unknown')}`);
|
|
208
208
|
console.log(` Runs: ${continuousSession.runs_completed || 0}/${continuousSession.max_runs || '?'}`);
|
|
209
|
+
if (continuousSession.owner_type === 'schedule') {
|
|
210
|
+
console.log(chalk.dim(` Owner: schedule:${continuousSession.owner_id}`));
|
|
211
|
+
}
|
|
209
212
|
if (continuousSession.current_vision_objective) {
|
|
210
213
|
console.log(` Objective: ${chalk.yellow(continuousSession.current_vision_objective)}`);
|
|
211
214
|
}
|
|
@@ -278,7 +278,155 @@ export function resolveContinuousOptions(opts, config) {
|
|
|
278
278
|
}
|
|
279
279
|
|
|
280
280
|
// ---------------------------------------------------------------------------
|
|
281
|
-
//
|
|
281
|
+
// Single-step continuous advancement primitive
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Advance a continuous session by exactly one step.
|
|
286
|
+
*
|
|
287
|
+
* This is the shared primitive used by both `run --continuous` (CLI-owned loop)
|
|
288
|
+
* and `schedule daemon` (daemon-owned poll). Neither caller embeds a nested
|
|
289
|
+
* poll/sleep loop — the caller owns cadence, this function owns one step.
|
|
290
|
+
*
|
|
291
|
+
* @param {object} context - { root, config }
|
|
292
|
+
* @param {object} session - mutable session object (read/written by caller)
|
|
293
|
+
* @param {object} contOpts - resolved continuous options (visionPath, maxRuns, maxIdleCycles, triageApproval)
|
|
294
|
+
* @param {Function} executeGovernedRun - the run executor function
|
|
295
|
+
* @param {Function} [log] - logging function
|
|
296
|
+
* @returns {Promise<{ ok: boolean, status: string, action: string, run_id?: string, intent_id?: string, stop_reason?: string }>}
|
|
297
|
+
*/
|
|
298
|
+
export async function advanceContinuousRunOnce(context, session, contOpts, executeGovernedRun, log = console.log) {
|
|
299
|
+
const { root } = context;
|
|
300
|
+
const absVisionPath = resolveVisionPath(root, contOpts.visionPath);
|
|
301
|
+
|
|
302
|
+
// Terminal checks
|
|
303
|
+
if (session.runs_completed >= contOpts.maxRuns) {
|
|
304
|
+
session.status = 'completed';
|
|
305
|
+
writeContinuousSession(root, session);
|
|
306
|
+
return { ok: true, status: 'completed', action: 'max_runs_reached', stop_reason: 'max_runs' };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (session.idle_cycles >= contOpts.maxIdleCycles) {
|
|
310
|
+
session.status = 'completed';
|
|
311
|
+
writeContinuousSession(root, session);
|
|
312
|
+
return { ok: true, status: 'idle_exit', action: 'max_idle_reached', stop_reason: 'idle_exit' };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Validate vision file
|
|
316
|
+
if (!existsSync(absVisionPath)) {
|
|
317
|
+
session.status = 'failed';
|
|
318
|
+
writeContinuousSession(root, session);
|
|
319
|
+
return { ok: false, status: 'failed', action: 'vision_missing', stop_reason: `VISION.md not found at ${absVisionPath}` };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Step 1: Check intake queue for pending work
|
|
323
|
+
const queued = findNextQueuedIntent(root);
|
|
324
|
+
let targetIntentId = null;
|
|
325
|
+
let visionObjective = null;
|
|
326
|
+
|
|
327
|
+
if (queued.ok) {
|
|
328
|
+
targetIntentId = queued.intentId;
|
|
329
|
+
session.idle_cycles = 0;
|
|
330
|
+
log(`Found queued intent: ${queued.intentId} (${queued.status})`);
|
|
331
|
+
} else {
|
|
332
|
+
// Step 2: Derive from vision
|
|
333
|
+
const seeded = seedFromVision(root, absVisionPath, {
|
|
334
|
+
triageApproval: contOpts.triageApproval,
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
if (!seeded.ok) {
|
|
338
|
+
log(`Vision scan error: ${seeded.error}`);
|
|
339
|
+
session.status = 'failed';
|
|
340
|
+
writeContinuousSession(root, session);
|
|
341
|
+
return { ok: false, status: 'failed', action: 'vision_scan_error', stop_reason: seeded.error };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (seeded.idle) {
|
|
345
|
+
session.idle_cycles += 1;
|
|
346
|
+
log(`Idle cycle ${session.idle_cycles}/${contOpts.maxIdleCycles} — no derivable work from vision.`);
|
|
347
|
+
writeContinuousSession(root, session);
|
|
348
|
+
return { ok: true, status: 'running', action: 'no_work_found' };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// If triage_approval is "human", the intent is in "triaged" state — don't auto-start
|
|
352
|
+
if (contOpts.triageApproval === 'human') {
|
|
353
|
+
log(`Vision-derived intent ${seeded.intentId} left in triaged state (triage_approval: human).`);
|
|
354
|
+
session.idle_cycles += 1;
|
|
355
|
+
writeContinuousSession(root, session);
|
|
356
|
+
return { ok: true, status: 'running', action: 'waited_for_human', intent_id: seeded.intentId };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
targetIntentId = seeded.intentId;
|
|
360
|
+
visionObjective = `${seeded.section}: ${seeded.goal}`;
|
|
361
|
+
session.idle_cycles = 0;
|
|
362
|
+
log(`Vision-derived: ${visionObjective}`);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Prepare intent through intake lifecycle
|
|
366
|
+
const provenance = buildContinuousProvenance(targetIntentId, {
|
|
367
|
+
trigger: visionObjective ? 'vision_scan' : 'intake',
|
|
368
|
+
triggerReason: visionObjective || readIntent(root, targetIntentId)?.charter || null,
|
|
369
|
+
});
|
|
370
|
+
const preparedIntent = prepareIntentForRun(root, targetIntentId, { provenance });
|
|
371
|
+
if (!preparedIntent.ok) {
|
|
372
|
+
log(`Continuous start error: ${preparedIntent.error}`);
|
|
373
|
+
session.status = 'failed';
|
|
374
|
+
writeContinuousSession(root, session);
|
|
375
|
+
return { ok: false, status: 'failed', action: 'prepare_failed', stop_reason: preparedIntent.error, intent_id: targetIntentId };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Execute the governed run
|
|
379
|
+
session.current_run_id = preparedIntent.runId;
|
|
380
|
+
session.current_vision_objective = visionObjective || preparedIntent.intent?.charter || null;
|
|
381
|
+
session.status = 'running';
|
|
382
|
+
writeContinuousSession(root, session);
|
|
383
|
+
|
|
384
|
+
const execution = await executeGovernedRun(context, {
|
|
385
|
+
autoApprove: true,
|
|
386
|
+
report: true,
|
|
387
|
+
log,
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
session.runs_completed += 1;
|
|
391
|
+
session.current_run_id = execution.result?.state?.run_id || null;
|
|
392
|
+
|
|
393
|
+
const stopReason = execution.result?.stop_reason;
|
|
394
|
+
log(`Run ${session.runs_completed}/${contOpts.maxRuns} completed: ${stopReason || 'unknown'}`);
|
|
395
|
+
|
|
396
|
+
// Resolve the consumed intent
|
|
397
|
+
const resolved = resolveIntent(root, targetIntentId);
|
|
398
|
+
if (!resolved.ok) {
|
|
399
|
+
log(`Continuous resolve error: ${resolved.error}`);
|
|
400
|
+
session.status = 'failed';
|
|
401
|
+
writeContinuousSession(root, session);
|
|
402
|
+
return { ok: false, status: 'failed', action: 'resolve_failed', stop_reason: resolved.error, intent_id: targetIntentId };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (stopReason === 'blocked') {
|
|
406
|
+
session.status = 'paused';
|
|
407
|
+
log('Run blocked — continuous loop paused. Use `agentxchain unblock <id>` to resume.');
|
|
408
|
+
writeContinuousSession(root, session);
|
|
409
|
+
return { ok: true, status: 'blocked', action: 'run_blocked', run_id: session.current_run_id, intent_id: targetIntentId };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (stopReason === 'priority_preempted') {
|
|
413
|
+
log('Priority preemption detected — consuming injected work next cycle.');
|
|
414
|
+
writeContinuousSession(root, session);
|
|
415
|
+
return { ok: true, status: 'running', action: 'consumed_injected_priority', run_id: session.current_run_id, intent_id: targetIntentId };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
writeContinuousSession(root, session);
|
|
419
|
+
return {
|
|
420
|
+
ok: true,
|
|
421
|
+
status: 'running',
|
|
422
|
+
action: visionObjective ? 'seeded_from_vision' : 'started_run',
|
|
423
|
+
run_id: session.current_run_id,
|
|
424
|
+
intent_id: targetIntentId,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// ---------------------------------------------------------------------------
|
|
429
|
+
// Main continuous loop (CLI-owned, built on advanceContinuousRunOnce)
|
|
282
430
|
// ---------------------------------------------------------------------------
|
|
283
431
|
|
|
284
432
|
/**
|
|
@@ -293,7 +441,6 @@ export function resolveContinuousOptions(opts, config) {
|
|
|
293
441
|
export async function executeContinuousRun(context, contOpts, executeGovernedRun, log = console.log) {
|
|
294
442
|
const { root } = context;
|
|
295
443
|
const absVisionPath = resolveVisionPath(root, contOpts.visionPath);
|
|
296
|
-
let exitCode = 0;
|
|
297
444
|
|
|
298
445
|
// Validate vision file exists
|
|
299
446
|
if (!existsSync(absVisionPath)) {
|
|
@@ -315,122 +462,26 @@ export async function executeContinuousRun(context, contOpts, executeGovernedRun
|
|
|
315
462
|
|
|
316
463
|
try {
|
|
317
464
|
while (!stopping) {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
if (session.idle_cycles >= contOpts.maxIdleCycles) {
|
|
327
|
-
session.status = 'completed';
|
|
328
|
-
log(`All vision goals appear addressed (${contOpts.maxIdleCycles} consecutive idle cycles). Stopping.`);
|
|
329
|
-
break;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// Step 1: Check intake queue for pending work
|
|
333
|
-
const queued = findNextQueuedIntent(root);
|
|
334
|
-
let targetIntentId = null;
|
|
335
|
-
let visionObjective = null;
|
|
336
|
-
let preparedIntent = null;
|
|
337
|
-
|
|
338
|
-
if (queued.ok) {
|
|
339
|
-
targetIntentId = queued.intentId;
|
|
340
|
-
session.idle_cycles = 0;
|
|
341
|
-
log(`Found queued intent: ${queued.intentId} (${queued.status})`);
|
|
342
|
-
} else {
|
|
343
|
-
// Step 2: Derive from vision
|
|
344
|
-
const seeded = seedFromVision(root, absVisionPath, {
|
|
345
|
-
triageApproval: contOpts.triageApproval,
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
if (!seeded.ok) {
|
|
349
|
-
log(`Vision scan error: ${seeded.error}`);
|
|
350
|
-
session.status = 'stopped';
|
|
351
|
-
exitCode = 1;
|
|
352
|
-
break;
|
|
465
|
+
const step = await advanceContinuousRunOnce(context, session, contOpts, executeGovernedRun, log);
|
|
466
|
+
|
|
467
|
+
// Terminal states
|
|
468
|
+
if (step.status === 'completed' || step.status === 'idle_exit' || step.status === 'failed' || step.status === 'blocked') {
|
|
469
|
+
if (step.status === 'completed') {
|
|
470
|
+
log(`Max runs reached (${contOpts.maxRuns}). Stopping.`);
|
|
471
|
+
} else if (step.status === 'idle_exit') {
|
|
472
|
+
log(`All vision goals appear addressed (${contOpts.maxIdleCycles} consecutive idle cycles). Stopping.`);
|
|
353
473
|
}
|
|
354
|
-
|
|
355
|
-
if (seeded.idle) {
|
|
356
|
-
session.idle_cycles += 1;
|
|
357
|
-
log(`Idle cycle ${session.idle_cycles}/${contOpts.maxIdleCycles} — no derivable work from vision.`);
|
|
358
|
-
writeContinuousSession(root, session);
|
|
359
|
-
if (session.idle_cycles >= contOpts.maxIdleCycles) continue;
|
|
360
|
-
await new Promise(r => setTimeout(r, contOpts.pollSeconds * 1000));
|
|
361
|
-
continue;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// If triage_approval is "human", the intent is in "triaged" state — don't auto-start
|
|
365
|
-
if (contOpts.triageApproval === 'human') {
|
|
366
|
-
log(`Vision-derived intent ${seeded.intentId} left in triaged state (triage_approval: human).`);
|
|
367
|
-
session.idle_cycles += 1;
|
|
368
|
-
writeContinuousSession(root, session);
|
|
369
|
-
await new Promise(r => setTimeout(r, contOpts.pollSeconds * 1000));
|
|
370
|
-
continue;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
targetIntentId = seeded.intentId;
|
|
374
|
-
visionObjective = `${seeded.section}: ${seeded.goal}`;
|
|
375
|
-
session.idle_cycles = 0;
|
|
376
|
-
log(`Vision-derived: ${visionObjective}`);
|
|
474
|
+
return { exitCode: step.ok ? 0 : 1, session };
|
|
377
475
|
}
|
|
378
476
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
exitCode = 1;
|
|
388
|
-
break;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// Step 3: Execute the prepared governed run.
|
|
392
|
-
session.current_run_id = preparedIntent.runId;
|
|
393
|
-
session.current_vision_objective = visionObjective || preparedIntent.intent?.charter || null;
|
|
394
|
-
session.status = 'running';
|
|
395
|
-
writeContinuousSession(root, session);
|
|
396
|
-
|
|
397
|
-
const execution = await executeGovernedRun(context, {
|
|
398
|
-
autoApprove: true,
|
|
399
|
-
report: true,
|
|
400
|
-
log,
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
session.runs_completed += 1;
|
|
404
|
-
session.current_run_id = execution.result?.state?.run_id || null;
|
|
405
|
-
|
|
406
|
-
const stopReason = execution.result?.stop_reason;
|
|
407
|
-
log(`Run ${session.runs_completed}/${contOpts.maxRuns} completed: ${stopReason || 'unknown'}`);
|
|
408
|
-
|
|
409
|
-
const resolved = resolveIntent(root, targetIntentId);
|
|
410
|
-
if (!resolved.ok) {
|
|
411
|
-
log(`Continuous resolve error: ${resolved.error}`);
|
|
412
|
-
session.status = 'stopped';
|
|
413
|
-
writeContinuousSession(root, session);
|
|
414
|
-
return { exitCode: 1, session };
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
if (stopReason === 'blocked') {
|
|
418
|
-
session.status = 'paused';
|
|
419
|
-
log('Run blocked — continuous loop paused. Use `agentxchain unblock <id>` to resume.');
|
|
420
|
-
writeContinuousSession(root, session);
|
|
421
|
-
break;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
if (stopReason === 'priority_preempted') {
|
|
425
|
-
log('Priority preemption detected — consuming injected work next cycle.');
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
writeContinuousSession(root, session);
|
|
429
|
-
|
|
430
|
-
// Brief cooldown between runs
|
|
431
|
-
const cooldownMs = (contOpts.cooldownSeconds ?? 5) * 1000;
|
|
432
|
-
if (!stopping && session.runs_completed < contOpts.maxRuns && cooldownMs > 0) {
|
|
433
|
-
await new Promise(r => setTimeout(r, cooldownMs));
|
|
477
|
+
// Non-terminal: sleep before next step
|
|
478
|
+
if (!stopping) {
|
|
479
|
+
const sleepMs = step.action === 'no_work_found' || step.action === 'waited_for_human'
|
|
480
|
+
? contOpts.pollSeconds * 1000
|
|
481
|
+
: (contOpts.cooldownSeconds ?? 5) * 1000;
|
|
482
|
+
if (sleepMs > 0) {
|
|
483
|
+
await new Promise(r => setTimeout(r, sleepMs));
|
|
484
|
+
}
|
|
434
485
|
}
|
|
435
486
|
}
|
|
436
487
|
|
|
@@ -440,7 +491,7 @@ export async function executeContinuousRun(context, contOpts, executeGovernedRun
|
|
|
440
491
|
}
|
|
441
492
|
|
|
442
493
|
writeContinuousSession(root, session);
|
|
443
|
-
return { exitCode, session };
|
|
494
|
+
return { exitCode: 0, session };
|
|
444
495
|
|
|
445
496
|
} finally {
|
|
446
497
|
process.removeListener('SIGINT', sigHandler);
|
|
@@ -689,6 +689,30 @@ export function validateSchedulesConfig(schedules, roles) {
|
|
|
689
689
|
errors.push(`Schedule "${scheduleId}": initial_role "${schedule.initial_role}" is not a defined role`);
|
|
690
690
|
}
|
|
691
691
|
}
|
|
692
|
+
|
|
693
|
+
// Continuous mode validation
|
|
694
|
+
if ('continuous' in schedule && schedule.continuous != null) {
|
|
695
|
+
const cont = schedule.continuous;
|
|
696
|
+
if (typeof cont !== 'object' || Array.isArray(cont)) {
|
|
697
|
+
errors.push(`Schedule "${scheduleId}": continuous must be an object`);
|
|
698
|
+
} else {
|
|
699
|
+
if ('enabled' in cont && typeof cont.enabled !== 'boolean') {
|
|
700
|
+
errors.push(`Schedule "${scheduleId}": continuous.enabled must be a boolean`);
|
|
701
|
+
}
|
|
702
|
+
if (cont.enabled === true && (!cont.vision_path || typeof cont.vision_path !== 'string' || !cont.vision_path.trim())) {
|
|
703
|
+
errors.push(`Schedule "${scheduleId}": continuous.vision_path is required when continuous.enabled is true`);
|
|
704
|
+
}
|
|
705
|
+
if ('max_runs' in cont && (!Number.isInteger(cont.max_runs) || cont.max_runs < 1)) {
|
|
706
|
+
errors.push(`Schedule "${scheduleId}": continuous.max_runs must be an integer >= 1`);
|
|
707
|
+
}
|
|
708
|
+
if ('max_idle_cycles' in cont && (!Number.isInteger(cont.max_idle_cycles) || cont.max_idle_cycles < 1)) {
|
|
709
|
+
errors.push(`Schedule "${scheduleId}": continuous.max_idle_cycles must be an integer >= 1`);
|
|
710
|
+
}
|
|
711
|
+
if ('triage_approval' in cont && cont.triage_approval !== 'auto' && cont.triage_approval !== 'human') {
|
|
712
|
+
errors.push(`Schedule "${scheduleId}": continuous.triage_approval must be "auto" or "human"`);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
692
716
|
}
|
|
693
717
|
|
|
694
718
|
return { ok: errors.length === 0, errors };
|
|
@@ -1120,6 +1144,18 @@ export function normalizeV4(raw) {
|
|
|
1120
1144
|
};
|
|
1121
1145
|
}
|
|
1122
1146
|
|
|
1147
|
+
function normalizeContinuousConfig(raw) {
|
|
1148
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
1149
|
+
if (raw.enabled !== true) return null;
|
|
1150
|
+
return {
|
|
1151
|
+
enabled: true,
|
|
1152
|
+
vision_path: raw.vision_path || '.planning/VISION.md',
|
|
1153
|
+
max_runs: Number.isInteger(raw.max_runs) && raw.max_runs >= 1 ? raw.max_runs : 50,
|
|
1154
|
+
max_idle_cycles: Number.isInteger(raw.max_idle_cycles) && raw.max_idle_cycles >= 1 ? raw.max_idle_cycles : 5,
|
|
1155
|
+
triage_approval: raw.triage_approval === 'human' ? 'human' : 'auto',
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1123
1159
|
function normalizeSchedules(rawSchedules) {
|
|
1124
1160
|
if (!rawSchedules || typeof rawSchedules !== 'object' || Array.isArray(rawSchedules)) {
|
|
1125
1161
|
return {};
|
|
@@ -1135,6 +1171,7 @@ function normalizeSchedules(rawSchedules) {
|
|
|
1135
1171
|
max_turns: schedule?.max_turns ?? 50,
|
|
1136
1172
|
initial_role: schedule?.initial_role || null,
|
|
1137
1173
|
trigger_reason: schedule?.trigger_reason?.trim() || `schedule:${scheduleId}`,
|
|
1174
|
+
continuous: normalizeContinuousConfig(schedule?.continuous),
|
|
1138
1175
|
},
|
|
1139
1176
|
]),
|
|
1140
1177
|
);
|
package/src/lib/run-schedule.js
CHANGED
|
@@ -27,6 +27,7 @@ function normalizeScheduleStateRecord(value) {
|
|
|
27
27
|
last_status: null,
|
|
28
28
|
last_skip_at: null,
|
|
29
29
|
last_skip_reason: null,
|
|
30
|
+
last_continuous_session_id: null,
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
33
|
|
|
@@ -37,6 +38,7 @@ function normalizeScheduleStateRecord(value) {
|
|
|
37
38
|
last_status: typeof value.last_status === 'string' ? value.last_status : null,
|
|
38
39
|
last_skip_at: typeof value.last_skip_at === 'string' ? value.last_skip_at : null,
|
|
39
40
|
last_skip_reason: typeof value.last_skip_reason === 'string' ? value.last_skip_reason : null,
|
|
41
|
+
last_continuous_session_id: typeof value.last_continuous_session_id === 'string' ? value.last_continuous_session_id : null,
|
|
40
42
|
};
|
|
41
43
|
}
|
|
42
44
|
|