agentxchain 2.115.0 → 2.117.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.
@@ -6,12 +6,14 @@ 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';
16
18
 
17
19
  function loadScheduleContext() {
@@ -86,7 +88,136 @@ function buildScheduleProvenance(entry) {
86
88
  };
87
89
  }
88
90
 
91
+ function buildScheduleExecutionResult(entryId, execution, fallbackState, action = 'ran') {
92
+ const state = execution.result?.state || fallbackState || null;
93
+ return {
94
+ id: entryId,
95
+ action,
96
+ run_id: state?.run_id || null,
97
+ stop_reason: execution.result?.stop_reason || null,
98
+ exit_code: execution.exitCode,
99
+ };
100
+ }
101
+
102
+ function recordScheduleExecution(context, entryId, execution, fallbackState, nowIso, action = 'ran') {
103
+ const state = execution.result?.state || fallbackState || null;
104
+ const runId = state?.run_id || null;
105
+ const startedAt = state?.created_at || nowIso;
106
+
107
+ updateScheduleState(context.root, context.config, entryId, (record) => ({
108
+ ...record,
109
+ last_started_at: startedAt,
110
+ last_finished_at: new Date().toISOString(),
111
+ last_run_id: runId,
112
+ last_status: execution.result?.stop_reason || (execution.exitCode === 0 ? 'completed' : 'launch_failed'),
113
+ last_skip_at: null,
114
+ last_skip_reason: null,
115
+ }));
116
+
117
+ return buildScheduleExecutionResult(entryId, execution, fallbackState, action);
118
+ }
119
+
120
+ function consumeScheduledPriorityPreemption(context, scheduleId, schedule, execution, fallbackState, at) {
121
+ const scheduleResult = recordScheduleExecution(
122
+ context,
123
+ scheduleId,
124
+ execution,
125
+ fallbackState,
126
+ at || new Date().toISOString(),
127
+ 'preempted',
128
+ );
129
+ const consumed = consumePreemptionMarker(context.root, {
130
+ role: schedule.initial_role || undefined,
131
+ });
132
+
133
+ if (!consumed.ok) {
134
+ return {
135
+ ok: false,
136
+ exitCode: 1,
137
+ result: {
138
+ ...scheduleResult,
139
+ action: 'preemption_failed',
140
+ error: consumed.error,
141
+ injected_intent_id: execution.result?.preempted_by || null,
142
+ },
143
+ };
144
+ }
145
+
146
+ return {
147
+ ok: true,
148
+ exitCode: 0,
149
+ result: {
150
+ ...scheduleResult,
151
+ action: 'preempted',
152
+ injected_intent_id: consumed.intent_id,
153
+ injected_turn_id: consumed.turn_id,
154
+ injected_role: consumed.role,
155
+ },
156
+ };
157
+ }
158
+
159
+ async function continueActiveScheduledRun(context, opts = {}) {
160
+ const continuation = findContinuableScheduleRun(context.root, context.config, {
161
+ scheduleId: opts.schedule || null,
162
+ });
163
+ if (!continuation.ok) {
164
+ return { matched: false, reason: continuation.reason };
165
+ }
166
+
167
+ const { schedule_id: scheduleId, schedule, state } = continuation;
168
+
169
+ if (!opts.json) {
170
+ console.log(chalk.cyan(`Continuing active scheduled run: ${scheduleId}`));
171
+ }
172
+
173
+ const execution = await executeGovernedRun(context, {
174
+ maxTurns: schedule.max_turns,
175
+ autoApprove: schedule.auto_approve !== false,
176
+ report: true,
177
+ log: opts.json ? () => {} : console.log,
178
+ });
179
+
180
+ if (execution.result?.stop_reason === 'priority_preempted') {
181
+ const promoted = consumeScheduledPriorityPreemption(context, scheduleId, schedule, execution, state, opts.at);
182
+ return {
183
+ matched: true,
184
+ ok: promoted.ok,
185
+ exitCode: promoted.exitCode,
186
+ result: promoted.result,
187
+ };
188
+ }
189
+
190
+ const blocked = execution.result?.stop_reason === 'blocked';
191
+ const action = blocked && opts.tolerateBlockedRun ? 'blocked' : 'continued';
192
+ const result = recordScheduleExecution(context, scheduleId, execution, state, opts.at || new Date().toISOString(), action);
193
+
194
+ if (execution.exitCode !== 0 && !(opts.tolerateBlockedRun && blocked)) {
195
+ return {
196
+ matched: true,
197
+ ok: false,
198
+ exitCode: execution.exitCode,
199
+ result,
200
+ };
201
+ }
202
+
203
+ return {
204
+ matched: true,
205
+ ok: true,
206
+ exitCode: 0,
207
+ result,
208
+ };
209
+ }
210
+
89
211
  async function runDueSchedules(context, opts = {}) {
212
+ if (opts.continueActiveScheduleRuns) {
213
+ const continuation = await continueActiveScheduledRun(context, opts);
214
+ if (continuation.matched) {
215
+ return continuation.ok
216
+ ? { ok: true, exitCode: continuation.exitCode, results: [continuation.result] }
217
+ : { ok: false, exitCode: continuation.exitCode, results: [continuation.result], error: 'Scheduled run failed' };
218
+ }
219
+ }
220
+
90
221
  const resolved = resolveScheduleEntries(context, opts.schedule, opts.at);
91
222
  if (!resolved.ok) {
92
223
  return { ok: false, exitCode: 1, error: resolved.error, results: [] };
@@ -150,26 +281,29 @@ async function runDueSchedules(context, opts = {}) {
150
281
  continue;
151
282
  }
152
283
 
153
- const runId = execution.result?.state?.run_id || null;
154
- const startedAt = execution.result?.state?.created_at || nowIso;
155
- updateScheduleState(context.root, context.config, entry.id, (record) => ({
156
- ...record,
157
- last_started_at: startedAt,
158
- last_finished_at: new Date().toISOString(),
159
- last_run_id: runId,
160
- last_status: execution.result?.stop_reason || (execution.exitCode === 0 ? 'completed' : 'launch_failed'),
161
- last_skip_at: null,
162
- last_skip_reason: null,
163
- }));
164
- results.push({
165
- id: entry.id,
166
- action: 'ran',
167
- run_id: runId,
168
- stop_reason: execution.result?.stop_reason || null,
169
- exit_code: execution.exitCode,
170
- });
284
+ if (execution.result?.stop_reason === 'priority_preempted') {
285
+ const promoted = consumeScheduledPriorityPreemption(context, entry.id, entry, execution, execution.result?.state || null, nowIso);
286
+ results.push(promoted.result);
287
+ if (!promoted.ok) {
288
+ return { ok: false, exitCode: promoted.exitCode, results };
289
+ }
290
+ continue;
291
+ }
292
+
293
+ const blocked = execution.result?.stop_reason === 'blocked';
294
+ results.push(recordScheduleExecution(
295
+ context,
296
+ entry.id,
297
+ execution,
298
+ execution.result?.state || null,
299
+ nowIso,
300
+ blocked && opts.tolerateBlockedRun ? 'blocked' : 'ran',
301
+ ));
171
302
 
172
303
  if (execution.exitCode !== 0) {
304
+ if (opts.tolerateBlockedRun && blocked) {
305
+ continue;
306
+ }
173
307
  return { ok: false, exitCode: execution.exitCode, results };
174
308
  }
175
309
  }
@@ -214,6 +348,14 @@ export async function scheduleRunDueCommand(opts) {
214
348
  for (const entry of result.results) {
215
349
  if (entry.action === 'ran') {
216
350
  console.log(chalk.green(`Schedule ran: ${entry.id} (${entry.run_id || 'no run id'})`));
351
+ } else if (entry.action === 'continued') {
352
+ console.log(chalk.green(`Schedule continued: ${entry.id} (${entry.run_id || 'no run id'})`));
353
+ } else if (entry.action === 'preempted') {
354
+ console.log(chalk.yellow(`Schedule preempted by injected priority: ${entry.id} (${entry.injected_intent_id || 'unknown intent'})`));
355
+ } else if (entry.action === 'preemption_failed') {
356
+ console.log(chalk.red(`Schedule preemption failed: ${entry.id} (${entry.error || 'unknown error'})`));
357
+ } else if (entry.action === 'blocked') {
358
+ console.log(chalk.yellow(`Schedule waiting on unblock: ${entry.id}`));
217
359
  } else if (entry.action === 'skipped') {
218
360
  console.log(chalk.yellow(`Schedule skipped: ${entry.id} (${entry.reason})`));
219
361
  } else if (entry.action === 'not_due') {
@@ -338,7 +480,11 @@ export async function scheduleDaemonCommand(opts) {
338
480
  while (true) {
339
481
  cycle += 1;
340
482
  daemonState.last_cycle_started_at = new Date().toISOString();
341
- const result = await runDueSchedules(context, opts);
483
+ const result = await runDueSchedules(context, {
484
+ ...opts,
485
+ continueActiveScheduleRuns: true,
486
+ tolerateBlockedRun: true,
487
+ });
342
488
 
343
489
  updateDaemonHeartbeat(context.root, daemonState, result);
344
490
 
@@ -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,39 @@ 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.current_vision_objective) {
210
+ console.log(` Objective: ${chalk.yellow(continuousSession.current_vision_objective)}`);
211
+ }
212
+ if (continuousSession.idle_cycles > 0) {
213
+ console.log(chalk.dim(` Idle cycles: ${continuousSession.idle_cycles}/${continuousSession.max_idle_cycles}`));
214
+ }
215
+ console.log(chalk.dim(' ' + '─'.repeat(44)));
216
+ console.log('');
217
+ }
218
+
177
219
  console.log(` ${chalk.dim('Project:')} ${config.project.name}`);
178
220
  if (config.project.goal) {
179
221
  console.log(` ${chalk.dim('Goal:')} ${config.project.goal}`);
@@ -325,6 +367,16 @@ function renderGovernedStatus(context, opts) {
325
367
  }
326
368
  }
327
369
 
370
+ if (humanEscalation) {
371
+ console.log('');
372
+ console.log(` ${chalk.dim('Human task:')} ${chalk.yellow(humanEscalation.escalation_id)}${humanEscalation.service ? ` (${humanEscalation.service})` : ''}`);
373
+ console.log(` ${chalk.dim('Type:')} ${humanEscalation.type}`);
374
+ console.log(` ${chalk.dim('Unblock:')} ${chalk.cyan(humanEscalation.resolution_command)}`);
375
+ if (humanEscalation.action) {
376
+ console.log(` ${chalk.dim('Task:')} ${humanEscalation.action}`);
377
+ }
378
+ }
379
+
328
380
  if (runtimeGuidance.length > 0) {
329
381
  console.log('');
330
382
  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
+ }