@sagentlab/navarch-runtime 0.1.75 → 0.1.77

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.
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.detectAgentAuthFailure = detectAgentAuthFailure;
4
+ const exit_conditions_cjs_1 = require("./exit-conditions.cjs");
5
+ /**
6
+ * Errors the coding agent CLIs print when their stored login cannot be used.
7
+ * Matched only against CLI-authored error channels (a structured error result,
8
+ * Codex error events, stderr), never the agent's own prose, so a task about a
9
+ * login page cannot pause the machine.
10
+ *
11
+ * - Claude Code: "Failed to authenticate: OAuth session expired and could not
12
+ * be refreshed", "OAuth token has expired", "Invalid API key · Please run
13
+ * /login", "Not logged in · Please run /login", 401 authentication_error.
14
+ * - Codex: "Your access token could not be refreshed because your refresh
15
+ * token has expired/was already used", "Not logged in", "Please run `codex
16
+ * login`", token_expired / refresh_token_* error codes.
17
+ */
18
+ const AGENT_AUTH_PATTERNS = [
19
+ /\bfailed to authenticate\b/i,
20
+ /\boauth (?:session|token) (?:has )?expired\b/i,
21
+ /\bplease run \/login\b/i,
22
+ /\bnot logged in\b/i,
23
+ /\bcodex login\b/i,
24
+ /\baccess token could not be refreshed\b/i,
25
+ /\brefresh token (?:has expired|was already used|is invalid|expired)\b/i,
26
+ /\b(?:refresh_token_(?:expired|reused|invalidated)|token_expired)\b/i,
27
+ /\bauthentication_error\b/i,
28
+ ];
29
+ function matchingLine(text) {
30
+ for (const line of text.split("\n")) {
31
+ const trimmed = line.trim();
32
+ if (trimmed && AGENT_AUTH_PATTERNS.some((pattern) => pattern.test(trimmed))) {
33
+ return trimmed.length > 300 ? `${trimmed.slice(0, 299)}…` : trimmed;
34
+ }
35
+ }
36
+ return null;
37
+ }
38
+ /**
39
+ * Detects a turn that ended because the coding agent CLI could not
40
+ * authenticate. Only failed turns qualify: a clean exit whose structured result
41
+ * is not an error did real work, whatever its transcript mentions.
42
+ */
43
+ function detectAgentAuthFailure(result) {
44
+ if (result.killedByLeaseLoss || result.timedOut || result.capacityError)
45
+ return null;
46
+ const claude = (0, exit_conditions_cjs_1.parseClaudeJsonResult)(result.stdout) ?? (0, exit_conditions_cjs_1.parseClaudeJsonResult)(result.stderr);
47
+ if (claude) {
48
+ if (!claude.is_error && result.exitCode === 0)
49
+ return null;
50
+ if (claude.is_error && typeof claude.result === "string") {
51
+ const detail = matchingLine(claude.result);
52
+ if (detail)
53
+ return { detail };
54
+ }
55
+ }
56
+ const codexErrors = (0, exit_conditions_cjs_1.parseCodexJsonEvents)(result.stdout).flatMap((event) => {
57
+ const raw = event;
58
+ if (raw.type === "error" && typeof raw.message === "string")
59
+ return [raw.message];
60
+ if (raw.type === "turn.failed") {
61
+ const error = raw.error;
62
+ if (typeof error?.message === "string")
63
+ return [error.message];
64
+ }
65
+ return [];
66
+ });
67
+ if (result.exitCode === 0 && codexErrors.length === 0 && !claude?.is_error)
68
+ return null;
69
+ for (const message of codexErrors) {
70
+ const detail = matchingLine(message);
71
+ if (detail)
72
+ return { detail };
73
+ }
74
+ const detail = matchingLine(result.stderr);
75
+ return detail ? { detail } : null;
76
+ }
@@ -1,5 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AGENT_AUTH_FAILURE_PREFIX = void 0;
4
+ exports.agentAuthFailureReport = agentAuthFailureReport;
3
5
  exports.parseClaudeJsonResult = parseClaudeJsonResult;
