@sagentlab/navarch-runtime 0.1.122 → 0.1.123

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,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.detectAgentModelUnavailable = detectAgentModelUnavailable;
4
+ /**
5
+ * OpenCode's words for "no configured provider serves this model": its JSON
6
+ * error event (`{"type":"error","error":{"type":"provider.no-route",
7
+ * "message":"Model unavailable: opencode-go/deepseek-v4.1-flash"}}`) and the
8
+ * matching ERROR log line (`"Failed to drain Session" cause
9
+ * "SessionRunnerModel.ModelUnavailableError: Model unavailable: ..."`).
10
+ * Verified against OpenCode 2.0.29 on flotilla-agent-1435 (2026-10-19).
11
+ */
12
+ const NO_ROUTE_PATTERN = /\bprovider\.no-route\b|\bModelUnavailableError\b|\bModel unavailable:/i;
13
+ const MODEL_NAME_PATTERN = /\bModel unavailable:\s*([^\s"'\\,;)]+)/i;
14
+ function clip(text) {
15
+ const trimmed = text.trim().replace(/\s+/g, " ");
16
+ return trimmed.length > 300 ? `${trimmed.slice(0, 299)}…` : trimmed;
17
+ }
18
+ function failure(detail) {
19
+ const model = detail.match(MODEL_NAME_PATTERN)?.[1]?.replace(/[.:]+$/, "") ?? null;
20
+ return { model, detail: clip(detail) };
21
+ }
22
+ /** Messages of the CLI's own error events on stdout; never assistant text events. */
23
+ function errorEventMessages(stdout) {
24
+ const messages = [];
25
+ for (const line of stdout.split(/\r?\n/)) {
26
+ if (!line.trim().startsWith("{"))
27
+ continue;
28
+ let event;
29
+ try {
30
+ event = JSON.parse(line);
31
+ }
32
+ catch {
33
+ continue;
34
+ }
35
+ if (event.type !== "error" && event.type !== "session.error")
36
+ continue;
37
+ const properties = event.properties;
38
+ const error = (event.error ?? properties?.error);
39
+ if (typeof error === "string") {
40
+ messages.push(error);
41
+ continue;
42
+ }
43
+ const message = error?.data?.message ?? error?.message;
44
+ const kind = [error?.type, error?.name].filter((value) => typeof value === "string").join(" ");
45
+ const text = [kind, typeof message === "string" ? message : ""].filter(Boolean).join(": ");
46
+ if (text)
47
+ messages.push(text);
48
+ }
49
+ return messages;
50
+ }
51
+ /**
52
+ * Detects a turn the coding agent CLI could not start because the model it
53
+ * was told to use has no route on this machine (OpenCode `provider.no-route`).
54
+ * Like a dead login (agent-auth.cts), every task the machine claims then fails
55
+ * the same way within seconds, so the control plane requeues the task without
56
+ * spending a retry and pauses the machine until the model is fixed.
57
+ *
58
+ * Matched only against CLI-authored channels — OpenCode's error events and
59
+ * ERROR log lines — and only for a turn that failed or produced no report, so
60
+ * an agent writing about this very error cannot pause its own machine.
61
+ */
62
+ function detectAgentModelUnavailable(result) {
63
+ if (result.killedByLeaseLoss || result.timedOut || result.capacityError || result.authError)
64
+ return null;
65
+ if (result.exitCode === 0 && result.reportText)
66
+ return null;
67
+ for (const message of errorEventMessages(result.stdout)) {
68
+ if (NO_ROUTE_PATTERN.test(message))
69
+ return failure(message);
70
+ }
71
+ if (result.exitCode === 0)
72
+ return null;
73
+ for (const line of result.stderr.split(/\r?\n/)) {
74
+ if (/ERROR/.test(line) && NO_ROUTE_PATTERN.test(line)) {
75
+ const cause = line.match(/Model unavailable:[^"\n]*/i)?.[0];
76
+ return failure(cause ?? line);
77
+ }
78
+ }
79
+ return null;
80
+ }
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AGENT_AUTH_FAILURE_PREFIX = void 0;
3
+ exports.AGENT_MODEL_UNAVAILABLE_PREFIX = exports.AGENT_AUTH_FAILURE_PREFIX = void 0;
4
4
  exports.agentAuthFailureReport = agentAuthFailureReport;
5
+ exports.agentModelUnavailableReport = agentModelUnavailableReport;
5
6
  exports.parseClaudeJsonResult = parseClaudeJsonResult;
6
7
  exports.extractUsageFromClaudeJson = extractUsageFromClaudeJson;
7
8
  exports.parseCodexJsonEvents = parseCodexJsonEvents;
@@ -10,6 +11,7 @@ exports.extractFinalMessageFromCodexEvents = extractFinalMessageFromCodexEvents;
10
11
  exports.leadingVerificationVerdict = leadingVerificationVerdict;
11
12
  exports.leadWithReviewFixMarker = leadWithReviewFixMarker;
12
13
  exports.leadWithReviewAttestation = leadWithReviewAttestation;
14
+ exports.failureOutputText = failureOutputText;
13
15
  exports.mapExitCondition = mapExitCondition;
14
16
  /**
15
17
  * Leading text of every report for a turn the coding agent CLI refused because
@@ -24,6 +26,19 @@ function agentAuthFailureReport(detail) {
24
26
  return `${exports.AGENT_AUTH_FAILURE_PREFIX}: ${detail} ` +
25
27
  "Log the coding agent in again on this machine (`claude /login`, `codex login`, an interactive `agy` session, or open `muse` and run `/login`), then resume the agent.";
26
28
  }
29
+ /**
30
+ * Leading text of every report for a turn the coding agent CLI could not start
31
+ * because the model it was given has no route on this machine (agent-model.cts,
32
+ * OpenCode `provider.no-route`). The control plane recognizes it and, as for a
33
+ * dead login, requeues the task without spending a retry while pausing this
34
+ * machine's claims (lib/navarch/failure-attribution.ts
35
+ * `provider.model_unavailable`).
36
+ */
37
+ exports.AGENT_MODEL_UNAVAILABLE_PREFIX = "Coding agent model unavailable";
38
+ function agentModelUnavailableReport(detail) {
39
+ return `${exports.AGENT_MODEL_UNAVAILABLE_PREFIX}: ${detail} ` +
40
+ "Make that model reachable from the coding agent on this machine (log in to its provider, e.g. `opencode auth login`, or update the CLI), or connect the agent with a model it can reach, then resume the agent.";
41
+ }
27
42
  /**
28
43
  * Best-effort parse of `claude -p --output-format json` (or `stream-json`,
29
44
  * whose last result record has the same shape) stdout into the assumed
@@ -298,6 +313,47 @@ function declaresBuildFailure(report) {
298
313
  const firstLine = report.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? "";
299
314
  return /^(?:Build|Task) outcome:[*_`~]*\s*[*_`~]*(?:FAILED|BLOCKED)(?![a-z0-9_])/i.test(firstLine.replace(/^(?:#{1,6}\s+|>\s*|[*_`~]+\s*)+/, ""));
300
315
  }
316
+ /**
317
+ * A CLI-authored error line: a JSON error event (OpenCode/Codex `error`,
318
+ * `session.error`, `turn.failed`), an ERROR/FATAL log line, or a leading
319
+ * `error:`/`fatal:`. Case-sensitive on the log levels so prose saying "error"
320
+ * stays where it was.
321
+ */
322
+ function isFailureLine(line) {
323
+ return /(?:^|[^A-Za-z])(?:ERROR|FATAL)(?:[^A-Za-z]|$)/.test(line) ||
324
+ /"type"\s*:\s*"(?:error|session\.error|turn\.failed)"|^\s*(?:error|fatal)\b\s*[:\[]/i.test(line);
325
+ }
326
+ /** A JSON error event on stdout; text and tool events never qualify, whatever they say. */
327
+ function isErrorEventLine(line) {
328
+ if (!line.startsWith("{"))
329
+ return false;
330
+ try {
331
+ const type = JSON.parse(line)?.type;
332
+ return type === "error" || type === "session.error" || type === "turn.failed";
333
+ }
334
+ catch {
335
+ return false;
336
+ }
337
+ }
338
+ /**
339
+ * The failure output of a CLI that exited badly, error lines first. OpenCode
340
+ * prints its INFO startup log ("cli starting ... serve --stdio") ahead of the
341
+ * real error and puts its JSON error event on stdout, so summarizing stderr
342
+ * from the top showed only the INFO lines on the board (2026-10-19). The JSON
343
+ * error events on stdout are CLI-authored, unlike its text events, so they are
344
+ * safe to lift into the summary.
345
+ */
346
+ function failureOutputText(stdout, stderr) {
347
+ const stdoutErrors = stdout
348
+ .split(/\r?\n/)
349
+ .map((line) => line.trim())
350
+ .filter(isErrorEventLine);
351
+ const lines = [...stdoutErrors, ...stderr.split(/\r?\n/)].map((line) => line.trim()).filter(Boolean);
352
+ const errors = lines.filter(isFailureLine);
353
+ if (errors.length === 0)
354
+ return stderr;
355
+ return [...errors, ...lines.filter((line) => !isFailureLine(line))].join("\n");
356
+ }
301
357
  function summarize(text, maxLen = 500) {
302
358
  const trimmed = text.trim();
303
359
  if (!trimmed)
@@ -344,6 +400,14 @@ function mapExitCondition(result) {
344
400
  evidenceUrls,
345
401
  };
346
402
  }
403
+ if (result.modelUnavailableError) {
404
+ return {
405
+ leaseOutcome: "failed",
406
+ exitStatus: "failed",
407
+ reportSummary: summarize(agentModelUnavailableReport(result.modelUnavailableError)),
408
+ evidenceUrls,
409
+ };
410
+ }
347
411
  if (result.timedOut) {
348
412
  return {
349
413
  leaseOutcome: "failed",
@@ -362,7 +426,7 @@ function mapExitCondition(result) {
362
426
  return {
363
427
  leaseOutcome: "failed",
364
428
  exitStatus: "crashed",
365
- reportSummary: summarize(result.stderr) || "Adapter process crashed before exiting.",
429
+ reportSummary: summarize(failureOutputText(result.stdout, result.stderr)) || "Adapter process crashed before exiting.",
366
430
  evidenceUrls,
367
431
  };
368
432
  }
@@ -373,7 +437,7 @@ function mapExitCondition(result) {
373
437
  return {
374
438
  leaseOutcome: "failed",
375
439
  exitStatus: "failed",
376
- reportSummary: summarize(parsedJson?.result || result.stderr || result.stdout) ||
440
+ reportSummary: summarize(parsedJson?.result || failureOutputText(result.stdout, result.stderr) || result.stdout) ||
377
441
  `Adapter exited with code ${result.exitCode}.`,
378
442
  evidenceUrls,
379
443
  };
package/dist/session.cjs CHANGED
@@ -31,6 +31,7 @@ const lease_heartbeat_cjs_1 = require("./lease-heartbeat.cjs");
31
31
  const completion_retry_cjs_1 = require("./completion-retry.cjs");
32
32
  const adapter_capacity_cjs_1 = require("./adapter-capacity.cjs");
33
33
  const agent_auth_cjs_1 = require("./agent-auth.cjs");
34
+ const agent_model_cjs_1 = require("./agent-model.cjs");
34
35
  const browser_evidence_cjs_1 = require("./browser-evidence.cjs");
35
36
  /** Filename the generated platform MCP config is written under inside the session metadata directory. */
36
37
  const MCP_CONFIG_FILENAME = "mcp-config.json";
@@ -691,6 +692,13 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
691
692
  turnResult.authError = authFailure.detail;
692
693
  log.warn(`session ${leaseId}: coding agent authentication failed: ${authFailure.detail}`);
693
694
  }
695
+ // A model the CLI has no route to (OpenCode `provider.no-route`) fails
696
+ // every task the same way, so it gets the same treatment as a dead login.
697
+ const modelUnavailable = capacityLimit || authFailure ? null : (0, agent_model_cjs_1.detectAgentModelUnavailable)(turnResult);
698
+ if (modelUnavailable) {
699
+ turnResult.modelUnavailableError = modelUnavailable.detail;
700
+ log.warn(`session ${leaseId}: coding agent model unavailable: ${modelUnavailable.detail}`);
701
+ }
694
702
  // Close the small race between a naturally completed turn and the next
695
703
  // scheduled heartbeat. If guidance landed, run another turn before the
696
704
  // lease can be completed. A terminal heartbeat response means the
@@ -703,7 +711,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
703
711
  log.warn(`session ${leaseId} stopped without completion because its lease is no longer active.`);
704
712
  return sessionOutcome;
705
713
  }
706
- if (!capacityLimit && !authFailure && !leaseLost && !turnResult.timedOut && pendingGuidance.length > 0) {
714
+ if (!capacityLimit && !authFailure && !modelUnavailable && !leaseLost && !turnResult.timedOut && pendingGuidance.length > 0) {
707
715
  // Muse cancellation kills the whole task-owned container. Guidance
708
716
  // still holds the lease, so restart that SAME container and restore
709
717
  // its tmpfs environment before the next turn; never launch into a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sagentlab/navarch-runtime",
3
- "version": "0.1.122",
3
+ "version": "0.1.123",
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",