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/commands/schedule.js
CHANGED
|
@@ -6,13 +6,24 @@ import {
|
|
|
6
6
|
listSchedules,
|
|
7
7
|
updateScheduleState,
|
|
8
8
|
evaluateScheduleLaunchEligibility,
|
|
9
|
+
findContinuableScheduleRun,
|
|
9
10
|
readDaemonState,
|
|
10
11
|
writeDaemonState,
|
|
11
12
|
updateDaemonHeartbeat,
|
|
12
13
|
createDaemonState,
|
|
13
14
|
evaluateDaemonStatus,
|
|
14
15
|
} from '../lib/run-schedule.js';
|
|
16
|
+
import { consumePreemptionMarker } from '../lib/intake.js';
|
|
15
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';
|
|
16
27
|
|
|
17
28
|
function loadScheduleContext() {
|
|
18
29
|
const context = loadProjectContext();
|
|
@@ -86,7 +97,136 @@ function buildScheduleProvenance(entry) {
|
|
|
86
97
|
};
|
|
87
98
|
}
|
|
88
99
|
|
|
100
|
+
function buildScheduleExecutionResult(entryId, execution, fallbackState, action = 'ran') {
|
|
101
|
+
const state = execution.result?.state || fallbackState || null;
|
|
102
|
+
return {
|
|
103
|
+
id: entryId,
|
|
104
|
+
action,
|
|
105
|
+
run_id: state?.run_id || null,
|
|
106
|
+
stop_reason: execution.result?.stop_reason || null,
|
|
107
|
+
exit_code: execution.exitCode,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function recordScheduleExecution(context, entryId, execution, fallbackState, nowIso, action = 'ran') {
|
|
112
|
+
const state = execution.result?.state || fallbackState || null;
|
|
113
|
+
const runId = state?.run_id || null;
|
|
114
|
+
const startedAt = state?.created_at || nowIso;
|
|
115
|
+
|
|
116
|
+
updateScheduleState(context.root, context.config, entryId, (record) => ({
|
|
117
|
+
...record,
|
|
118
|
+
last_started_at: startedAt,
|
|
119
|
+
last_finished_at: new Date().toISOString(),
|
|
120
|
+
last_run_id: runId,
|
|
121
|
+
last_status: execution.result?.stop_reason || (execution.exitCode === 0 ? 'completed' : 'launch_failed'),
|
|
122
|
+
last_skip_at: null,
|
|
123
|
+
last_skip_reason: null,
|
|
124
|
+
}));
|
|
125
|
+
|
|
126
|
+
return buildScheduleExecutionResult(entryId, execution, fallbackState, action);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function consumeScheduledPriorityPreemption(context, scheduleId, schedule, execution, fallbackState, at) {
|
|
130
|
+
const scheduleResult = recordScheduleExecution(
|
|
131
|
+
context,
|
|
132
|
+
scheduleId,
|
|
133
|
+
execution,
|
|
134
|
+
fallbackState,
|
|
135
|
+
at || new Date().toISOString(),
|
|
136
|
+
'preempted',
|
|
137
|
+
);
|
|
138
|
+
const consumed = consumePreemptionMarker(context.root, {
|
|
139
|
+
role: schedule.initial_role || undefined,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (!consumed.ok) {
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
exitCode: 1,
|
|
146
|
+
result: {
|
|
147
|
+
...scheduleResult,
|
|
148
|
+
action: 'preemption_failed',
|
|
149
|
+
error: consumed.error,
|
|
150
|
+
injected_intent_id: execution.result?.preempted_by || null,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
ok: true,
|
|
157
|
+
exitCode: 0,
|
|
158
|
+
result: {
|
|
159
|
+
...scheduleResult,
|
|
160
|
+
action: 'preempted',
|
|
161
|
+
injected_intent_id: consumed.intent_id,
|
|
162
|
+
injected_turn_id: consumed.turn_id,
|
|
163
|
+
injected_role: consumed.role,
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function continueActiveScheduledRun(context, opts = {}) {
|
|
169
|
+
const continuation = findContinuableScheduleRun(context.root, context.config, {
|
|
170
|
+
scheduleId: opts.schedule || null,
|
|
171
|
+
});
|
|
172
|
+
if (!continuation.ok) {
|
|
173
|
+
return { matched: false, reason: continuation.reason };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const { schedule_id: scheduleId, schedule, state } = continuation;
|
|
177
|
+
|
|
178
|
+
if (!opts.json) {
|
|
179
|
+
console.log(chalk.cyan(`Continuing active scheduled run: ${scheduleId}`));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const execution = await executeGovernedRun(context, {
|
|
183
|
+
maxTurns: schedule.max_turns,
|
|
184
|
+
autoApprove: schedule.auto_approve !== false,
|
|
185
|
+
report: true,
|
|
186
|
+
log: opts.json ? () => {} : console.log,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
if (execution.result?.stop_reason === 'priority_preempted') {
|
|
190
|
+
const promoted = consumeScheduledPriorityPreemption(context, scheduleId, schedule, execution, state, opts.at);
|
|
191
|
+
return {
|
|
192
|
+
matched: true,
|
|
193
|
+
ok: promoted.ok,
|
|
194
|
+
exitCode: promoted.exitCode,
|
|
195
|
+
result: promoted.result,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const blocked = execution.result?.stop_reason === 'blocked';
|
|
200
|
+
const action = blocked && opts.tolerateBlockedRun ? 'blocked' : 'continued';
|
|
201
|
+
const result = recordScheduleExecution(context, scheduleId, execution, state, opts.at || new Date().toISOString(), action);
|
|
202
|
+
|
|
203
|
+
if (execution.exitCode !== 0 && !(opts.tolerateBlockedRun && blocked)) {
|
|
204
|
+
return {
|
|
205
|
+
matched: true,
|
|
206
|
+
ok: false,
|
|
207
|
+
exitCode: execution.exitCode,
|
|
208
|
+
result,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
matched: true,
|
|
214
|
+
ok: true,
|
|
215
|
+
exitCode: 0,
|
|
216
|
+
result,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
89
220
|
async function runDueSchedules(context, opts = {}) {
|
|
221
|
+
if (opts.continueActiveScheduleRuns) {
|
|
222
|
+
const continuation = await continueActiveScheduledRun(context, opts);
|
|
223
|
+
if (continuation.matched) {
|
|
224
|
+
return continuation.ok
|
|
225
|
+
? { ok: true, exitCode: continuation.exitCode, results: [continuation.result] }
|
|
226
|
+
: { ok: false, exitCode: continuation.exitCode, results: [continuation.result], error: 'Scheduled run failed' };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
90
230
|
const resolved = resolveScheduleEntries(context, opts.schedule, opts.at);
|
|
91
231
|
if (!resolved.ok) {
|
|
92
232
|
return { ok: false, exitCode: 1, error: resolved.error, results: [] };
|
|
@@ -96,6 +236,10 @@ async function runDueSchedules(context, opts = {}) {
|
|
|
96
236
|
const results = [];
|
|
97
237
|
|
|
98
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
|
+
}
|
|
99
243
|
if (!entry.enabled) {
|
|
100
244
|
results.push({ id: entry.id, action: 'disabled' });
|
|
101
245
|
continue;
|
|
@@ -150,26 +294,29 @@ async function runDueSchedules(context, opts = {}) {
|
|
|
150
294
|
continue;
|
|
151
295
|
}
|
|
152
296
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
297
|
+
if (execution.result?.stop_reason === 'priority_preempted') {
|
|
298
|
+
const promoted = consumeScheduledPriorityPreemption(context, entry.id, entry, execution, execution.result?.state || null, nowIso);
|
|
299
|
+
results.push(promoted.result);
|
|
300
|
+
if (!promoted.ok) {
|
|
301
|
+
return { ok: false, exitCode: promoted.exitCode, results };
|
|
302
|
+
}
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const blocked = execution.result?.stop_reason === 'blocked';
|
|
307
|
+
results.push(recordScheduleExecution(
|
|
308
|
+
context,
|
|
309
|
+
entry.id,
|
|
310
|
+
execution,
|
|
311
|
+
execution.result?.state || null,
|
|
312
|
+
nowIso,
|
|
313
|
+
blocked && opts.tolerateBlockedRun ? 'blocked' : 'ran',
|
|
314
|
+
));
|
|
171
315
|
|
|
172
316
|
if (execution.exitCode !== 0) {
|
|
317
|
+
if (opts.tolerateBlockedRun && blocked) {
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
173
320
|
return { ok: false, exitCode: execution.exitCode, results };
|
|
174
321
|
}
|
|
175
322
|
}
|
|
@@ -177,6 +324,159 @@ async function runDueSchedules(context, opts = {}) {
|
|
|
177
324
|
return { ok: true, exitCode: 0, results };
|
|
178
325
|
}
|
|
179
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
|
+
|
|
180
480
|
export async function scheduleListCommand(opts) {
|
|
181
481
|
const context = loadScheduleContext();
|
|
182
482
|
if (!context) return;
|
|
@@ -214,6 +514,14 @@ export async function scheduleRunDueCommand(opts) {
|
|
|
214
514
|
for (const entry of result.results) {
|
|
215
515
|
if (entry.action === 'ran') {
|
|
216
516
|
console.log(chalk.green(`Schedule ran: ${entry.id} (${entry.run_id || 'no run id'})`));
|
|
517
|
+
} else if (entry.action === 'continued') {
|
|
518
|
+
console.log(chalk.green(`Schedule continued: ${entry.id} (${entry.run_id || 'no run id'})`));
|
|
519
|
+
} else if (entry.action === 'preempted') {
|
|
520
|
+
console.log(chalk.yellow(`Schedule preempted by injected priority: ${entry.id} (${entry.injected_intent_id || 'unknown intent'})`));
|
|
521
|
+
} else if (entry.action === 'preemption_failed') {
|
|
522
|
+
console.log(chalk.red(`Schedule preemption failed: ${entry.id} (${entry.error || 'unknown error'})`));
|
|
523
|
+
} else if (entry.action === 'blocked') {
|
|
524
|
+
console.log(chalk.yellow(`Schedule waiting on unblock: ${entry.id}`));
|
|
217
525
|
} else if (entry.action === 'skipped') {
|
|
218
526
|
console.log(chalk.yellow(`Schedule skipped: ${entry.id} (${entry.reason})`));
|
|
219
527
|
} else if (entry.action === 'not_due') {
|
|
@@ -338,7 +646,66 @@ export async function scheduleDaemonCommand(opts) {
|
|
|
338
646
|
while (true) {
|
|
339
647
|
cycle += 1;
|
|
340
648
|
daemonState.last_cycle_started_at = new Date().toISOString();
|
|
341
|
-
|
|
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,
|
|
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
|
+
}
|
|
342
709
|
|
|
343
710
|
updateDaemonHeartbeat(context.root, daemonState, result);
|
|
344
711
|
|
package/src/commands/status.js
CHANGED
|
@@ -19,7 +19,10 @@ import { summarizeRunProvenance } from '../lib/run-provenance.js';
|
|
|
19
19
|
import { readRecentRunEventSummary } from '../lib/recent-event-summary.js';
|
|
20
20
|
import { deriveConflictedTurnResolutionActions } from '../lib/conflict-actions.js';
|
|
21
21
|
import { summarizeLatestGateActionAttempt } from '../lib/gate-actions.js';
|
|
22
|
+
import { findCurrentHumanEscalation } from '../lib/human-escalations.js';
|
|
22
23
|
import { getDashboardPid, getDashboardSession } from './dashboard.js';
|
|
24
|
+
import { readPreemptionMarker } from '../lib/intake.js';
|
|
25
|
+
import { readContinuousSession } from '../lib/continuous-run.js';
|
|
23
26
|
|
|
24
27
|
export async function statusCommand(opts) {
|
|
25
28
|
const context = loadStatusContext();
|
|
@@ -127,6 +130,9 @@ function renderGovernedStatus(context, opts) {
|
|
|
127
130
|
const repoDecisionSummary = summarizeRepoDecisions(readRepoDecisions(root), config);
|
|
128
131
|
|
|
129
132
|
const workflowKitArtifacts = deriveWorkflowKitArtifacts(root, config, state);
|
|
133
|
+
const humanEscalation = findCurrentHumanEscalation(root, state);
|
|
134
|
+
const preemptionMarker = readPreemptionMarker(root);
|
|
135
|
+
const continuousSession = readContinuousSession(root);
|
|
130
136
|
const gateActionAttempt = state?.pending_phase_transition
|
|
131
137
|
? summarizeLatestGateActionAttempt(root, 'phase_transition', state.pending_phase_transition.gate)
|
|
132
138
|
: state?.pending_run_completion
|
|
@@ -162,6 +168,9 @@ function renderGovernedStatus(context, opts) {
|
|
|
162
168
|
next_actions: nextActions,
|
|
163
169
|
connector_health: connectorHealth,
|
|
164
170
|
recent_event_summary: recentEventSummary,
|
|
171
|
+
human_escalation: humanEscalation,
|
|
172
|
+
preemption_marker: preemptionMarker,
|
|
173
|
+
continuous_session: continuousSession,
|
|
165
174
|
gate_action_attempt: gateActionAttempt,
|
|
166
175
|
workflow_kit_artifacts: workflowKitArtifacts,
|
|
167
176
|
dashboard_session: dashboardSessionObj,
|
|
@@ -174,6 +183,42 @@ function renderGovernedStatus(context, opts) {
|
|
|
174
183
|
console.log(chalk.dim(' ' + '─'.repeat(44)));
|
|
175
184
|
console.log('');
|
|
176
185
|
|
|
186
|
+
// Priority injection banner — above all other status
|
|
187
|
+
if (preemptionMarker) {
|
|
188
|
+
console.log(chalk.red.bold(' ⚡ Priority injection pending'));
|
|
189
|
+
console.log(chalk.dim(` Intent: ${preemptionMarker.intent_id}`));
|
|
190
|
+
console.log(` Priority: ${chalk.red.bold(preemptionMarker.priority)}`);
|
|
191
|
+
if (preemptionMarker.description) {
|
|
192
|
+
console.log(chalk.dim(` Description: ${preemptionMarker.description}`));
|
|
193
|
+
}
|
|
194
|
+
if (preemptionMarker.injected_at) {
|
|
195
|
+
console.log(chalk.dim(` Injected at: ${preemptionMarker.injected_at}`));
|
|
196
|
+
}
|
|
197
|
+
console.log(chalk.dim(' Effect: Will preempt current workstream after this turn completes'));
|
|
198
|
+
console.log(chalk.dim(' ' + '─'.repeat(44)));
|
|
199
|
+
console.log('');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Continuous session banner
|
|
203
|
+
if (continuousSession) {
|
|
204
|
+
console.log(chalk.cyan.bold(' 🔄 Continuous Vision-Driven Session'));
|
|
205
|
+
console.log(chalk.dim(` Session: ${continuousSession.session_id}`));
|
|
206
|
+
console.log(chalk.dim(` Vision: ${continuousSession.vision_path}`));
|
|
207
|
+
console.log(` Status: ${chalk.cyan(continuousSession.status || 'unknown')}`);
|
|
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
|
+
}
|
|
212
|
+
if (continuousSession.current_vision_objective) {
|
|
213
|
+
console.log(` Objective: ${chalk.yellow(continuousSession.current_vision_objective)}`);
|
|
214
|
+
}
|
|
215
|
+
if (continuousSession.idle_cycles > 0) {
|
|
216
|
+
console.log(chalk.dim(` Idle cycles: ${continuousSession.idle_cycles}/${continuousSession.max_idle_cycles}`));
|
|
217
|
+
}
|
|
218
|
+
console.log(chalk.dim(' ' + '─'.repeat(44)));
|
|
219
|
+
console.log('');
|
|
220
|
+
}
|
|
221
|
+
|
|
177
222
|
console.log(` ${chalk.dim('Project:')} ${config.project.name}`);
|
|
178
223
|
if (config.project.goal) {
|
|
179
224
|
console.log(` ${chalk.dim('Goal:')} ${config.project.goal}`);
|
|
@@ -325,6 +370,16 @@ function renderGovernedStatus(context, opts) {
|
|
|
325
370
|
}
|
|
326
371
|
}
|
|
327
372
|
|
|
373
|
+
if (humanEscalation) {
|
|
374
|
+
console.log('');
|
|
375
|
+
console.log(` ${chalk.dim('Human task:')} ${chalk.yellow(humanEscalation.escalation_id)}${humanEscalation.service ? ` (${humanEscalation.service})` : ''}`);
|
|
376
|
+
console.log(` ${chalk.dim('Type:')} ${humanEscalation.type}`);
|
|
377
|
+
console.log(` ${chalk.dim('Unblock:')} ${chalk.cyan(humanEscalation.resolution_command)}`);
|
|
378
|
+
if (humanEscalation.action) {
|
|
379
|
+
console.log(` ${chalk.dim('Task:')} ${humanEscalation.action}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
328
383
|
if (runtimeGuidance.length > 0) {
|
|
329
384
|
console.log('');
|
|
330
385
|
console.log(` ${chalk.dim('Runtime guidance:')}`);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { loadProjectContext, loadProjectState } from '../lib/config.js';
|
|
3
|
+
import { findCurrentHumanEscalation, getOpenHumanEscalation } from '../lib/human-escalations.js';
|
|
4
|
+
import { resumeCommand } from './resume.js';
|
|
5
|
+
|
|
6
|
+
export async function unblockCommand(escalationId) {
|
|
7
|
+
const context = loadProjectContext();
|
|
8
|
+
if (!context) {
|
|
9
|
+
console.log(chalk.red('No agentxchain.json found. Run `agentxchain init` first.'));
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { root, config } = context;
|
|
14
|
+
|
|
15
|
+
if (config.protocol_mode !== 'governed') {
|
|
16
|
+
console.log(chalk.red('The unblock command is only available for governed projects.'));
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!escalationId || !String(escalationId).trim()) {
|
|
21
|
+
console.log(chalk.red('An escalation id is required. Example: agentxchain unblock hesc_1234'));
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const state = loadProjectState(root, config);
|
|
26
|
+
if (!state) {
|
|
27
|
+
console.log(chalk.red('No governed state.json found. Run `agentxchain init --governed` first.'));
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (state.status !== 'blocked') {
|
|
32
|
+
console.log(chalk.red(`Cannot unblock run: status is "${state.status}", expected "blocked".`));
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const requested = getOpenHumanEscalation(root, escalationId);
|
|
37
|
+
if (!requested) {
|
|
38
|
+
console.log(chalk.red(`No open human escalation found for ${escalationId}.`));
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const current = findCurrentHumanEscalation(root, state);
|
|
43
|
+
if (!current) {
|
|
44
|
+
console.log(chalk.red('The current blocked run does not have a linked human escalation record.'));
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (current.escalation_id !== requested.escalation_id) {
|
|
49
|
+
console.log(chalk.red(`Escalation ${escalationId} is not the current blocker for this run.`));
|
|
50
|
+
console.log(chalk.dim(`Current blocker: ${current.escalation_id}`));
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log('');
|
|
55
|
+
console.log(chalk.green(` Unblocking ${requested.escalation_id}`));
|
|
56
|
+
console.log(chalk.dim(` Type: ${requested.type}${requested.service ? ` (${requested.service})` : ''}`));
|
|
57
|
+
if (requested.detail) {
|
|
58
|
+
console.log(chalk.dim(` Detail: ${requested.detail}`));
|
|
59
|
+
}
|
|
60
|
+
console.log(chalk.dim(' Continuing governed execution...'));
|
|
61
|
+
console.log('');
|
|
62
|
+
|
|
63
|
+
await resumeCommand({
|
|
64
|
+
_via: 'operator_unblock',
|
|
65
|
+
turn: requested.turn_id || undefined,
|
|
66
|
+
});
|
|
67
|
+
}
|