@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.
- package/dist/agent-auth.cjs +76 -0
- package/dist/completion-retry.cjs +48 -0
- package/dist/exit-conditions.cjs +23 -0
- package/dist/session.cjs +17 -4
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/dist/exit-conditions.cjs
CHANGED
|
@@ -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