pi-fold 0.1.1 → 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 +37 -6
- package/extensions/active-context.ts +2158 -255
- package/extensions/lib/advisory.ts +90 -19
- package/extensions/lib/canonical.ts +12 -0
- package/extensions/lib/curation.ts +437 -0
- package/extensions/lib/folding.ts +388 -21
- package/extensions/lib/instrumentation.ts +333 -0
- package/extensions/lib/measurement.ts +111 -0
- package/extensions/lib/persistence.ts +117 -2
- package/extensions/lib/policy.ts +314 -1
- package/extensions/lib/scheduling.ts +923 -0
- package/extensions/lib/selection.ts +437 -12
- package/extensions/lib/surfacing.ts +23 -6
- package/extensions/lib/tool-surface.ts +50 -4
- package/extensions/lib/transcript.ts +58 -4
- package/package.json +2 -2
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
EXPAND_LEASE_GENERATIONS,
|
|
23
23
|
MAX_ADVISORY_DELIVERIES_PER_MILESTONE,
|
|
24
24
|
MAX_EXPAND_LEASES,
|
|
25
|
+
MAX_PENDING_MARKS,
|
|
25
26
|
SURFACING_MAX_LOG_RECORDS,
|
|
26
27
|
} from "./policy.ts";
|
|
27
28
|
import type {
|
|
@@ -36,6 +37,8 @@ import type {
|
|
|
36
37
|
FoldPart,
|
|
37
38
|
FoldRecordEntry,
|
|
38
39
|
FoldRecordRef,
|
|
40
|
+
MarkOrigin,
|
|
41
|
+
PendingMark,
|
|
39
42
|
PreparedFold,
|
|
40
43
|
SurfacingRecord,
|
|
41
44
|
} from "./policy.ts";
|
|
@@ -53,6 +56,11 @@ export const FOLD_RECORD_REF_KEYS = ["id", "sha256"] as const;
|
|
|
53
56
|
export const FOLD_RECORD_ENTRY_KEYS = ["version", "sessionId", "foldId", "recordSha256", "fold"] as const;
|
|
54
57
|
export const SURFACING_RECORD_KEYS = ["source", "id", "score", "ordinal", "outcome"] as const;
|
|
55
58
|
export const SURFACING_OUTCOMES = Object.freeze(["shown", "accept", "reject"] as const);
|
|
59
|
+
export const PENDING_FOLD_MARK_KEYS = [
|
|
60
|
+
"mark", "id", "kind", "parts", "brief", "briefProvenance", "origin", "ordinal",
|
|
61
|
+
] as const;
|
|
62
|
+
export const PENDING_REFOLD_MARK_KEYS = ["mark", "id", "origin", "ordinal"] as const;
|
|
63
|
+
export const MARK_ORIGINS = Object.freeze(["agent", "ladder"] as const);
|
|
56
64
|
export const STATE_CHECKPOINT_V2_KEYS = [
|
|
57
65
|
"version", "kind", "sessionId", "revision", "foldRefs", "expanded", "protected", "prepared", "stateSha256",
|
|
58
66
|
] as const;
|
|
@@ -89,6 +97,67 @@ export function parseSurfacingLog(value: unknown): SurfacingRecord[] {
|
|
|
89
97
|
return clone(records) as SurfacingRecord[];
|
|
90
98
|
}
|
|
91
99
|
|
|
100
|
+
/** Stable identity of one pending mark: its phase plus the fold it names. */
|
|
101
|
+
export function pendingMarkKey(mark: Pick<PendingMark, "mark" | "id">): string {
|
|
102
|
+
return `${mark.mark}:${mark.id}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function validPendingMark(value: unknown): value is PendingMark {
|
|
106
|
+
const id = ownValue(value, "id");
|
|
107
|
+
const ordinal = ownValue(value, "ordinal");
|
|
108
|
+
if (typeof id !== "string" || !id ||
|
|
109
|
+
!MARK_ORIGINS.includes(ownValue(value, "origin") as MarkOrigin) ||
|
|
110
|
+
!Number.isSafeInteger(ordinal) || Number(ordinal) < 0) return false;
|
|
111
|
+
const mark = ownValue(value, "mark");
|
|
112
|
+
if (mark === "refold") return exactRecord(value, PENDING_REFOLD_MARK_KEYS);
|
|
113
|
+
if (mark !== "fold" || !exactRecord(value, PENDING_FOLD_MARK_KEYS)) return false;
|
|
114
|
+
const kind = ownValue(value, "kind");
|
|
115
|
+
const parts = denseOwnArrayValues(ownValue(value, "parts"));
|
|
116
|
+
return (kind === "tool-result" || kind === "chapter" || kind === "consolidation") &&
|
|
117
|
+
Boolean(parts?.length) && parts!.length <= ACTIVE_CONTEXT_POLICY.maxFoldSourceRefs &&
|
|
118
|
+
parts!.every(validFoldPart) && structurallyValidBrief(ownValue(value, "brief")) &&
|
|
119
|
+
validProvenance(ownValue(value, "briefProvenance")) &&
|
|
120
|
+
id === foldIdFor(kind as FoldKind, parts as FoldPart[]);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Agent-corrected fold briefs, keyed by fold id. Bounded by the same brief cap a
|
|
125
|
+
* supplied brief obeys, and refused for a fold that is not in the forest, exactly
|
|
126
|
+
* like a pin or a refold mark naming a missing fold.
|
|
127
|
+
*/
|
|
128
|
+
export function parseBriefOverrides(
|
|
129
|
+
value: unknown,
|
|
130
|
+
ids?: ReadonlySet<string>,
|
|
131
|
+
): Record<string, string> {
|
|
132
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
133
|
+
throw new Error("Invalid active-context brief overrides");
|
|
134
|
+
}
|
|
135
|
+
const entries = Object.entries(value as Record<string, unknown>);
|
|
136
|
+
if (entries.length > MAX_ACTIVE_FOLD_RECORDS ||
|
|
137
|
+
entries.some(([id, brief]) => !id || typeof brief !== "string" || !brief.trim() ||
|
|
138
|
+
brief.length > ACTIVE_CONTEXT_POLICY.maxBriefChars || (ids && !ids.has(id)))) {
|
|
139
|
+
throw new Error("Invalid active-context brief overrides");
|
|
140
|
+
}
|
|
141
|
+
return Object.fromEntries(entries.sort(([left], [right]) => left.localeCompare(right))) as Record<string, string>;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The brief a fold PRESENTS: the agent's correction when it made one, else the record's. */
|
|
145
|
+
export function foldBrief(
|
|
146
|
+
fold: Pick<ActiveFold, "id" | "brief">,
|
|
147
|
+
state: Pick<ActiveContextState, "briefs">,
|
|
148
|
+
): string {
|
|
149
|
+
return state.briefs?.[fold.id] ?? fold.brief;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function parsePendingMarks(value: unknown): PendingMark[] {
|
|
153
|
+
const marks = denseOwnArrayValues(value);
|
|
154
|
+
if (!marks || marks.length > MAX_PENDING_MARKS || !marks.every(validPendingMark) ||
|
|
155
|
+
new Set((marks as PendingMark[]).map(pendingMarkKey)).size !== marks.length) {
|
|
156
|
+
throw new Error("Invalid active-context pending marks");
|
|
157
|
+
}
|
|
158
|
+
return clone(marks) as PendingMark[];
|
|
159
|
+
}
|
|
160
|
+
|
|
92
161
|
export function parseLeases(value: unknown, foldIds?: ReadonlySet<string>): Record<string, number> {
|
|
93
162
|
if (!isPlainRecord(value)) throw new Error("Invalid active-context expand leases");
|
|
94
163
|
const entries = Object.entries(value);
|
|
@@ -258,12 +327,22 @@ export function parseActiveContextState(
|
|
|
258
327
|
Object.prototype.hasOwnProperty.call(value, "tokensSinceToolFold");
|
|
259
328
|
const hasLeases = recordLike && Object.prototype.hasOwnProperty.call(value, "leases");
|
|
260
329
|
const hasSurfacing = recordLike && Object.prototype.hasOwnProperty.call(value, "surfacing");
|
|
330
|
+
const hasPendingMarks = recordLike && Object.prototype.hasOwnProperty.call(value, "pendingMarks");
|
|
331
|
+
// Accepted, never read, never written. `pinnedPeeks` pinned a peek result against
|
|
332
|
+
// per-projection reclamation, which was deleted after rep 22 measured it rewriting
|
|
333
|
+
// the prefix mid-window. State written before that cut still carries the key, and
|
|
334
|
+
// rejecting it here would suspend automatic management on upgrade.
|
|
335
|
+
const hasPinnedPeeks = recordLike && Object.prototype.hasOwnProperty.call(value, "pinnedPeeks");
|
|
336
|
+
const hasBriefs = recordLike && Object.prototype.hasOwnProperty.call(value, "briefs");
|
|
261
337
|
const extraKeys = [
|
|
262
338
|
...(hasPrepared ? ["prepared"] : []),
|
|
263
339
|
...(hasAdvisory ? ["advisory"] : []),
|
|
264
340
|
...(hasTokensSinceToolFold ? ["tokensSinceToolFold"] : []),
|
|
265
341
|
...(hasLeases ? ["leases"] : []),
|
|
266
342
|
...(hasSurfacing ? ["surfacing"] : []),
|
|
343
|
+
...(hasPendingMarks ? ["pendingMarks"] : []),
|
|
344
|
+
...(hasPinnedPeeks ? ["pinnedPeeks"] : []),
|
|
345
|
+
...(hasBriefs ? ["briefs"] : []),
|
|
267
346
|
];
|
|
268
347
|
if (!exactRecord(value, [...ACTIVE_STATE_KEYS, ...extraKeys])) throw new Error("Invalid active-context state keys");
|
|
269
348
|
const folds = denseOwnArrayValues(ownValue(value, "folds"));
|
|
@@ -290,6 +369,11 @@ export function parseActiveContextState(
|
|
|
290
369
|
}
|
|
291
370
|
const leases = hasLeases ? parseLeases(ownValue(value, "leases"), ids) : {};
|
|
292
371
|
const surfacing = hasSurfacing ? parseSurfacingLog(ownValue(value, "surfacing")) : [];
|
|
372
|
+
const marks = hasPendingMarks ? parsePendingMarks(ownValue(value, "pendingMarks")) : [];
|
|
373
|
+
if (marks.some((mark) => mark.mark === "refold" && !ids.has(mark.id))) {
|
|
374
|
+
throw new Error("Invalid active-context pending marks");
|
|
375
|
+
}
|
|
376
|
+
const briefs = hasBriefs ? parseBriefOverrides(ownValue(value, "briefs"), ids) : {};
|
|
293
377
|
const source = clone(value) as unknown as ActiveContextState;
|
|
294
378
|
// Provenance normalization is presentation-only; never mutate a durable
|
|
295
379
|
// content-addressed fold record: changing its bytes causes the next re-persist
|
|
@@ -308,6 +392,8 @@ export function parseActiveContextState(
|
|
|
308
392
|
// Omitted when empty so every state written before surfacing existed keeps its
|
|
309
393
|
// exact digest; the replay digest is order- and presence-sensitive by design.
|
|
310
394
|
...(surfacing.length ? { surfacing } : {}),
|
|
395
|
+
...(marks.length ? { pendingMarks: marks } : {}),
|
|
396
|
+
...(Object.keys(briefs).length ? { briefs } : {}),
|
|
311
397
|
...(hasAdvisory
|
|
312
398
|
? { advisory: clone(source.advisory!) }
|
|
313
399
|
: defaultAdvisory ? { advisory: { highWater: 0, delivered: {} } } : {}),
|
|
@@ -384,6 +470,8 @@ export function sameStateProjection(left: ActiveContextState, right: ActiveConte
|
|
|
384
470
|
if (normalizedState.tokensSinceToolFold === 0) delete normalizedState.tokensSinceToolFold;
|
|
385
471
|
if (normalizedState.leases && Object.keys(normalizedState.leases).length === 0) delete normalizedState.leases;
|
|
386
472
|
if (normalizedState.surfacing && normalizedState.surfacing.length === 0) delete normalizedState.surfacing;
|
|
473
|
+
if (normalizedState.pendingMarks && normalizedState.pendingMarks.length === 0) delete normalizedState.pendingMarks;
|
|
474
|
+
if (normalizedState.briefs && !Object.keys(normalizedState.briefs).length) delete normalizedState.briefs;
|
|
387
475
|
return normalizedState;
|
|
388
476
|
};
|
|
389
477
|
return stableStringify(normalized(left)) === stableStringify(normalized(right));
|
|
@@ -425,6 +513,8 @@ export function validateV2ProjectionFields(
|
|
|
425
513
|
tokensSinceToolFoldValue: unknown,
|
|
426
514
|
leasesValue: unknown,
|
|
427
515
|
surfacingValue?: unknown,
|
|
516
|
+
pendingMarksValue?: unknown,
|
|
517
|
+
briefsValue?: unknown,
|
|
428
518
|
): {
|
|
429
519
|
expanded: string[];
|
|
430
520
|
protected: EvidenceRef[];
|
|
@@ -433,6 +523,8 @@ export function validateV2ProjectionFields(
|
|
|
433
523
|
tokensSinceToolFold: number;
|
|
434
524
|
leases: Record<string, number>;
|
|
435
525
|
surfacing: SurfacingRecord[];
|
|
526
|
+
pendingMarks: PendingMark[];
|
|
527
|
+
briefs: Record<string, string>;
|
|
436
528
|
} {
|
|
437
529
|
const expanded = denseOwnArrayValues(expandedValue);
|
|
438
530
|
const protectedRefs = denseOwnArrayValues(protectedValue);
|
|
@@ -451,6 +543,8 @@ export function validateV2ProjectionFields(
|
|
|
451
543
|
}
|
|
452
544
|
const leases = leasesValue === undefined ? {} : parseLeases(leasesValue);
|
|
453
545
|
const surfacing = surfacingValue === undefined ? [] : parseSurfacingLog(surfacingValue);
|
|
546
|
+
const pendingMarks = pendingMarksValue === undefined ? [] : parsePendingMarks(pendingMarksValue);
|
|
547
|
+
const briefs = briefsValue === undefined ? {} : parseBriefOverrides(briefsValue);
|
|
454
548
|
return {
|
|
455
549
|
expanded: clone(expanded) as string[],
|
|
456
550
|
protected: clone(protectedRefs) as EvidenceRef[],
|
|
@@ -461,6 +555,8 @@ export function validateV2ProjectionFields(
|
|
|
461
555
|
tokensSinceToolFold: tokensSinceToolFoldValue === undefined ? 0 : Number(tokensSinceToolFoldValue),
|
|
462
556
|
leases,
|
|
463
557
|
surfacing,
|
|
558
|
+
pendingMarks,
|
|
559
|
+
briefs,
|
|
464
560
|
};
|
|
465
561
|
}
|
|
466
562
|
|
|
@@ -474,11 +570,24 @@ export function parseActiveContextStateV2(value: unknown, sessionId: string): Ac
|
|
|
474
570
|
Object.prototype.hasOwnProperty.call(value, "leases"));
|
|
475
571
|
const hasSurfacing = Boolean(value && typeof value === "object" &&
|
|
476
572
|
Object.prototype.hasOwnProperty.call(value, "surfacing"));
|
|
573
|
+
const hasPendingMarks = Boolean(value && typeof value === "object" &&
|
|
574
|
+
Object.prototype.hasOwnProperty.call(value, "pendingMarks"));
|
|
575
|
+
// Accepted, never read, never written. `pinnedPeeks` pinned a peek result against
|
|
576
|
+
// per-projection reclamation, which was deleted after rep 22 measured it rewriting
|
|
577
|
+
// the prefix mid-window. State written before that cut still carries the key, and
|
|
578
|
+
// rejecting it here would suspend automatic management on upgrade.
|
|
579
|
+
const hasPinnedPeeks = Boolean(value && typeof value === "object" &&
|
|
580
|
+
Object.prototype.hasOwnProperty.call(value, "pinnedPeeks"));
|
|
581
|
+
const hasBriefs = Boolean(value && typeof value === "object" &&
|
|
582
|
+
Object.prototype.hasOwnProperty.call(value, "briefs"));
|
|
477
583
|
const optionalKeys = [
|
|
478
584
|
...(hasAdvisory ? ["advisory"] : []),
|
|
479
585
|
...(hasTokensSinceToolFold ? ["tokensSinceToolFold"] : []),
|
|
480
586
|
...(hasLeases ? ["leases"] : []),
|
|
481
587
|
...(hasSurfacing ? ["surfacing"] : []),
|
|
588
|
+
...(hasPendingMarks ? ["pendingMarks"] : []),
|
|
589
|
+
...(hasPinnedPeeks ? ["pinnedPeeks"] : []),
|
|
590
|
+
...(hasBriefs ? ["briefs"] : []),
|
|
482
591
|
];
|
|
483
592
|
const checkpoint = kind === "checkpoint" &&
|
|
484
593
|
exactRecord(value, [...STATE_CHECKPOINT_V2_KEYS, ...optionalKeys]);
|
|
@@ -493,7 +602,7 @@ export function parseActiveContextStateV2(value: unknown, sessionId: string): Ac
|
|
|
493
602
|
validateV2ProjectionFields(
|
|
494
603
|
ownValue(value, "expanded"), ownValue(value, "protected"), ownValue(value, "prepared"),
|
|
495
604
|
ownValue(value, "advisory"), ownValue(value, "tokensSinceToolFold"), ownValue(value, "leases"),
|
|
496
|
-
ownValue(value, "surfacing"),
|
|
605
|
+
ownValue(value, "surfacing"), ownValue(value, "pendingMarks"), ownValue(value, "briefs"),
|
|
497
606
|
);
|
|
498
607
|
if (checkpoint) {
|
|
499
608
|
const refs = denseOwnArrayValues(ownValue(value, "foldRefs"));
|
|
@@ -558,7 +667,7 @@ export function stateFromFoldRefs(
|
|
|
558
667
|
wire: Pick<
|
|
559
668
|
ActiveContextCheckpointV2,
|
|
560
669
|
"sessionId" | "revision" | "expanded" | "protected" | "prepared" | "advisory" |
|
|
561
|
-
"tokensSinceToolFold" | "leases" | "surfacing"
|
|
670
|
+
"tokensSinceToolFold" | "leases" | "surfacing" | "pendingMarks" | "briefs"
|
|
562
671
|
>,
|
|
563
672
|
refs: FoldRecordRef[],
|
|
564
673
|
records: Map<string, FoldRecordEntry>,
|
|
@@ -579,6 +688,8 @@ export function stateFromFoldRefs(
|
|
|
579
688
|
tokensSinceToolFold: wire.tokensSinceToolFold ?? 0,
|
|
580
689
|
leases: clone(wire.leases ?? {}),
|
|
581
690
|
...(wire.surfacing?.length ? { surfacing: clone(wire.surfacing) } : {}),
|
|
691
|
+
...(wire.pendingMarks?.length ? { pendingMarks: clone(wire.pendingMarks) } : {}),
|
|
692
|
+
...(wire.briefs && Object.keys(wire.briefs).length ? { briefs: clone(wire.briefs) } : {}),
|
|
582
693
|
...(wire.advisory === undefined ? {} : { advisory: clone(wire.advisory) }),
|
|
583
694
|
...(wire.prepared === null || wire.prepared === undefined ? {} : { prepared: clone(wire.prepared) }),
|
|
584
695
|
};
|
|
@@ -729,6 +840,8 @@ export function makeStateCheckpoint(state: ActiveContextState): ActiveContextChe
|
|
|
729
840
|
tokensSinceToolFold: state.tokensSinceToolFold,
|
|
730
841
|
leases: clone(state.leases),
|
|
731
842
|
...(state.surfacing?.length ? { surfacing: clone(state.surfacing) } : {}),
|
|
843
|
+
...(state.pendingMarks?.length ? { pendingMarks: clone(state.pendingMarks) } : {}),
|
|
844
|
+
...(state.briefs && Object.keys(state.briefs).length ? { briefs: clone(state.briefs) } : {}),
|
|
732
845
|
...(state.advisory ? { advisory: clone(state.advisory) } : {}),
|
|
733
846
|
stateSha256: semanticStateSha256(state),
|
|
734
847
|
}, state.sessionId) as ActiveContextCheckpointV2;
|
|
@@ -764,6 +877,8 @@ export function makeStateDelta(previous: ActiveContextState, next: ActiveContext
|
|
|
764
877
|
tokensSinceToolFold: next.tokensSinceToolFold,
|
|
765
878
|
leases: clone(next.leases),
|
|
766
879
|
...(next.surfacing?.length ? { surfacing: clone(next.surfacing) } : {}),
|
|
880
|
+
...(next.pendingMarks?.length ? { pendingMarks: clone(next.pendingMarks) } : {}),
|
|
881
|
+
...(next.briefs && Object.keys(next.briefs).length ? { briefs: clone(next.briefs) } : {}),
|
|
767
882
|
...(next.advisory ? { advisory: clone(next.advisory) } : {}),
|
|
768
883
|
stateSha256: semanticStateSha256(next),
|
|
769
884
|
}, next.sessionId) as ActiveContextDeltaV2;
|
package/extensions/lib/policy.ts
CHANGED
|
@@ -41,8 +41,25 @@ export const PROVIDER_CONTEXT_MEASUREMENT_ENTRY = `${DEFAULT_ENTRY_NAMESPACE}-pr
|
|
|
41
41
|
export const ACTIVE_CONTEXT_STATUS_KEY = DEFAULT_ACTIVE_CONTEXT_ENTRY_TYPE_PREFIX;
|
|
42
42
|
export const ACTIVE_CONTEXT_TOOL_ACTIONS = Object.freeze([
|
|
43
43
|
"status", "peek", "fold", "expand", "refold", "protect", "unprotect",
|
|
44
|
+
// The correction verbs. Curation the agent cannot fix afterwards is curation it
|
|
45
|
+
// will not risk making, so re-briefing a fold and dissolving a mis-cut boundary
|
|
46
|
+
// are part of the ordinary surface, not an epoch-only extra.
|
|
47
|
+
"rebrief", "reboundary",
|
|
44
48
|
] as const);
|
|
45
|
-
|
|
49
|
+
/**
|
|
50
|
+
* The epoch surface: the immediate actions plus `unmark`. A mark is a standing decision
|
|
51
|
+
* rather than a one-shot attempt, so withdrawing one needs a verb of its own.
|
|
52
|
+
*
|
|
53
|
+
* There is deliberately NO agent-callable commit verb. Marking is the agent's job and
|
|
54
|
+
* folding is the runtime's, and a verb the runtime is entitled to overrule is surface
|
|
55
|
+
* without function: measured 2026-08-07 (rep 17), the agent called commit twice and the
|
|
56
|
+
* runtime correctly held it both times. The internal commit paths -- the epoch commit,
|
|
57
|
+
* the gated commit, the fence and the recovery lane -- are unchanged and unexposed.
|
|
58
|
+
*/
|
|
59
|
+
export const EPOCH_ACTIVE_CONTEXT_TOOL_ACTIONS = Object.freeze([
|
|
60
|
+
...ACTIVE_CONTEXT_TOOL_ACTIONS, "unmark",
|
|
61
|
+
] as const);
|
|
62
|
+
export type ActiveContextToolAction = typeof EPOCH_ACTIVE_CONTEXT_TOOL_ACTIONS[number];
|
|
46
63
|
export const USER_RESCUE_MAX_SOURCE_CHARS = 512_000;
|
|
47
64
|
export const DEFAULT_CONTEXT_WINDOW = 272_000;
|
|
48
65
|
export const TOOL_FOLD_CADENCE_MIN_TOKENS = 12_000;
|
|
@@ -75,6 +92,285 @@ export const SURFACING_SOURCE_ID = "fold-brief";
|
|
|
75
92
|
// the protected byte tail as a share of a small window; never to estimate usage.
|
|
76
93
|
export const BYTES_PER_TOKEN_FLOOR = 2;
|
|
77
94
|
|
|
95
|
+
// Two-phase fold scheduling. A provider prefix cache is positional: any mid-window
|
|
96
|
+
// edit invalidates every byte after it, so the cost of folding is dominated by how
|
|
97
|
+
// OFTEN the projection changes, not by how much it saves. In "epoch" mode a fold
|
|
98
|
+
// decision becomes a free MARK and a later COMMIT applies every pending mark in one
|
|
99
|
+
// rewrite. "immediate" is the default and is byte-identical to pre-0.1.2 behavior.
|
|
100
|
+
export const FOLD_SCHEDULING_MODES = Object.freeze(["immediate", "epoch"] as const);
|
|
101
|
+
export type FoldSchedulingMode = typeof FOLD_SCHEDULING_MODES[number];
|
|
102
|
+
export const DEFAULT_FOLD_SCHEDULING: FoldSchedulingMode = "immediate";
|
|
103
|
+
|
|
104
|
+
// Epoch knobs stay INTERNAL constants for the same reason the surfacing knobs do:
|
|
105
|
+
// they are what the round-2 cost measurement exists to settle, and an option surface
|
|
106
|
+
// fixed before that data would freeze a guess.
|
|
107
|
+
/**
|
|
108
|
+
* An automatic commit tops up with stale tool batches until it would free this share
|
|
109
|
+
* of the window. It is a FLOOR under an epoch's accumulated marks, not the driver of
|
|
110
|
+
* one, so it is set where a commit is unambiguously worth its single rewrite.
|
|
111
|
+
*/
|
|
112
|
+
export const EPOCH_COMMIT_TARGET_WINDOW_SHARE = 0.40;
|
|
113
|
+
/**
|
|
114
|
+
* Commit when the ELIGIBLE marked mass is worth its one rewrite. Window pressure stays
|
|
115
|
+
* underneath as the safety backstop; this is the economic trigger above it.
|
|
116
|
+
*/
|
|
117
|
+
export const EPOCH_ELIGIBLE_SHARE_COMMIT_THRESHOLD = 0.30;
|
|
118
|
+
/**
|
|
119
|
+
* The reclaim floor, and the single constant that decides whether a commit is worth
|
|
120
|
+
* firing at all.
|
|
121
|
+
*
|
|
122
|
+
* Measured on memex (19,082 calls, 254 sessions): a token-weighted pooled cache share
|
|
123
|
+
* of 89.9% across two provider wires, with steady-state requests at 91.6% and any
|
|
124
|
+
* request following a mutation at 15.7%. A fold costs exactly what one user message
|
|
125
|
+
* costs. Their mutations are 5.3% of requests; rep 14's were about 34%, and that
|
|
126
|
+
* FREQUENCY difference is the entire gap between their 90% and our 64% -- the
|
|
127
|
+
* per-mutation penalty is the same on both.
|
|
128
|
+
*
|
|
129
|
+
* So a commit that would free less than this share of the truthful budget does not
|
|
130
|
+
* fire: it defers and accumulates until it is worth a full prefix. Safety outranks
|
|
131
|
+
* economics, so the hard fence and overflow recovery fire regardless.
|
|
132
|
+
*/
|
|
133
|
+
export const COMMIT_RECLAIM_FLOOR_SHARE = 0.02;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The thermostat's lower line.
|
|
137
|
+
*
|
|
138
|
+
* The floor above says what is worth firing; this says how DEEP a firing goes. The
|
|
139
|
+
* failure shape it removes is rep 13's crumb ratchet: a trigger that fires, frees a
|
|
140
|
+
* little, and re-fires almost immediately, so the window climbs a staircase of
|
|
141
|
+
* mutations. Firing at the trigger line and folding down to a target line makes the
|
|
142
|
+
* spacing between events structural -- the next event cannot arrive until inflow has
|
|
143
|
+
* refilled the gap -- so the window saws instead of climbing.
|
|
144
|
+
*
|
|
145
|
+
* The gap here is 15 points of a truthful budget. Rep 13's largest measured inflow step
|
|
146
|
+
* was 27,815 tokens against a 400,000 window, about 7 points, so at least two full
|
|
147
|
+
* worst-case steps must land before the trigger can re-arm, and the agent keeps roughly
|
|
148
|
+
* two thirds of its budget as working room after every event. Eligibility still binds:
|
|
149
|
+
* if the eligible mass cannot reach the target, the commit takes what it may and
|
|
150
|
+
* records the shortfall rather than loosening a protection to hit a number.
|
|
151
|
+
*/
|
|
152
|
+
export const CURATION_TARGET_OCCUPANCY_SHARE = 0.35;
|
|
153
|
+
export const MAX_PENDING_MARKS = 256;
|
|
154
|
+
/** Enough batches to actually reach the floor on a wide window before the loop exits. */
|
|
155
|
+
export const EPOCH_MAX_TOPUP_MARKS = 64;
|
|
156
|
+
/** Estimate only; provider token accounting always comes from a measured response. */
|
|
157
|
+
export const ESTIMATED_BYTES_PER_TOKEN = 4;
|
|
158
|
+
/** Rendered navigation/topology overhead assumed around a brief when estimating a placeholder. */
|
|
159
|
+
export const ESTIMATED_PLACEHOLDER_OVERHEAD_BYTES = 240;
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// The reliability spine. Every behavior below shipped as an off-by-default lever
|
|
163
|
+
// through iterations 2 and 3 and was sealed ON by rep 14 (64/64), so it is now
|
|
164
|
+
// simply how pi-fold works: the flags and their conditional twins are gone, and
|
|
165
|
+
// git history is the lineage. What stays configurable is only what is genuinely a
|
|
166
|
+
// deployment fact (`providerTotalWindow`) or an experiment condition (`guidance`,
|
|
167
|
+
// `foldScheduling`, `foldPeekResults`, `guidedCuration`).
|
|
168
|
+
//
|
|
169
|
+
// Sealed unconditional: admission control, retained pending marks, the eligible-share
|
|
170
|
+
// commit trigger, stage-identified briefs, the current-turn commit guard, the
|
|
171
|
+
// pinned-mass backstop, the status index diet, delivery-counted advisories, and
|
|
172
|
+
// projection instrumentation.
|
|
173
|
+
//
|
|
174
|
+
// NOT sealed, deleted: ephemeral peek and its per-call `ephemeral` override. It rewrote
|
|
175
|
+
// a consumed peek result in place on the theory the edit was tail-local. It is not: the
|
|
176
|
+
// rewrite waits for a later assistant message to exist, by which point the window has
|
|
177
|
+
// grown over it and the edit lands mid-prefix. Rep 22 measured two of them costing 100k
|
|
178
|
+
// fresh tokens. A peek is append-only; the tool-fold rung reclaims the duplicate bytes
|
|
179
|
+
// at the next commit, which is the one moment a rewrite is already being paid for.
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The provider serving window is the TOTAL admission budget minus whatever output
|
|
184
|
+
* reservation the deployment actually asked for; the per-request max-input descriptor
|
|
185
|
+
* assumes a full output reservation and understates the real ceiling. Measured
|
|
186
|
+
* 2026-08-06: a run aborted at ~297k projected tokens against a 272k descriptor while
|
|
187
|
+
* the same provider had just accepted 339,689 tokens. Declaring the total window is
|
|
188
|
+
* the deployment's own fact, so it is the option; there is no separate boolean, and
|
|
189
|
+
* an undeclared window falls back to the descriptor and SAYS so in the accounting.
|
|
190
|
+
*/
|
|
191
|
+
|
|
192
|
+
/** Narrowing is what makes a refusal governance instead of denial. */
|
|
193
|
+
export const PEEK_MIN_SLICE_BYTES = 1_024;
|
|
194
|
+
|
|
195
|
+
/** How many folds the dieted status payload ranks by what they would reclaim. */
|
|
196
|
+
export const STATUS_DIET_SUGGESTIONS = 5;
|
|
197
|
+
|
|
198
|
+
export const MAX_PROJECTION_HASH_RECORDS = 64;
|
|
199
|
+
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// Guided curation. The one iteration-4 experiment condition: the two-signal
|
|
202
|
+
// curation trigger, its bounded last-call gate, and the reactive receipt block.
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
export const DEFAULT_GUIDED_CURATION = false;
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Signal one: measured occupancy of the truthful serving budget.
|
|
209
|
+
*
|
|
210
|
+
* Four fifths, not half (Shane RULING 2026-08-06, after rep 15): below this line the
|
|
211
|
+
* runtime is QUIET -- nothing folds automatically at all -- so the threshold is where
|
|
212
|
+
* the fold event should happen, and the remaining fifth of the budget is the runway the
|
|
213
|
+
* gate rounds and the commit itself spend. An earlier line buys announcement room by
|
|
214
|
+
* spending window the agent still wants for the task.
|
|
215
|
+
*/
|
|
216
|
+
export const CURATION_OCCUPANCY_SHARE = 0.80;
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* The two sparse reminders, as shares of the truthful serving budget.
|
|
220
|
+
*
|
|
221
|
+
* Exactly two per window cycle, one line each, informatory: mark chapters as you go so
|
|
222
|
+
* the folds are neat when the fold event triggers. They are the ONLY pre-gate guidance
|
|
223
|
+
* in guided curation -- recurring pressure nags trained rep 15's agent to ignore the
|
|
224
|
+
* one message that mattered -- and they re-arm after each commit cycle, so a window that
|
|
225
|
+
* climbs twice is reminded twice.
|
|
226
|
+
*/
|
|
227
|
+
export const CURATION_REMINDER_SHARES: readonly number[] = Object.freeze([0.45, 0.65]);
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Signal two: tool-result mass OUTSIDE the fresh tail, as a share of the window.
|
|
231
|
+
* Occupancy alone announces commits a commit cannot serve -- a window full of
|
|
232
|
+
* conversation has nothing stale to fold. Rep 14 ran 138 tool results over ~4.2MB of
|
|
233
|
+
* payload against a 400k window, so a fifth of the window (~80k tokens, ~320KB) is
|
|
234
|
+
* roughly a dozen ordinary results: reached early in a tool-heavy run, and never
|
|
235
|
+
* reached by a run whose window is not actually made of stale tool output. It is half
|
|
236
|
+
* the commit's own top-up target, so the announcement always precedes a commit that
|
|
237
|
+
* has at least half a target of real mass to work with.
|
|
238
|
+
*/
|
|
239
|
+
export const CURATION_STALE_TOOL_SHARE = 0.20;
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* The gate's hard round cap. Every context pass with the gate open consumes one round,
|
|
243
|
+
* so the commit proceeds after at most this many passes NO MATTER WHAT the agent does;
|
|
244
|
+
* a pass whose response was not a context-tool call proceeds immediately. Two is a
|
|
245
|
+
* bounded conversation, not a negotiation, and it makes a stall non-constructible.
|
|
246
|
+
*/
|
|
247
|
+
export const CURATION_GATE_MAX_ROUNDS = 2;
|
|
248
|
+
|
|
249
|
+
/** How many automatic-action receipts stay in the window; the oldest ages out. */
|
|
250
|
+
export const MAX_CONTEXT_RECEIPTS = 3;
|
|
251
|
+
/** Hard byte cap on the rendered receipt block, so a receipt can never itself bloat. */
|
|
252
|
+
export const CONTEXT_RECEIPT_BLOCK_BYTES = 900;
|
|
253
|
+
/** Durable instrumentation records kept in the in-memory ledger for status. */
|
|
254
|
+
export const MAX_CONTEXT_ATTEMPT_RECORDS = 128;
|
|
255
|
+
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
// The mark response.
|
|
258
|
+
//
|
|
259
|
+
// Measured 2026-08-07 (rep 17): every context action was followed by a projection
|
|
260
|
+
// REWRITE, never an append, because the surface answered a mark by refreshing blocks
|
|
261
|
+
// the agent had already paid to cache. A tail-appended tool result is the one place
|
|
262
|
+
// left that can inform the agent for free, so the awareness moved THERE and the
|
|
263
|
+
// projection went byte-frozen between fold events. The result is bounded on the same
|
|
264
|
+
// principle as the receipt block: a report about bloat that becomes bloat has argued
|
|
265
|
+
// against itself.
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
/** Hard byte cap on the rendered awareness block a mark call answers with. */
|
|
269
|
+
export const CONTEXT_MARK_RESPONSE_BYTES = 900;
|
|
270
|
+
/**
|
|
271
|
+
* Hard byte cap on one serialized status payload, every detail variant included.
|
|
272
|
+
* Measured 2026-08-07 (rep 19): six paged status results accumulated to ~254k tokens
|
|
273
|
+
* and the seventh answered with 526KB, 6.2x the remaining headroom, aborting the
|
|
274
|
+
* request. Paging already existed; nothing FORCED it. Above the cap the listing is
|
|
275
|
+
* truncated at a unit boundary and a continuation names the next offset, so the
|
|
276
|
+
* biggest possible status answer is about 6k tokens and the rest is another page.
|
|
277
|
+
*/
|
|
278
|
+
export const CONTEXT_STATUS_RESPONSE_BYTES = 24_000;
|
|
279
|
+
/** How many unmarked candidates the awareness block names; the remainder is an aggregate. */
|
|
280
|
+
export const MAX_UNMARKED_CANDIDATES = 3;
|
|
281
|
+
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
// Bite-sized folds and bounded peeks. These two constants are one decision read
|
|
284
|
+
// from two ends, so they are stated together and kept equal.
|
|
285
|
+
//
|
|
286
|
+
// Measured 2026-08-06 (rep 6): a single 60,432-byte chapter fold hid the needed
|
|
287
|
+
// fact in its tail, and every peek of it was either truncated short of the answer
|
|
288
|
+
// or too expensive to widen. A fold is only navigable if reading one back is cheap,
|
|
289
|
+
// so a fold's source is capped and an oversized span is SPLIT into sequential
|
|
290
|
+
// bounded folds, each with its own brief. The peek bound is the same number, so a
|
|
291
|
+
// default peek of one bite-sized fold returns it whole -- about 4,000 tokens, a
|
|
292
|
+
// read any session can afford -- and only a legacy oversized fold is ever truncated.
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
/** A fold whose exact source exceeds this is split into sequential bounded folds. */
|
|
296
|
+
export const MAX_FOLD_SPAN_CHARS = 16_000;
|
|
297
|
+
/**
|
|
298
|
+
* The other bound: a SLIVER of stale raw content hugging a fold boundary is absorbed
|
|
299
|
+
* into its later neighbour inside a commit the epoch already paid for.
|
|
300
|
+
*
|
|
301
|
+
* The threshold is deliberately tiny, measured in the session's own calibrated TOKENS,
|
|
302
|
+
* and it is not pressure-scaled. Non-sequential curation is a first-class state: an
|
|
303
|
+
* agent may hold folds at 10:20, 40:55 and 60:70 with raw spans between them ON
|
|
304
|
+
* PURPOSE, and a mechanism that swallowed those gaps would quietly turn pi-fold into
|
|
305
|
+
* system-controlled compaction with the curation element deleted. 256 tokens is about
|
|
306
|
+
* one short prompt or one one-line result -- nobody deliberately keeps a sliver that
|
|
307
|
+
* small as standing context -- and it sits an order of magnitude under the minimum
|
|
308
|
+
* chapter size the ladder itself will fold. Anything larger stays raw permanently,
|
|
309
|
+
* however ragged the projection looks; the ladder's ordinary fold eligibility is the
|
|
310
|
+
* only thing that ever touches it, and absorption never becomes a second folding path.
|
|
311
|
+
*
|
|
312
|
+
* Later, not earlier, because extending the LATER fold backward mutates at a shallower
|
|
313
|
+
* prefix position and preserves more of the cache. There is no content-affinity logic,
|
|
314
|
+
* every absorption is receipted, and a wrong grouping is one `reboundary` away.
|
|
315
|
+
*/
|
|
316
|
+
export const MAX_WEDGE_ABSORB_TOKENS = 256;
|
|
317
|
+
/** What a peek returns without an explicit widening argument. */
|
|
318
|
+
export const PEEK_DEFAULT_MAX_BYTES = 16_000;
|
|
319
|
+
/** Head share of a truncated peek; the remainder is the tail, where conclusions live. */
|
|
320
|
+
export const PEEK_HEAD_SHARE = 0.6;
|
|
321
|
+
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
// Overflow rollback and recovery.
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* How many times one inflow may be recovered before the runtime gives up.
|
|
328
|
+
*
|
|
329
|
+
* A provider context-overflow rejection mutates nothing durable -- the assistant
|
|
330
|
+
* message never lands and the projection is rebuilt per request -- so recovery is a
|
|
331
|
+
* reduction plus a rebuild, not a branch rewind. Two attempts is the cap: the first
|
|
332
|
+
* folds at fence pressure with the guard fully waived, the second reaches into the
|
|
333
|
+
* fresh tail, and a third would be asking the same question of the same evidence.
|
|
334
|
+
* Past it the run fails LOUDLY, because an inflow that cannot fit after maximal
|
|
335
|
+
* folding is a genuine impossibility rather than a recoverable state.
|
|
336
|
+
*/
|
|
337
|
+
export const OVERFLOW_RECOVERY_MAX_ATTEMPTS = 2;
|
|
338
|
+
|
|
339
|
+
/** Active-context tool actions that read without mutating, so their results may fold. */
|
|
340
|
+
export const READ_ONLY_CONTEXT_ACTIONS_DEFAULT: ReadonlySet<string> = new Set(["status"]);
|
|
341
|
+
/**
|
|
342
|
+
* The classification that also lets a `peek` result be reclaimed by the tool-fold rung.
|
|
343
|
+
* A peek copies a fold's own stored source back into the window, so leaving it raw is a
|
|
344
|
+
* pure duplicate of evidence the runtime already holds; epoch scheduling adopts this set
|
|
345
|
+
* unconditionally, and immediate scheduling reaches it through `foldPeekResults`.
|
|
346
|
+
*/
|
|
347
|
+
export const PEEK_READ_ONLY_CONTEXT_ACTIONS: ReadonlySet<string> = new Set(["status", "peek"]);
|
|
348
|
+
|
|
349
|
+
export type MarkOrigin = "agent" | "ladder";
|
|
350
|
+
|
|
351
|
+
/** A fold decided but not yet applied: no projection byte has moved for it. */
|
|
352
|
+
export interface PendingFoldMark {
|
|
353
|
+
mark: "fold";
|
|
354
|
+
id: string;
|
|
355
|
+
kind: FoldKind;
|
|
356
|
+
parts: FoldPart[];
|
|
357
|
+
brief: string;
|
|
358
|
+
briefProvenance: BriefProvenance;
|
|
359
|
+
origin: MarkOrigin;
|
|
360
|
+
/** Transcript ordinal at mark time; orders the commit. Never a wall clock. */
|
|
361
|
+
ordinal: number;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** An expanded fold decided to return to its placeholder at the next commit. */
|
|
365
|
+
export interface PendingRefoldMark {
|
|
366
|
+
mark: "refold";
|
|
367
|
+
id: string;
|
|
368
|
+
origin: MarkOrigin;
|
|
369
|
+
ordinal: number;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export type PendingMark = PendingFoldMark | PendingRefoldMark;
|
|
373
|
+
|
|
78
374
|
export const ACTIVE_CONTEXT_POLICY = Object.freeze({
|
|
79
375
|
freshTurns: 3,
|
|
80
376
|
freshBytes: 24_000,
|
|
@@ -190,6 +486,17 @@ export interface ActiveContextState {
|
|
|
190
486
|
tokensSinceToolFold: number;
|
|
191
487
|
leases: Record<string, number>;
|
|
192
488
|
surfacing?: SurfacingRecord[];
|
|
489
|
+
/** Epoch scheduling only; omitted when empty so immediate-mode state digests never move. */
|
|
490
|
+
pendingMarks?: PendingMark[];
|
|
491
|
+
/** Retired with peek reclamation; carried only so pre-cut state still parses. */
|
|
492
|
+
pinnedPeeks?: never;
|
|
493
|
+
/**
|
|
494
|
+
* Agent-corrected fold briefs, by fold id. A fold RECORD is content-addressed and
|
|
495
|
+
* immutable -- rewriting its brief in place would report a conflicting durable fold
|
|
496
|
+
* and suspend automatic management -- so a re-brief is durable state beside the
|
|
497
|
+
* record rather than a mutation of it. Omitted when empty.
|
|
498
|
+
*/
|
|
499
|
+
briefs?: Record<string, string>;
|
|
193
500
|
prepared?: PreparedFold;
|
|
194
501
|
advisory?: {
|
|
195
502
|
highWater: number;
|
|
@@ -223,6 +530,8 @@ export interface ActiveContextCheckpointV2 {
|
|
|
223
530
|
tokensSinceToolFold?: number;
|
|
224
531
|
leases?: Record<string, number>;
|
|
225
532
|
surfacing?: SurfacingRecord[];
|
|
533
|
+
pendingMarks?: PendingMark[];
|
|
534
|
+
briefs?: Record<string, string>;
|
|
226
535
|
advisory?: NonNullable<ActiveContextState["advisory"]>;
|
|
227
536
|
stateSha256: string;
|
|
228
537
|
}
|
|
@@ -242,6 +551,8 @@ export interface ActiveContextDeltaV2 {
|
|
|
242
551
|
tokensSinceToolFold?: number;
|
|
243
552
|
leases?: Record<string, number>;
|
|
244
553
|
surfacing?: SurfacingRecord[];
|
|
554
|
+
pendingMarks?: PendingMark[];
|
|
555
|
+
briefs?: Record<string, string>;
|
|
245
556
|
advisory?: NonNullable<ActiveContextState["advisory"]>;
|
|
246
557
|
stateSha256: string;
|
|
247
558
|
}
|
|
@@ -279,6 +590,8 @@ export interface ActiveContextSnapshot {
|
|
|
279
590
|
brandNoun: string;
|
|
280
591
|
entryTypePrefix: string;
|
|
281
592
|
readOnlyTools: ReadonlySet<string>;
|
|
593
|
+
/** Active-context tool actions treated as read-only when classifying tool batches. */
|
|
594
|
+
readOnlyContextActions: ReadonlySet<string>;
|
|
282
595
|
contextWindow: number;
|
|
283
596
|
windowSource: "reported" | "fallback";
|
|
284
597
|
}
|