@miller-tech/uap 2.36.4 → 2.39.1

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 (100) hide show
  1. package/config/model-profiles/qwen38.json +3 -3
  2. package/config/model-registry.json +96 -6
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/bin/cli.js +57 -0
  5. package/dist/bin/cli.js.map +1 -1
  6. package/dist/cli/deliver.d.ts +46 -0
  7. package/dist/cli/deliver.d.ts.map +1 -1
  8. package/dist/cli/deliver.js +155 -36
  9. package/dist/cli/deliver.js.map +1 -1
  10. package/dist/cli/models.d.ts +11 -1
  11. package/dist/cli/models.d.ts.map +1 -1
  12. package/dist/cli/models.js +164 -3
  13. package/dist/cli/models.js.map +1 -1
  14. package/dist/cli/workflow.d.ts +36 -0
  15. package/dist/cli/workflow.d.ts.map +1 -0
  16. package/dist/cli/workflow.js +246 -0
  17. package/dist/cli/workflow.js.map +1 -0
  18. package/dist/delivery/run-state.d.ts +1 -1
  19. package/dist/delivery/run-state.d.ts.map +1 -1
  20. package/dist/delivery/run-state.js +1 -1
  21. package/dist/delivery/run-state.js.map +1 -1
  22. package/dist/delivery/team-solve-deliver.d.ts +83 -0
  23. package/dist/delivery/team-solve-deliver.d.ts.map +1 -0
  24. package/dist/delivery/team-solve-deliver.js +191 -0
  25. package/dist/delivery/team-solve-deliver.js.map +1 -0
  26. package/dist/inference/model-files.d.ts +111 -0
  27. package/dist/inference/model-files.d.ts.map +1 -0
  28. package/dist/inference/model-files.js +172 -0
  29. package/dist/inference/model-files.js.map +1 -0
  30. package/dist/placement/registry.d.ts +650 -0
  31. package/dist/placement/registry.d.ts.map +1 -1
  32. package/dist/placement/registry.js +21 -0
  33. package/dist/placement/registry.js.map +1 -1
  34. package/dist/workflow/loader.d.ts +34 -0
  35. package/dist/workflow/loader.d.ts.map +1 -0
  36. package/dist/workflow/loader.js +232 -0
  37. package/dist/workflow/loader.js.map +1 -0
  38. package/dist/workflow/runner.d.ts +90 -0
  39. package/dist/workflow/runner.d.ts.map +1 -0
  40. package/dist/workflow/runner.js +416 -0
  41. package/dist/workflow/runner.js.map +1 -0
  42. package/dist/workflow/types.d.ts +400 -0
  43. package/dist/workflow/types.d.ts.map +1 -0
  44. package/dist/workflow/types.js +147 -0
  45. package/dist/workflow/types.js.map +1 -0
  46. package/docs/INDEX.md +1 -0
  47. package/docs/architecture/adr/0013-declarative-workflow-layer.md +99 -0
  48. package/docs/architecture/adr/0014-loop-steps-and-team-solve-deliver.md +142 -0
  49. package/docs/benchmarks/LCB_DROID_SMOKE.md +111 -0
  50. package/docs/guides/DELIVER.md +3 -1
  51. package/docs/guides/WORKFLOWS.md +179 -0
  52. package/docs/plans/model-performance-proof-options.md +39 -0
  53. package/docs/reference/CLI.md +33 -0
  54. package/docs/reference/CONFIGURATION.md +1 -0
  55. package/docs/reference/FEATURES.md +18 -0
  56. package/examples/auto-coordinator-example.js +127 -0
  57. package/examples/demo-order-solid.ts +19 -0
  58. package/examples/order-processing-strategy-pattern.ts +186 -0
  59. package/examples/order-processor-refactored.ts +473 -0
  60. package/examples/order-solid-refactor.ts +830 -0
  61. package/examples/refactored-order-processing.ts +106 -0
  62. package/examples/run-order-processing.ts +48 -0
  63. package/examples/solid-order-processing.ts +324 -0
  64. package/examples/strategy-pattern-example.ts +100 -0
  65. package/examples/workflows/team-solve/README.md +85 -0
  66. package/examples/workflows/team-solve/check-samples.sh +50 -0
  67. package/examples/workflows/team-solve/sample/task.md +22 -0
  68. package/examples/workflows/team-solve/sample/tests/1.in +2 -0
  69. package/examples/workflows/team-solve/sample/tests/1.out +1 -0
  70. package/examples/workflows/team-solve/sample/tests/2.in +2 -0
  71. package/examples/workflows/team-solve/sample/tests/2.out +1 -0
  72. package/examples/workflows/team-solve/sample/tests/3.in +2 -0
  73. package/examples/workflows/team-solve/sample/tests/3.out +1 -0
  74. package/examples/workflows/team-solve/sample/tests/4.in +2 -0
  75. package/examples/workflows/team-solve/sample/tests/4.out +1 -0
  76. package/examples/workflows/team-solve/sample/tests/5.in +1 -0
  77. package/examples/workflows/team-solve/sample/tests/5.out +1 -0
  78. package/examples/workflows/team-solve/steps/ideate-a.md +26 -0
  79. package/examples/workflows/team-solve/steps/ideate-b.md +28 -0
  80. package/examples/workflows/team-solve/steps/plan.md +20 -0
  81. package/examples/workflows/team-solve/steps/repair.md +25 -0
  82. package/examples/workflows/team-solve/steps/review.md +29 -0
  83. package/examples/workflows/team-solve/steps/solve.md +26 -0
  84. package/examples/workflows/team-solve/workflow.yaml +78 -0
  85. package/examples/workflows/team-solve-deliver/README.md +84 -0
  86. package/examples/workflows/team-solve-deliver/steps/ideate-a.md +25 -0
  87. package/examples/workflows/team-solve-deliver/steps/ideate-b.md +28 -0
  88. package/examples/workflows/team-solve-deliver/steps/plan.md +25 -0
  89. package/examples/workflows/team-solve-deliver/steps/repair.md +20 -0
  90. package/examples/workflows/team-solve-deliver/steps/review.md +31 -0
  91. package/examples/workflows/team-solve-deliver/steps/solve.md +28 -0
  92. package/examples/workflows/team-solve-deliver/steps/verify.md +9 -0
  93. package/examples/workflows/team-solve-deliver/workflow.yaml +74 -0
  94. package/package.json +2 -1
  95. package/src/policies/enforcers/__pycache__/_common.cpython-311.pyc +0 -0
  96. package/src/policies/enforcers/__pycache__/expert_review_required.cpython-311.pyc +0 -0
  97. package/src/policies/enforcers/__pycache__/quality_metrics_gate.cpython-311.pyc +0 -0
  98. package/templates/hooks/__pycache__/deliver_autoroute.cpython-311.pyc +0 -0
  99. package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-311.pyc +0 -0
  100. package/tools/agents/scripts/anthropic_proxy.py +163 -18
