@miller-tech/uap 1.186.2 → 1.187.3

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 (55) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/bin/cli.js +4 -0
  3. package/dist/bin/cli.js.map +1 -1
  4. package/dist/cli/deliver.d.ts +100 -0
  5. package/dist/cli/deliver.d.ts.map +1 -1
  6. package/dist/cli/deliver.js +507 -20
  7. package/dist/cli/deliver.js.map +1 -1
  8. package/dist/coordination/reactor.d.ts.map +1 -1
  9. package/dist/coordination/reactor.js +28 -5
  10. package/dist/coordination/reactor.js.map +1 -1
  11. package/dist/delivery/agentic-executor.d.ts +3 -3
  12. package/dist/delivery/agentic-executor.d.ts.map +1 -1
  13. package/dist/delivery/agentic-executor.js +74 -4
  14. package/dist/delivery/agentic-executor.js.map +1 -1
  15. package/dist/delivery/convergence-loop.d.ts +6 -0
  16. package/dist/delivery/convergence-loop.d.ts.map +1 -1
  17. package/dist/delivery/convergence-loop.js +31 -0
  18. package/dist/delivery/convergence-loop.js.map +1 -1
  19. package/dist/delivery/edit-match.d.ts.map +1 -1
  20. package/dist/delivery/edit-match.js +19 -1
  21. package/dist/delivery/edit-match.js.map +1 -1
  22. package/dist/delivery/epic-controller.d.ts +6 -0
  23. package/dist/delivery/epic-controller.d.ts.map +1 -1
  24. package/dist/delivery/epic-controller.js +10 -0
  25. package/dist/delivery/epic-controller.js.map +1 -1
  26. package/dist/delivery/epic-mission.d.ts.map +1 -1
  27. package/dist/delivery/epic-mission.js +3 -0
  28. package/dist/delivery/epic-mission.js.map +1 -1
  29. package/dist/delivery/verifier-ladder.d.ts +99 -6
  30. package/dist/delivery/verifier-ladder.d.ts.map +1 -1
  31. package/dist/delivery/verifier-ladder.js +208 -25
  32. package/dist/delivery/verifier-ladder.js.map +1 -1
  33. package/dist/mcp-router/tools/deliver.d.ts.map +1 -1
  34. package/dist/mcp-router/tools/deliver.js +17 -0
  35. package/dist/mcp-router/tools/deliver.js.map +1 -1
  36. package/dist/models/long-fetch.d.ts +19 -0
  37. package/dist/models/long-fetch.d.ts.map +1 -1
  38. package/dist/models/long-fetch.js +47 -0
  39. package/dist/models/long-fetch.js.map +1 -1
  40. package/dist/telemetry/tool-failure.d.ts +9 -0
  41. package/dist/telemetry/tool-failure.d.ts.map +1 -1
  42. package/dist/telemetry/tool-failure.js +4 -0
  43. package/dist/telemetry/tool-failure.js.map +1 -1
  44. package/docs/reference/CLI.md +13 -2
  45. package/docs/reference/CONFIGURATION.md +2 -0
  46. package/package.json +2 -2
  47. package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
  48. package/src/policies/enforcers/enforcement_infra_protect.py +68 -5
  49. package/src/policies/enforcers/enforcement_self_protect.py +36 -0
  50. package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
  51. package/templates/hooks/pre-tool-use-bash.sh +10 -2
  52. package/templates/hooks/uap-policy-gate.sh +4 -1
  53. package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
  54. package/tools/agents/scripts/anthropic_proxy.py +101 -9
  55. package/tools/agents/tests/test_repeat_call_guard.py +228 -0
@@ -18,6 +18,16 @@
18
18
  */
19
19
  import { Agent } from 'undici';
20
20
  const globalFetch = (url, init) => globalThis.fetch(url, init);
21
+ /**
22
+ * Marks a failure as "the model endpoint is not reachable".
23
+ *
24
+ * A greppable prefix rather than an Error subclass because it has to survive
25
+ * being stringified into an executor error string and read back by the
26
+ * convergence loop, which never sees the original Error object. Lives here, in
27
+ * the transport module, so both the executor that raises it and the loop that
28
+ * consumes it can import it without a cycle.
29
+ */
30
+ export const ENDPOINT_UNREACHABLE = 'ENDPOINT_UNREACHABLE';
21
31
  const DEFAULT_MODEL_HTTP_TIMEOUT_MS = 30 * 60 * 1000;
22
32
  const CONNECT_TIMEOUT_MS = 30_000;
23
33
  const DEFAULT_RETRIES = 2;
@@ -51,6 +61,43 @@ const TRANSIENT_CODES = new Set([
51
61
  'UND_ERR_CONNECT_TIMEOUT',
52
62
  'UND_ERR_SOCKET',
53
63
  ]);
64
+ /**
65
+ * Codes that mean the connection could never be ESTABLISHED — nothing is
66
+ * listening, or the name does not resolve.
67
+ *
68
+ * Deliberately a strict subset of TRANSIENT_CODES, and deliberately NOT reused
69
+ * from it. The transient set answers "is a retry safe?", which is a different
70
+ * question from "is the endpoint dead?", and most of it means *reachable but
71
+ * flaky or slow*: ECONNRESET/EPIPE/UND_ERR_SOCKET are a dropped connection
72
+ * under load, and UND_ERR_*_TIMEOUT is a model still thinking past the
73
+ * 30-minute ceiling. Treating those as "unreachable" would kill a long,
74
+ * healthy run and tell the operator to go restart a proxy that is fine.
75
+ */
76
+ const UNREACHABLE_CODES = new Set([
77
+ 'ECONNREFUSED',
78
+ 'ENOTFOUND',
79
+ 'EAI_AGAIN',
80
+ 'UND_ERR_CONNECT_TIMEOUT',
81
+ ]);
82
+ /**
83
+ * True only when the endpoint could not be reached at all.
84
+ *
85
+ * Note the asymmetry with `isTransientNetworkError`: a bare
86
+ * `TypeError: fetch failed` with no cause code is NOT enough here. It is
87
+ * retryable (safe), but it does not say the listener is gone, and this
88
+ * predicate gates an abort.
89
+ */
90
+ export function isEndpointUnreachable(err) {
91
+ if (!(err instanceof Error))
92
+ return false;
93
+ if (err.name === 'AbortError')
94
+ return false;
95
+ const cause = err.cause;
96
+ if (cause?.code && UNREACHABLE_CODES.has(cause.code))
97
+ return true;
98
+ const direct = err.code;
99
+ return Boolean(direct && UNREACHABLE_CODES.has(direct));
100
+ }
54
101
  /** True for network-level failures worth retrying (never HTTP responses). */
