pi-fold 0.0.1 → 0.1.1

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.
@@ -0,0 +1,2157 @@
1
+ import {
2
+ denseOwnArrayValues,
3
+ objectRefKey,
4
+ sha256Value,
5
+ stableStringify,
6
+ } from "./json.ts";
7
+ import {
8
+ advisorySchedule,
9
+ advisoryState,
10
+ clearArmedAdvisory,
11
+ liveAdvisoryText,
12
+ milestoneText,
13
+ normalizeGuidanceProfile,
14
+ surfacingText,
15
+ updateAdvisoryMilestone,
16
+ } from "./lib/advisory.ts";
17
+ import {
18
+ bytes,
19
+ clone,
20
+ emptyActiveContextState,
21
+ ownValue,
22
+ uniqueMessageDigestAnchor,
23
+ } from "./lib/canonical.ts";
24
+ import {
25
+ activeContextStatus,
26
+ automaticPreparationId,
27
+ commitPreparedFold,
28
+ encodedFoldSource,
29
+ foldCandidatesDetail,
30
+ foldTreeDetail,
31
+ peekFoldSource,
32
+ prepareFold,
33
+ preparedFoldError,
34
+ projectActiveContext,
35
+ projectionSlateCandidates,
36
+ protectEvidence,
37
+ requireActiveFold,
38
+ selectAutomaticChapter,
39
+ selectAutomaticRung,
40
+ setFoldProjectionState,
41
+ withExpandLease,
42
+ } from "./lib/folding.ts";
43
+ import {
44
+ boundReceiptText,
45
+ boundedInteger,
46
+ contextUsageRatio,
47
+ contextWindowFor,
48
+ hardFenceRatio,
49
+ latestProviderContextMeasurement,
50
+ orderedRoots,
51
+ parseNativeCompactionCompletion,
52
+ parseNativeCompactionDecision,
53
+ parseProviderContextMeasurementReceipt,
54
+ persistenceProjection,
55
+ providerContextMeasurement,
56
+ stringIds,
57
+ toolPayload,
58
+ } from "./lib/measurement.ts";
59
+ import type {
60
+ NativeCompactionCompletionReceipt,
61
+ NativeCompactionDecisionReceipt,
62
+ ProviderContextMeasurement,
63
+ ProviderMeasurementAnchor,
64
+ } from "./lib/measurement.ts";
65
+ import {
66
+ clearPrepared,
67
+ makeFoldRecordEntry,
68
+ makeStateCheckpoint,
69
+ makeStateDelta,
70
+ MAX_ACTIVE_FOLD_RECORDS,
71
+ materializeStatePersistence,
72
+ normalizeFoldsForPersistedRecords,
73
+ normalizeLegacyProvenance,
74
+ protectionSha256,
75
+ sameStateProjection,
76
+ semanticStateSha256,
77
+ topologySha256,
78
+ } from "./lib/persistence.ts";
79
+ import type { MaterializedStatePersistence } from "./lib/persistence.ts";
80
+ import {
81
+ activeContextSource,
82
+ ACTIVE_CONTEXT_POLICY,
83
+ ACTIVE_CONTEXT_STATUS_KEY,
84
+ ACTIVE_CONTEXT_TOOL_ACTIONS,
85
+ contextBrand,
86
+ DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN,
87
+ DEFAULT_ACTIVE_CONTEXT_COMMAND_NAMES,
88
+ DEFAULT_ACTIVE_CONTEXT_ENTRY_TYPE_PREFIX,
89
+ DEFAULT_ACTIVE_CONTEXT_TOOL_LABEL,
90
+ DEFAULT_ACTIVE_CONTEXT_TOOL_NAME,
91
+ DEFAULT_CONTEXT_WINDOW,
92
+ DEFAULT_SURFACING_ENABLED,
93
+ entryTypeNamespace,
94
+ READ_ONLY_TOOLS_DEFAULT,
95
+ USER_RESCUE_MAX_SOURCE_CHARS,
96
+ } from "./lib/policy.ts";
97
+ import type {
98
+ ActiveContextSnapshot,
99
+ ActiveContextState,
100
+ ActiveContextToolAction,
101
+ AdvisoryMilestone,
102
+ FoldCandidate,
103
+ FoldKind,
104
+ FoldRecordEntry,
105
+ GuidanceProfile,
106
+ PreparedFold,
107
+ SuggestionSource,
108
+ SurfacingSuggestion,
109
+ } from "./lib/policy.ts";
110
+ import {
111
+ automaticToolBrief,
112
+ deterministicChapterCandidateBrief,
113
+ deterministicConsolidationBrief,
114
+ manualFoldCandidate,
115
+ selectAutomaticToolBatch,
116
+ selectAutomaticToolForRung,
117
+ } from "./lib/selection.ts";
118
+ import {
119
+ acceptSurfacingSuggestion,
120
+ FOLD_BRIEF_SUGGESTION_SOURCE,
121
+ surfacingOrdinal,
122
+ updateSurfacing,
123
+ } from "./lib/surfacing.ts";
124
+ import { buildActiveContextCommands, buildActiveContextTool } from "./lib/tool-surface.ts";
125
+ import { mapActiveContext } from "./lib/transcript.ts";
126
+
127
+ // Test seam only; the package API is registerPiFold because package.json exports block deep imports.
128
+ export * from "./lib/advisory.ts";
129
+ export * from "./lib/canonical.ts";
130
+ export * from "./lib/folding.ts";
131
+ export * from "./lib/measurement.ts";
132
+ export * from "./lib/persistence.ts";
133
+ export * from "./lib/policy.ts";
134
+ export * from "./lib/selection.ts";
135
+ export * from "./lib/surfacing.ts";
136
+ export * from "./lib/transcript.ts";
137
+
138
+ /** Handle a suggestion source keeps: withdraw itself, and report that its item was acted on. */
139
+ export interface SuggestionSourceHandle {
140
+ unregister: () => void;
141
+ accepted: (id: string) => void;
142
+ }
143
+
144
+ export type SuggestionSourceRegistrar = (source: SuggestionSource) => SuggestionSourceHandle;
145
+
146
+ export function registerActiveContext(pi: any, options: {
147
+ summarizeContextSpan?: (request: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>;
148
+ setProjectionProvider?: (provider: (ctx: any) => Array<Record<string, unknown>>) => void;
149
+ setSuggestionSourceRegistrar?: (register: SuggestionSourceRegistrar) => void;
150
+ surfacing?: boolean;
151
+ toolActions?: readonly ActiveContextToolAction[];
152
+ toolName?: string;
153
+ toolLabel?: string;
154
+ brandNoun?: string;
155
+ entryTypePrefix?: string;
156
+ commandPrefix?: string;
157
+ commandNames?: { status?: string; fold?: string };
158
+ readOnlyTools?: ReadonlySet<string>;
159
+ blockingTools?: readonly string[];
160
+ guidance?: GuidanceProfile;
161
+ }): {
162
+ projectionCandidates: (ctx: any) => Array<Record<string, unknown>>;
163
+ registerSuggestionSource: SuggestionSourceRegistrar;
164
+ } {
165
+ const toolName = options.toolName ?? DEFAULT_ACTIVE_CONTEXT_TOOL_NAME;
166
+ const toolLabel = options.toolLabel ?? DEFAULT_ACTIVE_CONTEXT_TOOL_LABEL;
167
+ const brandNoun = options.brandNoun ?? DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN;
168
+ const entryTypePrefix = options.entryTypePrefix ?? DEFAULT_ACTIVE_CONTEXT_ENTRY_TYPE_PREFIX;
169
+ const commandPrefix = options.commandPrefix ?? "";
170
+ const readOnlyTools = options.readOnlyTools ?? READ_ONLY_TOOLS_DEFAULT;
171
+ const guidance = normalizeGuidanceProfile(options.guidance);
172
+ if (options.surfacing !== undefined && typeof options.surfacing !== "boolean") {
173
+ throw new Error("surfacing must be a boolean");
174
+ }
175
+ const surfacingEnabled = options.surfacing ?? DEFAULT_SURFACING_ENABLED;
176
+ if (!toolName || !toolLabel || !brandNoun || !entryTypePrefix || typeof commandPrefix !== "string" ||
177
+ (commandPrefix && !/^[a-z0-9-]+$/.test(commandPrefix)) ||
178
+ [...readOnlyTools].some((name) => typeof name !== "string" || !name)) {
179
+ throw new Error("Active-context names and read-only tools must be nonempty strings");
180
+ }
181
+ const commandStem = commandPrefix ? `${commandPrefix.replace(/-+$/, "")}-` : "";
182
+ // Full-name override for hosts that need non-default command names (e.g. the
183
+ // pi-fold package's neutral "context"); commandPrefix remains the derived form.
184
+ const commandNames = {
185
+ status: options.commandNames?.status ?? `${commandStem}${DEFAULT_ACTIVE_CONTEXT_COMMAND_NAMES.status}`,
186
+ fold: options.commandNames?.fold ?? `${commandStem}${DEFAULT_ACTIVE_CONTEXT_COMMAND_NAMES.fold}`,
187
+ };
188
+ if (![commandNames.status, commandNames.fold].every((name) =>
189
+ typeof name === "string" && /^[a-z0-9][a-z0-9-]*$/.test(name)) ||
190
+ commandNames.status === commandNames.fold) {
191
+ throw new Error("Active-context command names must be distinct kebab-case strings");
192
+ }
193
+ const configuredBlockingTools = denseOwnArrayValues(options.blockingTools ?? ["Agent"]);
194
+ if (!configuredBlockingTools || configuredBlockingTools.some((name) => typeof name !== "string" || !name) ||
195
+ new Set(configuredBlockingTools).size !== configuredBlockingTools.length) {
196
+ throw new Error("Blocking tools must be one dense array of unique nonempty strings");
197
+ }
198
+ const blockingTools = new Set(configuredBlockingTools as string[]);
199
+ const stateEntryType = `${entryTypePrefix}-state`;
200
+ const foldRecordEntryType = `${entryTypePrefix}-fold-record`;
201
+ const milestoneProjectionType = `${entryTypePrefix}-milestone`;
202
+ const advisoryProjectionType = `${entryTypePrefix}-advisory`;
203
+ const surfacingProjectionType = `${entryTypePrefix}-surfacing`;
204
+ const entryNamespace = entryTypeNamespace(entryTypePrefix);
205
+ const providerMeasurementEntryType = `${entryNamespace}-provider-context-measurement`;
206
+ const nativeReceiptEntryType = `${entryNamespace}-native-compaction-receipt`;
207
+ const nativeDecisionEntryType = `${entryNamespace}-native-compaction-decision`;
208
+ const configuredToolActions = denseOwnArrayValues(
209
+ options.toolActions ?? ACTIVE_CONTEXT_TOOL_ACTIONS,
210
+ );
211
+ if (!configuredToolActions || configuredToolActions.length < 1) {
212
+ throw new Error("Active-context tool actions must be one non-empty dense array");
213
+ }
214
+ const allowedToolActions: ActiveContextToolAction[] = [];
215
+ const allowedToolActionSet = new Set<string>();
216
+ for (const value of configuredToolActions) {
217
+ if (typeof value !== "string" ||
218
+ !ACTIVE_CONTEXT_TOOL_ACTIONS.includes(value as ActiveContextToolAction) ||
219
+ allowedToolActionSet.has(value)) {
220
+ throw new Error(`Invalid or duplicate active-context tool action '${String(value)}'`);
221
+ }
222
+ allowedToolActions.push(value as ActiveContextToolAction);
223
+ allowedToolActionSet.add(value);
224
+ }
225
+ Object.freeze(allowedToolActions);
226
+
227
+ type AutomaticFailureState = {
228
+ key: string;
229
+ phase: string;
230
+ message: string;
231
+ firstFailedAt: number;
232
+ attempts: 1;
233
+ suppressedCallbacks: number;
234
+ persistenceDisposition: "none" | "record-only" | "state-committed";
235
+ };
236
+
237
+ // Owns in-memory/durable state and wire/record bookkeeping; persistenceQueue serializes it.
238
+ // Once the state event succeeds, replay owns this exact state; RAM may not roll behind it.
239
+ const persistence = {
240
+ state: null as ActiveContextState | null,
241
+ persisted: null as ActiveContextState | null,
242
+ persistedWireVersion: 0 as 0 | 1 | 2,
243
+ persistedStateSha256: "",
244
+ persistedFoldRecords: new Map<string, FoldRecordEntry>(),
245
+ persistenceQueue: Promise.resolve<void>(undefined),
246
+ };
247
+
248
+ // Owns preparation/ladder rollback, selection, and suspension; actionQueue serializes it.
249
+ const ladder = {
250
+ pendingManual: false,
251
+ preparing: null as { id: string; controller: AbortController; promise: Promise<void> } | null,
252
+ lastPreparationError: null as string | null,
253
+ boundaryFailure: null as string | null,
254
+ lastPreparationCandidateId: null as string | null,
255
+ lastSelectionKind: null as FoldKind | "refold" | null,
256
+ lastSelectionSourceIds: [] as string[],
257
+ pendingContextNote: null as string | null,
258
+ lastAutomaticAction: null as Record<string, unknown> | null,
259
+ automaticFailure: null as AutomaticFailureState | null,
260
+ failedPreparations: new Set<string>(),
261
+ actionQueue: Promise.resolve<unknown>(undefined),
262
+ };
263
+
264
+ // Owns provider receipts, anchors, and usage; providerMeasurementQueue serializes receipt writes.
265
+ const measurements = {
266
+ latestRatio: null as number | null,
267
+ lastProviderMeasurement: null as ProviderContextMeasurement | null,
268
+ providerMeasurementQueue: Promise.resolve<void>(undefined),
269
+ providerMeasurementReceipts: new Set<string>(),
270
+ providerMeasurementRevisionByMessageSha: new Map<string, number>(),
271
+ providerMeasurementByMessageSha: new Map<string, ProviderContextMeasurementReceipt>(),
272
+ providerMeasurementAnchorByMessageSha: new Map<string, ProviderMeasurementAnchor>(),
273
+ };
274
+
275
+ // Owns advisory arming and hard-fence delivery; durable effects use persistenceQueue.
276
+ const advisory = {
277
+ historicalGuidanceEntries: 0,
278
+ armedMilestone: null as AdvisoryMilestone | null,
279
+ advisoryScheduleKey: null as string | null,
280
+ hardFenceNoticeKey: null as string | null,
281
+ hardFenceReleaseSessionId: null as string | null,
282
+ hardFenceReleasedProjectionKeys: new Set<string>(),
283
+ };
284
+
285
+ // Owns the suggestion sources feeding the ephemeral surfacing carrier plus the slate
286
+ // rendered on the current context event. Nothing here is durable: the carrier is
287
+ // rebuilt from state on every projection, and only the log lives in session state.
288
+ const surfacing = {
289
+ sources: [] as SuggestionSource[],
290
+ suggestions: [] as SurfacingSuggestion[],
291
+ };
292
+
293
+ // Owns native-compaction decisions and completion retry state; nativeReceiptQueue serializes it.
294
+ const nativeCompaction = {
295
+ lastThresholdDecision: null as Record<string, unknown> | null,
296
+ pendingNativeReceipt: null as NativeCompactionCompletionReceipt | null,
297
+ nativeReceiptQueue: Promise.resolve<void>(undefined),
298
+ };
299
+
300
+ // Owns session generation, snapshots/reloads, and per-turn blocking-tool harvest state.
301
+ // Pi normally requests context serially, but retries, reloads, and host
302
+ // integrations can overlap callbacks. Serialize the entire authority →
303
+ // preparation → commit → projection transaction so a follower cannot
304
+ // observe a published measurement before the leader's durable receipt or
305
+ // return raw final-rung context while the leader is preparing a brief.
306
+ const lifecycle = {
307
+ generation: 0,
308
+ shuttingDown: false,
309
+ latestSnapshot: null as ActiveContextSnapshot | null,
310
+ latestSnapshotError: null as string | null,
311
+ blockingToolHarvestedThisTurn: false,
312
+ blockingToolHarvestQueuedThisTurn: false,
313
+ contextQueue: Promise.resolve<void>(undefined),
314
+ };
315
+
316
+ const durableProviderMeasurementReceiptMatches = (
317
+ measurement: ProviderContextMeasurement,
318
+ projectionRevision: number,
319
+ ): boolean => {
320
+ const receipt = measurements.providerMeasurementByMessageSha.get(measurement.messageSha256);
321
+ return Boolean(receipt && receipt.projectionRevision === projectionRevision &&
322
+ receipt.provider === measurement.provider && receipt.model === measurement.model &&
323
+ receipt.tokens === measurement.tokens && receipt.contextWindow === measurement.contextWindow);
324
+ };
325
+
326
+ const durableProviderMeasurementMatches = (
327
+ measurement: ProviderContextMeasurement,
328
+ ): boolean => {
329
+ if (!persistence.state) return false;
330
+ const receipt = measurements.providerMeasurementByMessageSha.get(measurement.messageSha256);
331
+ const anchor = measurements.providerMeasurementAnchorByMessageSha.get(measurement.messageSha256);
332
+ return Boolean(receipt && anchor && anchor.sessionId === persistence.state.sessionId &&
333
+ anchor.generation === lifecycle.generation &&
334
+ anchor.topologySha256 === topologySha256(persistence.state) &&
335
+ anchor.protectionSha256 === protectionSha256(persistence.state) &&
336
+ receipt.provider === measurement.provider && receipt.model === measurement.model &&
337
+ receipt.tokens === measurement.tokens && receipt.contextWindow === measurement.contextWindow);
338
+ };
339
+
340
+ const safeNotify = (ctx: any, message: string, level: "info" | "warning" | "error"): void => {
341
+ try { ctx.ui?.notify?.(message, level); } catch { /* Presentation cannot block Pi lifecycle progress. */ }
342
+ };
343
+
344
+ const contextSessionMatches = (ctx: any, sessionId: string): boolean => {
345
+ try { return ctx.sessionManager.getSessionId() === sessionId; }
346
+ catch { return false; }
347
+ };
348
+
349
+ const sessionIdentityStillValid = (ctx: any, sessionId: string, expectedGeneration: number): boolean =>
350
+ lifecycle.generation === expectedGeneration && persistence.state?.sessionId === sessionId &&
351
+ (!ctx || contextSessionMatches(ctx, sessionId));
352
+
353
+ const updateStatus = (ctx: any): void => {
354
+ try {
355
+ const roots = persistence.state && lifecycle.latestSnapshot ? orderedRoots(persistence.state, lifecycle.latestSnapshot).length : 0;
356
+ const prepared = persistence.state?.prepared ? " · brief ready" : ladder.preparing ? " · briefing" : "";
357
+ const usage = measurements.lastProviderMeasurement
358
+ ? ` · provider ${measurements.lastProviderMeasurement.tokens}/${measurements.lastProviderMeasurement.contextWindow}`
359
+ : " · provider usage unmeasured";
360
+ const suspended = ladder.automaticFailure ? " · automatic suspended" : "";
361
+ ctx.ui?.setStatus?.(entryTypePrefix, `${toolName} folds: ${roots}${prepared}${usage}${suspended}`);
362
+ } catch { /* Status presentation is request-ephemeral and never a lifecycle boundary. */ }
363
+ };
364
+
365
+ const snapshotForEvent = (ctx: any, messages: unknown[]): ActiveContextSnapshot => mapActiveContext({
366
+ sessionId: ctx.sessionManager.getSessionId(),
367
+ eventMessages: messages,
368
+ contextEntries: ctx.sessionManager.buildContextEntries(),
369
+ toolName,
370
+ brandNoun,
371
+ entryTypePrefix,
372
+ readOnlyTools,
373
+ contextWindow: contextWindowFor(ctx) ?? undefined,
374
+ });
375
+
376
+ const authoritativeSnapshotFor = (ctx: any): ActiveContextSnapshot => {
377
+ const sessionId = ctx.sessionManager.getSessionId();
378
+ if (!lifecycle.latestSnapshot || lifecycle.latestSnapshot.sessionId !== sessionId) {
379
+ throw new Error("A current same-session Pi context event is required");
380
+ }
381
+ return mapActiveContext({
382
+ sessionId,
383
+ eventMessages: lifecycle.latestSnapshot.messages,
384
+ contextEntries: ctx.sessionManager.buildContextEntries(),
385
+ policy: lifecycle.latestSnapshot.policy,
386
+ toolName,
387
+ brandNoun,
388
+ entryTypePrefix,
389
+ readOnlyTools,
390
+ contextWindow: contextWindowFor(ctx) ?? undefined,
391
+ });
392
+ };
393
+
394
+ const providerMeasurementBranchIndex = (
395
+ ctx: any,
396
+ measurement: ProviderContextMeasurement,
397
+ ): number => {
398
+ let branch: unknown[] | null = null;
399
+ try { branch = denseOwnArrayValues(ctx.sessionManager.getBranch()); }
400
+ catch { return -1; }
401
+ if (!branch) return -1;
402
+ const anchor = uniqueMessageDigestAnchor(branch, measurement.messageSha256);
403
+ if (!anchor) return -1;
404
+ const candidate = providerContextMeasurement(
405
+ anchor.message,
406
+ measurement.contextWindow,
407
+ { provider: measurement.provider, id: measurement.model },
408
+ );
409
+ return candidate?.messageSha256 === measurement.messageSha256 &&
410
+ candidate.tokens === measurement.tokens ? anchor.index : -1;
411
+ };
412
+
413
+ const cancelPreparation = (): void => {
414
+ ladder.preparing?.controller.abort();
415
+ ladder.preparing = null;
416
+ };
417
+
418
+ const providerMeasurementReceiptKey = (
419
+ sessionId: string,
420
+ projectionRevision: number,
421
+ measurement: Pick<ProviderContextMeasurement, "messageSha256" | "provider" | "model">,
422
+ ): string => sha256Value({
423
+ sessionId,
424
+ projectionRevision,
425
+ messageSha256: measurement.messageSha256,
426
+ provider: measurement.provider,
427
+ model: measurement.model,
428
+ });
429
+
430
+ const load = (ctx: any, preserveThresholdDecision = false): void => {
431
+ lifecycle.generation += 1;
432
+ lifecycle.shuttingDown = false;
433
+ cancelPreparation();
434
+ lifecycle.latestSnapshot = null;
435
+ lifecycle.latestSnapshotError = null;
436
+ measurements.latestRatio = null;
437
+ measurements.lastProviderMeasurement = null;
438
+ ladder.pendingManual = false;
439
+ lifecycle.blockingToolHarvestedThisTurn = false;
440
+ lifecycle.blockingToolHarvestQueuedThisTurn = false;
441
+ if (!preserveThresholdDecision) nativeCompaction.lastThresholdDecision = null;
442
+ ladder.lastPreparationError = null;
443
+ ladder.boundaryFailure = null;
444
+ ladder.lastPreparationCandidateId = null;
445
+ ladder.lastSelectionKind = null;
446
+ ladder.lastSelectionSourceIds = [];
447
+ ladder.pendingContextNote = null;
448
+ advisory.historicalGuidanceEntries = 0;
449
+ advisory.armedMilestone = null;
450
+ advisory.advisoryScheduleKey = null;
451
+ ladder.lastAutomaticAction = null;
452
+ ladder.automaticFailure = null;
453
+ advisory.hardFenceNoticeKey = null;
454
+ ladder.failedPreparations.clear();
455
+ measurements.providerMeasurementReceipts.clear();
456
+ measurements.providerMeasurementRevisionByMessageSha.clear();
457
+ measurements.providerMeasurementByMessageSha.clear();
458
+ measurements.providerMeasurementAnchorByMessageSha.clear();
459
+ const sessionId = ctx.sessionManager.getSessionId();
460
+ if (advisory.hardFenceReleaseSessionId !== sessionId) {
461
+ advisory.hardFenceReleaseSessionId = sessionId;
462
+ advisory.hardFenceReleasedProjectionKeys.clear();
463
+ }
464
+ let restored: ActiveContextState | null = null;
465
+ let restoreError: unknown = null;
466
+ let restoredPersistence: MaterializedStatePersistence | null = null;
467
+ let measurementRestoreError: unknown = null;
468
+ const branchEntries = [...ctx.sessionManager.getBranch()];
469
+ try {
470
+ restoredPersistence = materializeStatePersistence(
471
+ branchEntries,
472
+ sessionId,
473
+ stateEntryType,
474
+ foldRecordEntryType,
475
+ );
476
+ restored = restoredPersistence.state;
477
+ } catch (error) {
478
+ restoreError = error;
479
+ }
480
+ for (const entry of branchEntries) {
481
+ if (entry?.type !== "custom") continue;
482
+ if (typeof entry.customType === "string" && [
483
+ `${entryTypePrefix}-guidance-`,
484
+ `${ACTIVE_CONTEXT_STATUS_KEY}-guidance-`,
485
+ ].some((prefix) => entry.customType.startsWith(prefix))) {
486
+ advisory.historicalGuidanceEntries += 1;
487
+ continue;
488
+ }
489
+ if (entry.customType !== providerMeasurementEntryType) continue;
490
+ try {
491
+ const receipt = parseProviderContextMeasurementReceipt(entry.data, sessionId);
492
+ const boundRevision = measurements.providerMeasurementRevisionByMessageSha.get(receipt.messageSha256);
493
+ if (boundRevision !== undefined && boundRevision !== receipt.projectionRevision) {
494
+ throw new Error("One provider response is bound to multiple projection revisions");
495
+ }
496
+ measurements.providerMeasurementRevisionByMessageSha.set(receipt.messageSha256, receipt.projectionRevision);
497
+ const priorReceipt = measurements.providerMeasurementByMessageSha.get(receipt.messageSha256);
498
+ if (priorReceipt && stableStringify(priorReceipt) !== stableStringify(receipt)) {
499
+ throw new Error("One provider response has conflicting durable measurement receipts");
500
+ }
501
+ measurements.providerMeasurementByMessageSha.set(receipt.messageSha256, receipt);
502
+ const fingerprint = restoredPersistence?.projectionFingerprints.get(receipt.projectionRevision);
503
+ if (fingerprint) {
504
+ measurements.providerMeasurementAnchorByMessageSha.set(receipt.messageSha256, {
505
+ sessionId,
506
+ generation: lifecycle.generation,
507
+ ...fingerprint,
508
+ });
509
+ }
510
+ measurements.providerMeasurementReceipts.add(providerMeasurementReceiptKey(
511
+ sessionId,
512
+ receipt.projectionRevision,
513
+ receipt,
514
+ ));
515
+ } catch (error) {
516
+ measurementRestoreError = error;
517
+ }
518
+ }
519
+ const durableRestored = restored ?? emptyActiveContextState(sessionId);
520
+ persistence.state = durableRestored.prepared ? clearPrepared(durableRestored) : clone(durableRestored);
521
+ advisory.armedMilestone = advisoryState(persistence.state).armed?.milestone ?? null;
522
+ persistence.persistedWireVersion = restoredPersistence?.wireVersion ?? 0;
523
+ persistence.persistedFoldRecords = restoredPersistence?.records ?? new Map<string, FoldRecordEntry>();
524
+ persistence.persistedStateSha256 = restoredPersistence?.stateSha256 ?? semanticStateSha256(durableRestored);
525
+ const restoredMessages = ctx.sessionManager.buildSessionContext?.()?.messages;
526
+ measurements.lastProviderMeasurement = latestProviderContextMeasurement(
527
+ Array.isArray(restoredMessages) ? restoredMessages : [],
528
+ contextWindowFor(ctx),
529
+ ctx.model,
530
+ );
531
+ measurements.latestRatio = contextUsageRatio(measurements.lastProviderMeasurement);
532
+ persistence.persisted = clone(durableRestored);
533
+ if (restoreError) safeNotify(
534
+ ctx,
535
+ `Active-context state was ignored; Pi native context remains authoritative: ${String(restoreError)}`,
536
+ "warning",
537
+ );
538
+ if (measurementRestoreError) safeNotify(
539
+ ctx,
540
+ `Malformed provider measurement receipt was ignored; automatic context remains unmeasured: ${String(measurementRestoreError)}`,
541
+ "warning",
542
+ );
543
+ updateStatus(ctx);
544
+ };
545
+ const persist = (ctx?: any): Promise<void> => {
546
+ const operation = persistence.persistenceQueue.then(async () => {
547
+ if (!persistence.state || !persistence.persisted) return;
548
+ let next = clone(persistence.state);
549
+ if (ctx && lifecycle.latestSnapshot?.sessionId === next.sessionId) {
550
+ next = persistenceProjection(next, authoritativeSnapshotFor(ctx));
551
+ }
552
+ next.folds = normalizeFoldsForPersistedRecords(next.folds, persistence.persistedFoldRecords);
553
+ if (sameStateProjection(next, persistence.persisted)) {
554
+ persistence.state = clone(persistence.persisted);
555
+ return;
556
+ }
557
+ if (next.revision <= persistence.persisted.revision) next.revision = persistence.persisted.revision + 1;
558
+ const generationAtStart = lifecycle.generation;
559
+ const sessionId = next.sessionId;
560
+ if (!sessionIdentityStillValid(ctx, sessionId, generationAtStart)) {
561
+ throw new Error("Active-context session changed before persistence");
562
+ }
563
+ if (next.folds.length > MAX_ACTIVE_FOLD_RECORDS) {
564
+ throw new Error("Active-context fold closure exceeds persistence limit");
565
+ }
566
+ for (const fold of next.folds) {
567
+ const record = makeFoldRecordEntry(fold, sessionId);
568
+ const existing = persistence.persistedFoldRecords.get(record.foldId);
569
+ if (existing) {
570
+ if (existing.recordSha256 !== record.recordSha256) {
571
+ throw new Error(`Conflicting durable active-context fold ${record.foldId}`);
572
+ }
573
+ continue;
574
+ }
575
+ await pi.appendEntry(foldRecordEntryType, record);
576
+ persistence.persistedFoldRecords.set(record.foldId, record);
577
+ if (!sessionIdentityStillValid(ctx, sessionId, generationAtStart)) {
578
+ if (ctx && !contextSessionMatches(ctx, sessionId)) load(ctx);
579
+ throw new Error("Active-context session changed after fold-record persistence");
580
+ }
581
+ }
582
+ const wire = persistence.persistedWireVersion === 2 ? makeStateDelta(persistence.persisted, next) : makeStateCheckpoint(next);
583
+ if (persistence.persistedWireVersion === 2 && persistence.persistedStateSha256 !== semanticStateSha256(persistence.persisted)) {
584
+ throw new Error("Active-context durable base digest drift");
585
+ }
586
+ await pi.appendEntry(stateEntryType, wire);
587
+ persistence.persisted = clone(next);
588
+ persistence.persistedWireVersion = 2;
589
+ persistence.persistedStateSha256 = semanticStateSha256(next);
590
+ persistence.state = lifecycle.shuttingDown ? null : clone(next);
591
+ if (!sessionIdentityStillValid(ctx, sessionId, generationAtStart)) {
592
+ if (ctx && !contextSessionMatches(ctx, sessionId)) load(ctx);
593
+ throw new Error("Active-context session changed after durable persistence");
594
+ }
595
+ });
596
+ persistence.persistenceQueue = operation.catch(() => undefined);
597
+ return operation;
598
+ };
599
+
600
+ const persistThroughActionQueue = (ctx?: any): Promise<void> => {
601
+ const operation = ladder.actionQueue.then(() => persist(ctx));
602
+ ladder.actionQueue = operation.catch(() => undefined);
603
+ return operation;
604
+ };
605
+
606
+ const persistProviderMeasurement = (
607
+ ctx: any,
608
+ measurement: ProviderContextMeasurement,
609
+ projectionRevision: number,
610
+ ): Promise<boolean> => {
611
+ if (!persistence.state || !Number.isSafeInteger(projectionRevision) || projectionRevision < 0) {
612
+ return Promise.resolve(false);
613
+ }
614
+ const generationAtStart = lifecycle.generation;
615
+ const sessionId = persistence.state.sessionId;
616
+ const queuedMeasurement = clone(measurement);
617
+ const revision = projectionRevision;
618
+ const anchor: ProviderMeasurementAnchor = {
619
+ sessionId,
620
+ generation: generationAtStart,
621
+ topologySha256: topologySha256(persistence.state),
622
+ protectionSha256: protectionSha256(persistence.state),
623
+ };
624
+ const operation = measurements.providerMeasurementQueue.then(async () => {
625
+ if (lifecycle.shuttingDown || !sessionIdentityStillValid(ctx, sessionId, generationAtStart)) {
626
+ throw new Error("Active-context session changed before measurement persistence");
627
+ }
628
+ const measurement = queuedMeasurement;
629
+ const boundRevision = measurements.providerMeasurementRevisionByMessageSha.get(measurement.messageSha256);
630
+ if (boundRevision !== undefined) {
631
+ return boundRevision === revision &&
632
+ durableProviderMeasurementReceiptMatches(measurement, revision);
633
+ }
634
+ const receiptKey = providerMeasurementReceiptKey(sessionId, revision, measurement);
635
+ if (measurements.providerMeasurementReceipts.has(receiptKey)) {
636
+ measurements.providerMeasurementRevisionByMessageSha.set(measurement.messageSha256, revision);
637
+ return durableProviderMeasurementReceiptMatches(measurement, revision);
638
+ }
639
+ const receipt = parseProviderContextMeasurementReceipt({
640
+ version: 1,
641
+ sessionId,
642
+ projectionRevision: revision,
643
+ messageSha256: measurement.messageSha256,
644
+ provider: measurement.provider,
645
+ model: measurement.model,
646
+ tokens: measurement.tokens,
647
+ contextWindow: measurement.contextWindow,
648
+ occurredAt: Date.now(),
649
+ }, sessionId);
650
+ await pi.appendEntry(providerMeasurementEntryType, receipt);
651
+ // The append is authoritative even if lifecycle attribution changes immediately after it.
652
+ measurements.providerMeasurementReceipts.add(receiptKey);
653
+ measurements.providerMeasurementRevisionByMessageSha.set(measurement.messageSha256, revision);
654
+ measurements.providerMeasurementByMessageSha.set(measurement.messageSha256, receipt);
655
+ measurements.providerMeasurementAnchorByMessageSha.set(measurement.messageSha256, anchor);
656
+ if (!sessionIdentityStillValid(ctx, sessionId, generationAtStart)) {
657
+ throw new Error("Active-context session changed during measurement persistence");
658
+ }
659
+ return true;
660
+ });
661
+ measurements.providerMeasurementQueue = operation.then(() => undefined, () => undefined);
662
+ return operation;
663
+ };
664
+
665
+ const makeNativeDecision = (
666
+ ctx: any,
667
+ reason: string,
668
+ willRetry: boolean,
669
+ failureCode: string,
670
+ message: string,
671
+ ): NativeCompactionDecisionReceipt => {
672
+ const sessionId = ctx.sessionManager.getSessionId();
673
+ const triggerEntryId = boundReceiptText(ctx.sessionManager.getLeafId?.(), 512, "no-leaf");
674
+ const boundedReason = boundReceiptText(reason, 64, "unknown");
675
+ const boundedFailureCode = boundReceiptText(failureCode, 128, "unspecified-native-fallback");
676
+ const decision: NativeCompactionDecisionReceipt = {
677
+ version: 1,
678
+ decisionKey: sha256Value({
679
+ sessionId, triggerEntryId, reason: boundedReason, failureCode: boundedFailureCode,
680
+ }),
681
+ sessionId,
682
+ triggerEntryId,
683
+ reason: boundedReason,
684
+ willRetry,
685
+ failureCode: boundedFailureCode,
686
+ message: boundReceiptText(message, 1_200, "Pi native compaction safety net allowed"),
687
+ preparationError: ladder.lastPreparationError
688
+ ? boundReceiptText(ladder.lastPreparationError, 1_200, "context preparation failed")
689
+ : null,
690
+ boundaryFailure: ladder.boundaryFailure
691
+ ? boundReceiptText(ladder.boundaryFailure, 1_200, "context boundary failed")
692
+ : null,
693
+ selectionKind: ladder.lastSelectionKind ? boundReceiptText(ladder.lastSelectionKind, 64, "unknown") : null,
694
+ selectionSourceIds: ladder.lastSelectionSourceIds.slice(0, 64)
695
+ .map((id) => boundReceiptText(id, 512, "unknown-source")),
696
+ automaticActionKind: typeof ownValue(ladder.lastAutomaticAction, "kind") === "string"
697
+ ? boundReceiptText(ownValue(ladder.lastAutomaticAction, "kind"), 128, "unknown")
698
+ : null,
699
+ providerMessageSha256: measurements.lastProviderMeasurement?.messageSha256 ?? null,
700
+ occurredAt: Date.now(),
701
+ };
702
+ return parseNativeCompactionDecision(decision, sessionId);
703
+ };
704
+
705
+ const buildNativeCompletion = (
706
+ event: Record<string, unknown>,
707
+ ctx: any,
708
+ decision: NativeCompactionDecisionReceipt,
709
+ ): NativeCompactionCompletionReceipt => {
710
+ const entry = ownValue(event, "compactionEntry");
711
+ const compactionEntryId = String(ownValue(entry, "id") ?? "");
712
+ const sessionId = ctx.sessionManager.getSessionId();
713
+ const entries = ctx.sessionManager.getEntries?.() ?? ctx.sessionManager.getBranch();
714
+ const persistedCompaction = entries.find((candidate: any) =>
715
+ candidate?.type === "compaction" && candidate.id === compactionEntryId);
716
+ if (!persistedCompaction) {
717
+ throw new Error(`Native completion does not reference a persisted compaction entry: ${compactionEntryId || "missing"}`);
718
+ }
719
+ return parseNativeCompactionCompletion({
720
+ version: 2,
721
+ receiptKey: sha256Value({ sessionId, compactionEntryId }),
722
+ sessionId,
723
+ compactionEntryId,
724
+ reason: boundReceiptText(ownValue(event, "reason"), 64, decision.reason),
725
+ willRetry: ownValue(event, "willRetry") === true,
726
+ fromExtension: ownValue(event, "fromExtension") === true,
727
+ occurredAt: Date.now(),
728
+ goal: "zero-native-compactions",
729
+ decision,
730
+ }, sessionId);
731
+ };
732
+
733
+ const persistNativeCompletion = (receipt: NativeCompactionCompletionReceipt, ctx: any): Promise<void> => {
734
+ const operation = nativeCompaction.nativeReceiptQueue.then(async () => {
735
+ const entries = ctx.sessionManager.getEntries?.() ?? ctx.sessionManager.getBranch();
736
+ const existing = entries
737
+ .filter((entry: any) => entry?.type === "custom" && entry.customType === nativeReceiptEntryType &&
738
+ ownValue(entry.data, "version") === 2)
739
+ .map((entry: any) => parseNativeCompactionCompletion(entry.data, receipt.sessionId))
740
+ .find((item: NativeCompactionCompletionReceipt) => item.receiptKey === receipt.receiptKey);
741
+ if (existing) {
742
+ if (stableStringify(existing) !== stableStringify(receipt)) {
743
+ throw new Error(`Conflicting native compaction receipt ${receipt.receiptKey}`);
744
+ }
745
+ } else {
746
+ await pi.appendEntry(nativeReceiptEntryType, receipt);
747
+ }
748
+ nativeCompaction.pendingNativeReceipt = null;
749
+ });
750
+ nativeCompaction.nativeReceiptQueue = operation.catch(() => undefined);
751
+ return operation;
752
+ };
753
+
754
+ const recoverNativeReceipts = async (ctx: any): Promise<void> => {
755
+ if (nativeCompaction.pendingNativeReceipt) await persistNativeCompletion(nativeCompaction.pendingNativeReceipt, ctx);
756
+ const sessionId = ctx.sessionManager.getSessionId();
757
+ const entries = [...(ctx.sessionManager.getEntries?.() ?? ctx.sessionManager.getBranch())];
758
+ const completions = new Map<string, NativeCompactionCompletionReceipt>();
759
+ for (const entry of entries) {
760
+ if (entry?.type !== "custom" || entry.customType !== nativeReceiptEntryType ||
761
+ ownValue(entry.data, "version") !== 2) continue;
762
+ const receipt = parseNativeCompactionCompletion(entry.data, sessionId);
763
+ completions.set(receipt.decision.decisionKey, receipt);
764
+ }
765
+ const usedCompactions = new Set([...completions.values()].map((receipt) => receipt.compactionEntryId));
766
+ for (let index = 0; index < entries.length; index += 1) {
767
+ const entry = entries[index] as any;
768
+ if (entry?.type !== "custom" || entry.customType !== nativeDecisionEntryType) continue;
769
+ const decision = parseNativeCompactionDecision(entry.data, sessionId);
770
+ if (completions.has(decision.decisionKey)) continue;
771
+ const compaction = entries.find((candidate: any) =>
772
+ candidate?.type === "compaction" && !usedCompactions.has(candidate.id) &&
773
+ (candidate.parentId === entry.id || candidate.id === decision.triggerEntryId)) as any;
774
+ if (!compaction) continue;
775
+ const receipt = parseNativeCompactionCompletion({
776
+ version: 2,
777
+ receiptKey: sha256Value({ sessionId, compactionEntryId: compaction.id }),
778
+ sessionId,
779
+ compactionEntryId: compaction.id,
780
+ reason: decision.reason,
781
+ willRetry: decision.willRetry,
782
+ fromExtension: compaction.fromHook === true,
783
+ occurredAt: Number.isFinite(Date.parse(String(compaction.timestamp)))
784
+ ? Date.parse(String(compaction.timestamp))
785
+ : decision.occurredAt,
786
+ goal: "zero-native-compactions",
787
+ decision,
788
+ }, sessionId);
789
+ await persistNativeCompletion(receipt, ctx);
790
+ completions.set(decision.decisionKey, receipt);
791
+ usedCompactions.add(compaction.id);
792
+ }
793
+ const latestCompletion = [...completions.values()]
794
+ .sort((left, right) => left.occurredAt - right.occurredAt)
795
+ .at(-1);
796
+ if (latestCompletion) {
797
+ nativeCompaction.lastThresholdDecision = {
798
+ handled: true,
799
+ retry: false,
800
+ reason: `native compaction completed; ${brandNoun} folding state rebuilt`,
801
+ compactionReason: latestCompletion.reason,
802
+ nativeCompactionCompleted: true,
803
+ receiptKey: latestCompletion.receiptKey,
804
+ decision: latestCompletion.decision,
805
+ };
806
+ }
807
+ };
808
+
809
+ const markManual = (
810
+ next: ActiveContextState,
811
+ action: Exclude<ActiveContextToolAction, "status">,
812
+ ): void => {
813
+ cancelPreparation();
814
+ persistence.state = clearPrepared(next);
815
+ ladder.pendingManual = true;
816
+ if (action === "fold") advisory.armedMilestone = null;
817
+ ladder.boundaryFailure = null;
818
+ };
819
+
820
+ const persistManual = async (
821
+ next: ActiveContextState,
822
+ action: Exclude<ActiveContextToolAction, "status">,
823
+ ctx: any,
824
+ ): Promise<void> => {
825
+ const stateAtEntry = persistence.state ? clone(persistence.state) : null;
826
+ const persistedAtEntry = persistence.persisted;
827
+ const transientAtEntry = captureTransient();
828
+ markManual(next, action);
829
+ try {
830
+ await persist(ctx);
831
+ ladder.pendingManual = false;
832
+ ladder.automaticFailure = null;
833
+ ladder.boundaryFailure = null;
834
+ } catch (error) {
835
+ if (persistence.persisted === persistedAtEntry && stateAtEntry) {
836
+ persistence.state = stateAtEntry;
837
+ restoreTransient(transientAtEntry);
838
+ }
839
+ ladder.boundaryFailure = error instanceof Error ? error.message : String(error);
840
+ safeNotify(ctx, `Active-context change was not persisted: ${ladder.boundaryFailure}`, "error");
841
+ throw error;
842
+ }
843
+ };
844
+
845
+ const captureTransient = () => ({
846
+ pendingManual: ladder.pendingManual,
847
+ preparing: ladder.preparing,
848
+ pendingContextNote: ladder.pendingContextNote,
849
+ armedMilestone: advisory.armedMilestone,
850
+ lastAutomaticAction: ladder.lastAutomaticAction,
851
+ automaticFailure: ladder.automaticFailure ? clone(ladder.automaticFailure) : null,
852
+ boundaryFailure: ladder.boundaryFailure,
853
+ });
854
+ const restoreTransient = (saved: ReturnType<typeof captureTransient>): void => {
855
+ ladder.pendingManual = saved.pendingManual;
856
+ ladder.preparing = saved.preparing?.controller.signal.aborted ? null : saved.preparing;
857
+ ladder.pendingContextNote = saved.pendingContextNote;
858
+ advisory.armedMilestone = saved.armedMilestone;
859
+ ladder.lastAutomaticAction = saved.lastAutomaticAction;
860
+ ladder.automaticFailure = saved.automaticFailure;
861
+ ladder.boundaryFailure = saved.boundaryFailure;
862
+ };
863
+ const automaticOperationKey = (
864
+ phase: string,
865
+ snapshot: ActiveContextSnapshot | null = lifecycle.latestSnapshot,
866
+ ratio: number | null = measurements.latestRatio,
867
+ ): string => {
868
+ const lifecyclePhase = ["context", "message-end", "turn-end"].includes(phase) ? "automatic-rung" : phase;
869
+ let selection: Record<string, unknown> = { kind: lifecyclePhase };
870
+ try {
871
+ if (persistence.state && snapshot && ratio !== null) {
872
+ const selected = selectAutomaticRung(snapshot, persistence.state, ratio, {
873
+ summarizerAvailable: Boolean(options.summarizeContextSpan),
874
+ failedPreparationIds: ladder.failedPreparations,
875
+ });
876
+ if (selected?.kind === "refold") {
877
+ selection = { kind: "refold", foldId: selected.foldId };
878
+ } else if (selected && "candidate" in selected) {
879
+ selection = {
880
+ kind: selected.kind === "prepared-chapter" || selected.kind === "chapter"
881
+ ? "chapter-fold"
882
+ : selected.kind,
883
+ refs: selected.candidate.sourceRefs.map(objectRefKey),
884
+ };
885
+ }
886
+ }
887
+ } catch {
888
+ selection = { kind: `${lifecyclePhase}-selection` };
889
+ }
890
+ return sha256Value({
891
+ sessionId: persistence.state?.sessionId ?? snapshot?.sessionId ?? null,
892
+ revision: persistence.state?.revision ?? null,
893
+ topology: persistence.state ? topologySha256(persistence.state) : null,
894
+ protection: persistence.state ? protectionSha256(persistence.state) : null,
895
+ selection,
896
+ policy: snapshot ? {
897
+ toolFoldRatio: snapshot.policy.toolFoldRatio,
898
+ refoldRatio: snapshot.policy.refoldRatio,
899
+ prepareRatio: snapshot.policy.prepareRatio,
900
+ hardFenceRatio: hardFenceRatio(snapshot),
901
+ consolidationRatio: snapshot.policy.consolidationRatio,
902
+ } : null,
903
+ });
904
+ };
905
+
906
+ const suspendAutomatic = (
907
+ error: unknown,
908
+ phase: string,
909
+ ctx: any,
910
+ key = automaticOperationKey(phase),
911
+ persistenceDisposition: AutomaticFailureState["persistenceDisposition"] = "none",
912
+ ): void => {
913
+ const message = boundReceiptText(
914
+ error instanceof Error ? error.message : String(error),
915
+ 1_200,
916
+ "automatic context failure",
917
+ );
918
+ ladder.boundaryFailure = message;
919
+ cancelPreparation();
920
+ if (persistence.state?.prepared) persistence.state = clearPrepared(persistence.state);
921
+ if (ladder.automaticFailure) {
922
+ ladder.automaticFailure.suppressedCallbacks = Math.min(
923
+ Number.MAX_SAFE_INTEGER,
924
+ ladder.automaticFailure.suppressedCallbacks + 1,
925
+ );
926
+ return;
927
+ }
928
+ ladder.automaticFailure = {
929
+ key,
930
+ phase,
931
+ message,
932
+ firstFailedAt: Date.now(),
933
+ attempts: 1,
934
+ suppressedCallbacks: 0,
935
+ persistenceDisposition,
936
+ };
937
+ ladder.pendingContextNote = `Automatic context management suspended after one ${phase} failure; exact Pi context remains raw and manual context actions remain available.`;
938
+ safeNotify(ctx, `Automatic context management suspended: ${message}`, "warning");
939
+ };
940
+
941
+ const abortUnsafeHardContext = (
942
+ snapshot: ActiveContextSnapshot | null,
943
+ ctx: any,
944
+ allowUnmeasuredRevisionRelease = false,
945
+ ): boolean => {
946
+ if (measurements.latestRatio === null || measurements.latestRatio < hardFenceRatio(snapshot ?? undefined, ctx) || !persistence.state) return false;
947
+ const measuredRevision = measurements.lastProviderMeasurement
948
+ ? measurements.providerMeasurementRevisionByMessageSha.get(measurements.lastProviderMeasurement.messageSha256)
949
+ : undefined;
950
+ // A durable projection topology after the measured response gets exactly
951
+ // one provider attempt so its fold can be measured. Concurrent
952
+ // callbacks, retries, and same-session reloads may not repeatedly release
953
+ // that unmeasured projection. Non-structural state persistence does not
954
+ // spend this release. A failed automatic transaction never gets this
955
+ // escape, even if a record/state append preceded its projection failure.
956
+ if (allowUnmeasuredRevisionRelease && !ladder.automaticFailure &&
957
+ measuredRevision !== undefined && measurements.lastProviderMeasurement &&
958
+ !durableProviderMeasurementMatches(measurements.lastProviderMeasurement)) {
959
+ const releaseKey = sha256Value({
960
+ sessionId: persistence.state.sessionId,
961
+ topologySha256: topologySha256(persistence.state),
962
+ protectionSha256: protectionSha256(persistence.state),
963
+ measuredRevision,
964
+ providerMessageSha256: measurements.lastProviderMeasurement?.messageSha256 ?? null,
965
+ });
966
+ if (!advisory.hardFenceReleasedProjectionKeys.has(releaseKey) &&
967
+ advisory.hardFenceReleasedProjectionKeys.size < 4_096) {
968
+ advisory.hardFenceReleasedProjectionKeys.add(releaseKey);
969
+ return false;
970
+ }
971
+ }
972
+ const key = ladder.automaticFailure?.key ?? sha256Value({
973
+ sessionId: snapshot?.sessionId ?? persistence.state.sessionId,
974
+ revision: persistence.state.revision,
975
+ providerMessageSha256: measurements.lastProviderMeasurement?.messageSha256 ?? null,
976
+ phase: "hard-provider-fence",
977
+ });
978
+ ladder.pendingContextNote = `Provider context reached the hard ${brandNoun} fence without a newly committed lossless fold. ` +
979
+ "The provider request was aborted before transmission; run /compact or make an explicit bounded context fold.";
980
+ if (advisory.hardFenceNoticeKey !== key) {
981
+ advisory.hardFenceNoticeKey = key;
982
+ safeNotify(
983
+ ctx,
984
+ "Provider request aborted at the hard context fence; run /compact or make an explicit bounded context fold.",
985
+ "error",
986
+ );
987
+ }
988
+ // Stock Pi exposes abort() in every extension event context. Calling it
989
+ // here aborts the signal passed to the provider stream after this context
990
+ // transform returns, so exact raw Pi messages remain canonical but are not
991
+ // transmitted as an overflowing request.
992
+ if (typeof ctx.abort !== "function") {
993
+ throw new Error(`Pi hard-fence abort capability is unavailable at ratio ${measurements.latestRatio}`);
994
+ }
995
+ ctx.abort();
996
+ return true;
997
+ };
998
+
999
+ const startPreparation = (snapshot: ActiveContextSnapshot, ratio: number | null, ctx: any): void => {
1000
+ if (lifecycle.shuttingDown || !persistence.state || ladder.automaticFailure || ratio === null || persistence.state.prepared || ladder.preparing ||
1001
+ ratio < snapshot.policy.warmRatio ||
1002
+ !measurements.lastProviderMeasurement || !durableProviderMeasurementMatches(measurements.lastProviderMeasurement)) return;
1003
+ // Preparation is asynchronous but never jumps ahead of an immediately
1004
+ // committable deterministic fold on the same measured projection.
1005
+ const selection = selectAutomaticRung(snapshot, persistence.state, ratio, {
1006
+ summarizerAvailable: Boolean(options.summarizeContextSpan),
1007
+ failedPreparationIds: ladder.failedPreparations,
1008
+ });
1009
+ ladder.lastSelectionKind = selection && "candidate" in selection ? selection.candidate.kind : null;
1010
+ ladder.lastSelectionSourceIds = selection && "candidate" in selection
1011
+ ? selection.candidate.sourceRefs.slice(0, 8).map((ref) => ref.entryId)
1012
+ : [];
1013
+ if (selection?.kind !== "chapter-prepare") return;
1014
+ const candidate = selection.candidate;
1015
+ const id = automaticPreparationId(candidate, persistence.state);
1016
+ const controller = new AbortController();
1017
+ ladder.lastPreparationError = null;
1018
+ ladder.lastPreparationCandidateId = id;
1019
+ const slot = { id, controller, promise: Promise.resolve() };
1020
+ ladder.preparing = slot;
1021
+ const capturedState = clone(persistence.state);
1022
+ const capturedGeneration = lifecycle.generation;
1023
+ slot.promise = prepareFold({
1024
+ candidate,
1025
+ snapshot,
1026
+ state: capturedState,
1027
+ generation: capturedGeneration,
1028
+ summarize: ladder.failedPreparations.has(id) ? undefined : options.summarizeContextSpan,
1029
+ onSummarizerFailure: (error) => {
1030
+ ladder.lastPreparationError = error instanceof Error ? error.message : String(error);
1031
+ ladder.failedPreparations.add(id);
1032
+ },
1033
+ ctx,
1034
+ signal: controller.signal,
1035
+ }).then((preparedFold) => {
1036
+ const operation = ladder.actionQueue.then(() => {
1037
+ if (controller.signal.aborted ||
1038
+ !sessionIdentityStillValid(ctx, snapshot.sessionId, capturedGeneration)) return;
1039
+ const currentState = persistence.state;
1040
+ if (!currentState || topologySha256(currentState) !== preparedFold.topologySha256 ||
1041
+ protectionSha256(currentState) !== preparedFold.protectionSha256) return;
1042
+ persistence.state = { ...currentState, prepared: preparedFold };
1043
+ return persist(ctx);
1044
+ });
1045
+ ladder.actionQueue = operation.catch(() => undefined);
1046
+ return operation;
1047
+ }).catch((error) => {
1048
+ if (controller.signal.aborted ||
1049
+ !sessionIdentityStillValid(ctx, snapshot.sessionId, capturedGeneration)) return;
1050
+ ladder.lastPreparationError = error instanceof Error ? error.message : String(error);
1051
+ ladder.failedPreparations.add(id);
1052
+ suspendAutomatic(
1053
+ error,
1054
+ "chapter-prepare",
1055
+ ctx,
1056
+ sha256Value({ sessionId: snapshot.sessionId, operation: "chapter-prepare", candidateId: id }),
1057
+ );
1058
+ }).finally(() => {
1059
+ const ownsSlot = ladder.preparing === slot;
1060
+ if (ownsSlot) ladder.preparing = null;
1061
+ if (ownsSlot && sessionIdentityStillValid(ctx, snapshot.sessionId, capturedGeneration)) updateStatus(ctx);
1062
+ });
1063
+ };
1064
+ // Appended at the very TAIL, after the stable prefix, so a suggestion never
1065
+ // invalidates a cached prefix and never becomes durable transcript.
1066
+ const appendSurfacing = (projected: unknown[], snapshot: ActiveContextSnapshot): unknown[] => {
1067
+ const content = surfacingText({ suggestions: surfacing.suggestions, brandNoun });
1068
+ if (!content) return projected;
1069
+ projected.push({
1070
+ role: "custom",
1071
+ customType: surfacingProjectionType,
1072
+ content,
1073
+ display: false,
1074
+ details: {
1075
+ source: activeContextSource(entryTypePrefix),
1076
+ ephemeral: true,
1077
+ suggestions: surfacing.suggestions.map((suggestion) => ({
1078
+ source: suggestion.source,
1079
+ id: suggestion.id,
1080
+ score: suggestion.score,
1081
+ })),
1082
+ },
1083
+ timestamp: typeof ownValue(snapshot.messages.at(-1), "timestamp") === "number"
1084
+ ? ownValue(snapshot.messages.at(-1), "timestamp")
1085
+ : 0,
1086
+ });
1087
+ return projected;
1088
+ };
1089
+ const projectWithAdvisory = (snapshot: ActiveContextSnapshot): unknown[] => {
1090
+ const projected = projectActiveContext(snapshot, persistence.state!).filter((message) => {
1091
+ const customType = ownValue(message, "customType");
1092
+ return customType !== milestoneProjectionType && customType !== advisoryProjectionType &&
1093
+ customType !== surfacingProjectionType;
1094
+ });
1095
+ const armed = advisoryState(persistence.state!).armed;
1096
+ if (!armed || armed.milestone !== advisory.armedMilestone || measurements.latestRatio === null ||
1097
+ measurements.latestRatio < 0.85 * armed.threshold) return appendSurfacing(projected, snapshot);
1098
+ const status = activeContextStatus(snapshot, persistence.state!, 0, 1, snapshot.policy.maxFoldSourceRefs);
1099
+ const eligible = ownValue(status, "eligibleChapter");
1100
+ const startId = ownValue(eligible, "startId");
1101
+ const endId = ownValue(eligible, "endId");
1102
+ const chapterEndpoints = typeof startId === "string" && typeof endId === "string"
1103
+ ? [startId, endId]
1104
+ : [];
1105
+ const toolEndpoints = selectAutomaticToolBatch(snapshot, persistence.state!, 1)
1106
+ .flatMap((candidate) => candidate.sourceRefs.at(-1)?.entryId ?? [])
1107
+ .slice(0, 3);
1108
+ const remediationCount = advisoryState(persistence.state!).delivered[armed.milestone] ?? 0;
1109
+ projected.push({
1110
+ role: "custom",
1111
+ customType: milestoneProjectionType,
1112
+ content: milestoneText(
1113
+ armed.milestone, persistence.state!.sessionId, armed.threshold, toolName, brandNoun, guidance,
1114
+ ),
1115
+ display: false,
1116
+ details: { source: activeContextSource(entryTypePrefix), ephemeral: true, milestone: armed.milestone },
1117
+ timestamp: 0,
1118
+ });
1119
+ projected.push({
1120
+ role: "custom",
1121
+ customType: advisoryProjectionType,
1122
+ content: liveAdvisoryText({
1123
+ milestone: armed.milestone,
1124
+ ratio: measurements.latestRatio,
1125
+ toolEndpoints,
1126
+ chapterEndpoints,
1127
+ remediationCount,
1128
+ brandNoun,
1129
+ }),
1130
+ display: false,
1131
+ details: { source: activeContextSource(entryTypePrefix), ephemeral: true, milestone: armed.milestone },
1132
+ timestamp: typeof ownValue(snapshot.messages.at(-1), "timestamp") === "number"
1133
+ ? ownValue(snapshot.messages.at(-1), "timestamp")
1134
+ : 0,
1135
+ });
1136
+ // A suggestion never shares an advisory with urgent fence text: at the fence the
1137
+ // only useful next action is the fold that keeps the request transmissible.
1138
+ return armed.milestone === "urgent" ? projected : appendSurfacing(projected, snapshot);
1139
+ };
1140
+ const commitDeterministicCandidate = async (
1141
+ snapshot: ActiveContextSnapshot,
1142
+ candidate: FoldCandidate,
1143
+ brief: string,
1144
+ ): Promise<string> => {
1145
+ const preparedFold = await prepareFold({
1146
+ candidate,
1147
+ snapshot,
1148
+ state: persistence.state!,
1149
+ generation: lifecycle.generation,
1150
+ brief,
1151
+ briefProvenance: "deterministic",
1152
+ });
1153
+ persistence.state = commitPreparedFold({
1154
+ prepared: preparedFold,
1155
+ snapshot,
1156
+ state: persistence.state!,
1157
+ generation: lifecycle.generation,
1158
+ });
1159
+ return preparedFold.id;
1160
+ };
1161
+
1162
+ const prepareAndCommitExplicit = async (input: {
1163
+ snapshot: ActiveContextSnapshot;
1164
+ candidate: FoldCandidate;
1165
+ brief?: string;
1166
+ ctx: any;
1167
+ signal?: AbortSignal;
1168
+ maximumSourceChars?: number;
1169
+ }): Promise<{ preparedFold: PreparedFold; nextState: ActiveContextState }> => {
1170
+ const baseState = persistence.state!;
1171
+ const generationAtStart = lifecycle.generation;
1172
+ const sessionId = baseState.sessionId;
1173
+ const sourceChars = bytes(encodedFoldSource(input.snapshot, baseState, input.candidate.parts, input.candidate.kind));
1174
+ if (sourceChars > (input.maximumSourceChars ?? USER_RESCUE_MAX_SOURCE_CHARS)) {
1175
+ throw new Error(`Selected fold source is ${sourceChars} bytes; choose a smaller bounded span`);
1176
+ }
1177
+ const preparedFold = await prepareFold({
1178
+ candidate: input.candidate,
1179
+ snapshot: input.snapshot,
1180
+ state: baseState,
1181
+ generation: generationAtStart,
1182
+ brief: input.brief,
1183
+ summarize: options.summarizeContextSpan,
1184
+ ctx: input.ctx,
1185
+ signal: input.signal,
1186
+ });
1187
+ if (!sessionIdentityStillValid(input.ctx, sessionId, generationAtStart)) {
1188
+ throw new Error("Active-context session changed while preparing the explicit fold");
1189
+ }
1190
+ const current = authoritativeSnapshotFor(input.ctx);
1191
+ if (!sessionIdentityStillValid(input.ctx, sessionId, generationAtStart)) {
1192
+ throw new Error("Active-context session changed during explicit fold revalidation");
1193
+ }
1194
+ const nextState = commitPreparedFold({
1195
+ prepared: preparedFold,
1196
+ snapshot: current,
1197
+ state: persistence.state!,
1198
+ generation: generationAtStart,
1199
+ });
1200
+ return { preparedFold, nextState };
1201
+ };
1202
+
1203
+ const applyAutomaticRung = async (
1204
+ snapshot: ActiveContextSnapshot,
1205
+ ratio: number,
1206
+ rungOptions: { waiveToolCadence?: boolean; toolOnly?: boolean } = {},
1207
+ ): Promise<Record<string, unknown> | null> => {
1208
+ if (!persistence.state || ladder.automaticFailure || ladder.preparing) return null;
1209
+ const selection = selectAutomaticRung(snapshot, persistence.state, ratio, {
1210
+ waiveToolCadence: rungOptions.waiveToolCadence,
1211
+ toolOnly: rungOptions.toolOnly,
1212
+ summarizerAvailable: Boolean(options.summarizeContextSpan),
1213
+ failedPreparationIds: ladder.failedPreparations,
1214
+ });
1215
+ if (!selection) return null;
1216
+ const projectedBytesBefore = bytes(projectActiveContext(snapshot, persistence.state));
1217
+ let action: Record<string, unknown> | null = null;
1218
+ if (selection.kind === "prepared-chapter" && persistence.state.prepared) {
1219
+ const error = preparedFoldError({
1220
+ prepared: persistence.state.prepared,
1221
+ snapshot,
1222
+ state: persistence.state,
1223
+ generation: lifecycle.generation,
1224
+ ratio,
1225
+ });
1226
+ if (error) {
1227
+ persistence.state = clearPrepared(persistence.state);
1228
+ } else {
1229
+ const id = persistence.state.prepared.id;
1230
+ const sourceIds = persistence.state.prepared.sourceRefs.map((ref) => ref.entryId);
1231
+ persistence.state = commitPreparedFold({
1232
+ prepared: persistence.state.prepared,
1233
+ snapshot,
1234
+ state: persistence.state,
1235
+ generation: lifecycle.generation,
1236
+ });
1237
+ action = { kind: "chapter-fold", foldIds: [id], sourceIds };
1238
+ ladder.pendingContextNote = `A coherent stale chapter was folded under ${id}; exact evidence remains expandable.`;
1239
+ }
1240
+ } else if (selection.kind === "tool") {
1241
+ cancelPreparation();
1242
+ const tool = selection.candidate;
1243
+ const id = await commitDeterministicCandidate(snapshot, tool, automaticToolBrief(snapshot, tool));
1244
+ action = {
1245
+ kind: "tool-fold",
1246
+ foldIds: [id],
1247
+ sourceIds: tool.sourceRefs.map((ref) => ref.entryId),
1248
+ };
1249
+ ladder.pendingContextNote = `${tool.sourceRefs.length} stale completed read-only tool result(s) were folded.`;
1250
+ } else if (selection.kind === "refold") {
1251
+ cancelPreparation();
1252
+ persistence.state = setFoldProjectionState(persistence.state, selection.foldId, "folded");
1253
+ action = { kind: "refold", foldIds: [selection.foldId] };
1254
+ ladder.pendingContextNote = `Stale expanded fold ${selection.foldId} returned to its identical placeholder.`;
1255
+ } else if (selection.kind === "consolidation") {
1256
+ cancelPreparation();
1257
+ const consolidation = selection.candidate;
1258
+ const id = await commitDeterministicCandidate(
1259
+ snapshot,
1260
+ consolidation,
1261
+ deterministicConsolidationBrief(consolidation, persistence.state),
1262
+ );
1263
+ action = {
1264
+ kind: "consolidation",
1265
+ foldIds: [id],
1266
+ sourceIds: consolidation.sourceRefs.map((ref) => ref.entryId),
1267
+ };
1268
+ ladder.pendingContextNote =
1269
+ `Stale folded chapters were consolidated under ${id}; every child remains expandable.`;
1270
+ } else if (selection.kind === "chapter") {
1271
+ const chapter = selection.candidate;
1272
+ const id = await commitDeterministicCandidate(
1273
+ snapshot,
1274
+ chapter,
1275
+ deterministicChapterCandidateBrief(snapshot, chapter),
1276
+ );
1277
+ action = {
1278
+ kind: "chapter-fold",
1279
+ foldIds: [id],
1280
+ sourceIds: chapter.sourceRefs.map((ref) => ref.entryId),
1281
+ };
1282
+ ladder.pendingContextNote =
1283
+ `A coherent stale chapter was folded under ${id}; exact evidence remains expandable.`;
1284
+ }
1285
+ if (!action || !persistence.state) return null;
1286
+ const projectedBytesAfter = bytes(projectActiveContext(snapshot, persistence.state));
1287
+ persistence.state = clearArmedAdvisory(persistence.state);
1288
+ advisory.armedMilestone = null;
1289
+ ladder.lastAutomaticAction = {
1290
+ ...action,
1291
+ sourceBytesSaved: Math.max(0, projectedBytesBefore - projectedBytesAfter),
1292
+ };
1293
+ return ladder.lastAutomaticAction;
1294
+ };
1295
+ const runAutomaticRungTransaction = async (
1296
+ snapshot: ActiveContextSnapshot,
1297
+ ratio: number,
1298
+ ctx: any,
1299
+ phase: string,
1300
+ rungOptions: { waiveToolCadence?: boolean; toolOnly?: boolean } = {},
1301
+ ): Promise<Record<string, unknown> | null> => {
1302
+ if (!persistence.state || !measurements.lastProviderMeasurement ||
1303
+ !durableProviderMeasurementMatches(measurements.lastProviderMeasurement)) return null;
1304
+ if (ladder.automaticFailure) {
1305
+ ladder.automaticFailure.suppressedCallbacks = Math.min(
1306
+ Number.MAX_SAFE_INTEGER,
1307
+ ladder.automaticFailure.suppressedCallbacks + 1,
1308
+ );
1309
+ return null;
1310
+ }
1311
+ const key = automaticOperationKey(phase, snapshot, ratio);
1312
+ const stateAtEntry = clone(persistence.state);
1313
+ const persistedAtEntry = persistence.persisted;
1314
+ const recordsAtEntry = persistence.persistedFoldRecords.size;
1315
+ const transientAtEntry = captureTransient();
1316
+ let action: Record<string, unknown> | null = null;
1317
+ try {
1318
+ action = await applyAutomaticRung(snapshot, ratio, rungOptions);
1319
+ if (action) await persist(ctx);
1320
+ if (action) ladder.boundaryFailure = null;
1321
+ return action;
1322
+ } catch (error) {
1323
+ const stateCommitted = persistence.persisted !== persistedAtEntry;
1324
+ const recordOnly = !stateCommitted && persistence.persistedFoldRecords.size > recordsAtEntry;
1325
+ if (!stateCommitted) {
1326
+ persistence.state = stateAtEntry;
1327
+ restoreTransient(transientAtEntry);
1328
+ }
1329
+ suspendAutomatic(
1330
+ error,
1331
+ phase,
1332
+ ctx,
1333
+ key,
1334
+ stateCommitted ? "state-committed" : recordOnly ? "record-only" : "none",
1335
+ );
1336
+ updateStatus(ctx);
1337
+ return stateCommitted ? action : null;
1338
+ }
1339
+ };
1340
+ const attemptAutomaticRung = (
1341
+ snapshot: ActiveContextSnapshot,
1342
+ ratio: number,
1343
+ ctx: any,
1344
+ phase: string,
1345
+ ): Promise<Record<string, unknown> | null> => {
1346
+ const operation = ladder.actionQueue.then(() =>
1347
+ runAutomaticRungTransaction(snapshot, ratio, ctx, phase));
1348
+ ladder.actionQueue = operation.catch(() => undefined);
1349
+ return operation;
1350
+ };
1351
+ const projectionCandidates = (ctx: any): Array<Record<string, unknown>> => {
1352
+ if (lifecycle.shuttingDown || !persistence.state) return [];
1353
+ try {
1354
+ return projectionSlateCandidates(persistence.state, authoritativeSnapshotFor(ctx));
1355
+ } catch {
1356
+ return [];
1357
+ }
1358
+ };
1359
+ options.setProjectionProvider?.(projectionCandidates);
1360
+
1361
+ /**
1362
+ * The suggestion-source hook. The built-in fold-brief source registers through this
1363
+ * same call, so an external memory system's items reach the identical carrier,
1364
+ * budget, ranking, and accept/reject log; pi-fold owns the channel, not the content.
1365
+ */
1366
+ const registerSuggestionSource: SuggestionSourceRegistrar = (source: SuggestionSource) => {
1367
+ if (!source || typeof source !== "object" || typeof source.id !== "string" || !source.id ||
1368
+ typeof source.candidates !== "function") {
1369
+ throw new Error("A suggestion source needs a nonempty id and a candidates function");
1370
+ }
1371
+ if (surfacing.sources.some((registered) => registered.id === source.id)) {
1372
+ throw new Error(`Suggestion source '${source.id}' is already registered`);
1373
+ }
1374
+ surfacing.sources.push(source);
1375
+ return {
1376
+ unregister: () => {
1377
+ surfacing.sources = surfacing.sources.filter((registered) => registered !== source);
1378
+ },
1379
+ accepted: (id: string) => {
1380
+ if (!persistence.state || typeof id !== "string" || !id) return;
1381
+ // In-memory only: the next persisted state carries it. Reporting an outcome
1382
+ // must never write a durable entry of its own.
1383
+ persistence.state = acceptSurfacingSuggestion(
1384
+ persistence.state,
1385
+ id,
1386
+ lifecycle.latestSnapshot ? surfacingOrdinal(lifecycle.latestSnapshot) : 0,
1387
+ );
1388
+ },
1389
+ };
1390
+ };
1391
+ if (surfacingEnabled) registerSuggestionSource(FOLD_BRIEF_SUGGESTION_SOURCE);
1392
+ options.setSuggestionSourceRegistrar?.(registerSuggestionSource);
1393
+
1394
+ /**
1395
+ * Selects this context event's slate and logs what will be shown. Selection is
1396
+ * deterministic and model-free, and a selector fault costs the session nothing but
1397
+ * its suggestions: the projection itself is never at risk.
1398
+ *
1399
+ * Returns whether the log now differs from the DURABLE one rather than merely from
1400
+ * the pre-selection value: an outcome marked by an ephemeral peek carries no
1401
+ * persistence of its own and would otherwise never reach the session file.
1402
+ */
1403
+ const refreshSurfacing = (snapshot: ActiveContextSnapshot): boolean => {
1404
+ surfacing.suggestions = [];
1405
+ if (!surfacingEnabled || !surfacing.sources.length || !persistence.state) return false;
1406
+ try {
1407
+ const update = updateSurfacing({
1408
+ state: persistence.state,
1409
+ snapshot,
1410
+ sources: surfacing.sources,
1411
+ toolName,
1412
+ });
1413
+ surfacing.suggestions = update.suggestions;
1414
+ persistence.state = update.state;
1415
+ } catch {
1416
+ surfacing.suggestions = [];
1417
+ }
1418
+ return stableStringify(persistence.state.surfacing ?? null) !==
1419
+ stableStringify(persistence.persisted?.surfacing ?? null);
1420
+ };
1421
+
1422
+ // A same-session start/tree reload is a projection-generation mutation.
1423
+ // Queue it behind every context authority → preparation → commit →
1424
+ // projection transaction, then serialize the actual load with the action
1425
+ // queue. Appending to actionQueue only after the context queue drains is
1426
+ // deliberate: a running context may itself need actionQueue to commit its
1427
+ // final-rung chapter, so capturing both queues up front would deadlock.
1428
+ const enqueueLifecycleLoad = async (ctx: any): Promise<void> => {
1429
+ const operation = lifecycle.contextQueue.then(() => {
1430
+ const loadOperation = ladder.actionQueue.then(async () => {
1431
+ load(ctx);
1432
+ await recoverNativeReceipts(ctx);
1433
+ });
1434
+ ladder.actionQueue = loadOperation.catch(() => undefined);
1435
+ return loadOperation;
1436
+ });
1437
+ lifecycle.contextQueue = operation.then(() => undefined, () => undefined);
1438
+ await operation;
1439
+ };
1440
+
1441
+ const safeLifecycleLoad = async (ctx: any, phase: "session-start" | "session-tree"): Promise<void> => {
1442
+ try { await enqueueLifecycleLoad(ctx); }
1443
+ catch (error) {
1444
+ const sessionId = ctx.sessionManager.getSessionId();
1445
+ if (!persistence.state || persistence.state.sessionId !== sessionId) persistence.state = emptyActiveContextState(sessionId);
1446
+ if (!persistence.persisted || persistence.persisted.sessionId !== sessionId) persistence.persisted = clone(persistence.state);
1447
+ lifecycle.latestSnapshotError = error instanceof Error ? error.message : String(error);
1448
+ suspendAutomatic(error, phase, ctx);
1449
+ updateStatus(ctx);
1450
+ }
1451
+ };
1452
+
1453
+ pi.on("session_start", async (_event: unknown, ctx: any) => { await safeLifecycleLoad(ctx, "session-start"); });
1454
+ pi.on("session_tree", async (_event: unknown, ctx: any) => { await safeLifecycleLoad(ctx, "session-tree"); });
1455
+ pi.on("session_compact", async (event: Record<string, unknown>, ctx: any) => {
1456
+ const reason = boundReceiptText(ownValue(event, "reason"), 64, "unknown");
1457
+ const decision = makeNativeDecision(
1458
+ ctx,
1459
+ reason,
1460
+ ownValue(event, "willRetry") === true,
1461
+ reason === "manual" ? "manual-user-request" : "native-completion-without-predecision",
1462
+ reason === "manual"
1463
+ ? "Manual native compaction explicitly requested as the model-independent safety net"
1464
+ : "Native compaction completed without a matching pre-compaction decision receipt",
1465
+ );
1466
+ try {
1467
+ const entries = ctx.sessionManager.getEntries?.() ?? ctx.sessionManager.getBranch();
1468
+ const existing = entries.find((entry: any) => entry?.type === "custom" &&
1469
+ entry.customType === nativeDecisionEntryType &&
1470
+ ownValue(entry.data, "decisionKey") === decision.decisionKey);
1471
+ if (existing) parseNativeCompactionDecision(existing.data, decision.sessionId);
1472
+ else await pi.appendEntry(nativeDecisionEntryType, decision);
1473
+ } catch (error) {
1474
+ safeNotify(ctx, `Native completion decision could not persist for recovery: ${String(error)}`, "error");
1475
+ }
1476
+ const receipt = buildNativeCompletion(event, ctx, decision);
1477
+ nativeCompaction.pendingNativeReceipt = receipt;
1478
+ load(ctx, true);
1479
+ nativeCompaction.lastThresholdDecision = {
1480
+ handled: true,
1481
+ retry: false,
1482
+ reason: `native compaction completed; ${brandNoun} folding state rebuilt`,
1483
+ compactionReason: reason,
1484
+ nativeCompactionCompleted: true,
1485
+ receiptKey: receipt.receiptKey,
1486
+ decision,
1487
+ };
1488
+ try {
1489
+ await persistNativeCompletion(receipt, ctx);
1490
+ } catch (error) {
1491
+ safeNotify(ctx, `Native compaction completed; its receipt remains queued for retry: ${String(error)}`, "error");
1492
+ }
1493
+ safeNotify(ctx, `Pi native compaction ran; ${brandNoun} folding state was rebuilt.`, "warning");
1494
+ updateStatus(ctx);
1495
+ });
1496
+
1497
+ const attributionChanged = async (_event: unknown, ctx: any): Promise<void> => {
1498
+ lifecycle.generation += 1;
1499
+ cancelPreparation();
1500
+ if (persistence.state?.prepared) persistence.state = clearPrepared(persistence.state);
1501
+ measurements.latestRatio = null;
1502
+ measurements.lastProviderMeasurement = null;
1503
+ advisory.armedMilestone = persistence.state ? advisoryState(persistence.state).armed?.milestone ?? null : null;
1504
+ advisory.advisoryScheduleKey = "pending-reseed";
1505
+ nativeCompaction.lastThresholdDecision = null;
1506
+ updateStatus(ctx);
1507
+ };
1508
+ pi.on("model_select", attributionChanged);
1509
+ pi.on("thinking_level_select", attributionChanged);
1510
+
1511
+ const armMilestoneForMeasurement = (
1512
+ snapshot: ActiveContextSnapshot,
1513
+ measurement: ProviderContextMeasurement,
1514
+ ): boolean => {
1515
+ if (!persistence.state) return false;
1516
+ const ratio = contextUsageRatio(measurement);
1517
+ if (ratio === null) return false;
1518
+ const schedule = advisorySchedule(snapshot, guidance);
1519
+ const scheduleKey = sha256Value({
1520
+ schedule: schedule.key,
1521
+ provider: measurement.provider,
1522
+ model: measurement.model,
1523
+ contextWindow: measurement.contextWindow,
1524
+ });
1525
+ const scheduleChanged = advisory.advisoryScheduleKey !== null && advisory.advisoryScheduleKey !== scheduleKey;
1526
+ advisory.advisoryScheduleKey = scheduleKey;
1527
+ const advisoryBefore = stableStringify(advisoryState(persistence.state));
1528
+ const advisoryUpdate = updateAdvisoryMilestone(persistence.state, ratio, schedule, scheduleChanged, scheduleKey);
1529
+ persistence.state = advisoryUpdate.state;
1530
+ const armed = advisoryState(persistence.state).armed;
1531
+ if (armed && ratio < 0.85 * armed.threshold) {
1532
+ persistence.state = clearArmedAdvisory(persistence.state);
1533
+ advisory.armedMilestone = null;
1534
+ } else {
1535
+ advisory.armedMilestone = armed?.milestone ?? null;
1536
+ }
1537
+ return advisoryBefore !== stableStringify(advisoryState(persistence.state));
1538
+ };
1539
+
1540
+ const accountAnchoredMeasurement = (measurement: ProviderContextMeasurement): boolean => {
1541
+ if (!persistence.state) return false;
1542
+ if (!measurements.lastProviderMeasurement) {
1543
+ measurements.lastProviderMeasurement = measurement;
1544
+ return false;
1545
+ }
1546
+ if (measurements.lastProviderMeasurement.messageSha256 === measurement.messageSha256) return false;
1547
+ const previousTokens = measurements.lastProviderMeasurement.tokens;
1548
+ const delta = Math.max(0, measurement.tokens - previousTokens);
1549
+ const tokensSinceToolFold = Math.min(Number.MAX_SAFE_INTEGER, persistence.state.tokensSinceToolFold + delta);
1550
+ const leases = Object.fromEntries(Object.entries(persistence.state.leases)
1551
+ .flatMap(([id, remaining]) => remaining > 1 ? [[id, remaining - 1]] : []));
1552
+ const changed = tokensSinceToolFold !== persistence.state.tokensSinceToolFold ||
1553
+ stableStringify(leases) !== stableStringify(persistence.state.leases);
1554
+ if (changed) persistence.state = { ...persistence.state, tokensSinceToolFold, leases };
1555
+ return changed;
1556
+ };
1557
+
1558
+ const handleContext = async (event: { messages: unknown[] }, ctx: any) => {
1559
+ if (lifecycle.shuttingDown) return { messages: event.messages };
1560
+ if (!persistence.state) persistence.state = emptyActiveContextState(ctx.sessionManager.getSessionId());
1561
+ lifecycle.latestSnapshot = null;
1562
+ lifecycle.latestSnapshotError = null;
1563
+ const stateAtEntry = clone(persistence.state);
1564
+ const persistedAtEntry = persistence.persisted;
1565
+ const transientAtEntry = captureTransient();
1566
+ const generationAtEntry = lifecycle.generation;
1567
+ let mutationAttempted = ladder.pendingManual;
1568
+ let persistedSucceeded = false;
1569
+ try {
1570
+ const snapshot = snapshotForEvent(ctx, event.messages);
1571
+ lifecycle.latestSnapshot = snapshot;
1572
+ if (ladder.automaticFailure) {
1573
+ ladder.automaticFailure.suppressedCallbacks = Math.min(
1574
+ Number.MAX_SAFE_INTEGER,
1575
+ ladder.automaticFailure.suppressedCallbacks + 1,
1576
+ );
1577
+ }
1578
+ let observedMeasurement = latestProviderContextMeasurement(
1579
+ snapshot.messages,
1580
+ contextWindowFor(ctx) ?? DEFAULT_CONTEXT_WINDOW,
1581
+ ctx.model,
1582
+ );
1583
+ if (observedMeasurement && providerMeasurementBranchIndex(ctx, observedMeasurement) < 0) {
1584
+ observedMeasurement = null;
1585
+ }
1586
+ let advisoryChanged = false;
1587
+ let measurementStateChanged = false;
1588
+ if (observedMeasurement) {
1589
+ const boundRevision = measurements.providerMeasurementRevisionByMessageSha.get(
1590
+ observedMeasurement.messageSha256,
1591
+ );
1592
+ if (boundRevision !== undefined &&
1593
+ !durableProviderMeasurementReceiptMatches(observedMeasurement, boundRevision)) {
1594
+ try { await persistProviderMeasurement(ctx, observedMeasurement, boundRevision); }
1595
+ catch (error) { suspendAutomatic(error, "provider-measurement", ctx); }
1596
+ }
1597
+ measurements.latestRatio = contextUsageRatio(observedMeasurement);
1598
+ if (durableProviderMeasurementMatches(observedMeasurement) && measurements.latestRatio !== null) {
1599
+ measurementStateChanged = accountAnchoredMeasurement(observedMeasurement);
1600
+ measurements.lastProviderMeasurement = observedMeasurement;
1601
+ advisoryChanged = armMilestoneForMeasurement(snapshot, observedMeasurement);
1602
+ startPreparation(snapshot, measurements.latestRatio, ctx);
1603
+ if (!ladder.automaticFailure && measurements.latestRatio >= hardFenceRatio(snapshot) && ladder.preparing) {
1604
+ mutationAttempted = true;
1605
+ await ladder.preparing.promise;
1606
+ if (!sessionIdentityStillValid(ctx, snapshot.sessionId, generationAtEntry)) {
1607
+ return { messages: event.messages };
1608
+ }
1609
+ }
1610
+ if (selectAutomaticToolForRung(snapshot, persistence.state, measurements.latestRatio)) mutationAttempted = true;
1611
+ const action = await attemptAutomaticRung(snapshot, measurements.latestRatio, ctx, "context");
1612
+ if (action) {
1613
+ mutationAttempted = true;
1614
+ persistedSucceeded = true;
1615
+ advisoryChanged = false;
1616
+ }
1617
+ } else measurements.lastProviderMeasurement = observedMeasurement;
1618
+ } else {
1619
+ measurements.latestRatio = contextUsageRatio(measurements.lastProviderMeasurement);
1620
+ }
1621
+ const surfacingChanged = refreshSurfacing(snapshot);
1622
+ if ((advisoryChanged || measurementStateChanged || surfacingChanged) && persistence.state &&
1623
+ persistence.persisted && !sameStateProjection(persistence.state, persistence.persisted)) {
1624
+ mutationAttempted = true;
1625
+ await persistThroughActionQueue(ctx);
1626
+ persistedSucceeded = true;
1627
+ }
1628
+ let projected: unknown[];
1629
+ try { projected = projectWithAdvisory(snapshot); }
1630
+ catch (error) {
1631
+ suspendAutomatic(error, "projection", ctx);
1632
+ abortUnsafeHardContext(snapshot, ctx);
1633
+ return { messages: event.messages };
1634
+ }
1635
+ if (abortUnsafeHardContext(snapshot, ctx, true)) {
1636
+ updateStatus(ctx);
1637
+ return { messages: event.messages };
1638
+ }
1639
+ updateStatus(ctx);
1640
+ return { messages: projected };
1641
+ } catch (error) {
1642
+ if (!persistedSucceeded && persistence.persisted === persistedAtEntry) {
1643
+ persistence.state = stateAtEntry;
1644
+ restoreTransient(transientAtEntry);
1645
+ }
1646
+ lifecycle.latestSnapshotError = error instanceof Error ? error.message : String(error);
1647
+ if (mutationAttempted) {
1648
+ if (!persistedSucceeded) ladder.boundaryFailure = lifecycle.latestSnapshotError;
1649
+ suspendAutomatic(error, persistedSucceeded ? "post-persist-projection" : "context", ctx);
1650
+ }
1651
+ abortUnsafeHardContext(lifecycle.latestSnapshot, ctx);
1652
+ return { messages: event.messages };
1653
+ }
1654
+ };
1655
+ pi.on("context", (event: { messages: unknown[] }, ctx: any) => {
1656
+ const operation = lifecycle.contextQueue.then(() => handleContext(event, ctx));
1657
+ lifecycle.contextQueue = operation.then(() => undefined, () => undefined);
1658
+ return operation;
1659
+ });
1660
+
1661
+ const applyAnchoredProviderMeasurement = async (
1662
+ measurement: ProviderContextMeasurement,
1663
+ ctx: any,
1664
+ capturedSessionId: string,
1665
+ capturedGeneration: number,
1666
+ capturedProjectionRevision: number,
1667
+ capturedTopologySha256: string,
1668
+ capturedProtectionSha256: string,
1669
+ ): Promise<void> => {
1670
+ if (lifecycle.shuttingDown ||
1671
+ !sessionIdentityStillValid(ctx, capturedSessionId, capturedGeneration) ||
1672
+ topologySha256(persistence.state!) !== capturedTopologySha256 ||
1673
+ protectionSha256(persistence.state!) !== capturedProtectionSha256) return;
1674
+ try {
1675
+ await persistProviderMeasurement(ctx, measurement, capturedProjectionRevision);
1676
+ } catch (error) {
1677
+ suspendAutomatic(error, "provider-measurement", ctx);
1678
+ return;
1679
+ }
1680
+ const measuredRatio = contextUsageRatio(measurement);
1681
+ if (!sessionIdentityStillValid(ctx, capturedSessionId, capturedGeneration) ||
1682
+ !durableProviderMeasurementMatches(measurement) ||
1683
+ measuredRatio === null) return;
1684
+ const measurementStateChanged = accountAnchoredMeasurement(measurement);
1685
+ measurements.lastProviderMeasurement = measurement;
1686
+ measurements.latestRatio = measuredRatio;
1687
+ let snapshot: ActiveContextSnapshot;
1688
+ try { snapshot = authoritativeSnapshotFor(ctx); }
1689
+ catch {
1690
+ updateStatus(ctx);
1691
+ return;
1692
+ }
1693
+ const advisoryChanged = armMilestoneForMeasurement(snapshot, measurement);
1694
+ startPreparation(snapshot, measurements.latestRatio, ctx);
1695
+ if (measurements.latestRatio >= hardFenceRatio(measurement, ctx) && ladder.preparing) await ladder.preparing.promise;
1696
+ if (!sessionIdentityStillValid(ctx, capturedSessionId, capturedGeneration) ||
1697
+ !durableProviderMeasurementMatches(measurement)) return;
1698
+ const action = await attemptAutomaticRung(
1699
+ authoritativeSnapshotFor(ctx),
1700
+ measurements.latestRatio,
1701
+ ctx,
1702
+ "message-end",
1703
+ );
1704
+ if (!action && (advisoryChanged || measurementStateChanged) && persistence.state && persistence.persisted &&
1705
+ !sameStateProjection(persistence.state, persistence.persisted)) {
1706
+ await persistThroughActionQueue(ctx);
1707
+ }
1708
+ updateStatus(ctx);
1709
+ };
1710
+
1711
+ pi.on("message_end", async (event: Record<string, unknown>, ctx: any) => {
1712
+ if (lifecycle.shuttingDown) return;
1713
+ try {
1714
+ const message = ownValue(event, "message");
1715
+ const measurement = providerContextMeasurement(
1716
+ message,
1717
+ contextWindowFor(ctx) ?? DEFAULT_CONTEXT_WINDOW,
1718
+ ctx.model,
1719
+ );
1720
+ if (!measurement || !persistence.state) return;
1721
+ const capturedSessionId = ctx.sessionManager.getSessionId();
1722
+ const capturedGeneration = lifecycle.generation;
1723
+ const capturedProjectionRevision = persistence.state.revision;
1724
+ const capturedTopologySha256 = topologySha256(persistence.state);
1725
+ const capturedProtectionSha256 = protectionSha256(persistence.state);
1726
+ await applyAnchoredProviderMeasurement(
1727
+ measurement,
1728
+ ctx,
1729
+ capturedSessionId,
1730
+ capturedGeneration,
1731
+ capturedProjectionRevision,
1732
+ capturedTopologySha256,
1733
+ capturedProtectionSha256,
1734
+ );
1735
+ } catch (error) {
1736
+ suspendAutomatic(error, "message-end", ctx);
1737
+ try { updateStatus(ctx); } catch { /* The provider loop must keep running. */ }
1738
+ }
1739
+ });
1740
+ pi.on("tool_call", (event: Record<string, unknown>, ctx: any) => {
1741
+ const calledTool = ownValue(event, "toolName");
1742
+ if (lifecycle.shuttingDown || lifecycle.blockingToolHarvestedThisTurn || lifecycle.blockingToolHarvestQueuedThisTurn ||
1743
+ typeof calledTool !== "string" || !blockingTools.has(calledTool)) return;
1744
+ lifecycle.blockingToolHarvestQueuedThisTurn = true;
1745
+ const operation = ladder.actionQueue.then(async () => {
1746
+ try {
1747
+ if (!persistence.state || measurements.latestRatio === null || !measurements.lastProviderMeasurement || ladder.automaticFailure ||
1748
+ !durableProviderMeasurementMatches(measurements.lastProviderMeasurement)) return null;
1749
+ cancelPreparation();
1750
+ let snapshot: ActiveContextSnapshot;
1751
+ try { snapshot = authoritativeSnapshotFor(ctx); }
1752
+ catch { return null; }
1753
+ const candidate = selectAutomaticToolForRung(snapshot, persistence.state, measurements.latestRatio, true);
1754
+ if (!candidate) {
1755
+ lifecycle.blockingToolHarvestedThisTurn = true;
1756
+ return null;
1757
+ }
1758
+ const action = await runAutomaticRungTransaction(
1759
+ snapshot,
1760
+ measurements.latestRatio,
1761
+ ctx,
1762
+ "tool-call",
1763
+ { waiveToolCadence: true, toolOnly: true },
1764
+ );
1765
+ if (action) lifecycle.blockingToolHarvestedThisTurn = true;
1766
+ return action;
1767
+ } finally {
1768
+ lifecycle.blockingToolHarvestQueuedThisTurn = false;
1769
+ try { updateStatus(ctx); } catch { /* A blocking tool call must never wait on presentation. */ }
1770
+ }
1771
+ });
1772
+ ladder.actionQueue = operation.catch(() => undefined);
1773
+ });
1774
+ pi.on("turn_end", async (_event: unknown, ctx: any) => {
1775
+ lifecycle.blockingToolHarvestedThisTurn = false;
1776
+ lifecycle.blockingToolHarvestQueuedThisTurn = false;
1777
+ if (lifecycle.shuttingDown || !persistence.state || !persistence.persisted) return;
1778
+ const stateAtEntry = clone(persistence.state);
1779
+ const persistedAtEntry = persistence.persisted;
1780
+ const transientAtEntry = captureTransient();
1781
+ try {
1782
+ const snapshot = authoritativeSnapshotFor(ctx);
1783
+ if (!ladder.pendingManual && !ladder.automaticFailure && measurements.latestRatio !== null && measurements.lastProviderMeasurement &&
1784
+ durableProviderMeasurementMatches(measurements.lastProviderMeasurement)) {
1785
+ startPreparation(snapshot, measurements.latestRatio, ctx);
1786
+ if (measurements.latestRatio >= hardFenceRatio(snapshot) && ladder.preparing) await ladder.preparing.promise;
1787
+ await attemptAutomaticRung(snapshot, measurements.latestRatio, ctx, "turn-end");
1788
+ }
1789
+ ladder.pendingManual = false;
1790
+ if (!ladder.automaticFailure) ladder.boundaryFailure = null;
1791
+ } catch (error) {
1792
+ if (persistence.persisted === persistedAtEntry) {
1793
+ persistence.state = stateAtEntry;
1794
+ restoreTransient(transientAtEntry);
1795
+ }
1796
+ suspendAutomatic(error, "turn-end", ctx);
1797
+ }
1798
+ try { updateStatus(ctx); } catch { /* turn_end must never stall Pi. */ }
1799
+ });
1800
+ pi.on("session_before_tree", (event: Record<string, unknown>) => {
1801
+ const preparation = ownValue(event, "preparation");
1802
+ if (!preparation || typeof preparation !== "object") return;
1803
+ return ownValue(preparation, "userWantsSummary") === true ? { cancel: true } : undefined;
1804
+ });
1805
+
1806
+ pi.on("session_before_compact", (event: Record<string, unknown>, ctx: any) => {
1807
+ const reason = ownValue(event, "reason");
1808
+ if (reason === "manual") {
1809
+ nativeCompaction.lastThresholdDecision = {
1810
+ handled: false,
1811
+ retry: false,
1812
+ reason: "manual native compaction explicitly requested by the user",
1813
+ compactionReason: reason,
1814
+ };
1815
+ try { updateStatus(ctx); } catch { /* Manual rescue must survive presentation failure. */ }
1816
+ return undefined;
1817
+ }
1818
+ nativeCompaction.lastThresholdDecision = {
1819
+ handled: true,
1820
+ retry: false,
1821
+ reason: `blocked stock automatic compaction; ${contextBrand(brandNoun)} folding remains authoritative`,
1822
+ compactionReason: reason,
1823
+ nativeCompactionCompleted: false,
1824
+ };
1825
+ return { cancel: true };
1826
+ });
1827
+ pi.on("agent_settled", async (_event: unknown, ctx: any) => {
1828
+ if (ladder.pendingManual && persistence.persisted && ladder.boundaryFailure === null) {
1829
+ cancelPreparation();
1830
+ persistence.state = clone(persistence.persisted);
1831
+ ladder.pendingManual = false;
1832
+ }
1833
+ try {
1834
+ const snapshot = authoritativeSnapshotFor(ctx);
1835
+ startPreparation(snapshot, measurements.latestRatio, ctx);
1836
+ } catch {
1837
+ // The next authoritative context event will retry mapping.
1838
+ }
1839
+ try { await recoverNativeReceipts(ctx); }
1840
+ catch (error) { safeNotify(ctx, `Native compaction receipt recovery remains pending: ${String(error)}`, "error"); }
1841
+ updateStatus(ctx);
1842
+ });
1843
+
1844
+ const executeAction = async (
1845
+ params: Record<string, unknown>,
1846
+ signal: AbortSignal | undefined,
1847
+ ctx: any,
1848
+ ): Promise<unknown> => {
1849
+ if (lifecycle.shuttingDown) throw new Error("Active-context runtime is shut down");
1850
+ if (!persistence.state) persistence.state = emptyActiveContextState(ctx.sessionManager.getSessionId());
1851
+ const executionArgumentsSha256 = sha256Value(params);
1852
+ const action = String(params.action ?? "");
1853
+ if (!allowedToolActionSet.has(action)) {
1854
+ throw new Error(`${toolName} action '${action}' is not enabled in this runtime`);
1855
+ }
1856
+ if (action === "status" && !lifecycle.latestSnapshot) {
1857
+ return toolPayload({
1858
+ version: 1,
1859
+ service: "active-context-folding",
1860
+ available: false,
1861
+ contextEventError: lifecycle.latestSnapshotError ?? "No current same-session Pi context event has been observed",
1862
+ });
1863
+ }
1864
+ const snapshot = authoritativeSnapshotFor(ctx);
1865
+ if (action === "status") {
1866
+ const detail = ownValue(params, "detail");
1867
+ if (detail !== undefined && detail !== "fold_candidates" && detail !== "tree") {
1868
+ throw new Error("status detail must be 'fold_candidates' or 'tree'");
1869
+ }
1870
+ const schedule = advisorySchedule(snapshot, guidance);
1871
+ return toolPayload({
1872
+ ...activeContextStatus(
1873
+ snapshot,
1874
+ persistence.state,
1875
+ boundedInteger(params.offset, 0, 0, 1_000_000, "offset"),
1876
+ boundedInteger(params.limit, 40, 1, 100, "limit"),
1877
+ snapshot.policy.maxFoldSourceRefs,
1878
+ ),
1879
+ available: true,
1880
+ automatic: {
1881
+ pressureRatio: measurements.latestRatio,
1882
+ milestones: Object.fromEntries(schedule.rungs.map((rung) => [
1883
+ rung.milestone,
1884
+ { threshold: rung.threshold, budget: rung.budget },
1885
+ ])),
1886
+ armedMilestone: advisory.armedMilestone,
1887
+ advisory: advisoryState(persistence.state),
1888
+ surfacing: {
1889
+ enabled: surfacingEnabled,
1890
+ sources: surfacing.sources.map((source) => source.id),
1891
+ shown: surfacing.suggestions.map((suggestion) => ({
1892
+ source: suggestion.source,
1893
+ id: suggestion.id,
1894
+ score: suggestion.score,
1895
+ })),
1896
+ log: clone(persistence.state.surfacing ?? []),
1897
+ },
1898
+ historicalGuidanceEntries: advisory.historicalGuidanceEntries,
1899
+ warningRatio: snapshot.policy.warningRatio,
1900
+ toolFoldRatio: snapshot.policy.toolFoldRatio,
1901
+ refoldRatio: snapshot.policy.refoldRatio,
1902
+ chapterPrepareRatio: snapshot.policy.prepareRatio,
1903
+ hardFenceRatio: hardFenceRatio(snapshot),
1904
+ responseReserve: Math.min(
1905
+ ACTIVE_CONTEXT_POLICY.responseReserve,
1906
+ Math.floor(snapshot.contextWindow * 0.1),
1907
+ ),
1908
+ windowSource: snapshot.windowSource,
1909
+ consolidationRatio: snapshot.policy.consolidationRatio,
1910
+ providerMeasurement: measurements.lastProviderMeasurement ? {
1911
+ tokens: measurements.lastProviderMeasurement.tokens,
1912
+ contextWindow: measurements.lastProviderMeasurement.contextWindow,
1913
+ messageSha256: measurements.lastProviderMeasurement.messageSha256,
1914
+ provider: measurements.lastProviderMeasurement.provider,
1915
+ model: measurements.lastProviderMeasurement.model,
1916
+ } : null,
1917
+ measurementFresh: Boolean(measurements.lastProviderMeasurement &&
1918
+ durableProviderMeasurementMatches(measurements.lastProviderMeasurement)),
1919
+ preparing: Boolean(ladder.preparing),
1920
+ preparedFoldId: persistence.state.prepared?.id ?? null,
1921
+ preparedSourceCount: persistence.state.prepared?.sourceRefs.length ?? null,
1922
+ pendingContextNote: ladder.pendingContextNote,
1923
+ lastCandidateId: ladder.lastPreparationCandidateId,
1924
+ lastPreparationError: ladder.lastPreparationError,
1925
+ boundaryFailure: ladder.boundaryFailure,
1926
+ lastSelectionKind: ladder.lastSelectionKind,
1927
+ lastSelectionSourceIds: ladder.lastSelectionSourceIds,
1928
+ lastAutomaticAction: ladder.lastAutomaticAction,
1929
+ automaticSuspended: ladder.automaticFailure !== null,
1930
+ automaticFailure: ladder.automaticFailure ? clone(ladder.automaticFailure) : null,
1931
+ lastCompactionDecision: nativeCompaction.lastThresholdDecision,
1932
+ nativeSummaries: "disabled",
1933
+ freeHarvest: blockingTools.size === 0 ? "disabled" : "enabled",
1934
+ pressureSource: "last-successful-provider-response-only",
1935
+ postOverflowCallback: "blocked-while-stock-native-compaction-is-disabled",
1936
+ sameOperationRetry: false,
1937
+ },
1938
+ ...(detail === "fold_candidates" ? {
1939
+ candidates: foldCandidatesDetail(snapshot, persistence.state, measurements.latestRatio, {
1940
+ summarizerAvailable: Boolean(options.summarizeContextSpan),
1941
+ generation: lifecycle.generation,
1942
+ measurementFresh: Boolean(measurements.lastProviderMeasurement &&
1943
+ durableProviderMeasurementMatches(measurements.lastProviderMeasurement)),
1944
+ automaticFailure: ladder.automaticFailure !== null,
1945
+ preparing: Boolean(ladder.preparing),
1946
+ failedPreparationIds: ladder.failedPreparations,
1947
+ }),
1948
+ } : {}),
1949
+ ...(detail === "tree" ? { tree: foldTreeDetail(snapshot, persistence.state) } : {}),
1950
+ });
1951
+ }
1952
+ // Peeking or expanding a suggested fold is the accept signal. Peek stays
1953
+ // ephemeral: the mark rides the next persisted state, never an entry of its own.
1954
+ const noteSurfacingAccept = (id: string): void => {
1955
+ if (!surfacingEnabled || !persistence.state) return;
1956
+ persistence.state = acceptSurfacingSuggestion(persistence.state, id, surfacingOrdinal(snapshot));
1957
+ };
1958
+ if (action === "peek") {
1959
+ const id = String(params.id ?? "").trim();
1960
+ if (!id) throw new Error("peek requires id");
1961
+ const payload = toolPayload(peekFoldSource({
1962
+ foldId: id,
1963
+ state: persistence.state,
1964
+ entries: ctx.sessionManager.getEntries?.() ?? ctx.sessionManager.getBranch(),
1965
+ sessionId: ctx.sessionManager.getSessionId(),
1966
+ maximumBytes: snapshot.policy.maxChapterChars,
1967
+ }));
1968
+ noteSurfacingAccept(id);
1969
+ return payload;
1970
+ }
1971
+ if (action === "expand" || action === "refold") {
1972
+ const id = String(params.id ?? "").trim();
1973
+ if (!id) throw new Error(`${action} requires id`);
1974
+ requireActiveFold(snapshot, persistence.state, id);
1975
+ if (action === "expand") noteSurfacingAccept(id);
1976
+ let next = setFoldProjectionState(persistence.state, id, action === "expand" ? "expanded" : "folded");
1977
+ if (action === "expand") next = withExpandLease(next, id);
1978
+ else {
1979
+ const leases = { ...next.leases };
1980
+ delete leases[id];
1981
+ next = { ...next, leases };
1982
+ }
1983
+ await persistManual(
1984
+ next,
1985
+ action,
1986
+ ctx,
1987
+ );
1988
+ updateStatus(ctx);
1989
+ return toolPayload({
1990
+ version: 1,
1991
+ action,
1992
+ id,
1993
+ state: action === "expand" ? "expanded" : "folded",
1994
+ activation: "durable immediately; projected on the next model call in this same turn",
1995
+ });
1996
+ }
1997
+ if (action === "protect" || action === "unprotect") {
1998
+ const ids = stringIds(params.ids);
1999
+ await persistManual(protectEvidence(snapshot, persistence.state, ids, action === "protect"), action, ctx);
2000
+ updateStatus(ctx);
2001
+ return toolPayload({
2002
+ version: 1,
2003
+ action,
2004
+ ids,
2005
+ activation: "durable immediately; projected on the next model call in this same turn",
2006
+ });
2007
+ }
2008
+ if (action === "fold") {
2009
+ const ids = stringIds(params.ids);
2010
+ const candidate = manualFoldCandidate(snapshot, persistence.state, ids);
2011
+ const supplied = typeof params.brief === "string" && params.brief.trim() ? params.brief : undefined;
2012
+ const { preparedFold, nextState } = await prepareAndCommitExplicit({
2013
+ snapshot,
2014
+ candidate,
2015
+ brief: supplied,
2016
+ ctx,
2017
+ signal,
2018
+ });
2019
+ advisory.armedMilestone = null;
2020
+ await persistManual(clearArmedAdvisory(nextState), "fold", ctx);
2021
+ updateStatus(ctx);
2022
+ return toolPayload({
2023
+ version: 1,
2024
+ action,
2025
+ id: preparedFold.id,
2026
+ kind: preparedFold.fold.kind,
2027
+ brief: preparedFold.fold.brief,
2028
+ provenance: normalizeLegacyProvenance(preparedFold.fold.provenance),
2029
+ argumentsSha256: executionArgumentsSha256,
2030
+ durableRevision: persistence.state.revision,
2031
+ activation: "durable immediately; projected on the next model call in this same turn",
2032
+ expand: { action: "expand", id: preparedFold.id },
2033
+ });
2034
+ }
2035
+ throw new Error(`Unknown ${toolName} action '${action}'`);
2036
+ };
2037
+ const toolHandler = async (
2038
+ _toolCallId: string,
2039
+ params: Record<string, unknown>,
2040
+ signal: AbortSignal,
2041
+ _onUpdate: unknown,
2042
+ ctx: any,
2043
+ ): Promise<unknown> => {
2044
+ const operation = ladder.actionQueue.then(() => executeAction(params, signal, ctx));
2045
+ ladder.actionQueue = operation.catch(() => undefined);
2046
+ return operation;
2047
+ };
2048
+ const statusCommandHandler = async (_args: string, ctx: any): Promise<void> => {
2049
+ if (!persistence.state) persistence.state = emptyActiveContextState(ctx.sessionManager.getSessionId());
2050
+ try {
2051
+ const snapshot = authoritativeSnapshotFor(ctx);
2052
+ const status = activeContextStatus(snapshot, persistence.state, 0, 40);
2053
+ safeNotify(
2054
+ ctx,
2055
+ `Active context: ${status.totalFolds} fold(s), roots ${(status.roots as string[]).join(", ") || "none"}. ` +
2056
+ `Use ${toolName} status for exact recursive actions.`,
2057
+ "info",
2058
+ );
2059
+ } catch (error) {
2060
+ safeNotify(ctx, `Active-context status unavailable; native Pi context is unchanged: ${String(error)}`, "warning");
2061
+ }
2062
+ };
2063
+ const foldCommandHandler = async (args: string, ctx: any): Promise<void> => {
2064
+ const operation = ladder.actionQueue.then(async () => {
2065
+ if (!persistence.state) persistence.state = emptyActiveContextState(ctx.sessionManager.getSessionId());
2066
+ const snapshot = lifecycle.latestSnapshot
2067
+ ? authoritativeSnapshotFor(ctx)
2068
+ : snapshotForEvent(ctx, ctx.sessionManager.buildSessionContext().messages);
2069
+ if (!lifecycle.latestSnapshot) lifecycle.latestSnapshot = snapshot;
2070
+ const divider = args.indexOf(" -- ");
2071
+ const selector = (divider >= 0 ? args.slice(0, divider) : args).trim();
2072
+ const supplied = (divider >= 0 ? args.slice(divider + 4) : "").trim() || undefined;
2073
+ const ids = selector ? selector.replace(/\.\./g, " ").split(/[\s,]+/).filter(Boolean) : [];
2074
+ const candidate = ids.length
2075
+ ? manualFoldCandidate(snapshot, persistence.state!, ids)
2076
+ : selectAutomaticChapter(snapshot, persistence.state!) ?? selectAutomaticToolBatch(snapshot, persistence.state!, 1)[0] ?? null;
2077
+ if (!candidate) throw new Error("No exact stale rescue span is currently eligible");
2078
+ const stateBefore = persistence.state!;
2079
+ const persistedBefore = persistence.persisted ? clone(persistence.persisted) : null;
2080
+ const generationBefore = lifecycle.generation;
2081
+ const { preparedFold, nextState } = await prepareAndCommitExplicit({
2082
+ snapshot,
2083
+ candidate,
2084
+ brief: supplied,
2085
+ ctx,
2086
+ maximumSourceChars: USER_RESCUE_MAX_SOURCE_CHARS,
2087
+ });
2088
+ if (!sessionIdentityStillValid(ctx, stateBefore.sessionId, generationBefore)) {
2089
+ throw new Error("Active-context session changed before rescue persistence");
2090
+ }
2091
+ persistence.state = clearArmedAdvisory(nextState);
2092
+ advisory.armedMilestone = null;
2093
+ try { await persist(ctx); }
2094
+ catch (error) {
2095
+ if (sessionIdentityStillValid(ctx, stateBefore.sessionId, generationBefore)) {
2096
+ persistence.state = stateBefore;
2097
+ persistence.persisted = persistedBefore;
2098
+ }
2099
+ throw error;
2100
+ }
2101
+ ladder.pendingManual = false;
2102
+ ladder.automaticFailure = null;
2103
+ advisory.armedMilestone = null;
2104
+ ladder.pendingContextNote =
2105
+ `User rescue folded stale context under ${preparedFold.id}; exact source remains expandable.`;
2106
+ ladder.lastAutomaticAction = {
2107
+ kind: "user-rescue-fold",
2108
+ foldIds: [preparedFold.id],
2109
+ sourceIds: preparedFold.sourceRefs.map((ref) => ref.entryId),
2110
+ };
2111
+ updateStatus(ctx);
2112
+ safeNotify(
2113
+ ctx,
2114
+ `Folded ${preparedFold.fold.sourceChars} bytes into ${preparedFold.id}. Exact source remains expandable.`,
2115
+ "info",
2116
+ );
2117
+ });
2118
+ ladder.actionQueue = operation.catch(() => undefined);
2119
+ try { await operation; }
2120
+ catch (error) {
2121
+ safeNotify(ctx, `Context rescue failed: ${error instanceof Error ? error.message : String(error)}`, "error");
2122
+ }
2123
+ };
2124
+
2125
+ pi.registerTool(buildActiveContextTool({
2126
+ name: toolName,
2127
+ label: toolLabel,
2128
+ allowedActions: allowedToolActions,
2129
+ fullSurface: allowedToolActions.length === ACTIVE_CONTEXT_TOOL_ACTIONS.length,
2130
+ maxBriefChars: ACTIVE_CONTEXT_POLICY.maxBriefChars,
2131
+ handler: toolHandler,
2132
+ }));
2133
+ for (const command of buildActiveContextCommands({
2134
+ statusName: commandNames.status,
2135
+ foldName: commandNames.fold,
2136
+ statusHandler: statusCommandHandler,
2137
+ foldHandler: foldCommandHandler,
2138
+ })) {
2139
+ pi.registerCommand(command.name, {
2140
+ description: command.description,
2141
+ handler: command.handler,
2142
+ });
2143
+ }
2144
+ pi.on("session_shutdown", (_event: unknown, ctx: any) => {
2145
+ lifecycle.generation += 1;
2146
+ lifecycle.shuttingDown = true;
2147
+ cancelPreparation();
2148
+ persistence.state = null;
2149
+ persistence.persisted = null;
2150
+ lifecycle.latestSnapshot = null;
2151
+ advisory.armedMilestone = null;
2152
+ surfacing.suggestions = [];
2153
+ try { ctx.ui?.setStatus?.(entryTypePrefix, undefined); } catch { /* Shutdown cannot be blocked by UI. */ }
2154
+ });
2155
+
2156
+ return { projectionCandidates, registerSuggestionSource };
2157
+ }