agentxchain 2.116.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/README.md +1 -1
- package/bin/agentxchain.js +24 -0
- package/package.json +1 -1
- package/scripts/render-github-release-body.mjs +1 -1
- package/src/commands/events.js +8 -1
- package/src/commands/inject.js +81 -0
- package/src/commands/resume.js +6 -4
- package/src/commands/run.js +13 -0
- package/src/commands/schedule.js +386 -19
- package/src/commands/status.js +55 -0
- package/src/commands/unblock.js +67 -0
- package/src/lib/continuous-run.js +499 -0
- package/src/lib/governed-state.js +37 -1
- package/src/lib/human-escalations.js +434 -0
- package/src/lib/intake.js +243 -11
- package/src/lib/normalized-config.js +37 -0
- package/src/lib/notification-runner.js +3 -1
- package/src/lib/run-events.js +2 -0
- package/src/lib/run-loop.js +17 -0
- package/src/lib/run-provenance.js +4 -0
- package/src/lib/run-schedule.js +45 -0
- package/src/lib/vision-reader.js +229 -0
package/src/lib/intake.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync, readdirSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, mkdirSync, writeFileSync, unlinkSync } from 'node:fs';
|
|
2
2
|
import { join, basename, resolve as pathResolve } from 'node:path';
|
|
3
3
|
import { createHash, randomBytes } from 'node:crypto';
|
|
4
4
|
import { safeWriteJson } from './safe-write.js';
|
|
@@ -18,7 +18,7 @@ import { loadCoordinatorConfig } from './coordinator-config.js';
|
|
|
18
18
|
import { loadCoordinatorState, readBarriers } from './coordinator-state.js';
|
|
19
19
|
import { writeCoordinatorHandoff } from './intake-handoff.js';
|
|
20
20
|
|
|
21
|
-
const VALID_SOURCES = ['manual', 'ci_failure', 'git_ref_change', 'schedule'];
|
|
21
|
+
const VALID_SOURCES = ['manual', 'ci_failure', 'git_ref_change', 'schedule', 'vision_scan'];
|
|
22
22
|
const VALID_PRIORITIES = ['p0', 'p1', 'p2', 'p3'];
|
|
23
23
|
const EVENT_ID_RE = /^evt_\d+_[0-9a-f]{4}$/;
|
|
24
24
|
const INTENT_ID_RE = /^intent_\d+_[0-9a-f]{4}$/;
|
|
@@ -680,6 +680,18 @@ export function startIntent(root, intentId, options = {}) {
|
|
|
680
680
|
return { ok: false, error: 'Failed to parse governed state.json', exitCode: 2 };
|
|
681
681
|
}
|
|
682
682
|
|
|
683
|
+
const allowCompletedRestart = options.allowTerminalRestart === true
|
|
684
|
+
&& state.status === 'completed'
|
|
685
|
+
&& getActiveTurnCount(state) === 0;
|
|
686
|
+
|
|
687
|
+
const startProvenance = {
|
|
688
|
+
trigger: 'intake',
|
|
689
|
+
intake_intent_id: intent.intent_id,
|
|
690
|
+
trigger_reason: intent.charter || null,
|
|
691
|
+
created_by: 'operator',
|
|
692
|
+
...(options.provenance && typeof options.provenance === 'object' ? options.provenance : {}),
|
|
693
|
+
};
|
|
694
|
+
|
|
683
695
|
// Check busy-run conditions
|
|
684
696
|
const activeTurns = getActiveTurns(state);
|
|
685
697
|
const activeCount = getActiveTurnCount(state);
|
|
@@ -698,7 +710,7 @@ export function startIntent(root, intentId, options = {}) {
|
|
|
698
710
|
return { ok: false, error: `cannot start: run is blocked (${reason})`, exitCode: 1 };
|
|
699
711
|
}
|
|
700
712
|
|
|
701
|
-
if (state.status === 'completed') {
|
|
713
|
+
if (state.status === 'completed' && !allowCompletedRestart) {
|
|
702
714
|
return {
|
|
703
715
|
ok: false,
|
|
704
716
|
error: 'cannot start: governed run is already completed. S3 does not reopen completed runs.',
|
|
@@ -719,14 +731,10 @@ export function startIntent(root, intentId, options = {}) {
|
|
|
719
731
|
}
|
|
720
732
|
|
|
721
733
|
// Bootstrap: idle with no run → initialize
|
|
722
|
-
if (state.status === 'idle' && !state.run_id) {
|
|
734
|
+
if ((state.status === 'idle' && !state.run_id) || allowCompletedRestart) {
|
|
723
735
|
const initResult = initializeGovernedRun(root, config, {
|
|
724
|
-
provenance:
|
|
725
|
-
|
|
726
|
-
intake_intent_id: intent.intent_id,
|
|
727
|
-
trigger_reason: intent.charter || null,
|
|
728
|
-
created_by: 'operator',
|
|
729
|
-
},
|
|
736
|
+
provenance: startProvenance,
|
|
737
|
+
allow_terminal_restart: allowCompletedRestart,
|
|
730
738
|
});
|
|
731
739
|
if (!initResult.ok) {
|
|
732
740
|
return { ok: false, error: `run initialization failed: ${initResult.error}`, exitCode: 1 };
|
|
@@ -1370,8 +1378,232 @@ export function scanSource(root, source, snapshot) {
|
|
|
1370
1378
|
};
|
|
1371
1379
|
}
|
|
1372
1380
|
|
|
1381
|
+
// ---------------------------------------------------------------------------
|
|
1382
|
+
// Inject — composed record + triage + approve in one operation
|
|
1383
|
+
// ---------------------------------------------------------------------------
|
|
1384
|
+
|
|
1385
|
+
const PREEMPTION_MARKER_PATH = '.agentxchain/intake/injected-priority.json';
|
|
1386
|
+
|
|
1387
|
+
function preemptionMarkerPath(root) {
|
|
1388
|
+
return join(root, PREEMPTION_MARKER_PATH);
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
export function readPreemptionMarker(root) {
|
|
1392
|
+
const p = preemptionMarkerPath(root);
|
|
1393
|
+
if (!existsSync(p)) return null;
|
|
1394
|
+
try {
|
|
1395
|
+
return JSON.parse(readFileSync(p, 'utf8'));
|
|
1396
|
+
} catch {
|
|
1397
|
+
return null;
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
export function clearPreemptionMarker(root) {
|
|
1402
|
+
const p = preemptionMarkerPath(root);
|
|
1403
|
+
if (existsSync(p)) {
|
|
1404
|
+
try {
|
|
1405
|
+
unlinkSync(p);
|
|
1406
|
+
} catch {
|
|
1407
|
+
// best-effort
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
export function consumePreemptionMarker(root, options = {}) {
|
|
1413
|
+
const marker = readPreemptionMarker(root);
|
|
1414
|
+
if (!marker?.intent_id) {
|
|
1415
|
+
return { ok: false, error: 'no preemption marker found', exitCode: 1 };
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
const loadedIntent = readIntent(root, marker.intent_id);
|
|
1419
|
+
if (!loadedIntent.ok) {
|
|
1420
|
+
return {
|
|
1421
|
+
ok: false,
|
|
1422
|
+
error: `preemption marker references missing intent ${marker.intent_id}`,
|
|
1423
|
+
marker,
|
|
1424
|
+
exitCode: 1,
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
const startingStatus = loadedIntent.intent.status;
|
|
1429
|
+
let intent = loadedIntent.intent;
|
|
1430
|
+
let planned = false;
|
|
1431
|
+
let started = false;
|
|
1432
|
+
|
|
1433
|
+
if (intent.status === 'approved') {
|
|
1434
|
+
const plannedResult = planIntent(root, intent.intent_id, {
|
|
1435
|
+
projectName: options.projectName,
|
|
1436
|
+
force: options.forcePlan === true,
|
|
1437
|
+
});
|
|
1438
|
+
if (!plannedResult.ok) {
|
|
1439
|
+
return {
|
|
1440
|
+
ok: false,
|
|
1441
|
+
error: `failed to plan injected intent ${intent.intent_id}: ${plannedResult.error}`,
|
|
1442
|
+
marker,
|
|
1443
|
+
intent_status: intent.status,
|
|
1444
|
+
exitCode: plannedResult.exitCode || 1,
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
intent = plannedResult.intent;
|
|
1448
|
+
planned = true;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
if (intent.status === 'planned') {
|
|
1452
|
+
const startResult = startIntent(root, intent.intent_id, {
|
|
1453
|
+
role: options.role || undefined,
|
|
1454
|
+
});
|
|
1455
|
+
if (!startResult.ok) {
|
|
1456
|
+
return {
|
|
1457
|
+
ok: false,
|
|
1458
|
+
error: `failed to start injected intent ${intent.intent_id}: ${startResult.error}`,
|
|
1459
|
+
marker,
|
|
1460
|
+
intent_status: intent.status,
|
|
1461
|
+
exitCode: startResult.exitCode || 1,
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
clearPreemptionMarker(root);
|
|
1465
|
+
return {
|
|
1466
|
+
ok: true,
|
|
1467
|
+
marker,
|
|
1468
|
+
intent_id: intent.intent_id,
|
|
1469
|
+
starting_status: startingStatus,
|
|
1470
|
+
final_status: startResult.intent.status,
|
|
1471
|
+
planned,
|
|
1472
|
+
started: true,
|
|
1473
|
+
run_id: startResult.run_id,
|
|
1474
|
+
turn_id: startResult.turn_id,
|
|
1475
|
+
role: startResult.role,
|
|
1476
|
+
intent: startResult.intent,
|
|
1477
|
+
exitCode: 0,
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
if (intent.status === 'executing') {
|
|
1482
|
+
clearPreemptionMarker(root);
|
|
1483
|
+
return {
|
|
1484
|
+
ok: true,
|
|
1485
|
+
marker,
|
|
1486
|
+
intent_id: intent.intent_id,
|
|
1487
|
+
starting_status: startingStatus,
|
|
1488
|
+
final_status: intent.status,
|
|
1489
|
+
planned,
|
|
1490
|
+
started: false,
|
|
1491
|
+
run_id: intent.target_run || null,
|
|
1492
|
+
turn_id: intent.target_turn || null,
|
|
1493
|
+
role: options.role || null,
|
|
1494
|
+
intent,
|
|
1495
|
+
exitCode: 0,
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
return {
|
|
1500
|
+
ok: false,
|
|
1501
|
+
error: `cannot consume preemption marker from intent status "${intent.status}"`,
|
|
1502
|
+
marker,
|
|
1503
|
+
intent_id: intent.intent_id,
|
|
1504
|
+
intent_status: intent.status,
|
|
1505
|
+
exitCode: 1,
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
export function injectIntent(root, description, options = {}) {
|
|
1510
|
+
if (!description || typeof description !== 'string' || !description.trim()) {
|
|
1511
|
+
return { ok: false, error: 'description is required', exitCode: 1 };
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
const priority = options.priority || 'p0';
|
|
1515
|
+
if (!VALID_PRIORITIES.includes(priority)) {
|
|
1516
|
+
return { ok: false, error: `priority must be one of: ${VALID_PRIORITIES.join(', ')}`, exitCode: 1 };
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
const template = options.template || 'generic';
|
|
1520
|
+
if (!VALID_GOVERNED_TEMPLATE_IDS.includes(template)) {
|
|
1521
|
+
return { ok: false, error: `template must be one of: ${VALID_GOVERNED_TEMPLATE_IDS.join(', ')}`, exitCode: 1 };
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
const charter = options.charter || description.trim();
|
|
1525
|
+
const acceptance_contract = options.acceptance
|
|
1526
|
+
? options.acceptance.split(',').map(s => s.trim()).filter(Boolean)
|
|
1527
|
+
: [description.trim()];
|
|
1528
|
+
const approver = options.approver || 'human';
|
|
1529
|
+
const noApprove = options.noApprove === true;
|
|
1530
|
+
|
|
1531
|
+
// Step 1: Record event
|
|
1532
|
+
const recordResult = recordEvent(root, {
|
|
1533
|
+
source: 'manual',
|
|
1534
|
+
category: 'operator_injection',
|
|
1535
|
+
signal: { description: description.trim(), injected: true, priority },
|
|
1536
|
+
evidence: [{ type: 'text', value: description.trim() }],
|
|
1537
|
+
});
|
|
1538
|
+
|
|
1539
|
+
if (!recordResult.ok) {
|
|
1540
|
+
return recordResult;
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
// If deduplicated, return existing intent
|
|
1544
|
+
if (recordResult.deduplicated) {
|
|
1545
|
+
return {
|
|
1546
|
+
ok: true,
|
|
1547
|
+
intent: recordResult.intent,
|
|
1548
|
+
event: recordResult.event,
|
|
1549
|
+
deduplicated: true,
|
|
1550
|
+
preemption_marker: false,
|
|
1551
|
+
exitCode: 0,
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
const intentId = recordResult.intent.intent_id;
|
|
1556
|
+
|
|
1557
|
+
// Step 2: Triage
|
|
1558
|
+
const triageResult = triageIntent(root, intentId, {
|
|
1559
|
+
priority,
|
|
1560
|
+
template,
|
|
1561
|
+
charter,
|
|
1562
|
+
acceptance_contract,
|
|
1563
|
+
});
|
|
1564
|
+
|
|
1565
|
+
if (!triageResult.ok) {
|
|
1566
|
+
return { ok: false, error: `triage failed: ${triageResult.error}`, exitCode: 1 };
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
// Step 3: Approve (unless --no-approve)
|
|
1570
|
+
if (!noApprove) {
|
|
1571
|
+
const approveResult = approveIntent(root, intentId, { approver, reason: 'operator injection' });
|
|
1572
|
+
if (!approveResult.ok) {
|
|
1573
|
+
return { ok: false, error: `approve failed: ${approveResult.error}`, exitCode: 1 };
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// Step 4: Write preemption marker for p0
|
|
1578
|
+
let preemptionMarker = false;
|
|
1579
|
+
if (priority === 'p0' && !noApprove) {
|
|
1580
|
+
const marker = {
|
|
1581
|
+
intent_id: intentId,
|
|
1582
|
+
priority,
|
|
1583
|
+
description: description.trim(),
|
|
1584
|
+
injected_at: nowISO(),
|
|
1585
|
+
};
|
|
1586
|
+
ensureIntakeDirs(root);
|
|
1587
|
+
safeWriteJson(preemptionMarkerPath(root), marker);
|
|
1588
|
+
preemptionMarker = true;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
// Re-read final intent state
|
|
1592
|
+
const finalRead = readIntent(root, intentId);
|
|
1593
|
+
const finalIntent = finalRead.ok ? finalRead.intent : triageResult.intent;
|
|
1594
|
+
|
|
1595
|
+
return {
|
|
1596
|
+
ok: true,
|
|
1597
|
+
intent: finalIntent,
|
|
1598
|
+
event: recordResult.event,
|
|
1599
|
+
deduplicated: false,
|
|
1600
|
+
preemption_marker: preemptionMarker,
|
|
1601
|
+
exitCode: 0,
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1373
1605
|
// ---------------------------------------------------------------------------
|
|
1374
1606
|
// Exports for testing
|
|
1375
1607
|
// ---------------------------------------------------------------------------
|
|
1376
1608
|
|
|
1377
|
-
export { VALID_SOURCES, VALID_PRIORITIES, VALID_TRANSITIONS, S1_STATES, TERMINAL_STATES, SCAN_SOURCES };
|
|
1609
|
+
export { VALID_SOURCES, VALID_PRIORITIES, VALID_TRANSITIONS, S1_STATES, TERMINAL_STATES, SCAN_SOURCES, PREEMPTION_MARKER_PATH };
|
|
@@ -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
|
);
|
|
@@ -10,6 +10,8 @@ export const VALID_NOTIFICATION_EVENTS = [
|
|
|
10
10
|
'run_blocked',
|
|
11
11
|
'operator_escalation_raised',
|
|
12
12
|
'escalation_resolved',
|
|
13
|
+
'human_escalation_raised',
|
|
14
|
+
'human_escalation_resolved',
|
|
13
15
|
'phase_transition_pending',
|
|
14
16
|
'run_completion_pending',
|
|
15
17
|
'run_completed',
|
|
@@ -170,7 +172,7 @@ export function validateNotificationsConfig(notifications) {
|
|
|
170
172
|
return { ok: false, errors, warnings: [] };
|
|
171
173
|
}
|
|
172
174
|
|
|
173
|
-
const allowedKeys = new Set(['webhooks', 'approval_sla']);
|
|
175
|
+
const allowedKeys = new Set(['webhooks', 'approval_sla', 'local']);
|
|
174
176
|
for (const key of Object.keys(notifications)) {
|
|
175
177
|
if (!allowedKeys.has(key)) {
|
|
176
178
|
errors.push(`notifications contains unknown field "${key}"`);
|
package/src/lib/run-events.js
CHANGED
package/src/lib/run-loop.js
CHANGED
|
@@ -36,6 +36,7 @@ import { runAdmissionControl } from './admission-control.js';
|
|
|
36
36
|
import { mkdirSync, writeFileSync } from 'fs';
|
|
37
37
|
import { join, dirname } from 'path';
|
|
38
38
|
import { evaluateApprovalSlaReminders } from './notification-runner.js';
|
|
39
|
+
import { readPreemptionMarker } from './intake.js';
|
|
39
40
|
|
|
40
41
|
const DEFAULT_MAX_TURNS = 50;
|
|
41
42
|
|
|
@@ -128,6 +129,22 @@ export async function runLoop(root, config, callbacks, options = {}) {
|
|
|
128
129
|
return makeResult(false, 'max_turns_reached', state, turnsExecuted, turnHistory, gatesApproved, errors);
|
|
129
130
|
}
|
|
130
131
|
|
|
132
|
+
// ── Priority preemption check ────────────────────────────────────────
|
|
133
|
+
// If a p0 intent was injected via `agentxchain inject`, yield the run
|
|
134
|
+
// so the scheduler/continuous loop can pick up the injected work.
|
|
135
|
+
// Only preempt when no turns are currently active (avoid mid-dispatch
|
|
136
|
+
// interruption).
|
|
137
|
+
const activeTurnCount = getActiveTurnCount(state);
|
|
138
|
+
if (activeTurnCount === 0) {
|
|
139
|
+
const marker = readPreemptionMarker(root);
|
|
140
|
+
if (marker && marker.priority === 'p0') {
|
|
141
|
+
emit({ type: 'priority_injected', intent_id: marker.intent_id, priority: marker.priority });
|
|
142
|
+
const result = makeResult(false, 'priority_preempted', state, turnsExecuted, turnHistory, gatesApproved, errors);
|
|
143
|
+
result.preempted_by = marker.intent_id;
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
131
148
|
// ── Determine concurrency mode ────────────────────────────────────────
|
|
132
149
|
const maxConcurrent = getMaxConcurrentTurns(config, state.phase);
|
|
133
150
|
|
|
@@ -3,12 +3,14 @@ const VALID_TRIGGERS = new Set([
|
|
|
3
3
|
'continuation',
|
|
4
4
|
'recovery',
|
|
5
5
|
'intake',
|
|
6
|
+
'vision_scan',
|
|
6
7
|
'schedule',
|
|
7
8
|
'coordinator',
|
|
8
9
|
]);
|
|
9
10
|
|
|
10
11
|
const VALID_CREATORS = new Set([
|
|
11
12
|
'operator',
|
|
13
|
+
'continuous_loop',
|
|
12
14
|
'coordinator',
|
|
13
15
|
]);
|
|
14
16
|
|
|
@@ -71,6 +73,8 @@ export function summarizeRunProvenance(provenance) {
|
|
|
71
73
|
: normalized.trigger;
|
|
72
74
|
const creatorSuffix = normalized.created_by === 'coordinator'
|
|
73
75
|
? ' (created by coordinator)'
|
|
76
|
+
: normalized.created_by === 'continuous_loop'
|
|
77
|
+
? ' (created by continuous loop)'
|
|
74
78
|
: '';
|
|
75
79
|
const reasonSuffix = normalized.trigger_reason
|
|
76
80
|
? ` ("${normalized.trigger_reason}")`
|
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
|
|
|
@@ -161,6 +163,49 @@ export function evaluateScheduleLaunchEligibility(root, config) {
|
|
|
161
163
|
return { ok: false, status, reason: `run_${status}` };
|
|
162
164
|
}
|
|
163
165
|
|
|
166
|
+
function resolveScheduleTriggerReason(scheduleId, schedule) {
|
|
167
|
+
return schedule?.trigger_reason || `schedule:${scheduleId}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function findContinuableScheduleRun(root, config, { scheduleId = null } = {}) {
|
|
171
|
+
const projectState = loadProjectState(root, config);
|
|
172
|
+
if (!projectState || projectState.status !== 'active' || !projectState.run_id) {
|
|
173
|
+
return { ok: false, reason: 'not_active' };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (projectState.provenance?.trigger !== 'schedule') {
|
|
177
|
+
return { ok: false, reason: 'not_schedule_run' };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const scheduleState = readScheduleState(root, config);
|
|
181
|
+
const matches = Object.entries(config?.schedules || {}).filter(([candidateId, schedule]) => {
|
|
182
|
+
if (scheduleId && candidateId !== scheduleId) return false;
|
|
183
|
+
|
|
184
|
+
const record = scheduleState.schedules[candidateId];
|
|
185
|
+
if (record?.last_run_id === projectState.run_id) {
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return projectState.provenance?.trigger_reason === resolveScheduleTriggerReason(candidateId, schedule);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
if (matches.length === 0) {
|
|
193
|
+
return { ok: false, reason: 'no_matching_schedule' };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (matches.length > 1) {
|
|
197
|
+
return { ok: false, reason: 'ambiguous_schedule_run' };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const [matchedId, matchedSchedule] = matches[0];
|
|
201
|
+
return {
|
|
202
|
+
ok: true,
|
|
203
|
+
schedule_id: matchedId,
|
|
204
|
+
schedule: matchedSchedule,
|
|
205
|
+
state: projectState,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
164
209
|
// ── Daemon Health State ─────────────────────────────────────────────────────
|
|
165
210
|
|
|
166
211
|
export function readDaemonState(root) {
|