ds4-context-engine 0.1.1 → 0.2.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { existsSync, realpathSync } from "node:fs";
3
- import { join, resolve } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { join, parse, resolve } from "node:path";
4
5
  import type {
5
6
  Api,
6
7
  AssistantMessage,
@@ -14,6 +15,7 @@ import {
14
15
  type SessionBeforeCompactEvent,
15
16
  type SessionCompactEvent,
16
17
  } from "@earendil-works/pi-coding-agent";
18
+ import type { RuntimeCapabilityNegotiation } from "ds4-context-core/adapter/runtime-adapter";
17
19
  import {
18
20
  ArtifactManager,
19
21
  disabledArtifactDiagnostics,
@@ -41,7 +43,12 @@ export type {
41
43
  CompactionPhase,
42
44
  SummaryGraphDiagnostics,
43
45
  } from "../pi-adapter/compaction-coordinator.ts";
44
- import { loadConfig, resolveDatabasePath, type LoadedConfig } from "ds4-context-core/config/config-loader";
46
+ import {
47
+ loadConfig,
48
+ resolveDatabasePath,
49
+ resolveRankingModelPath,
50
+ type LoadedConfig,
51
+ } from "ds4-context-core/config/config-loader";
45
52
  import { createDefaultConfig, type Ds4ContextConfig } from "ds4-context-core/config/config";
46
53
  import { calculateContextBudget, type ContextBudget } from "ds4-context-core/core/budget-manager";
47
54
  import {
@@ -61,6 +68,7 @@ import type {
61
68
  } from "ds4-context-core/manifest/context-manifest";
62
69
  import type { ExcludedContextSource, ObservedTool } from "ds4-context-core/manifest/observer";
63
70
  import {
71
+ disabledCrossSessionMemoryDiagnostics,
64
72
  disabledMemoryDiagnostics,
65
73
  MemoryManager,
66
74
  type MemoryDiagnostics,
@@ -69,14 +77,36 @@ import {
69
77
  import {
70
78
  MEMORY_CUSTOM_ENTRY_TYPE,
71
79
  PIN_CUSTOM_ENTRY_TYPE,
80
+ type CrossSessionMemoryDiagnostics,
72
81
  type MemoryItem,
73
82
  type MemoryMutation,
74
83
  type MemoryScope,
75
84
  type PinItem,
76
85
  type PinMutation,
77
86
  type PinScope,
87
+ type ProjectMemorySource,
78
88
  } from "ds4-context-core/memory/memory-types";
79
89
  import { planManagedContext, type SupplementalContextMessage } from "ds4-context-core/planner/context-planner";
90
+ import {
91
+ disabledContextQualityDiagnostics,
92
+ evaluateManifestQuality,
93
+ type ContextQualityDiagnostics,
94
+ } from "ds4-context-core/quality/context-quality";
95
+ import {
96
+ createRankingFeatures,
97
+ createRankingFeedback,
98
+ disabledRankingDiagnostics,
99
+ loadLearnedRankingModel,
100
+ rankCandidates,
101
+ RANKING_FEEDBACK_CUSTOM_ENTRY_TYPE,
102
+ saveLearnedRankingModel,
103
+ trainLearnedRankingModel,
104
+ type LearnedRankingModel,
105
+ type RankingDiagnostics,
106
+ type RankingFeatureVector,
107
+ type RankingFeedbackEntry,
108
+ type RankingFeedbackLabel,
109
+ } from "ds4-context-core/ranking/learned-ranker";
80
110
  import {
81
111
  disabledPrivacyDiagnostics,
82
112
  emptyPrivacyCounts,
@@ -89,12 +119,15 @@ import {
89
119
  ProjectKnowledgeManager,
90
120
  type ProjectKnowledgeDiagnostics,
91
121
  } from "ds4-context-core/project/project-knowledge";
122
+ import type { EmbeddingPort } from "ds4-context-core/retrieval/embedding";
123
+ import { SemanticEmbeddingIndex } from "ds4-context-core/retrieval/semantic-index";
92
124
  import {
93
125
  emptyRetrievalDiagnostics,
94
126
  HistoricalRetrievalEngine,
95
127
  type RetrievalDiagnostics,
96
128
  } from "ds4-context-core/retrieval/retrieval-engine";
97
129
  import { currentRequestText } from "ds4-context-core/retrieval/task-descriptor";
130
+ import { LocalFeatureHashEmbedding } from "../pi-adapter/local-embedding.ts";
98
131
  import { ContextDatabase, type DatabaseHealth, type SessionIndexStats } from "ds4-context-core/persistence/sqlite";
99
132
  import {
100
133
  activeTools,
@@ -103,6 +136,9 @@ import {
103
136
  findPiPinnedMessageIndices,
104
137
  } from "../pi-adapter/context-observer.ts";
105
138
  import { projectSessionFileMutations } from "../pi-adapter/memory-adapter.ts";
139
+ import { ProjectMemorySynchronizer } from "../pi-adapter/project-memory-sync.ts";
140
+ import { projectRankingLabels } from "../pi-adapter/ranking-adapter.ts";
141
+ import { negotiatePiRuntimeCapabilities } from "../pi-adapter/runtime-contract.ts";
106
142
  import { PiSessionIndexer, type SessionIndexResult } from "../pi-adapter/session-indexer.ts";
107
143
  import { snapshotModel, snapshotSession, type PiSessionSnapshot } from "../pi-adapter/session-reader.ts";
108
144
  import { silentLogger, StructuredLogger, type Logger } from "ds4-context-core/shared/logging";
@@ -117,6 +153,24 @@ import {
117
153
  export type RuntimePhase = "idle" | "initializing" | "disabled" | "observer" | "managed" | "degraded" | "closed";
118
154
 
119
155
  const READ_ONLY_PROJECT_TOOLS = new Set(["read", "grep", "find", "ls"]);
156
+ const MAX_PRIOR_RANKING_OUTCOMES = 50_000;
157
+
158
+ function comparablePath(path: string): string {
159
+ let canonical: string;
160
+ try {
161
+ canonical = realpathSync(path);
162
+ } catch {
163
+ canonical = resolve(path);
164
+ }
165
+ return process.platform === "win32" ? canonical.toLowerCase() : canonical;
166
+ }
167
+
168
+ export function isBroadProjectRoot(projectPath: string, homePath = homedir()): boolean {
169
+ const project = comparablePath(projectPath);
170
+ const home = comparablePath(homePath);
171
+ const filesystemRoot = comparablePath(parse(project).root);
172
+ return project === home || project === filesystemRoot;
173
+ }
120
174
 
121
175
  export interface RuntimeDependencies {
122
176
  agentDir: string;
@@ -125,6 +179,8 @@ export interface RuntimeDependencies {
125
179
  now?: () => number;
126
180
  idGenerator?: () => string;
127
181
  logSink?: (line: string) => void;
182
+ /** Optional runtime-owned embedding provider; required for configured remote models. */
183
+ embeddingPort?: EmbeddingPort;
128
184
  }
129
185
 
130
186
  interface PreparedPrivacyContext {
@@ -231,7 +287,30 @@ export interface CreateMemoryInput {
231
287
  sourceEntryIds: string[];
232
288
  }
233
289
 
234
- export type CustomEntryAppender = (customType: string, data: MemoryMutation | PinMutation) => void;
290
+ export type CustomEntryAppender = (
291
+ customType: string,
292
+ data: MemoryMutation | PinMutation | RankingFeedbackEntry,
293
+ ) => void;
294
+
295
+ export interface RuntimeRankingDiagnostics extends RankingDiagnostics {
296
+ modelPath?: string;
297
+ modelLoaded: boolean;
298
+ promoted: boolean;
299
+ trainingSamples: number;
300
+ malformedFeedback: number;
301
+ duplicateFeedback: number;
302
+ warnings: string[];
303
+ }
304
+
305
+ export interface RankingTrainingResult {
306
+ modelId: string;
307
+ modelPath: string;
308
+ sampleCount: number;
309
+ positiveSamples: number;
310
+ negativeSamples: number;
311
+ repositoryCount: number;
312
+ warnings: string[];
313
+ }
235
314
 
236
315
  export interface RuntimeDiagnostics {
237
316
  extensionVersion: string;
@@ -240,6 +319,7 @@ export interface RuntimeDiagnostics {
240
319
  phase: RuntimePhase;
241
320
  enabled: boolean;
242
321
  contextMode: "observer" | "managed";
322
+ adapter: RuntimeCapabilityNegotiation;
243
323
  session?: PiSessionSnapshot;
244
324
  model?: { provider: string; id: string };
245
325
  databasePath?: string;
@@ -253,6 +333,8 @@ export interface RuntimeDiagnostics {
253
333
  privacy: PrivacyDiagnostics;
254
334
  modelAwareness?: ModelAwarenessManifest;
255
335
  nativeContinuation: NativeContinuationDiagnostics;
336
+ quality: ContextQualityDiagnostics;
337
+ ranking: RuntimeRankingDiagnostics;
256
338
  artifacts: ArtifactDiagnostics;
257
339
  compaction: CompactionDiagnostics;
258
340
  lastIndexResult?: SessionIndexResult;
@@ -275,10 +357,15 @@ export class Ds4ContextRuntime {
275
357
  private pendingManifestId?: string;
276
358
  private retrievalEngine?: HistoricalRetrievalEngine;
277
359
  private lastRetrieval: RetrievalDiagnostics = emptyRetrievalDiagnostics();
360
+ private semanticIndex?: SemanticEmbeddingIndex;
361
+ private semanticFallbackReason?: string;
278
362
  private projectKnowledge?: ProjectKnowledgeManager;
279
363
  private lastProject: ProjectKnowledgeDiagnostics = emptyProjectDiagnostics();
280
364
  private projectRefreshPending = false;
365
+ private readonly projectLeaseOwnerId = randomUUID();
281
366
  private memoryManager?: MemoryManager;
367
+ private projectMemorySynchronizer?: ProjectMemorySynchronizer;
368
+ private lastCrossSessionMemory: CrossSessionMemoryDiagnostics = disabledCrossSessionMemoryDiagnostics();
282
369
  private lastMemory: MemoryDiagnostics = disabledMemoryDiagnostics();
283
370
  private privacyEngine?: PrivacyPolicyEngine;
284
371
  private lastPrivacy: PrivacyDiagnostics = disabledPrivacyDiagnostics();
@@ -294,6 +381,18 @@ export class Ds4ContextRuntime {
294
381
  private compaction?: CompactionCoordinator;
295
382
  private lastIndexResult?: SessionIndexResult;
296
383
  private lastIndexError?: string;
384
+ private lastQualityError?: string;
385
+ private rankingModel?: LearnedRankingModel;
386
+ private rankingModelPath?: string;
387
+ private lastRanking: RankingDiagnostics = disabledRankingDiagnostics();
388
+ private readonly rankingCandidates = new Map<string, RankingFeatureVector>();
389
+ private readonly priorRankingSelection = new Map<string, number>();
390
+ private rankingWarnings: string[] = [];
391
+ private readonly pendingQuality: Array<{
392
+ manifest: ContextManifest;
393
+ contextConfig: Ds4ContextConfig["context"];
394
+ recordedAt: number;
395
+ }> = [];
297
396
  private lastError?: string;
298
397
  private logger: Logger = silentLogger;
299
398
  private readonly now: () => number;
@@ -309,14 +408,25 @@ export class Ds4ContextRuntime {
309
408
  }
310
409
 
311
410
  openSession(ctx: ExtensionContext): void {
411
+ this.flushContextQuality();
312
412
  this.closeDatabase();
313
413
  this.phase = "initializing";
314
414
  this.lastError = undefined;
315
415
  this.lastIndexError = undefined;
416
+ this.lastQualityError = undefined;
417
+ this.rankingModel = undefined;
418
+ this.rankingModelPath = undefined;
419
+ this.lastRanking = disabledRankingDiagnostics(this.config.ranking.mode);
420
+ this.rankingCandidates.clear();
421
+ this.priorRankingSelection.clear();
422
+ this.rankingWarnings = [];
423
+ this.pendingQuality.length = 0;
316
424
  this.lastIndexResult = undefined;
317
425
  this.lastManifest = undefined;
318
426
  this.pendingManifestId = undefined;
319
427
  this.retrievalEngine = undefined;
428
+ this.semanticIndex = undefined;
429
+ this.semanticFallbackReason = undefined;
320
430
  this.lastRetrieval = emptyRetrievalDiagnostics(
321
431
  this.config.context.maxRetrievedHistoryTokens,
322
432
  this.config.retrieval.maxResults,
@@ -325,6 +435,8 @@ export class Ds4ContextRuntime {
325
435
  this.lastProject = emptyProjectDiagnostics();
326
436
  this.projectRefreshPending = false;
327
437
  this.memoryManager = undefined;
438
+ this.projectMemorySynchronizer = undefined;
439
+ this.lastCrossSessionMemory = disabledCrossSessionMemoryDiagnostics();
328
440
  this.lastMemory = disabledMemoryDiagnostics();
329
441
  this.privacyEngine = undefined;
330
442
  this.lastPrivacy = disabledPrivacyDiagnostics();
@@ -339,7 +451,7 @@ export class Ds4ContextRuntime {
339
451
  this.lastArtifacts = disabledArtifactDiagnostics();
340
452
  this.compaction = undefined;
341
453
  this.observation = undefined;
342
- this.session = snapshotSession(ctx);
454
+ this.session = this.snapshotCanonicalSession(ctx);
343
455
 
344
456
  try {
345
457
  this.loadedConfig = loadConfig({
@@ -383,13 +495,22 @@ export class Ds4ContextRuntime {
383
495
  this.database = ContextDatabase.open(this.databasePath, {
384
496
  logger: this.logger,
385
497
  now: this.now(),
498
+ busyTimeoutMs: this.config.storage.busyTimeoutMs,
499
+ writeRetryTimeoutMs: this.config.storage.writeRetryTimeoutMs,
386
500
  });
501
+ this.initializeRanking();
387
502
 
388
503
  this.indexer = new PiSessionIndexer(this.database.sessionIndex, {
389
504
  logger: this.logger,
390
505
  now: this.now,
391
506
  });
392
- this.retrievalEngine = new HistoricalRetrievalEngine(this.database.sessionIndex);
507
+ this.initializeSemanticRetrieval();
508
+ this.retrievalEngine = new HistoricalRetrievalEngine(
509
+ this.database.sessionIndex,
510
+ undefined,
511
+ this.semanticIndex,
512
+ this.semanticFallbackReason,
513
+ );
393
514
  this.lastRetrieval = emptyRetrievalDiagnostics(
394
515
  this.config.context.maxRetrievedHistoryTokens,
395
516
  this.config.retrieval.maxResults,
@@ -674,6 +795,7 @@ export class Ds4ContextRuntime {
674
795
  const preparedPrivacy = this.preparePrivacyContext(event, ctx, pi);
675
796
  if (preparedPrivacy.changed) privacyFallback = { messages: preparedPrivacy.event.messages };
676
797
  this.syncSessionIndex(ctx);
798
+ this.refreshProjectMemorySources();
677
799
  this.refreshProjectIndexIfPending();
678
800
  if (this.config.context.mode !== "managed") {
679
801
  this.lastRetrieval = emptyRetrievalDiagnostics(
@@ -765,6 +887,7 @@ export class Ds4ContextRuntime {
765
887
  let result: { messages?: ContextEvent["messages"] } | undefined = privacyFallback;
766
888
  if (this.config.context.mode === "managed" && budget) {
767
889
  const planningStartedAt = this.now();
890
+ this.rankingCandidates.clear();
768
891
  const requestText = currentRequestText(effectiveEvent.messages);
769
892
  const activeEntryIds = new Set(ctx.sessionManager.getBranch().map((entry) => entry.id));
770
893
  const memorySelection: MemorySelection = this.memoryManager?.select(requestText, activeEntryIds) ?? {
@@ -814,6 +937,10 @@ export class Ds4ContextRuntime {
814
937
  sourceIds: [evidence.item.id],
815
938
  score: 90 + Math.min(0.999999, Math.max(0, evidence.score) / 1_000),
816
939
  reason: evidence.reason,
940
+ rankingFeatures: createRankingFeatures({
941
+ ...evidence.rankingFeatures,
942
+ priorSelected: this.priorRankingSelection.get(`memory:${evidence.item.id}`) ?? 0,
943
+ }),
817
944
  ...(evidence.item.classification ? { classification: evidence.item.classification } : {}),
818
945
  })),
819
946
  ...retrieval.selected.map((evidence) => ({
@@ -823,6 +950,10 @@ export class Ds4ContextRuntime {
823
950
  sourceIds: [evidence.entryId],
824
951
  score: 85 + Math.min(0.999999, Math.max(0, evidence.score) / 1_000),
825
952
  reason: `Historical evidence: ${evidence.reason}`,
953
+ rankingFeatures: createRankingFeatures({
954
+ ...evidence.rankingFeatures,
955
+ priorSelected: this.priorRankingSelection.get(`retrieval:${evidence.entryId}`) ?? 0,
956
+ }),
826
957
  })),
827
958
  ...project.selected.map((evidence) => ({
828
959
  id: `project:${evidence.snippetId}`,
@@ -832,6 +963,10 @@ export class Ds4ContextRuntime {
832
963
  score: 80 + Math.min(0.999999, Math.max(0, evidence.score) / 1_000),
833
964
  reason: `Project source: ${evidence.reason}`,
834
965
  projectSnippet: evidence.manifestRef,
966
+ rankingFeatures: createRankingFeatures({
967
+ ...evidence.rankingFeatures,
968
+ priorSelected: this.priorRankingSelection.get(`project:${evidence.snippetId}`) ?? 0,
969
+ }),
835
970
  })),
836
971
  ];
837
972
  const privacyExcludedSources: ExcludedContextSource[] = [];
@@ -870,13 +1005,45 @@ export class Ds4ContextRuntime {
870
1005
  ...this.lastPrivacy,
871
1006
  excludedSources: this.lastPrivacy.excludedSources + privacyExcludedSources.length,
872
1007
  };
1008
+ const learnedCandidates = supplementalMessages.flatMap((supplement) =>
1009
+ supplement.kind !== "pin" && supplement.rankingFeatures
1010
+ ? [{
1011
+ id: supplement.id,
1012
+ staticScore: supplement.score,
1013
+ features: supplement.rankingFeatures,
1014
+ value: supplement,
1015
+ }]
1016
+ : []
1017
+ );
1018
+ const ranking = rankCandidates(learnedCandidates, {
1019
+ mode: this.config.ranking.mode,
1020
+ ...(this.rankingModel ? { model: this.rankingModel } : {}),
1021
+ now: this.now,
1022
+ maxLatencyMs: this.config.ranking.maxLatencyMs,
1023
+ });
1024
+ this.lastRanking = ranking.diagnostics;
1025
+ const rankedById = new Map(ranking.ranked.map((candidate) => [candidate.id, candidate]));
1026
+ const rankedSupplementalMessages = supplementalMessages.map((supplement) => {
1027
+ const ranked = rankedById.get(supplement.id);
1028
+ if (!ranked) return supplement;
1029
+ return {
1030
+ ...supplement,
1031
+ score: ranked.effectiveScore,
1032
+ reason: ranking.diagnostics.status === "active"
1033
+ ? `${supplement.reason}; learned rank ${ranked.learnedScore.toFixed(6)}`
1034
+ : supplement.reason,
1035
+ };
1036
+ });
873
1037
  const plan = planManagedContext({
874
1038
  messages: effectiveEvent.messages,
875
1039
  fixedTokens: baseline.composition.systemTokens + baseline.composition.toolTokens,
876
1040
  budget,
877
1041
  config: effectiveContextConfig,
878
1042
  pinnedMessageIndices: findPiPinnedMessageIndices(effectiveEvent, ctx),
879
- supplementalMessages,
1043
+ supplementalMessages: rankedSupplementalMessages,
1044
+ ...(ranking.diagnostics.status === "active"
1045
+ ? { supplementalSelectionOrder: ranking.ranked.map((candidate) => candidate.id) }
1046
+ : {}),
880
1047
  ...(this.config.privacy.enabled
881
1048
  ? {
882
1049
  messageClassifications: preparedPrivacy.messageClassifications,
@@ -884,6 +1051,11 @@ export class Ds4ContextRuntime {
884
1051
  }
885
1052
  : {}),
886
1053
  });
1054
+ if (plan.mode === "managed") {
1055
+ for (const candidate of learnedCandidates) {
1056
+ this.rankingCandidates.set(candidate.id, { ...candidate.features });
1057
+ }
1058
+ }
887
1059
  const selectedPinIds = new Set(
888
1060
  plan.selected.flatMap((metadata) => metadata.kind === "pin" && metadata.sourceId ? [metadata.sourceId] : []),
889
1061
  );
@@ -912,6 +1084,16 @@ export class Ds4ContextRuntime {
912
1084
  plan.selected.flatMap((metadata) => metadata.projectSnippet?.snippetId ? [metadata.projectSnippet.snippetId] : []),
913
1085
  );
914
1086
  const plannerSelectedProject = project.selected.filter((evidence) => selectedProjectIds.has(evidence.snippetId));
1087
+ for (const supplement of rankedSupplementalMessages) {
1088
+ if (supplement.kind === "pin" || !supplement.rankingFeatures) continue;
1089
+ const selected = supplement.kind === "memory"
1090
+ ? selectedMemoryIds.has(supplement.sourceIds[0] ?? "")
1091
+ : supplement.kind === "retrieval"
1092
+ ? selectedRetrievalIds.has(supplement.sourceIds[0] ?? "")
1093
+ : Boolean(supplement.projectSnippet?.snippetId
1094
+ && selectedProjectIds.has(supplement.projectSnippet.snippetId));
1095
+ this.rememberPriorRankingSelection(supplement.id, selected ? 1 : 0);
1096
+ }
915
1097
  this.lastProject = {
916
1098
  ...project,
917
1099
  plannerExcludedCount: project.selected.length - plannerSelectedProject.length,
@@ -986,6 +1168,7 @@ export class Ds4ContextRuntime {
986
1168
 
987
1169
  manifest = {
988
1170
  ...manifest,
1171
+ ranking: { ...this.lastRanking },
989
1172
  nativeContinuation: this.nativeContinuation.initialManifest(
990
1173
  manifest.provider,
991
1174
  manifest.model,
@@ -1011,6 +1194,11 @@ export class Ds4ContextRuntime {
1011
1194
  observedAt,
1012
1195
  ...(budget ? { budget } : {}),
1013
1196
  };
1197
+ if (this.config.quality.enabled) {
1198
+ this.pendingQuality.push({ manifest, contextConfig: effectiveContextConfig, recordedAt: observedAt });
1199
+ const pendingLimit = Math.min(this.config.quality.maxSamples, 256);
1200
+ if (this.pendingQuality.length > pendingLimit) this.pendingQuality.splice(0, this.pendingQuality.length - pendingLimit);
1201
+ }
1014
1202
  this.lastManifest = manifest;
1015
1203
  this.pendingManifestId = manifest.id;
1016
1204
  if (this.config.diagnostics.storeContextManifest && this.session?.sessionFile) {
@@ -1315,8 +1503,10 @@ export class Ds4ContextRuntime {
1315
1503
  }
1316
1504
 
1317
1505
  afterAgentSettled(ctx: ExtensionContext): void {
1506
+ this.flushContextQuality();
1318
1507
  if (this.compaction) this.compaction.afterAgentSettled(ctx);
1319
1508
  else this.syncSessionIndex(ctx);
1509
+ this.refreshProjectMemorySources();
1320
1510
  this.refreshProjectIndex();
1321
1511
  }
1322
1512
 
@@ -1350,6 +1540,107 @@ export class Ds4ContextRuntime {
1350
1540
  return this.memoryManager?.listMemories(activeOnly) ?? [];
1351
1541
  }
1352
1542
 
1543
+ projectMemorySources(): ProjectMemorySource[] {
1544
+ return this.lastCrossSessionMemory.sources.map((source) => ({ ...source }));
1545
+ }
1546
+
1547
+ setProjectMemorySourceExcluded(
1548
+ sessionId: string,
1549
+ excluded: boolean,
1550
+ reason?: string,
1551
+ ): ProjectMemorySource[] {
1552
+ if (!this.projectMemorySynchronizer || !this.memoryManager) {
1553
+ throw new Error("Cross-session project memory is unavailable for this session");
1554
+ }
1555
+ this.lastCrossSessionMemory = this.projectMemorySynchronizer.setExcluded(
1556
+ sessionId,
1557
+ excluded,
1558
+ reason,
1559
+ );
1560
+ this.memoryManager.setCrossSessionDiagnostics(this.lastCrossSessionMemory);
1561
+ this.lastMemory = this.memoryManager.diagnostics();
1562
+ return this.projectMemorySources();
1563
+ }
1564
+
1565
+ recordRankingFeedback(
1566
+ candidateId: string,
1567
+ label: RankingFeedbackLabel,
1568
+ classification: PrivacyClassification | undefined,
1569
+ ctx: ExtensionContext,
1570
+ appendEntry: CustomEntryAppender,
1571
+ ): RankingFeedbackEntry {
1572
+ const normalizedId = candidateId.trim();
1573
+ let features = this.rankingCandidates.get(normalizedId);
1574
+ let resolvedId = normalizedId;
1575
+ if (!features) {
1576
+ const matches = [...this.rankingCandidates].filter(([id]) => id.endsWith(`:${normalizedId}`));
1577
+ if (matches.length === 1) {
1578
+ resolvedId = matches[0]?.[0] ?? normalizedId;
1579
+ features = matches[0]?.[1];
1580
+ }
1581
+ }
1582
+ if (!features) {
1583
+ throw new Error(`Ranking candidate ${candidateId} is not available in the latest managed context`);
1584
+ }
1585
+ if (!this.session) throw new Error("Canonical session identity is unavailable");
1586
+ const feedback = createRankingFeedback({
1587
+ feedbackId: this.idGenerator(),
1588
+ createdAt: this.now(),
1589
+ repositoryIdentity: this.session.projectPath,
1590
+ candidateKey: resolvedId,
1591
+ label,
1592
+ classification: classification ?? this.config.privacy.defaultClassification,
1593
+ features,
1594
+ });
1595
+ appendEntry(RANKING_FEEDBACK_CUSTOM_ENTRY_TYPE, feedback);
1596
+ this.syncSessionIndex(ctx);
1597
+ this.logger.info("ranking.feedback_recorded", {
1598
+ feedbackId: feedback.feedbackId,
1599
+ label,
1600
+ classification: feedback.classification,
1601
+ });
1602
+ return feedback;
1603
+ }
1604
+
1605
+ trainRankingModel(ctx: ExtensionContext): RankingTrainingResult {
1606
+ if (!this.rankingModelPath) throw new Error("Ranking model path is unavailable");
1607
+ const projection = projectRankingLabels(
1608
+ ctx.sessionManager.getBranch(),
1609
+ this.config.ranking.maxTrainingSamples,
1610
+ );
1611
+ const model = trainLearnedRankingModel(projection.samples, {
1612
+ createdAt: this.now(),
1613
+ minimumSamples: this.config.ranking.minimumTrainingSamples,
1614
+ });
1615
+ saveLearnedRankingModel(this.rankingModelPath, model);
1616
+ this.rankingModel = model;
1617
+ this.rankingWarnings = [...projection.warnings];
1618
+ this.lastRanking = rankCandidates([], {
1619
+ mode: this.config.ranking.mode,
1620
+ model,
1621
+ now: this.now,
1622
+ maxLatencyMs: this.config.ranking.maxLatencyMs,
1623
+ }).diagnostics;
1624
+ if (this.config.ranking.mode === "off") {
1625
+ this.lastRanking = { ...this.lastRanking, modelId: model.modelId };
1626
+ }
1627
+ this.logger.info("ranking.model_trained", {
1628
+ modelId: model.modelId,
1629
+ samples: model.training.sampleCount,
1630
+ repositories: model.training.repositoryCount,
1631
+ promoted: Boolean(model.promotion?.eligible),
1632
+ });
1633
+ return {
1634
+ modelId: model.modelId,
1635
+ modelPath: this.rankingModelPath,
1636
+ sampleCount: model.training.sampleCount,
1637
+ positiveSamples: model.training.positiveSamples,
1638
+ negativeSamples: model.training.negativeSamples,
1639
+ repositoryCount: model.training.repositoryCount,
1640
+ warnings: [...projection.warnings],
1641
+ };
1642
+ }
1643
+
1353
1644
  createPin(
1354
1645
  input: CreatePinInput,
1355
1646
  ctx: ExtensionContext,
@@ -1550,33 +1841,66 @@ export class Ds4ContextRuntime {
1550
1841
  }
1551
1842
 
1552
1843
  private initializeMemory(ctx: ExtensionContext): void {
1844
+ this.projectMemorySynchronizer = undefined;
1845
+ this.lastCrossSessionMemory = disabledCrossSessionMemoryDiagnostics();
1553
1846
  if (this.config.context.mode !== "managed" || !this.config.memory.enabled) {
1554
1847
  this.memoryManager = undefined;
1555
1848
  this.lastMemory = disabledMemoryDiagnostics();
1556
1849
  return;
1557
1850
  }
1558
- if (!this.database || !this.session?.sessionFile) {
1851
+ if (!this.database || !this.indexer || !this.session?.sessionFile) {
1559
1852
  this.memoryManager = undefined;
1560
1853
  this.lastMemory = disabledMemoryDiagnostics("Memory and pins require a persisted Pi session");
1561
1854
  return;
1562
1855
  }
1563
1856
  try {
1857
+ const projectPath = canonicalProjectPath(this.session.projectPath);
1858
+ const projectTrusted = ctx.isProjectTrusted();
1564
1859
  this.memoryManager = new MemoryManager(
1565
1860
  this.database.memory,
1566
1861
  this.config.memory,
1567
1862
  this.config.context.maxPinnedTokens,
1568
1863
  this.config.context.maxMemoryTokens,
1569
1864
  this.session.sessionId,
1570
- canonicalProjectPath(this.session.projectPath),
1571
- ctx.isProjectTrusted(),
1865
+ projectPath,
1866
+ projectTrusted,
1572
1867
  this.now,
1573
1868
  this.idGenerator,
1574
1869
  );
1575
1870
  if (existsSync(this.session.sessionFile)) this.reconcileMemory(ctx);
1576
1871
  else this.lastMemory = this.memoryManager.diagnostics();
1872
+
1873
+ if (this.config.memory.crossSession && projectTrusted) {
1874
+ this.projectMemorySynchronizer = new ProjectMemorySynchronizer(
1875
+ this.database.memory,
1876
+ this.indexer,
1877
+ {
1878
+ projectPath,
1879
+ activeSessionId: this.session.sessionId,
1880
+ activeSessionFile: this.session.sessionFile,
1881
+ maxSessions: this.config.memory.maxProjectSessions,
1882
+ logger: this.logger,
1883
+ now: this.now,
1884
+ },
1885
+ );
1886
+ this.refreshProjectMemorySources();
1887
+ } else {
1888
+ const reason = this.config.memory.crossSession
1889
+ ? "Cross-session project memory requires a trusted Pi project"
1890
+ : undefined;
1891
+ this.lastCrossSessionMemory = disabledCrossSessionMemoryDiagnostics(reason);
1892
+ this.memoryManager.setCrossSessionDiagnostics(this.lastCrossSessionMemory);
1893
+ this.lastMemory = this.memoryManager.diagnostics();
1894
+ }
1577
1895
  } catch (error) {
1578
1896
  const message = error instanceof Error ? error.message : String(error);
1579
1897
  this.memoryManager = undefined;
1898
+ this.projectMemorySynchronizer = undefined;
1899
+ this.lastCrossSessionMemory = {
1900
+ ...disabledCrossSessionMemoryDiagnostics(message),
1901
+ enabled: this.config.memory.crossSession,
1902
+ status: "failed",
1903
+ };
1580
1904
  this.lastMemory = {
1581
1905
  ...disabledMemoryDiagnostics(message),
1582
1906
  status: "failed",
@@ -1600,7 +1924,7 @@ export class Ds4ContextRuntime {
1600
1924
  ];
1601
1925
  if (sourceEntryIds.some((entryId) => !this.database?.sessionIndex.hasEntry(this.session!.sessionId, entryId))) {
1602
1926
  if (!this.indexer) throw new Error("Session index is unavailable for memory mutation replay");
1603
- this.session = snapshotSession(ctx);
1927
+ this.session = this.snapshotCanonicalSession(ctx);
1604
1928
  const rebuilt = this.indexer.sync(this.session, true);
1605
1929
  this.lastIndexResult = rebuilt;
1606
1930
  this.lastIndexError = undefined;
@@ -1622,6 +1946,24 @@ export class Ds4ContextRuntime {
1622
1946
  });
1623
1947
  }
1624
1948
 
1949
+ private refreshProjectMemorySources(): void {
1950
+ if (!this.projectMemorySynchronizer || !this.memoryManager) return;
1951
+ try {
1952
+ this.lastCrossSessionMemory = this.projectMemorySynchronizer.sync();
1953
+ } catch (error) {
1954
+ const message = error instanceof Error ? error.message : String(error);
1955
+ this.lastCrossSessionMemory = {
1956
+ ...this.lastCrossSessionMemory,
1957
+ enabled: true,
1958
+ status: "failed",
1959
+ warnings: [...new Set([...this.lastCrossSessionMemory.warnings, message])],
1960
+ };
1961
+ this.logger.warn("project_memory.failed", { error: message });
1962
+ }
1963
+ this.memoryManager.setCrossSessionDiagnostics(this.lastCrossSessionMemory);
1964
+ this.lastMemory = this.memoryManager.diagnostics();
1965
+ }
1966
+
1625
1967
  private requireMemoryManager(): MemoryManager {
1626
1968
  if (!this.memoryManager) throw new Error("DS4 memory and pins are unavailable for this session");
1627
1969
  return this.memoryManager;
@@ -1694,6 +2036,102 @@ export class Ds4ContextRuntime {
1694
2036
  });
1695
2037
  }
1696
2038
 
2039
+ private rememberPriorRankingSelection(candidateId: string, selected: number): void {
2040
+ this.priorRankingSelection.delete(candidateId);
2041
+ this.priorRankingSelection.set(candidateId, selected);
2042
+ while (this.priorRankingSelection.size > MAX_PRIOR_RANKING_OUTCOMES) {
2043
+ const oldest = this.priorRankingSelection.keys().next().value as string | undefined;
2044
+ if (!oldest) break;
2045
+ this.priorRankingSelection.delete(oldest);
2046
+ }
2047
+ }
2048
+
2049
+ private initializeRanking(): void {
2050
+ this.rankingModel = undefined;
2051
+ this.rankingWarnings = [];
2052
+ this.rankingModelPath = resolveRankingModelPath(
2053
+ this.config.ranking.modelPath,
2054
+ this.dependencies.agentDir,
2055
+ this.dependencies.homeDir,
2056
+ );
2057
+ this.lastRanking = disabledRankingDiagnostics(this.config.ranking.mode);
2058
+ if (this.config.ranking.mode === "off") return;
2059
+ const loaded = loadLearnedRankingModel(this.rankingModelPath);
2060
+ if (loaded.model) {
2061
+ this.rankingModel = loaded.model;
2062
+ this.lastRanking = rankCandidates([], {
2063
+ mode: this.config.ranking.mode,
2064
+ model: loaded.model,
2065
+ now: this.now,
2066
+ maxLatencyMs: this.config.ranking.maxLatencyMs,
2067
+ }).diagnostics;
2068
+ if (this.config.ranking.mode === "active" && !loaded.model.promotion?.eligible) {
2069
+ this.rankingWarnings.push("Configured active model has not passed the promotion gate; static ranking remains active");
2070
+ }
2071
+ return;
2072
+ }
2073
+ this.rankingWarnings.push(`Learned ranking unavailable: ${loaded.error ?? "model load failed"}`);
2074
+ }
2075
+
2076
+ private initializeSemanticRetrieval(): void {
2077
+ this.semanticIndex = undefined;
2078
+ this.semanticFallbackReason = undefined;
2079
+ if (!this.config.retrieval.semantic || !this.database) return;
2080
+
2081
+ const configured = this.config.retrieval.embedding;
2082
+ const expectedDestination = configured.mode;
2083
+ const matchesConfiguration = (port: EmbeddingPort): boolean =>
2084
+ port.identity.provider === configured.provider
2085
+ && port.identity.model === configured.model
2086
+ && port.identity.dimensions === configured.dimensions
2087
+ && port.identity.destination === expectedDestination;
2088
+
2089
+ let port = this.dependencies.embeddingPort;
2090
+ if (port && !matchesConfiguration(port)) port = undefined;
2091
+ if (!port && configured.mode === "local"
2092
+ && configured.provider === "ds4-local"
2093
+ && configured.model === "feature-hash-v1") {
2094
+ port = new LocalFeatureHashEmbedding(configured.dimensions);
2095
+ }
2096
+ if (configured.mode === "remote") {
2097
+ const profile = `${configured.provider}/${configured.model}`;
2098
+ if (!configured.remoteProfiles.includes(profile)) {
2099
+ this.semanticFallbackReason = "remote embedding profile lacks explicit consent";
2100
+ return;
2101
+ }
2102
+ if (!this.config.privacy.enabled || !this.privacyEngine) {
2103
+ this.semanticFallbackReason = "remote embedding requires privacy filtering";
2104
+ return;
2105
+ }
2106
+ }
2107
+ if (!port) {
2108
+ this.semanticFallbackReason = `embedding model unavailable: ${configured.provider}/${configured.model}`;
2109
+ return;
2110
+ }
2111
+
2112
+ this.semanticIndex = new SemanticEmbeddingIndex(
2113
+ this.database.embeddings,
2114
+ port,
2115
+ {
2116
+ maxSources: configured.maxSources,
2117
+ candidatePool: configured.candidatePool,
2118
+ batchSize: configured.batchSize,
2119
+ queryCacheSize: configured.queryCacheSize,
2120
+ timeoutMs: configured.timeoutMs,
2121
+ },
2122
+ (text) => {
2123
+ if (port!.identity.destination === "local") return text;
2124
+ const sanitized = this.privacyEngine?.sanitizeText(text, port!.identity.provider);
2125
+ if (!sanitized || sanitized.classification === "local-only" || sanitized.blockedBlocks > 0) {
2126
+ return undefined;
2127
+ }
2128
+ return sanitized.value;
2129
+ },
2130
+ this.now,
2131
+ this.now,
2132
+ );
2133
+ }
2134
+
1697
2135
  private initializeProjectKnowledge(ctx: ExtensionContext): void {
1698
2136
  if (!this.config.project.enabled) {
1699
2137
  this.lastProject = emptyProjectDiagnostics(
@@ -1713,6 +2151,23 @@ export class Ds4ContextRuntime {
1713
2151
  );
1714
2152
  return;
1715
2153
  }
2154
+ if (isBroadProjectRoot(ctx.cwd, this.dependencies.homeDir ?? homedir())) {
2155
+ this.lastProject = {
2156
+ ...emptyProjectDiagnostics(
2157
+ "disabled",
2158
+ true,
2159
+ this.config.context.maxProjectTokens,
2160
+ this.config.project.maxResults,
2161
+ ),
2162
+ projectPath: resolve(ctx.cwd),
2163
+ fallbackReason: "Project indexing is skipped for filesystem roots and the user home directory",
2164
+ };
2165
+ this.logger.warn("project_index.skipped", {
2166
+ reason: "broad-root",
2167
+ projectPath: resolve(ctx.cwd),
2168
+ });
2169
+ return;
2170
+ }
1716
2171
  if (!this.database) return;
1717
2172
 
1718
2173
  try {
@@ -1722,8 +2177,22 @@ export class Ds4ContextRuntime {
1722
2177
  this.config.project,
1723
2178
  this.config.context.maxProjectTokens,
1724
2179
  this.now,
2180
+ {
2181
+ enabled: this.config.retrieval.semantic,
2182
+ ...(this.semanticIndex ? { index: this.semanticIndex } : {}),
2183
+ ...(this.semanticFallbackReason ? { fallbackReason: this.semanticFallbackReason } : {}),
2184
+ },
1725
2185
  );
1726
- const sync = this.projectKnowledge.sync();
2186
+ const sync = this.syncProjectKnowledge();
2187
+ if (!sync) {
2188
+ this.projectRefreshPending = true;
2189
+ this.lastProject = this.projectKnowledge.diagnostics(
2190
+ "ready",
2191
+ "Project indexing deferred while another Pi session indexes this project",
2192
+ );
2193
+ this.logger.debug("project_index.deferred", { projectPath: this.projectKnowledge.projectPath });
2194
+ return;
2195
+ }
1727
2196
  this.lastProject = this.projectKnowledge.diagnostics();
1728
2197
  this.logger.info("project_index.opened", {
1729
2198
  files: sync.discoveredFiles,
@@ -1751,15 +2220,78 @@ export class Ds4ContextRuntime {
1751
2220
  }
1752
2221
  }
1753
2222
 
2223
+ private withProjectIndexLease<T>(
2224
+ operation: (checkpoint: () => void) => T,
2225
+ ): { acquired: true; value: T } | { acquired: false } {
2226
+ const database = this.database;
2227
+ const project = this.projectKnowledge;
2228
+ if (!project || !database) return { acquired: false };
2229
+ const durationMs = this.config.storage.projectIndexLeaseMs;
2230
+ const lease = database.leases.acquire(
2231
+ "project-index",
2232
+ project.projectPath,
2233
+ this.projectLeaseOwnerId,
2234
+ this.now(),
2235
+ durationMs,
2236
+ );
2237
+ if (!lease) return { acquired: false };
2238
+ let activeLease = lease;
2239
+
2240
+ let renewAt = this.now() + Math.max(1_000, Math.floor(durationMs / 3));
2241
+ const checkpoint = (): void => {
2242
+ const current = this.now();
2243
+ if (current < renewAt) {
2244
+ if (!database.leases.isHeld(activeLease, current)) {
2245
+ throw new Error("Project indexing lease expired or was superseded");
2246
+ }
2247
+ return;
2248
+ }
2249
+ const renewed = database.leases.renew(activeLease, current, durationMs);
2250
+ if (!renewed) throw new Error("Project indexing lease expired or was superseded");
2251
+ activeLease = renewed;
2252
+ renewAt = current + Math.max(1_000, Math.floor(durationMs / 3));
2253
+ };
2254
+
2255
+ try {
2256
+ return { acquired: true, value: operation(checkpoint) };
2257
+ } finally {
2258
+ try {
2259
+ database.leases.release(activeLease);
2260
+ } catch (error) {
2261
+ this.logger.warn("project_index.lease_release_failed", {
2262
+ error: error instanceof Error ? error.message : String(error),
2263
+ });
2264
+ }
2265
+ }
2266
+ }
2267
+
2268
+ private syncProjectKnowledge(
2269
+ force = false,
2270
+ ): ReturnType<ProjectKnowledgeManager["sync"]> | undefined {
2271
+ const project = this.projectKnowledge;
2272
+ if (!project) return undefined;
2273
+ const result = this.withProjectIndexLease((checkpoint) => project.sync(force, checkpoint));
2274
+ return result.acquired ? result.value : undefined;
2275
+ }
2276
+
1754
2277
  private refreshProjectIndexIfPending(): void {
1755
2278
  if (this.projectRefreshPending) this.refreshProjectIndex();
1756
2279
  }
1757
2280
 
1758
2281
  private refreshProjectIndex(force = false): void {
1759
2282
  if (!this.projectKnowledge) return;
1760
- this.projectRefreshPending = false;
1761
2283
  try {
1762
- const sync = this.projectKnowledge.sync(force);
2284
+ const sync = this.syncProjectKnowledge(force);
2285
+ if (!sync) {
2286
+ this.projectRefreshPending = true;
2287
+ this.lastProject = this.projectKnowledge.diagnostics(
2288
+ "ready",
2289
+ "Project indexing deferred while another Pi session indexes this project",
2290
+ );
2291
+ this.logger.debug("project_index.deferred", { projectPath: this.projectKnowledge.projectPath });
2292
+ return;
2293
+ }
2294
+ this.projectRefreshPending = false;
1763
2295
  this.lastProject = this.projectKnowledge.diagnostics();
1764
2296
  this.logger.debug("project_index.synced", {
1765
2297
  mode: sync.mode,
@@ -1784,7 +2316,16 @@ export class Ds4ContextRuntime {
1784
2316
  if (!this.projectKnowledge) return this.lastProject;
1785
2317
  if (!requestText) return this.projectKnowledge.diagnostics();
1786
2318
  try {
1787
- const diagnostics = this.projectKnowledge.retrieve(requestText, this.now(), maxTokens);
2319
+ const project = this.projectKnowledge;
2320
+ const timestamp = this.now();
2321
+ const guarded = this.withProjectIndexLease((checkpoint) =>
2322
+ project.retrieve(requestText, timestamp, maxTokens, checkpoint));
2323
+ const diagnostics = guarded.acquired
2324
+ ? guarded.value
2325
+ : project.retrieve(requestText, timestamp, maxTokens, false);
2326
+ if (!guarded.acquired) {
2327
+ this.logger.debug("project_retrieval.validation_deferred", { projectPath: project.projectPath });
2328
+ }
1788
2329
  this.logger.debug("project_retrieval.completed", {
1789
2330
  candidates: diagnostics.candidateCount,
1790
2331
  selected: diagnostics.selected.length,
@@ -1849,14 +2390,20 @@ export class Ds4ContextRuntime {
1849
2390
  }
1850
2391
  }
1851
2392
 
2393
+ private snapshotCanonicalSession(ctx: ExtensionContext): PiSessionSnapshot {
2394
+ const snapshot = snapshotSession(ctx);
2395
+ return { ...snapshot, projectPath: canonicalProjectPath(snapshot.projectPath) };
2396
+ }
2397
+
1852
2398
  syncSessionIndex(ctx: ExtensionContext): SessionIndexResult | undefined {
1853
2399
  if (!this.indexer || this.phase === "disabled" || this.phase === "degraded" || this.phase === "closed") {
1854
2400
  return undefined;
1855
2401
  }
1856
2402
 
1857
- this.session = snapshotSession(ctx);
2403
+ this.session = this.snapshotCanonicalSession(ctx);
1858
2404
  try {
1859
2405
  this.lastIndexResult = this.indexer.sync(this.session);
2406
+ this.retrievalEngine?.syncSemantic(this.session.sessionId);
1860
2407
  this.lastIndexError = undefined;
1861
2408
  return this.lastIndexResult;
1862
2409
  } catch (error) {
@@ -1871,7 +2418,7 @@ export class Ds4ContextRuntime {
1871
2418
 
1872
2419
  rebuildIndex(ctx: ExtensionContext): SessionIndexResult {
1873
2420
  if (!this.indexer || !this.database) throw new Error("Context database is unavailable");
1874
- this.session = snapshotSession(ctx);
2421
+ this.session = this.snapshotCanonicalSession(ctx);
1875
2422
  if (!this.session.sessionFile) throw new Error("Ephemeral Pi sessions do not have a JSONL index");
1876
2423
 
1877
2424
  const result = this.indexer.sync(this.session, true);
@@ -1879,12 +2426,100 @@ export class Ds4ContextRuntime {
1879
2426
  this.lastIndexError = undefined;
1880
2427
  this.refreshProjectIndex(true);
1881
2428
  this.reconcileMemory(ctx, true);
2429
+ this.refreshProjectMemorySources();
1882
2430
  this.rebuildArtifacts(ctx);
1883
2431
  return result;
1884
2432
  }
1885
2433
 
2434
+ private flushContextQuality(): void {
2435
+ const pending = this.pendingQuality.splice(0);
2436
+ for (const item of pending) {
2437
+ this.recordContextQuality(item.manifest, item.contextConfig, item.recordedAt);
2438
+ }
2439
+ }
2440
+
2441
+ private recordContextQuality(
2442
+ manifest: ContextManifest,
2443
+ contextConfig: Ds4ContextConfig["context"],
2444
+ recordedAt: number,
2445
+ ): void {
2446
+ if (!this.config.quality.enabled || !this.database) return;
2447
+ try {
2448
+ const sample = evaluateManifestQuality(manifest, {
2449
+ pin: contextConfig.maxPinnedTokens,
2450
+ memory: contextConfig.maxMemoryTokens,
2451
+ retrieval: contextConfig.maxRetrievedHistoryTokens,
2452
+ project: contextConfig.maxProjectTokens,
2453
+ summary: contextConfig.maxSummaryTokens,
2454
+ recent: manifest.planning?.recentTailTokenLimit ?? contextConfig.recentTailTokens,
2455
+ });
2456
+ this.database.quality.save(sample, recordedAt, this.config.quality.maxSamples);
2457
+ this.lastQualityError = undefined;
2458
+ } catch (error) {
2459
+ this.lastQualityError = error instanceof Error ? error.message : String(error);
2460
+ this.logger.warn("quality.sample_ignored", { error: this.lastQualityError });
2461
+ }
2462
+ }
2463
+
2464
+ private contextQualityDiagnostics(): ContextQualityDiagnostics {
2465
+ const empty = disabledContextQualityDiagnostics();
2466
+ if (!this.config.quality.enabled) return empty;
2467
+ if (!this.database) {
2468
+ return {
2469
+ ...empty,
2470
+ enabled: true,
2471
+ ...(this.lastQualityError ? { lastError: this.lastQualityError } : {}),
2472
+ };
2473
+ }
2474
+ try {
2475
+ const stored = this.database.quality.aggregate(this.config.quality.maxSamples);
2476
+ return {
2477
+ enabled: true,
2478
+ metricsVersion: empty.metricsVersion,
2479
+ storedSamples: stored.samples.length,
2480
+ ignoredSamples: stored.ignoredSamples,
2481
+ aggregate: stored.aggregate,
2482
+ timing: stored.timing,
2483
+ ...(this.lastQualityError ? { lastError: this.lastQualityError } : {}),
2484
+ };
2485
+ } catch (error) {
2486
+ const message = error instanceof Error ? error.message : String(error);
2487
+ return { ...empty, enabled: true, lastError: message };
2488
+ }
2489
+ }
2490
+
2491
+ private runtimeRankingDiagnostics(ctx: ExtensionContext): RuntimeRankingDiagnostics {
2492
+ try {
2493
+ const projection = projectRankingLabels(
2494
+ ctx.sessionManager.getBranch(),
2495
+ this.config.ranking.maxTrainingSamples,
2496
+ );
2497
+ return {
2498
+ ...this.lastRanking,
2499
+ ...(this.rankingModelPath ? { modelPath: this.rankingModelPath } : {}),
2500
+ modelLoaded: Boolean(this.rankingModel),
2501
+ promoted: Boolean(this.rankingModel?.promotion?.eligible),
2502
+ trainingSamples: projection.samples.length,
2503
+ malformedFeedback: projection.malformedEntries,
2504
+ duplicateFeedback: projection.duplicateEntries,
2505
+ warnings: [...new Set([...this.rankingWarnings, ...projection.warnings])],
2506
+ };
2507
+ } catch (error) {
2508
+ return {
2509
+ ...this.lastRanking,
2510
+ ...(this.rankingModelPath ? { modelPath: this.rankingModelPath } : {}),
2511
+ modelLoaded: Boolean(this.rankingModel),
2512
+ promoted: Boolean(this.rankingModel?.promotion?.eligible),
2513
+ trainingSamples: 0,
2514
+ malformedFeedback: 0,
2515
+ duplicateFeedback: 0,
2516
+ warnings: [...this.rankingWarnings, error instanceof Error ? error.message : String(error)],
2517
+ };
2518
+ }
2519
+ }
2520
+
1886
2521
  diagnostics(ctx: ExtensionContext): RuntimeDiagnostics {
1887
- const currentSession = this.session ?? snapshotSession(ctx);
2522
+ const currentSession = this.session ?? this.snapshotCanonicalSession(ctx);
1888
2523
  if (this.artifactManager) {
1889
2524
  this.lastArtifacts = this.artifactManager.diagnostics(
1890
2525
  new Set(ctx.sessionManager.getBranch().map((entry) => entry.id)),
@@ -1906,6 +2541,7 @@ export class Ds4ContextRuntime {
1906
2541
  phase: this.phase,
1907
2542
  enabled: this.config.enabled,
1908
2543
  contextMode: this.config.context.mode,
2544
+ adapter: negotiatePiRuntimeCapabilities(),
1909
2545
  session: currentSession,
1910
2546
  ...(ctx.model ? { model: { provider: ctx.model.provider, id: ctx.model.id } } : {}),
1911
2547
  ...(this.databasePath ? { databasePath: this.databasePath } : {}),
@@ -1919,6 +2555,8 @@ export class Ds4ContextRuntime {
1919
2555
  privacy: this.lastPrivacy,
1920
2556
  ...(this.lastModelAwareness ? { modelAwareness: this.lastModelAwareness } : {}),
1921
2557
  nativeContinuation: this.nativeContinuation.diagnostics(),
2558
+ quality: this.contextQualityDiagnostics(),
2559
+ ranking: this.runtimeRankingDiagnostics(ctx),
1922
2560
  artifacts: this.lastArtifacts,
1923
2561
  compaction: this.getCompactionDiagnostics(ctx),
1924
2562
  ...(this.lastIndexResult ? { lastIndexResult: this.lastIndexResult } : {}),
@@ -1944,6 +2582,7 @@ export class Ds4ContextRuntime {
1944
2582
 
1945
2583
  shutdown(ctx?: ExtensionContext): void {
1946
2584
  if (ctx) this.syncSessionIndex(ctx);
2585
+ this.flushContextQuality();
1947
2586
  this.nativeContinuation.invalidate("session-shutdown");
1948
2587
  this.closeDatabase();
1949
2588
  if (ctx) this.setStatus(ctx, undefined);
@@ -1976,6 +2615,8 @@ export class Ds4ContextRuntime {
1976
2615
  this.projectKnowledge = undefined;
1977
2616
  this.projectRefreshPending = false;
1978
2617
  this.memoryManager = undefined;
2618
+ this.projectMemorySynchronizer = undefined;
2619
+ this.lastCrossSessionMemory = disabledCrossSessionMemoryDiagnostics();
1979
2620
  this.lastMemoryMutationSignature = undefined;
1980
2621
  this.artifactManager = undefined;
1981
2622
  this.indexer = undefined;