ds4-context-engine 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,1989 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { existsSync, realpathSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+ import type {
5
+ Api,
6
+ AssistantMessage,
7
+ Model,
8
+ } from "@earendil-works/pi-ai";
9
+ import {
10
+ sessionEntryToContextMessages,
11
+ type ContextEvent,
12
+ type ExtensionAPI,
13
+ type ExtensionContext,
14
+ type SessionBeforeCompactEvent,
15
+ type SessionCompactEvent,
16
+ } from "@earendil-works/pi-coding-agent";
17
+ import {
18
+ ArtifactManager,
19
+ disabledArtifactDiagnostics,
20
+ type ArtifactDiagnostics,
21
+ type ArtifactSearchResult,
22
+ } from "ds4-context-core/artifacts/artifact-manager";
23
+ import { FileArtifactStore } from "ds4-context-core/artifacts/artifact-store";
24
+ import {
25
+ CompactionCoordinator,
26
+ defaultCompactionDiagnostics,
27
+ defaultSummaryGraphDiagnostics,
28
+ type CompactionDiagnostics,
29
+ type SessionCompactFailedLike,
30
+ type SummaryGraphDiagnostics,
31
+ } from "../pi-adapter/compaction-coordinator.ts";
32
+ import {
33
+ NativeContinuationManager,
34
+ type NativeContinuationAttempt,
35
+ type NativeContinuationDiagnostics,
36
+ type NativeContinuationManifest,
37
+ type PreparedNativeContinuation,
38
+ } from "ds4-context-core/continuation/native-continuation";
39
+ export type {
40
+ CompactionDiagnostics,
41
+ CompactionPhase,
42
+ SummaryGraphDiagnostics,
43
+ } from "../pi-adapter/compaction-coordinator.ts";
44
+ import { loadConfig, resolveDatabasePath, type LoadedConfig } from "ds4-context-core/config/config-loader";
45
+ import { createDefaultConfig, type Ds4ContextConfig } from "ds4-context-core/config/config";
46
+ import { calculateContextBudget, type ContextBudget } from "ds4-context-core/core/budget-manager";
47
+ import {
48
+ modelProfileKey,
49
+ resolveModelAwareness,
50
+ type ResolvedModelAwareness,
51
+ type TokenCalibrationSample,
52
+ } from "ds4-context-core/core/model-awareness";
53
+ import type { ModelDescriptor } from "ds4-context-core/core/model-profile";
54
+ import { estimateMessagesTokens } from "ds4-context-core/core/token-estimator";
55
+ import type {
56
+ ContextManifest,
57
+ ModelAwarenessManifest,
58
+ ModelSwitchManifest,
59
+ PrivacyManifest,
60
+ ProviderUsageManifest,
61
+ } from "ds4-context-core/manifest/context-manifest";
62
+ import type { ExcludedContextSource, ObservedTool } from "ds4-context-core/manifest/observer";
63
+ import {
64
+ disabledMemoryDiagnostics,
65
+ MemoryManager,
66
+ type MemoryDiagnostics,
67
+ type MemorySelection,
68
+ } from "ds4-context-core/memory/memory-manager";
69
+ import {
70
+ MEMORY_CUSTOM_ENTRY_TYPE,
71
+ PIN_CUSTOM_ENTRY_TYPE,
72
+ type MemoryItem,
73
+ type MemoryMutation,
74
+ type MemoryScope,
75
+ type PinItem,
76
+ type PinMutation,
77
+ type PinScope,
78
+ } from "ds4-context-core/memory/memory-types";
79
+ import { planManagedContext, type SupplementalContextMessage } from "ds4-context-core/planner/context-planner";
80
+ import {
81
+ disabledPrivacyDiagnostics,
82
+ emptyPrivacyCounts,
83
+ PrivacyPolicyEngine,
84
+ type PrivacyClassification,
85
+ type PrivacyDiagnostics,
86
+ } from "ds4-context-core/privacy/privacy-policy";
87
+ import {
88
+ emptyProjectDiagnostics,
89
+ ProjectKnowledgeManager,
90
+ type ProjectKnowledgeDiagnostics,
91
+ } from "ds4-context-core/project/project-knowledge";
92
+ import {
93
+ emptyRetrievalDiagnostics,
94
+ HistoricalRetrievalEngine,
95
+ type RetrievalDiagnostics,
96
+ } from "ds4-context-core/retrieval/retrieval-engine";
97
+ import { currentRequestText } from "ds4-context-core/retrieval/task-descriptor";
98
+ import { ContextDatabase, type DatabaseHealth, type SessionIndexStats } from "ds4-context-core/persistence/sqlite";
99
+ import {
100
+ activeTools,
101
+ buildPiObserverManifest,
102
+ findExactPiMessageSourceIds,
103
+ findPiPinnedMessageIndices,
104
+ } from "../pi-adapter/context-observer.ts";
105
+ import { projectSessionFileMutations } from "../pi-adapter/memory-adapter.ts";
106
+ import { PiSessionIndexer, type SessionIndexResult } from "../pi-adapter/session-indexer.ts";
107
+ import { snapshotModel, snapshotSession, type PiSessionSnapshot } from "../pi-adapter/session-reader.ts";
108
+ import { silentLogger, StructuredLogger, type Logger } from "ds4-context-core/shared/logging";
109
+ import {
110
+ EXTENSION_VERSION,
111
+ OBSERVER_PLANNER_VERSION,
112
+ PLANNER_VERSION,
113
+ POLICY_VERSION,
114
+ SUPPORTED_PI_VERSION,
115
+ } from "../pi-adapter/version.ts";
116
+
117
+ export type RuntimePhase = "idle" | "initializing" | "disabled" | "observer" | "managed" | "degraded" | "closed";
118
+
119
+ const READ_ONLY_PROJECT_TOOLS = new Set(["read", "grep", "find", "ls"]);
120
+
121
+ export interface RuntimeDependencies {
122
+ agentDir: string;
123
+ configDirName: string;
124
+ homeDir?: string;
125
+ now?: () => number;
126
+ idGenerator?: () => string;
127
+ logSink?: (line: string) => void;
128
+ }
129
+
130
+ interface PreparedPrivacyContext {
131
+ event: ContextEvent;
132
+ messageClassifications: PrivacyClassification[];
133
+ messagePrivacyReasons: Array<string | undefined>;
134
+ systemPrompt: string;
135
+ systemClassification?: PrivacyClassification;
136
+ systemPrivacyReason?: string;
137
+ tools: ObservedTool[];
138
+ changed: boolean;
139
+ blockedBlocks: number;
140
+ secretRedactions: number;
141
+ }
142
+
143
+ export interface ContextObservation {
144
+ mode: "observer" | "managed" | "fallback";
145
+ messageCount: number;
146
+ estimatedMessageTokens: number;
147
+ originalMessageCount: number;
148
+ originalEstimatedMessageTokens: number;
149
+ reportedTokens?: number;
150
+ planningDurationMs?: number;
151
+ fallbackReason?: string;
152
+ observedAt: number;
153
+ budget?: ContextBudget;
154
+ }
155
+
156
+ function canonicalProjectPath(path: string): string {
157
+ const absolute = resolve(path);
158
+ try {
159
+ return realpathSync.native(absolute);
160
+ } catch {
161
+ return absolute;
162
+ }
163
+ }
164
+
165
+ function failClosedMessage(message: ContextEvent["messages"][number]): ContextEvent["messages"][number] {
166
+ if (!message || typeof message !== "object") return message;
167
+ const record = message as unknown as Record<string, unknown>;
168
+ const replacement = "[DS4 content unavailable because privacy enforcement failed closed]";
169
+ const sanitizeBlock = (block: unknown): unknown => {
170
+ if (!block || typeof block !== "object") return replacement;
171
+ const value = block as Record<string, unknown>;
172
+ if (value.type === "toolCall") return { ...value, arguments: {} };
173
+ if (value.type === "text" || value.type === "thinking") return { ...value, text: replacement };
174
+ if (value.type === "image") return { type: "text", text: replacement };
175
+ return { type: "text", text: replacement };
176
+ };
177
+ return {
178
+ ...record,
179
+ ...(Array.isArray(record.content)
180
+ ? { content: record.content.map(sanitizeBlock) }
181
+ : "content" in record
182
+ ? { content: replacement }
183
+ : {}),
184
+ ...(record.arguments && typeof record.arguments === "object" ? { arguments: {} } : {}),
185
+ } as unknown as ContextEvent["messages"][number];
186
+ }
187
+
188
+ function privacyReason(blockedBlocks: number, secretRedactions: number): string | undefined {
189
+ const reasons = [
190
+ ...(blockedBlocks > 0 ? [`${blockedBlocks} classified block(s) excluded due to privacy policy`] : []),
191
+ ...(secretRedactions > 0 ? [`${secretRedactions} credential-like value(s) redacted`] : []),
192
+ ];
193
+ return reasons.length > 0 ? reasons.join("; ") : undefined;
194
+ }
195
+
196
+ function numericUsage(value: unknown): number {
197
+ return typeof value === "number" && Number.isFinite(value) && value > 0
198
+ ? Math.floor(value)
199
+ : 0;
200
+ }
201
+
202
+ function providerUsageManifest(input: {
203
+ inputTokens: number;
204
+ cacheReadTokens: number;
205
+ cacheWriteTokens: number;
206
+ }): ProviderUsageManifest {
207
+ const totalInputTokens = input.inputTokens + input.cacheReadTokens + input.cacheWriteTokens;
208
+ const rounded = (value: number): number => Math.round(value * 1_000_000) / 1_000_000;
209
+ return {
210
+ ...input,
211
+ totalInputTokens,
212
+ cacheReadShare: totalInputTokens > 0 ? rounded(input.cacheReadTokens / totalInputTokens) : 0,
213
+ cacheWriteShare: totalInputTokens > 0 ? rounded(input.cacheWriteTokens / totalInputTokens) : 0,
214
+ };
215
+ }
216
+
217
+ export interface CreatePinInput {
218
+ content: string;
219
+ scope: PinScope;
220
+ sourceEntryId?: string;
221
+ sourceFile?: string;
222
+ supersedes?: string;
223
+ classification?: PrivacyClassification;
224
+ }
225
+
226
+ export interface CreateMemoryInput {
227
+ claim: string;
228
+ scope: MemoryScope;
229
+ key?: string;
230
+ classification?: PrivacyClassification;
231
+ sourceEntryIds: string[];
232
+ }
233
+
234
+ export type CustomEntryAppender = (customType: string, data: MemoryMutation | PinMutation) => void;
235
+
236
+ export interface RuntimeDiagnostics {
237
+ extensionVersion: string;
238
+ supportedPiVersion: string;
239
+ plannerVersion: string;
240
+ phase: RuntimePhase;
241
+ enabled: boolean;
242
+ contextMode: "observer" | "managed";
243
+ session?: PiSessionSnapshot;
244
+ model?: { provider: string; id: string };
245
+ databasePath?: string;
246
+ databaseSchemaVersion?: number;
247
+ indexed?: SessionIndexStats;
248
+ observation?: ContextObservation;
249
+ lastManifest?: ContextManifest;
250
+ retrieval: RetrievalDiagnostics;
251
+ project: ProjectKnowledgeDiagnostics;
252
+ memory: MemoryDiagnostics;
253
+ privacy: PrivacyDiagnostics;
254
+ modelAwareness?: ModelAwarenessManifest;
255
+ nativeContinuation: NativeContinuationDiagnostics;
256
+ artifacts: ArtifactDiagnostics;
257
+ compaction: CompactionDiagnostics;
258
+ lastIndexResult?: SessionIndexResult;
259
+ configFiles: string[];
260
+ configWarnings: string[];
261
+ lastIndexError?: string;
262
+ lastError?: string;
263
+ }
264
+
265
+ export class Ds4ContextRuntime {
266
+ private phase: RuntimePhase = "idle";
267
+ private config: Ds4ContextConfig = createDefaultConfig();
268
+ private loadedConfig?: LoadedConfig;
269
+ private session?: PiSessionSnapshot;
270
+ private database?: ContextDatabase;
271
+ private indexer?: PiSessionIndexer;
272
+ private databasePath?: string;
273
+ private observation?: ContextObservation;
274
+ private lastManifest?: ContextManifest;
275
+ private pendingManifestId?: string;
276
+ private retrievalEngine?: HistoricalRetrievalEngine;
277
+ private lastRetrieval: RetrievalDiagnostics = emptyRetrievalDiagnostics();
278
+ private projectKnowledge?: ProjectKnowledgeManager;
279
+ private lastProject: ProjectKnowledgeDiagnostics = emptyProjectDiagnostics();
280
+ private projectRefreshPending = false;
281
+ private memoryManager?: MemoryManager;
282
+ private lastMemory: MemoryDiagnostics = disabledMemoryDiagnostics();
283
+ private privacyEngine?: PrivacyPolicyEngine;
284
+ private lastPrivacy: PrivacyDiagnostics = disabledPrivacyDiagnostics();
285
+ private lastModelAwareness?: ModelAwarenessManifest;
286
+ private pendingModelSwitch?: ModelSwitchManifest;
287
+ private nativeContinuation: NativeContinuationManager;
288
+ private lastContextProfileKey?: string;
289
+ private readonly knownModelProfiles = new Set<string>();
290
+ private readonly volatileCalibration = new Map<string, TokenCalibrationSample[]>();
291
+ private lastMemoryMutationSignature?: string;
292
+ private artifactManager?: ArtifactManager;
293
+ private lastArtifacts: ArtifactDiagnostics = disabledArtifactDiagnostics();
294
+ private compaction?: CompactionCoordinator;
295
+ private lastIndexResult?: SessionIndexResult;
296
+ private lastIndexError?: string;
297
+ private lastError?: string;
298
+ private logger: Logger = silentLogger;
299
+ private readonly now: () => number;
300
+ private readonly idGenerator: () => string;
301
+
302
+ constructor(private readonly dependencies: RuntimeDependencies) {
303
+ this.now = dependencies.now ?? Date.now;
304
+ this.idGenerator = dependencies.idGenerator ?? randomUUID;
305
+ this.nativeContinuation = new NativeContinuationManager(
306
+ this.config.nativeContinuation,
307
+ this.now,
308
+ );
309
+ }
310
+
311
+ openSession(ctx: ExtensionContext): void {
312
+ this.closeDatabase();
313
+ this.phase = "initializing";
314
+ this.lastError = undefined;
315
+ this.lastIndexError = undefined;
316
+ this.lastIndexResult = undefined;
317
+ this.lastManifest = undefined;
318
+ this.pendingManifestId = undefined;
319
+ this.retrievalEngine = undefined;
320
+ this.lastRetrieval = emptyRetrievalDiagnostics(
321
+ this.config.context.maxRetrievedHistoryTokens,
322
+ this.config.retrieval.maxResults,
323
+ );
324
+ this.projectKnowledge = undefined;
325
+ this.lastProject = emptyProjectDiagnostics();
326
+ this.projectRefreshPending = false;
327
+ this.memoryManager = undefined;
328
+ this.lastMemory = disabledMemoryDiagnostics();
329
+ this.privacyEngine = undefined;
330
+ this.lastPrivacy = disabledPrivacyDiagnostics();
331
+ this.lastModelAwareness = undefined;
332
+ this.pendingModelSwitch = undefined;
333
+ this.nativeContinuation.invalidate("session-opened");
334
+ this.lastContextProfileKey = undefined;
335
+ this.knownModelProfiles.clear();
336
+ this.volatileCalibration.clear();
337
+ this.lastMemoryMutationSignature = undefined;
338
+ this.artifactManager = undefined;
339
+ this.lastArtifacts = disabledArtifactDiagnostics();
340
+ this.compaction = undefined;
341
+ this.observation = undefined;
342
+ this.session = snapshotSession(ctx);
343
+
344
+ try {
345
+ this.loadedConfig = loadConfig({
346
+ agentDir: this.dependencies.agentDir,
347
+ cwd: ctx.cwd,
348
+ configDirName: this.dependencies.configDirName,
349
+ projectTrusted: ctx.isProjectTrusted(),
350
+ ...(this.dependencies.homeDir ? { homeDir: this.dependencies.homeDir } : {}),
351
+ });
352
+ this.config = this.loadedConfig.config;
353
+ this.nativeContinuation = new NativeContinuationManager(
354
+ this.config.nativeContinuation,
355
+ this.now,
356
+ );
357
+ this.privacyEngine = new PrivacyPolicyEngine(this.config.privacy);
358
+ this.logger = new StructuredLogger({
359
+ level: this.config.diagnostics.logLevel,
360
+ ...(this.dependencies.logSink ? { sink: this.dependencies.logSink } : {}),
361
+ });
362
+
363
+ for (const warning of this.loadedConfig.warnings) this.logger.warn("configuration.warning", { warning });
364
+ if (this.loadedConfig.warnings.length > 0 && ctx.hasUI) {
365
+ ctx.ui.notify(
366
+ `DS4 Context Engine: ${this.loadedConfig.warnings.length} configuration warning(s). See logs for details.`,
367
+ "warning",
368
+ );
369
+ }
370
+
371
+ if (!this.config.enabled) {
372
+ this.phase = "disabled";
373
+ this.setStatus(ctx, "DS4 ctx: disabled");
374
+ this.logger.info("runtime.disabled", { sessionId: this.session.sessionId });
375
+ return;
376
+ }
377
+
378
+ this.databasePath = resolveDatabasePath(
379
+ this.config.storage.databasePath,
380
+ this.dependencies.agentDir,
381
+ this.dependencies.homeDir,
382
+ );
383
+ this.database = ContextDatabase.open(this.databasePath, {
384
+ logger: this.logger,
385
+ now: this.now(),
386
+ });
387
+
388
+ this.indexer = new PiSessionIndexer(this.database.sessionIndex, {
389
+ logger: this.logger,
390
+ now: this.now,
391
+ });
392
+ this.retrievalEngine = new HistoricalRetrievalEngine(this.database.sessionIndex);
393
+ this.lastRetrieval = emptyRetrievalDiagnostics(
394
+ this.config.context.maxRetrievedHistoryTokens,
395
+ this.config.retrieval.maxResults,
396
+ );
397
+ this.initializeProjectKnowledge(ctx);
398
+ if (this.session.sessionFile) {
399
+ this.database.upsertSession({
400
+ sessionId: this.session.sessionId,
401
+ sessionFile: this.session.sessionFile,
402
+ projectPath: this.session.projectPath,
403
+ indexedAt: this.now(),
404
+ });
405
+ this.syncSessionIndex(ctx);
406
+ this.lastManifest = this.database.manifests.getLatest(this.session.sessionId);
407
+ if (this.lastManifest) {
408
+ this.lastContextProfileKey = modelProfileKey(
409
+ this.lastManifest.provider,
410
+ this.lastManifest.model,
411
+ );
412
+ this.knownModelProfiles.add(this.lastContextProfileKey);
413
+ this.lastModelAwareness = this.lastManifest.modelAwareness;
414
+ }
415
+ }
416
+ this.initializeMemory(ctx);
417
+ this.initializeArtifacts(ctx);
418
+ this.compaction = new CompactionCoordinator({
419
+ config: this.config,
420
+ database: this.database,
421
+ sessionId: this.session.sessionId,
422
+ persisted: Boolean(this.session.sessionFile),
423
+ logger: this.logger,
424
+ now: this.now,
425
+ idGenerator: this.idGenerator,
426
+ syncSessionIndex: (context) => {
427
+ this.syncSessionIndex(context);
428
+ },
429
+ latestManifest: () => this.lastManifest,
430
+ resolveModelBudget: (model) => {
431
+ const resolved = this.resolveModelPolicy(model);
432
+ return {
433
+ budget: resolved.budget,
434
+ recentTailTokens: resolved.awareness.limits.recentTailTokens,
435
+ };
436
+ },
437
+ sanitizeContent: (text, provider) => this.privacyEngine?.sanitizeText(text, provider).value ?? text,
438
+ classifyContent: (text, provider) => {
439
+ const sanitized = this.privacyEngine?.sanitizeText(text, provider);
440
+ return sanitized
441
+ ? { value: sanitized.value, classification: sanitized.classification }
442
+ : { value: text, classification: "normal" };
443
+ },
444
+ });
445
+ this.compaction.initialize(ctx.sessionManager.getEntries());
446
+
447
+ this.phase = this.config.context.mode;
448
+ this.setStatus(ctx, `DS4 ctx: ${this.config.context.mode}`);
449
+ this.logger.info("session.opened", {
450
+ sessionId: this.session.sessionId,
451
+ persisted: Boolean(this.session.sessionFile),
452
+ projectPath: this.session.projectPath,
453
+ });
454
+ } catch (error) {
455
+ this.enterDegradedMode(ctx, error, "startup");
456
+ }
457
+ }
458
+
459
+ private preparePrivacyContext(
460
+ event: ContextEvent,
461
+ ctx: ExtensionContext,
462
+ pi: ExtensionAPI,
463
+ ): PreparedPrivacyContext {
464
+ const provider = ctx.model?.provider ?? "unknown";
465
+ const tools = activeTools(pi);
466
+ if (!this.config.privacy.enabled || !this.privacyEngine) {
467
+ return {
468
+ event,
469
+ messageClassifications: [],
470
+ messagePrivacyReasons: [],
471
+ systemPrompt: ctx.getSystemPrompt(),
472
+ tools,
473
+ changed: false,
474
+ blockedBlocks: 0,
475
+ secretRedactions: 0,
476
+ };
477
+ }
478
+
479
+ const messages = event.messages.map((message) => this.privacyEngine!.sanitizeMessage(message, provider));
480
+ const system = this.privacyEngine.sanitizeText(ctx.getSystemPrompt(), provider);
481
+ const toolResults = tools.map((tool) => this.privacyEngine!.sanitizeMessage(tool, provider));
482
+ const sanitizedTools = toolResults.map((sanitized) => ({
483
+ ...sanitized.value,
484
+ classification: sanitized.classification,
485
+ ...(privacyReason(sanitized.blockedBlocks, sanitized.secretRedactions)
486
+ ? { privacyReason: privacyReason(sanitized.blockedBlocks, sanitized.secretRedactions) }
487
+ : {}),
488
+ }));
489
+ const blockedBlocks = messages.reduce((total, item) => total + item.blockedBlocks, system.blockedBlocks)
490
+ + toolResults.reduce((total, item) => total + item.blockedBlocks, 0);
491
+ const secretRedactions = messages.reduce((total, item) => total + item.secretRedactions, system.secretRedactions)
492
+ + toolResults.reduce((total, item) => total + item.secretRedactions, 0);
493
+ const policy = this.privacyEngine.policy(provider);
494
+ this.lastPrivacy = {
495
+ enabled: true,
496
+ provider,
497
+ destination: policy.destination,
498
+ allowedClassifications: [...policy.allowedClassifications],
499
+ inspectedMessages: messages.length,
500
+ selectedClassifications: emptyPrivacyCounts(),
501
+ blockedBlocks,
502
+ excludedSources: 0,
503
+ secretRedactions,
504
+ providerChecks: 0,
505
+ providerPayloadRedactions: 0,
506
+ enforcement: "context",
507
+ warnings: [],
508
+ };
509
+ return {
510
+ event: { type: "context", messages: messages.map((item) => item.value) },
511
+ messageClassifications: messages.map((item) => item.classification),
512
+ messagePrivacyReasons: messages.map((item) => privacyReason(item.blockedBlocks, item.secretRedactions)),
513
+ systemPrompt: system.value,
514
+ systemClassification: system.classification,
515
+ ...(privacyReason(system.blockedBlocks, system.secretRedactions)
516
+ ? { systemPrivacyReason: privacyReason(system.blockedBlocks, system.secretRedactions) }
517
+ : {}),
518
+ tools: sanitizedTools,
519
+ changed: system.changed || messages.some((item) => item.changed) || toolResults.some((item) => item.changed),
520
+ blockedBlocks,
521
+ secretRedactions,
522
+ };
523
+ }
524
+
525
+ private privacyManifest(classifications: readonly PrivacyClassification[]): PrivacyManifest | undefined {
526
+ if (!this.lastPrivacy.enabled || !this.lastPrivacy.provider || !this.lastPrivacy.destination) return undefined;
527
+ const provider = this.lastPrivacy.provider;
528
+ const destination = this.lastPrivacy.destination;
529
+ const counts = emptyPrivacyCounts();
530
+ for (const classification of classifications) counts[classification]++;
531
+ this.lastPrivacy = { ...this.lastPrivacy, selectedClassifications: counts };
532
+ return {
533
+ enabled: true,
534
+ destination,
535
+ provider,
536
+ allowedClassifications: [...this.lastPrivacy.allowedClassifications],
537
+ selectedClassifications: { ...counts },
538
+ blockedBlocks: this.lastPrivacy.blockedBlocks,
539
+ excludedSources: this.lastPrivacy.excludedSources,
540
+ secretRedactions: this.lastPrivacy.secretRedactions,
541
+ providerChecks: this.lastPrivacy.providerChecks,
542
+ providerPayloadRedactions: this.lastPrivacy.providerPayloadRedactions,
543
+ enforcement: this.lastPrivacy.enforcement === "context-and-provider"
544
+ ? "context-and-provider"
545
+ : "context",
546
+ };
547
+ }
548
+
549
+ private calibrationSamples(model: ModelDescriptor): TokenCalibrationSample[] {
550
+ if (this.database && this.session?.sessionFile && this.config.diagnostics.storeContextManifest) {
551
+ return this.database.manifests.listCalibrationSamples(
552
+ model.provider,
553
+ model.id,
554
+ this.config.modelAwareness.calibrationWindow,
555
+ );
556
+ }
557
+ return [...(this.volatileCalibration.get(modelProfileKey(model.provider, model.id)) ?? [])];
558
+ }
559
+
560
+ private switchForModel(model: ModelDescriptor): ModelSwitchManifest {
561
+ const key = modelProfileKey(model.provider, model.id);
562
+ const pending = this.pendingModelSwitch;
563
+ if (pending) {
564
+ this.pendingModelSwitch = undefined;
565
+ return pending;
566
+ }
567
+ const previousKey = this.lastContextProfileKey;
568
+ const separator = previousKey?.indexOf("/") ?? -1;
569
+ const switched = previousKey !== undefined && previousKey !== key;
570
+ return {
571
+ source: "context",
572
+ switched,
573
+ ...(separator > 0 && previousKey ? {
574
+ previousProvider: previousKey.slice(0, separator),
575
+ previousModel: previousKey.slice(separator + 1),
576
+ } : {}),
577
+ profileReused: this.knownModelProfiles.has(key),
578
+ cacheDisposition: switched
579
+ ? "cold-model-switch"
580
+ : previousKey === key
581
+ ? "eligible"
582
+ : "unknown",
583
+ };
584
+ }
585
+
586
+ private resolveModelPolicy(model: ModelDescriptor): {
587
+ awareness: ResolvedModelAwareness;
588
+ budget: ContextBudget;
589
+ } {
590
+ const awareness = resolveModelAwareness(
591
+ model,
592
+ this.config.context,
593
+ this.config.modelAwareness,
594
+ this.calibrationSamples(model),
595
+ );
596
+ return {
597
+ awareness,
598
+ budget: calculateContextBudget(
599
+ awareness.profile,
600
+ awareness.contextConfig,
601
+ awareness.calibration,
602
+ ),
603
+ };
604
+ }
605
+
606
+ private resolveActiveModel(model: ModelDescriptor): {
607
+ awareness: ResolvedModelAwareness;
608
+ budget: ContextBudget;
609
+ manifest: ModelAwarenessManifest;
610
+ } {
611
+ const { awareness, budget } = this.resolveModelPolicy(model);
612
+ const modelSwitch = this.switchForModel(model);
613
+ const manifest: ModelAwarenessManifest = {
614
+ enabled: this.config.modelAwareness.enabled,
615
+ profileKey: awareness.profileKey,
616
+ overrideKeys: [...awareness.overrideKeys],
617
+ contextWindow: awareness.profile.contextWindow,
618
+ ...(awareness.profile.maxOutputTokens !== undefined
619
+ ? { maxOutputTokens: awareness.profile.maxOutputTokens }
620
+ : {}),
621
+ safetyMarginTokens: awareness.profile.safetyMarginTokens,
622
+ calibration: {
623
+ ...awareness.calibration,
624
+ cache: { ...awareness.calibration.cache },
625
+ },
626
+ adaptive: { ...awareness.limits },
627
+ switch: modelSwitch,
628
+ };
629
+ this.lastModelAwareness = manifest;
630
+ this.lastContextProfileKey = awareness.profileKey;
631
+ this.knownModelProfiles.add(awareness.profileKey);
632
+ return { awareness, budget, manifest };
633
+ }
634
+
635
+ private rememberVolatileCalibration(
636
+ manifest: ContextManifest,
637
+ usage: ProviderUsageManifest,
638
+ createdAt: number,
639
+ ): void {
640
+ const key = modelProfileKey(manifest.provider, manifest.model);
641
+ const samples = this.volatileCalibration.get(key) ?? [];
642
+ samples.unshift({
643
+ estimatedTokens: manifest.estimatedInputTokens,
644
+ actualInputTokens: usage.totalInputTokens,
645
+ inputTokens: usage.inputTokens,
646
+ cacheReadTokens: usage.cacheReadTokens,
647
+ cacheWriteTokens: usage.cacheWriteTokens,
648
+ createdAt,
649
+ });
650
+ this.volatileCalibration.set(
651
+ key,
652
+ samples.slice(0, this.config.modelAwareness.calibrationWindow),
653
+ );
654
+ }
655
+
656
+ transformContext(
657
+ event: ContextEvent,
658
+ ctx: ExtensionContext,
659
+ pi: ExtensionAPI,
660
+ ): { messages?: ContextEvent["messages"] } | undefined {
661
+ if (!this.config.enabled) return undefined;
662
+ if (this.phase === "degraded" && this.config.privacy.enabled) {
663
+ try {
664
+ const prepared = this.preparePrivacyContext(event, ctx, pi);
665
+ return prepared.changed ? { messages: prepared.event.messages } : undefined;
666
+ } catch {
667
+ return { messages: event.messages.map(failClosedMessage) };
668
+ }
669
+ }
670
+ if (this.phase !== "observer" && this.phase !== "managed") return undefined;
671
+
672
+ let privacyFallback: { messages: ContextEvent["messages"] } | undefined;
673
+ try {
674
+ const preparedPrivacy = this.preparePrivacyContext(event, ctx, pi);
675
+ if (preparedPrivacy.changed) privacyFallback = { messages: preparedPrivacy.event.messages };
676
+ this.syncSessionIndex(ctx);
677
+ this.refreshProjectIndexIfPending();
678
+ if (this.config.context.mode !== "managed") {
679
+ this.lastRetrieval = emptyRetrievalDiagnostics(
680
+ this.config.context.maxRetrievedHistoryTokens,
681
+ this.config.retrieval.maxResults,
682
+ );
683
+ }
684
+ let effectiveEvent = preparedPrivacy.event;
685
+ let artifactReferences = [] as NonNullable<ContextManifest["artifacts"]>;
686
+ const artifactReferenceByMessageIndex = new Map<number, NonNullable<ContextManifest["artifacts"]>[number]>();
687
+ let artifactized = false;
688
+ if (this.config.context.mode === "managed" && this.artifactManager) {
689
+ const sourceEntryIds = findExactPiMessageSourceIds(event.messages, ctx);
690
+ const transformed = this.artifactManager.transform(
691
+ preparedPrivacy.event.messages,
692
+ sourceEntryIds,
693
+ preparedPrivacy.messageClassifications,
694
+ );
695
+ effectiveEvent = { type: "context", messages: transformed.messages };
696
+ artifactReferences = transformed.artifacts.map((artifact, artifactIndex) => {
697
+ const messageIndex = transformed.artifactMessageIndices[artifactIndex];
698
+ const classification = messageIndex === undefined
699
+ ? undefined
700
+ : preparedPrivacy.messageClassifications[messageIndex];
701
+ return classification ? { ...artifact, classification } : artifact;
702
+ });
703
+ transformed.artifactMessageIndices.forEach((messageIndex, artifactIndex) => {
704
+ const artifact = artifactReferences[artifactIndex];
705
+ if (artifact) artifactReferenceByMessageIndex.set(messageIndex, artifact);
706
+ });
707
+ artifactized = transformed.offloadedCount > 0;
708
+ this.lastArtifacts = this.artifactManager.diagnostics(
709
+ new Set(ctx.sessionManager.getBranch().map((entry) => entry.id)),
710
+ );
711
+ this.logger.debug("artifact_context.processed", {
712
+ offloaded: transformed.offloadedCount,
713
+ offloadedBytes: transformed.offloadedBytes,
714
+ estimatedTokensSaved: transformed.estimatedTokensSaved,
715
+ failed: transformed.failedCount,
716
+ });
717
+ }
718
+ const usage = ctx.getContextUsage();
719
+ const model = snapshotModel(ctx);
720
+ const activeModel = model ? this.resolveActiveModel(model) : undefined;
721
+ const budget = activeModel?.budget;
722
+ const effectiveContextConfig = activeModel?.awareness.contextConfig ?? this.config.context;
723
+ const observedAt = this.now();
724
+ const manifestId = this.idGenerator();
725
+ const baselineClassifications = this.config.privacy.enabled
726
+ ? [
727
+ ...(preparedPrivacy.systemPrompt
728
+ ? [preparedPrivacy.systemClassification ?? this.config.privacy.defaultClassification]
729
+ : []),
730
+ ...preparedPrivacy.tools.map((tool) => tool.classification ?? this.config.privacy.defaultClassification),
731
+ ...preparedPrivacy.messageClassifications,
732
+ ]
733
+ : [];
734
+ const baselinePrivacy = this.privacyManifest(baselineClassifications);
735
+ const baseline = buildPiObserverManifest({
736
+ pi,
737
+ event: effectiveEvent,
738
+ ctx,
739
+ contextConfig: effectiveContextConfig,
740
+ manifestId,
741
+ createdAt: observedAt,
742
+ policyVersion: POLICY_VERSION,
743
+ plannerVersion: OBSERVER_PLANNER_VERSION,
744
+ ...(activeModel ? {
745
+ profile: activeModel.awareness.profile,
746
+ budget: activeModel.budget,
747
+ modelAwareness: activeModel.manifest,
748
+ } : {}),
749
+ systemPrompt: preparedPrivacy.systemPrompt,
750
+ ...(preparedPrivacy.systemClassification
751
+ ? { systemClassification: preparedPrivacy.systemClassification }
752
+ : {}),
753
+ ...(preparedPrivacy.systemPrivacyReason
754
+ ? { systemPrivacyReason: preparedPrivacy.systemPrivacyReason }
755
+ : {}),
756
+ tools: preparedPrivacy.tools,
757
+ messageClassifications: preparedPrivacy.messageClassifications,
758
+ messagePrivacyReasons: preparedPrivacy.messagePrivacyReasons,
759
+ ...(baselinePrivacy ? { privacy: baselinePrivacy } : {}),
760
+ ...(this.lastProject.revision ? { projectRevision: this.lastProject.revision } : {}),
761
+ artifacts: artifactReferences,
762
+ });
763
+
764
+ let manifest = baseline;
765
+ let result: { messages?: ContextEvent["messages"] } | undefined = privacyFallback;
766
+ if (this.config.context.mode === "managed" && budget) {
767
+ const planningStartedAt = this.now();
768
+ const requestText = currentRequestText(effectiveEvent.messages);
769
+ const activeEntryIds = new Set(ctx.sessionManager.getBranch().map((entry) => entry.id));
770
+ const memorySelection: MemorySelection = this.memoryManager?.select(requestText, activeEntryIds) ?? {
771
+ pins: [],
772
+ memories: [],
773
+ excludedPins: 0,
774
+ excludedMemories: 0,
775
+ pinTokens: 0,
776
+ memoryTokens: 0,
777
+ };
778
+ if (this.memoryManager) this.lastMemory = this.memoryManager.diagnostics();
779
+ const retrieval = this.retrieveHistory(
780
+ effectiveEvent,
781
+ ctx,
782
+ effectiveContextConfig.maxRetrievedHistoryTokens,
783
+ );
784
+ const project = this.retrieveProjectKnowledge(
785
+ requestText,
786
+ effectiveContextConfig.maxProjectTokens,
787
+ );
788
+ this.lastRetrieval = {
789
+ ...retrieval,
790
+ plannerExcludedCount: retrieval.selected.length,
791
+ selectedTokens: 0,
792
+ selected: [],
793
+ };
794
+ this.lastProject = {
795
+ ...project,
796
+ plannerExcludedCount: project.selected.length,
797
+ selectedTokens: 0,
798
+ selected: [],
799
+ };
800
+ const rawSupplements: Array<SupplementalContextMessage<ContextEvent["messages"][number]>> = [
801
+ ...memorySelection.pins.map((evidence) => ({
802
+ id: `pin:${evidence.item.id}`,
803
+ message: evidence.message,
804
+ kind: "pin" as const,
805
+ sourceIds: [evidence.item.id],
806
+ score: 950,
807
+ reason: evidence.reason,
808
+ ...(evidence.item.classification ? { classification: evidence.item.classification } : {}),
809
+ })),
810
+ ...memorySelection.memories.map((evidence) => ({
811
+ id: `memory:${evidence.item.id}`,
812
+ message: evidence.message,
813
+ kind: "memory" as const,
814
+ sourceIds: [evidence.item.id],
815
+ score: 90 + Math.min(0.999999, Math.max(0, evidence.score) / 1_000),
816
+ reason: evidence.reason,
817
+ ...(evidence.item.classification ? { classification: evidence.item.classification } : {}),
818
+ })),
819
+ ...retrieval.selected.map((evidence) => ({
820
+ id: `retrieval:${evidence.entryId}`,
821
+ message: evidence.message,
822
+ kind: "retrieval" as const,
823
+ sourceIds: [evidence.entryId],
824
+ score: 85 + Math.min(0.999999, Math.max(0, evidence.score) / 1_000),
825
+ reason: `Historical evidence: ${evidence.reason}`,
826
+ })),
827
+ ...project.selected.map((evidence) => ({
828
+ id: `project:${evidence.snippetId}`,
829
+ message: evidence.message,
830
+ kind: "project" as const,
831
+ sourceIds: [evidence.sourceId],
832
+ score: 80 + Math.min(0.999999, Math.max(0, evidence.score) / 1_000),
833
+ reason: `Project source: ${evidence.reason}`,
834
+ projectSnippet: evidence.manifestRef,
835
+ })),
836
+ ];
837
+ const privacyExcludedSources: ExcludedContextSource[] = [];
838
+ const supplementalMessages = rawSupplements.flatMap((supplement) => {
839
+ if (!this.config.privacy.enabled || !this.privacyEngine || !model) return [supplement];
840
+ const sanitized = this.privacyEngine.sanitizeMessage(
841
+ supplement.message,
842
+ model.provider,
843
+ supplement.classification,
844
+ );
845
+ const reason = privacyReason(sanitized.blockedBlocks, sanitized.secretRedactions);
846
+ this.lastPrivacy = {
847
+ ...this.lastPrivacy,
848
+ blockedBlocks: this.lastPrivacy.blockedBlocks + sanitized.blockedBlocks,
849
+ secretRedactions: this.lastPrivacy.secretRedactions + sanitized.secretRedactions,
850
+ };
851
+ if (sanitized.blockedBlocks > 0) {
852
+ privacyExcludedSources.push({
853
+ sourceId: supplement.sourceIds[0],
854
+ tokens: estimateMessagesTokens([supplement.message]),
855
+ kind: supplement.kind,
856
+ classification: sanitized.classification,
857
+ score: supplement.score,
858
+ reason: `${reason ?? "Classified source excluded"}; source omitted before provider serialization`,
859
+ });
860
+ return [];
861
+ }
862
+ return [{
863
+ ...supplement,
864
+ message: sanitized.value,
865
+ classification: sanitized.classification,
866
+ ...(reason ? { privacyReason: reason } : {}),
867
+ }];
868
+ });
869
+ this.lastPrivacy = {
870
+ ...this.lastPrivacy,
871
+ excludedSources: this.lastPrivacy.excludedSources + privacyExcludedSources.length,
872
+ };
873
+ const plan = planManagedContext({
874
+ messages: effectiveEvent.messages,
875
+ fixedTokens: baseline.composition.systemTokens + baseline.composition.toolTokens,
876
+ budget,
877
+ config: effectiveContextConfig,
878
+ pinnedMessageIndices: findPiPinnedMessageIndices(effectiveEvent, ctx),
879
+ supplementalMessages,
880
+ ...(this.config.privacy.enabled
881
+ ? {
882
+ messageClassifications: preparedPrivacy.messageClassifications,
883
+ messagePrivacyReasons: preparedPrivacy.messagePrivacyReasons,
884
+ }
885
+ : {}),
886
+ });
887
+ const selectedPinIds = new Set(
888
+ plan.selected.flatMap((metadata) => metadata.kind === "pin" && metadata.sourceId ? [metadata.sourceId] : []),
889
+ );
890
+ const selectedMemoryIds = new Set(
891
+ plan.selected.flatMap((metadata) => metadata.kind === "memory" && metadata.sourceId ? [metadata.sourceId] : []),
892
+ );
893
+ this.memoryManager?.applyPlannerSelection(selectedPinIds, selectedMemoryIds);
894
+ if (this.memoryManager) this.lastMemory = this.memoryManager.diagnostics();
895
+ const selectedPinReferences = memorySelection.pins
896
+ .filter((evidence) => selectedPinIds.has(evidence.item.id))
897
+ .map((evidence) => evidence.manifestRef);
898
+ const selectedMemoryReferences = memorySelection.memories
899
+ .filter((evidence) => selectedMemoryIds.has(evidence.item.id))
900
+ .map((evidence) => evidence.manifestRef);
901
+ const selectedRetrievalIds = new Set(
902
+ plan.selected.flatMap((metadata) => metadata.retrievedEventIds ?? []),
903
+ );
904
+ const plannerSelected = retrieval.selected.filter((evidence) => selectedRetrievalIds.has(evidence.entryId));
905
+ this.lastRetrieval = {
906
+ ...retrieval,
907
+ plannerExcludedCount: retrieval.selected.length - plannerSelected.length,
908
+ selectedTokens: plannerSelected.reduce((total, evidence) => total + evidence.estimatedTokens, 0),
909
+ selected: plannerSelected,
910
+ };
911
+ const selectedProjectIds = new Set(
912
+ plan.selected.flatMap((metadata) => metadata.projectSnippet?.snippetId ? [metadata.projectSnippet.snippetId] : []),
913
+ );
914
+ const plannerSelectedProject = project.selected.filter((evidence) => selectedProjectIds.has(evidence.snippetId));
915
+ this.lastProject = {
916
+ ...project,
917
+ plannerExcludedCount: project.selected.length - plannerSelectedProject.length,
918
+ selectedTokens: plannerSelectedProject.reduce((total, evidence) => total + evidence.estimatedTokens, 0),
919
+ selected: plannerSelectedProject,
920
+ };
921
+ plan.planning.durationMs = Math.max(0, this.now() - planningStartedAt);
922
+ const plannedEvent: ContextEvent = { type: "context", messages: plan.messages };
923
+ const selectedArtifactReferences = plan.mode === "managed"
924
+ ? plan.selected.flatMap((metadata) => {
925
+ const artifact = artifactReferenceByMessageIndex.get(metadata.originalIndex);
926
+ return artifact ? [artifact] : [];
927
+ })
928
+ : artifactReferences;
929
+ const planMetadataByIndex = new Map(
930
+ [...plan.selected, ...plan.excluded].map((metadata) => [metadata.originalIndex, metadata] as const),
931
+ );
932
+ const planMessageClassifications = plan.originalMessages.map(
933
+ (_message, index) => planMetadataByIndex.get(index)?.classification
934
+ ?? this.config.privacy.defaultClassification,
935
+ );
936
+ const planMessagePrivacyReasons = plan.originalMessages.map(
937
+ (_message, index) => planMetadataByIndex.get(index)?.privacyReason,
938
+ );
939
+ const managedPrivacy = this.privacyManifest(this.config.privacy.enabled
940
+ ? [
941
+ ...(preparedPrivacy.systemPrompt
942
+ ? [preparedPrivacy.systemClassification ?? this.config.privacy.defaultClassification]
943
+ : []),
944
+ ...preparedPrivacy.tools.map((tool) => tool.classification ?? this.config.privacy.defaultClassification),
945
+ ...plan.selected.map((metadata) => metadata.classification ?? this.config.privacy.defaultClassification),
946
+ ]
947
+ : []);
948
+ manifest = buildPiObserverManifest({
949
+ pi,
950
+ event: plannedEvent,
951
+ ctx,
952
+ contextConfig: effectiveContextConfig,
953
+ manifestId,
954
+ createdAt: observedAt,
955
+ policyVersion: POLICY_VERSION,
956
+ plannerVersion: PLANNER_VERSION,
957
+ ...(activeModel ? {
958
+ profile: activeModel.awareness.profile,
959
+ budget: activeModel.budget,
960
+ modelAwareness: activeModel.manifest,
961
+ } : {}),
962
+ plan,
963
+ systemPrompt: preparedPrivacy.systemPrompt,
964
+ ...(preparedPrivacy.systemClassification
965
+ ? { systemClassification: preparedPrivacy.systemClassification }
966
+ : {}),
967
+ ...(preparedPrivacy.systemPrivacyReason
968
+ ? { systemPrivacyReason: preparedPrivacy.systemPrivacyReason }
969
+ : {}),
970
+ tools: preparedPrivacy.tools,
971
+ messageClassifications: planMessageClassifications,
972
+ messagePrivacyReasons: planMessagePrivacyReasons,
973
+ privacyExcludedSources,
974
+ ...(managedPrivacy ? { privacy: managedPrivacy } : {}),
975
+ ...(this.lastProject.revision ? { projectRevision: this.lastProject.revision } : {}),
976
+ pins: selectedPinReferences,
977
+ memories: selectedMemoryReferences,
978
+ artifacts: selectedArtifactReferences,
979
+ artifactSources: artifactReferences,
980
+ });
981
+ if (plan.mode === "managed") result = { messages: plan.messages };
982
+ else if (artifactized) result = { messages: effectiveEvent.messages };
983
+ } else if (this.config.context.mode === "managed" && artifactized) {
984
+ result = { messages: effectiveEvent.messages };
985
+ }
986
+
987
+ manifest = {
988
+ ...manifest,
989
+ nativeContinuation: this.nativeContinuation.initialManifest(
990
+ manifest.provider,
991
+ manifest.model,
992
+ manifest.planning?.mode === "managed",
993
+ ctx.model?.api,
994
+ ),
995
+ };
996
+
997
+ this.observation = {
998
+ mode: manifest.planning?.mode ?? "observer",
999
+ messageCount: manifest.composition.messageCount,
1000
+ estimatedMessageTokens: manifest.composition.messageTokens,
1001
+ originalMessageCount: manifest.planning?.originalMessageCount ?? event.messages.length,
1002
+ originalEstimatedMessageTokens: manifest.planning?.originalMessageTokens
1003
+ ?? estimateMessagesTokens(event.messages),
1004
+ ...(usage?.tokens !== null && usage?.tokens !== undefined ? { reportedTokens: usage.tokens } : {}),
1005
+ ...(manifest.planning?.durationMs !== undefined
1006
+ ? { planningDurationMs: manifest.planning.durationMs }
1007
+ : {}),
1008
+ ...(manifest.planning?.fallbackReason
1009
+ ? { fallbackReason: manifest.planning.fallbackReason }
1010
+ : {}),
1011
+ observedAt,
1012
+ ...(budget ? { budget } : {}),
1013
+ };
1014
+ this.lastManifest = manifest;
1015
+ this.pendingManifestId = manifest.id;
1016
+ if (this.config.diagnostics.storeContextManifest && this.session?.sessionFile) {
1017
+ this.database?.manifests.save(manifest);
1018
+ }
1019
+
1020
+ this.logger.trace("context.planned", {
1021
+ sessionId: this.session?.sessionId,
1022
+ manifestId: manifest.id,
1023
+ mode: manifest.planning?.mode ?? "observer",
1024
+ originalMessageCount: this.observation.originalMessageCount,
1025
+ selectedMessageCount: manifest.composition.messageCount,
1026
+ estimatedInputTokens: manifest.estimatedInputTokens,
1027
+ planningDurationMs: manifest.planning?.durationMs,
1028
+ promptHash: manifest.promptHash,
1029
+ });
1030
+ return result;
1031
+ } catch (error) {
1032
+ // Planner and observer failures must never replace or block Pi's context.
1033
+ const message = error instanceof Error ? error.message : String(error);
1034
+ this.logger.warn("context.planner_failed", {
1035
+ error: this.config.privacy.enabled
1036
+ ? error instanceof Error ? error.name : "UnknownError"
1037
+ : message,
1038
+ });
1039
+ if (privacyFallback) return privacyFallback;
1040
+ return this.config.privacy.enabled
1041
+ ? { messages: event.messages.map(failClosedMessage) }
1042
+ : undefined;
1043
+ }
1044
+ }
1045
+
1046
+ enforceProviderPayload(payload: unknown, ctx: ExtensionContext): unknown {
1047
+ if (!this.config.privacy.enabled || !this.privacyEngine) return payload;
1048
+ const provider = ctx.model?.provider ?? this.lastPrivacy.provider ?? "unknown";
1049
+ try {
1050
+ const sanitized = this.privacyEngine.sanitizeProviderPayload(payload, provider);
1051
+ const redactions = sanitized.blockedBlocks + sanitized.secretRedactions;
1052
+ this.lastPrivacy = {
1053
+ ...this.lastPrivacy,
1054
+ enabled: true,
1055
+ provider,
1056
+ destination: this.privacyEngine.policy(provider).destination,
1057
+ allowedClassifications: [...this.privacyEngine.policy(provider).allowedClassifications],
1058
+ blockedBlocks: this.lastPrivacy.blockedBlocks + sanitized.blockedBlocks,
1059
+ secretRedactions: this.lastPrivacy.secretRedactions + sanitized.secretRedactions,
1060
+ providerChecks: this.lastPrivacy.providerChecks + 1,
1061
+ providerPayloadRedactions: this.lastPrivacy.providerPayloadRedactions + redactions,
1062
+ enforcement: "context-and-provider",
1063
+ };
1064
+ this.updateManifestPrivacy();
1065
+ this.logger.debug("privacy.provider_enforced", {
1066
+ provider,
1067
+ destination: this.lastPrivacy.destination,
1068
+ blockedBlocks: sanitized.blockedBlocks,
1069
+ secretRedactions: sanitized.secretRedactions,
1070
+ });
1071
+ return sanitized.value;
1072
+ } catch (error) {
1073
+ const errorName = error instanceof Error ? error.name : "UnknownError";
1074
+ this.lastPrivacy = {
1075
+ ...this.lastPrivacy,
1076
+ enabled: true,
1077
+ provider,
1078
+ providerChecks: this.lastPrivacy.providerChecks + 1,
1079
+ providerPayloadRedactions: this.lastPrivacy.providerPayloadRedactions + 1,
1080
+ enforcement: "context-and-provider",
1081
+ warnings: [...this.lastPrivacy.warnings, "Provider payload replaced after privacy enforcement failure"],
1082
+ };
1083
+ this.updateManifestPrivacy();
1084
+ this.logger.error("privacy.provider_fail_closed", { provider, errorName });
1085
+ return {};
1086
+ }
1087
+ }
1088
+
1089
+ private updateManifestPrivacy(): void {
1090
+ if (!this.lastManifest?.privacy) return;
1091
+ const updatedPrivacy = this.privacyManifest(
1092
+ Object.entries(this.lastPrivacy.selectedClassifications)
1093
+ .flatMap(([classification, count]) => Array.from(
1094
+ { length: count },
1095
+ () => classification as PrivacyClassification,
1096
+ )),
1097
+ );
1098
+ if (!updatedPrivacy) return;
1099
+ this.lastManifest = { ...this.lastManifest, privacy: updatedPrivacy };
1100
+ if (this.config.diagnostics.storeContextManifest && this.session?.sessionFile) {
1101
+ this.database?.manifests.save(this.lastManifest);
1102
+ }
1103
+ }
1104
+
1105
+ nativeContinuationProviderIds(): string[] {
1106
+ return this.nativeContinuation.providerIds();
1107
+ }
1108
+
1109
+ nativeContinuationProviderRegistered(provider: string): void {
1110
+ this.nativeContinuation.registerProvider(provider);
1111
+ this.logger.info("native_continuation.provider_registered", { provider });
1112
+ }
1113
+
1114
+ nativeContinuationProviderRegistrationFailed(provider: string, error: unknown): void {
1115
+ this.nativeContinuation.providerRegistrationFailed(provider);
1116
+ this.logger.warn("native_continuation.provider_registration_failed", {
1117
+ provider,
1118
+ errorType: error instanceof Error ? error.name : "UnknownError",
1119
+ });
1120
+ }
1121
+
1122
+ prepareNativeContinuation(
1123
+ payload: unknown,
1124
+ model: Model<Api>,
1125
+ requestSessionId: string | undefined,
1126
+ options: {
1127
+ forceManagedReplay: boolean;
1128
+ retryOfContinuation: boolean;
1129
+ fallbackReason?: string;
1130
+ },
1131
+ ): PreparedNativeContinuation {
1132
+ try {
1133
+ const manifest = this.lastManifest?.provider === model.provider
1134
+ && this.lastManifest.model === model.id
1135
+ ? this.lastManifest
1136
+ : undefined;
1137
+ const prepared = this.nativeContinuation.prepare({
1138
+ payload,
1139
+ provider: model.provider,
1140
+ model: model.id,
1141
+ api: model.api,
1142
+ ...(requestSessionId ? { requestSessionId } : {}),
1143
+ canonicalSessionId: this.session?.sessionId ?? "",
1144
+ ...(manifest ? { manifestId: manifest.id } : {}),
1145
+ ...(manifest?.branchLeafId ? { branchLeafId: manifest.branchLeafId } : {}),
1146
+ managed: this.phase === "managed" && manifest?.planning?.mode === "managed",
1147
+ forceManagedReplay: options.forceManagedReplay,
1148
+ retryOfContinuation: options.retryOfContinuation,
1149
+ ...(options.fallbackReason ? { fallbackReason: options.fallbackReason } : {}),
1150
+ });
1151
+ if (prepared.tracked && prepared.decision) {
1152
+ this.updateManifestNativeContinuation(manifest?.id, prepared.decision);
1153
+ this.logger.debug("native_continuation.request_prepared", {
1154
+ provider: model.provider,
1155
+ model: model.id,
1156
+ mode: prepared.decision.mode,
1157
+ fullInputItems: prepared.decision.fullInputItems,
1158
+ sentInputItems: prepared.decision.sentInputItems,
1159
+ omittedInputItems: prepared.decision.omittedInputItems,
1160
+ fallbackReason: prepared.decision.fallbackReason,
1161
+ invalidationReason: prepared.decision.invalidationReason,
1162
+ retry: prepared.decision.retry,
1163
+ });
1164
+ }
1165
+ return prepared;
1166
+ } catch (error) {
1167
+ this.nativeContinuation.invalidate("continuation-preparation-failed");
1168
+ this.logger.warn("native_continuation.preparation_failed", {
1169
+ provider: model.provider,
1170
+ model: model.id,
1171
+ errorType: error instanceof Error ? error.name : "UnknownError",
1172
+ });
1173
+ return { payload, tracked: false };
1174
+ }
1175
+ }
1176
+
1177
+ beginNativeContinuationManagedReplayRetry(attempt: NativeContinuationAttempt): void {
1178
+ this.nativeContinuation.beginManagedReplayRetry(attempt);
1179
+ }
1180
+
1181
+ completeNativeContinuation(
1182
+ attempt: NativeContinuationAttempt,
1183
+ message: AssistantMessage,
1184
+ responseItemHashes: readonly string[],
1185
+ ): void {
1186
+ const decision = message.stopReason === "deferred"
1187
+ ? this.nativeContinuation.fail(attempt, "deferred-response-not-continuable")
1188
+ : this.nativeContinuation.complete(attempt, message.responseId, responseItemHashes);
1189
+ this.updateManifestNativeContinuation(attempt.manifestId, decision);
1190
+ this.logger.debug("native_continuation.request_completed", {
1191
+ provider: attempt.provider,
1192
+ model: attempt.model,
1193
+ mode: decision.mode,
1194
+ stateReused: decision.stateReused,
1195
+ retry: decision.retry,
1196
+ responseStateAvailable: this.nativeContinuation.diagnostics().stateAvailable,
1197
+ });
1198
+ }
1199
+
1200
+ failNativeContinuation(attempt: NativeContinuationAttempt, reason: string): void {
1201
+ const decision = this.nativeContinuation.fail(attempt, reason);
1202
+ this.updateManifestNativeContinuation(attempt.manifestId, decision);
1203
+ this.logger.debug("native_continuation.request_failed", {
1204
+ provider: attempt.provider,
1205
+ model: attempt.model,
1206
+ mode: decision.mode,
1207
+ reason,
1208
+ retry: decision.retry,
1209
+ });
1210
+ }
1211
+
1212
+ shouldRetryNativeContinuationManagedReplay(): boolean {
1213
+ return this.nativeContinuation.shouldRetryManagedReplay();
1214
+ }
1215
+
1216
+ invalidateNativeContinuation(reason: string): void {
1217
+ this.nativeContinuation.invalidate(reason);
1218
+ }
1219
+
1220
+ private updateManifestNativeContinuation(
1221
+ manifestId: string | undefined,
1222
+ continuation: NativeContinuationManifest,
1223
+ ): void {
1224
+ if (!manifestId || this.lastManifest?.id !== manifestId) return;
1225
+ this.lastManifest = {
1226
+ ...this.lastManifest,
1227
+ nativeContinuation: { ...continuation },
1228
+ };
1229
+ if (!this.config.diagnostics.storeContextManifest || !this.session?.sessionFile) return;
1230
+ try {
1231
+ this.database?.manifests.save(this.lastManifest);
1232
+ } catch (error) {
1233
+ this.logger.warn("native_continuation.manifest_persistence_failed", {
1234
+ manifestId,
1235
+ errorType: error instanceof Error ? error.name : "UnknownError",
1236
+ });
1237
+ }
1238
+ }
1239
+
1240
+ recordAssistantUsage(message: unknown): void {
1241
+ if (!this.pendingManifestId || !message || typeof message !== "object") return;
1242
+ const record = message as Record<string, unknown>;
1243
+ if (record.role !== "assistant") return;
1244
+
1245
+ const manifestId = this.pendingManifestId;
1246
+ this.pendingManifestId = undefined;
1247
+ if (record.stopReason === "error" || record.stopReason === "aborted") return;
1248
+ const usage = record.usage;
1249
+ if (!usage || typeof usage !== "object") return;
1250
+ const usageRecord = usage as Record<string, unknown>;
1251
+ const providerUsage = providerUsageManifest({
1252
+ inputTokens: numericUsage(usageRecord.input),
1253
+ cacheReadTokens: numericUsage(usageRecord.cacheRead),
1254
+ cacheWriteTokens: numericUsage(usageRecord.cacheWrite),
1255
+ });
1256
+ if (providerUsage.totalInputTokens <= 0) return;
1257
+
1258
+ const manifest = this.lastManifest?.id === manifestId ? this.lastManifest : undefined;
1259
+ if (manifest) {
1260
+ this.lastManifest = {
1261
+ ...manifest,
1262
+ actualInputTokens: providerUsage.totalInputTokens,
1263
+ providerUsage,
1264
+ };
1265
+ }
1266
+ const createdAt = this.now();
1267
+ const persisted = Boolean(
1268
+ this.config.diagnostics.storeContextManifest
1269
+ && this.session?.sessionFile
1270
+ && this.database,
1271
+ );
1272
+ try {
1273
+ if (persisted) {
1274
+ const updated = this.database?.manifests.recordProviderUsage(
1275
+ manifestId,
1276
+ providerUsage,
1277
+ createdAt,
1278
+ );
1279
+ if (updated && this.lastManifest?.id === manifestId) this.lastManifest = updated;
1280
+ } else if (manifest?.estimatedInputTokens) {
1281
+ this.rememberVolatileCalibration(manifest, providerUsage, createdAt);
1282
+ }
1283
+ } catch (error) {
1284
+ if (manifest?.estimatedInputTokens) {
1285
+ this.rememberVolatileCalibration(manifest, providerUsage, createdAt);
1286
+ }
1287
+ this.logger.warn("context.usage_persistence_failed", {
1288
+ manifestId,
1289
+ error: error instanceof Error ? error.message : String(error),
1290
+ });
1291
+ }
1292
+ this.logger.debug("context.actual_usage_recorded", {
1293
+ manifestId,
1294
+ inputTokens: providerUsage.inputTokens,
1295
+ cacheReadTokens: providerUsage.cacheReadTokens,
1296
+ cacheWriteTokens: providerUsage.cacheWriteTokens,
1297
+ actualInputTokens: providerUsage.totalInputTokens,
1298
+ });
1299
+ }
1300
+
1301
+ beforeCompact(
1302
+ event: SessionBeforeCompactEvent,
1303
+ ctx: ExtensionContext,
1304
+ ): ReturnType<CompactionCoordinator["beforeCompact"]> {
1305
+ return this.compaction?.beforeCompact(event, ctx) ?? Promise.resolve(undefined);
1306
+ }
1307
+
1308
+ afterCompaction(event: SessionCompactEvent, ctx: ExtensionContext): void {
1309
+ this.nativeContinuation.invalidate("session-compacted");
1310
+ this.compaction?.afterCompaction(event, ctx);
1311
+ }
1312
+
1313
+ compactionFailed(event: SessionCompactFailedLike): void {
1314
+ this.compaction?.compactionFailed(event);
1315
+ }
1316
+
1317
+ afterAgentSettled(ctx: ExtensionContext): void {
1318
+ if (this.compaction) this.compaction.afterAgentSettled(ctx);
1319
+ else this.syncSessionIndex(ctx);
1320
+ this.refreshProjectIndex();
1321
+ }
1322
+
1323
+ projectMayHaveChanged(toolName?: string): void {
1324
+ if (toolName && READ_ONLY_PROJECT_TOOLS.has(toolName.toLowerCase())) return;
1325
+ if (this.projectKnowledge) this.projectRefreshPending = true;
1326
+ }
1327
+
1328
+ sessionTreeChanged(ctx: ExtensionContext): void {
1329
+ this.nativeContinuation.invalidate("session-branch-changed");
1330
+ this.syncSessionIndex(ctx);
1331
+ }
1332
+
1333
+ latestManifest(): ContextManifest | undefined {
1334
+ return this.lastManifest;
1335
+ }
1336
+
1337
+ summaryGraph(ctx: ExtensionContext): SummaryGraphDiagnostics {
1338
+ return this.compaction?.summaryGraph(ctx) ?? defaultSummaryGraphDiagnostics();
1339
+ }
1340
+
1341
+ retrievalDiagnostics(): RetrievalDiagnostics {
1342
+ return this.lastRetrieval;
1343
+ }
1344
+
1345
+ listPins(activeOnly = false): PinItem[] {
1346
+ return this.memoryManager?.listPins(activeOnly) ?? [];
1347
+ }
1348
+
1349
+ listMemories(activeOnly = false): MemoryItem[] {
1350
+ return this.memoryManager?.listMemories(activeOnly) ?? [];
1351
+ }
1352
+
1353
+ createPin(
1354
+ input: CreatePinInput,
1355
+ ctx: ExtensionContext,
1356
+ appendEntry: CustomEntryAppender,
1357
+ ): { pin: PinItem; duplicate: boolean } {
1358
+ const manager = this.requireMemoryManager();
1359
+ const branchLeafId = ctx.sessionManager.getLeafId();
1360
+ const proposal = manager.proposePin({
1361
+ ...input,
1362
+ ...(input.scope === "branch" && branchLeafId ? { branchLeafId } : {}),
1363
+ activeEntryIds: new Set(ctx.sessionManager.getBranch().map((entry) => entry.id)),
1364
+ });
1365
+ if (proposal.duplicateId) {
1366
+ const duplicate = manager.getPin(proposal.duplicateId);
1367
+ if (!duplicate) throw new Error(`Duplicate pin ${proposal.duplicateId} disappeared`);
1368
+ return { pin: duplicate, duplicate: true };
1369
+ }
1370
+ if (!proposal.mutation || proposal.mutation.operation === "status") {
1371
+ throw new Error("Pin mutation was not created");
1372
+ }
1373
+ appendEntry(PIN_CUSTOM_ENTRY_TYPE, proposal.mutation);
1374
+ this.syncSessionIndex(ctx);
1375
+ this.reconcileMemory(ctx);
1376
+ const id = proposal.mutation.item.id;
1377
+ const pin = manager.getPin(id);
1378
+ if (!pin) throw new Error(`Pin ${id} was not materialized`);
1379
+ this.logger.info("pin.created", { pinId: pin.id, scope: pin.scope });
1380
+ return { pin, duplicate: false };
1381
+ }
1382
+
1383
+ unpin(pinId: string, reason: string | undefined, ctx: ExtensionContext, appendEntry: CustomEntryAppender): PinItem {
1384
+ const manager = this.requireMemoryManager();
1385
+ const mutation = manager.proposeUnpin(pinId, reason);
1386
+ appendEntry(PIN_CUSTOM_ENTRY_TYPE, mutation);
1387
+ this.syncSessionIndex(ctx);
1388
+ this.reconcileMemory(ctx);
1389
+ const pin = manager.getPin(pinId);
1390
+ if (!pin) throw new Error(`Pin ${pinId} was not materialized`);
1391
+ this.logger.info("pin.deleted", { pinId });
1392
+ return pin;
1393
+ }
1394
+
1395
+ createMemory(
1396
+ input: CreateMemoryInput,
1397
+ ctx: ExtensionContext,
1398
+ appendEntry: CustomEntryAppender,
1399
+ ): { memory: MemoryItem; duplicate: boolean } {
1400
+ const manager = this.requireMemoryManager();
1401
+ const proposal = manager.proposeMemory({
1402
+ ...input,
1403
+ activeEntryIds: new Set(ctx.sessionManager.getBranch().map((entry) => entry.id)),
1404
+ });
1405
+ if (proposal.duplicateId) {
1406
+ const duplicate = manager.getMemory(proposal.duplicateId);
1407
+ if (!duplicate) throw new Error(`Duplicate memory ${proposal.duplicateId} disappeared`);
1408
+ return { memory: duplicate, duplicate: true };
1409
+ }
1410
+ if (!proposal.mutation || proposal.mutation.operation !== "add") {
1411
+ throw new Error("Memory mutation was not created");
1412
+ }
1413
+ appendEntry(MEMORY_CUSTOM_ENTRY_TYPE, proposal.mutation);
1414
+ this.syncSessionIndex(ctx);
1415
+ this.reconcileMemory(ctx);
1416
+ const memory = manager.getMemory(proposal.mutation.item.id);
1417
+ if (!memory) throw new Error(`Memory ${proposal.mutation.item.id} was not materialized`);
1418
+ this.logger.info("memory.created", { memoryId: memory.id, scope: memory.scope, hasKey: Boolean(memory.key) });
1419
+ return { memory, duplicate: false };
1420
+ }
1421
+
1422
+ supersedeMemory(
1423
+ previousId: string,
1424
+ claim: string,
1425
+ sourceEntryIds: string[],
1426
+ classification: PrivacyClassification | undefined,
1427
+ ctx: ExtensionContext,
1428
+ appendEntry: CustomEntryAppender,
1429
+ ): MemoryItem {
1430
+ const manager = this.requireMemoryManager();
1431
+ const mutation = manager.proposeMemorySupersession({
1432
+ previousId,
1433
+ claim,
1434
+ sourceEntryIds,
1435
+ ...(classification ? { classification } : {}),
1436
+ activeEntryIds: new Set(ctx.sessionManager.getBranch().map((entry) => entry.id)),
1437
+ });
1438
+ if (mutation.operation !== "supersede") throw new Error("Memory supersession was not created");
1439
+ appendEntry(MEMORY_CUSTOM_ENTRY_TYPE, mutation);
1440
+ this.syncSessionIndex(ctx);
1441
+ this.reconcileMemory(ctx);
1442
+ const memory = manager.getMemory(mutation.item.id);
1443
+ if (!memory) throw new Error(`Memory ${mutation.item.id} was not materialized`);
1444
+ this.logger.info("memory.superseded", { previousId, memoryId: memory.id });
1445
+ return memory;
1446
+ }
1447
+
1448
+ setMemoryStatus(
1449
+ memoryId: string,
1450
+ status: "invalid" | "expired",
1451
+ reason: string | undefined,
1452
+ ctx: ExtensionContext,
1453
+ appendEntry: CustomEntryAppender,
1454
+ ): MemoryItem {
1455
+ const manager = this.requireMemoryManager();
1456
+ const mutation = manager.proposeMemoryStatus(memoryId, status, reason);
1457
+ appendEntry(MEMORY_CUSTOM_ENTRY_TYPE, mutation);
1458
+ this.syncSessionIndex(ctx);
1459
+ this.reconcileMemory(ctx);
1460
+ const memory = manager.getMemory(memoryId);
1461
+ if (!memory) throw new Error(`Memory ${memoryId} was not materialized`);
1462
+ this.logger.info("memory.status_changed", { memoryId, status });
1463
+ return memory;
1464
+ }
1465
+
1466
+ searchArtifact(
1467
+ artifactId: string,
1468
+ query: string,
1469
+ maxMatches: number,
1470
+ ctx: ExtensionContext,
1471
+ ): ArtifactSearchResult {
1472
+ if (!this.artifactManager) throw new Error("Artifact Store is unavailable for this session");
1473
+ let result = this.artifactManager.search(
1474
+ artifactId,
1475
+ query,
1476
+ maxMatches,
1477
+ new Set(ctx.sessionManager.getBranch().map((entry) => entry.id)),
1478
+ );
1479
+ if (this.config.privacy.enabled && this.privacyEngine) {
1480
+ const sanitized = this.privacyEngine.sanitizeText(
1481
+ result.text,
1482
+ ctx.model?.provider ?? "unknown",
1483
+ result.classification,
1484
+ );
1485
+ result = {
1486
+ ...result,
1487
+ text: sanitized.value,
1488
+ ...(sanitized.blockedBlocks > 0 ? { matches: 0 } : {}),
1489
+ };
1490
+ this.lastPrivacy = {
1491
+ ...this.lastPrivacy,
1492
+ blockedBlocks: this.lastPrivacy.blockedBlocks + sanitized.blockedBlocks,
1493
+ secretRedactions: this.lastPrivacy.secretRedactions + sanitized.secretRedactions,
1494
+ };
1495
+ }
1496
+ this.lastArtifacts = this.artifactManager.diagnostics(
1497
+ new Set(ctx.sessionManager.getBranch().map((entry) => entry.id)),
1498
+ );
1499
+ return result;
1500
+ }
1501
+
1502
+ modelChanged(
1503
+ provider: string,
1504
+ modelId: string,
1505
+ previousProvider?: string,
1506
+ previousModelId?: string,
1507
+ source: "set" | "cycle" | "restore" = "set",
1508
+ ): void {
1509
+ const key = modelProfileKey(provider, modelId);
1510
+ const explicitPrevious = previousProvider && previousModelId
1511
+ ? modelProfileKey(previousProvider, previousModelId)
1512
+ : undefined;
1513
+ const previousKey = explicitPrevious ?? this.lastContextProfileKey;
1514
+ const separator = previousKey?.indexOf("/") ?? -1;
1515
+ const switched = previousKey !== undefined && previousKey !== key;
1516
+ if (switched) this.nativeContinuation.invalidate("provider-or-model-changed");
1517
+ this.pendingModelSwitch = {
1518
+ source,
1519
+ switched,
1520
+ ...(separator > 0 && previousKey ? {
1521
+ previousProvider: previousKey.slice(0, separator),
1522
+ previousModel: previousKey.slice(separator + 1),
1523
+ } : {}),
1524
+ profileReused: this.knownModelProfiles.has(key),
1525
+ cacheDisposition: switched
1526
+ ? "cold-model-switch"
1527
+ : previousKey === key
1528
+ ? "eligible"
1529
+ : "unknown",
1530
+ };
1531
+ if (this.config.privacy.enabled && this.privacyEngine) {
1532
+ const policy = this.privacyEngine.policy(provider);
1533
+ this.lastPrivacy = {
1534
+ ...this.lastPrivacy,
1535
+ enabled: true,
1536
+ provider,
1537
+ destination: policy.destination,
1538
+ allowedClassifications: [...policy.allowedClassifications],
1539
+ enforcement: "context",
1540
+ };
1541
+ }
1542
+ this.logger.info("model.changed", {
1543
+ provider,
1544
+ modelId,
1545
+ previousProvider,
1546
+ previousModelId,
1547
+ source,
1548
+ profileReused: this.pendingModelSwitch.profileReused,
1549
+ });
1550
+ }
1551
+
1552
+ private initializeMemory(ctx: ExtensionContext): void {
1553
+ if (this.config.context.mode !== "managed" || !this.config.memory.enabled) {
1554
+ this.memoryManager = undefined;
1555
+ this.lastMemory = disabledMemoryDiagnostics();
1556
+ return;
1557
+ }
1558
+ if (!this.database || !this.session?.sessionFile) {
1559
+ this.memoryManager = undefined;
1560
+ this.lastMemory = disabledMemoryDiagnostics("Memory and pins require a persisted Pi session");
1561
+ return;
1562
+ }
1563
+ try {
1564
+ this.memoryManager = new MemoryManager(
1565
+ this.database.memory,
1566
+ this.config.memory,
1567
+ this.config.context.maxPinnedTokens,
1568
+ this.config.context.maxMemoryTokens,
1569
+ this.session.sessionId,
1570
+ canonicalProjectPath(this.session.projectPath),
1571
+ ctx.isProjectTrusted(),
1572
+ this.now,
1573
+ this.idGenerator,
1574
+ );
1575
+ if (existsSync(this.session.sessionFile)) this.reconcileMemory(ctx);
1576
+ else this.lastMemory = this.memoryManager.diagnostics();
1577
+ } catch (error) {
1578
+ const message = error instanceof Error ? error.message : String(error);
1579
+ this.memoryManager = undefined;
1580
+ this.lastMemory = {
1581
+ ...disabledMemoryDiagnostics(message),
1582
+ status: "failed",
1583
+ };
1584
+ this.logger.warn("memory_manager.failed", { error: message });
1585
+ }
1586
+ }
1587
+
1588
+ private reconcileMemory(ctx: ExtensionContext, force = false): void {
1589
+ if (!this.memoryManager || !this.session?.sessionFile || !existsSync(this.session.sessionFile)) return;
1590
+ const projection = projectSessionFileMutations(this.session.sessionFile, this.session.sessionId);
1591
+ const signature = [
1592
+ ...projection.memoryMutations.map((mutation) => `m:${mutation.mutationKey}:${mutation.mutationId}`),
1593
+ ...projection.pinMutations.map((mutation) => `p:${mutation.mutationKey}:${mutation.mutationId}`),
1594
+ ...projection.warnings.map((warning) => `w:${warning}`),
1595
+ ].join("\0");
1596
+ if (!force && signature === this.lastMemoryMutationSignature) return;
1597
+ const sourceEntryIds = [
1598
+ ...projection.memoryMutations.map((mutation) => mutation.mutationKey.slice(this.session!.sessionId.length + 1)),
1599
+ ...projection.pinMutations.map((mutation) => mutation.mutationKey.slice(this.session!.sessionId.length + 1)),
1600
+ ];
1601
+ if (sourceEntryIds.some((entryId) => !this.database?.sessionIndex.hasEntry(this.session!.sessionId, entryId))) {
1602
+ if (!this.indexer) throw new Error("Session index is unavailable for memory mutation replay");
1603
+ this.session = snapshotSession(ctx);
1604
+ const rebuilt = this.indexer.sync(this.session, true);
1605
+ this.lastIndexResult = rebuilt;
1606
+ this.lastIndexError = undefined;
1607
+ }
1608
+ const missing = sourceEntryIds.filter((entryId) => !this.database?.sessionIndex.hasEntry(this.session!.sessionId, entryId));
1609
+ if (missing.length > 0) {
1610
+ throw new Error(`Canonical memory mutation entries missing from session index: ${missing.join(", ")}`);
1611
+ }
1612
+ const result = this.memoryManager.reconcile(projection);
1613
+ this.lastMemoryMutationSignature = signature;
1614
+ this.lastMemory = this.memoryManager.diagnostics();
1615
+ this.logger.debug("memory.materialized", {
1616
+ memories: result.memories,
1617
+ pins: result.pins,
1618
+ memoryMutations: result.memoryMutations,
1619
+ pinMutations: result.pinMutations,
1620
+ ignoredMutations: result.ignoredMutations,
1621
+ warnings: result.warnings.length,
1622
+ });
1623
+ }
1624
+
1625
+ private requireMemoryManager(): MemoryManager {
1626
+ if (!this.memoryManager) throw new Error("DS4 memory and pins are unavailable for this session");
1627
+ return this.memoryManager;
1628
+ }
1629
+
1630
+ private initializeArtifacts(ctx: ExtensionContext): void {
1631
+ if (this.config.context.mode !== "managed"
1632
+ || !this.config.artifacts.enabled
1633
+ || !this.config.artifacts.storeLargeOutputs) {
1634
+ this.lastArtifacts = disabledArtifactDiagnostics();
1635
+ return;
1636
+ }
1637
+ if (!this.database || !this.session?.sessionFile) {
1638
+ this.lastArtifacts = {
1639
+ ...disabledArtifactDiagnostics(),
1640
+ warnings: ["Artifact offload requires a persisted Pi session"],
1641
+ };
1642
+ return;
1643
+ }
1644
+ try {
1645
+ const store = new FileArtifactStore(
1646
+ join(this.dependencies.agentDir, "ds4-context", "artifacts"),
1647
+ this.now,
1648
+ );
1649
+ this.artifactManager = new ArtifactManager(
1650
+ store,
1651
+ this.database.artifacts,
1652
+ this.config.artifacts,
1653
+ this.session.sessionId,
1654
+ this.now,
1655
+ );
1656
+ this.lastArtifacts = this.artifactManager.diagnostics(
1657
+ new Set(ctx.sessionManager.getBranch().map((entry) => entry.id)),
1658
+ );
1659
+ } catch (error) {
1660
+ const message = error instanceof Error ? error.message : String(error);
1661
+ this.artifactManager = undefined;
1662
+ this.lastArtifacts = {
1663
+ ...disabledArtifactDiagnostics(),
1664
+ warnings: [`Artifact Store initialization failed: ${message}`],
1665
+ };
1666
+ this.logger.warn("artifact_store.failed", { error: message });
1667
+ }
1668
+ }
1669
+
1670
+ private rebuildArtifacts(ctx: ExtensionContext): void {
1671
+ if (!this.artifactManager) return;
1672
+ const messages: ContextEvent["messages"] = [];
1673
+ const sourceEntryIds: string[] = [];
1674
+ for (const entry of ctx.sessionManager.getEntries()) {
1675
+ for (const message of sessionEntryToContextMessages(entry)) {
1676
+ messages.push(message);
1677
+ sourceEntryIds.push(entry.id);
1678
+ }
1679
+ }
1680
+ const classifications = this.config.privacy.enabled && this.privacyEngine
1681
+ ? messages.map((message) => this.privacyEngine!.sanitizeMessage(
1682
+ message,
1683
+ ctx.model?.provider ?? "unknown",
1684
+ ).classification)
1685
+ : [];
1686
+ const result = this.artifactManager.reconcile(messages, sourceEntryIds, classifications);
1687
+ this.lastArtifacts = this.artifactManager.diagnostics(
1688
+ new Set(ctx.sessionManager.getBranch().map((entry) => entry.id)),
1689
+ );
1690
+ this.logger.info("artifact_store.rebuilt", {
1691
+ references: result.artifactIds.length,
1692
+ offloadedBytes: result.offloadedBytes,
1693
+ failed: result.failedCount,
1694
+ });
1695
+ }
1696
+
1697
+ private initializeProjectKnowledge(ctx: ExtensionContext): void {
1698
+ if (!this.config.project.enabled) {
1699
+ this.lastProject = emptyProjectDiagnostics(
1700
+ "disabled",
1701
+ ctx.isProjectTrusted(),
1702
+ this.config.context.maxProjectTokens,
1703
+ this.config.project.maxResults,
1704
+ );
1705
+ return;
1706
+ }
1707
+ if (!ctx.isProjectTrusted()) {
1708
+ this.lastProject = emptyProjectDiagnostics(
1709
+ "untrusted",
1710
+ false,
1711
+ this.config.context.maxProjectTokens,
1712
+ this.config.project.maxResults,
1713
+ );
1714
+ return;
1715
+ }
1716
+ if (!this.database) return;
1717
+
1718
+ try {
1719
+ this.projectKnowledge = new ProjectKnowledgeManager(
1720
+ ctx.cwd,
1721
+ this.database.projectKnowledge,
1722
+ this.config.project,
1723
+ this.config.context.maxProjectTokens,
1724
+ this.now,
1725
+ );
1726
+ const sync = this.projectKnowledge.sync();
1727
+ this.lastProject = this.projectKnowledge.diagnostics();
1728
+ this.logger.info("project_index.opened", {
1729
+ files: sync.discoveredFiles,
1730
+ indexedFiles: sync.indexedFiles,
1731
+ currentSnippets: sync.currentSnippets,
1732
+ staleSnippets: sync.staleSnippets,
1733
+ gitAvailable: sync.git.available,
1734
+ dirty: sync.git.dirty,
1735
+ durationMs: sync.durationMs,
1736
+ });
1737
+ } catch (error) {
1738
+ const message = error instanceof Error ? error.message : String(error);
1739
+ this.projectKnowledge = undefined;
1740
+ this.lastProject = {
1741
+ ...emptyProjectDiagnostics(
1742
+ "failed",
1743
+ true,
1744
+ this.config.context.maxProjectTokens,
1745
+ this.config.project.maxResults,
1746
+ ),
1747
+ projectPath: ctx.cwd,
1748
+ fallbackReason: message,
1749
+ };
1750
+ this.logger.warn("project_index.failed", { error: message });
1751
+ }
1752
+ }
1753
+
1754
+ private refreshProjectIndexIfPending(): void {
1755
+ if (this.projectRefreshPending) this.refreshProjectIndex();
1756
+ }
1757
+
1758
+ private refreshProjectIndex(force = false): void {
1759
+ if (!this.projectKnowledge) return;
1760
+ this.projectRefreshPending = false;
1761
+ try {
1762
+ const sync = this.projectKnowledge.sync(force);
1763
+ this.lastProject = this.projectKnowledge.diagnostics();
1764
+ this.logger.debug("project_index.synced", {
1765
+ mode: sync.mode,
1766
+ discoveredFiles: sync.discoveredFiles,
1767
+ indexedFiles: sync.indexedFiles,
1768
+ unchangedFiles: sync.unchangedFiles,
1769
+ deletedFiles: sync.deletedFiles,
1770
+ staleSnippets: sync.staleSnippets,
1771
+ durationMs: sync.durationMs,
1772
+ });
1773
+ } catch (error) {
1774
+ const message = error instanceof Error ? error.message : String(error);
1775
+ this.lastProject = this.projectKnowledge.diagnostics("failed", message);
1776
+ this.logger.warn("project_index.sync_failed", { error: message });
1777
+ }
1778
+ }
1779
+
1780
+ private retrieveProjectKnowledge(
1781
+ requestText: string,
1782
+ maxTokens = this.config.context.maxProjectTokens,
1783
+ ): ProjectKnowledgeDiagnostics {
1784
+ if (!this.projectKnowledge) return this.lastProject;
1785
+ if (!requestText) return this.projectKnowledge.diagnostics();
1786
+ try {
1787
+ const diagnostics = this.projectKnowledge.retrieve(requestText, this.now(), maxTokens);
1788
+ this.logger.debug("project_retrieval.completed", {
1789
+ candidates: diagnostics.candidateCount,
1790
+ selected: diagnostics.selected.length,
1791
+ invalidatedSnippets: diagnostics.invalidatedSnippets,
1792
+ reindexedFiles: diagnostics.reindexedFiles,
1793
+ selectedTokens: diagnostics.selectedTokens,
1794
+ durationMs: diagnostics.durationMs,
1795
+ });
1796
+ return diagnostics;
1797
+ } catch (error) {
1798
+ const message = error instanceof Error ? error.message : String(error);
1799
+ this.logger.warn("project_retrieval.failed", { error: message });
1800
+ return this.projectKnowledge.diagnostics("failed", message);
1801
+ }
1802
+ }
1803
+
1804
+ private retrieveHistory(
1805
+ event: ContextEvent,
1806
+ ctx: ExtensionContext,
1807
+ maxTokens = this.config.context.maxRetrievedHistoryTokens,
1808
+ ): RetrievalDiagnostics {
1809
+ const requestText = currentRequestText(event.messages);
1810
+ if (!this.retrievalEngine || !this.session?.sessionFile || !requestText) {
1811
+ return emptyRetrievalDiagnostics(
1812
+ maxTokens,
1813
+ this.config.retrieval.maxResults,
1814
+ );
1815
+ }
1816
+ try {
1817
+ const diagnostics = this.retrievalEngine.retrieve({
1818
+ sessionId: this.session.sessionId,
1819
+ requestText,
1820
+ activeBranchEntryIds: new Set(ctx.sessionManager.getBranch().map((entry) => entry.id)),
1821
+ activeContextEntryIds: new Set(ctx.sessionManager.buildContextEntries().map((entry) => entry.id)),
1822
+ exact: this.config.retrieval.exact,
1823
+ fts: this.config.retrieval.fts,
1824
+ semantic: this.config.retrieval.semantic,
1825
+ maxResults: this.config.retrieval.maxResults,
1826
+ maxTokens,
1827
+ timestamp: this.now(),
1828
+ });
1829
+ this.logger.debug("retrieval.completed", {
1830
+ status: diagnostics.status,
1831
+ candidates: diagnostics.candidateCount,
1832
+ selected: diagnostics.selected.length,
1833
+ alternateBranchCandidates: diagnostics.alternateBranchCandidates,
1834
+ selectedTokens: diagnostics.selectedTokens,
1835
+ durationMs: diagnostics.durationMs,
1836
+ });
1837
+ return diagnostics;
1838
+ } catch (error) {
1839
+ const message = error instanceof Error ? error.message : String(error);
1840
+ this.logger.warn("retrieval.failed", { error: message });
1841
+ return {
1842
+ ...emptyRetrievalDiagnostics(
1843
+ maxTokens,
1844
+ this.config.retrieval.maxResults,
1845
+ ),
1846
+ status: "failed",
1847
+ fallbackReason: message,
1848
+ };
1849
+ }
1850
+ }
1851
+
1852
+ syncSessionIndex(ctx: ExtensionContext): SessionIndexResult | undefined {
1853
+ if (!this.indexer || this.phase === "disabled" || this.phase === "degraded" || this.phase === "closed") {
1854
+ return undefined;
1855
+ }
1856
+
1857
+ this.session = snapshotSession(ctx);
1858
+ try {
1859
+ this.lastIndexResult = this.indexer.sync(this.session);
1860
+ this.lastIndexError = undefined;
1861
+ return this.lastIndexResult;
1862
+ } catch (error) {
1863
+ this.lastIndexError = error instanceof Error ? error.message : String(error);
1864
+ this.logger.warn("session_index.failed", {
1865
+ sessionId: this.session.sessionId,
1866
+ error: this.lastIndexError,
1867
+ });
1868
+ return undefined;
1869
+ }
1870
+ }
1871
+
1872
+ rebuildIndex(ctx: ExtensionContext): SessionIndexResult {
1873
+ if (!this.indexer || !this.database) throw new Error("Context database is unavailable");
1874
+ this.session = snapshotSession(ctx);
1875
+ if (!this.session.sessionFile) throw new Error("Ephemeral Pi sessions do not have a JSONL index");
1876
+
1877
+ const result = this.indexer.sync(this.session, true);
1878
+ this.lastIndexResult = result;
1879
+ this.lastIndexError = undefined;
1880
+ this.refreshProjectIndex(true);
1881
+ this.reconcileMemory(ctx, true);
1882
+ this.rebuildArtifacts(ctx);
1883
+ return result;
1884
+ }
1885
+
1886
+ diagnostics(ctx: ExtensionContext): RuntimeDiagnostics {
1887
+ const currentSession = this.session ?? snapshotSession(ctx);
1888
+ if (this.artifactManager) {
1889
+ this.lastArtifacts = this.artifactManager.diagnostics(
1890
+ new Set(ctx.sessionManager.getBranch().map((entry) => entry.id)),
1891
+ );
1892
+ }
1893
+ let indexed: SessionIndexStats | undefined;
1894
+ if (this.database && currentSession.sessionFile) {
1895
+ try {
1896
+ indexed = this.database.getSessionStats(currentSession.sessionId);
1897
+ } catch (error) {
1898
+ this.logger.warn("diagnostics.index_stats_failed", { error });
1899
+ }
1900
+ }
1901
+
1902
+ return {
1903
+ extensionVersion: EXTENSION_VERSION,
1904
+ supportedPiVersion: SUPPORTED_PI_VERSION,
1905
+ plannerVersion: this.config.context.mode === "managed" ? PLANNER_VERSION : OBSERVER_PLANNER_VERSION,
1906
+ phase: this.phase,
1907
+ enabled: this.config.enabled,
1908
+ contextMode: this.config.context.mode,
1909
+ session: currentSession,
1910
+ ...(ctx.model ? { model: { provider: ctx.model.provider, id: ctx.model.id } } : {}),
1911
+ ...(this.databasePath ? { databasePath: this.databasePath } : {}),
1912
+ ...(this.database ? { databaseSchemaVersion: this.database.schemaVersion } : {}),
1913
+ ...(indexed ? { indexed } : {}),
1914
+ ...(this.observation ? { observation: this.observation } : {}),
1915
+ ...(this.lastManifest ? { lastManifest: this.lastManifest } : {}),
1916
+ retrieval: this.lastRetrieval,
1917
+ project: this.lastProject,
1918
+ memory: this.lastMemory,
1919
+ privacy: this.lastPrivacy,
1920
+ ...(this.lastModelAwareness ? { modelAwareness: this.lastModelAwareness } : {}),
1921
+ nativeContinuation: this.nativeContinuation.diagnostics(),
1922
+ artifacts: this.lastArtifacts,
1923
+ compaction: this.getCompactionDiagnostics(ctx),
1924
+ ...(this.lastIndexResult ? { lastIndexResult: this.lastIndexResult } : {}),
1925
+ configFiles: [...(this.loadedConfig?.loadedFiles ?? [])],
1926
+ configWarnings: [...(this.loadedConfig?.warnings ?? [])],
1927
+ ...(this.lastIndexError ? { lastIndexError: this.lastIndexError } : {}),
1928
+ ...(this.lastError ? { lastError: this.lastError } : {}),
1929
+ };
1930
+ }
1931
+
1932
+ verifyArtifactHealth(ctx: ExtensionContext): void {
1933
+ if (!this.artifactManager) return;
1934
+ this.artifactManager.verifyIntegrity();
1935
+ this.lastArtifacts = this.artifactManager.diagnostics(
1936
+ new Set(ctx.sessionManager.getBranch().map((entry) => entry.id)),
1937
+ );
1938
+ }
1939
+
1940
+ health(): DatabaseHealth | undefined {
1941
+ if (!this.database) return undefined;
1942
+ return this.database.health();
1943
+ }
1944
+
1945
+ shutdown(ctx?: ExtensionContext): void {
1946
+ if (ctx) this.syncSessionIndex(ctx);
1947
+ this.nativeContinuation.invalidate("session-shutdown");
1948
+ this.closeDatabase();
1949
+ if (ctx) this.setStatus(ctx, undefined);
1950
+ this.phase = "closed";
1951
+ this.logger.info("runtime.closed", { sessionId: this.session?.sessionId });
1952
+ }
1953
+
1954
+ private getCompactionDiagnostics(ctx: ExtensionContext): CompactionDiagnostics {
1955
+ return this.compaction?.diagnostics(ctx) ?? defaultCompactionDiagnostics(this.config);
1956
+ }
1957
+
1958
+ private enterDegradedMode(ctx: ExtensionContext, error: unknown, stage: string): void {
1959
+ this.nativeContinuation.invalidate("runtime-degraded");
1960
+ this.closeDatabase();
1961
+ this.phase = "degraded";
1962
+ this.lastError = error instanceof Error ? error.message : String(error);
1963
+ this.logger.error("runtime.degraded", { stage, error: this.lastError });
1964
+ this.setStatus(ctx, "DS4 ctx: Pi fallback");
1965
+ if (ctx.hasUI) {
1966
+ ctx.ui.notify(`DS4 Context Engine unavailable; using Pi context. ${this.lastError}`, "warning");
1967
+ }
1968
+ }
1969
+
1970
+ private closeDatabase(): void {
1971
+ try {
1972
+ this.database?.close();
1973
+ } finally {
1974
+ this.compaction = undefined;
1975
+ this.retrievalEngine = undefined;
1976
+ this.projectKnowledge = undefined;
1977
+ this.projectRefreshPending = false;
1978
+ this.memoryManager = undefined;
1979
+ this.lastMemoryMutationSignature = undefined;
1980
+ this.artifactManager = undefined;
1981
+ this.indexer = undefined;
1982
+ this.database = undefined;
1983
+ }
1984
+ }
1985
+
1986
+ private setStatus(ctx: ExtensionContext, value: string | undefined): void {
1987
+ if (ctx.hasUI) ctx.ui.setStatus("ds4-context-engine", value);
1988
+ }
1989
+ }