@miller-tech/uap 2.11.0 → 2.16.2

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.
Files changed (166) hide show
  1. package/README.md +7 -6
  2. package/config/capacity-policy.json +15 -3
  3. package/config/model-profiles/qwen38.json +42 -34
  4. package/dist/.tsbuildinfo +1 -1
  5. package/dist/bin/cli.js +39 -0
  6. package/dist/bin/cli.js.map +1 -1
  7. package/dist/capacity/policy.d.ts +15 -0
  8. package/dist/capacity/policy.d.ts.map +1 -1
  9. package/dist/capacity/policy.js +23 -0
  10. package/dist/capacity/policy.js.map +1 -1
  11. package/dist/capacity/probe.d.ts.map +1 -1
  12. package/dist/capacity/probe.js +6 -1
  13. package/dist/capacity/probe.js.map +1 -1
  14. package/dist/cli/deliver.d.ts.map +1 -1
  15. package/dist/cli/deliver.js +148 -2
  16. package/dist/cli/deliver.js.map +1 -1
  17. package/dist/cli/inference.d.ts +13 -0
  18. package/dist/cli/inference.d.ts.map +1 -0
  19. package/dist/cli/inference.js +132 -0
  20. package/dist/cli/inference.js.map +1 -0
  21. package/dist/cli/loops.d.ts +39 -0
  22. package/dist/cli/loops.d.ts.map +1 -0
  23. package/dist/cli/loops.js +154 -0
  24. package/dist/cli/loops.js.map +1 -0
  25. package/dist/cli/wizard-config.d.ts.map +1 -1
  26. package/dist/cli/wizard-config.js +5 -1
  27. package/dist/cli/wizard-config.js.map +1 -1
  28. package/dist/cli/worktree.d.ts.map +1 -1
  29. package/dist/cli/worktree.js +8 -4
  30. package/dist/cli/worktree.js.map +1 -1
  31. package/dist/delivery/adversarial-attack.d.ts +99 -0
  32. package/dist/delivery/adversarial-attack.d.ts.map +1 -0
  33. package/dist/delivery/adversarial-attack.js +326 -0
  34. package/dist/delivery/adversarial-attack.js.map +1 -0
  35. package/dist/delivery/adversarial-gate.d.ts +120 -0
  36. package/dist/delivery/adversarial-gate.d.ts.map +1 -0
  37. package/dist/delivery/adversarial-gate.js +259 -0
  38. package/dist/delivery/adversarial-gate.js.map +1 -0
  39. package/dist/delivery/adversarial-sanction.d.ts +52 -0
  40. package/dist/delivery/adversarial-sanction.d.ts.map +1 -0
  41. package/dist/delivery/adversarial-sanction.js +134 -0
  42. package/dist/delivery/adversarial-sanction.js.map +1 -0
  43. package/dist/delivery/agentic-executor.d.ts +69 -22
  44. package/dist/delivery/agentic-executor.d.ts.map +1 -1
  45. package/dist/delivery/agentic-executor.js +152 -8
  46. package/dist/delivery/agentic-executor.js.map +1 -1
  47. package/dist/delivery/applier.d.ts +8 -0
  48. package/dist/delivery/applier.d.ts.map +1 -1
  49. package/dist/delivery/applier.js +3 -1
  50. package/dist/delivery/applier.js.map +1 -1
  51. package/dist/delivery/convergence-loop.d.ts +15 -0
  52. package/dist/delivery/convergence-loop.d.ts.map +1 -1
  53. package/dist/delivery/convergence-loop.js.map +1 -1
  54. package/dist/delivery/criteria-lint.d.ts +74 -0
  55. package/dist/delivery/criteria-lint.d.ts.map +1 -0
  56. package/dist/delivery/criteria-lint.js +134 -0
  57. package/dist/delivery/criteria-lint.js.map +1 -0
  58. package/dist/delivery/deliver-toggle.d.ts +21 -0
  59. package/dist/delivery/deliver-toggle.d.ts.map +1 -0
  60. package/dist/delivery/deliver-toggle.js +40 -0
  61. package/dist/delivery/deliver-toggle.js.map +1 -0
  62. package/dist/delivery/delivery-evidence.d.ts +109 -0
  63. package/dist/delivery/delivery-evidence.d.ts.map +1 -0
  64. package/dist/delivery/delivery-evidence.js +139 -0
  65. package/dist/delivery/delivery-evidence.js.map +1 -0
  66. package/dist/delivery/epic-mission.d.ts +5 -1
  67. package/dist/delivery/epic-mission.d.ts.map +1 -1
  68. package/dist/delivery/epic-mission.js +34 -6
  69. package/dist/delivery/epic-mission.js.map +1 -1
  70. package/dist/delivery/execution-gate.d.ts.map +1 -1
  71. package/dist/delivery/execution-gate.js +90 -7
  72. package/dist/delivery/execution-gate.js.map +1 -1
  73. package/dist/delivery/index.d.ts +9 -1
  74. package/dist/delivery/index.d.ts.map +1 -1
  75. package/dist/delivery/index.js +9 -1
  76. package/dist/delivery/index.js.map +1 -1
  77. package/dist/delivery/journey-depth.d.ts +61 -0
  78. package/dist/delivery/journey-depth.d.ts.map +1 -0
  79. package/dist/delivery/journey-depth.js +91 -0
  80. package/dist/delivery/journey-depth.js.map +1 -0
  81. package/dist/delivery/master-pipeline.d.ts +50 -0
  82. package/dist/delivery/master-pipeline.d.ts.map +1 -0
  83. package/dist/delivery/master-pipeline.js +52 -0
  84. package/dist/delivery/master-pipeline.js.map +1 -0
  85. package/dist/delivery/mission-acceptance.d.ts.map +1 -1
  86. package/dist/delivery/mission-acceptance.js +54 -2
  87. package/dist/delivery/mission-acceptance.js.map +1 -1
  88. package/dist/delivery/orchestrated-mission.d.ts +4 -1
  89. package/dist/delivery/orchestrated-mission.d.ts.map +1 -1
  90. package/dist/delivery/orchestrated-mission.js +18 -9
  91. package/dist/delivery/orchestrated-mission.js.map +1 -1
  92. package/dist/delivery/plan-check.d.ts +1 -0
  93. package/dist/delivery/plan-check.d.ts.map +1 -1
  94. package/dist/delivery/plan-check.js +48 -0
  95. package/dist/delivery/plan-check.js.map +1 -1
  96. package/dist/delivery/task-orchestrator.d.ts +16 -9
  97. package/dist/delivery/task-orchestrator.d.ts.map +1 -1
  98. package/dist/delivery/task-orchestrator.js +4 -2
  99. package/dist/delivery/task-orchestrator.js.map +1 -1
  100. package/dist/delivery/task-workspace.d.ts +35 -5
  101. package/dist/delivery/task-workspace.d.ts.map +1 -1
  102. package/dist/delivery/task-workspace.js +53 -7
  103. package/dist/delivery/task-workspace.js.map +1 -1
  104. package/dist/delivery/test-oracle-additive.d.ts +26 -6
  105. package/dist/delivery/test-oracle-additive.d.ts.map +1 -1
  106. package/dist/delivery/test-oracle-additive.js +40 -15
  107. package/dist/delivery/test-oracle-additive.js.map +1 -1
  108. package/dist/delivery/user-validation.d.ts +14 -0
  109. package/dist/delivery/user-validation.d.ts.map +1 -1
  110. package/dist/delivery/user-validation.js +103 -9
  111. package/dist/delivery/user-validation.js.map +1 -1
  112. package/dist/delivery/verifier-ladder.d.ts.map +1 -1
  113. package/dist/delivery/verifier-ladder.js +24 -6
  114. package/dist/delivery/verifier-ladder.js.map +1 -1
  115. package/dist/inference/analysis.d.ts +138 -0
  116. package/dist/inference/analysis.d.ts.map +1 -0
  117. package/dist/inference/analysis.js +243 -0
  118. package/dist/inference/analysis.js.map +1 -0
  119. package/dist/inference/probe.d.ts +85 -0
  120. package/dist/inference/probe.d.ts.map +1 -0
  121. package/dist/inference/probe.js +403 -0
  122. package/dist/inference/probe.js.map +1 -0
  123. package/dist/models/types.d.ts.map +1 -1
  124. package/dist/models/types.js +45 -18
  125. package/dist/models/types.js.map +1 -1
  126. package/dist/self-tuning/profiles/qwen38.d.ts +17 -8
  127. package/dist/self-tuning/profiles/qwen38.d.ts.map +1 -1
  128. package/dist/self-tuning/profiles/qwen38.js +21 -11
  129. package/dist/self-tuning/profiles/qwen38.js.map +1 -1
  130. package/dist/utils/model-slots.d.ts +11 -5
  131. package/dist/utils/model-slots.d.ts.map +1 -1
  132. package/dist/utils/model-slots.js +11 -5
  133. package/dist/utils/model-slots.js.map +1 -1
  134. package/docs/INDEX.md +7 -1
  135. package/docs/guides/CAPACITY_POLICY.md +29 -4
  136. package/docs/guides/INFERENCE_HEALTH.md +184 -0
  137. package/docs/guides/PROXY.md +1 -0
  138. package/docs/guides/QWEN38_EXL3.md +268 -0
  139. package/docs/plans/hlg-master-architecture-uplift-2026-09-24.md +67 -0
  140. package/docs/plans/system1-uplift-2026-09-18.md +1 -2
  141. package/docs/reference/CONFIGURATION.md +10 -1
  142. package/docs/reference/CONFIGURATION_REFERENCE.md +14 -0
  143. package/docs/reference/automode-risk.md +1 -1
  144. package/docs/reference/evidence-bound-ship.md +3 -2
  145. package/docs/reference/semantic-supervisor.md +3 -3
  146. package/docs/reference/system1-classifier.md +4 -4
  147. package/package.json +2 -2
  148. package/scripts/lib/llama-upstream.sh +7 -1
  149. package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
  150. package/src/policies/enforcers/__pycache__/expert_review_required.cpython-312.pyc +0 -0
  151. package/src/policies/enforcers/__pycache__/quality_metrics_gate.cpython-312.pyc +0 -0
  152. package/src/policies/enforcers/expert_review_required.py +1 -4
  153. package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
  154. package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
  155. package/tools/agents/scripts/anthropic_proxy.py +374 -16
  156. package/tools/agents/scripts/confidence_escalation.py +4 -2
  157. package/tools/agents/scripts/project_telemetry.py +1 -0
  158. package/tools/agents/scripts/tool_risk.py +1 -0
  159. package/tools/agents/scripts/tool_risk_calibrate.py +1 -0
  160. package/tools/agents/scripts/toolcall_path_normalizer.py +1 -0
  161. package/tools/agents/tests/test_anthropic_proxy_streaming.py +52 -1
  162. package/tools/agents/tests/test_confidence_escalation.py +3 -2
  163. package/tools/agents/tests/test_count_tokens_scale_per_session.py +126 -0
  164. package/tools/agents/tests/test_expert_review_pr_scope.py +10 -11
  165. package/tools/agents/tests/test_lockstep_escalation.py +324 -0
  166. package/tools/agents/tests/test_thinking_template_kwargs.py +10 -4
