pi-fold 0.0.1 → 0.1.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/LICENSE +21 -0
- package/README.md +76 -11
- package/extensions/active-context.ts +1990 -0
- package/extensions/evidence.js +673 -0
- package/extensions/index.js +32 -0
- package/extensions/json.ts +228 -0
- package/extensions/lib/advisory.ts +152 -0
- package/extensions/lib/canonical.ts +163 -0
- package/extensions/lib/folding.ts +1012 -0
- package/extensions/lib/measurement.ts +429 -0
- package/extensions/lib/persistence.ts +773 -0
- package/extensions/lib/policy.ts +233 -0
- package/extensions/lib/selection.ts +501 -0
- package/extensions/lib/tool-surface.ts +66 -0
- package/extensions/lib/transcript.ts +479 -0
- package/extensions/summarizer.js +95 -0
- package/package.json +48 -11
- package/index.js +0 -4
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import type { EvidenceRef } from "../json.ts";
|
|
2
|
+
import {
|
|
3
|
+
denseOwnArrayValues,
|
|
4
|
+
evidenceSha256,
|
|
5
|
+
objectRefKey,
|
|
6
|
+
sameObjectIdentity,
|
|
7
|
+
sha256Value,
|
|
8
|
+
} from "../json.ts";
|
|
9
|
+
import {
|
|
10
|
+
clone,
|
|
11
|
+
exactRecord,
|
|
12
|
+
messageRole,
|
|
13
|
+
ownValue,
|
|
14
|
+
} from "./canonical.ts";
|
|
15
|
+
import {
|
|
16
|
+
childFoldIds,
|
|
17
|
+
deriveFoldParents,
|
|
18
|
+
flattenFoldRefs,
|
|
19
|
+
foldMap,
|
|
20
|
+
parseActiveContextState,
|
|
21
|
+
} from "./persistence.ts";
|
|
22
|
+
import {
|
|
23
|
+
ACTIVE_CONTEXT_POLICY,
|
|
24
|
+
DEFAULT_CONTEXT_WINDOW,
|
|
25
|
+
TOOL_FOLD_CADENCE_MIN_TOKENS,
|
|
26
|
+
TOOL_FOLD_CADENCE_WINDOW_FRACTION,
|
|
27
|
+
} from "./policy.ts";
|
|
28
|
+
import type {
|
|
29
|
+
ActiveContextSnapshot,
|
|
30
|
+
ActiveContextState,
|
|
31
|
+
ActiveFold,
|
|
32
|
+
MappedMessage,
|
|
33
|
+
} from "./policy.ts";
|
|
34
|
+
|
|
35
|
+
export { protectionSha256, topologySha256 } from "./persistence.ts";
|
|
36
|
+
export { clone } from "./canonical.ts";
|
|
37
|
+
export { sha256Value } from "../json.ts";
|
|
38
|
+
export { ADVISORY_BUDGETS, validAdvisoryState } from "./persistence.ts";
|
|
39
|
+
|
|
40
|
+
export const MAPPED_BY_KEY = new WeakMap<ActiveContextSnapshot, Map<string, MappedMessage>>();
|
|
41
|
+
|
|
42
|
+
export function mappedByKey(snapshot: ActiveContextSnapshot): Map<string, MappedMessage> {
|
|
43
|
+
let indexed = MAPPED_BY_KEY.get(snapshot);
|
|
44
|
+
if (!indexed) {
|
|
45
|
+
indexed = new Map(snapshot.mapped.flatMap((item) => item.ref ? [[objectRefKey(item.ref), item]] : []));
|
|
46
|
+
MAPPED_BY_KEY.set(snapshot, indexed);
|
|
47
|
+
}
|
|
48
|
+
return indexed;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function exactMapped(snapshot: ActiveContextSnapshot, ref: EvidenceRef): MappedMessage | null {
|
|
52
|
+
const item = mappedByKey(snapshot).get(objectRefKey(ref));
|
|
53
|
+
return item && sameObjectIdentity(item.ref!, ref) && item.ref!.sha256 === ref.sha256 &&
|
|
54
|
+
evidenceSha256(item.message) === ref.sha256 ? item : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function refsInOrder(snapshot: ActiveContextSnapshot, refs: EvidenceRef[]): number[] | null {
|
|
58
|
+
const indices = refs.map((ref) => exactMapped(snapshot, ref)?.index ?? -1);
|
|
59
|
+
if (indices.some((index) => index < 0) || new Set(indices).size !== indices.length) return null;
|
|
60
|
+
for (let index = 1; index < indices.length; index += 1) if (indices[index] !== indices[index - 1] + 1) return null;
|
|
61
|
+
return indices;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const BRANCH_POSITIONS = new WeakMap<ActiveContextSnapshot, Map<string, { index: number; sha256: string }>>();
|
|
65
|
+
|
|
66
|
+
export function branchSha256(snapshot: ActiveContextSnapshot, refs: EvidenceRef[]): string {
|
|
67
|
+
let indexed = BRANCH_POSITIONS.get(snapshot);
|
|
68
|
+
if (!indexed) {
|
|
69
|
+
indexed = new Map(snapshot.branchObjects.map((item, index) => [
|
|
70
|
+
objectRefKey(item.ref),
|
|
71
|
+
{ index, sha256: item.ref.sha256 },
|
|
72
|
+
]));
|
|
73
|
+
BRANCH_POSITIONS.set(snapshot, indexed);
|
|
74
|
+
}
|
|
75
|
+
const positions = refs.map((ref) => {
|
|
76
|
+
const position = indexed!.get(objectRefKey(ref));
|
|
77
|
+
return position?.sha256 === ref.sha256 ? position.index : -1;
|
|
78
|
+
});
|
|
79
|
+
if (positions.some((index) => index < 0)) return sha256Value({ missing: refs.map(objectRefKey) });
|
|
80
|
+
const anchor = Math.max(...positions);
|
|
81
|
+
return sha256Value(snapshot.branchObjects.slice(0, anchor + 1).map((item) => item.ref));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function contextUsageRatio(value: unknown): number | null {
|
|
85
|
+
const tokens = ownValue(value, "tokens");
|
|
86
|
+
const contextWindow = ownValue(value, "contextWindow");
|
|
87
|
+
if (typeof tokens !== "number" || !Number.isFinite(tokens) || tokens < 0 ||
|
|
88
|
+
typeof contextWindow !== "number" || !Number.isFinite(contextWindow) || contextWindow <= 0) return null;
|
|
89
|
+
return tokens / contextWindow;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function hardFenceRatio(value?: unknown, ctx?: any): number {
|
|
93
|
+
const direct = ownValue(value, "contextWindow");
|
|
94
|
+
const reported = ownValue(ctx?.getContextUsage?.(), "contextWindow");
|
|
95
|
+
const contextWindow = typeof direct === "number" && Number.isFinite(direct) && direct > 0
|
|
96
|
+
? direct
|
|
97
|
+
: typeof reported === "number" && Number.isFinite(reported) && reported > 0
|
|
98
|
+
? reported
|
|
99
|
+
: DEFAULT_CONTEXT_WINDOW;
|
|
100
|
+
const reserve = Math.min(
|
|
101
|
+
ACTIVE_CONTEXT_POLICY.responseReserve,
|
|
102
|
+
Math.floor(contextWindow * 0.1),
|
|
103
|
+
);
|
|
104
|
+
return (contextWindow - reserve) / contextWindow;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface ProviderContextMeasurement {
|
|
108
|
+
tokens: number;
|
|
109
|
+
contextWindow: number;
|
|
110
|
+
messageSha256: string;
|
|
111
|
+
provider: string | null;
|
|
112
|
+
model: string | null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface ProviderMeasurementAnchor {
|
|
116
|
+
sessionId: string;
|
|
117
|
+
generation: number;
|
|
118
|
+
topologySha256: string;
|
|
119
|
+
protectionSha256: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface ProviderContextMeasurementReceipt {
|
|
123
|
+
version: 1;
|
|
124
|
+
sessionId: string;
|
|
125
|
+
projectionRevision: number;
|
|
126
|
+
messageSha256: string;
|
|
127
|
+
provider: string;
|
|
128
|
+
model: string;
|
|
129
|
+
tokens: number;
|
|
130
|
+
contextWindow: number;
|
|
131
|
+
occurredAt: number;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export const PROVIDER_CONTEXT_MEASUREMENT_KEYS = [
|
|
135
|
+
"version", "sessionId", "projectionRevision", "messageSha256", "provider", "model",
|
|
136
|
+
"tokens", "contextWindow", "occurredAt",
|
|
137
|
+
] as const;
|
|
138
|
+
|
|
139
|
+
export function parseProviderContextMeasurementReceipt(
|
|
140
|
+
value: unknown,
|
|
141
|
+
expectedSessionId?: string,
|
|
142
|
+
): ProviderContextMeasurementReceipt {
|
|
143
|
+
if (!exactRecord(value, PROVIDER_CONTEXT_MEASUREMENT_KEYS)) {
|
|
144
|
+
throw new Error("Invalid provider context measurement receipt shape");
|
|
145
|
+
}
|
|
146
|
+
const sessionId = ownValue(value, "sessionId");
|
|
147
|
+
const projectionRevision = ownValue(value, "projectionRevision");
|
|
148
|
+
const messageSha256 = ownValue(value, "messageSha256");
|
|
149
|
+
const provider = ownValue(value, "provider");
|
|
150
|
+
const model = ownValue(value, "model");
|
|
151
|
+
const tokens = ownValue(value, "tokens");
|
|
152
|
+
const contextWindow = ownValue(value, "contextWindow");
|
|
153
|
+
const occurredAt = ownValue(value, "occurredAt");
|
|
154
|
+
if (ownValue(value, "version") !== 1 || typeof sessionId !== "string" || !sessionId ||
|
|
155
|
+
(expectedSessionId && sessionId !== expectedSessionId) ||
|
|
156
|
+
!Number.isSafeInteger(projectionRevision) || Number(projectionRevision) < 0 ||
|
|
157
|
+
typeof messageSha256 !== "string" || !/^[a-f0-9]{64}$/.test(messageSha256) ||
|
|
158
|
+
typeof provider !== "string" || !provider || typeof model !== "string" || !model ||
|
|
159
|
+
typeof tokens !== "number" || !Number.isFinite(tokens) || tokens <= 0 ||
|
|
160
|
+
typeof contextWindow !== "number" || !Number.isFinite(contextWindow) || contextWindow <= 0 ||
|
|
161
|
+
!Number.isSafeInteger(occurredAt) || Number(occurredAt) < 0) {
|
|
162
|
+
throw new Error("Invalid provider context measurement receipt");
|
|
163
|
+
}
|
|
164
|
+
return clone(value) as unknown as ProviderContextMeasurementReceipt;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function providerTokens(message: unknown): number | null {
|
|
168
|
+
const stopReason = ownValue(message, "stopReason");
|
|
169
|
+
if (messageRole(message) !== "assistant" ||
|
|
170
|
+
(stopReason !== "stop" && stopReason !== "length" && stopReason !== "toolUse")) return null;
|
|
171
|
+
const usage = ownValue(message, "usage");
|
|
172
|
+
if (!usage || typeof usage !== "object") return null;
|
|
173
|
+
const total = ownValue(usage, "totalTokens");
|
|
174
|
+
if (typeof total === "number" && Number.isFinite(total) && total > 0) return total;
|
|
175
|
+
const components = ["input", "output", "cacheRead", "cacheWrite"].map((key) => ownValue(usage, key));
|
|
176
|
+
if (components.some((value) => typeof value !== "number" || !Number.isFinite(value) || value < 0)) return null;
|
|
177
|
+
const calculated = components.reduce<number>((sum, value) => sum + Number(value), 0);
|
|
178
|
+
return calculated > 0 ? calculated : null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function providerContextMeasurement(
|
|
182
|
+
message: unknown,
|
|
183
|
+
contextWindow: unknown,
|
|
184
|
+
expectedModel?: { provider?: unknown; id?: unknown } | null,
|
|
185
|
+
): ProviderContextMeasurement | null {
|
|
186
|
+
const tokens = providerTokens(message);
|
|
187
|
+
if (tokens === null || typeof contextWindow !== "number" || !Number.isFinite(contextWindow) || contextWindow <= 0) return null;
|
|
188
|
+
const provider = typeof ownValue(message, "provider") === "string" ? String(ownValue(message, "provider")) : null;
|
|
189
|
+
const model = typeof ownValue(message, "model") === "string" ? String(ownValue(message, "model")) : null;
|
|
190
|
+
if (!provider || !model) return null;
|
|
191
|
+
const expectedProvider = typeof expectedModel?.provider === "string" ? expectedModel.provider : null;
|
|
192
|
+
const expectedId = typeof expectedModel?.id === "string" ? expectedModel.id : null;
|
|
193
|
+
if ((expectedProvider && provider !== expectedProvider) || (expectedId && model !== expectedId)) return null;
|
|
194
|
+
return { tokens, contextWindow, messageSha256: evidenceSha256(message), provider, model };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function latestProviderContextMeasurement(
|
|
198
|
+
messages: unknown[],
|
|
199
|
+
contextWindow: unknown,
|
|
200
|
+
expectedModel?: { provider?: unknown; id?: unknown } | null,
|
|
201
|
+
): ProviderContextMeasurement | null {
|
|
202
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
203
|
+
const measurement = providerContextMeasurement(messages[index], contextWindow);
|
|
204
|
+
if (!measurement) continue;
|
|
205
|
+
const expectedProvider = typeof expectedModel?.provider === "string" ? expectedModel.provider : null;
|
|
206
|
+
const expectedId = typeof expectedModel?.id === "string" ? expectedModel.id : null;
|
|
207
|
+
if ((expectedProvider && measurement.provider !== expectedProvider) ||
|
|
208
|
+
(expectedId && measurement.model !== expectedId)) return null;
|
|
209
|
+
return measurement;
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function contextWindowFor(ctx: any): number | null {
|
|
215
|
+
const hostWindow = ownValue(ctx.getContextUsage?.(), "contextWindow");
|
|
216
|
+
if (typeof hostWindow === "number" && Number.isFinite(hostWindow) && hostWindow > 0) return hostWindow;
|
|
217
|
+
const modelWindow = ownValue(ctx?.model, "contextWindow");
|
|
218
|
+
return typeof modelWindow === "number" && Number.isFinite(modelWindow) && modelWindow > 0 ? modelWindow : null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function toolFoldCadence(contextWindow: number): number {
|
|
222
|
+
if (!Number.isFinite(contextWindow) || contextWindow <= 0) {
|
|
223
|
+
throw new Error("Tool-fold cadence requires a positive context window");
|
|
224
|
+
}
|
|
225
|
+
return Math.max(TOOL_FOLD_CADENCE_MIN_TOKENS, TOOL_FOLD_CADENCE_WINDOW_FRACTION * contextWindow);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function rootFolds(state: ActiveContextState): ActiveFold[] {
|
|
229
|
+
return state.folds.filter((fold) => fold.parentId === null);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function foldInterval(fold: ActiveFold, state: ActiveContextState, snapshot: ActiveContextSnapshot): { start: number; end: number } | null {
|
|
233
|
+
const refs = flattenFoldRefs(fold, state);
|
|
234
|
+
const indices = refsInOrder(snapshot, refs);
|
|
235
|
+
return indices ? { start: indices[0], end: indices.at(-1)! } : null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function orderedRoots(state: ActiveContextState, snapshot: ActiveContextSnapshot): Array<{ fold: ActiveFold; start: number; end: number }> {
|
|
239
|
+
return rootFolds(state).flatMap((fold) => {
|
|
240
|
+
const interval = foldInterval(fold, state, snapshot);
|
|
241
|
+
return interval ? [{ fold, ...interval }] : [];
|
|
242
|
+
}).sort((left, right) => left.start - right.start);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function visibleCollapsedRoots(state: ActiveContextState, snapshot: ActiveContextSnapshot) {
|
|
246
|
+
return orderedRoots(state, snapshot).filter(({ fold }) => !state.expanded.includes(fold.id));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function persistenceProjection(state: ActiveContextState, snapshot: ActiveContextSnapshot): ActiveContextState {
|
|
250
|
+
const byId = foldMap(state);
|
|
251
|
+
const retained = new Set<string>();
|
|
252
|
+
const retain = (fold: ActiveFold): void => {
|
|
253
|
+
if (retained.has(fold.id)) return;
|
|
254
|
+
retained.add(fold.id);
|
|
255
|
+
for (const childId of childFoldIds(fold)) {
|
|
256
|
+
const child = byId.get(childId);
|
|
257
|
+
if (!child) throw new Error(`Missing active-context child ${childId}`);
|
|
258
|
+
retain(child);
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
for (const root of orderedRoots(state, snapshot)) retain(root.fold);
|
|
262
|
+
const mapped = new Set(snapshot.branchObjects.map((item) => objectRefKey(item.ref)));
|
|
263
|
+
const projected: ActiveContextState = {
|
|
264
|
+
...clone(state),
|
|
265
|
+
folds: state.folds.filter((fold) => retained.has(fold.id)),
|
|
266
|
+
expanded: state.expanded.filter((id) => retained.has(id)),
|
|
267
|
+
protected: state.protected.filter((ref) => mapped.has(objectRefKey(ref))),
|
|
268
|
+
leases: Object.fromEntries(Object.entries(state.leases)
|
|
269
|
+
.filter(([id]) => retained.has(id))),
|
|
270
|
+
};
|
|
271
|
+
if (projected.prepared && projected.prepared.sourceRefs.some((ref) => !mapped.has(objectRefKey(ref)))) {
|
|
272
|
+
delete projected.prepared;
|
|
273
|
+
}
|
|
274
|
+
projected.folds = deriveFoldParents(projected.folds);
|
|
275
|
+
return parseActiveContextState(projected, state.sessionId);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function explicitProtectedKeys(state: ActiveContextState): Set<string> {
|
|
279
|
+
return new Set(state.protected.map(objectRefKey));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function refsProtected(
|
|
283
|
+
refs: EvidenceRef[],
|
|
284
|
+
state: ActiveContextState,
|
|
285
|
+
snapshot: ActiveContextSnapshot,
|
|
286
|
+
): boolean {
|
|
287
|
+
const explicit = explicitProtectedKeys(state);
|
|
288
|
+
return refs.some((ref) => {
|
|
289
|
+
const item = exactMapped(snapshot, ref);
|
|
290
|
+
return !item || explicit.has(objectRefKey(ref)) || snapshot.protectedIndices.has(item.index);
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function toolRefsProtected(
|
|
295
|
+
refs: EvidenceRef[],
|
|
296
|
+
state: ActiveContextState,
|
|
297
|
+
snapshot: ActiveContextSnapshot,
|
|
298
|
+
): boolean {
|
|
299
|
+
const explicit = explicitProtectedKeys(state);
|
|
300
|
+
return refs.some((ref) => {
|
|
301
|
+
const item = exactMapped(snapshot, ref);
|
|
302
|
+
return !item || explicit.has(objectRefKey(ref)) || snapshot.toolProtectedIndices.has(item.index);
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function toolPayload(value: unknown): { content: Array<{ type: "text"; text: string }>; details: unknown } {
|
|
307
|
+
return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }], details: value };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function boundedInteger(value: unknown, fallback: number, minimum: number, maximum: number, label: string): number {
|
|
311
|
+
if (value === undefined) return fallback;
|
|
312
|
+
if (!Number.isSafeInteger(value) || Number(value) < minimum || Number(value) > maximum) {
|
|
313
|
+
throw new Error(`${label} must be an integer in [${minimum}, ${maximum}]`);
|
|
314
|
+
}
|
|
315
|
+
return Number(value);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function stringIds(value: unknown): string[] {
|
|
319
|
+
if (!Array.isArray(value) || !value.length || value.length > 64) throw new Error("ids must contain 1-64 values");
|
|
320
|
+
const ids = value.map((item) => String(item).trim());
|
|
321
|
+
if (ids.some((id) => !id)) throw new Error("ids must be nonempty strings");
|
|
322
|
+
return ids;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export interface NativeCompactionDecisionReceipt {
|
|
326
|
+
version: 1;
|
|
327
|
+
decisionKey: string;
|
|
328
|
+
sessionId: string;
|
|
329
|
+
triggerEntryId: string;
|
|
330
|
+
reason: string;
|
|
331
|
+
willRetry: boolean;
|
|
332
|
+
failureCode: string;
|
|
333
|
+
message: string;
|
|
334
|
+
preparationError: string | null;
|
|
335
|
+
boundaryFailure: string | null;
|
|
336
|
+
selectionKind: string | null;
|
|
337
|
+
selectionSourceIds: string[];
|
|
338
|
+
automaticActionKind: string | null;
|
|
339
|
+
providerMessageSha256: string | null;
|
|
340
|
+
occurredAt: number;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export interface NativeCompactionCompletionReceipt {
|
|
344
|
+
version: 2;
|
|
345
|
+
receiptKey: string;
|
|
346
|
+
sessionId: string;
|
|
347
|
+
compactionEntryId: string;
|
|
348
|
+
reason: string;
|
|
349
|
+
willRetry: boolean;
|
|
350
|
+
fromExtension: boolean;
|
|
351
|
+
occurredAt: number;
|
|
352
|
+
goal: "zero-native-compactions";
|
|
353
|
+
decision: NativeCompactionDecisionReceipt;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export function boundedReceiptString(value: unknown, maximum = 1_200): value is string {
|
|
357
|
+
return typeof value === "string" && value.length > 0 && Buffer.byteLength(value, "utf8") <= maximum;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function boundReceiptText(value: unknown, maximum: number, fallback: string): string {
|
|
361
|
+
const text = typeof value === "string" && value ? value : fallback;
|
|
362
|
+
if (Buffer.byteLength(text, "utf8") <= maximum) return text;
|
|
363
|
+
let output = "";
|
|
364
|
+
for (const character of text) {
|
|
365
|
+
if (Buffer.byteLength(output + character, "utf8") > maximum) break;
|
|
366
|
+
output += character;
|
|
367
|
+
}
|
|
368
|
+
return output || fallback.slice(0, maximum);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function parseNativeCompactionDecision(value: unknown, expectedSessionId?: string): NativeCompactionDecisionReceipt {
|
|
372
|
+
const keys = [
|
|
373
|
+
"version", "decisionKey", "sessionId", "triggerEntryId", "reason", "willRetry", "failureCode", "message",
|
|
374
|
+
"preparationError", "boundaryFailure", "selectionKind", "selectionSourceIds",
|
|
375
|
+
"automaticActionKind", "providerMessageSha256", "occurredAt",
|
|
376
|
+
];
|
|
377
|
+
if (!exactRecord(value, keys) || ownValue(value, "version") !== 1 ||
|
|
378
|
+
typeof ownValue(value, "decisionKey") !== "string" || !/^[a-f0-9]{64}$/.test(String(ownValue(value, "decisionKey"))) ||
|
|
379
|
+
!boundedReceiptString(ownValue(value, "sessionId"), 512) ||
|
|
380
|
+
!boundedReceiptString(ownValue(value, "triggerEntryId"), 512) ||
|
|
381
|
+
(expectedSessionId && ownValue(value, "sessionId") !== expectedSessionId) ||
|
|
382
|
+
!boundedReceiptString(ownValue(value, "reason"), 64) || !boundedReceiptString(ownValue(value, "failureCode"), 128) ||
|
|
383
|
+
!boundedReceiptString(ownValue(value, "message")) || typeof ownValue(value, "willRetry") !== "boolean" ||
|
|
384
|
+
!Number.isSafeInteger(ownValue(value, "occurredAt")) || Number(ownValue(value, "occurredAt")) < 0) {
|
|
385
|
+
throw new Error("Invalid native compaction decision receipt");
|
|
386
|
+
}
|
|
387
|
+
for (const key of ["preparationError", "boundaryFailure", "selectionKind", "automaticActionKind"] as const) {
|
|
388
|
+
const field = ownValue(value, key);
|
|
389
|
+
if (field !== null && !boundedReceiptString(field, 1_200)) throw new Error(`Invalid native compaction decision ${key}`);
|
|
390
|
+
}
|
|
391
|
+
const providerMessageSha256 = ownValue(value, "providerMessageSha256");
|
|
392
|
+
const sourceIds = denseOwnArrayValues(ownValue(value, "selectionSourceIds"));
|
|
393
|
+
if ((providerMessageSha256 !== null && (typeof providerMessageSha256 !== "string" || !/^[a-f0-9]{64}$/.test(providerMessageSha256))) ||
|
|
394
|
+
!sourceIds || sourceIds.length > 64 || sourceIds.some((id) => !boundedReceiptString(id, 512))) {
|
|
395
|
+
throw new Error("Invalid native compaction decision evidence");
|
|
396
|
+
}
|
|
397
|
+
const sessionId = String(ownValue(value, "sessionId"));
|
|
398
|
+
const triggerEntryId = String(ownValue(value, "triggerEntryId"));
|
|
399
|
+
const reason = String(ownValue(value, "reason"));
|
|
400
|
+
const failureCode = String(ownValue(value, "failureCode"));
|
|
401
|
+
if (ownValue(value, "decisionKey") !== sha256Value({ sessionId, triggerEntryId, reason, failureCode })) {
|
|
402
|
+
throw new Error("Native compaction decision identity drift");
|
|
403
|
+
}
|
|
404
|
+
return clone(value) as unknown as NativeCompactionDecisionReceipt;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export function parseNativeCompactionCompletion(value: unknown, expectedSessionId?: string): NativeCompactionCompletionReceipt {
|
|
408
|
+
const keys = [
|
|
409
|
+
"version", "receiptKey", "sessionId", "compactionEntryId", "reason", "willRetry",
|
|
410
|
+
"fromExtension", "occurredAt", "goal", "decision",
|
|
411
|
+
];
|
|
412
|
+
if (!exactRecord(value, keys) || ownValue(value, "version") !== 2 ||
|
|
413
|
+
typeof ownValue(value, "receiptKey") !== "string" || !/^[a-f0-9]{64}$/.test(String(ownValue(value, "receiptKey"))) ||
|
|
414
|
+
!boundedReceiptString(ownValue(value, "sessionId"), 512) ||
|
|
415
|
+
(expectedSessionId && ownValue(value, "sessionId") !== expectedSessionId) ||
|
|
416
|
+
!boundedReceiptString(ownValue(value, "compactionEntryId"), 512) ||
|
|
417
|
+
!boundedReceiptString(ownValue(value, "reason"), 64) || typeof ownValue(value, "willRetry") !== "boolean" ||
|
|
418
|
+
typeof ownValue(value, "fromExtension") !== "boolean" || !Number.isSafeInteger(ownValue(value, "occurredAt")) ||
|
|
419
|
+
ownValue(value, "goal") !== "zero-native-compactions") {
|
|
420
|
+
throw new Error("Invalid native compaction completion receipt");
|
|
421
|
+
}
|
|
422
|
+
const sessionId = String(ownValue(value, "sessionId"));
|
|
423
|
+
const compactionEntryId = String(ownValue(value, "compactionEntryId"));
|
|
424
|
+
const decision = parseNativeCompactionDecision(ownValue(value, "decision"), sessionId);
|
|
425
|
+
if (ownValue(value, "receiptKey") !== sha256Value({ sessionId, compactionEntryId })) {
|
|
426
|
+
throw new Error("Native compaction completion receipt identity drift");
|
|
427
|
+
}
|
|
428
|
+
return { ...(clone(value) as unknown as NativeCompactionCompletionReceipt), decision };
|
|
429
|
+
}
|