pi-fold 1.0.2 → 1.0.4
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 +29 -40
- package/extensions/active-context.ts +2408 -600
- package/extensions/evidence.js +15 -2
- package/extensions/index.js +69 -21
- package/extensions/json.ts +33 -1
- package/extensions/lib/curation.ts +141 -139
- package/extensions/lib/folding.ts +418 -74
- package/extensions/lib/instrumentation.ts +24 -4
- package/extensions/lib/measurement.ts +92 -16
- package/extensions/lib/persistence.ts +212 -42
- package/extensions/lib/policy.ts +511 -155
- package/extensions/lib/rollback.ts +216 -0
- package/extensions/lib/scheduling.ts +414 -106
- package/extensions/lib/selection.ts +528 -144
- package/extensions/lib/surfacing.ts +479 -224
- package/extensions/lib/tool-surface.ts +25 -5
- package/extensions/lib/transcript.ts +146 -62
- package/extensions/summarizer.js +206 -10
- package/package.json +1 -1
- package/extensions/lib/advisory.ts +0 -309
|
@@ -22,8 +22,11 @@ import {
|
|
|
22
22
|
EXPAND_LEASE_GENERATIONS,
|
|
23
23
|
MAX_ADVISORY_DELIVERIES_PER_MILESTONE,
|
|
24
24
|
MAX_EXPAND_LEASES,
|
|
25
|
+
MAX_LAST_CALL_TEXT_BYTES,
|
|
25
26
|
MAX_PENDING_MARKS,
|
|
26
|
-
|
|
27
|
+
MAX_THRESHOLD_NOTICE_TEXT_BYTES,
|
|
28
|
+
MAX_THRESHOLD_NOTICES,
|
|
29
|
+
SURFACING_MAX_LEDGER_RECORDS,
|
|
27
30
|
} from "./policy.ts";
|
|
28
31
|
import type {
|
|
29
32
|
ActiveContextCheckpointV2,
|
|
@@ -32,6 +35,7 @@ import type {
|
|
|
32
35
|
ActiveContextStateWireV2,
|
|
33
36
|
ActiveFold,
|
|
34
37
|
AdvisoryMilestone,
|
|
38
|
+
BriefOverride,
|
|
35
39
|
BriefProvenance,
|
|
36
40
|
FoldKind,
|
|
37
41
|
FoldPart,
|
|
@@ -54,8 +58,8 @@ export const PREPARED_FOLD_KEYS = [
|
|
|
54
58
|
export const ACTIVE_STATE_KEYS = ["version", "sessionId", "revision", "folds", "expanded", "protected"] as const;
|
|
55
59
|
export const FOLD_RECORD_REF_KEYS = ["id", "sha256"] as const;
|
|
56
60
|
export const FOLD_RECORD_ENTRY_KEYS = ["version", "sessionId", "foldId", "recordSha256", "fold"] as const;
|
|
57
|
-
export const SURFACING_RECORD_KEYS = ["
|
|
58
|
-
export const SURFACING_OUTCOMES = Object.freeze(["shown", "
|
|
61
|
+
export const SURFACING_RECORD_KEYS = ["id", "surfaced", "taken", "ordinal", "outcome"] as const;
|
|
62
|
+
export const SURFACING_OUTCOMES = Object.freeze(["shown", "acted", "used", "ignored"] as const);
|
|
59
63
|
export const PENDING_FOLD_MARK_KEYS = [
|
|
60
64
|
"mark", "id", "kind", "parts", "brief", "briefProvenance", "origin", "ordinal",
|
|
61
65
|
] as const;
|
|
@@ -81,18 +85,21 @@ export function validTokensSinceToolFold(value: unknown): value is number {
|
|
|
81
85
|
|
|
82
86
|
export function validSurfacingRecord(value: unknown): value is SurfacingRecord {
|
|
83
87
|
if (!exactRecord(value, SURFACING_RECORD_KEYS)) return false;
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
const surfaced = ownValue(value, "surfaced");
|
|
89
|
+
const taken = ownValue(value, "taken");
|
|
90
|
+
return typeof ownValue(value, "id") === "string" && Boolean(ownValue(value, "id")) &&
|
|
91
|
+
Number.isSafeInteger(surfaced) && Number(surfaced) > 0 &&
|
|
92
|
+
Number.isSafeInteger(taken) && Number(taken) >= 0 && Number(taken) <= Number(surfaced) &&
|
|
88
93
|
Number.isSafeInteger(ownValue(value, "ordinal")) && Number(ownValue(value, "ordinal")) >= 0 &&
|
|
89
94
|
SURFACING_OUTCOMES.includes(ownValue(value, "outcome") as SurfacingRecord["outcome"]);
|
|
90
95
|
}
|
|
91
96
|
|
|
97
|
+
/** One record per fold: a duplicate id is a ledger that forgot an ignore. */
|
|
92
98
|
export function parseSurfacingLog(value: unknown): SurfacingRecord[] {
|
|
93
99
|
const records = denseOwnArrayValues(value);
|
|
94
|
-
if (!records || records.length >
|
|
95
|
-
|
|
100
|
+
if (!records || records.length > SURFACING_MAX_LEDGER_RECORDS || !records.every(validSurfacingRecord) ||
|
|
101
|
+
new Set(records.map((record) => (record as SurfacingRecord).id)).size !== records.length) {
|
|
102
|
+
throw new Error("Invalid active-context surfacing ledger");
|
|
96
103
|
}
|
|
97
104
|
return clone(records) as SurfacingRecord[];
|
|
98
105
|
}
|
|
@@ -125,20 +132,41 @@ export function validPendingMark(value: unknown): value is PendingMark {
|
|
|
125
132
|
* supplied brief obeys, and refused for a fold that is not in the forest, exactly
|
|
126
133
|
* like a pin or a refold mark naming a missing fold.
|
|
127
134
|
*/
|
|
135
|
+
/**
|
|
136
|
+
* A model brief written after the fold committed carries its generator with it, so an
|
|
137
|
+
* override is either the bare corrected string the agent wrote or that string beside the
|
|
138
|
+
* attribution. Two shapes, one map: the string form is what every state written before
|
|
139
|
+
* the upgrade lane existed holds, and it must keep parsing and keep its exact digest.
|
|
140
|
+
*/
|
|
141
|
+
export function briefOverrideText(override: unknown): string | null {
|
|
142
|
+
if (typeof override === "string") return override;
|
|
143
|
+
const brief = ownValue(override, "brief");
|
|
144
|
+
return typeof brief === "string" ? brief : null;
|
|
145
|
+
}
|
|
146
|
+
|
|
128
147
|
export function parseBriefOverrides(
|
|
129
148
|
value: unknown,
|
|
130
149
|
ids?: ReadonlySet<string>,
|
|
131
|
-
): Record<string,
|
|
150
|
+
): Record<string, BriefOverride> {
|
|
132
151
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
133
152
|
throw new Error("Invalid active-context brief overrides");
|
|
134
153
|
}
|
|
135
154
|
const entries = Object.entries(value as Record<string, unknown>);
|
|
155
|
+
const validOverride = (override: unknown): boolean => {
|
|
156
|
+
const brief = briefOverrideText(override);
|
|
157
|
+
if (typeof brief !== "string" || !brief.trim() ||
|
|
158
|
+
brief.length > ACTIVE_CONTEXT_POLICY.maxBriefChars) return false;
|
|
159
|
+
if (typeof override === "string") return true;
|
|
160
|
+
return exactRecord(override, ["brief", "provenance"]) &&
|
|
161
|
+
validProvenance(ownValue(override, "provenance"));
|
|
162
|
+
};
|
|
136
163
|
if (entries.length > MAX_ACTIVE_FOLD_RECORDS ||
|
|
137
|
-
entries.some(([id,
|
|
138
|
-
brief.length > ACTIVE_CONTEXT_POLICY.maxBriefChars || (ids && !ids.has(id)))) {
|
|
164
|
+
entries.some(([id, override]) => !id || !validOverride(override) || (ids && !ids.has(id)))) {
|
|
139
165
|
throw new Error("Invalid active-context brief overrides");
|
|
140
166
|
}
|
|
141
|
-
return Object.fromEntries(
|
|
167
|
+
return Object.fromEntries(
|
|
168
|
+
entries.sort(([left], [right]) => left.localeCompare(right)),
|
|
169
|
+
) as Record<string, BriefOverride>;
|
|
142
170
|
}
|
|
143
171
|
|
|
144
172
|
/** The brief a fold PRESENTS: the agent's correction when it made one, else the record's. */
|
|
@@ -146,7 +174,23 @@ export function foldBrief(
|
|
|
146
174
|
fold: Pick<ActiveFold, "id" | "brief">,
|
|
147
175
|
state: Pick<ActiveContextState, "briefs">,
|
|
148
176
|
): string {
|
|
149
|
-
return state.briefs?.[fold.id] ?? fold.brief;
|
|
177
|
+
return briefOverrideText(state.briefs?.[fold.id]) ?? fold.brief;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* The provenance a fold PRESENTS. A fold RECORD is immutable, so a brief written after
|
|
182
|
+
* the commit lives in the override map and its attribution travels with it; without this
|
|
183
|
+
* lens a model-written upgrade would report the provenance of the brief it replaced.
|
|
184
|
+
*/
|
|
185
|
+
export function foldProvenance(
|
|
186
|
+
fold: Pick<ActiveFold, "id" | "provenance">,
|
|
187
|
+
state: Pick<ActiveContextState, "briefs">,
|
|
188
|
+
): BriefProvenance {
|
|
189
|
+
const override = state.briefs?.[fold.id];
|
|
190
|
+
const provenance = typeof override === "object" && override
|
|
191
|
+
? ownValue(override, "provenance")
|
|
192
|
+
: undefined;
|
|
193
|
+
return normalizeLegacyProvenance(provenance ?? fold.provenance);
|
|
150
194
|
}
|
|
151
195
|
|
|
152
196
|
export function parsePendingMarks(value: unknown): PendingMark[] {
|
|
@@ -237,7 +281,12 @@ export function foldMap(state: Pick<ActiveContextState, "folds">): Map<string, A
|
|
|
237
281
|
return new Map(state.folds.map((fold) => [fold.id, fold]));
|
|
238
282
|
}
|
|
239
283
|
|
|
240
|
-
|
|
284
|
+
/**
|
|
285
|
+
* The folds a span names as children. Read off the parts alone, so a PENDING fold mark
|
|
286
|
+
* answers it too: a mark carries the id its committed fold will get, so the ids here are
|
|
287
|
+
* exactly the folds that must already exist when this span applies.
|
|
288
|
+
*/
|
|
289
|
+
export function childFoldIds(fold: Pick<ActiveFold, "parts">): string[] {
|
|
241
290
|
return fold.parts.filter((part): part is Extract<FoldPart, { kind: "fold" }> => part.kind === "fold")
|
|
242
291
|
.map((part) => part.foldId);
|
|
243
292
|
}
|
|
@@ -260,6 +309,33 @@ export function flattenFoldRefs(fold: ActiveFold, state: Pick<ActiveContextState
|
|
|
260
309
|
return visit(fold);
|
|
261
310
|
}
|
|
262
311
|
|
|
312
|
+
/**
|
|
313
|
+
* Which fold DIRECTLY owns each raw evidence key.
|
|
314
|
+
*
|
|
315
|
+
* One derivation, two readers (Shane 2026-08-11). The forest invariant below reads it to
|
|
316
|
+
* reject a second owner, and the agent-facing candidate builder reads it BEFORE it builds
|
|
317
|
+
* anything, so a span naming evidence some fold already holds comes back as a refusal
|
|
318
|
+
* that names the owner instead of this invariant escaping the tool call as an exception.
|
|
319
|
+
*
|
|
320
|
+
* The two readings stay distinguishable because the input differs. Called on the folds
|
|
321
|
+
* already in state it can only throw when the stored forest is itself corrupt, which no
|
|
322
|
+
* agent input can produce: every state reaches storage through `validateFoldForest`.
|
|
323
|
+
* Called on a forest that includes a candidate the caller just built, a duplicate means
|
|
324
|
+
* the REQUEST collided with an existing fold, and that is the refusal case.
|
|
325
|
+
*/
|
|
326
|
+
export function directFoldOwners(folds: readonly ActiveFold[]): Map<string, string> {
|
|
327
|
+
const owner = new Map<string, string>();
|
|
328
|
+
for (const fold of folds) {
|
|
329
|
+
for (const part of fold.parts) {
|
|
330
|
+
if (part.kind !== "raw") continue;
|
|
331
|
+
const key = objectRefKey(part.ref);
|
|
332
|
+
if (owner.has(key)) throw new Error(`Evidence ${part.ref.entryId} has multiple direct fold owners`);
|
|
333
|
+
owner.set(key, fold.id);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return owner;
|
|
337
|
+
}
|
|
338
|
+
|
|
263
339
|
export function validateFoldForest(folds: ActiveFold[]): ActiveFold[] {
|
|
264
340
|
const rawValues = denseOwnArrayValues(folds);
|
|
265
341
|
if (!rawValues || rawValues.some((fold) => !validFoldShape(fold))) {
|
|
@@ -275,20 +351,28 @@ export function validateFoldForest(folds: ActiveFold[]): ActiveFold[] {
|
|
|
275
351
|
(!fold.parts.length || fold.parts.some((part) => part.kind !== "raw" || part.ref.role !== "toolResult"))) {
|
|
276
352
|
throw new Error("Tool-result fold must own one validated assistant batch of tool results");
|
|
277
353
|
}
|
|
278
|
-
|
|
279
|
-
|
|
354
|
+
// WHAT A PARENT IS, RE-DERIVED UNDER THE COUNT LAW (Shane 2026-08-10).
|
|
355
|
+
//
|
|
356
|
+
// Two shape rules stood here and both described the old approximation. "Only child
|
|
357
|
+
// folds" forbade the raw gaps between children, and a parent's span runs from its
|
|
358
|
+
// first child to its last one, so the gap material inside it is the parent's or the
|
|
359
|
+
// span is not a span. "Only chapter or consolidation children" forbade tool-result
|
|
360
|
+
// children, which was true while a bare tool batch nested through a chapter span;
|
|
361
|
+
// automatic chapters no longer take placeholders at any count, so that rule would
|
|
362
|
+
// now leave every tool fold as an obstacle no parent could ever reclaim.
|
|
363
|
+
//
|
|
364
|
+
// What survives is the one thing that makes a consolidation a consolidation: it
|
|
365
|
+
// groups folds. A parent with no child fold is a chapter wearing the wrong name.
|
|
366
|
+
if (fold.kind === "consolidation" && !fold.parts.some((part) => part.kind === "fold")) {
|
|
367
|
+
throw new Error("Consolidation folds must contain at least one child fold");
|
|
280
368
|
}
|
|
281
369
|
byId.set(fold.id, fold);
|
|
282
370
|
}
|
|
371
|
+
directFoldOwners(values);
|
|
283
372
|
const parent = new Map<string, string>();
|
|
284
|
-
const rawOwner = new Map<string, string>();
|
|
285
373
|
for (const fold of values) {
|
|
286
374
|
for (const part of fold.parts) {
|
|
287
|
-
if (part.kind === "
|
|
288
|
-
const key = objectRefKey(part.ref);
|
|
289
|
-
if (rawOwner.has(key)) throw new Error(`Evidence ${part.ref.entryId} has multiple direct fold owners`);
|
|
290
|
-
rawOwner.set(key, fold.id);
|
|
291
|
-
} else {
|
|
375
|
+
if (part.kind === "fold") {
|
|
292
376
|
if (!byId.has(part.foldId) || part.foldId === fold.id || parent.has(part.foldId)) {
|
|
293
377
|
throw new Error(`Invalid or multiply owned child fold ${part.foldId}`);
|
|
294
378
|
}
|
|
@@ -298,15 +382,6 @@ export function validateFoldForest(folds: ActiveFold[]): ActiveFold[] {
|
|
|
298
382
|
}
|
|
299
383
|
for (const fold of values) {
|
|
300
384
|
if ((parent.get(fold.id) ?? null) !== fold.parentId) throw new Error(`Fold ${fold.id} parent drift`);
|
|
301
|
-
for (const childId of childFoldIds(fold)) {
|
|
302
|
-
const child = byId.get(childId)!;
|
|
303
|
-
if (fold.kind === "chapter" && child.kind !== "tool-result") {
|
|
304
|
-
throw new Error(`Chapter ${fold.id} may contain only tool-result folds`);
|
|
305
|
-
}
|
|
306
|
-
if (fold.kind === "consolidation" && child.kind === "tool-result") {
|
|
307
|
-
throw new Error(`Consolidation ${fold.id} may contain only chapter or consolidation folds`);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
385
|
}
|
|
311
386
|
for (const fold of values) {
|
|
312
387
|
const refs = flattenFoldRefs(fold, { folds: values });
|
|
@@ -334,6 +409,9 @@ export function parseActiveContextState(
|
|
|
334
409
|
// rejecting it here would suspend automatic management on upgrade.
|
|
335
410
|
const hasPinnedPeeks = recordLike && Object.prototype.hasOwnProperty.call(value, "pinnedPeeks");
|
|
336
411
|
const hasBriefs = recordLike && Object.prototype.hasOwnProperty.call(value, "briefs");
|
|
412
|
+
const hasRider = recordLike && Object.prototype.hasOwnProperty.call(value, "rider");
|
|
413
|
+
const hasLastCall = recordLike && Object.prototype.hasOwnProperty.call(value, "lastCall");
|
|
414
|
+
const hasNotices = recordLike && Object.prototype.hasOwnProperty.call(value, "notices");
|
|
337
415
|
const extraKeys = [
|
|
338
416
|
...(hasPrepared ? ["prepared"] : []),
|
|
339
417
|
...(hasAdvisory ? ["advisory"] : []),
|
|
@@ -343,6 +421,9 @@ export function parseActiveContextState(
|
|
|
343
421
|
...(hasPendingMarks ? ["pendingMarks"] : []),
|
|
344
422
|
...(hasPinnedPeeks ? ["pinnedPeeks"] : []),
|
|
345
423
|
...(hasBriefs ? ["briefs"] : []),
|
|
424
|
+
...(hasRider ? ["rider"] : []),
|
|
425
|
+
...(hasLastCall ? ["lastCall"] : []),
|
|
426
|
+
...(hasNotices ? ["notices"] : []),
|
|
346
427
|
];
|
|
347
428
|
if (!exactRecord(value, [...ACTIVE_STATE_KEYS, ...extraKeys])) throw new Error("Invalid active-context state keys");
|
|
348
429
|
const folds = denseOwnArrayValues(ownValue(value, "folds"));
|
|
@@ -364,6 +445,15 @@ export function parseActiveContextState(
|
|
|
364
445
|
if (hasAdvisory && !validAdvisoryState(ownValue(value, "advisory"))) {
|
|
365
446
|
throw new Error("Invalid active-context advisory state");
|
|
366
447
|
}
|
|
448
|
+
if (hasRider && !validRiderState(ownValue(value, "rider"))) {
|
|
449
|
+
throw new Error("Invalid active-context rider state");
|
|
450
|
+
}
|
|
451
|
+
if (hasLastCall && !validLastCallState(ownValue(value, "lastCall"))) {
|
|
452
|
+
throw new Error("Invalid active-context last-call state");
|
|
453
|
+
}
|
|
454
|
+
if (hasNotices && !validNoticesState(ownValue(value, "notices"))) {
|
|
455
|
+
throw new Error("Invalid active-context notices state");
|
|
456
|
+
}
|
|
367
457
|
if (hasTokensSinceToolFold && !validTokensSinceToolFold(ownValue(value, "tokensSinceToolFold"))) {
|
|
368
458
|
throw new Error("Invalid active-context tool-fold cadence");
|
|
369
459
|
}
|
|
@@ -397,6 +487,9 @@ export function parseActiveContextState(
|
|
|
397
487
|
...(hasAdvisory
|
|
398
488
|
? { advisory: clone(source.advisory!) }
|
|
399
489
|
: defaultAdvisory ? { advisory: { highWater: 0, delivered: {} } } : {}),
|
|
490
|
+
...(hasRider ? { rider: clone(source.rider!) } : {}),
|
|
491
|
+
...(hasLastCall ? { lastCall: clone(source.lastCall!) } : {}),
|
|
492
|
+
...(hasNotices ? { notices: clone(source.notices!) } : {}),
|
|
400
493
|
...(hasPrepared ? { prepared: clone(source.prepared!) } : {}),
|
|
401
494
|
};
|
|
402
495
|
}
|
|
@@ -515,11 +608,17 @@ export function validateV2ProjectionFields(
|
|
|
515
608
|
surfacingValue?: unknown,
|
|
516
609
|
pendingMarksValue?: unknown,
|
|
517
610
|
briefsValue?: unknown,
|
|
611
|
+
riderValue?: unknown,
|
|
612
|
+
lastCallValue?: unknown,
|
|
613
|
+
noticesValue?: unknown,
|
|
518
614
|
): {
|
|
519
615
|
expanded: string[];
|
|
520
616
|
protected: EvidenceRef[];
|
|
521
617
|
prepared?: PreparedFold;
|
|
522
618
|
advisory?: NonNullable<ActiveContextState["advisory"]>;
|
|
619
|
+
rider?: NonNullable<ActiveContextState["rider"]>;
|
|
620
|
+
lastCall?: NonNullable<ActiveContextState["lastCall"]>;
|
|
621
|
+
notices?: NonNullable<ActiveContextState["notices"]>;
|
|
523
622
|
tokensSinceToolFold: number;
|
|
524
623
|
leases: Record<string, number>;
|
|
525
624
|
surfacing: SurfacingRecord[];
|
|
@@ -538,6 +637,15 @@ export function validateV2ProjectionFields(
|
|
|
538
637
|
if (advisoryValue !== undefined && !validAdvisoryState(advisoryValue)) {
|
|
539
638
|
throw new Error("Invalid active-context v2 advisory state");
|
|
540
639
|
}
|
|
640
|
+
if (riderValue !== undefined && !validRiderState(riderValue)) {
|
|
641
|
+
throw new Error("Invalid active-context v2 rider state");
|
|
642
|
+
}
|
|
643
|
+
if (lastCallValue !== undefined && !validLastCallState(lastCallValue)) {
|
|
644
|
+
throw new Error("Invalid active-context v2 last-call state");
|
|
645
|
+
}
|
|
646
|
+
if (noticesValue !== undefined && !validNoticesState(noticesValue)) {
|
|
647
|
+
throw new Error("Invalid active-context v2 notices state");
|
|
648
|
+
}
|
|
541
649
|
if (tokensSinceToolFoldValue !== undefined && !validTokensSinceToolFold(tokensSinceToolFoldValue)) {
|
|
542
650
|
throw new Error("Invalid active-context v2 tool-fold cadence");
|
|
543
651
|
}
|
|
@@ -552,6 +660,15 @@ export function validateV2ProjectionFields(
|
|
|
552
660
|
...(advisoryValue === undefined ? {} : {
|
|
553
661
|
advisory: clone(advisoryValue) as NonNullable<ActiveContextState["advisory"]>,
|
|
554
662
|
}),
|
|
663
|
+
...(riderValue === undefined ? {} : {
|
|
664
|
+
rider: clone(riderValue) as NonNullable<ActiveContextState["rider"]>,
|
|
665
|
+
}),
|
|
666
|
+
...(lastCallValue === undefined ? {} : {
|
|
667
|
+
lastCall: clone(lastCallValue) as NonNullable<ActiveContextState["lastCall"]>,
|
|
668
|
+
}),
|
|
669
|
+
...(noticesValue === undefined ? {} : {
|
|
670
|
+
notices: clone(noticesValue) as NonNullable<ActiveContextState["notices"]>,
|
|
671
|
+
}),
|
|
555
672
|
tokensSinceToolFold: tokensSinceToolFoldValue === undefined ? 0 : Number(tokensSinceToolFoldValue),
|
|
556
673
|
leases,
|
|
557
674
|
surfacing,
|
|
@@ -580,6 +697,12 @@ export function parseActiveContextStateV2(value: unknown, sessionId: string): Ac
|
|
|
580
697
|
Object.prototype.hasOwnProperty.call(value, "pinnedPeeks"));
|
|
581
698
|
const hasBriefs = Boolean(value && typeof value === "object" &&
|
|
582
699
|
Object.prototype.hasOwnProperty.call(value, "briefs"));
|
|
700
|
+
const hasRider = Boolean(value && typeof value === "object" &&
|
|
701
|
+
Object.prototype.hasOwnProperty.call(value, "rider"));
|
|
702
|
+
const hasLastCall = Boolean(value && typeof value === "object" &&
|
|
703
|
+
Object.prototype.hasOwnProperty.call(value, "lastCall"));
|
|
704
|
+
const hasNotices = Boolean(value && typeof value === "object" &&
|
|
705
|
+
Object.prototype.hasOwnProperty.call(value, "notices"));
|
|
583
706
|
const optionalKeys = [
|
|
584
707
|
...(hasAdvisory ? ["advisory"] : []),
|
|
585
708
|
...(hasTokensSinceToolFold ? ["tokensSinceToolFold"] : []),
|
|
@@ -588,6 +711,9 @@ export function parseActiveContextStateV2(value: unknown, sessionId: string): Ac
|
|
|
588
711
|
...(hasPendingMarks ? ["pendingMarks"] : []),
|
|
589
712
|
...(hasPinnedPeeks ? ["pinnedPeeks"] : []),
|
|
590
713
|
...(hasBriefs ? ["briefs"] : []),
|
|
714
|
+
...(hasRider ? ["rider"] : []),
|
|
715
|
+
...(hasLastCall ? ["lastCall"] : []),
|
|
716
|
+
...(hasNotices ? ["notices"] : []),
|
|
591
717
|
];
|
|
592
718
|
const checkpoint = kind === "checkpoint" &&
|
|
593
719
|
exactRecord(value, [...STATE_CHECKPOINT_V2_KEYS, ...optionalKeys]);
|
|
@@ -603,6 +729,7 @@ export function parseActiveContextStateV2(value: unknown, sessionId: string): Ac
|
|
|
603
729
|
ownValue(value, "expanded"), ownValue(value, "protected"), ownValue(value, "prepared"),
|
|
604
730
|
ownValue(value, "advisory"), ownValue(value, "tokensSinceToolFold"), ownValue(value, "leases"),
|
|
605
731
|
ownValue(value, "surfacing"), ownValue(value, "pendingMarks"), ownValue(value, "briefs"),
|
|
732
|
+
ownValue(value, "rider"), ownValue(value, "lastCall"), ownValue(value, "notices"),
|
|
606
733
|
);
|
|
607
734
|
if (checkpoint) {
|
|
608
735
|
const refs = denseOwnArrayValues(ownValue(value, "foldRefs"));
|
|
@@ -667,7 +794,8 @@ export function stateFromFoldRefs(
|
|
|
667
794
|
wire: Pick<
|
|
668
795
|
ActiveContextCheckpointV2,
|
|
669
796
|
"sessionId" | "revision" | "expanded" | "protected" | "prepared" | "advisory" |
|
|
670
|
-
"tokensSinceToolFold" | "leases" | "surfacing" | "pendingMarks" | "briefs"
|
|
797
|
+
"tokensSinceToolFold" | "leases" | "surfacing" | "pendingMarks" | "briefs" | "rider" |
|
|
798
|
+
"lastCall" | "notices"
|
|
671
799
|
>,
|
|
672
800
|
refs: FoldRecordRef[],
|
|
673
801
|
records: Map<string, FoldRecordEntry>,
|
|
@@ -691,6 +819,9 @@ export function stateFromFoldRefs(
|
|
|
691
819
|
...(wire.pendingMarks?.length ? { pendingMarks: clone(wire.pendingMarks) } : {}),
|
|
692
820
|
...(wire.briefs && Object.keys(wire.briefs).length ? { briefs: clone(wire.briefs) } : {}),
|
|
693
821
|
...(wire.advisory === undefined ? {} : { advisory: clone(wire.advisory) }),
|
|
822
|
+
...(wire.rider === undefined ? {} : { rider: clone(wire.rider) }),
|
|
823
|
+
...(wire.lastCall === undefined ? {} : { lastCall: clone(wire.lastCall) }),
|
|
824
|
+
...(wire.notices === undefined ? {} : { notices: clone(wire.notices) }),
|
|
694
825
|
...(wire.prepared === null || wire.prepared === undefined ? {} : { prepared: clone(wire.prepared) }),
|
|
695
826
|
};
|
|
696
827
|
return parseActiveContextState(state, wire.sessionId, false);
|
|
@@ -843,6 +974,9 @@ export function makeStateCheckpoint(state: ActiveContextState): ActiveContextChe
|
|
|
843
974
|
...(state.pendingMarks?.length ? { pendingMarks: clone(state.pendingMarks) } : {}),
|
|
844
975
|
...(state.briefs && Object.keys(state.briefs).length ? { briefs: clone(state.briefs) } : {}),
|
|
845
976
|
...(state.advisory ? { advisory: clone(state.advisory) } : {}),
|
|
977
|
+
...(state.rider ? { rider: clone(state.rider) } : {}),
|
|
978
|
+
...(state.lastCall ? { lastCall: clone(state.lastCall) } : {}),
|
|
979
|
+
...(state.notices ? { notices: clone(state.notices) } : {}),
|
|
846
980
|
stateSha256: semanticStateSha256(state),
|
|
847
981
|
}, state.sessionId) as ActiveContextCheckpointV2;
|
|
848
982
|
}
|
|
@@ -880,6 +1014,9 @@ export function makeStateDelta(previous: ActiveContextState, next: ActiveContext
|
|
|
880
1014
|
...(next.pendingMarks?.length ? { pendingMarks: clone(next.pendingMarks) } : {}),
|
|
881
1015
|
...(next.briefs && Object.keys(next.briefs).length ? { briefs: clone(next.briefs) } : {}),
|
|
882
1016
|
...(next.advisory ? { advisory: clone(next.advisory) } : {}),
|
|
1017
|
+
...(next.rider ? { rider: clone(next.rider) } : {}),
|
|
1018
|
+
...(next.lastCall ? { lastCall: clone(next.lastCall) } : {}),
|
|
1019
|
+
...(next.notices ? { notices: clone(next.notices) } : {}),
|
|
883
1020
|
stateSha256: semanticStateSha256(next),
|
|
884
1021
|
}, next.sessionId) as ActiveContextDeltaV2;
|
|
885
1022
|
}
|
|
@@ -892,18 +1029,51 @@ export function protectionSha256(state: Pick<ActiveContextState, "protected">):
|
|
|
892
1029
|
return sha256Value([...state.protected].sort((left, right) => objectRefKey(left).localeCompare(objectRefKey(right))));
|
|
893
1030
|
}
|
|
894
1031
|
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
1032
|
+
/**
|
|
1033
|
+
* The milestone vocabulary a persisted `advisory` state field may carry. The delivery
|
|
1034
|
+
* schedule that spent a per-milestone budget is deleted; this list stays because the
|
|
1035
|
+
* state field is written into every state and covered by the state digest, so sealed
|
|
1036
|
+
* runs have to keep parsing. It is its own literal now rather than the key set of the
|
|
1037
|
+
* deleted budget table.
|
|
1038
|
+
*/
|
|
903
1039
|
export const ADVISORY_MILESTONES = Object.freeze(
|
|
904
|
-
|
|
1040
|
+
["orientation", "notice", "tools", "chapters", "urgent"] as AdvisoryMilestone[],
|
|
905
1041
|
);
|
|
906
1042
|
|
|
1043
|
+
/** The rider is LITERAL persisted bytes, so the bound is on the stored text itself. */
|
|
1044
|
+
export const MAX_RIDER_TEXT_BYTES = 4_096;
|
|
1045
|
+
|
|
1046
|
+
export function validRiderState(value: unknown): value is NonNullable<ActiveContextState["rider"]> {
|
|
1047
|
+
if (!exactRecord(value, ["epoch", "text"])) return false;
|
|
1048
|
+
const epoch = ownValue(value, "epoch");
|
|
1049
|
+
const text = ownValue(value, "text");
|
|
1050
|
+
return Number.isSafeInteger(epoch) && Number(epoch) >= 0 &&
|
|
1051
|
+
typeof text === "string" && text.length > 0 && text.length <= MAX_RIDER_TEXT_BYTES;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
export function validLastCallState(value: unknown): value is NonNullable<ActiveContextState["lastCall"]> {
|
|
1055
|
+
if (!exactRecord(value, ["exposure", "ordinal", "contextCalls", "agentMarks", "text"])) return false;
|
|
1056
|
+
const text = ownValue(value, "text");
|
|
1057
|
+
return ["exposure", "ordinal", "contextCalls", "agentMarks"].every((field) =>
|
|
1058
|
+
Number.isSafeInteger(ownValue(value, field)) && Number(ownValue(value, field)) >= 0) &&
|
|
1059
|
+
typeof text === "string" && text.length > 0 && text.length <= MAX_LAST_CALL_TEXT_BYTES;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
export function validNoticesState(value: unknown): value is NonNullable<ActiveContextState["notices"]> {
|
|
1063
|
+
if (!exactRecord(value, ["fired", "ring"])) return false;
|
|
1064
|
+
const fired = denseOwnArrayValues(ownValue(value, "fired"));
|
|
1065
|
+
const ring = denseOwnArrayValues(ownValue(value, "ring"));
|
|
1066
|
+
if (!fired || !ring || ring.length > MAX_THRESHOLD_NOTICES) return false;
|
|
1067
|
+
if (fired.some((share) => typeof share !== "number" || !Number.isFinite(share) || share <= 0 || share >= 1) ||
|
|
1068
|
+
new Set(fired).size !== fired.length) return false;
|
|
1069
|
+
return ring.every((notice) => exactRecord(notice, ["share", "ordinal", "text"]) &&
|
|
1070
|
+
typeof ownValue(notice, "share") === "number" && Number.isFinite(ownValue(notice, "share")) &&
|
|
1071
|
+
Number(ownValue(notice, "share")) > 0 && Number(ownValue(notice, "share")) < 1 &&
|
|
1072
|
+
Number.isSafeInteger(ownValue(notice, "ordinal")) && Number(ownValue(notice, "ordinal")) >= 0 &&
|
|
1073
|
+
typeof ownValue(notice, "text") === "string" && String(ownValue(notice, "text")).length > 0 &&
|
|
1074
|
+
String(ownValue(notice, "text")).length <= MAX_THRESHOLD_NOTICE_TEXT_BYTES);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
907
1077
|
export function validAdvisoryState(value: unknown): value is NonNullable<ActiveContextState["advisory"]> {
|
|
908
1078
|
const hasArmed = Boolean(value && typeof value === "object" &&
|
|
909
1079
|
Object.prototype.hasOwnProperty.call(value, "armed"));
|