agentxchain 2.155.60 → 2.155.61

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.155.60",
3
+ "version": "2.155.61",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -46,6 +46,7 @@ import {
46
46
  import { getDispatchLogPath, getTurnStagingResultPath } from './turn-paths.js';
47
47
  import { reconcileOperatorHead } from './operator-commit-reconcile.js';
48
48
  import { getContinuityStatus } from './continuity-status.js';
49
+ import { resolveGovernedRole } from './role-resolution.js';
49
50
  import {
50
51
  archiveStaleIntentsForRun,
51
52
  formatLegacyIntentMigrationNotice,
@@ -132,6 +133,36 @@ function createSession(visionPath, maxRuns, maxIdleCycles, perSessionMaxUsd, cur
132
133
  };
133
134
  }
134
135
 
136
+ function canResumeExistingContinuousSession(session, contOpts) {
137
+ if (!session || typeof session !== 'object') return false;
138
+ if (!['paused', 'running'].includes(session.status)) return false;
139
+ if (session.owner_type === 'schedule') return false;
140
+ if (session.vision_path && session.vision_path !== contOpts.visionPath) return false;
141
+ if (contOpts.continueFrom && session.current_run_id && session.current_run_id !== contOpts.continueFrom) return false;
142
+ return true;
143
+ }
144
+
145
+ function resumeExistingContinuousSession(session, contOpts, initialRunId, snapshotOpts = {}) {
146
+ return {
147
+ ...session,
148
+ vision_path: session.vision_path || contOpts.visionPath,
149
+ max_runs: contOpts.maxRuns,
150
+ max_idle_cycles: contOpts.maxIdleCycles,
151
+ current_run_id: session.current_run_id || initialRunId || null,
152
+ status: session.status === 'paused' ? 'paused' : 'running',
153
+ per_session_max_usd: session.per_session_max_usd ?? contOpts.perSessionMaxUsd ?? null,
154
+ cumulative_spent_usd: session.cumulative_spent_usd || 0,
155
+ budget_exhausted: Boolean(session.budget_exhausted),
156
+ startup_reconciled_run_id: session.startup_reconciled_run_id || null,
157
+ vision_headings_snapshot: session.vision_headings_snapshot || snapshotOpts.visionHeadingsSnapshot || null,
158
+ vision_sha_at_snapshot: session.vision_sha_at_snapshot || snapshotOpts.visionShaAtSnapshot || null,
159
+ expansion_iteration: session.expansion_iteration ?? snapshotOpts.expansionIteration ?? 0,
160
+ _vision_stale_warned_shas: Array.isArray(session._vision_stale_warned_shas)
161
+ ? session._vision_stale_warned_shas
162
+ : [],
163
+ };
164
+ }
165
+
135
166
  function describeContinuousTerminalStep(step, contOpts) {
136
167
  if (step.action === 'max_runs_reached') {
137
168
  return `Max runs reached (${contOpts.maxRuns}). Stopping.`;
@@ -277,6 +308,59 @@ function getBlockedCategory(state) {
277
308
  return state?.blocked_reason?.category || null;
278
309
  }
279
310
 
311
+ const RECOVERABLE_ACTIVE_TURN_STATUSES = new Set(['assigned', 'dispatched', 'starting', 'running']);
312
+
313
+ function hasOnlyRecoverableActiveTurns(activeTurns = {}) {
314
+ const turns = Object.values(activeTurns || {});
315
+ if (turns.length === 0) return false;
316
+ return turns.every((turn) => RECOVERABLE_ACTIVE_TURN_STATUSES.has(turn?.status || 'assigned'));
317
+ }
318
+
319
+ export function isPausedContinuousSessionRecoverableActiveRun(session, state, config) {
320
+ if (!session || session.status !== 'paused') return false;
321
+ if (!state || state.status !== 'active') return false;
322
+ if (session.current_run_id && state.run_id && session.current_run_id !== state.run_id) return false;
323
+ if (state.blocked_on || state.blocked_reason || state.escalation) return false;
324
+ if (state.pending_phase_transition || state.pending_run_completion) return false;
325
+ if (state.queued_phase_transition || state.queued_run_completion) return false;
326
+ if (Object.keys(state.active_turns || {}).length > 0) {
327
+ return hasOnlyRecoverableActiveTurns(state.active_turns);
328
+ }
329
+
330
+ const resolved = resolveGovernedRole({ state, config });
331
+ return Boolean(resolved?.roleId && !resolved.error);
332
+ }
333
+
334
+ function isPausedActiveRunWaitingOnGovernance(session, state) {
335
+ if (!session || session.status !== 'paused') return false;
336
+ if (!state || state.status !== 'active') return false;
337
+ if (session.current_run_id && state.run_id && session.current_run_id !== state.run_id) return false;
338
+ return true;
339
+ }
340
+
341
+ function recoverPausedActiveContinuousSession(context, session, log = console.log, reason = 'paused_active_run') {
342
+ const latestSession = readContinuousSession(context.root) || session;
343
+ const state = loadProjectState(context.root, context.config);
344
+ if (!isPausedContinuousSessionRecoverableActiveRun(latestSession, state, context.config)) {
345
+ return false;
346
+ }
347
+
348
+ Object.assign(session, latestSession, { status: 'running' });
349
+ writeContinuousSession(context.root, session);
350
+ emitRunEvent(context.root, 'continuous_paused_active_run_recovered', {
351
+ run_id: state.run_id || session.current_run_id || null,
352
+ phase: state.phase || null,
353
+ status: state.status || 'active',
354
+ payload: {
355
+ session_id: session.session_id,
356
+ reason,
357
+ next_recommended_role: state.next_recommended_role || null,
358
+ },
359
+ });
360
+ log(`Continuous session was paused while run ${state.run_id || session.current_run_id || '(unknown)'} remained active; resuming next role dispatch.`);
361
+ return true;
362
+ }
363
+
280
364
  function writeGovernedState(root, state) {
281
365
  safeWriteJson(join(root, '.agentxchain', 'state.json'), state);
282
366
  }
@@ -1650,6 +1734,31 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
1650
1734
  blocked_category: getBlockedCategory(governedState),
1651
1735
  };
1652
1736
  }
1737
+ if (isPausedContinuousSessionRecoverableActiveRun(session, governedState, context.config)) {
1738
+ session.status = 'running';
1739
+ writeContinuousSession(root, session);
1740
+ emitRunEvent(root, 'continuous_paused_active_run_recovered', {
1741
+ run_id: governedState.run_id || session.current_run_id || null,
1742
+ phase: governedState.phase || null,
1743
+ status: governedState.status || 'active',
1744
+ payload: {
1745
+ session_id: session.session_id,
1746
+ reason: 'advance_once_paused_active_run',
1747
+ next_recommended_role: governedState.next_recommended_role || null,
1748
+ },
1749
+ });
1750
+ log(`Paused continuous session has active unblocked run ${governedState.run_id || session.current_run_id || '(unknown)'}; resuming next role dispatch.`);
1751
+ } else if (isPausedActiveRunWaitingOnGovernance(session, governedState)) {
1752
+ writeContinuousSession(root, session);
1753
+ return {
1754
+ ok: true,
1755
+ status: 'blocked',
1756
+ action: 'paused_active_run_waiting',
1757
+ run_id: session.current_run_id || governedState.run_id || null,
1758
+ recovery_action: 'Review pending approvals, active turns, or role-resolution errors before resuming this paused continuous session.',
1759
+ blocked_category: 'paused_active_run_waiting',
1760
+ };
1761
+ }
1653
1762
  // Unblocked — resume by continuing the existing governed run directly.
1654
1763
  // Skip the intake pipeline: the run is already in progress, and startIntent
1655
1764
  // would reject because the governed state is active.
@@ -2057,15 +2166,31 @@ export async function executeContinuousRun(context, contOpts, executeGovernedRun
2057
2166
  // VISION.md unreadable — will fail at first advanceContinuousRunOnce anyway
2058
2167
  }
2059
2168
 
2060
- const session = createSession(
2061
- contOpts.visionPath,
2062
- contOpts.maxRuns,
2063
- contOpts.maxIdleCycles,
2064
- contOpts.perSessionMaxUsd,
2065
- initialRunId,
2066
- { visionHeadingsSnapshot, visionShaAtSnapshot },
2067
- );
2169
+ const existingSession = readContinuousSession(root);
2170
+ const snapshotOpts = { visionHeadingsSnapshot, visionShaAtSnapshot };
2171
+ const session = canResumeExistingContinuousSession(existingSession, contOpts)
2172
+ ? resumeExistingContinuousSession(existingSession, contOpts, initialRunId, snapshotOpts)
2173
+ : createSession(
2174
+ contOpts.visionPath,
2175
+ contOpts.maxRuns,
2176
+ contOpts.maxIdleCycles,
2177
+ contOpts.perSessionMaxUsd,
2178
+ initialRunId,
2179
+ snapshotOpts,
2180
+ );
2068
2181
  writeContinuousSession(root, session);
2182
+ if (existingSession && session.session_id === existingSession.session_id) {
2183
+ emitRunEvent(root, 'continuous_session_resumed', {
2184
+ run_id: session.current_run_id || null,
2185
+ phase: startupState?.phase || null,
2186
+ status: session.status,
2187
+ payload: {
2188
+ session_id: session.session_id,
2189
+ prior_status: existingSession.status || null,
2190
+ },
2191
+ });
2192
+ log(`Resuming existing continuous session ${session.session_id}.`);
2193
+ }
2069
2194
 
2070
2195
  // SIGINT handler
2071
2196
  let stopping = false;
@@ -2079,6 +2204,10 @@ export async function executeContinuousRun(context, contOpts, executeGovernedRun
2079
2204
  while (!stopping) {
2080
2205
  const step = await advanceContinuousRunOnce(context, session, contOpts, executeGovernedRun, log);
2081
2206
 
2207
+ if (recoverPausedActiveContinuousSession(context, session, log, `post_step_${step.action || step.status || 'unknown'}`)) {
2208
+ continue;
2209
+ }
2210
+
2082
2211
  // Terminal states
2083
2212
  if (step.status === 'completed' || step.status === 'idle_exit' || step.status === 'failed' || step.status === 'blocked' || step.status === 'stopped' || step.status === 'vision_exhausted' || step.status === 'vision_expansion_exhausted' || step.status === 'session_budget') {
2084
2213
  const terminalMessage = describeContinuousTerminalStep(step, contOpts);