@miller-tech/uap 2.11.1 → 2.16.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/config/capacity-policy.json +15 -3
- package/config/model-profiles/qwen38.json +42 -34
- package/dist/.tsbuildinfo +1 -1
- package/dist/bin/cli.js +39 -0
- package/dist/bin/cli.js.map +1 -1
- package/dist/capacity/policy.d.ts +15 -0
- package/dist/capacity/policy.d.ts.map +1 -1
- package/dist/capacity/policy.js +23 -0
- package/dist/capacity/policy.js.map +1 -1
- package/dist/capacity/probe.d.ts.map +1 -1
- package/dist/capacity/probe.js +6 -1
- package/dist/capacity/probe.js.map +1 -1
- package/dist/cli/deliver.d.ts.map +1 -1
- package/dist/cli/deliver.js +148 -2
- package/dist/cli/deliver.js.map +1 -1
- package/dist/cli/inference.d.ts +13 -0
- package/dist/cli/inference.d.ts.map +1 -0
- package/dist/cli/inference.js +132 -0
- package/dist/cli/inference.js.map +1 -0
- package/dist/cli/loops.d.ts +39 -0
- package/dist/cli/loops.d.ts.map +1 -0
- package/dist/cli/loops.js +154 -0
- package/dist/cli/loops.js.map +1 -0
- package/dist/cli/wizard-config.d.ts.map +1 -1
- package/dist/cli/wizard-config.js +5 -1
- package/dist/cli/wizard-config.js.map +1 -1
- package/dist/cli/worktree.d.ts.map +1 -1
- package/dist/cli/worktree.js +8 -4
- package/dist/cli/worktree.js.map +1 -1
- package/dist/delivery/adversarial-attack.d.ts +99 -0
- package/dist/delivery/adversarial-attack.d.ts.map +1 -0
- package/dist/delivery/adversarial-attack.js +326 -0
- package/dist/delivery/adversarial-attack.js.map +1 -0
- package/dist/delivery/adversarial-gate.d.ts +120 -0
- package/dist/delivery/adversarial-gate.d.ts.map +1 -0
- package/dist/delivery/adversarial-gate.js +259 -0
- package/dist/delivery/adversarial-gate.js.map +1 -0
- package/dist/delivery/adversarial-sanction.d.ts +52 -0
- package/dist/delivery/adversarial-sanction.d.ts.map +1 -0
- package/dist/delivery/adversarial-sanction.js +134 -0
- package/dist/delivery/adversarial-sanction.js.map +1 -0
- package/dist/delivery/agentic-executor.d.ts +69 -22
- package/dist/delivery/agentic-executor.d.ts.map +1 -1
- package/dist/delivery/agentic-executor.js +152 -8
- package/dist/delivery/agentic-executor.js.map +1 -1
- package/dist/delivery/applier.d.ts +8 -0
- package/dist/delivery/applier.d.ts.map +1 -1
- package/dist/delivery/applier.js +3 -1
- package/dist/delivery/applier.js.map +1 -1
- package/dist/delivery/convergence-loop.d.ts +15 -0
- package/dist/delivery/convergence-loop.d.ts.map +1 -1
- package/dist/delivery/convergence-loop.js.map +1 -1
- package/dist/delivery/criteria-lint.d.ts +74 -0
- package/dist/delivery/criteria-lint.d.ts.map +1 -0
- package/dist/delivery/criteria-lint.js +134 -0
- package/dist/delivery/criteria-lint.js.map +1 -0
- package/dist/delivery/deliver-toggle.d.ts +21 -0
- package/dist/delivery/deliver-toggle.d.ts.map +1 -0
- package/dist/delivery/deliver-toggle.js +40 -0
- package/dist/delivery/deliver-toggle.js.map +1 -0
- package/dist/delivery/delivery-evidence.d.ts +109 -0
- package/dist/delivery/delivery-evidence.d.ts.map +1 -0
- package/dist/delivery/delivery-evidence.js +139 -0
- package/dist/delivery/delivery-evidence.js.map +1 -0
- package/dist/delivery/epic-mission.d.ts +5 -1
- package/dist/delivery/epic-mission.d.ts.map +1 -1
- package/dist/delivery/epic-mission.js +34 -6
- package/dist/delivery/epic-mission.js.map +1 -1
- package/dist/delivery/execution-gate.d.ts.map +1 -1
- package/dist/delivery/execution-gate.js +90 -7
- package/dist/delivery/execution-gate.js.map +1 -1
- package/dist/delivery/index.d.ts +9 -1
- package/dist/delivery/index.d.ts.map +1 -1
- package/dist/delivery/index.js +9 -1
- package/dist/delivery/index.js.map +1 -1
- package/dist/delivery/journey-depth.d.ts +61 -0
- package/dist/delivery/journey-depth.d.ts.map +1 -0
- package/dist/delivery/journey-depth.js +91 -0
- package/dist/delivery/journey-depth.js.map +1 -0
- package/dist/delivery/master-pipeline.d.ts +50 -0
- package/dist/delivery/master-pipeline.d.ts.map +1 -0
- package/dist/delivery/master-pipeline.js +52 -0
- package/dist/delivery/master-pipeline.js.map +1 -0
- package/dist/delivery/mission-acceptance.d.ts.map +1 -1
- package/dist/delivery/mission-acceptance.js +54 -2
- package/dist/delivery/mission-acceptance.js.map +1 -1
- package/dist/delivery/orchestrated-mission.d.ts +4 -1
- package/dist/delivery/orchestrated-mission.d.ts.map +1 -1
- package/dist/delivery/orchestrated-mission.js +18 -9
- package/dist/delivery/orchestrated-mission.js.map +1 -1
- package/dist/delivery/plan-check.d.ts +1 -0
- package/dist/delivery/plan-check.d.ts.map +1 -1
- package/dist/delivery/plan-check.js +48 -0
- package/dist/delivery/plan-check.js.map +1 -1
- package/dist/delivery/task-orchestrator.d.ts +16 -9
- package/dist/delivery/task-orchestrator.d.ts.map +1 -1
- package/dist/delivery/task-orchestrator.js +4 -2
- package/dist/delivery/task-orchestrator.js.map +1 -1
- package/dist/delivery/task-workspace.d.ts +35 -5
- package/dist/delivery/task-workspace.d.ts.map +1 -1
- package/dist/delivery/task-workspace.js +53 -7
- package/dist/delivery/task-workspace.js.map +1 -1
- package/dist/delivery/test-oracle-additive.d.ts +26 -6
- package/dist/delivery/test-oracle-additive.d.ts.map +1 -1
- package/dist/delivery/test-oracle-additive.js +40 -15
- package/dist/delivery/test-oracle-additive.js.map +1 -1
- package/dist/delivery/user-validation.d.ts +14 -0
- package/dist/delivery/user-validation.d.ts.map +1 -1
- package/dist/delivery/user-validation.js +103 -9
- package/dist/delivery/user-validation.js.map +1 -1
- package/dist/delivery/verifier-ladder.d.ts.map +1 -1
- package/dist/delivery/verifier-ladder.js +24 -6
- package/dist/delivery/verifier-ladder.js.map +1 -1
- package/dist/inference/analysis.d.ts +138 -0
- package/dist/inference/analysis.d.ts.map +1 -0
- package/dist/inference/analysis.js +243 -0
- package/dist/inference/analysis.js.map +1 -0
- package/dist/inference/probe.d.ts +85 -0
- package/dist/inference/probe.d.ts.map +1 -0
- package/dist/inference/probe.js +403 -0
- package/dist/inference/probe.js.map +1 -0
- package/dist/models/types.d.ts.map +1 -1
- package/dist/models/types.js +45 -18
- package/dist/models/types.js.map +1 -1
- package/dist/self-tuning/profiles/qwen38.d.ts +17 -8
- package/dist/self-tuning/profiles/qwen38.d.ts.map +1 -1
- package/dist/self-tuning/profiles/qwen38.js +21 -11
- package/dist/self-tuning/profiles/qwen38.js.map +1 -1
- package/dist/utils/model-slots.d.ts +11 -5
- package/dist/utils/model-slots.d.ts.map +1 -1
- package/dist/utils/model-slots.js +11 -5
- package/dist/utils/model-slots.js.map +1 -1
- package/docs/INDEX.md +1 -0
- package/docs/guides/CAPACITY_POLICY.md +24 -0
- package/docs/guides/INFERENCE_HEALTH.md +184 -0
- package/docs/guides/PROXY.md +1 -0
- package/docs/guides/QWEN38_EXL3.md +268 -0
- package/docs/plans/hlg-master-architecture-uplift-2026-09-24.md +67 -0
- package/docs/reference/CONFIGURATION.md +10 -1
- package/docs/reference/CONFIGURATION_REFERENCE.md +14 -0
- package/package.json +2 -2
- package/scripts/lib/llama-upstream.sh +7 -1
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/src/policies/enforcers/__pycache__/expert_review_required.cpython-312.pyc +0 -0
- package/src/policies/enforcers/__pycache__/quality_metrics_gate.cpython-312.pyc +0 -0
- package/src/policies/enforcers/expert_review_required.py +1 -4
- package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
- package/tools/agents/scripts/anthropic_proxy.py +374 -16
- package/tools/agents/scripts/confidence_escalation.py +4 -2
- package/tools/agents/scripts/project_telemetry.py +1 -0
- package/tools/agents/scripts/tool_risk.py +1 -0
- package/tools/agents/scripts/tool_risk_calibrate.py +1 -0
- package/tools/agents/scripts/toolcall_path_normalizer.py +1 -0
- package/tools/agents/tests/test_anthropic_proxy_streaming.py +52 -1
- package/tools/agents/tests/test_confidence_escalation.py +3 -2
- package/tools/agents/tests/test_count_tokens_scale_per_session.py +126 -0
- package/tools/agents/tests/test_expert_review_pr_scope.py +10 -11
- package/tools/agents/tests/test_lockstep_escalation.py +324 -0
- 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
|
|
376
|
-
# is not evidence
|
|
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:
|
|
211
|
-
|
|
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
|
|
250
|
-
#
|
|
251
|
-
#
|
|
252
|
-
self.assert_ship("
|
|
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
|
|
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("
|
|
256
|
+
self.assert_ship("env git -C /repo push origin master")
|
|
258
257
|
|
|
259
|
-
def
|
|
260
|
-
self.assert_ship("
|
|
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
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
`404 model_not_found` --
|
|
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",)):
|