pi-fold 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,6 +8,9 @@ export const DEFAULT_ACTIVE_CONTEXT_COMMAND_NAMES = Object.freeze({
8
8
  status: "context",
9
9
  fold: "fold-context",
10
10
  });
11
+ export const GUIDANCE_PROFILES = Object.freeze(["pressure", "curation", "minimal"] as const);
12
+ export type GuidanceProfile = typeof GUIDANCE_PROFILES[number];
13
+ export const DEFAULT_GUIDANCE_PROFILE: GuidanceProfile = "pressure";
11
14
 
12
15
  export function entryTypeNamespace(entryTypePrefix: string): string {
13
16
  const suffix = "-active-context";
@@ -37,9 +40,26 @@ export const NATIVE_COMPACTION_DECISION_ENTRY = `${DEFAULT_ENTRY_NAMESPACE}-nati
37
40
  export const PROVIDER_CONTEXT_MEASUREMENT_ENTRY = `${DEFAULT_ENTRY_NAMESPACE}-provider-context-measurement`;
38
41
  export const ACTIVE_CONTEXT_STATUS_KEY = DEFAULT_ACTIVE_CONTEXT_ENTRY_TYPE_PREFIX;
39
42
  export const ACTIVE_CONTEXT_TOOL_ACTIONS = Object.freeze([
40
- "status", "fold", "expand", "refold", "protect", "unprotect",
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",
41
48
  ] as const);
42
- export type ActiveContextToolAction = typeof ACTIVE_CONTEXT_TOOL_ACTIONS[number];
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];
43
63
  export const USER_RESCUE_MAX_SOURCE_CHARS = 512_000;
44
64
  export const DEFAULT_CONTEXT_WINDOW = 272_000;
45
65
  export const TOOL_FOLD_CADENCE_MIN_TOKENS = 12_000;
@@ -49,10 +69,308 @@ export const MAX_EXPAND_LEASES = 64;
49
69
  export const CONSOLIDATION_WIDTH_THRESHOLD = 10;
50
70
  export const MAX_ADVISORY_DELIVERIES_PER_MILESTONE = 16;
51
71
 
72
+ // Ephemeral surfacing structure. These stay INTERNAL constants rather than public
73
+ // options: they are the knobs the surfacing experiment is meant to settle, and an
74
+ // option surface fixed before the accept/reject data exists would freeze a guess.
75
+ export const DEFAULT_SURFACING_ENABLED = true;
76
+ export const SURFACING_TOP_K = 3;
77
+ export const SURFACING_MIN_SCORE = 0.30;
78
+ export const SURFACING_CHAR_BUDGET = 1_000;
79
+ export const SURFACING_COOLDOWN_ORDINALS = 8;
80
+ export const SURFACING_OUTCOME_WINDOW_ORDINALS = 12;
81
+ export const SURFACING_RECENT_TASK_SPANS = 6;
82
+ export const SURFACING_MAX_TASK_CHARS = 12_000;
83
+ export const SURFACING_MAX_LOG_RECORDS = 128;
84
+ export const SURFACING_LEXICAL_WEIGHT = 0.80;
85
+ export const SURFACING_RECENCY_WEIGHT = 0.14;
86
+ export const SURFACING_DEPTH_WEIGHT = 0.06;
87
+ export const SURFACING_MAX_DEPTH = 4;
88
+ export const SURFACING_MAX_TEXT_CHARS = 220;
89
+ export const SURFACING_SOURCE_ID = "fold-brief";
90
+
52
91
  // Conservative LOWER bound on UTF-8 bytes per provider token, used only to cap
53
- // the protected byte tail as a share of a small window — never to estimate usage.
92
+ // the protected byte tail as a share of a small window; never to estimate usage.
54
93
  export const BYTES_PER_TOKEN_FLOOR = 2;
55
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
+
56
374
  export const ACTIVE_CONTEXT_POLICY = Object.freeze({
57
375
  freshTurns: 3,
58
376
  freshBytes: 24_000,
@@ -120,6 +438,44 @@ export interface PreparedFold {
120
438
  fold: ActiveFold;
121
439
  }
122
440
 
441
+ export type SurfacingOutcome = "shown" | "accept" | "reject";
442
+
443
+ /** One durable surfacing observation: what was shown, how it scored, and what the agent did. */
444
+ export interface SurfacingRecord {
445
+ source: string;
446
+ id: string;
447
+ score: number;
448
+ ordinal: number;
449
+ outcome: SurfacingOutcome;
450
+ }
451
+
452
+ /** One item a suggestion source offers for the shared carrier. */
453
+ export interface SurfacingCandidate {
454
+ source: string;
455
+ id: string;
456
+ text: string;
457
+ route: string;
458
+ alternateRoute?: string;
459
+ /** Transcript position used for recency; higher is more recent. Never a wall clock. */
460
+ position?: number;
461
+ depth?: number;
462
+ }
463
+
464
+ export interface SurfacingSuggestion extends SurfacingCandidate {
465
+ score: number;
466
+ }
467
+
468
+ export interface SuggestionSourceInput {
469
+ state: ActiveContextState;
470
+ snapshot: ActiveContextSnapshot;
471
+ toolName: string;
472
+ }
473
+
474
+ export interface SuggestionSource {
475
+ id: string;
476
+ candidates: (input: SuggestionSourceInput) => SurfacingCandidate[];
477
+ }
478
+
123
479
  export interface ActiveContextState {
124
480
  version: 1;
125
481
  sessionId: string;
@@ -129,6 +485,18 @@ export interface ActiveContextState {
129
485
  protected: EvidenceRef[];
130
486
  tokensSinceToolFold: number;
131
487
  leases: Record<string, number>;
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>;
132
500
  prepared?: PreparedFold;
133
501
  advisory?: {
134
502
  highWater: number;
@@ -161,6 +529,9 @@ export interface ActiveContextCheckpointV2 {
161
529
  prepared: PreparedFold | null;
162
530
  tokensSinceToolFold?: number;
163
531
  leases?: Record<string, number>;
532
+ surfacing?: SurfacingRecord[];
533
+ pendingMarks?: PendingMark[];
534
+ briefs?: Record<string, string>;
164
535
  advisory?: NonNullable<ActiveContextState["advisory"]>;
165
536
  stateSha256: string;
166
537
  }
@@ -179,6 +550,9 @@ export interface ActiveContextDeltaV2 {
179
550
  prepared: PreparedFold | null;
180
551
  tokensSinceToolFold?: number;
181
552
  leases?: Record<string, number>;
553
+ surfacing?: SurfacingRecord[];
554
+ pendingMarks?: PendingMark[];
555
+ briefs?: Record<string, string>;
182
556
  advisory?: NonNullable<ActiveContextState["advisory"]>;
183
557
  stateSha256: string;
184
558
  }
@@ -216,6 +590,8 @@ export interface ActiveContextSnapshot {
216
590
  brandNoun: string;
217
591
  entryTypePrefix: string;
218
592
  readOnlyTools: ReadonlySet<string>;
593
+ /** Active-context tool actions treated as read-only when classifying tool batches. */
594
+ readOnlyContextActions: ReadonlySet<string>;
219
595
  contextWindow: number;
220
596
  windowSource: "reported" | "fallback";
221
597
  }
@@ -230,4 +606,4 @@ export const READ_ONLY_TOOLS_DEFAULT = new Set([
230
606
  "read", "grep", "find", "ls",
231
607
  ]);
232
608
 
233
- export type AdvisoryMilestone = "notice" | "tools" | "chapters" | "urgent";
609
+ export type AdvisoryMilestone = "orientation" | "notice" | "tools" | "chapters" | "urgent";