pi-fold 0.0.1 → 0.1.0

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