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 +1 -1
- package/src/commands/run.js +26 -1
- package/src/lib/adapters/local-cli-adapter.js +23 -1
- package/src/lib/continuous-run.js +60 -0
- package/src/lib/run-loop.js +28 -0
package/package.json
CHANGED
package/src/commands/run.js
CHANGED
|
@@ -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 {
|
|
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;
|
package/src/lib/run-loop.js
CHANGED
|
@@ -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
|
+
}
|