@@ -372,8 +372,9 @@ class SlotRightSizingTest(unittest.TestCase):
372
372
  """
373
373
 
374
374
  def test_unknown_width_changes_nothing(self):
375
- # Not every backend can be probed (ninfer serves no /slots), so silence
376
- # is not evidence -- unknown width must leave behaviour identical.
375
+ # Not every backend can be probed -- some engines serve no /slots, and a
376
+ # probe can fail -- so silence is not evidence: unknown width must leave
377
+ # behaviour identical.
377
378
  self.assertEqual(ce.select_recipe(body(), S(recipe="fusion"), False), "fusion")
378
379
 
379
380
  def test_single_slot_downgrades_explicit_fusion(self):
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env python3
2
+ """Compaction forcing must be computed against the SESSION's window, not the rail.
3
+
4
+ Regression (2026-09-20). The local server moved to `-np 2 -c 229376` with
5
+ `--kv-unified`, which makes 229376 a SHARED pool: /slots reports it for both
6
+ slots and either rail may address all of it. Sessions are therefore capped
7
+ below the detected rail by a model profile's `context_window`.
8
+
9
+ `_count_tokens_scale()` resolved its window from `_effective_context_window()`
10
+ — the process-wide value detected from /slots — so the two numbers disagreed
11
+ and only one of them actually bounded the session:
12
+
13
+ detected rail 229376
14
+ profile cap 114688 <- the real bound
15
+ compact fires 123060 computed from the RAIL, so ABOVE the cap
16
+ prune fires 80282 computed from the CAP
17
+
18
+ Compaction became unreachable and the pruner permanently replaced it. Those
19
+ are not equivalent: compaction writes an LLM summary, a prune drops the
20
+ messages and leaves a breadcrumb list. The proxy env's own comment states the
21
+ intent — "land compaction just under the pruner trigger so the client compacts
22
+ before the proxy ever prunes (pruner = backstop only)".
23
+ """
24
+
25
+ import importlib.util
26
+ import unittest
27
+ from pathlib import Path
28
+
29
+
30
+ def _load_proxy_module():
31
+ proxy_path = Path(__file__).resolve().parents[1] / "scripts" / "anthropic_proxy.py"
32
+ spec = importlib.util.spec_from_file_location("anthropic_proxy", proxy_path)
33
+ assert spec is not None and spec.loader is not None
34
+ module = importlib.util.module_from_spec(spec)
35
+ spec.loader.exec_module(module)
36
+ return module
37
+
38
+
39
+ PROXY = _load_proxy_module()
40
+
41
+ # The live geometry this regression came from.
42
+ SHARED_POOL = 229_376
43
+ SESSION_CAP = 114_688
44
+
45
+
46
+ class CountTokensScalePerSession(unittest.TestCase):
47
+ def setUp(self):
48
+ self._saved = (
49
+ PROXY.PROXY_COUNT_TOKENS_SCALE,
50
+ PROXY.PROXY_CLIENT_ASSUMED_WINDOW,
51
+ PROXY.PROXY_COMPACT_TARGET_FRACTION,
52
+ PROXY.default_context_window,
53
+ )
54
+ PROXY.PROXY_COUNT_TOKENS_SCALE = "auto"
55
+ PROXY.PROXY_CLIENT_ASSUMED_WINDOW = 200_000
56
+ PROXY.PROXY_COMPACT_TARGET_FRACTION = 0.58
57
+ # The process default is the whole shared pool, as /slots reports it.
58
+ PROXY.default_context_window = SHARED_POOL
59
+
60
+ def tearDown(self):
61
+ (
62
+ PROXY.PROXY_COUNT_TOKENS_SCALE,
63
+ PROXY.PROXY_CLIENT_ASSUMED_WINDOW,
64
+ PROXY.PROXY_COMPACT_TARGET_FRACTION,
65
+ PROXY.default_context_window,
66
+ ) = self._saved
67
+ PROXY.session_monitors.clear()
68
+
69
+ def _compact_trigger(self, scale: float) -> float:
70
+ """Real tokens at which the client's auto-compact fires."""
71
+ return PROXY.PROXY_CLIENT_ASSUMED_WINDOW * 0.925 / scale
72
+
73
+ def test_override_uses_the_session_window_not_the_pool(self):
74
+ scale = PROXY._count_tokens_scale(SESSION_CAP)
75
+ pool_scale = PROXY._count_tokens_scale(0)
76
+ self.assertGreater(
77
+ scale, pool_scale,
78
+ "a capped session must be scaled harder than the shared pool",
79
+ )
80
+
81
+ def test_compaction_fires_before_the_pruner_for_a_capped_session(self):
82
+ """The whole point: compact must precede prune, or prune is all there is."""
83
+ scale = PROXY._count_tokens_scale(SESSION_CAP)
84
+ compact_at = self._compact_trigger(scale)
85
+ prune_at = SESSION_CAP * PROXY.PROXY_CONTEXT_PRUNE_THRESHOLD
86
+ self.assertLess(
87
+ compact_at, prune_at,
88
+ f"compaction at {compact_at:,.0f} must fire before the pruner at "
89
+ f"{prune_at:,.0f}; the pruner is a backstop, not the mechanism",
90
+ )
91
+
92
+ def test_the_unfixed_behaviour_would_be_unreachable(self):
93
+ """Pin the bug itself, so a revert to the pool-wide window is loud."""
94
+ pool_scale = PROXY._count_tokens_scale(0) # resolves the shared pool
95
+ compact_at = self._compact_trigger(pool_scale)
96
+ self.assertGreater(
97
+ compact_at, SESSION_CAP,
98
+ "sanity: computed from the pool, the compact point IS above the "
99
+ "session cap — this is the condition the fix exists to avoid",
100
+ )
101
+
102
+ def test_session_window_is_picked_up_from_the_request_contextvar(self):
103
+ """Paths with no explicit override still resolve the session's window."""
104
+ sid = "test-session-capped"
105
+ monitor = PROXY.SessionMonitor(context_window=SESSION_CAP)
106
+ PROXY.session_monitors[sid] = monitor
107
+ token = PROXY._current_request_session.set(sid)
108
+ try:
109
+ self.assertEqual(PROXY._scale_window(), SESSION_CAP)
110
+ scale = PROXY._count_tokens_scale()
111
+ compact_at = self._compact_trigger(scale)
112
+ self.assertLess(compact_at, SESSION_CAP * PROXY.PROXY_CONTEXT_PRUNE_THRESHOLD)
113
+ finally:
114
+ PROXY._current_request_session.reset(token)
115
+
116
+ def test_falls_back_to_the_process_window_with_no_session(self):
117
+ PROXY.session_monitors.clear()
118
+ self.assertEqual(PROXY._scale_window(), SHARED_POOL)
119
+
120
+ def test_scale_stays_disabled_when_turned_off(self):
121
+ PROXY.PROXY_COUNT_TOKENS_SCALE = "off"
122
+ self.assertEqual(PROXY._count_tokens_scale(SESSION_CAP), 1.0)
123
+
124
+
125
+ if __name__ == "__main__":
126
+ unittest.main()
@@ -207,9 +207,8 @@ class TestShipDetectionIsCommandPosition(unittest.TestCase):
207
207
  review artifact was refused because the notes described the bug being fixed.
