@miller-tech/uap 2.36.1 → 2.39.0

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 (101) 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 +181 -15
  101. package/tools/agents/tests/test_signature_quality.py +51 -0
@@ -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.1",
3
+ "version": "2.39.0",
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",
@@ -697,6 +697,24 @@ _FAILURE_DECLARATION_RE = re.compile(
697
697
  re.IGNORECASE,
698
698
  )
699
699
 
700
+ # The harness's own zero-exit envelope: the tool CALL succeeded, whatever the
701
+ # output QUOTES. Droid/BYOK appends "[Process exited with code N]" to every
702
+ # Execute result; other harnesses print their own form. Decisive in the
703
+ # flag-ABSENT path only (a client that declares is_error True still wins):
704
+ # 2026-10-26, cognition-engine fdc94474 -- two ERROR-LOOP hard stops were
705
+ # served on SIX successful state-checks whose `git diff` output quoted the
706
+ # session's own in-flight `eprintln!("pg-server: metrics: failed to bind
707
+ # ...")` line; the keyword sniff turned each clean diff into the same
708
+ # "failure" signature and the streak escalated to a 400.
709
+ _ZERO_EXIT_ENVELOPE_RE = re.compile(
710
+ r"\[process exited with code 0\]" # Droid / BYOK Execute result footer
711
+ r"|\bexited with code 0\b" # generic harness prose
712
+ r"|\bexit (?:code|status)\s*[:=]?\s*0\b" # 'exit code 0' / 'exit status: 0'
713
+ r"|\bexit_code=0\b" # python subprocess env style
714
+ r"|\bEXIT=0\b", # shell-echoed status
715
+ re.IGNORECASE,
716
+ )
717
+
700
718
 
701
719
  def _is_successful_result(text: str) -> bool:
702
720
  """True when the payload structurally declares success.
@@ -867,10 +885,25 @@ def _error_signature_for_result(text: str, result_error: bool | None = None) ->
867
885
  (_first_meaningful_signature), so an unfamiliar error
868
886
  shape still forms a streak; a wrapper-only payload
869
887
  stays unnameable -- DOUBLING-DOWN owns that shape.
870
- absent -> fall back to the keyword heuristics as before.
888
+ absent -> keyword heuristics as before, EXCEPT a payload that
889
+ carries the harness's zero-exit envelope
890
+ (_ZERO_EXIT_ENVELOPE_RE): the call succeeded, so
891
+ failure-shaped words it merely QUOTES (a git diff of
892
+ in-flight edits, a grep over source) cannot anchor a
893
+ streak. 2026-10-26: two ERROR-LOOP hard stops on six
894
+ clean state-checks whose git-diff output quoted the
895
+ session's own `eprintln!("...failed to bind...")`
896
+ source line; a declared True flag still wins over
897
+ the envelope.
871
898
  """
872
899
  if result_error is False:
873
900
  return ""
901
+ if result_error is None and _ZERO_EXIT_ENVELOPE_RE.search(text or ""):
902
+ # Flag-ABSENT and the harness's own footer says the call exited 0:
903
+ # the result is clean whatever the bytes quote. Only the heuristic
904
+ # path is narrowed -- is_error True/False keep their declared
905
+ # precedence above.
906
+ return ""
874
907
  sig = _error_signature(text)
875
908
  if sig or result_error is not True:
876
909
  return sig
