@sagentlab/navarch-runtime 0.1.145 → 0.1.147

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.
@@ -431,7 +431,11 @@ const LITERAL_PATTERN_COMMANDS = new Set([...CHAINABLE_COMMANDS, "sed", "awk", "
431
431
  */
432
432
  const PROGRAM_COMMANDS = new Set(["bun", "deno", "node", "python", "python3", "tsx"]);
433
433
 
434
- const PROGRAM_SHELL_RE = /system|subprocess|popen|exec|spawn|child_process|shell|pty|command|\.run\b|`/i;
434
+ const PROGRAM_SHELL_WORDS_RE = /system|subprocess|popen|exec|spawn|child_process|shell|pty|command|\.run\b/i;
435
+ // A backtick also counts where the shell could expand it. A quoted heredoc
436
+ // body is literal, so there a backtick is only a JavaScript template literal
437
+ // (isProgramFileBody uses PROGRAM_SHELL_WORDS_RE alone).
438
+ const PROGRAM_SHELL_RE = new RegExp(`${PROGRAM_SHELL_WORDS_RE.source}|\``, "i");
435
439
 
436
440
  /**
437
441
  * git and gh run aliases, hooks and extensions through a shell, so only the
@@ -442,6 +446,12 @@ const MESSAGE_SUBCOMMAND_RE =
442
446
  /^\s*(?:git\s+(?:commit|tag|notes|merge)|gh\s+(?:pr|issue|release)\s+(?:create|edit|comment|review|close))\b/;
443
447
  const MESSAGE_FLAG_RE = /\s(?:-m|--message|-t|--title|-b|--body|--notes)(?:=|\s+)$/;
444
448
  const MESSAGE_FILE_RE = /\s(?:--body-file|-F|--file)(?:=|\s)/;
449
+ // git accepts unique long-option prefixes, so `--ed` also opens the editor.
450
+ const EDITOR_FLAG_RE = /\s(?:-e|--ed(?:i(?:t(?:or)?)?)?)(?:[\s=]|$)/;
451
+ // Names a git or husky hook location, where written text runs on a later git command.
452
+ const HOOK_PATH_RE = /\.git\/|\.husky|hooks/i;
453
+ // A git `-c` override or an alias can make a git/gh message command run text.
454
+ const CONFIG_OVERRIDE_RE = /\s-c\s|alias/;
445
455
 
446
456
  const ASSIGNMENTS_RE = /^\s*!?\s*(?:[A-Za-z_]\w*=\S*\s+)*/;
447
457
  const LEADING_REDIRECTS_RE = /^(?:\s*\d*(?:<<?|>>?|&>>?)&?\s*[^\s;|&<>()]+)*\s*/;
@@ -468,7 +478,7 @@ function classifySegment(command, start) {
468
478
  const word = commandWordOf(rest);
469
479
  if (CHAINABLE_COMMANDS.has(word)) return "text";
470
480
  if (PROGRAM_COMMANDS.has(word)) return "active";
471
- if (MESSAGE_SUBCOMMAND_RE.test(rest) && !/\s-c\s|alias/.test(command)) return "active";
481
+ if (MESSAGE_SUBCOMMAND_RE.test(rest) && !CONFIG_OVERRIDE_RE.test(command)) return "active";
472
482
  return "unsafe";
473
483
  }
474
484
 
@@ -488,7 +498,7 @@ function segmentText(command, start) {
488
498
  * (`--body "$(cat <<'EOF' ... )"`).
489
499
  */
490
500
  function exemptionContextIsSafe(command, segments) {
491
- if (/\.git\/|\.husky|hooks/i.test(command)) return false;
501
+ if (HOOK_PATH_RE.test(command)) return false;
492
502
  const classes = segments.map((segment) => ({ ...segment, kind: classifySegment(command, segment.at) }));
493
503
  if (classes.some(({ kind }) => kind === "unsafe")) return false;
494
504
  const active = classes.filter(({ kind }) => kind === "active");
@@ -497,8 +507,7 @@ function exemptionContextIsSafe(command, segments) {
497
507
  const activeText = segmentText(command, active[0].at);
498
508
  if (activeText.replace(ASSIGNMENTS_RE, "") !== activeText.trimStart()) return false;
499
509
  const isMessage = MESSAGE_SUBCOMMAND_RE.test(activeText);
500
- // git accepts unique long-option prefixes, so `--ed` also opens the editor.
501
- if (isMessage && /\s(?:-e|--ed(?:i(?:t(?:or)?)?)?)(?:[\s=]|$)/.test(activeText)) return false;
510
+ if (isMessage && EDITOR_FLAG_RE.test(activeText)) return false;
502
511
  return classes.every(({ at, kind, nested }) => {
503
512
  if (kind === "empty" || kind === "active") return true;
504
513
  if (!isMessage || !nested) return false;
@@ -513,18 +522,21 @@ function isBareRootText(token) {
513
522
  }
514
523
 
515
524
  /**
516
- * Heredoc bodies as [{start, end, word, segment, quoted}], every simple
517
- * command outside them as [{at, nested}] (nested inside `$(`, a backtick or a
518
- * subshell), and `ok`. A small lexer tracks single and double quotes, `$(`,
519
- * backticks, subshells and shell comments to find each `<<` operator and its
520
- * body. Any construct it cannot follow (`$'`, arithmetic `((`, process
525
+ * Heredoc bodies as [{start, end, word, segment, quoted, at, nested}] (`at`
526
+ * is where the body's simple command starts), every simple command outside
527
+ * them as [{at, nested}] (nested inside `$(`, a backtick or a subshell),
528
+ * every single-quoted shell word outside double quotes and heredoc bodies as
529
+ * [{open, close, segmentStart}], and `ok`. A small lexer tracks single and
530
+ * double quotes, `$(`, backticks, subshells and shell comments to find each
531
+ * `<<` operator and its body. Any construct it cannot follow (`$'`, arithmetic `((`, process
521
532
  * substitution `<(`/`>(`, a delimiter with `$` or an unbalanced quote, an
522
533
  * unterminated quote, substitution or heredoc, an unbalanced `)`) returns
523
534
  * `ok: false`, so nothing is exempted on a misread.
524
535
  */
525
536
  function scanHeredocs(command) {
526
- const failed = { ok: false, bodies: [], segments: [] };
537
+ const failed = { ok: false, bodies: [], segments: [], singleQuoted: [] };
527
538
  const bodies = [];
539
+ const singleQuoted = [];
528
540
  const segments = [{ at: 0, nested: false }];
529
541
  const stack = [];
530
542
  let pending = [];
@@ -562,6 +574,7 @@ function scanHeredocs(command) {
562
574
  if (ch === "'") {
563
575
  const close = command.indexOf("'", i + 1);
564
576
  if (close === -1) return failed;
577
+ singleQuoted.push({ open: i, close, segmentStart });
565
578
  i = close + 1;
566
579
  continue;
567
580
  }
@@ -627,6 +640,8 @@ function scanHeredocs(command) {
627
640
  quoted: /['"\\]/.test(word[0]),
628
641
  word: commandWordOf(command.slice(segmentStart, i)),
629
642
  segment: command.slice(segmentStart, lineEnd === -1 ? n : lineEnd),
643
+ at: segmentStart,
644
+ nested: stack.some((frame) => frame !== "dq"),
630
645
  });
631
646
  i = j + word[0].length;
632
647
  continue;
@@ -650,7 +665,15 @@ function scanHeredocs(command) {
650
665
  if (newline === -1) break;
651
666
  }
652
667
  if (end === -1) return failed;
653
- bodies.push({ start, end, word: doc.word, segment: doc.segment, quoted: doc.quoted });
668
+ bodies.push({
669
+ start,
670
+ end,
671
+ word: doc.word,
672
+ segment: doc.segment,
673
+ quoted: doc.quoted,
674
+ at: doc.at,
675
+ nested: doc.nested,
676
+ });
654
677
  }
655
678
  if (i > n) i = n;
656
679
  pending = [];
@@ -660,7 +683,7 @@ function scanHeredocs(command) {
660
683
  i++;
661
684
  }
662
685
  if (stack.length > 0 || pending.length > 0) return failed;
663
- return { ok: true, bodies, segments };
686
+ return { ok: true, bodies, segments, singleQuoted };
664
687
  }
665
688
 
666
689
  /**
@@ -674,6 +697,178 @@ function readsAsText(segment, text, isMessage) {
674
697
  return MESSAGE_SUBCOMMAND_RE.test(segment.replace(ASSIGNMENTS_RE, "")) && isMessage;
675
698
  }
676
699
 
700
+ /** Reserved words that may precede a command word (`do jq ...`, `then gh ...`). */
701
+ const SHELL_KEYWORDS_RE = /^\s*(?:(?:do|then|else|elif|if|while|until|time|!|\{)\s+)*/;
702
+
703
+ /**
704
+ * True when bare root text sits inside a single-quoted jq program: a filter
705
+ * argument of `jq`, or the `--jq`/`-q` value of `gh`
706
+ * (`--jq '"\(.url|split("/")|last)"'`, `--jq '.a // "none"'`). The shell
707
+ * passes single-quoted text through unchanged, and jq cannot open a path or
708
+ * start a process from a filter, so `/`, `//` or `/*` there is jq syntax. The
709
+ * command word must be a bare `jq` or `gh` (not `./jq`), and the word must
710
+ * hold more than the token, so `jq . '/'` stays screened. Only bare root text
711
+ * qualifies: `jq 'include "/home/x"; .'` and every unquoted path argument
712
+ * stay candidates.
713
+ */
714
+ function isJqProgramText(command, start, token, singleQuoted, segments) {
715
+ const span = singleQuoted.find(({ open, close }) => start > open && start < close);
716
+ if (!span) return false;
717
+ if (command.slice(span.open + 1, span.close).trim() === token) return false;
718
+ const prefix = command.slice(span.segmentStart, span.open).replace(SHELL_KEYWORDS_RE, "");
719
+ const rest = prefix.replace(ASSIGNMENTS_RE, "");
720
+ const isJq = /^jq\s/.test(rest) || (/^gh\s/.test(rest) && /\s(?:--jq|-q)(?:=|\s+)$/.test(prefix));
721
+ return isJq && outputStaysText(command, span.segmentStart, segments, singleQuoted);
722
+ }
723
+
724
+ /**
725
+ * True when the output of the simple command at `at` cannot become a shell
726
+ * argument: the command is top level (not inside `$(`, a backtick or a
727
+ * subshell), and when it pipes onward or writes a file, every later simple
728
+ * command is empty or a chainable text command (`gh api x --jq '...' | head`).
729
+ * This keeps `jq -n '"/"' | xargs rm -rf`, `rm -rf "$(jq -n '"/"')"` and
730
+ * `jq -n '"/"' > run.sh; sh run.sh` screened.
731
+ */
732
+ function outputStaysText(command, at, segments, singleQuoted) {
733
+ if (isNestedSegment(at, segments)) return false;
734
+ if (!otherOutputStaysText(command, at, segments, singleQuoted)) return false;
735
+ if (!pipesAfter(command, at, segments) && !writesOutput(command, at, segments, singleQuoted)) {
736
+ return true;
737
+ }
738
+ return segments.every(
739
+ (segment) => segment.at <= at || ["empty", "text"].includes(classifySegment(command, segment.at)),
740
+ );
741
+ }
742
+
743
+ /**
744
+ * True unless another simple command redirects output to a file that some
745
+ * other command could then run. A redirect outside the command at `at` can
746
+ * still capture its output: `exec >run.sh; jq ...; sh run.sh`,
747
+ * `{ jq ...; } > run.sh; sh run.sh` or `for ...; do jq ...; done > run.sh`.
748
+ * When any command writes output, every command except the one at `at` must
749
+ * be empty or a chainable text command. Pass `text` with heredoc bodies
750
+ * blanked, so a `>` in body prose does not count.
751
+ */
752
+ function otherOutputStaysText(text, at, segments, singleQuoted) {
753
+ const writes = segments.some(
754
+ (segment) => segment.at !== at && writesOutput(text, segment.at, segments, singleQuoted),
755
+ );
756
+ if (!writes) return true;
757
+ return segments.every(
758
+ (segment) => segment.at === at || ["empty", "text"].includes(classifySegment(text, segment.at)),
759
+ );
760
+ }
761
+
762
+ /**
763
+ * True when the simple command at `at` may redirect output to a file
764
+ * (`jq ... > run.sh`), where a later command could run it. The command runs to
765
+ * the next quote-aware segment start. Single-quoted words (a jq `>`
766
+ * comparison) and `2>/dev/null` or `2>&1` do not count; a `>` anywhere else,
767
+ * including inside double quotes, does.
768
+ */
769
+ function writesOutput(command, at, segments, singleQuoted) {
770
+ const next = segments.reduce(
771
+ (end, segment) => (segment.at > at && segment.at < end ? segment.at : end),
772
+ command.length,
773
+ );
774
+ let text = command.slice(at, next);
775
+ for (const { open, close } of singleQuoted) {
776
+ if (open >= at && close < next) {
777
+ text = text.slice(0, open - at) + " ".repeat(close - open + 1) + text.slice(close - at + 1);
778
+ }
779
+ }
780
+ return />/.test(text.replace(/\d*>\s*\/dev\/null\b|\d*>&\d/g, ""));
781
+ }
782
+
783
+ /** True unless `at` starts a top-level simple command that scanHeredocs found. */
784
+ function isNestedSegment(at, segments) {
785
+ const own = segments.find((segment) => segment.at === at);
786
+ return !own || own.nested;
787
+ }
788
+
789
+ /**
790
+ * True when any simple command after `at` starts at a pipe (`|` or `|&`, not
791
+ * `||`). The scanner's segment starts are quote-aware, so a `|` inside quotes
792
+ * never counts. This is broader than the pipeline of `at` on purpose: a pipe
793
+ * later in the command makes the caller check more, never less.
794
+ */
795
+ function pipesAfter(command, at, segments) {
796
+ return segments.some(
797
+ ({ at: next }) =>
798
+ next > at && command[next - 1] === "|" && command[next - 2] !== "|" && command[next] !== "|",
799
+ );
800
+ }
801
+
802
+ /** Command text with every heredoc body blanked out. */
803
+ function textOutsideBodies(command, bodies) {
804
+ let text = command;
805
+ for (const { start, end } of bodies) {
806
+ text = text.slice(0, start) + " ".repeat(end - start) + text.slice(end);
807
+ }
808
+ return text;
809
+ }
810
+
811
+ /**
812
+ * True when a quoted heredoc body can reach no command except its own reader,
813
+ * so the rest of the command does not matter: `cat <<'EOF'` prints it, and
814
+ * `gh pr create --body-file - <<'EOF'` or `git commit -F - <<'EOF'` sends it as
815
+ * a message (`git push && gh pr create --body-file - <<'EOF'`). The reader
816
+ * must be top level, without a leading assignment, a `>` redirect or a pipe
817
+ * onward, and no other command may redirect output while a non-text command
818
+ * runs (otherOutputStaysText). A message reader also gets no editor flag, and no `-c` or alias
819
+ * text may appear outside the bodies.
820
+ */
821
+ function isSealedBody(command, body, outside, segments, singleQuoted) {
822
+ if (body.nested || isNestedSegment(body.at, segments) || pipesAfter(command, body.at, segments)) {
823
+ return false;
824
+ }
825
+ if (!otherOutputStaysText(outside, body.at, segments, singleQuoted)) return false;
826
+ // body.segment runs from the reader to the end of its line, so this also
827
+ // rejects a `>` that hides behind a quoted separator (`cat 'a;b' > f`).
828
+ if (body.segment.includes(">")) return false;
829
+ const text = body.segment.trimStart();
830
+ if (text.replace(ASSIGNMENTS_RE, "") !== text) return false;
831
+ if (/^cat(?:\s|$)/.test(text)) return true;
832
+ if (!MESSAGE_SUBCOMMAND_RE.test(text) || !MESSAGE_FILE_RE.test(text)) return false;
833
+ return !EDITOR_FLAG_RE.test(text) && !CONFIG_OVERRIDE_RE.test(outside);
834
+ }
835
+
836
+ /**
837
+ * True when a quoted heredoc body is a non-shell program written to a file and
838
+ * run only as that program: `cat > ./x.cjs <<'EOF' ... EOF` then
839
+ * `node ./x.cjs; rm ./x.cjs`. This is the same text `node - <<'EOF'` would
840
+ * run, so the same rule applies: the body must not name a way to start a
841
+ * process (PROGRAM_SHELL_WORDS_RE; a backtick in a quoted body is a template
842
+ * literal, not a shell substitution). The writer must be exactly `cat > FILE`
843
+ * with one `>` and a plain file name, at top level, without a pipe onward.
844
+ * Every other command must be top level and either empty, a chainable text
845
+ * command, `rm`, or a PROGRAM_COMMANDS interpreter, without leading
846
+ * assignments, whose first argument is FILE. Anything else, such as `sh FILE`,
847
+ * `chmod +x FILE`, `python3 -c ...` or `mv`, could run the text another way.
848
+ */
849
+ function isProgramFileBody(command, body, segments) {
850
+ if (body.nested || PROGRAM_SHELL_WORDS_RE.test(command.slice(body.start, body.end))) return false;
851
+ if (isNestedSegment(body.at, segments)) return false;
852
+ // body.segment runs to the end of the line. The writer may be followed only
853
+ // by `;` or `&&`; the file-name class holds no quote, so no quoted
854
+ // separator can hide a pipe or a second redirect.
855
+ const writer = body.segment.replace(/\s*<<-?\s*\S+/, " ");
856
+ const redirect = /^\s*cat\s*>\s*([\w./-]+)\s*(?:$|;|&&)/.exec(writer);
857
+ if (!redirect || /<</.test(writer)) return false;
858
+ const file = redirect[1].replace(/^(?:\.\/)+/, "");
859
+ if (!file || file.endsWith("/")) return false;
860
+ const escaped = file.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
861
+ const runsFile = new RegExp(`^(?:${[...PROGRAM_COMMANDS].join("|")})\\s+(?:\\./)*${escaped}(?:\\s|$)`);
862
+ return segments.every(({ at, nested }) => {
863
+ if (at === body.at) return true;
864
+ if (nested) return false;
865
+ const kind = classifySegment(command, at);
866
+ if (kind === "empty" || kind === "text") return true;
867
+ const text = segmentText(command, at).trimStart();
868
+ return /^rm(?:\s|$)/.test(text) || runsFile.test(text);
869
+ });
870
+ }
871
+
677
872
  /**
678
873
  * True when bare root text sits in a heredoc body or quoted argument that a
679
874
  * text command, a non-shell program, or a git/gh message reads: a
@@ -682,13 +877,27 @@ function readsAsText(segment, text, isMessage) {
682
877
  * quoted argument must hold more than the token itself, so `ls "/"` stays
683
878
  * screened. An unquoted-delimiter heredoc qualifies only without `$(` or
684
879
  * backtick expansion in its body. No exemption applies unless the rest of the
685
- * command cannot run that text (exemptionContextIsSafe).
880
+ * command cannot run that text (exemptionContextIsSafe), except for a
881
+ * single-quoted jq program whose output stays text (isJqProgramText), and a
882
+ * quoted heredoc body that reaches only its reader (isSealedBody) or runs only
883
+ * as a program file (isProgramFileBody). The two heredoc rules look for hook
884
+ * locations (HOOK_PATH_RE) only outside the bodies, so body prose that
885
+ * mentions `hooks` stays exempt. The jq rule needs no hook check, because jq
886
+ * output never reaches a hook file when it reaches no other command.
686
887
  */
687
888
  function isProgramTextRootToken(command, start, token, heredocs) {
688
889
  if (!isBareRootText(token)) return false;
689
- const { ok, bodies, segments } = heredocs();
690
- if (!ok || !exemptionContextIsSafe(command, segments)) return false;
890
+ const { ok, bodies, segments, singleQuoted } = heredocs();
891
+ if (!ok) return false;
892
+ if (isJqProgramText(command, start, token, singleQuoted, segments)) return true;
691
893
  const body = bodies.find((doc) => start >= doc.start && start < doc.end);
894
+ const outside = body && body.quoted ? textOutsideBodies(command, bodies) : "";
895
+ if (outside && !HOOK_PATH_RE.test(outside)) {
896
+ if (isSealedBody(command, body, outside, segments, singleQuoted) || isProgramFileBody(command, body, segments)) {
897
+ return true;
898
+ }
899
+ }
900
+ if (!exemptionContextIsSafe(command, segments)) return false;
692
901
  if (body) {
693
902
  const text = command.slice(body.start, body.end);
694
903
  if (!body.quoted && /`|\$\(/.test(text)) return false;
@@ -11,6 +11,22 @@ exports.detectAgentModelUnavailable = detectAgentModelUnavailable;
11
11
  */
12
12
  const NO_ROUTE_PATTERN = /\bprovider\.no-route\b|\bModelUnavailableError\b|\bModel unavailable:/i;
13
13
  const MODEL_NAME_PATTERN = /\bModel unavailable:\s*([^\s"'\\,;)]+)/i;
14
+ const TRANSPORT_PATTERN = /\bprovider\.transport\b|\bConnectionRefused\b|\bECONNREFUSED\b|\bconnection refused\b/i;
15
+ /** A failed connection is a machine problem only before the session does work. */
16
+ function madeModelProgress(result) {
17
+ if (result.reportText?.trim() || (result.tokensIn ?? 0) > 0 ||
18
+ (result.tokensOut ?? 0) > 0 || (result.costUsd ?? 0) > 0)
19
+ return true;
20
+ return result.stdout.split(/\r?\n/).some((line) => {
21
+ try {
22
+ const event = JSON.parse(line);
23
+ return ["step_start", "step_finish", "text", "reasoning", "tool_use", "tool_result", "tool"].includes(event?.type);
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ });
29
+ }
14
30
  function clip(text) {
15
31
  const trimmed = text.trim().replace(/\s+/g, " ");
16
32
  return trimmed.length > 300 ? `${trimmed.slice(0, 299)}…` : trimmed;
@@ -50,7 +66,8 @@ function errorEventMessages(stdout) {
50
66
  }
51
67
  /**
52
68
  * 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`).
69
+ * was told to use has no route on this machine (OpenCode `provider.no-route`),
70
+ * or its provider connection fails before any model progress in this session.
54
71
  * Like a dead login (agent-auth.cts), every task the machine claims then fails
55
72
  * the same way within seconds, so the control plane requeues the task without
56
73
  * spending a retry and pauses the machine until the model is fixed.
@@ -59,19 +76,20 @@ function errorEventMessages(stdout) {
59
76
  * ERROR log lines — and only for a turn that failed or produced no report, so
60
77
  * an agent writing about this very error cannot pause its own machine.
61
78
  */
62
- function detectAgentModelUnavailable(result) {
79
+ function detectAgentModelUnavailable(result, previousAttempts = []) {
63
80
  if (result.killedByLeaseLoss || result.timedOut || result.capacityError || result.authError)
64
81
  return null;
65
82
  if (result.exitCode === 0 && result.reportText)
66
83
  return null;
84
+ const beforeProgress = !madeModelProgress(result) && !previousAttempts.some(madeModelProgress);
67
85
  for (const message of errorEventMessages(result.stdout)) {
68
- if (NO_ROUTE_PATTERN.test(message))
86
+ if (NO_ROUTE_PATTERN.test(message) || (beforeProgress && TRANSPORT_PATTERN.test(message)))
69
87
  return failure(message);
70
88
  }
71
89
  if (result.exitCode === 0)
72
90
  return null;
73
91
  for (const line of result.stderr.split(/\r?\n/)) {
74
- if (/ERROR/.test(line) && NO_ROUTE_PATTERN.test(line)) {
92
+ if (/ERROR/.test(line) && (NO_ROUTE_PATTERN.test(line) || (beforeProgress && TRANSPORT_PATTERN.test(line)))) {
75
93
  const cause = line.match(/Model unavailable:[^"\n]*/i)?.[0];
76
94
  return failure(cause ?? line);
77
95
  }
package/dist/session.cjs CHANGED
@@ -1080,7 +1080,10 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
1080
1080
  }
1081
1081
  // A model the CLI has no route to (OpenCode `provider.no-route`) fails
1082
1082
  // every task the same way, so it gets the same treatment as a dead login.
1083
- const modelUnavailable = capacityLimit || authFailure ? null : (0, agent_model_cjs_1.detectAgentModelUnavailable)(turnResult);
1083
+ // Startup transport failures also qualify, unless this session already
1084
+ // made model progress in this or an earlier turn.
1085
+ const modelUnavailable = capacityLimit || authFailure
1086
+ ? null : (0, agent_model_cjs_1.detectAgentModelUnavailable)(turnResult, attempts.slice(0, -1));
1084
1087
  if (modelUnavailable) {
1085
1088
  turnResult.modelUnavailableError = modelUnavailable.detail;
1086
1089
  log.warn(`session ${leaseId}: coding agent model unavailable: ${modelUnavailable.detail}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sagentlab/navarch-runtime",
3
- "version": "0.1.145",
3
+ "version": "0.1.147",
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",