4
6
  exports.extractUsageFromClaudeJson = extractUsageFromClaudeJson;
5
7
  exports.parseCodexJsonEvents = parseCodexJsonEvents;
@@ -8,6 +10,19 @@ exports.extractFinalMessageFromCodexEvents = extractFinalMessageFromCodexEvents;
8
10
  exports.leadingVerificationVerdict = leadingVerificationVerdict;
9
11
  exports.leadWithReviewFixMarker = leadWithReviewFixMarker;
10
12
  exports.mapExitCondition = mapExitCondition;
13
+ /**
14
+ * Leading text of every report for a turn the coding agent CLI refused because
15
+ * its own login on this machine is gone (agent-auth.cts). The control plane
16
+ * recognizes it -- and the raw CLI errors, for older runtimes -- and requeues
17
+ * the task without spending a retry while pausing this machine's claims until
18
+ * someone logs in again (lib/navarch/failure-attribution.ts
19
+ * `provider.agent_auth`).
20
+ */
21
+ exports.AGENT_AUTH_FAILURE_PREFIX = "Coding agent authentication failed";
22
+ function agentAuthFailureReport(detail) {
23
+ return `${exports.AGENT_AUTH_FAILURE_PREFIX}: ${detail} ` +
24
+ "Log the coding agent in again on this machine (`claude /login` or `codex login`), then resume the agent.";
25
+ }
11
26
  /**
12
27
  * Best-effort parse of `claude -p --output-format json` stdout into the
13
28
  * assumed result shape above. Tries the whole trimmed stdout first (the
@@ -260,6 +275,14 @@ function mapExitCondition(result) {
260
275
  evidenceUrls,
261
276
  };
262
277
  }
278
+ if (result.authError) {
279
+ return {
280
+ leaseOutcome: "failed",
281
+ exitStatus: "failed",
282
+ reportSummary: summarize(agentAuthFailureReport(result.authError)),
283
+ evidenceUrls,
284
+ };
285
+ }
263
286
  if (result.timedOut) {
264
287
  return {
265
288
  leaseOutcome: "failed",
package/dist/session.cjs CHANGED
@@ -26,6 +26,7 @@ const worktree_guard_cjs_1 = require("./worktree-guard.cjs");
26
26
  const adapter_watchdog_cjs_1 = require("./adapter-watchdog.cjs");
27
27
  const lease_heartbeat_cjs_1 = require("./lease-heartbeat.cjs");
28
28
  const adapter_capacity_cjs_1 = require("./adapter-capacity.cjs");
29
+ const agent_auth_cjs_1 = require("./agent-auth.cjs");
29
30
  const browser_evidence_cjs_1 = require("./browser-evidence.cjs");
30
31
  /** Filename the generated platform MCP config is written under inside the session metadata directory. */
31
32
  const MCP_CONFIG_FILENAME = "mcp-config.json";
@@ -551,6 +552,15 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
551
552
  await checkpointForRecovery(turnResult);
552
553
  sessionOutcome.claimCooldownUntil = Math.max(sessionOutcome.claimCooldownUntil ?? 0, capacityLimit.retryAtMs);
553
554
  }
555
+ // A dead CLI login fails every task this machine claims in well under a
556
+ // second. Report it as such so the control plane requeues the task
557
+ // without spending a retry and pauses this machine until it is logged in
558
+ // again, instead of burning the task's budget into needs_human.
559
+ const authFailure = capacityLimit ? null : (0, agent_auth_cjs_1.detectAgentAuthFailure)(turnResult);
560
+ if (authFailure) {
561
+ turnResult.authError = authFailure.detail;
562
+ log.warn(`session ${leaseId}: coding agent authentication failed: ${authFailure.detail}`);
563
+ }
554
564
  // Close the small race between a naturally completed turn and the next
555
565
  // scheduled heartbeat. If guidance landed, run another turn before the
556
566
  // lease can be completed. A terminal heartbeat response means the