@@ -0,0 +1,74 @@
1
+ # team-solve-deliver — the team directs the deliver convergence loop.
2
+ #
3
+ # The combination of the two engines: team-solve's role structure and ledger
4
+ # discipline (fresh contexts, files as the only channel, cross-correction)
5
+ # wrapped around deliver's convergence machinery (tools + the project's real
6
+ # completion gates). Roles think; loops build and verify.
7
+ #
8
+ # plan ──┬── ideate-a ──┬── solve ── review ── repair ── verify
9
+ # └── ideate-b ──┘ (loop) (judge) (loop) (loop, gates-only)
10
+ #
11
+ # solve/repair/verify are `kind: loop` steps — they execute through a
12
+ # CALLER-INJECTED convergence loop (uap deliver wires its own machinery and
13
+ # auto-engages this workflow for high/critical missions). A standalone
14
+ # `uap workflow run team-solve-deliver` fails closed: no loop provider.
15
+ name: team-solve-deliver
16
+ description: >
17
+ team-solve directing the deliver convergence loop for repo missions: an
18
+ approach plan, two parallel ideations from different engineering families,
19
+ a gates-driven solve loop, a fresh-context review, a review-seeded repair
20
+ loop, and a verify-only verdict. Auto-engaged by `uap deliver` on
21
+ high/critical complexity; force with `uap deliver --team-solve`.
22
+ parallel: 2
23
+ # 0 engine-level step retries: the solve/repair loops already converge
24
+ # internally (until-delivered + escalation inside the mission dispatch), and
25
+ # a workflow-level retry of `solve` would re-run the ENTIRE mission dispatch
26
+ # after it already burned its turn budget — doubling the hardest-tier spend
27
+ # and bypassing this workflow's own review→repair discipline.
28
+ maxRepairsPerStep: 0
29
+ steps:
30
+ - id: plan
31
+ kind: agent
32
+ title: Approach planner (judge rail)
33
+ prompt: steps/plan.md
34
+ writes: [plan.md]
35
+ requires: []
36
+ - id: ideate-a
37
+ kind: agent
38
+ title: Ideator A (minimal-change family)
39
+ prompt: steps/ideate-a.md
40
+ reads: [plan.md]
41
+ writes: [proposal-a.md]
42
+ requires: [plan]
43
+ - id: ideate-b
44
+ kind: agent
45
+ title: Ideator B (structural family)
46
+ prompt: steps/ideate-b.md
47
+ reads: [plan.md]
48
+ writes: [proposal-b.md]
49
+ requires: [plan]
50
+ - id: solve
51
+ kind: loop
52
+ title: Solve (convergence loop, project gates)
53
+ prompt: steps/solve.md
54
+ reads: [plan.md, proposal-a.md, proposal-b.md]
55
+ requires: [ideate-a, ideate-b]
56
+ - id: review
57
+ kind: agent
58
+ title: Fresh-context reviewer (judge rail)
59
+ prompt: steps/review.md
60
+ reads: [plan.md]
61
+ writes: [repair.md]
62
+ requires: [solve]
63
+ - id: repair
64
+ kind: loop
65
+ title: Repair (convergence loop, review-seeded)
66
+ prompt: steps/repair.md
67
+ reads: [repair.md]
68
+ requires: [review]
69
+ - id: verify
70
+ kind: loop
71
+ title: Verdict (gates only)
72
+ prompt: steps/verify.md
73
+ verifyOnly: true
74
+ requires: [repair]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miller-tech/uap",
3
- "version": "2.36.4",
3
+ "version": "2.39.1",
4
4
  "description": "Autonomous AI agent memory system with CLAUDE.md protocol enforcement",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -116,6 +116,7 @@