55
102
  export function isTransientNetworkError(err) {
56
103
  if (!(err instanceof Error))
@@ -1 +1 @@
1
- {"version":3,"file":"long-fetch.js","sourceRoot":"","sources":["../../src/models/long-fetch.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"long-fetch.js","sourceRoot":"","sources":["../../src/models/long-fetch.ts"],"names":[],"mappings":"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"}
@@ -21,6 +21,15 @@
21
21
  export type ToolOutcomeClass =
22
22
  /** Call succeeded with no caveat. */
23
23
  'ok'
24
+ /**
25
+ * The call was accepted but changed NOTHING — a write or edit whose result
26
+ * already matched the file. Neither a success nor a failure: the tool did
27
+ * exactly what it was told, and the turn made no progress. It gets its own
28
+ * class because folding it into `ok` is what the no-op guards exist to stop —
29
+ * the corpus would report edit tooling as healthy precisely while a run is
30
+ * spinning on repeated no-op edits.
31
+ */
32
+ | 'no-op'
24
33
  /** Succeeded, but only via the whitespace-tolerant edit rung (anchor drift). */
25
34
  | 'ok-tolerant'
26
35
  /** `old_string` matched nothing. The dominant edit-tool failure. */
@@ -1 +1 @@
1
- {"version":3,"file":"tool-failure.d.ts","sourceRoot":"","sources":["../../src/telemetry/tool-failure.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH;;;GAGG;AACH,MAAM,MAAM,gBAAgB;AAC1B,qCAAqC;AACnC,IAAI;AACN,gFAAgF;GAC9E,aAAa;AACf,oEAAoE;GAClE,WAAW;AACb,sEAAsE;GACpE,gBAAgB;AAClB,oDAAoD;GAClD,eAAe;AACjB,sCAAsC;GACpC,gBAAgB;AAClB,qCAAqC;GACnC,aAAa;AACf,wEAAwE;GACtE,gBAAgB;AAClB,uEAAuE;GACrE,cAAc;AAChB,4CAA4C;GAC1C,iBAAiB;AACnB,gDAAgD;GAC9C,gBAAgB;AAClB,qCAAqC;GACnC,SAAS;AACX,kDAAkD;GAChD,cAAc;AAChB,iFAAiF;GAC/E,SAAS;AACX,uEAAuE;GACrE,iBAAiB;AACnB,gDAAgD;GAC9C,aAAa,CAAC;AAElB,kFAAkF;AAClF,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,YAAY,GAAG,QAAQ,GAAG,QAAQ,GAAG,WAAW,CAAC;AAoB1F,wBAAgB,cAAc,CAAC,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAE3D;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,gBAAgB,CAiDjF;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,CAc/D"}
1
+ {"version":3,"file":"tool-failure.d.ts","sourceRoot":"","sources":["../../src/telemetry/tool-failure.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH;;;GAGG;AACH,MAAM,MAAM,gBAAgB;AAC1B,qCAAqC;AACnC,IAAI;AACN;;;;;;;GAOG;GACD,OAAO;AACT,gFAAgF;GAC9E,aAAa;AACf,oEAAoE;GAClE,WAAW;AACb,sEAAsE;GACpE,gBAAgB;AAClB,oDAAoD;GAClD,eAAe;AACjB,sCAAsC;GACpC,gBAAgB;AAClB,qCAAqC;GACnC,aAAa;AACf,wEAAwE;GACtE,gBAAgB;AAClB,uEAAuE;GACrE,cAAc;AAChB,4CAA4C;GAC1C,iBAAiB;AACnB,gDAAgD;GAC9C,gBAAgB;AAClB,qCAAqC;GACnC,SAAS;AACX,kDAAkD;GAChD,cAAc;AAChB,iFAAiF;GAC/E,SAAS;AACX,uEAAuE;GACrE,iBAAiB;AACnB,gDAAgD;GAC9C,aAAa,CAAC;AAElB,kFAAkF;AAClF,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,YAAY,GAAG,QAAQ,GAAG,QAAQ,GAAG,WAAW,CAAC;AAoB1F,wBAAgB,cAAc,CAAC,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAE3D;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,gBAAgB,CAoDjF;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,CAc/D"}
@@ -66,6 +66,10 @@ export function classifyToolResult(tool, result) {
66
66
  return lower.startsWith('error:') ? 'other-error' : 'ok';
67
67
  }
68
68
  if (!lower.startsWith('error:')) {
69
+ // Checked BEFORE the tolerant note: a no-op reached through the tolerant
70
+ // rung still changed nothing, and "no progress" is the more important fact.
71
+ if (lower.startsWith('no-op:'))
72
+ return 'no-op';
69
73
  // Success paths. The tolerant-match note is a success WITH a signal: the
70
74
  // model's anchors are drifting, which predicts future misses.
71
75
  if (lower.includes('did not match byte-for-byte'))
@@ -1 +1 @@
1
- {"version":3,"file":"tool-failure.js","sourceRoot":"","sources":["../../src/telemetry/tool-failure.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tool-failure.js","sourceRoot":"","sources":["../../src/telemetry/tool-failure.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAoDH,gEAAgE;AAChE,MAAM,eAAe,GAAkC,IAAI,GAAG,CAAmB;IAC/E,SAAS;IACT,iBAAiB;IACjB,WAAW;IACX,gBAAgB;IAChB,eAAe;IACf,gBAAgB;IAChB,aAAa;IACb,gBAAgB;IAChB,cAAc;IACd,iBAAiB;IACjB,gBAAgB;IAChB,SAAS;IACT,cAAc;IACd,aAAa;CACd,CAAC,CAAC;AAEH,MAAM,UAAU,cAAc,CAAC,CAAmB;IAChD,OAAO,eAAe,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AAChC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAY,EAAE,MAAc;IAC7D,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC;IAClC,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;IAEjC,IAAI,KAAK,CAAC,QAAQ,CAAC,cAAc,CAAC;QAAE,OAAO,cAAc,CAAC;IAE1D,2EAA2E;IAC3E,2EAA2E;IAC3E,+EAA+E;IAC/E,oEAAoE;IACpE,IAAI,IAAI,KAAK,UAAU,EAAE,CAAC;QACxB,IAAI,KAAK,CAAC,UAAU,CAAC,sBAAsB,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,aAAa,CAAC;YAAE,OAAO,SAAS,CAAC;QAChG,yEAAyE;QACzE,8CAA8C;QAC9C,IAAI,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,WAAW,CAAC;YAAE,OAAO,iBAAiB,CAAC;QACxF,IAAI,KAAK,CAAC,QAAQ,CAAC,WAAW,CAAC;YAAE,OAAO,SAAS,CAAC;QAClD,MAAM,IAAI,GAAG,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC/C,IAAI,IAAI;YAAE,OAAO,IAAI,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,gBAAgB,CAAC;QAC3D,OAAO,KAAK,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC;IAC3D,CAAC;IAED,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAChC,yEAAyE;QACzE,4EAA4E;QAC5E,IAAI,KAAK,CAAC,UAAU,CAAC,QAAQ,CAAC;YAAE,OAAO,OAAO,CAAC;QAC/C,yEAAyE;QACzE,8DAA8D;QAC9D,IAAI,KAAK,CAAC,QAAQ,CAAC,6BAA6B,CAAC;YAAE,OAAO,aAAa,CAAC;QACxE,OAAO,IAAI,CAAC;IACd,CAAC;IAED,mEAAmE;IACnE,IAAI,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC;QAAE,OAAO,gBAAgB,CAAC;IACnF,IAAI,KAAK,CAAC,QAAQ,CAAC,mCAAmC,CAAC;QAAE,OAAO,gBAAgB,CAAC;IAEjF,IAAI,KAAK,CAAC,QAAQ,CAAC,sBAAsB,CAAC;QAAE,OAAO,WAAW,CAAC;IAC/D,IAAI,KAAK,CAAC,QAAQ,CAAC,oBAAoB,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,YAAY,CAAC;QAAE,OAAO,gBAAgB,CAAC;IAClG,IAAI,KAAK,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC;QAAE,OAAO,eAAe,CAAC;IACvF,IAAI,KAAK,CAAC,QAAQ,CAAC,sBAAsB,CAAC;QAAE,OAAO,aAAa,CAAC;IACjE,IACE,KAAK,CAAC,QAAQ,CAAC,gBAAgB,CAAC;QAChC,KAAK,CAAC,QAAQ,CAAC,iBAAiB,CAAC;QACjC,KAAK,CAAC,QAAQ,CAAC,oBAAoB,CAAC;QACpC,KAAK,CAAC,QAAQ,CAAC,yCAAyC,CAAC,EACzD,CAAC;QACD,OAAO,gBAAgB,CAAC;IAC1B,CAAC;IACD,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC;QAAE,OAAO,cAAc,CAAC;IAChF,IAAI,KAAK,CAAC,QAAQ,CAAC,WAAW,CAAC;QAAE,OAAO,iBAAiB,CAAC;IAC1D,IAAI,KAAK,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC;QAAE,OAAO,SAAS,CAAC;IAC/E,IAAI,KAAK,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,gBAAgB,CAAC;QAAE,OAAO,gBAAgB,CAAC;IAC7F,OAAO,aAAa,CAAC;AACvB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,WAAW,CAAC;QACjB,KAAK,YAAY,CAAC;QAClB,KAAK,YAAY,CAAC;QAClB,KAAK,WAAW,CAAC;QACjB,KAAK,UAAU,CAAC;QAChB,KAAK,QAAQ;YACX,OAAO,OAAO,CAAC;QACjB,KAAK,UAAU;YACb,OAAO,WAAW,CAAC;QACrB;YACE,OAAO,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC;IAC1D,CAAC;AACH,CAAC"}
@@ -398,8 +398,12 @@ uap deliver --await-run --await-timeout 45 # one poll, then report and return
398
398
 
399
399
  Exit codes for `--await-run` are distinct because three of the four outcomes are
400
400
  not failures: **0** delivered · **1** the mission ended badly · **3** nothing was
401
- running (go ahead and launch) · **4** still running — the wait gave up, the
402
- mission did not.
401
+ running *on this root* · **4** still running — the wait gave up, the mission did
402
+ not.
403
+
404
+ A **3** normally means "safe to launch". The exception is a launch that was just
405
+ refused because an *overlapping* root holds the run: follow that root instead —
406
+ this one has nothing in flight and will keep answering 3.
403
407
 
404
408
  A `4` carries a `progress` object so consecutive polls can be compared. Always
405
409
  present: `heartbeatAgeSec`, `wedgeAfterSec`, `health`. Present only when the run
@@ -417,6 +421,13 @@ asserted:
417
421
  - `"wedged"` — silent past `UAP_DELIVER_WEDGE_TIMEOUT` (default 1800s, shared
418
422
  with the autoroute hook) and may be stuck.
419
423
 
424
+ Single-flight is a property of the **subtree**, not of the lock path. A run also
425
+ exits early when an older live run holds an *overlapping* (nested or identical)
426
+ project root: `repo` and `repo/src/ext` edit the same files even though their
427
+ `.uap/deliver.lock` paths differ. The refusal names the holder's root — follow
428
+ *that* root, because following your own reports nothing in flight. Live runs
429
+ register in `~/.uap/active-runs` (redirect with `UAP_ACTIVE_RUNS_DIR`).
430
+
420
431
  A wedged **lock** holder is reclaimed by the next launch. A resumed run holds no
421
432
  lock, so relaunching there does not reclaim it — it starts a second mission on
422
433
  the same tree; keep following instead. Never kill a deliver run by hand: that
@@ -81,6 +81,8 @@ Top level: `version`, `project`, `memory`, `worktree`, `costOptimization`,
81
81
  | `UAP_DELIVER_AUTO` | `0` disables auto-deliver | enabled |
82
82
  | `UAP_DELIVER_UNTIL_DELIVERED` | `0` disables loop-until-delivered | enabled |
83
83
  | `UAP_DELIVER_SANDBOX` | Deliver sandbox root path | — |
84
+ | `UAP_ACTIVE_RUNS_DIR` | Registry of live deliver runs, used for cross-root (nested-project) single-flight | `~/.uap/active-runs` |
85
+ | `UAP_DELIVER_NO_LOCK` | `1` disables the per-project lock **and** the overlapping-root check; the run also becomes invisible to other runs' checks | enabled |
84
86
  | `UAP_HALO_TRACE` | `1` enables HALO tracing | off |
85
87
  | `UAP_HALO_TRACE_PATH` | HALO trace output file | `.uap/halo/traces.jsonl` |
86
88
  | `UAP_HALO_PROJECT_ID` | HALO project id | `uap` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miller-tech/uap",
3
- "version": "1.186.2",
3
+ "version": "1.187.3",
4
4
  "description": "Autonomous AI agent memory system with CLAUDE.md protocol enforcement",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -21,7 +21,7 @@
21
21
  "start": "node dist/bin/cli.js",
22
22
  "test": "vitest",
23
23
  "test:ci": "vitest run",
24
- "test:enforcers": "UAP_PROXY_ENV_AUTOLOAD=0 python3 -m unittest tools.agents.tests.test_enforcement_self_protect tools.agents.tests.test_schema_diff_gate tools.agents.tests.test_stream_telemetry tools.agents.tests.test_project_telemetry_events tools.agents.tests.test_workdir_scope_enforcer tools.agents.tests.test_gate_evidence tools.agents.tests.test_gate_integrity tools.agents.tests.test_gate_failclosed_indirection tools.agents.tests.test_expert_review_pr_scope tools.agents.tests.test_path_containment tools.agents.tests.test_path_normalizer_hardened tools.agents.tests.test_sandbox_tool_strip tools.agents.tests.test_proxy_env_loader tools.agents.tests.test_stream_required_tool tools.agents.tests.test_tool_call_wrapper_profiles tools.agents.tests.test_tool_convert_cache tools.agents.tests.test_doubling_break tools.agents.tests.test_error_loop_ignores_correctives tools.agents.tests.test_attractor_detection tools.agents.tests.test_client_disconnect tools.agents.tests.test_confidence_escalation tools.agents.tests.test_coordination_ban tools.agents.tests.test_coordination_early_ban tools.agents.tests.test_cycle_break_exploration tools.agents.tests.test_deferral_break tools.agents.tests.test_deliver_autoroute tools.agents.tests.test_delivery_enforcement_all_langs tools.agents.tests.test_delivery_enforcement_exemptions tools.agents.tests.test_delivery_enforcement_filepath tools.agents.tests.test_delivery_enforcement_web_and_bash tools.agents.tests.test_disconnect_watcher tools.agents.tests.test_empty_maxtokens_recovery tools.agents.tests.test_empty_tool_loop_break tools.agents.tests.test_enforcer_escape_hatches tools.agents.tests.test_error_loop_break tools.agents.tests.test_finalize_suppression tools.agents.tests.test_malformed_unclosed_think tools.agents.tests.test_mandate_beats_recon tools.agents.tests.test_mandate_deliver tools.agents.tests.test_overflow_truncate_count_tokens tools.agents.tests.test_passthrough_oauth tools.agents.tests.test_project_telemetry tools.agents.tests.test_proxy_auth_headers tools.agents.tests.test_prune_preserve_force_write tools.agents.tests.test_recon_deliver_gate tools.agents.tests.test_session_admission tools.agents.tests.test_stream_heartbeat tools.agents.tests.test_stuck_break_reattach tools.agents.tests.test_turn_count_breaker_periodic tools.agents.tests.test_upstream_chokepoint tools.agents.tests.test_vision_passthrough tools.agents.tests.test_worktree_required tools.agents.tests.test_enforcer_suite_coverage tools.agents.tests.test_validate_plan_gate tools.agents.tests.test_validate_plan_inside_project tools.agents.tests.test_anthropic_proxy_streaming tools.agents.tests.test_delivery_enforcement_worktree tools.agents.tests.test_output_token_ceilings tools.agents.tests.test_tool_narrowing_core tools.agents.tests.test_models_context_window",
24
+ "test:enforcers": "UAP_PROXY_ENV_AUTOLOAD=0 python3 -m unittest tools.agents.tests.test_enforcement_self_protect tools.agents.tests.test_schema_diff_gate tools.agents.tests.test_stream_telemetry tools.agents.tests.test_project_telemetry_events tools.agents.tests.test_workdir_scope_enforcer tools.agents.tests.test_gate_evidence tools.agents.tests.test_repeat_call_guard tools.agents.tests.test_gate_integrity tools.agents.tests.test_gate_failclosed_indirection tools.agents.tests.test_expert_review_pr_scope tools.agents.tests.test_path_containment tools.agents.tests.test_path_normalizer_hardened tools.agents.tests.test_sandbox_tool_strip tools.agents.tests.test_proxy_env_loader tools.agents.tests.test_stream_required_tool tools.agents.tests.test_tool_call_wrapper_profiles tools.agents.tests.test_tool_convert_cache tools.agents.tests.test_doubling_break tools.agents.tests.test_error_loop_ignores_correctives tools.agents.tests.test_attractor_detection tools.agents.tests.test_client_disconnect tools.agents.tests.test_confidence_escalation tools.agents.tests.test_coordination_ban tools.agents.tests.test_coordination_early_ban tools.agents.tests.test_cycle_break_exploration tools.agents.tests.test_deferral_break tools.agents.tests.test_deliver_autoroute tools.agents.tests.test_delivery_enforcement_all_langs tools.agents.tests.test_delivery_enforcement_exemptions tools.agents.tests.test_delivery_enforcement_filepath tools.agents.tests.test_delivery_enforcement_web_and_bash tools.agents.tests.test_disconnect_watcher tools.agents.tests.test_empty_maxtokens_recovery tools.agents.tests.test_empty_tool_loop_break tools.agents.tests.test_enforcer_escape_hatches tools.agents.tests.test_error_loop_break tools.agents.tests.test_finalize_suppression tools.agents.tests.test_malformed_unclosed_think tools.agents.tests.test_mandate_beats_recon tools.agents.tests.test_mandate_deliver tools.agents.tests.test_overflow_truncate_count_tokens tools.agents.tests.test_passthrough_oauth tools.agents.tests.test_project_telemetry tools.agents.tests.test_proxy_auth_headers tools.agents.tests.test_prune_preserve_force_write tools.agents.tests.test_recon_deliver_gate tools.agents.tests.test_session_admission tools.agents.tests.test_stream_heartbeat tools.agents.tests.test_stuck_break_reattach tools.agents.tests.test_turn_count_breaker_periodic tools.agents.tests.test_upstream_chokepoint tools.agents.tests.test_vision_passthrough tools.agents.tests.test_worktree_required tools.agents.tests.test_enforcer_suite_coverage tools.agents.tests.test_validate_plan_gate tools.agents.tests.test_validate_plan_inside_project tools.agents.tests.test_anthropic_proxy_streaming tools.agents.tests.test_delivery_enforcement_worktree tools.agents.tests.test_output_token_ceilings tools.agents.tests.test_tool_narrowing_core tools.agents.tests.test_models_context_window",
25
25
  "test:coverage": "vitest --coverage",
26
26
  "bench": "vitest --config vitest.bench.config.ts",
27
27
  "lint": "eslint src --ext .ts",
@@ -246,6 +246,18 @@ def _laundered_infra_kill(cmd: str) -> bool:
246
246
  # exists to prevent. delivery_enforcement._deliver_lock_holder() has taken the
247
247
  # same precaution since the PID-reuse incident; this must not diverge from it.
248
248
  _PID_TOKEN_RE = re.compile(r"(?<![\w.])(-?\d{1,10})(?![\w.-])")
249
+ # The label both deliver paths agree on, so the caller has one thing to compare.
250
+ DELIVER_LABEL = "the deliver run in progress"
251
+
252
+ # A deliver PROCESS, by argv. Deliberately separate from _STACK_ARGV_RE: a
253
+ # deliver run is protected for a different reason than llama-server, and gets a
254
+ # different remedy, so the two must not be told apart by string-matching the
255
+ # combined pattern's output.
256
+ _DELIVER_ARGV_RE = re.compile(
257
+ r"(\buap\s+deliver\b|(?:cli\.js|uap)\s+(?:\S+\s+)*deliver\b)",
258
+ re.IGNORECASE,
259
+ )
260
+
249
261
  _STACK_ARGV_RE = re.compile(
250
262
  r"(llama-server|anthropic_proxy|nomic-embed"
251
263
  r"|\buap\s+deliver\b|(?:cli\.js|uap)\s+(?:\S+\s+)*deliver\b)",
@@ -276,7 +288,7 @@ def _lock_holder_pids() -> dict[str, str]:
276
288
  continue
277
289
  m = re.match(r"\s*(\d+)", text)
278
290
  if m:
279
- holders[str(int(m.group(1)))] = "the deliver run in progress"
291
+ holders[str(int(m.group(1)))] = DELIVER_LABEL
280
292
  return holders
281
293
 
282
294
 
@@ -293,14 +305,61 @@ def _identify_pid(pid: str, holders: dict[str, str]) -> str | None:
293
305
  return None # dead: a stale lock protects nothing
294
306
  m = _STACK_ARGV_RE.search(argv)
295
307
  if pid in holders:
296
- # Confirm identity too — a recycled PID must not inherit the claim.
297
- return holders[pid] if m else None
298
- return m.group(1).lower() if m else None
308
+ # Confirm identity too — a recycled PID must not inherit the claim. The
309
+ # claim is "this is the deliver run", so confirm it with the DELIVER
310
+ # pattern: checking the combined stack pattern let a stale lock whose
311
+ # PID had been recycled by llama-server inherit the deliver label, and
312
+ # with it a remedy that does not apply to the stack.
313
+ if _DELIVER_ARGV_RE.search(argv):
314
+ return holders[pid]
315
+ return m.group(1).lower() if m else None
316
+ if not m:
317
+ return None
318
+ # Canonicalise a deliver match to the SAME label the lock-holder path uses.
319
+ # Without this the caller fell through to the inference-stack message, which
320
+ # told the operator that killing their own mission "ends your own session"
321
+ # and pointed at llama-server / the proxy — none of which is true. The
322
+ # lock-holder path only fires when the lock is under THIS repo, so any
323
+ # deliver launched against another project (the usual case) got the wrong
324
+ # message and the wrong remedy.
325
+ if _DELIVER_ARGV_RE.search(argv):
326
+ return DELIVER_LABEL
327
+ return m.group(1).lower()
328
+
329
+
330
+ _PROBE_RE = re.compile(r"\bkill\b\s+(?:-0\b|-s\s+0\b|-s\s+SIGNULL\b)", re.IGNORECASE)
331
+
332
+
333
+ def _only_liveness_probes(text: str) -> bool:
334
+ """True when EVERY kill in `text` is `kill -0` / `kill -s 0` — a probe.
335
+
336
+ Signal 0 performs error checking only: it tests whether the PID exists and
337
+ is signalable, and delivers nothing. Refusing it protects nothing and denies
338
+ the cheapest way to ask "is that run still alive?".
339
+
340
+ Scoped per STATEMENT, and deliberately so. Testing the whole command instead
341
+ let one probe anywhere switch rule 9 off for every PID in the text, so
342
+ `kill -0 $P && kill -9 $P` — the canonical "kill it if it is alive" idiom,
343
+ and the `if kill -0 …; then kill -9 …; fi` form — reached the stack. Rule 9
344
+ is the ONLY guard on a bare-PID kill (rules 1-8 all need pkill/killall, a
345
+ service name, a -f pattern or a lookup verb), so a whole-text exemption
346
+ turned this module's one un-spell-around-able rule into a text rule whose
347
+ password was two characters. Measured before the fix: all four laundering
348
+ spellings were allowed.
349
+ """
350
+ kills = [
351
+ seg for seg in _STATEMENT_SPLIT_RE.split(text) if _KILL_VERB_RE.search(seg)
352
+ ]
353
+ if not kills:
354
+ return False
355
+ return all(_PROBE_RE.search(seg) for seg in kills)
299
356
 
300
357
 
301
358
  def _protected_pid_hit(text: str) -> tuple[str, str] | None:
302
359
  if not (_KILL_VERB_RE.search(text) and _PID_TOKEN_RE.search(text)):
303
360
  return None
361
+ if _only_liveness_probes(text):
362
+ return None
304
363
  holders = _lock_holder_pids()
305
364
  seen: set[str] = set()
306
365
  for m in _PID_TOKEN_RE.finditer(text):
@@ -325,6 +384,10 @@ DELIVER_PID_REASON = (
325
384
  "call the deliver tool with follow:true, which answers within about a "
326
385
  "minute; a 'STILL RUNNING' answer is normal and means keep polling, not "
327
386
  "fail. From a shell, `uap deliver --await-run` blocks until the run ends. "
387
+ "If it genuinely must stop, do NOT kill it: request the COOPERATIVE stop "
388
+ "(the dashboard's Cancel, or `touch <projectRoot>/.uap/deliver-runs/<runId>/STOP`) — "
389
+ "the loop observes it at the next turn boundary and exits with its work "
390
+ "checkpointed and the lock released, which a signal does not. "
328
391
  "Operator override: set UAP_INFRA_PROTECT_OFF=1 in the launch environment "
329
392
  "(not inline on the command)."
330
393
  )
@@ -438,7 +501,7 @@ def main() -> None:
438
501
  hit = _protected_pid_hit(text)
439
502
  if hit:
440
503
  pid, what = hit
441
- if what == "the deliver run in progress":
504
+ if what == DELIVER_LABEL:
442
505
  emit(False, DELIVER_PID_REASON.format(pid=pid) + label)
443
506
  emit(False, STACK_PID_REASON.format(what=what, pid=pid) + label)
444
507
  emit(True, "no infra-destructive pattern")
@@ -75,7 +75,27 @@ BYPASS_PATTERNS = (
75
75
  # not an override — it is an off switch, the same reasoning as every entry
76
76
  # above it.
77
77
  re.compile(r"UAP_DELIVER_NO_LOCK\s*=\s*['\"]?1", re.I),
78
+ # The gateless-root refusal exists BECAUSE an agent-driven launch does not
79
+ # honour a warning: the warning shipped, fired, named the right root, and
80
+ # the run went ahead at the gateless root anyway, then spent 34 minutes
81
+ # taking a crate from 1 failing test to 3 while reporting "100% of gates".
82
+ # A hatch the constrained party can write into its own command line is not
83
+ # an override, it is an off switch — same reasoning as every entry above.
84
+ # The CLI-flag spelling is just as self-grantable, but it is matched
85
+ # separately below: a bare flag NAME appears in ordinary prose (docs,
86
+ # commit messages, `grep`), so scanning the raw command for it refused
87
+ # honest work. It runs against `scannable_command`, which blanks quoted
88
+ # blobs, instead.
89
+ re.compile(r"UAP_ALLOW_GATELESS_ROOT\s*=\s*['\"]?1", re.I),
78
90
  )
91
+
92
+ # The gateless-root override in its CLI-flag spelling. Anchored on both sides
93
+ # so a longer option that merely starts the same way does not collide, and
94
+ # case-sensitive because commander does not accept an upper-case spelling.
95
+ GATELESS_FLAG_RE = re.compile(r"(?:^|\s)--allow-gateless-root(?:[=\s]|$)")
96
+ # Only a deliver invocation can be bypassed by the flag. `\b` keeps this off
97
+ # `src/delivery/`, which merely starts with the same letters.
98
+ DELIVER_INVOCATION_RE = re.compile(r"\bdeliver\b")
79
99
  # Destructive ops against the enforcer/policy surface.
80
100
  #
81
101
  # HONEST SCOPE: this is a NUDGE, not a boundary. An agent that can write a file
@@ -419,6 +439,22 @@ def main() -> None:
419
439
 
420
440
  if op in {"Bash", "bash"}:
421
441
  cmd = args.get("command") or ""
442
+ # The flag form is checked against the SCANNABLE command, not the raw
443
+ # one: `scannable_command` blanks multi-word quoted blobs, so a commit
444
+ # message or a `uap memory store "..."` that merely NAMES the switch is
445
+ # not refused, while text handed to a shell is left intact and still
446
+ # caught. It must ALSO look like a deliver invocation — the flag does
447
+ # nothing anywhere else, so refusing `rg -- <flag> src/` bought no
448
+ # security and blocked someone auditing the switch.
449
+ _scannable = scannable_command(cmd)
450
+ if GATELESS_FLAG_RE.search(_scannable) and DELIVER_INVOCATION_RE.search(_scannable):
451
+ emit(
452
+ False,
453
+ "BLOCKED: setting a delivery-enforcement bypass/advisory flag is "
454
+ "not allowed for the agent. Route your change through the "
455
+ "`deliver` tool instead of disabling the gate. "
456
+ "(Operator-only override: UAP_SELF_PROTECT_OFF=1.)",
457
+ )
422
458
  for pat in BYPASS_PATTERNS:
423
459
  if pat.search(cmd):
424
460
  emit(
@@ -122,8 +122,16 @@ if echo "$CMD" | grep -qE '\bgit\s+reset\s+--hard\b|\bgit\s+clean\s+-[a-z]*f'; t
122
122
  fi
123
123
 
124
124
  # ─── Manual Version Edit Protection ─────────────────────────────
125
- # Block direct edits to package.json version field via sed/awk
126
- if echo "$CMD" | grep -qE "(sed|awk).*package\.json.*(version|\"version\")|((sed|awk).*version.*package\.json)|(jq.*\.version.*package\.json)"; then
125
+ # Block direct edits to package.json version field via sed/awk/jq.
126
+ #
127
+ # Scoped to a single shell STATEMENT. Matching across the whole command line
128
+ # meant any sed/awk anywhere plus `package.json` and `version` anywhere later
129
+ # tripped it, so reading the version alongside unrelated text munging was
130
+ # refused: `curl ... | sed 's/^/x/'; node -p "require('./package.json').version"`
131
+ # That is a READ. Splitting on ; && || | and newline keeps every real edit
132
+ # (each has sed/awk/jq and package.json inside one statement) and drops the
133
+ # cross-statement coincidence. Verified against both corpora before and after.
134
+ if printf '%s' "$CMD" | tr ';|&\n' '\n\n\n\n' | grep -qE "(sed|awk).*package\.json.*(version|\"version\")|((sed|awk).*version.*package\.json)|(jq.*\.version.*package\.json)"; then
127
135
  echo "BLOCKED [semver-versioning]: Manual package.json version edits are prohibited. Use: npm run version:patch, version:minor, or version:major. See policies/semver-versioning.md" >&2
128
136
  exit 2
129
137
  fi
@@ -169,7 +169,10 @@ if not hit and cmd:
169
169
  break
170
170
  bypass = re.search(
171
171
  r"UAP_DELIVER_BYPASS\s*=\s*[\x27\"]?1|UAP_ENFORCE_DELIVERY\s*=\s*[\x27\"]?(advisory|off|0|false|no)"
172
- r"|UAP_SELF_PROTECT_OFF\s*=\s*[\x27\"]?1|UAP_NO_WORKTREE\s*=\s*[\x27\"]?1|UAP_WORKDIR_SCOPE_OFF\s*=\s*[\x27\"]?1|UAP_USER_VALIDATION\s*=\s*[\x27\"]?0",
172
+ r"|UAP_SELF_PROTECT_OFF\s*=\s*[\x27\"]?1|UAP_NO_WORKTREE\s*=\s*[\x27\"]?1"
173
+ r"|UAP_WORKDIR_SCOPE_OFF\s*=\s*[\x27\"]?1|UAP_USER_VALIDATION\s*=\s*[\x27\"]?0"
174
+ r"|UAP_DELIVER_NO_LOCK\s*=\s*[\x27\"]?1|UAP_NO_REVIEW\s*=\s*[\x27\"]?1"
175
+ r"|UAP_INFRA_PROTECT_OFF\s*=\s*[\x27\"]?1|UAP_ALLOW_GATELESS_ROOT\s*=\s*[\x27\"]?1",
173
176
  cmd, re.I)
174
177
  print("1" if (hit or bypass) else "0")
175
178
  ' 2>/dev/null || echo 1)"
@@ -374,6 +374,30 @@ _RATE_LIMITED_API_RE = re.compile(r"api\.github\.com", re.IGNORECASE)
374
374
  PROXY_STUCK_TEXT_THRESHOLD = int(os.environ.get("PROXY_STUCK_TEXT_THRESHOLD", "2"))
375
375
  PROXY_STUCK_API_THRESHOLD = int(os.environ.get("PROXY_STUCK_API_THRESHOLD", "3"))
376
376
 
377
+ # REPEAT-CALL guardrail: the same tool call, with the same arguments, over and
378
+ # over -- while SUCCEEDING every time.
379
+ #
380
+ # Observed live (opencode + qwen3.6, 2026-08-07): `git diff --stat` re-issued 44
381
+ # times in one run, ~2.5s apart, until the operator interrupted. On screen it
382
+ # reads as the final message repeating forever.
383
+ #
384
+ # Every existing guard missed it, and each for a defensible reason:
385
+ # STUCK-BREAK needs self-reported "stuck" phrasing or an api.github.com arg.
386
+ # ERROR-LOOP needs a repeated tool-RESULT error signature; this call works.
387
+ # LOOP BREAKER detects the identical fingerprint, but is ANDed with
388
+ # no_progress_streak -- and a command that returns output every
389
+ # time never accumulates one, so the condition never holds.
390
+ #
391
+ # The blind spot is therefore a repeatedly-SUCCESSFUL identical call: the other
392
+ # guards all key off failure or self-awareness, and this loop has neither. A
393
+ # read-only command issued four times with identical arguments is not a
394
+ # strategy, so this fires on the fingerprint alone, independent of outcome.
395
+ # PROXY_REPEAT_CALL_THRESHOLD=0 disables.
396
+ PROXY_REPEAT_CALL_THRESHOLD = int(os.environ.get("PROXY_REPEAT_CALL_THRESHOLD", "4"))
397
+ # Marker so the injected directive can address a SUCCEEDING loop correctly
398
+ # rather than telling the model to stop retrying "a failing action".
399
+ _REPEAT_CALL_REASON = "identical tool call"
400
+
377
401
  # ---------------------------------------------------------------------------
378
402
  # ERROR-LOOP guardrail: the model edits, runs a command, hits the SAME failure,
379
403
  # edits again (a DIFFERENT edit), runs, hits the same failure — for many turns.
@@ -1993,6 +2017,14 @@ class SessionMonitor:
1993
2017
  return True, f"self-reported stuck x{self.self_stuck_streak}"
1994
2018
  if self.rate_limited_api_streak >= PROXY_STUCK_API_THRESHOLD:
1995
2019
  return True, f"rate-limited-API retries x{self.rate_limited_api_streak}"
2020
+ # Repeated identical call, judged on the fingerprint ALONE. Deliberately
2021
+ # not ANDed with no_progress_streak the way the LOOP BREAKER is: a call
2022
+ # that succeeds every time never builds a no-progress streak, which is
2023
+ # exactly how a 44-turn `git diff --stat` loop ran unchallenged.
2024
+ if PROXY_REPEAT_CALL_THRESHOLD > 0:
2025
+ looping, count = self.detect_tool_loop(window=PROXY_REPEAT_CALL_THRESHOLD)
2026
+ if looping and count >= PROXY_REPEAT_CALL_THRESHOLD:
2027
+ return True, f"{_REPEAT_CALL_REASON} x{count}"
1996
2028
  return False, ""
1997
2029
 
1998
2030
  def note_deferral_signal(self, text: str, had_tool_call: bool) -> None:
@@ -5677,6 +5709,51 @@ def _strip_sandbox_unreachable_tools(body: dict) -> int:
5677
5709
  return removed
5678
5710
 
5679
5711
 
5712
+ def _seed_tool_history_from_request(monitor: "SessionMonitor", messages: list) -> None:
5713
+ """Rebuild the tool-call streak from the CONVERSATION, not from server state.
5714
+
5715
+ Every streak guard here counted appends to a per-session SessionMonitor. That
5716
+ monitor is keyed `fp:<hash of the first user message>` whenever the client
5717
+ sends no session header — and opencode sends none. So anything that shifts
5718
+ that text (compaction, a re-summarised opening turn) silently starts a FRESH
5719
+ monitor with empty history, and every streak restarts at zero. A proxy
5720
+ restart mid-session does the same.
5721
+
5722
+ The client re-sends the whole conversation each turn, so the streak is
5723
+ already in the request. Deriving it from there makes the guards independent
5724
+ of monitor identity and of proxy uptime.
5725
+
5726
+ Only ever EXTENDS: if the monitor already knows at least as much as the
5727
+ request implies, it is left alone, so this cannot double-count the normal
5728
+ path that appends one fingerprint per request.
5729
+ """
5730
+ if not isinstance(messages, list):
5731
+ return
5732
+ rebuilt: list[str] = []
5733
+ for msg in messages:
5734
+ if not isinstance(msg, dict) or msg.get("role") != "assistant":
5735
+ continue
5736
+ content = msg.get("content")
5737
+ if not isinstance(content, list):
5738
+ continue
5739
+ fps = [
5740
+ _tool_call_fingerprint(b)
5741
+ for b in content
5742
+ if isinstance(b, dict) and b.get("type") == "tool_use"
5743
+ ]
5744
+ if fps:
5745
+ rebuilt.append("|".join(sorted(fps)))
5746
+ # Drop the LAST turn: the incremental path immediately appends that one, and
5747
+ # seeding it here too would count the current turn twice — inflating every
5748
+ # streak by one and firing the guards a turn early. Caught by two existing
5749
+ # streaming tests, which replay one body three times and reached the
5750
+ # threshold sooner than they should have.
5751
+ rebuilt = rebuilt[:-1]
5752
+ if len(rebuilt) > len(monitor.tool_call_history):
5753
+ # Keep the same bound the incremental path uses.
5754
+ monitor.tool_call_history = rebuilt[-30:]
5755
+
5756
+
5680
5757
  def _maybe_inject_stuck_break(openai_body: dict, monitor: "SessionMonitor") -> None:
5681
5758
  """Force a terminal turn when the model is looping self-awarely or hammering
5682
5759
  a rate-limited API. Unlike the cycle-breaker (which narrows tools), this
@@ -5697,15 +5774,29 @@ def _maybe_inject_stuck_break(openai_body: dict, monitor: "SessionMonitor") -> N
5697
5774
  if monitor.sandboxed
5698
5775
  else "use the browser tool or `git clone` (git protocol)"
5699
5776
  )
5700
- directive = (
5701
- "\n\nSTOP you are repeating a failing action (" + reason + "). Do NOT "
5702
- "retry the same tool or fetch again. If a resource is unreachable (e.g. a "
5703
- "rate-limited GitHub REST API), switch channel: " + channel_hint + ", NOT "
5704
- "api.github.com. If it is still "
5705
- "unavailable, proceed WITHOUT it using what you already have, or ask the "
5706
- "operator the single blocking question in one sentence. Take a DIFFERENT "
5707
- "action now."
5708
- )
5777
+ if reason.startswith(_REPEAT_CALL_REASON):
5778
+ # The call SUCCEEDS every time, so "stop retrying a failing action" and
5779
+ # the switch-channel advice would both be nonsense here. Name the real
5780
+ # problem: the answer is already in hand and re-asking cannot change it.
5781
+ directive = (
5782
+ "\n\nSTOP you have issued the same tool call with the same "
5783
+ "arguments " + reason.rsplit("x", 1)[-1] + " times in a row. It "
5784
+ "SUCCEEDED each time and the result will not change by asking again. "
5785
+ "You already have that output. Do NOT repeat it. Either take the "
5786
+ "NEXT concrete action using what it told you, or — if you genuinely "
5787
+ "cannot proceed — state the single blocking question in one sentence "
5788
+ "and stop. Answer in plain text now if the work is done."
5789
+ )
5790
+ else:
5791
+ directive = (
5792
+ "\n\nSTOP — you are repeating a failing action (" + reason + "). Do NOT "
5793
+ "retry the same tool or fetch again. If a resource is unreachable (e.g. a "
5794
+ "rate-limited GitHub REST API), switch channel: " + channel_hint + ", NOT "
5795
+ "api.github.com. If it is still "
5796
+ "unavailable, proceed WITHOUT it using what you already have, or ask the "
5797
+ "operator the single blocking question in one sentence. Take a DIFFERENT "
5798
+ "action now."
5799
+ )
5709
5800
  msgs = openai_body.get("messages")
5710
5801
  if not isinstance(msgs, list):
5711
5802
  msgs = []
@@ -7095,6 +7186,7 @@ def _record_last_assistant_tool_calls(
7095
7186
  _latest_err = any(_flags)
7096
7187
  break
7097
7188
  monitor.note_tool_result_error(_latest_tr, _latest_err)
7189
+ _seed_tool_history_from_request(monitor, messages)
7098
7190
  tool_fingerprints = []
7099
7191
  tool_targets: dict[str, str] = {}
7100
7192
  assistant_had_text = False # Fix B: did the last assistant turn emit prose?