208
208
 
209
209
  The risk in the fix is UNDER-detection, so the ship cases below matter more
210
- than the prose ones. The wrapper cases especially: rtk is no longer
211
- integrated in UAP, but the binary can still exist on operator machines, so
212
- a verb check that stopped at the wrapper would miss wrapped ship commands.
210
+ than the prose ones. The wrapper cases especially: a verb check that
211
+ stopped at the wrapper would miss wrapped ship commands.
213
212
  """
214
213
 
215
214
  def setUp(self):
@@ -246,18 +245,18 @@ class TestShipDetectionIsCommandPosition(unittest.TestCase):
246
245
  def test_plain_git_push_is_a_ship_action(self):
247
246
  self.assert_ship("git push origin master")
248
247
 
249
- def test_rtk_wrapped_git_is_a_ship_action(self):
250
- # rtk is no longer a UAP integration, but the wrapper unwrap stays as
251
- # defense-in-depth for machines where the binary remains installed.
252
- self.assert_ship("rtk git push origin master")
248
+ def test_wrapped_git_is_a_ship_action(self):
249
+ # Wrapper unwrap: the gate must see through prefix wrappers to the
250
+ # real verb.
251
+ self.assert_ship("env git push origin master")
253
252
 
254
- def test_rtk_wrapped_git_with_value_option_is_a_ship_action(self):
253
+ def test_wrapped_git_with_value_option_is_a_ship_action(self):
255
254
  # The `-C <path>` form needs the wrapper unwrap — the raw prose
256
255
  # patterns only catch adjacent `git push`. Regression guard.
257
- self.assert_ship("rtk git -C /repo push origin master")
256
+ self.assert_ship("env git -C /repo push origin master")
258
257
 
259
- def test_rtk_wrapped_plumbing_is_a_ship_action(self):
260
- self.assert_ship("rtk git send-pack origin master")
258
+ def test_wrapped_plumbing_is_a_ship_action(self):
259
+ self.assert_ship("env git send-pack origin master")
261
260
 
262
261
  def test_ship_action_after_a_chained_command(self):
263
262
  self.assert_ship("cd sub && git commit -m x")
@@ -0,0 +1,324 @@
1
+ #!/usr/bin/env python3
2
+ """LOCKSTEP ESCALATION guardrail: the SAME tool call failing with the SAME
3
+ error, over and over. The older guards split this case against themselves --
4
+ DOUBLING-DOWN yielded to ERROR-LOOP, whose re-read remedy assumes a failing
5
+ file and is wrong for tool-misuse loops (observed live 2026-09-25: one ssh
6
+ call re-issued 27 times before the hard stop). Lockstep owns the case with a
7
+ decisive ladder: pivot directive -> final warning -> hard stop + a JSONL
8
+ loop-incident record for `uap loops`.
9
+ """
10
+ import importlib.util
11
+ import json
12
+ import os
13
+ import tempfile
14
+ import unittest
15
+ from pathlib import Path
16
+
17
+
18
+ def _load_proxy_module():
19
+ proxy_path = Path(__file__).resolve().parents[1] / "scripts" / "anthropic_proxy.py"
20
+ spec = importlib.util.spec_from_file_location("anthropic_proxy", proxy_path)
21
+ mod = importlib.util.module_from_spec(spec)
22
+ spec.loader.exec_module(mod)
23
+ return mod
24
+
25
+
26
+ proxy = _load_proxy_module()
27
+
28
+ # Built through the production fingerprinter so the tests pin the format the
29
+ # ledger actually sees ("name:md5[:8]"), not a hand-rolled stand-in.
30
+ FP = proxy._tool_call_fingerprint(
31
+ {"name": "bash", "input": {"command": 'ssh host "docker exec c python3 -c ..."'}}
32
+ )
33
+ ERR = "Traceback (most recent call last):\n File \"<string>\", line 3\nKeyError: 'x'"
34
+
35
+
36
+ def _drive_lockstep(monitor, n):
37
+ """Feed n consecutive identical-failing-call observations, the way the
38
+ request path feeds both trackers each turn."""
39
+ for i in range(n):
40
+ monitor.note_tool_result_error(ERR)
41
+ monitor.note_doubling_signal(FP, ERR, msg_count=100 + i, result_error=True)
42
+
43
+
44
+ class LockstepKnobGuard(unittest.TestCase):
45
+ """Module globals are read at call time; restore whatever a test patches."""
46
+
47
+ def setUp(self):
48
+ self._saved = {
49
+ name: getattr(proxy, name)
50
+ for name in (
51
+ "PROXY_LOCKSTEP_BREAK",
52
+ "PROXY_LOCKSTEP_PIVOT_AT",
53
+ "PROXY_LOCKSTEP_HARD_AT",
54
+ "UAP_LOOP_INCIDENTS",
55
+ )
56
+ }
57
+ self._tmp = tempfile.TemporaryDirectory()
58
+ proxy.UAP_LOOP_INCIDENTS = os.path.join(self._tmp.name, "incidents.jsonl")
59
+
60
+ def tearDown(self):
61
+ for name, value in self._saved.items():
62
+ setattr(proxy, name, value)
63
+ self._tmp.cleanup()
64
+
65
+ def _records(self):
66
+ if not os.path.exists(proxy.UAP_LOOP_INCIDENTS):
67
+ return []
68
+ with open(proxy.UAP_LOOP_INCIDENTS, encoding="utf-8") as fh:
69
+ return [json.loads(line) for line in fh if line.strip()]
70
+
71
+
72
+ class StreakMathTests(LockstepKnobGuard):
73
+ def test_single_signal_does_not_arm(self):
74
+ m = proxy.SessionMonitor()
75
+ for _ in range(4):
76
+ m.note_tool_result_error(ERR)
77
+ self.assertEqual(proxy._lockstep_streak(m), 0) # error only
78
+ m2 = proxy.SessionMonitor()
79
+ for i in range(4):
80
+ m2.note_doubling_signal(FP, ERR, msg_count=i, result_error=True)
81
+ self.assertEqual(proxy._lockstep_streak(m2), 0) # identical call only
82
+
83
+ def test_lockstep_is_the_shallower_streak(self):
84
+ m = proxy.SessionMonitor()
85
+ _drive_lockstep(m, 3)
86
+ self.assertEqual(proxy._lockstep_streak(m), 3)
87
+ # a different error restarts the signature streak at 1 -> the lockstep
88
+ # drops to the shallower side even though the call streak survives
89
+ m.note_tool_result_error("Error: something else entirely\n at y:9")
90
+ self.assertEqual(proxy._lockstep_streak(m), 1)
91
+ # a clean result zeroes the signature side -> lockstep fully broken
92
+ m.note_tool_result_error("ok, all green")
93
+ self.assertEqual(proxy._lockstep_streak(m), 0)
94
+
95
+ def test_disabled_globally(self):
96
+ proxy.PROXY_LOCKSTEP_BREAK = False
97
+ m = proxy.SessionMonitor()
98
+ _drive_lockstep(m, 5)
99
+ self.assertEqual(proxy._lockstep_streak(m), 0)
100
+ self.assertFalse(proxy._lockstep_active(m))
101
+
102
+
103
+ class LadderTests(LockstepKnobGuard):
104
+ def _body(self):
105
+ # Ends in a tool result, the way an unattended tool loop looks on the
106
+ # wire; a trailing user-text message is the human re-arm signal.
107
+ return {
108
+ "messages": [
109
+ {"role": "user", "content": "fix it"},
110
+ {"role": "assistant", "content": "retrying"},
111
+ {"role": "tool", "content": ERR},
112
+ ],
113
+ "tool_choice": "auto",
114
+ }
115
+
116
+ def test_pivot_directive_at_threshold(self):
117
+ m = proxy.SessionMonitor()
118
+ _drive_lockstep(m, proxy.PROXY_LOCKSTEP_PIVOT_AT)
119
+ body = self._body()
120
+ proxy._maybe_lockstep_escalate(body, m)
121
+ sysmsg = body["messages"][0]
122
+ self.assertEqual(sysmsg["role"], "system")
123
+ self.assertIn("LOOP-ESCALATE", sysmsg["content"])
124
+ self.assertIn("different approaches", sysmsg["content"])
125
+ self.assertEqual(m.lockstep_fires, 1)
126
+ # advisory: tool_choice untouched
127
+ self.assertEqual(body["tool_choice"], "auto")
128
+ recs = self._records()
129
+ self.assertEqual([r["outcome"] for r in recs], ["pivot"])
130
+
131
+ def test_final_warning_on_second_fire(self):
132
+ m = proxy.SessionMonitor()
133
+ _drive_lockstep(m, proxy.PROXY_LOCKSTEP_PIVOT_AT)
134
+ proxy._maybe_lockstep_escalate(self._body(), m)
135
+ _drive_lockstep(m, 1)
136
+ body = self._body()
137
+ proxy._maybe_lockstep_escalate(body, m)
138
+ self.assertIn("FINAL WARNING", body["messages"][0]["content"])
139
+ self.assertEqual(m.lockstep_fires, 2)
140
+ self.assertEqual(
141
+ [r["outcome"] for r in self._records()], ["pivot", "final_warning"]
142
+ )
143
+
144
+ def test_hard_stop_at_hard_at(self):
145
+ m = proxy.SessionMonitor()
146
+ _drive_lockstep(m, proxy.PROXY_LOCKSTEP_HARD_AT)
147
+ m.lockstep_fires = 2 # both directives already ignored
148
+ with self.assertRaises(proxy.LockstepHardBlock) as ctx:
149
+ proxy._maybe_lockstep_escalate(self._body(), m)
150
+ # the distinct subclass still flows through the single 400 handler
151
+ self.assertIsInstance(ctx.exception, proxy.ErrorLoopHardBlock)
152
+ self.assertIn("LOOP-ESCALATE hard stop", str(ctx.exception))
153
+ self.assertIn("uap loops", str(ctx.exception))
154
+ self.assertEqual(m.lockstep_blocks, 1)
155
+ recs = self._records()
156
+ self.assertEqual(recs[-1]["outcome"], "hard_blocked")
157
+ self.assertEqual(recs[-1]["blocks"], 1)
158
+
159
+ def test_hard_stop_requires_ignored_directives(self):
160
+ # streak deep but directives never injected (e.g. knob flipped on
161
+ # mid-loop): escalate through the ladder instead of jumping to a 400.
162
+ m = proxy.SessionMonitor()
163
+ _drive_lockstep(m, proxy.PROXY_LOCKSTEP_HARD_AT + 2)
164
+ body = self._body()
165
+ proxy._maybe_lockstep_escalate(body, m)
166
+ self.assertEqual(m.lockstep_fires, 1)
167
+ self.assertEqual(m.lockstep_blocks, 0)
168
+
169
+ def test_resent_transcript_does_not_refire_or_spam_ledger(self):
170
+ # A client retry (5xx / stream abort) resends the SAME trailing
171
+ # transcript: the streak does not advance, so no new directive and no
172
+ # new ledger record.
173
+ m = proxy.SessionMonitor()
174
+ _drive_lockstep(m, proxy.PROXY_LOCKSTEP_PIVOT_AT)
175
+ proxy._maybe_lockstep_escalate(self._body(), m)
176
+ self.assertEqual(m.lockstep_fires, 1)
177
+ proxy._maybe_lockstep_escalate(self._body(), m) # the retry
178
+ self.assertEqual(m.lockstep_fires, 1)
179
+ self.assertEqual(len(self._records()), 1)
180
+
181
+ def test_hard_stop_retry_refuses_without_duplicate_record(self):
182
+ m = proxy.SessionMonitor()
183
+ _drive_lockstep(m, proxy.PROXY_LOCKSTEP_HARD_AT)
184
+ m.lockstep_fires = 2
185
+ m.lockstep_last_fire_streak = proxy.PROXY_LOCKSTEP_HARD_AT - 1
186
+ with self.assertRaises(proxy.ErrorLoopHardBlock):
187
+ proxy._maybe_lockstep_escalate(self._body(), m)
188
+ with self.assertRaises(proxy.ErrorLoopHardBlock):
189
+ proxy._maybe_lockstep_escalate(self._body(), m) # client retry
190
+ self.assertEqual(m.lockstep_blocks, 2) # every retry refused
191
+ hard = [r for r in self._records() if r["outcome"] == "hard_blocked"]
192
+ self.assertEqual(len(hard), 1) # but recorded once
193
+
194
+ def test_fresh_user_turn_rearms(self):
195
+ m = proxy.SessionMonitor()
196
+ _drive_lockstep(m, proxy.PROXY_LOCKSTEP_HARD_AT)
197
+ m.lockstep_fires = 2
198
+ m.lockstep_last_fire_streak = proxy.PROXY_LOCKSTEP_HARD_AT
199
+ body = {
200
+ "messages": [
201
+ {"role": "user", "content": "tool result..."},
202
+ {"role": "user", "content": "human stepping in"},
203
+ ]
204
+ }
205
+ proxy._maybe_lockstep_escalate(body, m) # must NOT raise
206
+ self.assertEqual(m.lockstep_fires, 0)
207
+ self.assertEqual(m.lockstep_last_fire_streak, 0)
208
+ self.assertEqual(m.lockstep_blocks, 0)
209
+
210
+ def test_second_loop_climbs_the_full_ladder(self):
211
+ # A resolved loop must not leak its ladder position into the next one:
212
+ # the second loop gets its own pivot and final warning before any stop.
213
+ m = proxy.SessionMonitor()
214
+ _drive_lockstep(m, proxy.PROXY_LOCKSTEP_PIVOT_AT)
215
+ proxy._maybe_lockstep_escalate(self._body(), m)
216
+ _drive_lockstep(m, 1)
217
+ proxy._maybe_lockstep_escalate(self._body(), m)
218
+ self.assertEqual(m.lockstep_fires, 2)
219
+ # loop A resolves: clean result + a different (or no) call
220
+ m.note_tool_result_error("ok, all green")
221
+ m.note_doubling_signal("", "ok, all green", msg_count=900, result_error=False)
222
+ proxy._maybe_lockstep_escalate(self._body(), m) # streak 0 -> ladder reset
223
+ self.assertEqual(m.lockstep_fires, 0)
224
+ self.assertEqual(m.lockstep_last_fire_streak, 0)
225
+ # loop B (a different failing call) climbs from the pivot tier again
226
+ fp_b = proxy._tool_call_fingerprint(
227
+ {"name": "edit", "input": {"path": "/x.py", "old": "a", "new": "b"}}
228
+ )
229
+ for i in range(proxy.PROXY_LOCKSTEP_PIVOT_AT):
230
+ m.note_tool_result_error("Error: EACCES permission denied\n at x:1")
231
+ m.note_doubling_signal(fp_b, "Error: EACCES", msg_count=1000 + i, result_error=True)
232
+ body = self._body()
233
+ proxy._maybe_lockstep_escalate(body, m)
234
+ self.assertIn("LOOP-ESCALATE", body["messages"][0]["content"])
235
+ self.assertNotIn("FINAL WARNING", body["messages"][0]["content"])
236
+ self.assertEqual(m.lockstep_fires, 1)
237
+ self.assertEqual(
238
+ [r["outcome"] for r in self._records()],
239
+ ["pivot", "final_warning", "pivot"],
240
+ )
241
+
242
+
243
+ class YieldTests(LockstepKnobGuard):
244
+ def test_error_loop_and_doubling_yield_to_lockstep(self):
245
+ m = proxy.SessionMonitor()
246
+ _drive_lockstep(m, max(proxy.PROXY_ERROR_LOOP_THRESHOLD, proxy.PROXY_DOUBLING_THRESHOLD))
247
+ self.assertTrue(proxy._lockstep_active(m))
248
+ body = {"messages": [{"role": "user", "content": "go"}], "tool_choice": "required"}
249
+ proxy._maybe_inject_error_loop_break(body, m)
250
+ proxy._maybe_inject_doubling_break(body, m)
251
+ self.assertEqual(m.error_loop_fires, 0)
252
+ self.assertEqual(m.doubling_break_fires, 0)
253
+ self.assertEqual(len(body["messages"]), 1) # neither injected
254
+
255
+ def test_error_loop_still_fires_off_lockstep(self):
256
+ # varied edits, same error: doubling streak stays 0, so the lockstep
257
+ # never arms and ERROR-LOOP keeps owning this shape.
258
+ m = proxy.SessionMonitor()
259
+ for _ in range(proxy.PROXY_ERROR_LOOP_THRESHOLD):
260
+ m.note_tool_result_error(ERR)
261
+ self.assertFalse(proxy._lockstep_active(m))
262
+ body = {"messages": [{"role": "user", "content": "go"}]}
263
+ proxy._maybe_inject_error_loop_break(body, m)
264
+ self.assertEqual(m.error_loop_fires, 1)
265
+
266
+
267
+ class IncidentRecordTests(LockstepKnobGuard):
268
+ def test_record_shape_and_arg_redaction(self):
269
+ m = proxy.SessionMonitor()
270
+ m.session_id = "sess-123"
271
+ _drive_lockstep(m, proxy.PROXY_LOCKSTEP_PIVOT_AT)
272
+ proxy._maybe_lockstep_escalate(
273
+ {"messages": [{"role": "user", "content": "go"}]}, m
274
+ )
275
+ (rec,) = self._records()
276
+ self.assertEqual(rec["v"], 1)
277
+ self.assertEqual(rec["guard"], "lockstep")
278
+ self.assertEqual(rec["outcome"], "pivot")
279
+ self.assertEqual(rec["session"], "sess-123")
280
+ self.assertEqual(rec["tool"], "bash")
281
+ self.assertTrue(rec["fingerprint"])
282
+ self.assertNotIn(FP, json.dumps(rec)) # raw args never persisted
283
+ self.assertNotIn("ssh host", json.dumps(rec))
284
+ # the persisted signature is the normalized one (literals stripped)
285
+ self.assertIn("traceback", rec["error_signature"])
286
+
287
+ def test_ledger_disabled_by_empty_path(self):
288
+ proxy.UAP_LOOP_INCIDENTS = ""
289
+ m = proxy.SessionMonitor()
290
+ _drive_lockstep(m, proxy.PROXY_LOCKSTEP_PIVOT_AT)
291
+ proxy._maybe_lockstep_escalate(
292
+ {"messages": [{"role": "user", "content": "go"}]}, m
293
+ )
294
+ self.assertEqual(self._records(), [])
295
+
296
+ def test_ledger_created_owner_only(self):
297
+ m = proxy.SessionMonitor()
298
+ _drive_lockstep(m, proxy.PROXY_LOCKSTEP_PIVOT_AT)
299
+ proxy._maybe_lockstep_escalate(
300
+ {"messages": [{"role": "user", "content": "go"}]}, m
301
+ )
302
+ import stat
303
+
304
+ mode = stat.S_IMODE(os.stat(proxy.UAP_LOOP_INCIDENTS).st_mode)
305
+ self.assertEqual(mode, 0o600)
306
+
307
+ def test_symlink_ledger_refused(self):
308
+ target = os.path.join(self._tmp.name, "target.jsonl")
309
+ link = os.path.join(self._tmp.name, "link.jsonl")
310
+ with open(target, "w", encoding="utf-8") as fh:
311
+ fh.write("")
312
+ os.symlink(target, link)
313
+ proxy.UAP_LOOP_INCIDENTS = link
314
+ m = proxy.SessionMonitor()
315
+ _drive_lockstep(m, proxy.PROXY_LOCKSTEP_PIVOT_AT)
316
+ proxy._maybe_lockstep_escalate(
317
+ {"messages": [{"role": "user", "content": "go"}]}, m
318
+ )
319
+ with open(target, encoding="utf-8") as fh:
320
+ self.assertEqual(fh.read(), "") # untouched
321
+
322
+
323
+ if __name__ == "__main__":
324
+ unittest.main()
@@ -447,10 +447,16 @@ class ColdBackendProbeBudgetTest(unittest.TestCase):
447
447
  class WireModelReconciliationTest(unittest.TestCase):
448
448
  """Send a model the backend answers to, or the turn is lost to a 404.
449
449
 
450
- llama.cpp ignored the OpenAI `model` field; ninfer validates it. With the
451
- local-only sentinel every advertised id is served locally, so the four
452
- `claude-*` ids the proxy advertises for SDK compatibility all came back
453
- `404 model_not_found` -- measured against the live backend.
450
+ llama.cpp ignores the OpenAI `model` field; the ninfer-serve backend in
451
+ place on 2026-08-19 VALIDATED it. With the local-only sentinel every
452
+ advertised id is served locally, so the four `claude-*` ids the proxy
453
+ advertises for SDK compatibility all came back `404 model_not_found` --
454
+ measured against that backend.
455
+
456
+ As of 2026-09-21 the backend is llama.cpp again and ignores the field, so
457
+ this reconciliation is belt-and-braces rather than load-bearing today. The
458
+ behaviour is still worth pinning: the next engine may validate again, and
459
+ the cost of being wrong is every turn lost to a 404.
454
460
  """
455
461
 
456
462
  def _ap(self, ids=("qwen3.8-27b",)):