@@ -2423,6 +2456,31 @@ PROXY_ATTRACTOR_TEMP_OVERRIDE = float(
2423
2456
  PROXY_ATTRACTOR_FINALIZE_THRESHOLD = max(1, int(
2424
2457
  os.environ.get("PROXY_ATTRACTOR_FINALIZE_THRESHOLD", "2")
2425
2458
  ))
2459
+ # Post-contamination low-temp clamp exemption. The standard contamination
2460
+ # reset clamps the next tool turn to temperature 0.10 (Cycle 15 Option 2:
2461
+ # deterministic output repairs malformed JSON). For DEGENERATE-CONTENT faults
2462
+ # (garbled args) determinism is exactly wrong: temp ~0 re-enters the same
2463
+ # degenerate attractor that triggered the reset (live 2026-10-30: 9
2464
+ # contamination-forced finalize stops in one day, the 0.10 clamp active on
2465
+ # every post-reset turn). Reset causes listed here keep the normal
2466
+ # PROXY_TOOL_TURN_TEMPERATURE instead of the 0.10 clamp. Empty string
2467
+ # disables the exemption entirely (always clamp).
2468
+ PROXY_CONTAMINATION_LOW_TEMP_EXEMPT_KINDS = {
2469
+ k.strip()
2470
+ for k in os.environ.get(
2471
+ "PROXY_CONTAMINATION_LOW_TEMP_EXEMPT_KINDS", "invalid_tool_args"
2472
+ ).split(",")
2473
+ if k.strip()
2474
+ }
2475
+ # Late garbled-retry output cap. The deployed env pins
2476
+ # PROXY_TOOL_TURN_MAX_TOKENS_GARBLED high (32768, raised 2026-04-22 so legit
2477
+ # multi-file heredoc edits don't truncate), which also gives a degenerate
2478
+ # generation maximum room to run. From this retry attempt onward the budget
2479
+ # drops to this cap: the observed legit commands (~4.5K tokens) still fit,
2480
+ # while runaway degeneration is starved of room (live 2026-10-30: every
2481
+ # garbled call was a long inline heredoc; short commands never garbled).
2482
+ # 0 disables the late cap.
2483
+ PROXY_GARBLE_LATE_CAP = int(os.environ.get("PROXY_GARBLE_LATE_CAP", "8192"))
2426
2484
  PROXY_AGENTIC_SUPPLEMENT_MODE = (
2427
2485
  os.environ.get("PROXY_AGENTIC_SUPPLEMENT_MODE", "clean").strip().lower()
2428
2486
  )
@@ -3086,6 +3144,7 @@ class SessionMonitor:
3086
3144
  invalid_tool_call_streak: int = 0 # consecutive invalid tool arg payloads
3087
3145
  required_tool_miss_streak: int = 0 # required tool turns with no tool call
3088
3146
  contamination_resets: int = 0 # how many contamination resets were applied
3147
+ contamination_cause: str = "" # dominant streak behind the last reset ("invalid_tool_args"/"malformed_payload"/"required_tool_miss"/"generic")
3089
3148
  last_fault_excerpt_hash: str = "" # hash of last TOOL RESPONSE ISSUE excerpt (attractor detection)
3090
3149
  attractor_correction_active: bool = False # next turn uses high-temp escape sampling
3091
3150
  forced_auto_cooldown_turns: int = 0 # temporary auto override turns remaining
@@ -9789,13 +9848,24 @@ def build_openai_request(
9789
9848
  client_temp,
9790
9849
  )
9791
9850
  else:
9792
- if monitor.contamination_resets > 0:
9851
+ # Cycle 15 Option 2: lower temperature after contamination resets —
9852
+ # EXCEPT when the reset was driven by an exempt cause: for
9853
+ # degenerate-content faults (garbled args) determinism re-enters
9854
+ # the same attractor that triggered the reset (live 2026-10-30).
9855
+ if (
9856
+ monitor.contamination_resets > 0
9857
+ and monitor.contamination_cause
9858
+ not in PROXY_CONTAMINATION_LOW_TEMP_EXEMPT_KINDS
9859
+ ):
9793
9860
  target_temp = min(target_temp, 0.1)
9794
9861
  if client_temp is None or client_temp > target_temp:
9795
9862
  openai_body["temperature"] = target_temp
9796
9863
  extra = ""
9797
9864
  if monitor.contamination_resets > 0:
9798
- extra = f" (post-contamination reset, resets={monitor.contamination_resets})"
9865
+ extra = (
9866
+ f" (post-contamination reset, resets={monitor.contamination_resets},"
9867
+ f" cause={monitor.contamination_cause or 'generic'})"
9868
+ )
9799
9869
  logger.info(
9800
9870
  "TOOL TURN TEMP: forcing temperature=%.2f (was %s) for tool-enabled request%s",
9801
9871
  target_temp,
@@ -10684,6 +10754,68 @@ def _hash_fault_excerpt(excerpt: str) -> str:
10684
10754
  return hashlib.sha256(normalized.encode("utf-8")).hexdigest()[:16]
10685
10755
 
10686
10756
 
10757
+ def _fault_fingerprint(
10758
+ excerpt: str, arg_excerpt: str, fault_tool_names: list[str]
10759
+ ) -> str:
10760
+ """Stable fingerprint of a tool-response fault for attractor-repeat detection.
10761
+
10762
+ Tool-call faults carry NO assistant text (every observed garble incident
10763
+ logged text_len=0), so hashing only the text excerpt yielded "" — falsy —
10764
+ and the attractor detector (hard reset + temp escape + lowered finalize
10765
+ threshold) never fired for the dominant fault class (live 2026-10-30:
10766
+ 9 contamination-forced finalize stops, none attractor-detected). Fall
10767
+ back to the garbled-args excerpt — its stable command head survives
10768
+ normalization, so the same degenerate approach hashes equal across
10769
+ resets — then the failing tool names.
10770
+ """
10771
+ if excerpt:
10772
+ return _hash_fault_excerpt(excerpt)
10773
+ if arg_excerpt:
10774
+ return _hash_fault_excerpt(arg_excerpt)
10775
+ if fault_tool_names:
10776
+ return _hash_fault_excerpt(
10777
+ "tool:" + ",".join(sorted(set(fault_tool_names)))
10778
+ )
10779
+ return ""
10780
+
10781
+
10782
+ def _retry_temperature(attempt: int, is_garbled: bool) -> float:
10783
+ """Retry temperature ladder for malformed/garbled tool-call retries.
10784
+
10785
+ Attempt 1: configured retry temp (deterministic first try).
10786
+ Attempt 2: 0.5 — escape the degenerate local minimum (Cycle 15 Option 3).
10787
+ Attempt 3+ garbled (degenerate CONTENT): attractor-escape temperature —
10788
+ determinism re-enters the same degenerate trajectory (live 2026-10-30:
10789
+ garbled attempts 1-4 all reproduced at 0.05/0.5 while short commands
10790
+ never garbled); malformed-JSON retries keep the deterministic ladder
10791
+ because structure repair wants determinism.
10792
+ """
10793
+ if is_garbled and attempt >= 3:
10794
+ return max(
10795
+ PROXY_ATTRACTOR_TEMP_OVERRIDE, PROXY_MALFORMED_TOOL_RETRY_TEMPERATURE
10796
+ )
10797
+ if attempt > 1:
10798
+ return 0.5
10799
+ return PROXY_MALFORMED_TOOL_RETRY_TEMPERATURE
10800
+
10801
+
10802
+ def _contamination_reset_cause(monitor: "SessionMonitor") -> str:
10803
+ """Dominant output-quality streak behind a contamination reset.
10804
+
10805
+ Drives the post-reset temperature policy: degenerate-content faults
10806
+ (invalid_tool_args) are EXEMPT from the 0.10 low-temp clamp (see
10807
+ PROXY_CONTAMINATION_LOW_TEMP_EXEMPT_KINDS) because determinism re-enters
10808
+ the same attractor; malformed-JSON repairs keep the clamp.
10809
+ """
10810
+ if monitor.invalid_tool_call_streak > monitor.malformed_tool_streak:
10811
+ return "invalid_tool_args"
10812
+ if monitor.malformed_tool_streak > 0:
10813
+ return "malformed_payload"
10814
+ if monitor.required_tool_miss_streak > 0:
10815
+ return "required_tool_miss"
10816
+ return "generic"
10817
+
10818
+
10687
10819
  def _extract_openai_tool_calls(openai_resp: dict) -> list[dict]:
10688
10820
  _, message = _extract_openai_choice(openai_resp)
10689
10821
  tool_calls = message.get("tool_calls") or []
@@ -12366,10 +12498,20 @@ def _validate_tool_call_arguments(
12366
12498
  allowed_tools: set[str],
12367
12499
  ) -> ToolResponseIssue:
12368
12500
  if allowed_tools and tool_name not in allowed_tools:
12501
+ # 2026-10-30: name the kept alternatives — a narrowed retry (the
12502
+ # garbling tool excluded) observed the model emitting the excluded
12503
+ # tool anyway because "use the provided tool list" gave it nothing
12504
+ # concrete to switch to.
12505
+ known = ", ".join(sorted(allowed_tools)[:6])
12369
12506
  return ToolResponseIssue(
12370
12507
  kind="invalid_tool_args",
12371
12508
  reason=f"unknown tool '{tool_name}'",
12372
- retry_hint="Use exactly one tool from the provided tool list.",
12509
+ retry_hint=(
12510
+ f"'{tool_name}' is not available on this turn. "
12511
+ f"Call exactly one of: {known}."
12512
+ if known
12513
+ else "Use exactly one tool from the provided tool list."
12514
+ ),
12373
12515
  )
12374
12516
 
12375
12517
  if isinstance(raw_arguments, (dict, list)):
@@ -12441,7 +12583,10 @@ def _validate_tool_call_arguments(
12441
12583
  reason=f"arguments for '{tool_name}' contain garbled/degenerate content",
12442
12584
  retry_hint=(
12443
12585
  f"Emit exactly one `{tool_name}` tool call with well-formed JSON arguments. "
12444
- "Do not repeat closing braces, brackets, or digits."
12586
+ "Your previous arguments degenerated (repeated digit or brace runs). "
12587
+ "SIMPLIFY and SHORTEN the operation: split the work into smaller steps, "
12588
+ "avoid long inline heredocs and large generated numeric blocks, and "
12589
+ "prefer short commands. Do not repeat closing braces, brackets, or digits."
12445
12590
  ),
12446
12591
  )
12447
12592
 
@@ -13020,12 +13165,16 @@ def _build_malformed_retry_body(
13020
13165
  retry_body["stream"] = False
13021
13166
  retry_body["tool_choice"] = tool_choice
13022
13167
  # Cycle 15 Option 3: vary temperature across retries to break degenerate patterns.
13023
- # Attempt 1: use configured retry temp (default 0.0) for deterministic first try.
13024
- # Attempt 2+: increase to 0.5 to escape the degenerate local minimum.
13025
- if total_attempts > 1 and attempt > 1:
13026
- retry_body["temperature"] = 0.5
13027
- else:
13028
- retry_body["temperature"] = PROXY_MALFORMED_TOOL_RETRY_TEMPERATURE
13168
+ # Ladder lives in _retry_temperature (garbled retries escalate to the
13169
+ # attractor-escape temperature from attempt 3 — see its docstring).
13170
+ retry_body["temperature"] = _retry_temperature(attempt, is_garbled)
13171
+ if is_garbled and attempt >= 3 and PROXY_GARBLE_LATE_CAP > 0:
13172
+ logger.info(
13173
+ "RETRY GARBLED ESCALATION: temperature=%.2f late_cap=%d for garbled retry attempt=%d",
13174
+ retry_body["temperature"],
13175
+ PROXY_GARBLE_LATE_CAP,
13176
+ attempt,
13177
+ )
13029
13178
 
13030
13179
  if tool_choice == "required":
13031
13180
  retry_instruction = (
@@ -13057,7 +13206,17 @@ def _build_malformed_retry_body(
13057
13206
 
13058
13207
  # Option 1: Progressive garbled-cap within retries — use smaller max_tokens
13059
13208
  # when the issue involves garbled/degenerate args to limit degeneration room.
13060
- if is_garbled and PROXY_TOOL_TURN_MAX_TOKENS_GARBLED > 0:
13209
+ # 2026-10-30: the deployed env pins PROXY_TOOL_TURN_MAX_TOKENS_GARBLED high
13210
+ # (32768) so legit multi-file heredoc edits don't truncate (2026-04-22) —
13211
+ # from attempt 3 onward, starve degeneration room with the late cap.
13212
+ if is_garbled and attempt >= 3 and PROXY_GARBLE_LATE_CAP > 0:
13213
+ retry_body["max_tokens"] = PROXY_GARBLE_LATE_CAP
13214
+ logger.info(
13215
+ "RETRY GARBLED LATE CAP: max_tokens=%d for garbled retry attempt=%d",
13216
+ PROXY_GARBLE_LATE_CAP,
13217
+ attempt,
13218
+ )
13219
+ elif is_garbled and PROXY_TOOL_TURN_MAX_TOKENS_GARBLED > 0:
13061
13220
  retry_body["max_tokens"] = PROXY_TOOL_TURN_MAX_TOKENS_GARBLED
13062
13221
  logger.info(
13063
13222
  "RETRY GARBLED CAP: max_tokens=%d for garbled retry attempt=%d",
@@ -13402,17 +13561,22 @@ async def _apply_malformed_tool_guardrail(
13402
13561
  excerpt = _openai_message_text(working_resp)[:220].replace("\n", " ")
13403
13562
  # Option 2: Log garbled argument content for diagnostics
13404
13563
  arg_excerpt = ""
13564
+ fault_tool_names: list[str] = []
13405
13565
  if issue.kind == "invalid_tool_args":
13406
13566
  for tc in (working_resp.get("choices", [{}])[0].get("message", {}).get("tool_calls") or []):
13567
+ fn_name = tc.get("function", {}).get("name", "")
13407
13568
  raw_args = tc.get("function", {}).get("arguments", "")
13408
- if raw_args and _is_garbled_tool_arguments(raw_args):
13569
+ if fn_name:
13570
+ fault_tool_names.append(fn_name)
13571
+ if raw_args and _is_garbled_tool_arguments(raw_args) and not arg_excerpt:
13409
13572
  arg_excerpt = raw_args[:200].replace("\n", " ")
13410
- break
13411
13573
  # Attractor detection — hash the normalized fault excerpt so the
13412
13574
  # contamination breaker can recognize the same fixed-point response
13413
13575
  # reappearing across consecutive resets. Whitespace-normalized so trivial
13414
13576
  # rendering differences don't break the match.
13415
- monitor.last_fault_excerpt_hash = _hash_fault_excerpt(excerpt)
13577
+ monitor.last_fault_excerpt_hash = _fault_fingerprint(
13578
+ excerpt, arg_excerpt, fault_tool_names
13579
+ )
13416
13580
  logger.warning(
13417
13581
  "TOOL RESPONSE ISSUE: session=%s kind=%s reason=%s malformed=%d invalid=%d required_miss=%d excerpt=%.220s args=%.200s",
13418
13582
  session_id,
@@ -13768,6 +13932,7 @@ def _maybe_apply_session_contamination_breaker(
13768
13932
  max_contamination_resets,
13769
13933
  )
13770
13934
  monitor.set_tool_turn_phase("finalize", reason="contamination_loop")
13935
+ monitor.contamination_cause = _contamination_reset_cause(monitor)
13771
13936
  monitor.contamination_resets += 1
13772
13937
  monitor.malformed_tool_streak = 0
13773
13938
  monitor.invalid_tool_call_streak = 0
@@ -13890,6 +14055,7 @@ def _maybe_apply_session_contamination_breaker(
13890
14055
 
13891
14056
  forced_before = monitor.consecutive_forced_count
13892
14057
  required_miss_before = monitor.required_tool_miss_streak
14058
+ monitor.contamination_cause = _contamination_reset_cause(monitor)
13893
14059
  monitor.contamination_resets += 1
13894
14060
  # Emergency tier: the conversation itself is rewritten, so BOTH the
13895
14061
  # forcing-loop family (via its one policy point) and the output-quality
@@ -55,6 +55,20 @@ WORKTREE_STATUS = (
55
55
  SHELL_SOURCE_READ = "die() { printf 'fatal: %s\\n' \"$*\" >&2; exit 1; }\n"
56
56
 
57
57
 
58
+ # 2026-10-26, cognition-engine fdc94474: state-check output that QUOTES the
59
+ # session's own in-flight source (a numbered read of the metrics region,
60
+ # a git diff of the wedge fix) plus the harness's zero-exit footer. Six
61
+ # flag-absent results of this shape manufactured both ERROR-LOOP hard stops.
62
+ DIFF_QUOTING_SOURCE = (
63
+ " M src/rust-pg-ext/Cargo.toml\n"
64
+ " M src/rust-pg-ext/src/bin/pg-server.rs\n"
65
+ "2784→ eprintln!(\"pg-server: metrics: failed to bind {addr}: {e}"
66
+ " — metrics endpoint disabled\");\n"
67
+ "+ return None;\n"
68
+ "\n[Process exited with code 0]"
69
+ )
70
+
71
+
58
72
  class TracebackAnchoringTests(unittest.TestCase):
59
73
  def test_decisive_line_anchors_not_the_header(self):
60
74
  sig = proxy._error_signature(TRACEBACK_FULL)
@@ -225,6 +239,43 @@ class ReadOnlyContentTests(unittest.TestCase):
225
239
  self.assertEqual(len(body["messages"]), 1)
226
240
 
227
241
 
242
+ class ZeroExitEnvelopeTests(unittest.TestCase):
243
+ """A flag-ABSENT Execute result carrying the harness's zero-exit
244
+ envelope is a SUCCESS, whatever failure-shaped text its output QUOTES
245
+ (ledger 2026-10-26: two ERROR-LOOP hard stops served on six clean
246
+ state-checks whose output quoted an in-flight eprintln! source line)."""
247
+
248
+ def test_quoted_source_with_exit_zero_forms_no_signature(self):
249
+ m = proxy.SessionMonitor()
250
+ for _ in range(6):
251
+ m.note_tool_result_error(DIFF_QUOTING_SOURCE, None, tool_names=["execute"])
252
+ self.assertEqual(m.error_signature_streak, 0)
253
+ self.assertEqual(m.last_error_signature, "")
254
+
255
+ def test_exit_zero_streak_never_arms_the_guard(self):
256
+ m = proxy.SessionMonitor()
257
+ for _ in range(6):
258
+ m.note_tool_result_error(DIFF_QUOTING_SOURCE, None, tool_names=["execute"])
259
+ body = {"messages": [{"role": "user", "content": "go"}], "tool_choice": "auto"}
260
+ proxy._maybe_inject_error_loop_break(body, m)
261
+ self.assertEqual(m.error_loop_fires, 0)
262
+ self.assertEqual(len(body["messages"]), 1)
263
+
264
+ def test_nonzero_exit_still_keeps_heuristics(self):
265
+ payload = DIFF_QUOTING_SOURCE.replace(
266
+ "exited with code 0", "exited with code 1"
267
+ )
268
+ m = proxy.SessionMonitor()
269
+ m.note_tool_result_error(payload, None, tool_names=["execute"])
270
+ self.assertEqual(m.error_signature_streak, 1)
271
+ self.assertNotEqual(m.last_error_signature, "")
272
+
273
+ def test_declared_error_flag_beats_the_zero_exit_envelope(self):
274
+ m = proxy.SessionMonitor()
275
+ m.note_tool_result_error(DIFF_QUOTING_SOURCE, True, tool_names=["execute"])
276
+ self.assertEqual(m.error_signature_streak, 1)
277
+
278
+
228
279
  class KnownFixCoverageTests(unittest.TestCase):
229
280
  """The uncovered ledger classes now carry a KNOWN FIX."""
230
281