@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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/bin/cli.js +4 -0
- package/dist/bin/cli.js.map +1 -1
- package/dist/cli/deliver.d.ts +100 -0
- package/dist/cli/deliver.d.ts.map +1 -1
- package/dist/cli/deliver.js +507 -20
- package/dist/cli/deliver.js.map +1 -1
- package/dist/coordination/reactor.d.ts.map +1 -1
- package/dist/coordination/reactor.js +28 -5
- package/dist/coordination/reactor.js.map +1 -1
- package/dist/delivery/agentic-executor.d.ts +3 -3
- package/dist/delivery/agentic-executor.d.ts.map +1 -1
- package/dist/delivery/agentic-executor.js +74 -4
- package/dist/delivery/agentic-executor.js.map +1 -1
- package/dist/delivery/convergence-loop.d.ts +6 -0
- package/dist/delivery/convergence-loop.d.ts.map +1 -1
- package/dist/delivery/convergence-loop.js +31 -0
- package/dist/delivery/convergence-loop.js.map +1 -1
- package/dist/delivery/edit-match.d.ts.map +1 -1
- package/dist/delivery/edit-match.js +19 -1
- package/dist/delivery/edit-match.js.map +1 -1
- package/dist/delivery/epic-controller.d.ts +6 -0
- package/dist/delivery/epic-controller.d.ts.map +1 -1
- package/dist/delivery/epic-controller.js +10 -0
- package/dist/delivery/epic-controller.js.map +1 -1
- package/dist/delivery/epic-mission.d.ts.map +1 -1
- package/dist/delivery/epic-mission.js +3 -0
- package/dist/delivery/epic-mission.js.map +1 -1
- package/dist/delivery/verifier-ladder.d.ts +99 -6
- package/dist/delivery/verifier-ladder.d.ts.map +1 -1
- package/dist/delivery/verifier-ladder.js +208 -25
- package/dist/delivery/verifier-ladder.js.map +1 -1
- package/dist/mcp-router/tools/deliver.d.ts.map +1 -1
- package/dist/mcp-router/tools/deliver.js +17 -0
- package/dist/mcp-router/tools/deliver.js.map +1 -1
- package/dist/models/long-fetch.d.ts +19 -0
- package/dist/models/long-fetch.d.ts.map +1 -1
- package/dist/models/long-fetch.js +47 -0
- package/dist/models/long-fetch.js.map +1 -1
- package/dist/telemetry/tool-failure.d.ts +9 -0
- package/dist/telemetry/tool-failure.d.ts.map +1 -1
- package/dist/telemetry/tool-failure.js +4 -0
- package/dist/telemetry/tool-failure.js.map +1 -1
- package/docs/reference/CLI.md +13 -2
- package/docs/reference/CONFIGURATION.md +2 -0
- package/package.json +2 -2
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/src/policies/enforcers/enforcement_infra_protect.py +68 -5
- package/src/policies/enforcers/enforcement_self_protect.py +36 -0
- package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
- package/templates/hooks/pre-tool-use-bash.sh +10 -2
- package/templates/hooks/uap-policy-gate.sh +4 -1
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
- package/tools/agents/scripts/anthropic_proxy.py +101 -9
- 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,
|
|
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":"AAAA;;;;;;;;;;;;;;;GAeG;
|
|
1
|
+
{"version":3,"file":"tool-failure.js","sourceRoot":"","sources":["../../src/telemetry/tool-failure.ts"],"names":[],"mappings":"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"}
|
package/docs/reference/CLI.md
CHANGED
|
@@ -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
|
|
402
|
-
|
|
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.
|
|
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",
|
|
Binary file
|
|
@@ -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)))] =
|
|
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
|
-
|
|
298
|
-
|
|
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 ==
|
|
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(
|
|
Binary file
|
|
@@ -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
|
-
|
|
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
|
|
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)"
|
|
Binary file
|
|
@@ -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
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
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?
|