agentxchain 2.155.63 → 2.155.65

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.63",
3
+ "version": "2.155.65",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -497,11 +497,36 @@ export async function executeGovernedRun(context, opts = {}) {
497
497
  : 0,
498
498
  recommendation: `Turn ${turn.turn_id} failed to start within the startup watchdog window. Run \`agentxchain reissue-turn --turn ${turn.turn_id} --reason ghost\` to recover.`,
499
499
  });
500
- return { accept: false, blocked: true, reason: adapterResult.error || 'turn startup failed' };
500
+ return {
501
+ accept: false,
502
+ blocked: true,
503
+ blockedAlreadyPersisted: true,
504
+ reason: adapterResult.error || 'turn startup failed',
505
+ };
501
506
  }
502
507
 
503
508
  // Adapter failure
504
509
  if (!adapterResult.ok) {
510
+ if (adapterResult.blocked === true) {
511
+ const classified = adapterResult.classified || null;
512
+ const detail = classified
513
+ ? `${classified.error_class}: ${classified.recovery}`
514
+ : (adapterResult.error || 'adapter dispatch failed');
515
+ return {
516
+ accept: false,
517
+ blocked: true,
518
+ blockedOn: `dispatch:${classified?.error_class || 'subprocess_failed'}`,
519
+ blockedCategory: 'dispatch_error',
520
+ recovery: {
521
+ typed_reason: 'dispatch_error',
522
+ owner: 'human',
523
+ recovery_action: classified?.recovery || 'Resolve the dispatch issue, then run agentxchain step --resume',
524
+ turn_retained: true,
525
+ detail,
526
+ },
527
+ reason: detail,
528
+ };
529
+ }
505
530
  if (shouldSuggestManualQaFallback({
506
531
  roleId,
507
532
  runtimeId,
@@ -30,7 +30,7 @@ import {
30
30
  } from '../turn-paths.js';
31
31
  import { verifyDispatchManifestForAdapter } from '../dispatch-manifest.js';
32
32
  import { hasMeaningfulStagedResult } from '../staged-result-proof.js';
33
- import { getClaudeSubprocessAuthIssue } from '../claude-local-auth.js';
33
+ import { getClaudeSubprocessAuthIssue, isClaudeLocalCliRuntime } from '../claude-local-auth.js';
34
34
 
35
35
  const DIAGNOSTIC_ENV_KEYS = [
36
36
  'PATH',
@@ -43,6 +43,7 @@ const DIAGNOSTIC_ENV_KEYS = [
43
43
  const DIAGNOSTIC_STDERR_EXCERPT_LIMIT = 800;
44
44
  const DEFAULT_STARTUP_WATCHDOG_MS = 180_000;
45
45
  const DEFAULT_STARTUP_WATCHDOG_SIGKILL_GRACE_MS = 10_000;
46
+ const CLAUDE_AUTH_FAILURE_RE = /authentication_failed|authentication_error|invalid authentication credentials|unauthorized|API Error:\s*401/i;
46
47
 
47
48
  /**
48
49
  * Launch a local CLI subprocess for a governed turn.
@@ -427,6 +428,22 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
427
428
 
428
429
  if (hasResult) {
429
430
  settle({ ok: true, exitCode, timedOut: false, aborted: false, logs, firstOutputAt });
431
+ } else if (isClaudeLocalCliRuntime(runtime) && hasClaudeAuthFailureOutput(logs)) {
432
+ const recovery = 'Refresh Claude credentials before resuming: export a valid ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN, then run agentxchain step --resume.';
433
+ settle({
434
+ ok: false,
435
+ blocked: true,
436
+ exitCode,
437
+ timedOut: false,
438
+ aborted: false,
439
+ firstOutputAt,
440
+ classified: {
441
+ error_class: 'claude_auth_failed',
442
+ recovery,
443
+ },
444
+ error: `Claude local_cli authentication failed. ${recovery}`,
445
+ logs,
446
+ });
430
447
  } else if (startupTimedOut) {
431
448
  settle({
432
449
  ok: false,
@@ -640,6 +657,11 @@ function appendDiagnostic(logs, label, payload) {
640
657
  logs.push(`[adapter:diag] ${label} ${JSON.stringify(payload)}\n`);
641
658
  }
642
659
 
660
+ function hasClaudeAuthFailureOutput(logs) {
661
+ if (!Array.isArray(logs)) return false;
662
+ return logs.some((line) => typeof line === 'string' && CLAUDE_AUTH_FAILURE_RE.test(line));
663
+ }
664
+
643
665
  function pickDiagnosticEnv(env) {
644
666
  return Object.fromEntries(
645
667
  DIAGNOSTIC_ENV_KEYS
@@ -52,6 +52,7 @@ import {
52
52
  formatLegacyIntentMigrationNotice,
53
53
  formatPhantomIntentSupersessionNotice,
54
54
  } from './intent-startup-migration.js';
55
+ import { checkpointAcceptedTurn } from './turn-checkpoint.js';
55
56
 
56
57
  const CONTINUOUS_SESSION_PATH = '.agentxchain/continuous-session.json';
57
58
  const PRODUCTIVE_TIMEOUT_RETRY_MAX_PER_RUN = 1;
@@ -574,6 +575,59 @@ async function maybeAutoRetryContinuousBlocker(context, session, contOpts, block
574
575
  || await maybeAutoRetryGhostBlocker(context, session, contOpts, blockedState, log);
575
576
  }
576
577
 
578
+ function extractCheckpointTurnIdFromExecution(execution) {
579
+ const errors = Array.isArray(execution?.result?.errors) ? execution.result.errors : [];
580
+ for (const error of errors) {
581
+ const text = String(error || '');
582
+ const match = text.match(/\bcheckpoint-turn\s+--turn\s+(turn_[A-Za-z0-9_-]+)/);
583
+ if (match) return match[1];
584
+ }
585
+ return null;
586
+ }
587
+
588
+ function maybeAutoCheckpointBlockedExecution(context, session, contOpts, execution, log = console.log) {
589
+ if (!contOpts.autoCheckpoint) return null;
590
+ const turnId = extractCheckpointTurnIdFromExecution(execution);
591
+ if (!turnId) return null;
592
+
593
+ const checkpoint = checkpointAcceptedTurn(context.root, { turnId });
594
+ if (!checkpoint.ok) {
595
+ log(`Auto-checkpoint skipped for ${turnId}: ${checkpoint.error || 'checkpoint failed'}`);
596
+ return null;
597
+ }
598
+ if (checkpoint.already_checkpointed || checkpoint.skipped) {
599
+ log(`Auto-checkpoint skipped for ${turnId}: ${checkpoint.reason || 'no checkpoint changes were created'}`);
600
+ return null;
601
+ }
602
+
603
+ session.status = 'running';
604
+ session.current_run_id = session.current_run_id || execution?.result?.state?.run_id || null;
605
+ writeContinuousSession(context.root, session);
606
+
607
+ emitRunEvent(context.root, 'continuous_auto_checkpoint_recovered', {
608
+ run_id: session.current_run_id || execution?.result?.state?.run_id || null,
609
+ phase: execution?.result?.state?.phase || null,
610
+ status: 'active',
611
+ turn: { turn_id: turnId, role_id: null },
612
+ payload: {
613
+ session_id: session.session_id,
614
+ checkpoint_sha: checkpoint.checkpoint_sha || null,
615
+ already_checkpointed: Boolean(checkpoint.already_checkpointed),
616
+ recovered_files_changed: checkpoint.recovered_files_changed || checkpoint.files_changed || null,
617
+ },
618
+ });
619
+
620
+ log(`Auto-checkpoint recovered accepted turn ${turnId}; continuing active run.`);
621
+ return {
622
+ ok: true,
623
+ status: 'running',
624
+ action: 'auto_checkpoint_recovered',
625
+ run_id: session.current_run_id,
626
+ turn_id: turnId,
627
+ checkpoint_sha: checkpoint.checkpoint_sha || null,
628
+ };
629
+ }
630
+
577
631
  async function maybeAutoRetryGhostBlocker(context, session, contOpts, blockedState, log = console.log) {
578
632
  const { root, config } = context;
579
633
  const decision = classifyGhostRetryDecision({
@@ -1784,6 +1838,8 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
1784
1838
  const resumeStopReason = execution.result?.stop_reason;
1785
1839
 
1786
1840
  if (isBlockedContinuousExecution(execution)) {
1841
+ const checkpointed = maybeAutoCheckpointBlockedExecution(context, session, contOpts, execution, log);
1842
+ if (checkpointed) return checkpointed;
1787
1843
  const blockedState = execution?.result?.state || loadProjectState(root, context.config);
1788
1844
  const retried = await maybeAutoRetryContinuousBlocker(context, session, contOpts, blockedState, log);
1789
1845
  if (retried) return retried;
@@ -1852,6 +1908,8 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
1852
1908
  const resumeStopReason = execution.result?.stop_reason;
1853
1909
 
1854
1910
  if (isBlockedContinuousExecution(execution)) {
1911
+ const checkpointed = maybeAutoCheckpointBlockedExecution(context, session, contOpts, execution, log);
1912
+ if (checkpointed) return checkpointed;
1855
1913
  const blockedState = execution?.result?.state || loadProjectState(root, context.config);
1856
1914
  const retried = await maybeAutoRetryContinuousBlocker(context, session, contOpts, blockedState, log);
1857
1915
  if (retried) return retried;
@@ -2044,6 +2102,8 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
2044
2102
  }
2045
2103
 
2046
2104
  if (isBlockedContinuousExecution(execution)) {
2105
+ const checkpointed = maybeAutoCheckpointBlockedExecution(context, session, contOpts, execution, log);
2106
+ if (checkpointed) return checkpointed;
2047
2107
  const blockedState = execution?.result?.state || loadProjectState(root, context.config);
2048
2108
  const retried = await maybeAutoRetryContinuousBlocker(context, session, contOpts, blockedState, log);
2049
2109
  if (retried) return retried;
@@ -461,6 +461,9 @@ async function executeParallelTurns(root, config, state, maxConcurrent, callback
461
461
  emit({ type: 'turn_accepted', turn, role: roleId, state: acceptResult.state });
462
462
  } else {
463
463
  if (dispatchResult?.blocked === true) {
464
+ if (dispatchResult.blockedAlreadyPersisted !== true) {
465
+ persistDispatchBlocker(root, config, turn, dispatchResult, errors);
466
+ }
464
467
  history.push({ role: roleId, turn_id: turn.turn_id, accepted: false, blocked: true });
465
468
  const blockedState = loadState(root, config);
466
469
  emit({ type: 'blocked', state: blockedState });
@@ -653,6 +656,9 @@ async function dispatchAndProcess(root, config, turn, assignState, callbacks, em
653
656
  }
654
657
 
655
658
  if (dispatchResult?.blocked === true) {
659
+ if (dispatchResult.blockedAlreadyPersisted !== true) {
660
+ persistDispatchBlocker(root, config, turn, dispatchResult, errors);
661
+ }
656
662
  history.push({ role: roleId, turn_id: turn.turn_id, accepted: false, blocked: true });
657
663
  const blockedState = loadState(root, config);
658
664
  emit({ type: 'blocked', state: blockedState });
@@ -847,3 +853,25 @@ function persistDispatchTimeout(root, config, turn, timeoutResult, errors) {
847
853
  errors.push(`dispatch timed out for ${turn.assigned_role} after ${timeoutResult.limit_minutes}m`);
848
854
  return blocked;
849
855
  }
856
+
857
+ function persistDispatchBlocker(root, config, turn, dispatchResult, errors) {
858
+ const recovery = dispatchResult.recovery || {
859
+ typed_reason: 'dispatch_error',
860
+ owner: 'human',
861
+ recovery_action: 'Resolve the dispatch issue, then run agentxchain step --resume',
862
+ turn_retained: true,
863
+ detail: dispatchResult.reason || 'adapter dispatch failed',
864
+ };
865
+ const blocked = markRunBlocked(root, {
866
+ blockedOn: dispatchResult.blockedOn || 'dispatch:subprocess_failed',
867
+ category: dispatchResult.blockedCategory || 'dispatch_error',
868
+ recovery,
869
+ turnId: turn.turn_id,
870
+ notificationConfig: config,
871
+ });
872
+ if (!blocked.ok) {
873
+ errors.push(`markRunBlocked(dispatch): ${blocked.error}`);
874
+ return { state: loadState(root, config) };
875
+ }
876
+ return blocked;
877
+ }