@sagentlab/navarch-runtime 0.1.76 → 0.1.78

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
+ }
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.COMPLETION_TRANSIENT_RETRY_DELAYS_MS = void 0;
4
+ exports.isTransientCompletionError = isTransientCompletionError;
5
+ exports.completeWithTransientRetry = completeWithTransientRetry;
6
+ const api_cjs_1 = require("./api.cjs");
7
+ /**
8
+ * Backoff before each re-send of a completion that failed in transit. About
9
+ * two minutes in total: long enough to ride out a Wi-Fi drop or an edge blip,
10
+ * short enough that the lease is still live when the report finally lands.
11
+ */
12
+ exports.COMPLETION_TRANSIENT_RETRY_DELAYS_MS = [2_000, 5_000, 15_000, 30_000, 60_000];
13
+ /** Statuses the edge or a gateway answers for an unreachable origin; the completion route never chose them. */
14
+ const TRANSIENT_STATUSES = new Set([408, 429, 502, 503, 504, 520, 521, 522, 523, 524]);
15
+ /**
16
+ * A failure that says nothing about the completion itself: the request never
17
+ * got an answer from the completion route. A plain 500 is the route's own
18
+ * answer and keeps the existing crash path.
19
+ */
20
+ function isTransientCompletionError(err) {
21
+ return (err instanceof api_cjs_1.NavarchTransportError ||
22
+ (err instanceof api_cjs_1.NavarchApiError && TRANSIENT_STATUSES.has(err.status)));
23
+ }
24
+ /**
25
+ * Sends a lease completion, re-sending it while the control plane is
26
+ * unreachable. Without this, one dropped connection turned a finished session
27
+ * (pull request already open) into `Session crashed:`, which spent an
28
+ * infrastructure retry and discarded the report and evidence the review loop
29
+ * needed (carova#310). A re-send whose earlier copy did land gets the
30
+ * `already_released` 409, which callers already treat as success.
31
+ */
32
+ async function completeWithTransientRetry(send, options = {}) {
33
+ const delays = options.delaysMs ?? exports.COMPLETION_TRANSIENT_RETRY_DELAYS_MS;
34
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
35
+ for (let attempt = 0;; attempt += 1) {
36
+ try {
37
+ await send();
38
+ return;
39
+ }
40
+ catch (err) {
41
+ const delayMs = delays[attempt];
42
+ if (delayMs === undefined || !isTransientCompletionError(err))
43
+ throw err;
44
+ options.onRetry?.(attempt + 1, delayMs, err);
45
+ await sleep(delayMs);
46
+ }
47
+ }
48
+ }
@@ -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
@@ -25,7 +25,9 @@ const delivery_cjs_1 = require("./delivery.cjs");
25
25
  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
+ const completion_retry_cjs_1 = require("./completion-retry.cjs");
28
29
  const adapter_capacity_cjs_1 = require("./adapter-capacity.cjs");
30
+ const agent_auth_cjs_1 = require("./agent-auth.cjs");
29
31
  const browser_evidence_cjs_1 = require("./browser-evidence.cjs");
30
32
  /** Filename the generated platform MCP config is written under inside the session metadata directory. */
31
33
  const MCP_CONFIG_FILENAME = "mcp-config.json";
@@ -551,6 +553,15 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
551
553
  await checkpointForRecovery(turnResult);
552
554
  sessionOutcome.claimCooldownUntil = Math.max(sessionOutcome.claimCooldownUntil ?? 0, capacityLimit.retryAtMs);
553
555
  }
556
+ // A dead CLI login fails every task this machine claims in well under a
557
+ // second. Report it as such so the control plane requeues the task
558
+ // without spending a retry and pauses this machine until it is logged in
559
+ // again, instead of burning the task's budget into needs_human.
560
+ const authFailure = capacityLimit ? null : (0, agent_auth_cjs_1.detectAgentAuthFailure)(turnResult);
561
+ if (authFailure) {
562
+ turnResult.authError = authFailure.detail;
563
+ log.warn(`session ${leaseId}: coding agent authentication failed: ${authFailure.detail}`);
564
+ }
554
565
  // Close the small race between a naturally completed turn and the next
555
566
  // scheduled heartbeat. If guidance landed, run another turn before the
556
567
  // lease can be completed. A terminal heartbeat response means the
@@ -563,7 +574,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
563
574
  log.warn(`session ${leaseId} stopped without completion because its lease is no longer active.`);
564
575
  return sessionOutcome;
565
576
  }
566
- if (!capacityLimit && !leaseLost && !turnResult.timedOut && pendingGuidance.length > 0)
577
+ if (!capacityLimit && !authFailure && !leaseLost && !turnResult.timedOut && pendingGuidance.length > 0)
567
578
  continue;
568
579
  const result = {
569
580
  ...turnResult,
@@ -720,7 +731,9 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
720
731
  transcript_url: completion.transcript_url,
721
732
  };
722
733
  try {
723
- await api.completeLease(leaseId, completion);
734
+ await (0, completion_retry_cjs_1.completeWithTransientRetry)(() => api.completeLease(leaseId, completion), {
735
+ onRetry: (attempt, delayMs, err) => log.warn(`completion for ${leaseId} did not reach the control plane (${(0, redact_cjs_1.redactText)(String(err), knownSecrets)}); re-sending in ${delayMs / 1000}s (${attempt}).`),
736
+ });
724
737
  if (deliveredPullRequest && completion.status === "completed" && gitWorktree.restoredHostedCheckpoint) {
725
738
  // The pull request carries the hosted attempt's work now; a later
726
739
  // hosted attempt must not resume the stale snapshot.
@@ -756,7 +769,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
756
769
  log.error(`completion for ${leaseId} was rejected as ${rejection.code}, which is not remediable; failing the lease.`);
757
770
  }
758
771
  try {
759
- await api.completeLease(leaseId, {
772
+ await (0, completion_retry_cjs_1.completeWithTransientRetry)(() => api.completeLease(leaseId, {
760
773
  ...completion,
761
774
  status: "failed",
762
775
  report: remediable
@@ -764,7 +777,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
764
777
  : `${redactedRejection}\n\n---\n\n${completion.report}`,
765
778
  failure_summary: redactedRejection,
766
779
  exit_status: "failed",
767
- });
780
+ }));
768
781
  }
769
782
  catch (fallbackErr) {
770
783
  // Same race as the primary completion above: if something else released
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sagentlab/navarch-runtime",
3
- "version": "0.1.76",
3
+ "version": "0.1.78",
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",