agentxchain 2.155.64 → 2.155.66
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
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,11 @@ 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 {
|
|
33
|
+
import {
|
|
34
|
+
getClaudeSubprocessAuthIssue,
|
|
35
|
+
hasClaudeAuthenticationFailureText,
|
|
36
|
+
isClaudeLocalCliRuntime,
|
|
37
|
+
} from '../claude-local-auth.js';
|
|
34
38
|
|
|
35
39
|
const DIAGNOSTIC_ENV_KEYS = [
|
|
36
40
|
'PATH',
|
|
@@ -427,6 +431,22 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
427
431
|
|
|
428
432
|
if (hasResult) {
|
|
429
433
|
settle({ ok: true, exitCode, timedOut: false, aborted: false, logs, firstOutputAt });
|
|
434
|
+
} else if (isClaudeLocalCliRuntime(runtime) && hasClaudeAuthFailureOutput(logs)) {
|
|
435
|
+
const recovery = 'Refresh Claude credentials before resuming: export a valid ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN, then run agentxchain step --resume.';
|
|
436
|
+
settle({
|
|
437
|
+
ok: false,
|
|
438
|
+
blocked: true,
|
|
439
|
+
exitCode,
|
|
440
|
+
timedOut: false,
|
|
441
|
+
aborted: false,
|
|
442
|
+
firstOutputAt,
|
|
443
|
+
classified: {
|
|
444
|
+
error_class: 'claude_auth_failed',
|
|
445
|
+
recovery,
|
|
446
|
+
},
|
|
447
|
+
error: `Claude local_cli authentication failed. ${recovery}`,
|
|
448
|
+
logs,
|
|
449
|
+
});
|
|
430
450
|
} else if (startupTimedOut) {
|
|
431
451
|
settle({
|
|
432
452
|
ok: false,
|
|
@@ -640,6 +660,11 @@ function appendDiagnostic(logs, label, payload) {
|
|
|
640
660
|
logs.push(`[adapter:diag] ${label} ${JSON.stringify(payload)}\n`);
|
|
641
661
|
}
|
|
642
662
|
|
|
663
|
+
function hasClaudeAuthFailureOutput(logs) {
|
|
664
|
+
if (!Array.isArray(logs)) return false;
|
|
665
|
+
return logs.some((line) => hasClaudeAuthenticationFailureText(line));
|
|
666
|
+
}
|
|
667
|
+
|
|
643
668
|
function pickDiagnosticEnv(env) {
|
|
644
669
|
return Object.fromEntries(
|
|
645
670
|
DIAGNOSTIC_ENV_KEYS
|
|
@@ -10,6 +10,7 @@ const CLAUDE_ENV_AUTH_KEYS = [
|
|
|
10
10
|
|
|
11
11
|
const DEFAULT_SMOKE_PROBE_TIMEOUT_MS = 10_000;
|
|
12
12
|
const DEFAULT_SMOKE_PROBE_STDIN = 'ok';
|
|
13
|
+
const CLAUDE_AUTH_FAILURE_RE = /authentication_failed|authentication_error|invalid authentication credentials|unauthorized|API Error:\s*401/i;
|
|
13
14
|
|
|
14
15
|
function normalizeCommandTokens(runtime) {
|
|
15
16
|
if (Array.isArray(runtime?.command)) {
|
|
@@ -32,6 +33,10 @@ export function isClaudeLocalCliRuntime(runtime) {
|
|
|
32
33
|
return head === 'claude' || head.endsWith('/claude');
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
export function hasClaudeAuthenticationFailureText(text) {
|
|
37
|
+
return typeof text === 'string' && CLAUDE_AUTH_FAILURE_RE.test(text);
|
|
38
|
+
}
|
|
39
|
+
|
|
35
40
|
export function hasClaudeBareFlag(runtime) {
|
|
36
41
|
return normalizeCommandTokens(runtime).includes('--bare');
|
|
37
42
|
}
|
|
@@ -53,6 +53,10 @@ import {
|
|
|
53
53
|
formatPhantomIntentSupersessionNotice,
|
|
54
54
|
} from './intent-startup-migration.js';
|
|
55
55
|
import { checkpointAcceptedTurn } from './turn-checkpoint.js';
|
|
56
|
+
import {
|
|
57
|
+
hasClaudeAuthenticationFailureText,
|
|
58
|
+
isClaudeLocalCliRuntime,
|
|
59
|
+
} from './claude-local-auth.js';
|
|
56
60
|
|
|
57
61
|
const CONTINUOUS_SESSION_PATH = '.agentxchain/continuous-session.json';
|
|
58
62
|
const PRODUCTIVE_TIMEOUT_RETRY_MAX_PER_RUN = 1;
|
|
@@ -309,6 +313,87 @@ function getBlockedCategory(state) {
|
|
|
309
313
|
return state?.blocked_reason?.category || null;
|
|
310
314
|
}
|
|
311
315
|
|
|
316
|
+
const CLAUDE_AUTH_RECOVERY_ACTION =
|
|
317
|
+
'Refresh Claude credentials before resuming: export a valid ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN, then run agentxchain step --resume.';
|
|
318
|
+
|
|
319
|
+
function findRetainedClaudeAuthEscalation(root, state, config) {
|
|
320
|
+
if (!state || state.status !== 'blocked') return null;
|
|
321
|
+
if (state.blocked_reason?.category !== 'retries_exhausted') return null;
|
|
322
|
+
if (typeof state.blocked_on !== 'string' || !state.blocked_on.startsWith('escalation:retries-exhausted:')) {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
const turnId = state.blocked_reason?.turn_id || state.escalation?.from_turn_id || null;
|
|
326
|
+
const activeTurns = state.active_turns || {};
|
|
327
|
+
const candidateIds = turnId && activeTurns[turnId] ? [turnId] : Object.keys(activeTurns);
|
|
328
|
+
for (const candidateId of candidateIds) {
|
|
329
|
+
const turn = activeTurns[candidateId];
|
|
330
|
+
if (!turn || turn.status !== 'failed') continue;
|
|
331
|
+
if (turn.last_rejection?.failed_stage !== 'dispatch') continue;
|
|
332
|
+
const runtime = config?.runtimes?.[turn.runtime_id];
|
|
333
|
+
if (!isClaudeLocalCliRuntime(runtime)) continue;
|
|
334
|
+
const stagingPath = join(root, getTurnStagingResultPath(candidateId));
|
|
335
|
+
if (existsSync(stagingPath)) continue;
|
|
336
|
+
const logPath = join(root, getDispatchLogPath(candidateId));
|
|
337
|
+
if (!existsSync(logPath)) continue;
|
|
338
|
+
let logText = '';
|
|
339
|
+
try {
|
|
340
|
+
logText = readFileSync(logPath, 'utf8');
|
|
341
|
+
} catch {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (!hasClaudeAuthenticationFailureText(logText)) continue;
|
|
345
|
+
return { turn_id: candidateId, turn, previous_blocked_on: state.blocked_on };
|
|
346
|
+
}
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function maybeReclassifyRetainedClaudeAuthEscalation(context, session, state, log = console.log) {
|
|
351
|
+
const { root, config } = context;
|
|
352
|
+
const candidate = findRetainedClaudeAuthEscalation(root, state, config);
|
|
353
|
+
if (!candidate) return null;
|
|
354
|
+
|
|
355
|
+
const blockedAt = new Date().toISOString();
|
|
356
|
+
const nextState = {
|
|
357
|
+
...state,
|
|
358
|
+
status: 'blocked',
|
|
359
|
+
blocked_on: 'dispatch:claude_auth_failed',
|
|
360
|
+
blocked_reason: {
|
|
361
|
+
category: 'dispatch_error',
|
|
362
|
+
blocked_at: blockedAt,
|
|
363
|
+
turn_id: candidate.turn_id,
|
|
364
|
+
reclassified_from: {
|
|
365
|
+
blocked_on: state.blocked_on || null,
|
|
366
|
+
category: state.blocked_reason?.category || null,
|
|
367
|
+
},
|
|
368
|
+
recovery: {
|
|
369
|
+
typed_reason: 'dispatch_error',
|
|
370
|
+
owner: 'human',
|
|
371
|
+
recovery_action: CLAUDE_AUTH_RECOVERY_ACTION,
|
|
372
|
+
turn_retained: true,
|
|
373
|
+
detail: `claude_auth_failed: ${CLAUDE_AUTH_RECOVERY_ACTION}`,
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
escalation: null,
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
writeGovernedState(root, nextState);
|
|
380
|
+
emitRunEvent(root, 'retained_claude_auth_escalation_reclassified', {
|
|
381
|
+
run_id: nextState.run_id || session.current_run_id || null,
|
|
382
|
+
phase: nextState.phase || null,
|
|
383
|
+
status: 'blocked',
|
|
384
|
+
turn: { turn_id: candidate.turn_id, role_id: candidate.turn.assigned_role || null },
|
|
385
|
+
payload: {
|
|
386
|
+
turn_id: candidate.turn_id,
|
|
387
|
+
previous_blocked_on: candidate.previous_blocked_on,
|
|
388
|
+
blocked_on: nextState.blocked_on,
|
|
389
|
+
runtime_id: candidate.turn.runtime_id || null,
|
|
390
|
+
recovery_action: CLAUDE_AUTH_RECOVERY_ACTION,
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
log(`Reclassified retained Claude auth escalation for ${candidate.turn_id} as dispatch:claude_auth_failed.`);
|
|
394
|
+
return nextState;
|
|
395
|
+
}
|
|
396
|
+
|
|
312
397
|
const RECOVERABLE_ACTIVE_TURN_STATUSES = new Set(['assigned', 'dispatched', 'starting', 'running']);
|
|
313
398
|
|
|
314
399
|
function hasOnlyRecoverableActiveTurns(activeTurns = {}) {
|
|
@@ -1714,7 +1799,9 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
|
|
|
1714
1799
|
|
|
1715
1800
|
const startupGovernedState = loadProjectState(root, context.config);
|
|
1716
1801
|
if (startupGovernedState?.status === 'blocked') {
|
|
1717
|
-
const
|
|
1802
|
+
const reclassifiedState = maybeReclassifyRetainedClaudeAuthEscalation(context, session, startupGovernedState, log);
|
|
1803
|
+
const effectiveBlockedState = reclassifiedState || startupGovernedState;
|
|
1804
|
+
const retried = await maybeAutoRetryContinuousBlocker(context, session, contOpts, effectiveBlockedState, log);
|
|
1718
1805
|
if (retried) return retried;
|
|
1719
1806
|
session.status = 'paused';
|
|
1720
1807
|
writeContinuousSession(root, session);
|
|
@@ -1722,9 +1809,9 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
|
|
|
1722
1809
|
ok: true,
|
|
1723
1810
|
status: 'blocked',
|
|
1724
1811
|
action: 'still_blocked',
|
|
1725
|
-
run_id: session.current_run_id ||
|
|
1726
|
-
recovery_action: getBlockedRecoveryAction(
|
|
1727
|
-
blocked_category: getBlockedCategory(
|
|
1812
|
+
run_id: session.current_run_id || effectiveBlockedState.run_id || null,
|
|
1813
|
+
recovery_action: getBlockedRecoveryAction(effectiveBlockedState),
|
|
1814
|
+
blocked_category: getBlockedCategory(effectiveBlockedState),
|
|
1728
1815
|
};
|
|
1729
1816
|
}
|
|
1730
1817
|
|
package/src/lib/run-events.js
CHANGED
|
@@ -48,6 +48,7 @@ export const VALID_RUN_EVENTS = [
|
|
|
48
48
|
'ghost_retry_exhausted',
|
|
49
49
|
'auto_retried_productive_timeout',
|
|
50
50
|
'productive_timeout_retry_exhausted',
|
|
51
|
+
'retained_claude_auth_escalation_reclassified',
|
|
51
52
|
'state_reconciled_operator_commits',
|
|
52
53
|
'operator_commit_reconcile_refused',
|
|
53
54
|
'charter_materialization_required',
|
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
|
+
}
|