@miller-tech/uap 2.11.0 → 2.16.2
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/README.md +7 -6
- package/config/capacity-policy.json +15 -3
- package/config/model-profiles/qwen38.json +42 -34
- package/dist/.tsbuildinfo +1 -1
- package/dist/bin/cli.js +39 -0
- package/dist/bin/cli.js.map +1 -1
- package/dist/capacity/policy.d.ts +15 -0
- package/dist/capacity/policy.d.ts.map +1 -1
- package/dist/capacity/policy.js +23 -0
- package/dist/capacity/policy.js.map +1 -1
- package/dist/capacity/probe.d.ts.map +1 -1
- package/dist/capacity/probe.js +6 -1
- package/dist/capacity/probe.js.map +1 -1
- package/dist/cli/deliver.d.ts.map +1 -1
- package/dist/cli/deliver.js +148 -2
- package/dist/cli/deliver.js.map +1 -1
- package/dist/cli/inference.d.ts +13 -0
- package/dist/cli/inference.d.ts.map +1 -0
- package/dist/cli/inference.js +132 -0
- package/dist/cli/inference.js.map +1 -0
- package/dist/cli/loops.d.ts +39 -0
- package/dist/cli/loops.d.ts.map +1 -0
- package/dist/cli/loops.js +154 -0
- package/dist/cli/loops.js.map +1 -0
- package/dist/cli/wizard-config.d.ts.map +1 -1
- package/dist/cli/wizard-config.js +5 -1
- package/dist/cli/wizard-config.js.map +1 -1
- package/dist/cli/worktree.d.ts.map +1 -1
- package/dist/cli/worktree.js +8 -4
- package/dist/cli/worktree.js.map +1 -1
- package/dist/delivery/adversarial-attack.d.ts +99 -0
- package/dist/delivery/adversarial-attack.d.ts.map +1 -0
- package/dist/delivery/adversarial-attack.js +326 -0
- package/dist/delivery/adversarial-attack.js.map +1 -0
- package/dist/delivery/adversarial-gate.d.ts +120 -0
- package/dist/delivery/adversarial-gate.d.ts.map +1 -0
- package/dist/delivery/adversarial-gate.js +259 -0
- package/dist/delivery/adversarial-gate.js.map +1 -0
- package/dist/delivery/adversarial-sanction.d.ts +52 -0
- package/dist/delivery/adversarial-sanction.d.ts.map +1 -0
- package/dist/delivery/adversarial-sanction.js +134 -0
- package/dist/delivery/adversarial-sanction.js.map +1 -0
- package/dist/delivery/agentic-executor.d.ts +69 -22
- package/dist/delivery/agentic-executor.d.ts.map +1 -1
- package/dist/delivery/agentic-executor.js +152 -8
- package/dist/delivery/agentic-executor.js.map +1 -1
- package/dist/delivery/applier.d.ts +8 -0
- package/dist/delivery/applier.d.ts.map +1 -1
- package/dist/delivery/applier.js +3 -1
- package/dist/delivery/applier.js.map +1 -1
- package/dist/delivery/convergence-loop.d.ts +15 -0
- package/dist/delivery/convergence-loop.d.ts.map +1 -1
- package/dist/delivery/convergence-loop.js.map +1 -1
- package/dist/delivery/criteria-lint.d.ts +74 -0
- package/dist/delivery/criteria-lint.d.ts.map +1 -0
- package/dist/delivery/criteria-lint.js +134 -0
- package/dist/delivery/criteria-lint.js.map +1 -0
- package/dist/delivery/deliver-toggle.d.ts +21 -0
- package/dist/delivery/deliver-toggle.d.ts.map +1 -0
- package/dist/delivery/deliver-toggle.js +40 -0
- package/dist/delivery/deliver-toggle.js.map +1 -0
- package/dist/delivery/delivery-evidence.d.ts +109 -0
- package/dist/delivery/delivery-evidence.d.ts.map +1 -0
- package/dist/delivery/delivery-evidence.js +139 -0
- package/dist/delivery/delivery-evidence.js.map +1 -0
- package/dist/delivery/epic-mission.d.ts +5 -1
- package/dist/delivery/epic-mission.d.ts.map +1 -1
- package/dist/delivery/epic-mission.js +34 -6
- package/dist/delivery/epic-mission.js.map +1 -1
- package/dist/delivery/execution-gate.d.ts.map +1 -1
- package/dist/delivery/execution-gate.js +90 -7
- package/dist/delivery/execution-gate.js.map +1 -1
- package/dist/delivery/index.d.ts +9 -1
- package/dist/delivery/index.d.ts.map +1 -1
- package/dist/delivery/index.js +9 -1
- package/dist/delivery/index.js.map +1 -1
- package/dist/delivery/journey-depth.d.ts +61 -0
- package/dist/delivery/journey-depth.d.ts.map +1 -0
- package/dist/delivery/journey-depth.js +91 -0
- package/dist/delivery/journey-depth.js.map +1 -0
- package/dist/delivery/master-pipeline.d.ts +50 -0
- package/dist/delivery/master-pipeline.d.ts.map +1 -0
- package/dist/delivery/master-pipeline.js +52 -0
- package/dist/delivery/master-pipeline.js.map +1 -0
- package/dist/delivery/mission-acceptance.d.ts.map +1 -1
- package/dist/delivery/mission-acceptance.js +54 -2
- package/dist/delivery/mission-acceptance.js.map +1 -1
- package/dist/delivery/orchestrated-mission.d.ts +4 -1
- package/dist/delivery/orchestrated-mission.d.ts.map +1 -1
- package/dist/delivery/orchestrated-mission.js +18 -9
- package/dist/delivery/orchestrated-mission.js.map +1 -1
- package/dist/delivery/plan-check.d.ts +1 -0
- package/dist/delivery/plan-check.d.ts.map +1 -1
- package/dist/delivery/plan-check.js +48 -0
- package/dist/delivery/plan-check.js.map +1 -1
- package/dist/delivery/task-orchestrator.d.ts +16 -9
- package/dist/delivery/task-orchestrator.d.ts.map +1 -1
- package/dist/delivery/task-orchestrator.js +4 -2
- package/dist/delivery/task-orchestrator.js.map +1 -1
- package/dist/delivery/task-workspace.d.ts +35 -5
- package/dist/delivery/task-workspace.d.ts.map +1 -1
- package/dist/delivery/task-workspace.js +53 -7
- package/dist/delivery/task-workspace.js.map +1 -1
- package/dist/delivery/test-oracle-additive.d.ts +26 -6
- package/dist/delivery/test-oracle-additive.d.ts.map +1 -1
- package/dist/delivery/test-oracle-additive.js +40 -15
- package/dist/delivery/test-oracle-additive.js.map +1 -1
- package/dist/delivery/user-validation.d.ts +14 -0
- package/dist/delivery/user-validation.d.ts.map +1 -1
- package/dist/delivery/user-validation.js +103 -9
- package/dist/delivery/user-validation.js.map +1 -1
- package/dist/delivery/verifier-ladder.d.ts.map +1 -1
- package/dist/delivery/verifier-ladder.js +24 -6
- package/dist/delivery/verifier-ladder.js.map +1 -1
- package/dist/inference/analysis.d.ts +138 -0
- package/dist/inference/analysis.d.ts.map +1 -0
- package/dist/inference/analysis.js +243 -0
- package/dist/inference/analysis.js.map +1 -0
- package/dist/inference/probe.d.ts +85 -0
- package/dist/inference/probe.d.ts.map +1 -0
- package/dist/inference/probe.js +403 -0
- package/dist/inference/probe.js.map +1 -0
- package/dist/models/types.d.ts.map +1 -1
- package/dist/models/types.js +45 -18
- package/dist/models/types.js.map +1 -1
- package/dist/self-tuning/profiles/qwen38.d.ts +17 -8
- package/dist/self-tuning/profiles/qwen38.d.ts.map +1 -1
- package/dist/self-tuning/profiles/qwen38.js +21 -11
- package/dist/self-tuning/profiles/qwen38.js.map +1 -1
- package/dist/utils/model-slots.d.ts +11 -5
- package/dist/utils/model-slots.d.ts.map +1 -1
- package/dist/utils/model-slots.js +11 -5
- package/dist/utils/model-slots.js.map +1 -1
- package/docs/INDEX.md +7 -1
- package/docs/guides/CAPACITY_POLICY.md +29 -4
- package/docs/guides/INFERENCE_HEALTH.md +184 -0
- package/docs/guides/PROXY.md +1 -0
- package/docs/guides/QWEN38_EXL3.md +268 -0
- package/docs/plans/hlg-master-architecture-uplift-2026-09-24.md +67 -0
- package/docs/plans/system1-uplift-2026-09-18.md +1 -2
- package/docs/reference/CONFIGURATION.md +10 -1
- package/docs/reference/CONFIGURATION_REFERENCE.md +14 -0
- package/docs/reference/automode-risk.md +1 -1
- package/docs/reference/evidence-bound-ship.md +3 -2
- package/docs/reference/semantic-supervisor.md +3 -3
- package/docs/reference/system1-classifier.md +4 -4
- package/package.json +2 -2
- package/scripts/lib/llama-upstream.sh +7 -1
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/src/policies/enforcers/__pycache__/expert_review_required.cpython-312.pyc +0 -0
- package/src/policies/enforcers/__pycache__/quality_metrics_gate.cpython-312.pyc +0 -0
- package/src/policies/enforcers/expert_review_required.py +1 -4
- package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
- package/tools/agents/scripts/anthropic_proxy.py +374 -16
- package/tools/agents/scripts/confidence_escalation.py +4 -2
- package/tools/agents/scripts/project_telemetry.py +1 -0
- package/tools/agents/scripts/tool_risk.py +1 -0
- package/tools/agents/scripts/tool_risk_calibrate.py +1 -0
- package/tools/agents/scripts/toolcall_path_normalizer.py +1 -0
- package/tools/agents/tests/test_anthropic_proxy_streaming.py +52 -1
- package/tools/agents/tests/test_confidence_escalation.py +3 -2
- package/tools/agents/tests/test_count_tokens_scale_per_session.py +126 -0
- package/tools/agents/tests/test_expert_review_pr_scope.py +10 -11
- package/tools/agents/tests/test_lockstep_escalation.py +324 -0
- package/tools/agents/tests/test_thinking_template_kwargs.py +10 -4
|
@@ -42,26 +42,6 @@ export declare function editDiagnosticsEnabled(): boolean;
|
|
|
42
42
|
* Which way that trades is model-dependent, so it is measured, not asserted.
|
|
43
43
|
*/
|
|
44
44
|
export declare function readWindowBytes(): number;
|
|
45
|
-
/**
|
|
46
|
-
* One window of a file, plus an honest statement of what was left out.
|
|
47
|
-
*
|
|
48
|
-
* `read_file` used to return `contents.slice(0, readWindowBytes())` and say
|
|
49
|
-
* nothing about it. On a big file that is a trap with no exit: the model asks
|
|
50
|
-
* for the file, receives the first 8 KB, and has no way to learn that the rest
|
|
51
|
-
* exists or how to reach it.
|
|
52
|
-
*
|
|
53
|
-
* Measured live on 2026-08-11 (cognition-engine, run …T015944): the mission was
|
|
54
|
-
* "replace the 6 lateral joins in setup.sql at lines 607, 803, 861, 928, 1047,
|
|
55
|
-
* 1101". setup.sql is 246,583 bytes / 8,025 lines, so the window ended at line
|
|
56
|
-
* 270 — 3.2% of the file, and ALL SIX target lines were unreachable. The model
|
|
57
|
-
* read that same 8 KB 76 times and listed directories 133 more, across 439
|
|
58
|
-
* rounds and 2h55m, before the run was killed. It was not confused; it was
|
|
59
|
-
* hunting for text the tool would never return.
|
|
60
|
-
*
|
|
61
|
-
* So: `offset` to move the window, and a trailer that names the range, the
|
|
62
|
-
* total, and the exact call for the next chunk. The trailer matters more than
|
|
63
|
-
* the parameter — a truncation nobody is told about cannot be worked around.
|
|
64
|
-
*/
|
|
65
45
|
export declare function readFileWindow(contents: string, opts?: {
|
|
66
46
|
offset?: number;
|
|
67
47
|
windowBytes?: number;
|
|
@@ -676,14 +656,81 @@ export declare function readRoundsEnv(name: string, fallback: number): number;
|
|
|
676
656
|
* the operator just tried to disable stayed on at its default. Split rather
|
|
677
657
|
* than loosened, so round thresholds keep rejecting 0.
|
|
678
658
|
*/
|
|
679
|
-
export declare function readCountEnv(name: string, fallback: number): number;
|
|
659
|
+
export declare function readCountEnv(name: string, fallback: number, env?: NodeJS.ProcessEnv): number;
|
|
680
660
|
export declare function newReadCache(): ReadCache;
|
|
661
|
+
/**
|
|
662
|
+
* Content-hash de-duplication for read_file results (Harness-Loop-Graph state
|
|
663
|
+
* hashing). The ReadCache above decides whether a read is a REPEAT; this store
|
|
664
|
+
* decides whether the repeat's RESULT can be collapsed to a compact reference.
|
|
665
|
+
* The hash is computed from the bytes just read off disk — the saving is in
|
|
666
|
+
* the result handed back to the model, never in skipped I/O, so an external
|
|
667
|
+
* modification between reads is caught by the hash itself and always serves
|
|
668
|
+
* fresh content.
|
|
669
|
+
*
|
|
670
|
+
* The reference is served ONLY when this exact window was already served IN
|
|
671
|
+
* THE CURRENT CONVERSATION with an identical hash. v1.148.21 served
|
|
672
|
+
* "UNCHANGED — act on what you have" unconditionally and deadlocked runs (76
|
|
673
|
+
* re-reads, 64 nudges, zero writes in 36 minutes — see
|
|
674
|
+
* test/delivery/dedup-serves-content.test.ts): a model that no longer HAS the
|
|
675
|
+
* content cannot act on a pointer to it. Each turn starts a fresh conversation
|
|
676
|
+
* (runTurn rebuilds `messages`, and messages are never pruned mid-turn — the
|
|
677
|
+
* context budget stop fires first), so the run-level entry survives the turn
|
|
678
|
+
* boundary but `served` does not: the first read of a turn always pays full
|
|
679
|
+
* content, only repeats within that same turn collapse.
|
|
680
|
+
*/
|
|
681
|
+
export interface StateHashEntry {
|
|
682
|
+
sha256: string;
|
|
683
|
+
bytes: number;
|
|
684
|
+
round: number;
|
|
685
|
+
turn: number;
|
|
686
|
+
}
|
|
687
|
+
export interface StateHashStore {
|
|
688
|
+
/** Run-level: `${absPath}@${offsetLine}` → first-seen hash metadata. */
|
|
689
|
+
entries: Map<string, StateHashEntry>;
|
|
690
|
+
/**
|
|
691
|
+
* Conversation-level: windows whose full content the CURRENT turn has
|
|
692
|
+
* already received. Cleared at every turn open — see the block above.
|
|
693
|
+
*/
|
|
694
|
+
served: Set<string>;
|
|
695
|
+
}
|
|
696
|
+
/** Stable prefix so the call site can recognise a reference (and not double-nudge it). */
|
|
697
|
+
export declare const STATE_HASH_REF_PREFIX = "[unchanged:";
|
|
698
|
+
/** Below this many bytes, re-sending the content is cheaper than the reference to it. */
|
|
699
|
+
export declare const STATE_HASH_DEFAULT_MIN_BYTES = 1024;
|
|
700
|
+
export declare function newStateHashStore(): StateHashStore;
|
|
701
|
+
/** On by default; UAP_DELIVER_STATE_HASH=0 disables, same env style as the other deliver rails. */
|
|
702
|
+
export declare function stateHashEnabled(env?: NodeJS.ProcessEnv): boolean;
|
|
703
|
+
export declare function stateHashMinBytes(env?: NodeJS.ProcessEnv): number;
|
|
704
|
+
export declare function isStateHashReference(result: string): boolean;
|
|
705
|
+
/**
|
|
706
|
+
* Record the hash of the bytes just read and, when this window was already
|
|
707
|
+
* served to this conversation with an identical hash, return the compact
|
|
708
|
+
* reference to substitute for the full result. Returns null when full content
|
|
709
|
+
* must be served — first sight of the window, a hash change, below the size
|
|
710
|
+
* floor, or content this conversation has never received.
|
|
711
|
+
*/
|
|
712
|
+
export declare function stateHashReference(store: StateHashStore, abs: string, rel: string, content: string, offsetLine: number, round: number, turn: number): string | null;
|
|
713
|
+
/** Mark a window's full content as served to the current conversation. */
|
|
714
|
+
export declare function markStateHashServed(store: StateHashStore, abs: string, offsetLine: number): void;
|
|
715
|
+
/**
|
|
716
|
+
* A successful write/edit invalidates every recorded window of the path. The
|
|
717
|
+
* store is deliberately NOT refreshed with the new hash here: the model has
|
|
718
|
+
* never been SHOWN the post-write bytes (edit_file's whitespace-tolerant match
|
|
719
|
+
* can land text that differs from what it sent), so the next read must serve
|
|
720
|
+
* full content and re-record — a refreshed hash would serve a compact
|
|
721
|
+
* reference to bytes the model never received.
|
|
722
|
+
*/
|
|
723
|
+
export declare function invalidateStateHash(store: StateHashStore, abs: string): void;
|
|
681
724
|
/**
|
|
682
725
|
* Returns a terse steer when this exact read was already served and nothing has
|
|
683
726
|
* changed since; null when the call should run normally.
|
|
684
727
|
*/
|
|
685
728
|
export declare function repeatReadNote(cache: ReadCache, projectRoot: string, name: string, args: Record<string, unknown>, round: number): string | null;
|
|
686
|
-
export declare function runTool(projectRoot: string, name: string, args: Record<string, unknown>, bashTimeoutMs: number, protectedFiles: ReadonlySet<string>, protectGateConfigs: boolean, allowBash: boolean, contractFiles?: ReadonlySet<string>, sweep?: BashSweep, protectIac?: boolean, writeLedger?: Set<string
|
|
729
|
+
export declare function runTool(projectRoot: string, name: string, args: Record<string, unknown>, bashTimeoutMs: number, protectedFiles: ReadonlySet<string>, protectGateConfigs: boolean, allowBash: boolean, contractFiles?: ReadonlySet<string>, sweep?: BashSweep, protectIac?: boolean, writeLedger?: Set<string>, stateHash?: {
|
|
730
|
+
store: StateHashStore;
|
|
731
|
+
round: number;
|
|
732
|
+
turn: number;
|
|
733
|
+
}): string;
|
|
687
734
|
/**
|
|
688
735
|
* After a Rust-relevant write, run an incremental `cargo check` and return a
|
|
689
736
|
* compact feedback note for the tool result ('' when not applicable). Errors
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agentic-executor.d.ts","sourceRoot":"","sources":["../../src/delivery/agentic-executor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;
|
|
1
|
+
{"version":3,"file":"agentic-executor.d.ts","sourceRoot":"","sources":["../../src/delivery/agentic-executor.ts"],"names":[],"mappings":"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"}
|
|
@@ -23,6 +23,7 @@ import { normalizeToolPath } from './path-normalize.js';
|
|
|
23
23
|
import { generationHeartbeatMs, startGenerationTicker } from './heartbeat.js';
|
|
24
24
|
import { withModelSlot, recordModelSuccess, recordModelExhaustion, isExhaustionError } from '../utils/model-slot-lease.js';
|
|
25
25
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync, rmSync } from 'fs';
|
|
26
|
+
import { createHash } from 'crypto';
|
|
26
27
|
import { join, dirname, resolve, relative, isAbsolute } from 'path';
|
|
27
28
|
import { fetchModelWithRetry, isEndpointUnreachable, ENDPOINT_UNREACHABLE, TRANSIENT_HTTP_STATUSES } from '../models/long-fetch.js';
|
|
28
29
|
import { resolveRequestCredential } from '../models/openai-compat-client.js';
|
|
@@ -87,6 +88,13 @@ const DEFAULT_READ_WINDOW_BYTES = 8_000;
|
|
|
87
88
|
* total, and the exact call for the next chunk. The trailer matters more than
|
|
88
89
|
* the parameter — a truncation nobody is told about cannot be worked around.
|
|
89
90
|
*/
|
|
91
|
+
/**
|
|
92
|
+
* Prefix of the past-EOF note readFileWindow returns when the window starts
|
|
93
|
+
* beyond the last line. The read_file tool handler keys off this exact
|
|
94
|
+
* prefix: a past-EOF read served NO content, so it must not mark the window
|
|
95
|
+
* as served to the state-hash conversation set.
|
|
96
|
+
*/
|
|
97
|
+
const PAST_EOF_NOTE_PREFIX = 'NOTE: offset ';
|
|
90
98
|
export function readFileWindow(contents, opts = {}) {
|
|
91
99
|
const windowBytes = Math.max(1, opts.windowBytes ?? readWindowBytes());
|
|
92
100
|
const lines = contents.split('\n');
|
|
@@ -97,7 +105,7 @@ export function readFileWindow(contents, opts = {}) {
|
|
|
97
105
|
const rawOffset = Number(opts.offset);
|
|
98
106
|
const startLine = Number.isFinite(rawOffset) ? Math.max(1, Math.trunc(rawOffset)) : 1;
|
|
99
107
|
if (startLine > totalLines) {
|
|
100
|
-
return (
|
|
108
|
+
return (`${PAST_EOF_NOTE_PREFIX}${startLine} is past the end of ${opts.path ?? 'the file'} ` +
|
|
101
109
|
`(${totalLines} lines). Read from an earlier line.`);
|
|
102
110
|
}
|
|
103
111
|
const from = lines.slice(startLine - 1);
|
|
@@ -1328,8 +1336,8 @@ export function readRoundsEnv(name, fallback) {
|
|
|
1328
1336
|
* the operator just tried to disable stayed on at its default. Split rather
|
|
1329
1337
|
* than loosened, so round thresholds keep rejecting 0.
|
|
1330
1338
|
*/
|
|
1331
|
-
export function readCountEnv(name, fallback) {
|
|
1332
|
-
const raw =
|
|
1339
|
+
export function readCountEnv(name, fallback, env = process.env) {
|
|
1340
|
+
const raw = env[name];
|
|
1333
1341
|
if (raw === undefined || raw.trim() === '')
|
|
1334
1342
|
return fallback;
|
|
1335
1343
|
const n = Number(raw.trim());
|
|
@@ -1340,6 +1348,78 @@ export function readCountEnv(name, fallback) {
|
|
|
1340
1348
|
export function newReadCache() {
|
|
1341
1349
|
return { seen: new Map(), nextSeq: 1 };
|
|
1342
1350
|
}
|
|
1351
|
+
/** Stable prefix so the call site can recognise a reference (and not double-nudge it). */
|
|
1352
|
+
export const STATE_HASH_REF_PREFIX = '[unchanged:';
|
|
1353
|
+
/** Below this many bytes, re-sending the content is cheaper than the reference to it. */
|
|
1354
|
+
export const STATE_HASH_DEFAULT_MIN_BYTES = 1024;
|
|
1355
|
+
export function newStateHashStore() {
|
|
1356
|
+
return { entries: new Map(), served: new Set() };
|
|
1357
|
+
}
|
|
1358
|
+
/** On by default; UAP_DELIVER_STATE_HASH=0 disables, same env style as the other deliver rails. */
|
|
1359
|
+
export function stateHashEnabled(env = process.env) {
|
|
1360
|
+
return env.UAP_DELIVER_STATE_HASH !== '0';
|
|
1361
|
+
}
|
|
1362
|
+
export function stateHashMinBytes(env = process.env) {
|
|
1363
|
+
return readCountEnv('UAP_DELIVER_STATE_HASH_MIN_BYTES', STATE_HASH_DEFAULT_MIN_BYTES, env);
|
|
1364
|
+
}
|
|
1365
|
+
function stateHashKey(abs, offsetLine) {
|
|
1366
|
+
return `${abs}@${offsetLine}`;
|
|
1367
|
+
}
|
|
1368
|
+
export function isStateHashReference(result) {
|
|
1369
|
+
return result.startsWith(STATE_HASH_REF_PREFIX);
|
|
1370
|
+
}
|
|
1371
|
+
/**
|
|
1372
|
+
* Record the hash of the bytes just read and, when this window was already
|
|
1373
|
+
* served to this conversation with an identical hash, return the compact
|
|
1374
|
+
* reference to substitute for the full result. Returns null when full content
|
|
1375
|
+
* must be served — first sight of the window, a hash change, below the size
|
|
1376
|
+
* floor, or content this conversation has never received.
|
|
1377
|
+
*/
|
|
1378
|
+
export function stateHashReference(store, abs, rel, content, offsetLine, round, turn) {
|
|
1379
|
+
if (!stateHashEnabled())
|
|
1380
|
+
return null;
|
|
1381
|
+
const bytes = Buffer.byteLength(content, 'utf-8');
|
|
1382
|
+
if (bytes < stateHashMinBytes())
|
|
1383
|
+
return null;
|
|
1384
|
+
const key = stateHashKey(abs, offsetLine);
|
|
1385
|
+
const sha256 = createHash('sha256').update(content, 'utf-8').digest('hex');
|
|
1386
|
+
const prior = store.entries.get(key);
|
|
1387
|
+
if (!prior || prior.sha256 !== sha256) {
|
|
1388
|
+
// New window or CHANGED content: refresh the stored hash. Writes also
|
|
1389
|
+
// invalidate eagerly (see invalidateStateHash), but an edit that happened
|
|
1390
|
+
// outside the write tools — run_bash, another process — is caught here,
|
|
1391
|
+
// by the hash itself.
|
|
1392
|
+
store.entries.set(key, { sha256, bytes, round, turn });
|
|
1393
|
+
return null;
|
|
1394
|
+
}
|
|
1395
|
+
if (!store.served.has(key))
|
|
1396
|
+
return null;
|
|
1397
|
+
return (`${STATE_HASH_REF_PREFIX} sha256:${sha256.slice(0, 12)}… ${rel} is unchanged since ` +
|
|
1398
|
+
`turn ${prior.turn} round ${prior.round} (${bytes} bytes) — that content is already in this ` +
|
|
1399
|
+
'conversation; act on it instead of re-reading. For a different region, call read_file with an offset.]');
|
|
1400
|
+
}
|
|
1401
|
+
/** Mark a window's full content as served to the current conversation. */
|
|
1402
|
+
export function markStateHashServed(store, abs, offsetLine) {
|
|
1403
|
+
if (!stateHashEnabled())
|
|
1404
|
+
return;
|
|
1405
|
+
store.served.add(stateHashKey(abs, offsetLine));
|
|
1406
|
+
}
|
|
1407
|
+
/**
|
|
1408
|
+
* A successful write/edit invalidates every recorded window of the path. The
|
|
1409
|
+
* store is deliberately NOT refreshed with the new hash here: the model has
|
|
1410
|
+
* never been SHOWN the post-write bytes (edit_file's whitespace-tolerant match
|
|
1411
|
+
* can land text that differs from what it sent), so the next read must serve
|
|
1412
|
+
* full content and re-record — a refreshed hash would serve a compact
|
|
1413
|
+
* reference to bytes the model never received.
|
|
1414
|
+
*/
|
|
1415
|
+
export function invalidateStateHash(store, abs) {
|
|
1416
|
+
for (const key of [...store.entries.keys()]) {
|
|
1417
|
+
if (key === abs || key.startsWith(`${abs}@`)) {
|
|
1418
|
+
store.entries.delete(key);
|
|
1419
|
+
store.served.delete(key);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1343
1423
|
/** mtime of a path, or null when it does not exist / is unreadable. */
|
|
1344
1424
|
function mtimeOf(p) {
|
|
1345
1425
|
try {
|
|
@@ -1400,7 +1480,10 @@ sweep = disabledSweep(), protectIac = false,
|
|
|
1400
1480
|
// Run-scoped write ledger (see AgenticExecutorOptions.writeLedger). Recorded
|
|
1401
1481
|
// at the actual writeFileSync sites — NOT via the sweep's `authorised` map,
|
|
1402
1482
|
// which is a no-op when bash is disabled, exactly when the ledger matters.
|
|
1403
|
-
writeLedger
|
|
1483
|
+
writeLedger,
|
|
1484
|
+
// Content-hash dedup context (see stateHashReference). Optional — callers
|
|
1485
|
+
// that omit it (tests, one-off dispatches) simply get no result collapsing.
|
|
1486
|
+
stateHash) {
|
|
1404
1487
|
let pathNote = '';
|
|
1405
1488
|
// Contain/repair garbled tool-call paths against the known project root before
|
|
1406
1489
|
// any filesystem op — the small model mangles prefixes/subdirs and the write
|
|
@@ -1443,7 +1526,32 @@ writeLedger) {
|
|
|
1443
1526
|
return (`NOTE: '${String(args.path)}' is a DIRECTORY, not a file — use list_dir for it. ` +
|
|
1444
1527
|
`Its contents:\n${entries}`);
|
|
1445
1528
|
}
|
|
1446
|
-
|
|
1529
|
+
const fullContent = readFileSync(abs, 'utf-8');
|
|
1530
|
+
// Content-hash dedup: the bytes are already read (no I/O saved — that is
|
|
1531
|
+
// what keeps an external edit detectable); what a repeat saves is the
|
|
1532
|
+
// full RESULT. Only a window this conversation has already received,
|
|
1533
|
+
// unchanged, collapses to a reference — see stateHashReference.
|
|
1534
|
+
if (stateHash) {
|
|
1535
|
+
const offsetLine = Number.isFinite(Number(args.offset))
|
|
1536
|
+
? Math.max(1, Math.trunc(Number(args.offset)))
|
|
1537
|
+
: 1;
|
|
1538
|
+
const ref = stateHashReference(stateHash.store, abs, String(args.path), fullContent, offsetLine, stateHash.round, stateHash.turn);
|
|
1539
|
+
if (ref)
|
|
1540
|
+
return ref;
|
|
1541
|
+
const result = readFileWindow(fullContent, {
|
|
1542
|
+
offset: args.offset,
|
|
1543
|
+
path: String(args.path),
|
|
1544
|
+
});
|
|
1545
|
+
// Mark served ONLY when the window actually returned content. A
|
|
1546
|
+
// past-EOF read gets the "offset past the end" note — no bytes — so
|
|
1547
|
+
// recording it would let a repeat serve a reference claiming content
|
|
1548
|
+
// the model never received is "already in this conversation".
|
|
1549
|
+
if (!result.startsWith(PAST_EOF_NOTE_PREFIX)) {
|
|
1550
|
+
markStateHashServed(stateHash.store, abs, offsetLine);
|
|
1551
|
+
}
|
|
1552
|
+
return result;
|
|
1553
|
+
}
|
|
1554
|
+
return readFileWindow(fullContent, {
|
|
1447
1555
|
offset: args.offset,
|
|
1448
1556
|
path: String(args.path),
|
|
1449
1557
|
});
|
|
@@ -1603,6 +1711,9 @@ writeLedger) {
|
|
|
1603
1711
|
writeFileSync(abs, String(args.content ?? ''), 'utf-8');
|
|
1604
1712
|
recordAuthorisedWrite(sweep, rel, String(args.content ?? ''));
|
|
1605
1713
|
writeLedger?.add(rel);
|
|
1714
|
+
// The write succeeded: every recorded read of this path is now stale.
|
|
1715
|
+
if (stateHash)
|
|
1716
|
+
invalidateStateHash(stateHash.store, abs);
|
|
1606
1717
|
// Per-write compile feedback for Rust: without it, a weak model writes
|
|
1607
1718
|
// whole turns of code against types it invented, and the turn-end gate
|
|
1608
1719
|
// reports an avalanche it cannot dig out of (observed live 2026-07-10:
|
|
@@ -1762,6 +1873,9 @@ writeLedger) {
|
|
|
1762
1873
|
writeFileSync(abs, updated, 'utf-8');
|
|
1763
1874
|
recordAuthorisedWrite(sweep, rel, updated);
|
|
1764
1875
|
writeLedger?.add(rel);
|
|
1876
|
+
// The edit succeeded: every recorded read of this path is now stale.
|
|
1877
|
+
if (stateHash)
|
|
1878
|
+
invalidateStateHash(stateHash.store, abs);
|
|
1765
1879
|
const rustCheckNote = maybeRustWriteCheck(projectRoot, rel);
|
|
1766
1880
|
const jsCheckNote = maybeJsSyntaxCheck(projectRoot, rel);
|
|
1767
1881
|
const plural = batch.length === 1 ? 'replacement' : 'replacements';
|
|
@@ -1828,6 +1942,9 @@ writeLedger) {
|
|
|
1828
1942
|
writeFileSync(abs, ranged.text, 'utf-8');
|
|
1829
1943
|
recordAuthorisedWrite(sweep, rel, ranged.text);
|
|
1830
1944
|
writeLedger?.add(rel);
|
|
1945
|
+
// The edit succeeded: every recorded read of this path is now stale.
|
|
1946
|
+
if (stateHash)
|
|
1947
|
+
invalidateStateHash(stateHash.store, abs);
|
|
1831
1948
|
const rustCheckNote = maybeRustWriteCheck(projectRoot, rel);
|
|
1832
1949
|
const jsCheckNote = maybeJsSyntaxCheck(projectRoot, rel);
|
|
1833
1950
|
return (`OK: replaced lines ${Number(args.start_line)}-${Number(args.end_line)} ` +
|
|
@@ -2119,6 +2236,12 @@ export function createAgenticExecutor(model, opts) {
|
|
|
2119
2236
|
const protectIac = opts.protectIac ?? false;
|
|
2120
2237
|
// run_bash executes only when kernel-contained (uap sandbox sets
|
|
2121
2238
|
// UAP_SANDBOX_ACTIVE=1) or the operator explicitly opts in (audit X3).
|
|
2239
|
+
// Run-level content-hash dedup store: hash entries survive turn boundaries
|
|
2240
|
+
// (cross-turn persistence within the run), while the `served` set is cleared
|
|
2241
|
+
// at each turn open — see stateHashReference for why the two have different
|
|
2242
|
+
// lifetimes. Incremented once per turn by the wrapper below.
|
|
2243
|
+
const stateHashStore = newStateHashStore();
|
|
2244
|
+
let stateHashTurn = 0;
|
|
2122
2245
|
const allowBash = opts.allowBash === true ||
|
|
2123
2246
|
process.env.UAP_SANDBOX_ACTIVE === '1' ||
|
|
2124
2247
|
process.env.UAP_DELIVER_ALLOW_BASH === '1';
|
|
@@ -2151,6 +2274,13 @@ export function createAgenticExecutor(model, opts) {
|
|
|
2151
2274
|
const summaries = [];
|
|
2152
2275
|
// Per-session: what this agent has already read, and at what mtime.
|
|
2153
2276
|
const readCache = newReadCache();
|
|
2277
|
+
// A turn is a fresh conversation (messages rebuilt above), so no content
|
|
2278
|
+
// from a previous turn is in context: clear the served set, and the first
|
|
2279
|
+
// read of anything this turn pays full content again. Without this the
|
|
2280
|
+
// compact reference would point at content the model never received —
|
|
2281
|
+
// the v1.148.21 deadlock.
|
|
2282
|
+
stateHashStore.served.clear();
|
|
2283
|
+
const turn = stateHashTurn;
|
|
2154
2284
|
// Read-only-streak nudge: a weak model in a gap-closure/whole-mission epic
|
|
2155
2285
|
// can spend EVERY round on read_file/list_dir and end the turn with zero
|
|
2156
2286
|
// writes — attempt after attempt (run I live, 2026-07-17: two 5-turn
|
|
@@ -2334,7 +2464,10 @@ export function createAgenticExecutor(model, opts) {
|
|
|
2334
2464
|
if (recovered.length > 0) {
|
|
2335
2465
|
const written = [];
|
|
2336
2466
|
for (const block of recovered) {
|
|
2337
|
-
const result = runTool(opts.projectRoot, 'write_file', { path: block.path, content: block.content }, bashTimeoutMs, protectedFiles, protectGateConfigs, allowBash, contractFiles, sweep, protectIac, opts.writeLedger
|
|
2467
|
+
const result = runTool(opts.projectRoot, 'write_file', { path: block.path, content: block.content }, bashTimeoutMs, protectedFiles, protectGateConfigs, allowBash, contractFiles, sweep, protectIac, opts.writeLedger,
|
|
2468
|
+
// Recovered writes mutate files too — the store must hear about
|
|
2469
|
+
// them or the next read could collapse to a stale reference.
|
|
2470
|
+
{ store: stateHashStore, round, turn });
|
|
2338
2471
|
opts.onEvent?.({
|
|
2339
2472
|
round,
|
|
2340
2473
|
kind: 'tool',
|
|
@@ -2475,7 +2608,7 @@ export function createAgenticExecutor(model, opts) {
|
|
|
2475
2608
|
}
|
|
2476
2609
|
// A repeated read gets a NUDGE, never a denial — see repeatReadNote.
|
|
2477
2610
|
const repeat = repeatReadNote(readCache, opts.projectRoot, call.function.name, args, round);
|
|
2478
|
-
const toolResult = runTool(opts.projectRoot, call.function.name, args, bashTimeoutMs, protectedFiles, protectGateConfigs, allowBash, contractFiles, sweep, protectIac, opts.writeLedger);
|
|
2611
|
+
const toolResult = runTool(opts.projectRoot, call.function.name, args, bashTimeoutMs, protectedFiles, protectGateConfigs, allowBash, contractFiles, sweep, protectIac, opts.writeLedger, { store: stateHashStore, round, turn });
|
|
2479
2612
|
// #2a: per-tool-call progress — refresh the deliver heartbeat now, not
|
|
2480
2613
|
// just at turn end, so wedge-detection tracks real intra-turn activity.
|
|
2481
2614
|
opts.onToolProgress?.();
|
|
@@ -2514,7 +2647,14 @@ export function createAgenticExecutor(model, opts) {
|
|
|
2514
2647
|
// to proceed, and it simply asks again. Live: 76 re-reads of one file,
|
|
2515
2648
|
// 64 nudges, ZERO writes in 36 minutes. Serve the content, and prepend
|
|
2516
2649
|
// the nudge so repetition still costs it nothing but a line.
|
|
2517
|
-
|
|
2650
|
+
// The state-hash reference is the ONE safe exception: it is only ever
|
|
2651
|
+
// returned for content this very conversation has already received in
|
|
2652
|
+
// full (see stateHashReference), and it already carries the steer — so
|
|
2653
|
+
// the repeat note, which promises "content follows anyway", must NOT
|
|
2654
|
+
// be prepended over it.
|
|
2655
|
+
const result = repeat && !isStateHashReference(toolResult)
|
|
2656
|
+
? `${repeat}\n\n${toolResult}`
|
|
2657
|
+
: toolResult;
|
|
2518
2658
|
opts.onEvent?.({
|
|
2519
2659
|
round,
|
|
2520
2660
|
kind: 'tool',
|
|
@@ -2542,6 +2682,10 @@ export function createAgenticExecutor(model, opts) {
|
|
|
2542
2682
|
* must not be left holding it.
|
|
2543
2683
|
*/
|
|
2544
2684
|
return async (prompt) => {
|
|
2685
|
+
// Turn counter for the state-hash reference labels ("unchanged since turn
|
|
2686
|
+
// N"). Incremented here — the single per-turn entry point — not in
|
|
2687
|
+
// runTurn, which has early-return paths a counter inside it would miss.
|
|
2688
|
+
stateHashTurn += 1;
|
|
2545
2689
|
const sweep = beginBashSweep(opts.projectRoot, allowBash);
|
|
2546
2690
|
// Fold the sweep's observed changes into the run write ledger: tool writes
|
|
2547
2691
|
// are recorded at their writeFileSync sites, but shell writes only become
|