@animalabs/context-manager 0.5.4 → 0.5.9
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/src/adaptive/kv-control.d.ts +84 -33
- package/dist/src/adaptive/kv-control.d.ts.map +1 -1
- package/dist/src/adaptive/kv-control.js +510 -145
- package/dist/src/adaptive/kv-control.js.map +1 -1
- package/dist/src/adaptive/picker.d.ts +21 -0
- package/dist/src/adaptive/picker.d.ts.map +1 -1
- package/dist/src/adaptive/picker.js.map +1 -1
- package/dist/src/adaptive/strategies/kv-stable.d.ts +12 -4
- package/dist/src/adaptive/strategies/kv-stable.d.ts.map +1 -1
- package/dist/src/adaptive/strategies/kv-stable.js +67 -7
- package/dist/src/adaptive/strategies/kv-stable.js.map +1 -1
- package/dist/src/context-log.d.ts.map +1 -1
- package/dist/src/context-log.js +11 -0
- package/dist/src/context-log.js.map +1 -1
- package/dist/src/context-manager.d.ts +38 -6
- package/dist/src/context-manager.d.ts.map +1 -1
- package/dist/src/context-manager.js +51 -0
- package/dist/src/context-manager.js.map +1 -1
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/message-store.d.ts +60 -0
- package/dist/src/message-store.d.ts.map +1 -1
- package/dist/src/message-store.js +165 -10
- package/dist/src/message-store.js.map +1 -1
- package/dist/src/phase-channel.d.ts +13 -0
- package/dist/src/phase-channel.d.ts.map +1 -0
- package/dist/src/phase-channel.js +55 -0
- package/dist/src/phase-channel.js.map +1 -0
- package/dist/src/strategies/autobiographical.d.ts +247 -8
- package/dist/src/strategies/autobiographical.d.ts.map +1 -1
- package/dist/src/strategies/autobiographical.js +1419 -141
- package/dist/src/strategies/autobiographical.js.map +1 -1
- package/dist/src/strategies/index.d.ts +1 -1
- package/dist/src/strategies/index.d.ts.map +1 -1
- package/dist/src/strategies/index.js.map +1 -1
- package/dist/src/strategies/knowledge.d.ts +8 -0
- package/dist/src/strategies/knowledge.d.ts.map +1 -1
- package/dist/src/strategies/knowledge.js +23 -1
- package/dist/src/strategies/knowledge.js.map +1 -1
- package/dist/src/types/index.d.ts +1 -1
- package/dist/src/types/index.d.ts.map +1 -1
- package/dist/src/types/index.js.map +1 -1
- package/dist/src/types/strategy.d.ts +112 -11
- package/dist/src/types/strategy.d.ts.map +1 -1
- package/dist/src/types/strategy.js.map +1 -1
- package/dist/test/adaptive/doc-plus-chat.test.js +7 -3
- package/dist/test/adaptive/doc-plus-chat.test.js.map +1 -1
- package/dist/test/adaptive/hard-fail.test.js +22 -0
- package/dist/test/adaptive/hard-fail.test.js.map +1 -1
- package/dist/test/adaptive/ingestion.test.js +3 -1
- package/dist/test/adaptive/ingestion.test.js.map +1 -1
- package/dist/test/adaptive/kv-control.test.js +17 -11
- package/dist/test/adaptive/kv-control.test.js.map +1 -1
- package/dist/test/adaptive/pin-at-level.test.d.ts +16 -0
- package/dist/test/adaptive/pin-at-level.test.d.ts.map +1 -0
- package/dist/test/adaptive/pin-at-level.test.js +133 -0
- package/dist/test/adaptive/pin-at-level.test.js.map +1 -0
- package/dist/test/adaptive/single-path-solve.test.d.ts +16 -0
- package/dist/test/adaptive/single-path-solve.test.d.ts.map +1 -0
- package/dist/test/adaptive/single-path-solve.test.js +204 -0
- package/dist/test/adaptive/single-path-solve.test.js.map +1 -0
- package/dist/test/anti-redundancy-budget.test.d.ts +17 -0
- package/dist/test/anti-redundancy-budget.test.d.ts.map +1 -0
- package/dist/test/anti-redundancy-budget.test.js +197 -0
- package/dist/test/anti-redundancy-budget.test.js.map +1 -0
- package/dist/test/chunk-persistence.test.d.ts +29 -0
- package/dist/test/chunk-persistence.test.d.ts.map +1 -0
- package/dist/test/chunk-persistence.test.js +290 -0
- package/dist/test/chunk-persistence.test.js.map +1 -0
- package/dist/test/compression-dedup.test.d.ts +20 -0
- package/dist/test/compression-dedup.test.d.ts.map +1 -0
- package/dist/test/compression-dedup.test.js +92 -0
- package/dist/test/compression-dedup.test.js.map +1 -0
- package/dist/test/compression-shape-invariants.test.js +40 -3
- package/dist/test/compression-shape-invariants.test.js.map +1 -1
- package/dist/test/head-window-order.test.d.ts +19 -0
- package/dist/test/head-window-order.test.d.ts.map +1 -0
- package/dist/test/head-window-order.test.js +159 -0
- package/dist/test/head-window-order.test.js.map +1 -0
- package/dist/test/image-strip-render-stats.test.js +25 -3
- package/dist/test/image-strip-render-stats.test.js.map +1 -1
- package/dist/test/knowledge-tool-boundary.test.d.ts +14 -0
- package/dist/test/knowledge-tool-boundary.test.d.ts.map +1 -0
- package/dist/test/knowledge-tool-boundary.test.js +93 -0
- package/dist/test/knowledge-tool-boundary.test.js.map +1 -0
- package/dist/test/l1-holdback.test.d.ts +15 -0
- package/dist/test/l1-holdback.test.d.ts.map +1 -0
- package/dist/test/l1-holdback.test.js +119 -0
- package/dist/test/l1-holdback.test.js.map +1 -0
- package/dist/test/merge-contiguity.test.d.ts +11 -0
- package/dist/test/merge-contiguity.test.d.ts.map +1 -0
- package/dist/test/merge-contiguity.test.js +74 -0
- package/dist/test/merge-contiguity.test.js.map +1 -0
- package/dist/test/message-store-window.test.d.ts +13 -0
- package/dist/test/message-store-window.test.d.ts.map +1 -0
- package/dist/test/message-store-window.test.js +177 -0
- package/dist/test/message-store-window.test.js.map +1 -0
- package/dist/test/pins.test.js +40 -0
- package/dist/test/pins.test.js.map +1 -1
- package/dist/test/speculation-cap.test.d.ts +9 -3
- package/dist/test/speculation-cap.test.d.ts.map +1 -1
- package/dist/test/speculation-cap.test.js +179 -3
- package/dist/test/speculation-cap.test.js.map +1 -1
- package/dist/test/summary-log-consistency.test.d.ts +18 -0
- package/dist/test/summary-log-consistency.test.d.ts.map +1 -0
- package/dist/test/summary-log-consistency.test.js +97 -0
- package/dist/test/summary-log-consistency.test.js.map +1 -0
- package/dist/test/tool-pairing-invariant.test.d.ts +15 -0
- package/dist/test/tool-pairing-invariant.test.d.ts.map +1 -0
- package/dist/test/tool-pairing-invariant.test.js +225 -0
- package/dist/test/tool-pairing-invariant.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +11 -3
- package/src/adaptive/kv-control.ts +554 -152
- package/src/adaptive/picker.ts +21 -0
- package/src/adaptive/strategies/kv-stable.ts +62 -12
- package/src/context-log.ts +11 -0
- package/src/context-manager.ts +61 -4
- package/src/index.ts +6 -2
- package/src/message-store.ts +202 -9
- package/src/phase-channel.ts +57 -0
- package/src/strategies/autobiographical.ts +1482 -167
- package/src/strategies/index.ts +1 -1
- package/src/strategies/knowledge.ts +25 -1
- package/src/types/index.ts +1 -0
- package/src/types/strategy.ts +120 -7
|
@@ -1,22 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* KV-stable context controller
|
|
2
|
+
* KV-stable context controller — the rev 5.0 single-path solve
|
|
3
|
+
* (see `docs/adaptive-resolution-design.md` §13; supersedes the two-path
|
|
4
|
+
* fold-within-reach + emergency design of §12, and the older prose in
|
|
5
|
+
* `docs/kv-stable-context-control.md` where they conflict).
|
|
3
6
|
*
|
|
4
|
-
*
|
|
5
|
-
* objective — minimize real billed recompute — under a constraint hierarchy with
|
|
6
|
-
* a single hard wall (the physical window W) and shaped soft constraints
|
|
7
|
-
* (flat zone, perturbation cap, pins/saliency, operating budget). State (the
|
|
8
|
-
* carried frontier + the persistent cache) makes it a controller, not a
|
|
9
|
-
* per-turn solve.
|
|
7
|
+
* One algorithm per turn, one hard constraint:
|
|
10
8
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
9
|
+
* minimize relevance_loss(F) + perturbation(F, F_prev) [lexicographic]
|
|
10
|
+
* subject to render(F) ≤ W — the only physics
|
|
11
|
+
* perturbation(F, F_prev) ≤ P — trust region; overridable
|
|
12
|
+
*
|
|
13
|
+
* - The IDEAL CUT is built from scratch each turn by pure relevance: fold in
|
|
14
|
+
* salience-then-age priority order, level by level, under the soft shape
|
|
15
|
+
* prior (`foldDepthCap`), past it if W demands, never past the hard
|
|
16
|
+
* protections (flat zone / pins / locked); then pack youngest-first back
|
|
17
|
+
* toward the target. Age/salience-monotone by construction.
|
|
18
|
+
* - PERTURBATION is exact, not modeled: `kvCost` — the tokens the provider
|
|
19
|
+
* will re-read from the earliest layout divergence to the end.
|
|
20
|
+
* - P (`reachTokens`, re-priced) is a TRUST REGION on per-turn divergence,
|
|
21
|
+
* not a spatial eligibility gate. Within it the solver adopts the ideal;
|
|
22
|
+
* beyond it it adopts the ideal's newest changes only (suffix adoption —
|
|
23
|
+
* perturbation is prefix-based, so partial adoption is exactly a suffix
|
|
24
|
+
* cut) and converges over turns, receding-horizon style.
|
|
25
|
+
* - OVERRIDES (same code path, recorded on the plan, logged by the caller):
|
|
26
|
+
* 'bootstrap' (no carried frontier — nothing to preserve), 'infeasible'
|
|
27
|
+
* (nothing within P fits under W), 'quality-gap' (the best plan within P
|
|
28
|
+
* is certifiably bad vs the ideal). There is no emergency path.
|
|
20
29
|
*
|
|
21
30
|
* Pure and deterministic.
|
|
22
31
|
*/
|
|
@@ -25,14 +34,17 @@ import type { ChunkId } from './folding-strategy.js';
|
|
|
25
34
|
import type { PickerInputs, PickerChunk } from './picker.js';
|
|
26
35
|
import type { SummaryEntry } from '../types/strategy.js';
|
|
27
36
|
import { SummaryTree } from './summary-tree.js';
|
|
28
|
-
import { renderLayout, type RenderLayout, type Frontier } from './render-offsets.js';
|
|
37
|
+
import { renderLayout, kvCost, type RenderLayout, type Frontier } from './render-offsets.js';
|
|
29
38
|
import { placeMarkers, CacheStore } from './kv-cache-sim.js';
|
|
30
39
|
|
|
31
40
|
/** Provider price multipliers relative to base input tokens (Anthropic-ish). */
|
|
32
41
|
export const PRICE = { miss: 1.0, cacheRead: 0.1, cacheWrite: 1.25 } as const;
|
|
33
42
|
|
|
34
|
-
/**
|
|
35
|
-
|
|
43
|
+
/** Safety ceiling on saliency fold depth. The *effective* cap is
|
|
44
|
+
* `maxAvailableLevel(tree)` (fold only as deep as summaries actually exist),
|
|
45
|
+
* which every caller passes in; this constant is just a high sane upper bound so
|
|
46
|
+
* L4/L5/… are allowed as the merge pipeline builds them — never a pin at L3. */
|
|
47
|
+
export const MAX_FOLD_LEVEL = 8;
|
|
36
48
|
|
|
37
49
|
export interface ControlOptions {
|
|
38
50
|
/** Physical context window — the ONLY hard wall. */
|
|
@@ -107,143 +119,379 @@ export function foldDepthCap(
|
|
|
107
119
|
flatZone: ReadonlySet<ChunkId>,
|
|
108
120
|
flatZoneChunks: number,
|
|
109
121
|
mergeThreshold: number,
|
|
122
|
+
maxLevel: number = MAX_FOLD_LEVEL,
|
|
110
123
|
): number {
|
|
111
124
|
if (chunk.pinned || flatZone.has(chunk.id)) return 0;
|
|
112
125
|
const age = Math.max(0, now - chunk.sequence);
|
|
113
126
|
const ratio = age / Math.max(1, flatZoneChunks);
|
|
114
127
|
if (ratio < 1) return 0; // just outside the flat zone → still raw
|
|
115
128
|
const band = Math.floor(Math.log(ratio) / Math.log(Math.max(2, mergeThreshold))) + 1;
|
|
116
|
-
return Math.max(0, Math.min(
|
|
129
|
+
return Math.max(0, Math.min(maxLevel, band));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Deepest summary level that actually exists in the tree (0 if none). The
|
|
133
|
+
* fold depth is bounded by this — NOT a hardcoded constant — so the controller
|
|
134
|
+
* uses L4/L5… summaries when production has built them (else it floors at L3 and
|
|
135
|
+
* refuses deeper summaries that exist; cf. the lab L3-floor-with-L4-present). */
|
|
136
|
+
function maxAvailableLevel(tree: SummaryTree): number {
|
|
137
|
+
let m = 0;
|
|
138
|
+
for (const s of tree.allSummaries()) if (s.level > m) m = s.level;
|
|
139
|
+
return m;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Per-chunk salience: the coefficient on information loss (design §13.3).
|
|
143
|
+
* Defaults to 1. Lower = folds earlier and costs less when folded (code-heavy /
|
|
144
|
+
* tool-output / externalized content). Never overrides hard protections. */
|
|
145
|
+
function salienceOf(c: PickerChunk): number {
|
|
146
|
+
const s = c.salience;
|
|
147
|
+
if (s === undefined || !Number.isFinite(s)) return 1;
|
|
148
|
+
return Math.min(1, Math.max(0, s));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Salience-weighted MISALLOCATION loss of a frontier: Σ salience(c) ·
|
|
153
|
+
* max(0, level(c) − shapeCap(c)) over foldable chunks — the excess fold depth
|
|
154
|
+
* beyond what the relevance shape prior (log-age band) would assign. Zero for
|
|
155
|
+
* any cut that folds nothing deeper than its prior; positive exactly where
|
|
156
|
+
* fidelity is spent in the wrong place. Recency-aware by construction (the
|
|
157
|
+
* cap grows with age), so an INVERTED profile — recent content deep, ancient
|
|
158
|
+
* content fine — scores high even when a naive Σ level would tie it with the
|
|
159
|
+
* correct gradient. Used only to compare cuts of the same tree (quality-gap
|
|
160
|
+
* tests, §13.4).
|
|
161
|
+
*/
|
|
162
|
+
function relevanceLoss(
|
|
163
|
+
F: ReadonlyMap<ChunkId, number>,
|
|
164
|
+
ordered: PickerChunk[],
|
|
165
|
+
rawZone: ReadonlySet<ChunkId>,
|
|
166
|
+
caps: ReadonlyMap<ChunkId, number>,
|
|
167
|
+
): number {
|
|
168
|
+
let loss = 0;
|
|
169
|
+
for (const c of ordered) {
|
|
170
|
+
if (rawZone.has(c.id)) continue;
|
|
171
|
+
const lvl = F.get(c.id) ?? 0;
|
|
172
|
+
if (lvl <= 0) continue;
|
|
173
|
+
const cap = Math.max(0, caps.get(c.id) ?? 0);
|
|
174
|
+
if (lvl > cap) loss += salienceOf(c) * (lvl - cap);
|
|
175
|
+
}
|
|
176
|
+
return loss;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Incremental token accounting for whole-group fold/unfold moves.
|
|
181
|
+
*
|
|
182
|
+
* `renderLayout` is O(n); calling it per accepted fold makes the ideal-cut
|
|
183
|
+
* construction O(n·folds) ≈ O(n²) — seconds on a 4k-chunk store, paid on
|
|
184
|
+
* EVERY compile under the single-path solve. The ledger tracks rendered units
|
|
185
|
+
* (raw shards / recall pairs) as a multiset keyed exactly like renderLayout's
|
|
186
|
+
* units, so a group fold/unfold is O(group) and `tokens` stays equal to
|
|
187
|
+
* `renderLayout(...).totalTokens` for the same frontier (both share the
|
|
188
|
+
* "missing ancestor → raw" fallback; siblings share one recall unit).
|
|
189
|
+
*/
|
|
190
|
+
class TokenLedger {
|
|
191
|
+
private readonly unitTokens = new Map<string, number>(); // unitKey → tokens
|
|
192
|
+
private readonly unitRefs = new Map<string, number>(); // unitKey → leaf count
|
|
193
|
+
tokens = 0;
|
|
194
|
+
|
|
195
|
+
constructor(
|
|
196
|
+
private readonly tree: SummaryTree,
|
|
197
|
+
private readonly inputs: PickerInputs,
|
|
198
|
+
ordered: PickerChunk[],
|
|
199
|
+
F: ReadonlyMap<ChunkId, number>,
|
|
200
|
+
) {
|
|
201
|
+
this.tokens = inputs.headTokens + inputs.tailTokens;
|
|
202
|
+
for (const c of ordered) {
|
|
203
|
+
if (inputs.headChunkIds.has(c.id) || inputs.tailChunkIds.has(c.id)) continue;
|
|
204
|
+
const { key, tokens } = this.unitFor(c, F.get(c.id) ?? 0);
|
|
205
|
+
this.addRef(key, tokens);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Unit identity + cost for a leaf at a level — mirrors renderLayout. */
|
|
210
|
+
private unitFor(c: PickerChunk, level: number): { key: string; tokens: number } {
|
|
211
|
+
const effective = c.pinned ? 0 : level;
|
|
212
|
+
if (effective > 0) {
|
|
213
|
+
const ancestor = this.tree.ancestorAt(c.id, effective);
|
|
214
|
+
if (ancestor) return { key: `recall:${ancestor.id}`, tokens: ancestor.recallTokens };
|
|
215
|
+
}
|
|
216
|
+
return { key: `raw:${c.id}`, tokens: c.rawTokens };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private addRef(key: string, tokens: number): void {
|
|
220
|
+
const refs = this.unitRefs.get(key) ?? 0;
|
|
221
|
+
if (refs === 0) {
|
|
222
|
+
this.unitTokens.set(key, tokens);
|
|
223
|
+
this.tokens += tokens;
|
|
224
|
+
}
|
|
225
|
+
this.unitRefs.set(key, refs + 1);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private dropRef(key: string): void {
|
|
229
|
+
const refs = this.unitRefs.get(key) ?? 0;
|
|
230
|
+
if (refs <= 1) {
|
|
231
|
+
this.unitRefs.delete(key);
|
|
232
|
+
this.tokens -= this.unitTokens.get(key) ?? 0;
|
|
233
|
+
this.unitTokens.delete(key);
|
|
234
|
+
} else {
|
|
235
|
+
this.unitRefs.set(key, refs - 1);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Move one leaf from `fromLevel` to `toLevel`. Callers move whole groups
|
|
240
|
+
* (every leaf of the target node), which keeps units exact. */
|
|
241
|
+
move(c: PickerChunk, byId: ReadonlyMap<ChunkId, PickerChunk>, fromLevel: number, toLevel: number): void {
|
|
242
|
+
if (this.inputs.headChunkIds.has(c.id) || this.inputs.tailChunkIds.has(c.id)) return;
|
|
243
|
+
void byId;
|
|
244
|
+
this.dropRef(this.unitFor(c, fromLevel).key);
|
|
245
|
+
const next = this.unitFor(c, toLevel);
|
|
246
|
+
this.addRef(next.key, next.tokens);
|
|
247
|
+
}
|
|
117
248
|
}
|
|
118
249
|
|
|
119
250
|
/**
|
|
120
|
-
*
|
|
121
|
-
* (L1 groups, then L2, then L3) and oldest-first *within the reach window*. A
|
|
122
|
-
* chunk is editable this turn only if the raw tokens newer than it are <
|
|
123
|
-
* `reachTokens` — this bounds the divergence depth (and thus the per-turn KV
|
|
124
|
-
* perturbation) to ≈ reach. Each chunk is bounded by its saliency cap;
|
|
125
|
-
* flat-zone/pinned chunks (cap 0) are never touched.
|
|
251
|
+
* Build the IDEAL CUT from scratch — pure relevance, no P anywhere (§13.4).
|
|
126
252
|
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
253
|
+
* Fold order is (salience ascending, then oldest-first), level by level, so the
|
|
254
|
+
* profile is salience/age-monotone by construction: with uniform salience it
|
|
255
|
+
* reduces to the classic oldest-first shed. Two phases against the shape prior:
|
|
256
|
+
* A. fold under the soft `caps` (log-age shape + pin caps) until ≤ target;
|
|
257
|
+
* B. if still over W, keep folding past the shape caps (never past the
|
|
258
|
+
* hard-protected −1 sentinel or a pin cap) — the prior is a relevance
|
|
259
|
+
* shaper, never a feasibility wall;
|
|
260
|
+
* C. pack: un-fold youngest-first back toward `target` (accept-if-fit), so
|
|
261
|
+
* headroom is spent on the most recent foldable content.
|
|
262
|
+
* Ends projected to a valid group-consistent tree cut.
|
|
130
263
|
*/
|
|
131
|
-
function
|
|
132
|
-
frontier: Map<ChunkId, number>,
|
|
264
|
+
function relevanceCut(
|
|
133
265
|
inputs: PickerInputs,
|
|
134
266
|
tree: SummaryTree,
|
|
135
267
|
ordered: PickerChunk[],
|
|
136
|
-
|
|
268
|
+
base: ReadonlyMap<ChunkId, number>,
|
|
269
|
+
caps: ReadonlyMap<ChunkId, number>,
|
|
270
|
+
pinCaps: ReadonlyMap<ChunkId, number>,
|
|
271
|
+
rawZone: ReadonlySet<ChunkId>,
|
|
272
|
+
immovable: ReadonlySet<ChunkId>,
|
|
273
|
+
frozen: ReadonlySet<ChunkId>,
|
|
274
|
+
fixedLevels: ReadonlyMap<ChunkId, number>,
|
|
137
275
|
target: number,
|
|
138
|
-
reachTokens: number,
|
|
139
276
|
windowTokens: number,
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
277
|
+
maxFoldLevel: number,
|
|
278
|
+
): { F: Map<ChunkId, number>; tokens: number } {
|
|
279
|
+
const F = new Map(base);
|
|
280
|
+
const byId = new Map(ordered.map((c) => [c.id, c] as const));
|
|
281
|
+
// Fold priority: cheapest information first — ascending salience, then oldest.
|
|
282
|
+
const priority = [...ordered].sort((a, b) => {
|
|
283
|
+
const sa = salienceOf(a);
|
|
284
|
+
const sb = salienceOf(b);
|
|
285
|
+
if (sa !== sb) return sa - sb;
|
|
286
|
+
return a.sequence - b.sequence;
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Incremental accounting: O(group) per move instead of O(n) renderLayout.
|
|
290
|
+
const _tl = typeof process !== 'undefined' && process.env?.KV_TIMING ? Date.now() : 0;
|
|
291
|
+
const ledger = new TokenLedger(tree, inputs, ordered, F);
|
|
292
|
+
if (_tl) console.error(`[kv-timing] ledger-init ${Date.now() - _tl}ms tokens=${ledger.tokens}`);
|
|
293
|
+
|
|
294
|
+
const foldPass = (ignoreShapeCaps: boolean, stopAt: number): boolean => {
|
|
295
|
+
for (let level = 1; level <= maxFoldLevel; level++) {
|
|
296
|
+
for (const c of priority) {
|
|
297
|
+
if (ledger.tokens <= stopAt) return true;
|
|
298
|
+
if ((F.get(c.id) ?? 0) >= level) continue;
|
|
155
299
|
const ancestor = tree.ancestorAt(c.id, level);
|
|
156
300
|
if (!ancestor) continue; // summary not produced yet → can't fold here
|
|
157
|
-
// Group-
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
// group-consistent and the picker convergent.
|
|
301
|
+
// Group-atomicity: fold the WHOLE group to `level` only if every
|
|
302
|
+
// covered leaf permits it. Hard protections (−1 sentinel: flat zone /
|
|
303
|
+
// pins / locked) always block; pin-max-level always blocks past its
|
|
304
|
+
// bound; the soft shape cap blocks only in phase A.
|
|
162
305
|
let eligible = true;
|
|
163
306
|
for (const leafId of ancestor.leafChunkIds) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
307
|
+
const cap = caps.get(leafId) ?? 0;
|
|
308
|
+
const pinCap = pinCaps.get(leafId);
|
|
309
|
+
if (pinCap !== undefined && level > pinCap) { eligible = false; break; }
|
|
310
|
+
if (ignoreShapeCaps ? cap < 0 : cap < level) { eligible = false; break; }
|
|
168
311
|
}
|
|
169
312
|
if (!eligible) continue;
|
|
170
313
|
for (const leafId of ancestor.leafChunkIds) {
|
|
171
|
-
|
|
314
|
+
const leaf = byId.get(leafId);
|
|
315
|
+
const from = F.get(leafId) ?? 0;
|
|
316
|
+
if (from < level) {
|
|
317
|
+
if (leaf) ledger.move(leaf, byId, from, level);
|
|
318
|
+
F.set(leafId, level);
|
|
319
|
+
}
|
|
172
320
|
}
|
|
173
|
-
if (
|
|
321
|
+
if (ledger.tokens <= stopAt) return true;
|
|
174
322
|
}
|
|
175
323
|
}
|
|
176
324
|
return false;
|
|
177
325
|
};
|
|
178
326
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
//
|
|
182
|
-
if (tokens > windowTokens)
|
|
183
|
-
|
|
184
|
-
|
|
327
|
+
// Phase A: fold under the shape prior toward the target.
|
|
328
|
+
if (ledger.tokens > target) foldPass(false, target);
|
|
329
|
+
// Phase B: the prior yields to W (only hard protections stand).
|
|
330
|
+
if (ledger.tokens > windowTokens) foldPass(true, target);
|
|
331
|
+
|
|
332
|
+
// Phase C: pack youngest-first back toward the target (skip if infeasible).
|
|
333
|
+
if (ledger.tokens <= windowTokens && ledger.tokens < target) {
|
|
334
|
+
for (let level = maxFoldLevel; level >= 1 && ledger.tokens < target; level--) {
|
|
335
|
+
for (let oi = ordered.length - 1; oi >= 0; oi--) {
|
|
336
|
+
if (ledger.tokens >= target) break;
|
|
337
|
+
const c = ordered[oi]; // youngest-first
|
|
338
|
+
if ((F.get(c.id) ?? 0) !== level) continue;
|
|
339
|
+
if (rawZone.has(c.id) || immovable.has(c.id)) continue;
|
|
340
|
+
const node = tree.ancestorAt(c.id, level);
|
|
341
|
+
if (!node) continue;
|
|
342
|
+
let eligible = true;
|
|
343
|
+
for (const leafId of node.leafChunkIds) {
|
|
344
|
+
if ((F.get(leafId) ?? 0) !== level || rawZone.has(leafId) || immovable.has(leafId)) {
|
|
345
|
+
eligible = false;
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (!eligible) continue;
|
|
350
|
+
const before = ledger.tokens;
|
|
351
|
+
for (const leafId of node.leafChunkIds) {
|
|
352
|
+
const leaf = byId.get(leafId);
|
|
353
|
+
if (leaf) ledger.move(leaf, byId, level, level - 1);
|
|
354
|
+
F.set(leafId, level - 1);
|
|
355
|
+
}
|
|
356
|
+
// Accept an un-fold that lands PAST the target when it still gets
|
|
357
|
+
// CLOSER to it (and stays under W). Merge groups are coarse quanta
|
|
358
|
+
// (8-15k on production trees); accept-only-if-under-target strands
|
|
359
|
+
// real headroom un-spent whenever every remaining quantum overshoots
|
|
360
|
+
// (mythos 2026-07-12: 134k rendered of a 183.6k hard budget). The
|
|
361
|
+
// target stays the attractor — W stays the only wall (§13.2).
|
|
362
|
+
const nt = ledger.tokens;
|
|
363
|
+
const accept = nt <= windowTokens && Math.abs(nt - target) < Math.abs(before - target);
|
|
364
|
+
if (!accept) {
|
|
365
|
+
// moves away from the target (or breaches W) → revert
|
|
366
|
+
for (const leafId of node.leafChunkIds) {
|
|
367
|
+
const leaf = byId.get(leafId);
|
|
368
|
+
if (leaf) ledger.move(leaf, byId, level - 1, level);
|
|
369
|
+
F.set(leafId, level);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
185
374
|
}
|
|
186
|
-
|
|
375
|
+
|
|
376
|
+
const _tp = typeof process !== 'undefined' && process.env?.KV_TIMING ? Date.now() : 0;
|
|
377
|
+
projectToValidCut(F, tree, ordered, frozen, fixedLevels);
|
|
378
|
+
if (_tp) console.error(`[kv-timing] cut-projection ${Date.now() - _tp}ms`);
|
|
379
|
+
// One authoritative render at the end (the ledger tracks it exactly, but the
|
|
380
|
+
// projection may have moved leaves — recompute once, not per move).
|
|
381
|
+
const tokens = renderLayout(inputs, tree, F).totalTokens;
|
|
382
|
+
return { F, tokens };
|
|
187
383
|
}
|
|
188
384
|
|
|
189
385
|
/**
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
* (highest value, shallowest divergence). Stops before exceeding `target`.
|
|
197
|
-
* rawZone/frozen chunks are never touched. Returns final tokens.
|
|
386
|
+
* Partial adoption of the ideal under the trust region (§13.4): keep `prev`
|
|
387
|
+
* before a boundary sequence, take `ideal` at and after it. Perturbation is
|
|
388
|
+
* prefix-based, so the cheapest partial plans are exactly the suffix cuts;
|
|
389
|
+
* binary-search the oldest boundary whose perturbation stays ≤ P (perturbation
|
|
390
|
+
* is monotone non-increasing as the boundary moves newer). Each candidate is
|
|
391
|
+
* projected to a valid cut before costing (mixing two cuts can bisect a group).
|
|
198
392
|
*/
|
|
199
|
-
function
|
|
200
|
-
frontier: Map<ChunkId, number>,
|
|
393
|
+
function suffixAdopt(
|
|
201
394
|
inputs: PickerInputs,
|
|
202
395
|
tree: SummaryTree,
|
|
203
396
|
ordered: PickerChunk[],
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
397
|
+
prev: ReadonlyMap<ChunkId, number>,
|
|
398
|
+
prevLayout: RenderLayout,
|
|
399
|
+
ideal: ReadonlyMap<ChunkId, number>,
|
|
207
400
|
frozen: ReadonlySet<ChunkId>,
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
401
|
+
fixedLevels: ReadonlyMap<ChunkId, number>,
|
|
402
|
+
P: number,
|
|
403
|
+
): { F: Map<ChunkId, number>; tokens: number; perturbation: number } {
|
|
404
|
+
const changedSeqs: number[] = [];
|
|
405
|
+
for (const c of ordered) {
|
|
406
|
+
if ((prev.get(c.id) ?? 0) !== (ideal.get(c.id) ?? 0)) changedSeqs.push(c.sequence);
|
|
214
407
|
}
|
|
215
408
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
for (
|
|
219
|
-
if (
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
409
|
+
const build = (boundary: number): { F: Map<ChunkId, number>; layout: RenderLayout; pert: number } => {
|
|
410
|
+
const F = new Map(prev);
|
|
411
|
+
for (const c of ordered) {
|
|
412
|
+
if (c.sequence >= boundary) F.set(c.id, ideal.get(c.id) ?? 0);
|
|
413
|
+
}
|
|
414
|
+
projectToValidCut(F, tree, ordered, frozen, fixedLevels);
|
|
415
|
+
const layout = renderLayout(inputs, tree, F);
|
|
416
|
+
return { F, layout, pert: kvCost(prevLayout, layout) };
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
if (changedSeqs.length === 0) {
|
|
420
|
+
const layout = renderLayout(inputs, tree, new Map(prev));
|
|
421
|
+
return { F: new Map(prev), tokens: layout.totalTokens, perturbation: 0 };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Binary search: smallest index (oldest boundary) with perturbation ≤ P.
|
|
425
|
+
let lo = 0;
|
|
426
|
+
let hi = changedSeqs.length; // hi = adopt nothing (pert 0, always feasible)
|
|
427
|
+
let best = build(Number.MAX_SAFE_INTEGER);
|
|
428
|
+
let bestIdx = hi;
|
|
429
|
+
while (lo < hi) {
|
|
430
|
+
const mid = (lo + hi) >> 1;
|
|
431
|
+
const cand = build(changedSeqs[mid]);
|
|
432
|
+
if (cand.pert <= P) {
|
|
433
|
+
best = cand;
|
|
434
|
+
bestIdx = mid;
|
|
435
|
+
hi = mid;
|
|
436
|
+
} else {
|
|
437
|
+
lo = mid + 1;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
void bestIdx;
|
|
441
|
+
return { F: best.F, tokens: best.layout.totalTokens, perturbation: best.pert };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Project a per-chunk frontier onto the nearest VALID TREE CUT (group-consistent).
|
|
446
|
+
*
|
|
447
|
+
* A leaf folded to level k renders as part of its L_k ancestor node, and that
|
|
448
|
+
* node is atomic over its whole covered range — so EVERY leaf under it must share
|
|
449
|
+
* level k, or the node is unrenderable. The picker walks group-atomic raise/lower
|
|
450
|
+
* ops, so a frontier that folds only SOME leaves under a node is an unreachable
|
|
451
|
+
* target: it raises the group (over-folding a sibling), lowers it (under-folding
|
|
452
|
+
* another), forever — non-convergence (the wedge this fixes).
|
|
453
|
+
*
|
|
454
|
+
* Frontiers seeded per-chunk from the carried `previous` frontier can violate
|
|
455
|
+
* this: as a summary group accretes new raw leaves over turns while older leaves
|
|
456
|
+
* stay folded, its leaves end up at mixed levels. shed/expand only re-consolidate
|
|
457
|
+
* groups they actively move; a mixed group sitting in the budget dead-band passes
|
|
458
|
+
* through untouched. This pass repairs it.
|
|
459
|
+
*
|
|
460
|
+
* Shallowest-first: where a group disagrees, UN-fold the folded leaves one level
|
|
461
|
+
* toward raw (preserving continuity), cascading until every folded leaf's group
|
|
462
|
+
* is unanimous. Monotone (only lowers) → terminates. Any budget breach from the
|
|
463
|
+
* extra raw is re-folded whole-group by the shed pass that runs afterward.
|
|
464
|
+
*
|
|
465
|
+
* Pinned (`fixedLevels`) and frozen (locked) leaves are already made
|
|
466
|
+
* group-consistent by the caller (pin-at-level fixes a whole node's leaves), so
|
|
467
|
+
* they are held here rather than lowered.
|
|
468
|
+
*/
|
|
469
|
+
function projectToValidCut(
|
|
470
|
+
F: Map<ChunkId, number>,
|
|
471
|
+
tree: SummaryTree,
|
|
472
|
+
ordered: PickerChunk[],
|
|
473
|
+
frozen: ReadonlySet<ChunkId>,
|
|
474
|
+
fixedLevels: ReadonlyMap<ChunkId, number>,
|
|
475
|
+
): void {
|
|
476
|
+
const maxPasses = (ordered.length + 1) * (maxAvailableLevel(tree) + 2);
|
|
477
|
+
let changed = true;
|
|
478
|
+
let pass = 0;
|
|
479
|
+
while (changed && pass++ < maxPasses) {
|
|
480
|
+
changed = false;
|
|
481
|
+
for (const c of ordered) {
|
|
482
|
+
const lvl = F.get(c.id) ?? 0;
|
|
483
|
+
if (lvl <= 0) continue;
|
|
484
|
+
if (frozen.has(c.id) || fixedLevels.has(c.id)) continue; // held; consistent by construction
|
|
485
|
+
const node = tree.ancestorAt(c.id, lvl);
|
|
486
|
+
let unanimous = node != null;
|
|
487
|
+
if (node) {
|
|
488
|
+
for (const leafId of node.leafChunkIds) {
|
|
489
|
+
if ((F.get(leafId) ?? 0) !== lvl) { unanimous = false; break; }
|
|
237
490
|
}
|
|
238
491
|
}
|
|
239
|
-
if (!
|
|
240
|
-
for (const leafId of node.leafChunkIds) frontier.set(leafId, level - 1);
|
|
241
|
-
const nt = renderLayout(inputs, tree, frontier).totalTokens;
|
|
242
|
-
if (nt <= target) tokens = nt; // accept the un-fold
|
|
243
|
-
else for (const leafId of node.leafChunkIds) frontier.set(leafId, level); // overshoot → revert
|
|
492
|
+
if (!unanimous) { F.set(c.id, lvl - 1); changed = true; } // un-fold one level (shallowest-first)
|
|
244
493
|
}
|
|
245
494
|
}
|
|
246
|
-
return tokens;
|
|
247
495
|
}
|
|
248
496
|
|
|
249
497
|
/** Earliest source sequence whose resolution changed `prev → next`. */
|
|
@@ -285,6 +533,7 @@ function rawTailSet(ordered: PickerChunk[], tailTokens: number): Set<ChunkId> {
|
|
|
285
533
|
}
|
|
286
534
|
|
|
287
535
|
const EMPTY_SET: ReadonlySet<ChunkId> = new Set();
|
|
536
|
+
const EMPTY_LEVEL_MAP: ReadonlyMap<ChunkId, number> = new Map();
|
|
288
537
|
|
|
289
538
|
export interface ControlPlanParams {
|
|
290
539
|
/** Carried frontier (F_prev). */
|
|
@@ -297,15 +546,39 @@ export interface ControlPlanParams {
|
|
|
297
546
|
expandAtTokens?: number;
|
|
298
547
|
/** The attractor both fold and un-fold aim for (soft target). */
|
|
299
548
|
targetTokens: number;
|
|
300
|
-
/** Hard wall W —
|
|
549
|
+
/** Hard wall W — the only hard constraint (§13.2). */
|
|
301
550
|
windowTokens: number;
|
|
302
|
-
/**
|
|
303
|
-
*
|
|
551
|
+
/** Trust region P on per-turn perturbation, in tokens the provider would
|
|
552
|
+
* re-read (exact `kvCost`, not a spatial gate). Within P the solver adopts
|
|
553
|
+
* the ideal cut outright; beyond it it adopts the ideal's newest changes
|
|
554
|
+
* only (suffix adoption) unless an override fires (§13.4). Default =
|
|
555
|
+
* windowTokens (a rendered layout never exceeds W, so the default trust
|
|
556
|
+
* region never binds). */
|
|
304
557
|
reachTokens?: number;
|
|
558
|
+
/** Quality-gap override threshold (§13.4): a plan within P is rejected — and
|
|
559
|
+
* the ideal adopted, paying the perturbation — when its relevance loss
|
|
560
|
+
* exceeds the ideal's by more than this fraction of the ideal's loss.
|
|
561
|
+
* Also bounds how misallocated a dead-band hold may be before the solver
|
|
562
|
+
* self-heals. Default 0.35. */
|
|
563
|
+
qualityGapRatio?: number;
|
|
305
564
|
/** Chunks forced to raw and never folded (flat zone / head / tail / pinned). */
|
|
306
565
|
rawZone: ReadonlySet<ChunkId>;
|
|
307
566
|
/** Chunks kept at their carried resolution and never touched (locked). */
|
|
308
567
|
frozen?: ReadonlySet<ChunkId>;
|
|
568
|
+
/**
|
|
569
|
+
* V2 pin-at-level-k: chunks fixed to render at EXACTLY the given fold level
|
|
570
|
+
* (0 = raw). Held there and never folded or un-folded — the frontier cut
|
|
571
|
+
* passes through that L_k node. A level deeper than the tree has produced is
|
|
572
|
+
* clamped to the deepest available. (`ProtectedRange.level`.)
|
|
573
|
+
*/
|
|
574
|
+
fixedLevels?: ReadonlyMap<ChunkId, number>;
|
|
575
|
+
/**
|
|
576
|
+
* V2 pin-max-level: per-chunk HARD cap on fold depth — the chunk may fold no
|
|
577
|
+
* deeper than this level, enforced in normal AND emergency shedding, and any
|
|
578
|
+
* carried resolution deeper than the cap is un-folded to it at plan start.
|
|
579
|
+
* (`ProtectedRange.maxLevel`.)
|
|
580
|
+
*/
|
|
581
|
+
pinCaps?: ReadonlyMap<ChunkId, number>;
|
|
309
582
|
/** Newest source sequence — the age reference for the log-age fold caps. */
|
|
310
583
|
now: number;
|
|
311
584
|
/** Base-k summary grouping. Default 6. */
|
|
@@ -319,21 +592,36 @@ export interface ControlPlan {
|
|
|
319
592
|
folded: boolean;
|
|
320
593
|
/** An un-fold (raise toward raw, to use budget headroom) happened this turn. */
|
|
321
594
|
expanded: boolean;
|
|
322
|
-
/** Even full folding
|
|
595
|
+
/** Even full folding could not fit under W (terminal — production problem). */
|
|
323
596
|
escalated: boolean;
|
|
597
|
+
/** Exact prefix-invalidation cost of this plan vs the carried frontier:
|
|
598
|
+
* tokens the provider will re-read (0 on a hold/pure-append turn). */
|
|
599
|
+
perturbation: number;
|
|
600
|
+
/** Set when the trust region P did not bind normally (§13.4). Callers should
|
|
601
|
+
* log these loudly (`[kv-escalation]`):
|
|
602
|
+
* - 'bootstrap': no carried frontier — pure relevance solve, P not applicable;
|
|
603
|
+
* - 'infeasible': nothing within P fits under W — feasibility beats P;
|
|
604
|
+
* - 'quality-gap': the best plan within P was certifiably bad vs the ideal. */
|
|
605
|
+
override?: 'bootstrap' | 'infeasible' | 'quality-gap';
|
|
324
606
|
}
|
|
325
607
|
|
|
326
608
|
/**
|
|
327
|
-
* Plan one turn's frontier
|
|
328
|
-
* harness (`replayControlled`) and `KvStableStrategy`.
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
609
|
+
* Plan one turn's frontier — the rev 5.0 single-path solve (§13.4), shared by
|
|
610
|
+
* the replay harness (`replayControlled`) and `KvStableStrategy`.
|
|
611
|
+
*
|
|
612
|
+
* 1. Build the IDEAL CUT from scratch (pure relevance; W the only wall).
|
|
613
|
+
* 2. No carried frontier → adopt it (bootstrap; P not applicable).
|
|
614
|
+
* 3. Carried frontier in the [expandAt, foldAt] dead band with a small
|
|
615
|
+
* quality gap → hold it (zero perturbation). A LARGE gap falls through:
|
|
616
|
+
* a stuck misallocated profile self-heals instead of fossilizing.
|
|
617
|
+
* 4. Ideal within the trust region P (exact kvCost) → adopt it.
|
|
618
|
+
* 5. Otherwise adopt the ideal's newest changes only (suffix adoption) if
|
|
619
|
+
* that fits W and is not certifiably bad; else adopt the ideal and record
|
|
620
|
+
* the override ('infeasible' / 'quality-gap').
|
|
621
|
+
*
|
|
622
|
+
* There is no emergency path and no spatial eligibility gate: cache continuity
|
|
623
|
+
* is priced (and bounded by P) — never a reason a chunk can't move.
|
|
624
|
+
* Pure and deterministic.
|
|
337
625
|
*/
|
|
338
626
|
export function planControlledFrontier(
|
|
339
627
|
inputs: PickerInputs,
|
|
@@ -342,41 +630,155 @@ export function planControlledFrontier(
|
|
|
342
630
|
): ControlPlan {
|
|
343
631
|
const ordered = [...inputs.chunks].sort((a, b) => a.sequence - b.sequence);
|
|
344
632
|
const frozen = p.frozen ?? EMPTY_SET;
|
|
633
|
+
const fixedLevels = p.fixedLevels ?? EMPTY_LEVEL_MAP;
|
|
634
|
+
const pinCaps = p.pinCaps ?? EMPTY_LEVEL_MAP;
|
|
345
635
|
const mergeThreshold = p.mergeThreshold ?? 6;
|
|
346
|
-
const
|
|
636
|
+
const P = p.reachTokens ?? p.windowTokens;
|
|
347
637
|
const expandAt = p.expandAtTokens ?? p.targetTokens;
|
|
638
|
+
const gapRatio = p.qualityGapRatio ?? 0.35;
|
|
639
|
+
// Fold as deep as the tree actually goes (L4/L5… when produced), not a constant.
|
|
640
|
+
const maxFoldLevel = maxAvailableLevel(tree);
|
|
641
|
+
|
|
642
|
+
// Immovable = never folded OR un-folded by the solve: the locked (frozen)
|
|
643
|
+
// set plus V2 pin-at-level-k chunks (held at their fixed level).
|
|
644
|
+
const immovable = new Set<ChunkId>(frozen);
|
|
645
|
+
for (const id of fixedLevels.keys()) immovable.add(id);
|
|
646
|
+
|
|
647
|
+
// Base = the non-negotiable part of every cut: raw zone raw, pins at their
|
|
648
|
+
// level, frozen at their carried resolution. Everything else starts raw here;
|
|
649
|
+
// the carried frontier and the ideal cut both build on this base.
|
|
650
|
+
const base = new Map<ChunkId, number>();
|
|
651
|
+
for (const c of ordered) {
|
|
652
|
+
if (p.rawZone.has(c.id)) {
|
|
653
|
+
base.set(c.id, 0);
|
|
654
|
+
} else if (fixedLevels.has(c.id)) {
|
|
655
|
+
// Pin-at-k: fix to exactly k, clamped to the deepest produced level for
|
|
656
|
+
// this chunk (can't render at a level whose summary doesn't exist yet).
|
|
657
|
+
const k = Math.max(0, fixedLevels.get(c.id)!);
|
|
658
|
+
base.set(c.id, k === 0 ? 0 : Math.min(k, tree.maxLevel(c.id)));
|
|
659
|
+
} else if (frozen.has(c.id)) {
|
|
660
|
+
base.set(c.id, p.previous.get(c.id) ?? c.currentResolution);
|
|
661
|
+
} else {
|
|
662
|
+
base.set(c.id, 0);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
348
665
|
|
|
349
|
-
|
|
666
|
+
// Carried frontier (F_prev with the base enforced): what is actually
|
|
667
|
+
// rendered right now, and the reference for perturbation.
|
|
668
|
+
const carried = new Map(base);
|
|
669
|
+
let carriedNonEmpty = false;
|
|
670
|
+
for (const c of ordered) {
|
|
671
|
+
if (p.rawZone.has(c.id) || fixedLevels.has(c.id) || frozen.has(c.id)) continue;
|
|
672
|
+
// Enforce a pin-max-level immediately: un-fold anything carried deeper than
|
|
673
|
+
// its cap down to the cap (shallower summaries — its ancestors — are
|
|
674
|
+
// guaranteed to exist). The intended divergence cost of tightening a pin.
|
|
675
|
+
let lvl = p.previous.get(c.id) ?? 0;
|
|
676
|
+
const cap = pinCaps.get(c.id);
|
|
677
|
+
if (cap !== undefined && lvl > cap) lvl = Math.max(0, cap);
|
|
678
|
+
carried.set(c.id, lvl);
|
|
679
|
+
}
|
|
680
|
+
for (const v of p.previous.values()) {
|
|
681
|
+
if (v !== 0) { carriedNonEmpty = true; break; }
|
|
682
|
+
}
|
|
683
|
+
projectToValidCut(carried, tree, ordered, frozen, fixedLevels);
|
|
684
|
+
const carriedLayout = renderLayout(inputs, tree, carried);
|
|
685
|
+
const carriedTokens = carriedLayout.totalTokens;
|
|
686
|
+
|
|
687
|
+
// Shape prior: soft per-chunk depth caps (log-age band + pin caps), with the
|
|
688
|
+
// −1 sentinel marking the hard-protected set (never folded by anything).
|
|
689
|
+
const flatZoneChunks = p.rawZone.size;
|
|
690
|
+
const caps = new Map<ChunkId, number>();
|
|
350
691
|
for (const c of ordered) {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
692
|
+
let cap: number;
|
|
693
|
+
if (p.rawZone.has(c.id) || immovable.has(c.id)) {
|
|
694
|
+
cap = -1; // hard-protected sentinel: never folded, W notwithstanding
|
|
695
|
+
} else {
|
|
696
|
+
cap = foldDepthCap(c, p.now, p.rawZone, flatZoneChunks, mergeThreshold, maxFoldLevel);
|
|
697
|
+
const pinCap = pinCaps.get(c.id);
|
|
698
|
+
if (pinCap !== undefined) cap = Math.min(cap, Math.max(0, pinCap));
|
|
699
|
+
}
|
|
700
|
+
caps.set(c.id, cap);
|
|
354
701
|
}
|
|
355
702
|
|
|
356
|
-
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
703
|
+
// 1. The ideal cut — pure relevance, no P anywhere.
|
|
704
|
+
const _t0 = typeof process !== 'undefined' && process.env?.KV_TIMING ? Date.now() : 0;
|
|
705
|
+
const ideal = relevanceCut(
|
|
706
|
+
inputs, tree, ordered, base, caps, pinCaps, p.rawZone, immovable, frozen, fixedLevels,
|
|
707
|
+
p.targetTokens, p.windowTokens, maxFoldLevel,
|
|
708
|
+
);
|
|
709
|
+
if (_t0) console.error(`[kv-timing] relevanceCut ${Date.now() - _t0}ms idealTokens=${ideal.tokens}`);
|
|
710
|
+
const idealLayout = renderLayout(inputs, tree, ideal.F);
|
|
711
|
+
const idealLoss = relevanceLoss(ideal.F, ordered, p.rawZone, caps);
|
|
712
|
+
const gapCeiling = gapRatio * Math.max(1, idealLoss);
|
|
713
|
+
|
|
714
|
+
const flags = (F: ReadonlyMap<ChunkId, number>): { folded: boolean; expanded: boolean } => {
|
|
715
|
+
let folded = false;
|
|
716
|
+
let expanded = false;
|
|
364
717
|
for (const c of ordered) {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
);
|
|
718
|
+
const prev = carried.get(c.id) ?? 0;
|
|
719
|
+
const next = F.get(c.id) ?? 0;
|
|
720
|
+
if (next > prev) folded = true;
|
|
721
|
+
else if (next < prev) expanded = true;
|
|
722
|
+
if (folded && expanded) break;
|
|
371
723
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
724
|
+
return { folded, expanded };
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
const adoptIdeal = (override?: ControlPlan['override']): ControlPlan => ({
|
|
728
|
+
resolutions: ideal.F,
|
|
729
|
+
tokens: ideal.tokens,
|
|
730
|
+
...flags(ideal.F),
|
|
731
|
+
escalated: ideal.tokens > p.windowTokens,
|
|
732
|
+
perturbation: kvCost(carriedLayout, idealLayout),
|
|
733
|
+
...(override ? { override } : {}),
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
// 2. Bootstrap: nothing carried → nothing to preserve; pure relevance solve.
|
|
737
|
+
if (!carriedNonEmpty && p.previous.size === 0) return adoptIdeal('bootstrap');
|
|
738
|
+
|
|
739
|
+
// 3. Dead band with self-heal: hold the carried frontier only when it is
|
|
740
|
+
// feasible, in band, AND not certifiably misallocated.
|
|
741
|
+
const carriedLoss = relevanceLoss(carried, ordered, p.rawZone, caps);
|
|
742
|
+
const inBand = carriedTokens <= p.foldAtTokens && carriedTokens >= expandAt;
|
|
743
|
+
if (inBand && carriedTokens <= p.windowTokens && carriedLoss - idealLoss <= gapCeiling) {
|
|
744
|
+
return {
|
|
745
|
+
resolutions: carried,
|
|
746
|
+
tokens: carriedTokens,
|
|
747
|
+
...flags(carried), // vs itself → both false unless projection moved it
|
|
748
|
+
escalated: false,
|
|
749
|
+
perturbation: 0,
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// 4. Within the trust region → adopt the ideal outright.
|
|
754
|
+
const idealPert = kvCost(carriedLayout, idealLayout);
|
|
755
|
+
if (idealPert <= P) return adoptIdeal();
|
|
756
|
+
|
|
757
|
+
// 5. Suffix adoption: the ideal's newest changes only, within P.
|
|
758
|
+
const partial = suffixAdopt(
|
|
759
|
+
inputs, tree, ordered, carried, carriedLayout, ideal.F, frozen, fixedLevels, P,
|
|
760
|
+
);
|
|
761
|
+
const partialLoss = relevanceLoss(partial.F, ordered, p.rawZone, caps);
|
|
762
|
+
// Progress guard: when a shed is REQUIRED (carried over foldAt), a partial
|
|
763
|
+
// plan that neither reaches the band nor moves at all is not acceptable —
|
|
764
|
+
// an under-folded profile scores zero misallocation loss, so the quality
|
|
765
|
+
// gap alone cannot reject "do nothing forever". (P below the physical floor
|
|
766
|
+
// — the tail that any fold must invalidate — lands here and overrides.)
|
|
767
|
+
const mustShed = carriedTokens > p.foldAtTokens;
|
|
768
|
+
const madeProgress =
|
|
769
|
+
!mustShed || partial.tokens <= p.foldAtTokens || partial.perturbation > 0;
|
|
770
|
+
if (partial.tokens <= p.windowTokens && partialLoss - idealLoss <= gapCeiling && madeProgress) {
|
|
771
|
+
return {
|
|
772
|
+
resolutions: partial.F,
|
|
773
|
+
tokens: partial.tokens,
|
|
774
|
+
...flags(partial.F),
|
|
775
|
+
escalated: false,
|
|
776
|
+
perturbation: partial.perturbation,
|
|
777
|
+
};
|
|
378
778
|
}
|
|
379
|
-
|
|
779
|
+
|
|
780
|
+
// Trust region priced out: feasibility or quality demands the ideal.
|
|
781
|
+
return adoptIdeal(partial.tokens > p.windowTokens ? 'infeasible' : 'quality-gap');
|
|
380
782
|
}
|
|
381
783
|
|
|
382
784
|
/**
|