@miller-tech/uap 1.175.3 → 1.175.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miller-tech/uap",
3
- "version": "1.175.3",
3
+ "version": "1.175.4",
4
4
  "description": "Autonomous AI agent memory system with CLAUDE.md protocol enforcement",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -300,19 +300,47 @@ _ERROR_LINE_RE = re.compile(
300
300
  )
301
301
 
302
302
 
303
+ # Harness-generated CORRECTIVES, not tool failures.
304
+ #
305
+ # The delivery executor emits its own control lines, and some carry the word
306
+ # "error" purely as a channel label -- e.g.
307
+ #
308
+ # [agent r6 error] write-nudge injected after 5 read-only rounds
309
+ #
310
+ # which is the harness telling the model it has been READING too much and must
311
+ # now WRITE. _ERROR_LINE_RE matched it on "error", so the corrective became an
312
+ # error signature; after three repeats the ERROR-LOOP guard fired and injected
313
+ # "Do NOT make another edit yet. FIRST re-read the ENTIRE failing file" -- which
314
+ # tells a model already stuck in a read-only loop to read more, directly fighting
315
+ # the nudge it was reacting to. Observed live (2026-07-31): fired six times in a
316
+ # row against a run that was progressing.
317
+ #
318
+ # A corrective is the harness talking to the model about its BEHAVIOUR; an error
319
+ # is a tool reporting that something did not work. Only the latter belongs in the
320
+ # failure streak.
321
+ _HARNESS_CORRECTIVE_RE = re.compile(
322
+ r"(?:write-nudge|nudge)\s+injected|read-only rounds|forced[- ]write round|"
323
+ r"deferral[- ]break|stuck[- ]break|cycle[- ]break",
324
+ re.IGNORECASE,
325
+ )
326
+
327
+
303
328
  def _error_signature(text: str) -> str:
304
329
  """Edit-invariant signature of the first error line in a tool result.
305
330
 
306
331
  Normalizes away paths, line:col numbers, hex, and bare digits so that the
307
332
  SAME underlying failure produces the SAME signature across turns even as the
308
333
  model edits different code around it. Returns "" when no error line is found
309
- (a passing result resets the streak)."""
334
+ (a passing result resets the streak), and "" for the harness's own
335
+ correctives -- see _HARNESS_CORRECTIVE_RE."""
310
336
  if not text:
311
337
  return ""
312
338
  m = _ERROR_LINE_RE.search(text)
313
339
  if not m:
314
340
  return ""
315
341
  line = m.group(0)
342
+ if _HARNESS_CORRECTIVE_RE.search(line):
343
+ return ""
316
344
  line = re.sub(r"(/[^\s:]+)+", "<path>", line) # unix paths
317
345
  line = re.sub(r"\b[0-9a-fA-F]{6,}\b", "<hex>", line) # hashes/addresses
318
346
  line = re.sub(r"\d+", "#", line) # line numbers, counts
@@ -0,0 +1,92 @@
1
+ """The ERROR-LOOP guard must not fire on the harness's own correctives.
2
+
3
+ Observed live (opencode, 2026-07-31). The delivery executor emits control lines
4
+ on an "error" channel:
5
+
6
+ [agent r6 error] write-nudge injected after 5 read-only rounds
7
+
8
+ which is the harness telling the model it has been READING too much and must now
9
+ WRITE. `_ERROR_LINE_RE` matched it on the word "error", so the corrective entered
10
+ the failure streak; after three repeats the ERROR-LOOP guard injected
11
+
12
+ "Do NOT make another edit yet. FIRST re-read the ENTIRE failing file ..."
13
+
14
+ i.e. it told a model already stuck in a read-only loop to read more, fighting the
15
+ very nudge it was reacting to. It fired six times in a row against a run that was
16
+ making progress.
17
+
18
+ The distinction the fix encodes: a CORRECTIVE is the harness talking to the model
19
+ about its behaviour; an ERROR is a tool reporting that something did not work.
20
+ Only the latter belongs in the streak.
21
+ """
22
+
23
+ import re
24
+ from pathlib import Path
25
+
26
+ PROXY = Path(__file__).resolve().parents[1] / "scripts" / "anthropic_proxy.py"
27
+
28
+
29
+ def _load_signature():
30
+ """Extract _error_signature and its patterns without importing the server."""
31
+ src = PROXY.read_text()
32
+ ns = {"re": re}
33
+ for name in ("_ERROR_LINE_RE", "_HARNESS_CORRECTIVE_RE"):
34
+ start = src.index(f"{name} = re.compile(")
35
+ end = src.index(")\n", src.index("re.", start + 20)) + 1
36
+ exec(src[start:end], ns) # noqa: S102 - reading our own source, not input
37
+ start = src.index("def _error_signature")
38
+ end = src.index("\n# ---", start)
39
+ exec(src[start:end], ns) # noqa: S102
40
+ return ns["_error_signature"]
41
+
42
+
43
+ _error_signature = _load_signature()
44
+
45
+
46
+ class TestHarnessCorrectivesAreNotFailures:
47
+ def test_write_nudge_does_not_enter_the_failure_streak(self):
48
+ # The exact line from the incident.
49
+ assert _error_signature("[agent r6 error] write-nudge injected after 5 read-only rounds") == ""
50
+
51
+ def test_other_correctives_are_ignored_too(self):
52
+ for line in (
53
+ "[agent r9 error] forced-write round engaged",
54
+ "[agent r4 error] nudge injected after 5 read-only rounds",
55
+ "deferral-break: model deferred instead of acting",
56
+ "cycle-break engaged",
57
+ ):
58
+ assert _error_signature(line) == "", line
59
+
60
+ def test_a_corrective_repeated_never_builds_a_streak(self):
61
+ # The streak is what arms the guard; an empty signature can never match
62
+ # the previous one, so repetition alone cannot fire it.
63
+ line = "[agent r7 error] write-nudge injected after 5 read-only rounds"
64
+ assert {_error_signature(line) for _ in range(6)} == {""}
65
+
66
+
67
+ class TestRealFailuresStillTracked:
68
+ def test_tool_errors_still_produce_a_signature(self):
69
+ for line in (
70
+ "ERROR: TypeError: x is not a function at /a/b.js:12",
71
+ "FAILED tests/foo.test.ts > it works",
72
+ "SyntaxError: Unexpected token",
73
+ "bash: command not found: pytest",
74
+ ):
75
+ assert _error_signature(line) != "", line
76
+
77
+ def test_the_same_failure_still_produces_a_stable_signature(self):
78
+ # Edit-invariance is the whole point of the signature: the same failure
79
+ # must match across turns even as line numbers and paths move.
80
+ a = _error_signature("ERROR: TypeError: x is not a function at /src/a.js:12")
81
+ b = _error_signature("ERROR: TypeError: x is not a function at /lib/b.js:940")
82
+ assert a == b != ""
83
+
84
+ def test_a_passing_result_resets_the_streak(self):
85
+ assert _error_signature("all tests passed") == ""
86
+
87
+ def test_a_corrective_wrapped_around_a_REAL_error_still_counts(self):
88
+ # Only the matched line is inspected, so a genuine failure elsewhere in
89
+ # the same payload must still register — otherwise the exclusion would
90
+ # become a way to hide real errors behind a nudge.
91
+ text = "ERROR: ReferenceError: foo is not defined\n[agent r6 error] write-nudge injected"
92
+ assert _error_signature(text) != ""