116
116
  "dist",
117
117
  "web",
118
118
  "docs",
119
+ "examples",
119
120
  "config",
120
121
  "templates",
121
122
  "src/policies/schemas/policies",
@@ -1621,11 +1621,23 @@ PROXY_EMPTY_TOOL_LOOP_BREAK = int(
1621
1621
  # ever-growing transcript (observed: ctx 936%, model emits tool_calls 18/min
1622
1622
  # despite tool_choice=auto). After this many consecutive turns at/above the
1623
1623
  # ratio, strip tools entirely so the only possible response is a terminal text
1624
- # summary (end_turn), which ends the client's agentic loop. Ratio is set high
1625
- # enough that only a true runaway trips it — a merely-full session tops out near
1626
- # 100-130%, never 300%. 0 disables.
1624
+ # summary (end_turn), which ends the client's agentic loop.
1625
+ #
1626
+ # Ratio sits above the merely-full band but inside the runaway band. A
1627
+ # merely-full session tops out near 100-141%: a client that honestly believes
1628
+ # a ~200k window (PROXY_CLIENT_ASSUMED_WINDOW) and compacts at 92.5% sends at
1629
+ # most ~185k against a 131,072 rail. A window-blind runaway plateaus wherever
1630
+ # its own belief caps it, and that plateau can sit far below 300%: live
1631
+ # 2026-11-06 (droid + flash-next, cognition-engine), a client with no
1632
+ # maxContextLimit (assumed 250k) ramped 132% -> 198% in 4 minutes and then sat
1633
+ # at 190-198% for 4+ HOURS — every turn CRITICAL-PRUNEd blind to ~6 messages
1634
+ # (~24-40k actually sent upstream), findings evaporating, the model re-reading
1635
+ # the same files — because the 3.0 gate was written for 936% runaways and this
1636
+ # one never crossed 200%. 1.6 clears the merely-full band (141% plus one
1637
+ # ~19k-token turn of headroom) while ending that class of treadmill within
1638
+ # STREAK turns. 0 disables.
1627
1639
  PROXY_RAW_CTX_FINALIZE_RATIO = float(
1628
- os.environ.get("PROXY_RAW_CTX_FINALIZE_RATIO", "3.0")
1640
+ os.environ.get("PROXY_RAW_CTX_FINALIZE_RATIO", "1.6")
1629
1641
  )
1630
1642
  PROXY_RAW_CTX_FINALIZE_STREAK = int(
1631
1643
  os.environ.get("PROXY_RAW_CTX_FINALIZE_STREAK", "2")
@@ -2456,6 +2468,31 @@ PROXY_ATTRACTOR_TEMP_OVERRIDE = float(
2456
2468
  PROXY_ATTRACTOR_FINALIZE_THRESHOLD = max(1, int(
2457
2469
  os.environ.get("PROXY_ATTRACTOR_FINALIZE_THRESHOLD", "2")
2458
2470
  ))
2471
+ # Post-contamination low-temp clamp exemption. The standard contamination
2472
+ # reset clamps the next tool turn to temperature 0.10 (Cycle 15 Option 2:
2473
+ # deterministic output repairs malformed JSON). For DEGENERATE-CONTENT faults
2474
+ # (garbled args) determinism is exactly wrong: temp ~0 re-enters the same
2475
+ # degenerate attractor that triggered the reset (live 2026-10-30: 9
2476
+ # contamination-forced finalize stops in one day, the 0.10 clamp active on
2477
+ # every post-reset turn). Reset causes listed here keep the normal
2478
+ # PROXY_TOOL_TURN_TEMPERATURE instead of the 0.10 clamp. Empty string
2479
+ # disables the exemption entirely (always clamp).
2480
+ PROXY_CONTAMINATION_LOW_TEMP_EXEMPT_KINDS = {
2481
+ k.strip()
2482
+ for k in os.environ.get(
2483
+ "PROXY_CONTAMINATION_LOW_TEMP_EXEMPT_KINDS", "invalid_tool_args"
2484
+ ).split(",")
2485
+ if k.strip()
2486
+ }
2487
+ # Late garbled-retry output cap. The deployed env pins
2488
+ # PROXY_TOOL_TURN_MAX_TOKENS_GARBLED high (32768, raised 2026-04-22 so legit
2489
+ # multi-file heredoc edits don't truncate), which also gives a degenerate
2490
+ # generation maximum room to run. From this retry attempt onward the budget
2491
+ # drops to this cap: the observed legit commands (~4.5K tokens) still fit,
2492
+ # while runaway degeneration is starved of room (live 2026-10-30: every
2493
+ # garbled call was a long inline heredoc; short commands never garbled).
2494
+ # 0 disables the late cap.
2495
+ PROXY_GARBLE_LATE_CAP = int(os.environ.get("PROXY_GARBLE_LATE_CAP", "8192"))
2459
2496
  PROXY_AGENTIC_SUPPLEMENT_MODE = (
2460
2497
  os.environ.get("PROXY_AGENTIC_SUPPLEMENT_MODE", "clean").strip().lower()
2461
2498
  )
@@ -3119,6 +3156,7 @@ class SessionMonitor:
3119
3156
  invalid_tool_call_streak: int = 0 # consecutive invalid tool arg payloads
3120
3157
  required_tool_miss_streak: int = 0 # required tool turns with no tool call
3121
3158
  contamination_resets: int = 0 # how many contamination resets were applied
3159
+ contamination_cause: str = "" # dominant streak behind the last reset ("invalid_tool_args"/"malformed_payload"/"required_tool_miss"/"generic")
3122
3160
  last_fault_excerpt_hash: str = "" # hash of last TOOL RESPONSE ISSUE excerpt (attractor detection)
3123
3161
  attractor_correction_active: bool = False # next turn uses high-temp escape sampling
3124
3162
  forced_auto_cooldown_turns: int = 0 # temporary auto override turns remaining
@@ -9822,13 +9860,24 @@ def build_openai_request(
9822
9860
  client_temp,
9823
9861
  )
9824
9862
  else:
9825
- if monitor.contamination_resets > 0:
9863
+ # Cycle 15 Option 2: lower temperature after contamination resets —
9864
+ # EXCEPT when the reset was driven by an exempt cause: for
9865
+ # degenerate-content faults (garbled args) determinism re-enters
9866
+ # the same attractor that triggered the reset (live 2026-10-30).
9867
+ if (
9868
+ monitor.contamination_resets > 0
9869
+ and monitor.contamination_cause
9870
+ not in PROXY_CONTAMINATION_LOW_TEMP_EXEMPT_KINDS
9871
+ ):
9826
9872
  target_temp = min(target_temp, 0.1)
9827
9873
  if client_temp is None or client_temp > target_temp:
9828
9874
  openai_body["temperature"] = target_temp
9829
9875
  extra = ""
9830
9876
  if monitor.contamination_resets > 0:
9831
- extra = f" (post-contamination reset, resets={monitor.contamination_resets})"
9877
+ extra = (
9878
+ f" (post-contamination reset, resets={monitor.contamination_resets},"
9879
+ f" cause={monitor.contamination_cause or 'generic'})"
9880
+ )
9832
9881
  logger.info(
9833
9882
  "TOOL TURN TEMP: forcing temperature=%.2f (was %s) for tool-enabled request%s",
9834
9883
  target_temp,
@@ -10717,6 +10766,68 @@ def _hash_fault_excerpt(excerpt: str) -> str:
10717
10766
  return hashlib.sha256(normalized.encode("utf-8")).hexdigest()[:16]
10718
10767
 
10719
10768
 
10769
+ def _fault_fingerprint(
10770
+ excerpt: str, arg_excerpt: str, fault_tool_names: list[str]
10771
+ ) -> str:
10772
+ """Stable fingerprint of a tool-response fault for attractor-repeat detection.
10773
+
10774
+ Tool-call faults carry NO assistant text (every observed garble incident
10775
+ logged text_len=0), so hashing only the text excerpt yielded "" — falsy —
10776
+ and the attractor detector (hard reset + temp escape + lowered finalize
10777
+ threshold) never fired for the dominant fault class (live 2026-10-30:
10778
+ 9 contamination-forced finalize stops, none attractor-detected). Fall
10779
+ back to the garbled-args excerpt — its stable command head survives
10780
+ normalization, so the same degenerate approach hashes equal across
10781
+ resets — then the failing tool names.
10782
+ """
10783
+ if excerpt:
10784
+ return _hash_fault_excerpt(excerpt)
10785
+ if arg_excerpt:
10786
+ return _hash_fault_excerpt(arg_excerpt)
10787
+ if fault_tool_names:
10788
+ return _hash_fault_excerpt(
10789
+ "tool:" + ",".join(sorted(set(fault_tool_names)))
10790
+ )
10791
+ return ""
10792
+
10793
+
10794
+ def _retry_temperature(attempt: int, is_garbled: bool) -> float:
10795
+ """Retry temperature ladder for malformed/garbled tool-call retries.
10796
+
10797
+ Attempt 1: configured retry temp (deterministic first try).
10798
+ Attempt 2: 0.5 — escape the degenerate local minimum (Cycle 15 Option 3).
10799
+ Attempt 3+ garbled (degenerate CONTENT): attractor-escape temperature —
10800
+ determinism re-enters the same degenerate trajectory (live 2026-10-30:
10801
+ garbled attempts 1-4 all reproduced at 0.05/0.5 while short commands
10802
+ never garbled); malformed-JSON retries keep the deterministic ladder
10803
+ because structure repair wants determinism.
10804
+ """
10805
+ if is_garbled and attempt >= 3:
10806
+ return max(
10807
+ PROXY_ATTRACTOR_TEMP_OVERRIDE, PROXY_MALFORMED_TOOL_RETRY_TEMPERATURE
10808
+ )
10809
+ if attempt > 1:
10810
+ return 0.5
10811
+ return PROXY_MALFORMED_TOOL_RETRY_TEMPERATURE
10812
+
10813
+
10814
+ def _contamination_reset_cause(monitor: "SessionMonitor") -> str:
10815
+ """Dominant output-quality streak behind a contamination reset.
10816
+
10817
+ Drives the post-reset temperature policy: degenerate-content faults
10818
+ (invalid_tool_args) are EXEMPT from the 0.10 low-temp clamp (see
10819
+ PROXY_CONTAMINATION_LOW_TEMP_EXEMPT_KINDS) because determinism re-enters
10820
+ the same attractor; malformed-JSON repairs keep the clamp.
10821
+ """
10822
+ if monitor.invalid_tool_call_streak > monitor.malformed_tool_streak:
10823
+ return "invalid_tool_args"
10824
+ if monitor.malformed_tool_streak > 0:
10825
+ return "malformed_payload"
10826
+ if monitor.required_tool_miss_streak > 0:
10827
+ return "required_tool_miss"
10828
+ return "generic"
10829
+
10830
+
10720
10831
  def _extract_openai_tool_calls(openai_resp: dict) -> list[dict]:
10721
10832
  _, message = _extract_openai_choice(openai_resp)
10722
10833
  tool_calls = message.get("tool_calls") or []
@@ -12399,10 +12510,20 @@ def _validate_tool_call_arguments(
12399
12510
  allowed_tools: set[str],
12400
12511
  ) -> ToolResponseIssue:
12401
12512
  if allowed_tools and tool_name not in allowed_tools:
12513
+ # 2026-10-30: name the kept alternatives — a narrowed retry (the
12514
+ # garbling tool excluded) observed the model emitting the excluded
12515
+ # tool anyway because "use the provided tool list" gave it nothing
12516
+ # concrete to switch to.
12517
+ known = ", ".join(sorted(allowed_tools)[:6])
12402
12518
  return ToolResponseIssue(
12403
12519
  kind="invalid_tool_args",
12404
12520
  reason=f"unknown tool '{tool_name}'",
12405
- retry_hint="Use exactly one tool from the provided tool list.",
12521
+ retry_hint=(
12522
+ f"'{tool_name}' is not available on this turn. "
12523
+ f"Call exactly one of: {known}."
12524
+ if known
12525
+ else "Use exactly one tool from the provided tool list."
12526
+ ),
12406
12527
  )
12407
12528
 
12408
12529
  if isinstance(raw_arguments, (dict, list)):
@@ -12474,7 +12595,10 @@ def _validate_tool_call_arguments(
12474
12595
  reason=f"arguments for '{tool_name}' contain garbled/degenerate content",
12475
12596
  retry_hint=(
12476
12597
  f"Emit exactly one `{tool_name}` tool call with well-formed JSON arguments. "
12477
- "Do not repeat closing braces, brackets, or digits."
12598
+ "Your previous arguments degenerated (repeated digit or brace runs). "
12599
+ "SIMPLIFY and SHORTEN the operation: split the work into smaller steps, "
12600
+ "avoid long inline heredocs and large generated numeric blocks, and "
12601
+ "prefer short commands. Do not repeat closing braces, brackets, or digits."
12478
12602
  ),
12479
12603
  )
12480
12604
 
@@ -13053,12 +13177,16 @@ def _build_malformed_retry_body(
13053
13177
  retry_body["stream"] = False
13054
13178
  retry_body["tool_choice"] = tool_choice
13055
13179
  # Cycle 15 Option 3: vary temperature across retries to break degenerate patterns.
13056
- # Attempt 1: use configured retry temp (default 0.0) for deterministic first try.
13057
- # Attempt 2+: increase to 0.5 to escape the degenerate local minimum.
13058
- if total_attempts > 1 and attempt > 1:
13059
- retry_body["temperature"] = 0.5
13060
- else:
13061
- retry_body["temperature"] = PROXY_MALFORMED_TOOL_RETRY_TEMPERATURE
13180
+ # Ladder lives in _retry_temperature (garbled retries escalate to the
13181
+ # attractor-escape temperature from attempt 3 — see its docstring).
13182
+ retry_body["temperature"] = _retry_temperature(attempt, is_garbled)
13183
+ if is_garbled and attempt >= 3 and PROXY_GARBLE_LATE_CAP > 0:
13184
+ logger.info(
13185
+ "RETRY GARBLED ESCALATION: temperature=%.2f late_cap=%d for garbled retry attempt=%d",
13186
+ retry_body["temperature"],
13187
+ PROXY_GARBLE_LATE_CAP,
13188
+ attempt,
13189
+ )
13062
13190
 
13063
13191
  if tool_choice == "required":
13064
13192
  retry_instruction = (
@@ -13090,7 +13218,17 @@ def _build_malformed_retry_body(
13090
13218
 
13091
13219
  # Option 1: Progressive garbled-cap within retries — use smaller max_tokens
13092
13220
  # when the issue involves garbled/degenerate args to limit degeneration room.
13093
- if is_garbled and PROXY_TOOL_TURN_MAX_TOKENS_GARBLED > 0:
13221
+ # 2026-10-30: the deployed env pins PROXY_TOOL_TURN_MAX_TOKENS_GARBLED high
13222
+ # (32768) so legit multi-file heredoc edits don't truncate (2026-04-22) —
13223
+ # from attempt 3 onward, starve degeneration room with the late cap.
13224
+ if is_garbled and attempt >= 3 and PROXY_GARBLE_LATE_CAP > 0:
13225
+ retry_body["max_tokens"] = PROXY_GARBLE_LATE_CAP
13226
+ logger.info(
13227
+ "RETRY GARBLED LATE CAP: max_tokens=%d for garbled retry attempt=%d",
13228
+ PROXY_GARBLE_LATE_CAP,
13229
+ attempt,
13230
+ )
13231
+ elif is_garbled and PROXY_TOOL_TURN_MAX_TOKENS_GARBLED > 0:
13094
13232
  retry_body["max_tokens"] = PROXY_TOOL_TURN_MAX_TOKENS_GARBLED
13095
13233
  logger.info(
13096
13234
  "RETRY GARBLED CAP: max_tokens=%d for garbled retry attempt=%d",
@@ -13435,17 +13573,22 @@ async def _apply_malformed_tool_guardrail(
13435
13573
  excerpt = _openai_message_text(working_resp)[:220].replace("\n", " ")
13436
13574
  # Option 2: Log garbled argument content for diagnostics
13437
13575
  arg_excerpt = ""
13576
+ fault_tool_names: list[str] = []
13438
13577
  if issue.kind == "invalid_tool_args":
13439
13578
  for tc in (working_resp.get("choices", [{}])[0].get("message", {}).get("tool_calls") or []):
13579
+ fn_name = tc.get("function", {}).get("name", "")
13440
13580
  raw_args = tc.get("function", {}).get("arguments", "")
13441
- if raw_args and _is_garbled_tool_arguments(raw_args):
13581
+ if fn_name:
13582
+ fault_tool_names.append(fn_name)
13583
+ if raw_args and _is_garbled_tool_arguments(raw_args) and not arg_excerpt:
13442
13584
  arg_excerpt = raw_args[:200].replace("\n", " ")
13443
- break
13444
13585
  # Attractor detection — hash the normalized fault excerpt so the
13445
13586
  # contamination breaker can recognize the same fixed-point response
13446
13587
  # reappearing across consecutive resets. Whitespace-normalized so trivial
13447
13588
  # rendering differences don't break the match.
13448
- monitor.last_fault_excerpt_hash = _hash_fault_excerpt(excerpt)
13589
+ monitor.last_fault_excerpt_hash = _fault_fingerprint(
13590
+ excerpt, arg_excerpt, fault_tool_names
13591
+ )
13449
13592
  logger.warning(
13450
13593
  "TOOL RESPONSE ISSUE: session=%s kind=%s reason=%s malformed=%d invalid=%d required_miss=%d excerpt=%.220s args=%.200s",
13451
13594
  session_id,
@@ -13801,6 +13944,7 @@ def _maybe_apply_session_contamination_breaker(
13801
13944
  max_contamination_resets,
13802
13945
  )
13803
13946
  monitor.set_tool_turn_phase("finalize", reason="contamination_loop")
13947
+ monitor.contamination_cause = _contamination_reset_cause(monitor)
13804
13948
  monitor.contamination_resets += 1
13805
13949
  monitor.malformed_tool_streak = 0
13806
13950
  monitor.invalid_tool_call_streak = 0
@@ -13923,6 +14067,7 @@ def _maybe_apply_session_contamination_breaker(
13923
14067
 
13924
14068
  forced_before = monitor.consecutive_forced_count
13925
14069
  required_miss_before = monitor.required_tool_miss_streak
14070
+ monitor.contamination_cause = _contamination_reset_cause(monitor)
13926
14071
  monitor.contamination_resets += 1
13927
14072
  # Emergency tier: the conversation itself is rewritten, so BOTH the
13928
14073
  # forcing-loop family (via its one policy point) and the output-quality