@@ -563,7 +573,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
563
573
  log.warn(`session ${leaseId} stopped without completion because its lease is no longer active.`);
564
574
  return sessionOutcome;
565
575
  }
566
- if (!capacityLimit && !leaseLost && !turnResult.timedOut && pendingGuidance.length > 0)
576
+ if (!capacityLimit && !authFailure && !leaseLost && !turnResult.timedOut && pendingGuidance.length > 0)
567
577
  continue;
568
578
  const result = {
569
579
  ...turnResult,
@@ -1,16 +1,26 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.RuntimeUpdateCoordinator = void 0;
3
+ exports.RuntimeUpdateCoordinator = exports.UPDATE_RETRY_MAX_MS = exports.UPDATE_RETRY_BASE_MS = void 0;
4
4
  const logger_cjs_1 = require("./logger.cjs");
5
5
  const update_installer_cjs_1 = require("./update-installer.cjs");
6
6
  const version_cjs_1 = require("./version.cjs");
7
7
  const supervisor_cjs_1 = require("./supervisor.cjs");
8
8
  const log = (0, logger_cjs_1.createLogger)("update");
9
+ // A failed stage reports `failed`, which sends a heartbeat at once, whose
10
+ // response repeats the same directive. Without a pause that loop re-runs
11
+ // `npm pack` several times a second (seen while npm's CDN still served the
12
+ // pre-publish packument for a freshly rolled-out version).
13
+ exports.UPDATE_RETRY_BASE_MS = 30_000;
14
+ exports.UPDATE_RETRY_MAX_MS = 10 * 60_000;
15
+ function directiveKey(directive) {
16
+ return `${directive.target_version}\0${directive.integrity}`;
17
+ }
9
18
  class RuntimeUpdateCoordinator {
10
19
  options;
11
20
  targetInProgress = null;
12
21
  restartInProgress = null;
13
22
  warnedUnsupervised = false;
23
+ lastFailure = null;
14
24
  constructor(options) {
15
25
  this.options = options;
16
26
  }
@@ -26,6 +36,9 @@ class RuntimeUpdateCoordinator {
26
36
  }
27
37
  if (this.targetInProgress)
28
38
  return;
39
+ const key = directiveKey(directive);
40
+ if (this.lastFailure?.key === key && this.now() < this.lastFailure.retryAt)
41
+ return;
29
42
  this.targetInProgress = directive.target_version;
30
43
  void this.apply(directive);
31
44
  }
@@ -57,6 +70,9 @@ class RuntimeUpdateCoordinator {
57
70
  this.restartInProgress = null;
58
71
  }
59
72
  }
73
+ now() {
74
+ return (this.options.now ?? Date.now)();
75
+ }
60
76
  async apply(directive) {
61
77
  const { config, heartbeat, claimLoop, capacity } = this.options;
62
78
  try {
@@ -73,7 +89,11 @@ class RuntimeUpdateCoordinator {
73
89
  }
74
90
  catch (error) {
75
91
  const message = error instanceof Error ? error.message : String(error);
76
- log.error(`runtime update failed: ${message}`);
92
+ const key = directiveKey(directive);
93
+ const failures = this.lastFailure?.key === key ? this.lastFailure.failures + 1 : 1;
94
+ const delayMs = Math.min(exports.UPDATE_RETRY_BASE_MS * 2 ** (failures - 1), exports.UPDATE_RETRY_MAX_MS);
95
+ this.lastFailure = { key, failures, retryAt: this.now() + delayMs };
96
+ log.error(`runtime update failed: ${message} (retrying ${directive.target_version} in ${Math.round(delayMs / 1000)}s)`);
77
97
  heartbeat.setUpdateState("failed", message.slice(0, 500));
78
98
  claimLoop.start();
79
99
  this.targetInProgress = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sagentlab/navarch-runtime",
3
- "version": "0.1.75",
3
+ "version": "0.1.77",
4
4
  "description": "Navarch machine-side session manager: claims delivery tasks and runs them through local coding agents.",
5
5
  "type": "commonjs",
6
6
  "license": "MIT",