pi-fold 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -7
- package/extensions/active-context.ts +2252 -182
- package/extensions/lib/advisory.ts +171 -14
- package/extensions/lib/canonical.ts +20 -0
- package/extensions/lib/curation.ts +437 -0
- package/extensions/lib/folding.ts +468 -22
- package/extensions/lib/instrumentation.ts +333 -0
- package/extensions/lib/measurement.ts +111 -0
- package/extensions/lib/persistence.ts +158 -2
- package/extensions/lib/policy.ts +380 -4
- package/extensions/lib/scheduling.ts +923 -0
- package/extensions/lib/selection.ts +437 -12
- package/extensions/lib/surfacing.ts +349 -0
- package/extensions/lib/tool-surface.ts +52 -3
- package/extensions/lib/transcript.ts +58 -4
- package/package.json +2 -2
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { evidenceSha256, sha256Value } from "../json.ts";
|
|
2
|
+
import { ownValue } from "./canonical.ts";
|
|
3
|
+
import {
|
|
4
|
+
MAX_CONTEXT_ATTEMPT_RECORDS,
|
|
5
|
+
MAX_PROJECTION_HASH_RECORDS,
|
|
6
|
+
} from "./policy.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Projection instrumentation.
|
|
10
|
+
*
|
|
11
|
+
* The iteration-1 ledger reported 19 "observed mutations", and reconstruction showed
|
|
12
|
+
* only 7 of them were projection rewrites this runtime performed. The other 12 were
|
|
13
|
+
* provider-side cache misses on projections that had only GROWN: message count up by
|
|
14
|
+
* one, no message removed, system prompt and tool digests constant, and the request
|
|
15
|
+
* still re-paid ~200k fresh tokens. One dial counting both makes a scheduling lever
|
|
16
|
+
* look responsible for a cost no scheduling lever can remove.
|
|
17
|
+
*
|
|
18
|
+
* So the two are counted separately, and the append case is made PROVABLE from
|
|
19
|
+
* artifacts: per-message digests of consecutive projections show exactly where, or
|
|
20
|
+
* whether, the prefix diverged.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export type ProjectionChange = "identical" | "append" | "rewrite";
|
|
24
|
+
|
|
25
|
+
export interface ProjectionComparison {
|
|
26
|
+
change: ProjectionChange;
|
|
27
|
+
previousCount: number;
|
|
28
|
+
nextCount: number;
|
|
29
|
+
appendedCount: number;
|
|
30
|
+
/** First index whose digest differs; null when the prefix is intact. */
|
|
31
|
+
firstDivergentIndex: number | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function messageDigests(messages: readonly unknown[]): string[] {
|
|
35
|
+
return messages.map((message) => {
|
|
36
|
+
try { return evidenceSha256(message); }
|
|
37
|
+
catch { return sha256Value({ unhashable: true }); }
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Compare two consecutive projections by per-message digest. A pure append leaves the
|
|
43
|
+
* whole previous prefix intact, which is precisely the condition under which a
|
|
44
|
+
* provider cache SHOULD hit; anything else is a rewrite this runtime caused.
|
|
45
|
+
*/
|
|
46
|
+
export function compareProjections(
|
|
47
|
+
previous: readonly string[] | null,
|
|
48
|
+
next: readonly string[],
|
|
49
|
+
): ProjectionComparison {
|
|
50
|
+
if (!previous) {
|
|
51
|
+
return {
|
|
52
|
+
change: "append",
|
|
53
|
+
previousCount: 0,
|
|
54
|
+
nextCount: next.length,
|
|
55
|
+
appendedCount: next.length,
|
|
56
|
+
firstDivergentIndex: null,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
let firstDivergentIndex: number | null = null;
|
|
60
|
+
const shared = Math.min(previous.length, next.length);
|
|
61
|
+
for (let index = 0; index < shared; index += 1) {
|
|
62
|
+
if (previous[index] !== next[index]) { firstDivergentIndex = index; break; }
|
|
63
|
+
}
|
|
64
|
+
if (firstDivergentIndex !== null || next.length < previous.length) {
|
|
65
|
+
return {
|
|
66
|
+
change: "rewrite",
|
|
67
|
+
previousCount: previous.length,
|
|
68
|
+
nextCount: next.length,
|
|
69
|
+
appendedCount: Math.max(0, next.length - previous.length),
|
|
70
|
+
firstDivergentIndex: firstDivergentIndex ?? shared,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
change: next.length === previous.length ? "identical" : "append",
|
|
75
|
+
previousCount: previous.length,
|
|
76
|
+
nextCount: next.length,
|
|
77
|
+
appendedCount: next.length - previous.length,
|
|
78
|
+
firstDivergentIndex: null,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface ProjectionRecord extends ProjectionComparison {
|
|
83
|
+
ordinal: number;
|
|
84
|
+
prefixSha256: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** One provider response read against the projection that produced it. */
|
|
88
|
+
export interface CacheObservation {
|
|
89
|
+
ordinal: number;
|
|
90
|
+
change: ProjectionChange;
|
|
91
|
+
inputTokens: number;
|
|
92
|
+
cacheReadTokens: number;
|
|
93
|
+
/** A fresh re-read of a prefix that did not move: nothing here is ours to fix. */
|
|
94
|
+
providerSideMiss: boolean;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* THE context-management event stream.
|
|
99
|
+
*
|
|
100
|
+
* One stream, one envelope, one naming convention. Every context event this runtime
|
|
101
|
+
* produces -- tool attempts and their refusals, span corrections, gate lifecycle,
|
|
102
|
+
* receipts, commits, absorptions, splits, recoveries, projections and provider usage --
|
|
103
|
+
* lands here as a flat JSONL-style record, and every record is also appended as a
|
|
104
|
+
* durable session entry so an analyst can reconstruct the whole timeline from session
|
|
105
|
+
* artifacts alone.
|
|
106
|
+
*
|
|
107
|
+
* The envelope is fixed and self-describing:
|
|
108
|
+
* v schema version of the envelope
|
|
109
|
+
* seq monotonic per session, from 1, so records order without a clock
|
|
110
|
+
* kind "context.<noun>", the only field a reader must know to dispatch
|
|
111
|
+
* session_id the session these events belong to
|
|
112
|
+
* ordinal transcript position, the deterministic clock this runtime reasons in
|
|
113
|
+
* at wall-clock milliseconds, the join key against provider-side telemetry
|
|
114
|
+
* revision durable state revision at emit time
|
|
115
|
+
*
|
|
116
|
+
* Everything after the envelope is a FLAT snake_case payload. Records are not shaped
|
|
117
|
+
* around any metric we compute today: a reader that wants a number we never thought of
|
|
118
|
+
* should be able to get it with a query rather than a code change. Nothing nests, and
|
|
119
|
+
* anything one-to-many (a correction, an absorbed wedge, a created fold) is its own
|
|
120
|
+
* record referencing the record it belongs to by `seq`.
|
|
121
|
+
*/
|
|
122
|
+
export const CONTEXT_EVENT_SCHEMA_VERSION = 1;
|
|
123
|
+
|
|
124
|
+
export interface ContextEvent {
|
|
125
|
+
v: number;
|
|
126
|
+
seq: number;
|
|
127
|
+
kind: string;
|
|
128
|
+
session_id: string;
|
|
129
|
+
ordinal: number;
|
|
130
|
+
at: number;
|
|
131
|
+
revision: number;
|
|
132
|
+
[field: string]: unknown;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export type ContextEventKind =
|
|
136
|
+
/** One context-management tool call, accepted or refused. */
|
|
137
|
+
| "context.attempt"
|
|
138
|
+
/** One auto-snap applied to a requested span, referencing its attempt. */
|
|
139
|
+
| "context.correction"
|
|
140
|
+
/** The resolved serving budget, stated once at startup so a live run can prove it. */
|
|
141
|
+
| "context.capacity"
|
|
142
|
+
/** One evaluation of the guided-curation last-call gate. */
|
|
143
|
+
| "context.gate"
|
|
144
|
+
/** One sparse occupancy reminder delivered into the window. */
|
|
145
|
+
| "context.reminder"
|
|
146
|
+
/** One receipt delivered into the window. */
|
|
147
|
+
| "context.receipt"
|
|
148
|
+
/** One commit epoch: the single mutation an epoch pays for. */
|
|
149
|
+
| "context.commit"
|
|
150
|
+
/** One fold created, by the ladder or by the agent. */
|
|
151
|
+
| "context.fold"
|
|
152
|
+
/** One boundary sliver absorbed into its later neighbour. */
|
|
153
|
+
| "context.absorb"
|
|
154
|
+
/** One oversized span split into sequential bite-sized folds. */
|
|
155
|
+
| "context.split"
|
|
156
|
+
/** One overflow rollback and the recovery that followed it. */
|
|
157
|
+
| "context.recovery"
|
|
158
|
+
/** One projection built, classified against the one before it. */
|
|
159
|
+
| "context.projection"
|
|
160
|
+
/** One provider response, which is where this stream joins provider usage. */
|
|
161
|
+
| "context.usage"
|
|
162
|
+
/** One handoff, compared byte-for-byte against the previous transmitted projection. */
|
|
163
|
+
| "context.prefix";
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Where the prefix first differs from the previous TRANSMITTED projection, in chars.
|
|
167
|
+
*
|
|
168
|
+
* A positional cache is keyed on a byte prefix, so the message-level classification is
|
|
169
|
+
* not enough to attribute a miss: the analyst needs the position, and only the runtime
|
|
170
|
+
* knows why a byte at that position moved. This is the cheap half -- both projections
|
|
171
|
+
* are in hand at handoff -- and it is emission only. No analysis of it belongs here.
|
|
172
|
+
*/
|
|
173
|
+
export function prefixDivergence(
|
|
174
|
+
previous: string | null,
|
|
175
|
+
next: string,
|
|
176
|
+
): { index: number | null; identicalChars: number; identicalShare: number } {
|
|
177
|
+
if (previous === null) return { index: null, identicalChars: 0, identicalShare: 0 };
|
|
178
|
+
const shared = Math.min(previous.length, next.length);
|
|
179
|
+
let index = 0;
|
|
180
|
+
while (index < shared && previous.charCodeAt(index) === next.charCodeAt(index)) index += 1;
|
|
181
|
+
const diverged = index < shared || next.length < previous.length;
|
|
182
|
+
return {
|
|
183
|
+
index: diverged ? index : null,
|
|
184
|
+
identicalChars: index,
|
|
185
|
+
identicalShare: next.length > 0 ? index / next.length : 0,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface InstrumentationLedger {
|
|
190
|
+
projections: number;
|
|
191
|
+
/** Projections this runtime actually rewrote. The scheduling dial. */
|
|
192
|
+
rewrites: number;
|
|
193
|
+
appends: number;
|
|
194
|
+
identical: number;
|
|
195
|
+
/** Responses that re-read an unchanged prefix from scratch. The provider's dial. */
|
|
196
|
+
providerSideMisses: number;
|
|
197
|
+
observedMisses: number;
|
|
198
|
+
records: ProjectionRecord[];
|
|
199
|
+
observations: CacheObservation[];
|
|
200
|
+
/** The stream itself, oldest first. Bounded in memory; durable in full. */
|
|
201
|
+
events: ContextEvent[];
|
|
202
|
+
/** The monotonic sequence every record carries. */
|
|
203
|
+
sequence: number;
|
|
204
|
+
countsByKind: Record<string, number>;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function emptyLedger(): InstrumentationLedger {
|
|
208
|
+
return {
|
|
209
|
+
projections: 0,
|
|
210
|
+
rewrites: 0,
|
|
211
|
+
appends: 0,
|
|
212
|
+
identical: 0,
|
|
213
|
+
providerSideMisses: 0,
|
|
214
|
+
observedMisses: 0,
|
|
215
|
+
records: [],
|
|
216
|
+
observations: [],
|
|
217
|
+
events: [],
|
|
218
|
+
sequence: 0,
|
|
219
|
+
countsByKind: {},
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Stamp the envelope and append. The in-memory ring is the status view and is bounded;
|
|
225
|
+
* the durable session entry is the audit copy and is not.
|
|
226
|
+
*/
|
|
227
|
+
export function recordContextEvent(
|
|
228
|
+
ledger: InstrumentationLedger,
|
|
229
|
+
kind: ContextEventKind,
|
|
230
|
+
envelope: { session_id: string; ordinal: number; revision: number; at: number },
|
|
231
|
+
payload: Record<string, unknown> = {},
|
|
232
|
+
): ContextEvent {
|
|
233
|
+
ledger.sequence += 1;
|
|
234
|
+
const record: ContextEvent = {
|
|
235
|
+
v: CONTEXT_EVENT_SCHEMA_VERSION,
|
|
236
|
+
seq: ledger.sequence,
|
|
237
|
+
kind,
|
|
238
|
+
session_id: envelope.session_id,
|
|
239
|
+
ordinal: envelope.ordinal,
|
|
240
|
+
at: envelope.at,
|
|
241
|
+
revision: envelope.revision,
|
|
242
|
+
...payload,
|
|
243
|
+
};
|
|
244
|
+
ledger.countsByKind[kind] = (ledger.countsByKind[kind] ?? 0) + 1;
|
|
245
|
+
ledger.events.push(record);
|
|
246
|
+
if (ledger.events.length > MAX_CONTEXT_ATTEMPT_RECORDS) {
|
|
247
|
+
ledger.events = ledger.events.slice(ledger.events.length - MAX_CONTEXT_ATTEMPT_RECORDS);
|
|
248
|
+
}
|
|
249
|
+
return record;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function bounded<T>(values: T[], next: T): T[] {
|
|
253
|
+
values.push(next);
|
|
254
|
+
return values.length > MAX_PROJECTION_HASH_RECORDS
|
|
255
|
+
? values.slice(values.length - MAX_PROJECTION_HASH_RECORDS)
|
|
256
|
+
: values;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function recordProjection(
|
|
260
|
+
ledger: InstrumentationLedger,
|
|
261
|
+
comparison: ProjectionComparison,
|
|
262
|
+
digests: readonly string[],
|
|
263
|
+
): void {
|
|
264
|
+
ledger.projections += 1;
|
|
265
|
+
if (comparison.change === "rewrite") ledger.rewrites += 1;
|
|
266
|
+
else if (comparison.change === "append") ledger.appends += 1;
|
|
267
|
+
else ledger.identical += 1;
|
|
268
|
+
ledger.records = bounded(ledger.records, {
|
|
269
|
+
...comparison,
|
|
270
|
+
ordinal: ledger.projections,
|
|
271
|
+
// The prefix digest, not the whole projection: it is what a positional cache keys
|
|
272
|
+
// on, so two equal values are the claim that a cache hit was available.
|
|
273
|
+
prefixSha256: sha256Value(digests.slice(0, comparison.previousCount || digests.length)),
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Read one provider response against the projection that produced it. A miss on a
|
|
279
|
+
* projection that only grew is provider-side by construction: no byte we control
|
|
280
|
+
* moved, so no scheduling change could have prevented the re-read.
|
|
281
|
+
*
|
|
282
|
+
* A rewrite used to be charged to us unconditionally, and that hid the whole rep 21
|
|
283
|
+
* finding: a rewrite that preserved 99.86% of the prompt still read back as ours, so
|
|
284
|
+
* our own metrics reported a negligible divergence and a total cache loss and never
|
|
285
|
+
* put the two side by side. Attribution now asks how much of the previous prompt the
|
|
286
|
+
* rewrite actually kept. If we preserved nearly all of it and the provider still
|
|
287
|
+
* served nothing, our divergence cannot account for the loss and the miss is theirs.
|
|
288
|
+
* That is a claim about ATTRIBUTION, not about cost: a preserved-share miss is still
|
|
289
|
+
* a full rebuild, and it is still worth not causing.
|
|
290
|
+
*/
|
|
291
|
+
export const PROVIDER_SIDE_MISS_PRESERVED_SHARE = 0.99;
|
|
292
|
+
|
|
293
|
+
export function observeCacheUsage(
|
|
294
|
+
ledger: InstrumentationLedger,
|
|
295
|
+
input: { usage: unknown; change: ProjectionChange; preservedShare?: number | null },
|
|
296
|
+
): CacheObservation | null {
|
|
297
|
+
const usage = input.usage;
|
|
298
|
+
const inputTokens = ownValue(usage, "input");
|
|
299
|
+
const cacheReadTokens = ownValue(usage, "cacheRead");
|
|
300
|
+
if (typeof inputTokens !== "number" || !Number.isFinite(inputTokens) || inputTokens <= 0 ||
|
|
301
|
+
typeof cacheReadTokens !== "number" || !Number.isFinite(cacheReadTokens) ||
|
|
302
|
+
cacheReadTokens < 0) return null;
|
|
303
|
+
const missed = cacheReadTokens === 0;
|
|
304
|
+
const preserved = typeof input.preservedShare === "number" && Number.isFinite(input.preservedShare)
|
|
305
|
+
? input.preservedShare
|
|
306
|
+
: null;
|
|
307
|
+
const observation: CacheObservation = {
|
|
308
|
+
ordinal: ledger.projections,
|
|
309
|
+
change: input.change,
|
|
310
|
+
inputTokens,
|
|
311
|
+
cacheReadTokens,
|
|
312
|
+
providerSideMiss: missed && (input.change !== "rewrite" ||
|
|
313
|
+
(preserved !== null && preserved >= PROVIDER_SIDE_MISS_PRESERVED_SHARE)),
|
|
314
|
+
};
|
|
315
|
+
if (missed) ledger.observedMisses += 1;
|
|
316
|
+
if (observation.providerSideMiss) ledger.providerSideMisses += 1;
|
|
317
|
+
ledger.observations = bounded(ledger.observations, observation);
|
|
318
|
+
return observation;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** The compact ledger the evidence envelope carries; the record rings stay for audit. */
|
|
322
|
+
export function ledgerSummary(ledger: InstrumentationLedger): Record<string, unknown> {
|
|
323
|
+
return {
|
|
324
|
+
projections: ledger.projections,
|
|
325
|
+
projectionRewrites: ledger.rewrites,
|
|
326
|
+
projectionAppends: ledger.appends,
|
|
327
|
+
projectionsUnchanged: ledger.identical,
|
|
328
|
+
observedCacheMisses: ledger.observedMisses,
|
|
329
|
+
providerSideCacheMisses: ledger.providerSideMisses,
|
|
330
|
+
contextEvents: ledger.sequence,
|
|
331
|
+
contextEventsByKind: { ...ledger.countsByKind },
|
|
332
|
+
};
|
|
333
|
+
}
|
|
@@ -104,6 +104,58 @@ export function hardFenceRatio(value?: unknown, ctx?: any): number {
|
|
|
104
104
|
return (contextWindow - reserve) / contextWindow;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* What the runtime is actually allowed to send, in tokens.
|
|
109
|
+
*
|
|
110
|
+
* The per-request max-input DESCRIPTOR a provider advertises assumes a full output
|
|
111
|
+
* reservation, so it understates the real ceiling by whatever the deployment did not
|
|
112
|
+
* reserve. Measured 2026-08-06 on openai-codex gpt-5.x: a run aborted at ~297k
|
|
113
|
+
* projected tokens against a 272k descriptor, in the same session where the provider
|
|
114
|
+
* had just accepted 339,689 tokens. Serving admits roughly the TOTAL window minus the
|
|
115
|
+
* output reservation actually in force, which at a 16,384 reservation is ~384k.
|
|
116
|
+
*
|
|
117
|
+
* Every number here is introspectable in status on purpose: a fence nobody can audit
|
|
118
|
+
* is how a 25 percent error survived four instrumented runs.
|
|
119
|
+
*/
|
|
120
|
+
export interface CapacityAccounting {
|
|
121
|
+
/** "truthful" once the deployment declares its total serving window. */
|
|
122
|
+
mode: "descriptor" | "truthful";
|
|
123
|
+
/** The denominator every pressure ratio and fence is computed against. */
|
|
124
|
+
window: number;
|
|
125
|
+
outputReservation: number;
|
|
126
|
+
/** The token ceiling: window minus the output reservation. */
|
|
127
|
+
budgetTokens: number;
|
|
128
|
+
usedTokens: number | null;
|
|
129
|
+
headroomTokens: number | null;
|
|
130
|
+
/** What the provider descriptor claimed, kept so the gap stays visible. */
|
|
131
|
+
descriptorWindow: number | null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function capacityAccounting(input: {
|
|
135
|
+
window: number;
|
|
136
|
+
truthful: boolean;
|
|
137
|
+
descriptorWindow: number | null;
|
|
138
|
+
usedTokens: number | null;
|
|
139
|
+
}): CapacityAccounting {
|
|
140
|
+
const window = Number.isFinite(input.window) && input.window > 0 ? input.window : DEFAULT_CONTEXT_WINDOW;
|
|
141
|
+
const outputReservation = Math.min(
|
|
142
|
+
ACTIVE_CONTEXT_POLICY.responseReserve,
|
|
143
|
+
Math.floor(window * 0.1),
|
|
144
|
+
);
|
|
145
|
+
const budgetTokens = window - outputReservation;
|
|
146
|
+
const usedTokens = typeof input.usedTokens === "number" && Number.isFinite(input.usedTokens) &&
|
|
147
|
+
input.usedTokens >= 0 ? input.usedTokens : null;
|
|
148
|
+
return {
|
|
149
|
+
mode: input.truthful ? "truthful" : "descriptor",
|
|
150
|
+
window,
|
|
151
|
+
outputReservation,
|
|
152
|
+
budgetTokens,
|
|
153
|
+
usedTokens,
|
|
154
|
+
headroomTokens: usedTokens === null ? null : budgetTokens - usedTokens,
|
|
155
|
+
descriptorWindow: input.descriptorWindow,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
107
159
|
export interface ProviderContextMeasurement {
|
|
108
160
|
tokens: number;
|
|
109
161
|
contextWindow: number;
|
|
@@ -268,6 +320,21 @@ export function persistenceProjection(state: ActiveContextState, snapshot: Activ
|
|
|
268
320
|
leases: Object.fromEntries(Object.entries(state.leases)
|
|
269
321
|
.filter(([id]) => retained.has(id))),
|
|
270
322
|
};
|
|
323
|
+
// A pending mark is a promise about evidence that is still in the branch. Once its
|
|
324
|
+
// source or its named fold leaves, the mark is unfulfillable and must not survive.
|
|
325
|
+
const survivingMarks = (state.pendingMarks ?? []).filter((mark) => mark.mark === "refold"
|
|
326
|
+
? retained.has(mark.id)
|
|
327
|
+
: mark.parts.every((part) => part.kind === "raw"
|
|
328
|
+
? mapped.has(objectRefKey(part.ref))
|
|
329
|
+
: retained.has(part.foldId)));
|
|
330
|
+
if (survivingMarks.length) projected.pendingMarks = clone(survivingMarks);
|
|
331
|
+
else delete projected.pendingMarks;
|
|
332
|
+
delete projected.pinnedPeeks;
|
|
333
|
+
// A brief correction names a fold; once that fold is gone the correction has
|
|
334
|
+
// nothing to present, exactly like a mark whose source left the branch.
|
|
335
|
+
const survivingBriefs = Object.entries(state.briefs ?? {}).filter(([id]) => retained.has(id));
|
|
336
|
+
if (survivingBriefs.length) projected.briefs = Object.fromEntries(clone(survivingBriefs));
|
|
337
|
+
else delete projected.briefs;
|
|
271
338
|
if (projected.prepared && projected.prepared.sourceRefs.some((ref) => !mapped.has(objectRefKey(ref)))) {
|
|
272
339
|
delete projected.prepared;
|
|
273
340
|
}
|
|
@@ -303,6 +370,50 @@ export function toolRefsProtected(
|
|
|
303
370
|
});
|
|
304
371
|
}
|
|
305
372
|
|
|
373
|
+
export interface AdmissionVerdict {
|
|
374
|
+
admitted: boolean;
|
|
375
|
+
reason: "admitted" | "unmeasured" | "would-cross-fence";
|
|
376
|
+
requestedTokens: number;
|
|
377
|
+
headroomTokens: number | null;
|
|
378
|
+
budgetTokens: number;
|
|
379
|
+
/** Bytes of the target that WOULD fit; 0 when even the minimum slice will not. */
|
|
380
|
+
affordableBytes: number;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Preflight one read against the exact stored size of its target and the truthful
|
|
385
|
+
* headroom. A read that has not been measured yet is ADMITTED and says so: refusing
|
|
386
|
+
* on absent data would stall a session for a number we never had, and the absence is
|
|
387
|
+
* reported rather than papered over.
|
|
388
|
+
*/
|
|
389
|
+
export function admissionVerdict(input: {
|
|
390
|
+
requestedBytes: number;
|
|
391
|
+
capacity: CapacityAccounting;
|
|
392
|
+
bytesPerToken: number;
|
|
393
|
+
}): AdmissionVerdict {
|
|
394
|
+
const requestedTokens = Math.max(0, Math.ceil(input.requestedBytes / input.bytesPerToken));
|
|
395
|
+
const { headroomTokens, budgetTokens } = input.capacity;
|
|
396
|
+
if (headroomTokens === null) {
|
|
397
|
+
return {
|
|
398
|
+
admitted: true,
|
|
399
|
+
reason: "unmeasured",
|
|
400
|
+
requestedTokens,
|
|
401
|
+
headroomTokens,
|
|
402
|
+
budgetTokens,
|
|
403
|
+
affordableBytes: input.requestedBytes,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
const affordableBytes = Math.max(0, headroomTokens) * input.bytesPerToken;
|
|
407
|
+
return {
|
|
408
|
+
admitted: requestedTokens <= headroomTokens,
|
|
409
|
+
reason: requestedTokens <= headroomTokens ? "admitted" : "would-cross-fence",
|
|
410
|
+
requestedTokens,
|
|
411
|
+
headroomTokens,
|
|
412
|
+
budgetTokens,
|
|
413
|
+
affordableBytes,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
306
417
|
export function toolPayload(value: unknown): { content: Array<{ type: "text"; text: string }>; details: unknown } {
|
|
307
418
|
return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }], details: value };
|
|
308
419
|
}
|