ds4-context-engine 0.1.2 → 0.2.0-beta.2

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.
@@ -15,6 +15,7 @@ import {
15
15
  type SessionBeforeCompactEvent,
16
16
  type SessionCompactEvent,
17
17
  } from "@earendil-works/pi-coding-agent";
18
+ import type { RuntimeCapabilityNegotiation } from "ds4-context-core/adapter/runtime-adapter";
18
19
  import {
19
20
  ArtifactManager,
20
21
  disabledArtifactDiagnostics,
@@ -42,7 +43,12 @@ export type {
42
43
  CompactionPhase,
43
44
  SummaryGraphDiagnostics,
44
45
  } from "../pi-adapter/compaction-coordinator.ts";
45
- 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";
46
52
  import { createDefaultConfig, type Ds4ContextConfig } from "ds4-context-core/config/config";
47
53
  import { calculateContextBudget, type ContextBudget } from "ds4-context-core/core/budget-manager";
48
54
  import {
@@ -62,6 +68,7 @@ import type {
62
68
  } from "ds4-context-core/manifest/context-manifest";
63
69
  import type { ExcludedContextSource, ObservedTool } from "ds4-context-core/manifest/observer";
64
70
  import {
71
+ disabledCrossSessionMemoryDiagnostics,
65
72
  disabledMemoryDiagnostics,
66
73
  MemoryManager,
67
74
  type MemoryDiagnostics,
@@ -70,14 +77,36 @@ import {
70
77
  import {
71
78
  MEMORY_CUSTOM_ENTRY_TYPE,
72
79
  PIN_CUSTOM_ENTRY_TYPE,
80
+ type CrossSessionMemoryDiagnostics,
73
81
  type MemoryItem,
74
82
  type MemoryMutation,
75
83
  type MemoryScope,
76
84
  type PinItem,
77
85
  type PinMutation,
78
86
  type PinScope,
87
+ type ProjectMemorySource,
79
88
  } from "ds4-context-core/memory/memory-types";
80
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";
81
110
  import {
82
111
  disabledPrivacyDiagnostics,
83
112
  emptyPrivacyCounts,
@@ -90,12 +119,15 @@ import {
90
119
  ProjectKnowledgeManager,
91
120
  type ProjectKnowledgeDiagnostics,
92
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";
93
124
  import {
94
125
  emptyRetrievalDiagnostics,
95
126
  HistoricalRetrievalEngine,
96
127
  type RetrievalDiagnostics,
97
128
  } from "ds4-context-core/retrieval/retrieval-engine";
98
129
  import { currentRequestText } from "ds4-context-core/retrieval/task-descriptor";
130
+ import { LocalFeatureHashEmbedding } from "../pi-adapter/local-embedding.ts";
99
131
  import { ContextDatabase, type DatabaseHealth, type SessionIndexStats } from "ds4-context-core/persistence/sqlite";
100
132
  import {
101
133
  activeTools,
@@ -104,6 +136,9 @@ import {
104
136
  findPiPinnedMessageIndices,
105
137
  } from "../pi-adapter/context-observer.ts";
106
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";
107
142
  import { PiSessionIndexer, type SessionIndexResult } from "../pi-adapter/session-indexer.ts";
108
143
  import { snapshotModel, snapshotSession, type PiSessionSnapshot } from "../pi-adapter/session-reader.ts";
109
144
  import { silentLogger, StructuredLogger, type Logger } from "ds4-context-core/shared/logging";
@@ -118,6 +153,7 @@ import {
118
153
  export type RuntimePhase = "idle" | "initializing" | "disabled" | "observer" | "managed" | "degraded" | "closed";
119
154
 
120
155
  const READ_ONLY_PROJECT_TOOLS = new Set(["read", "grep", "find", "ls"]);
156
+ const MAX_PRIOR_RANKING_OUTCOMES = 50_000;
121
157
 
122
158
  function comparablePath(path: string): string {
123
159
  let canonical: string;
@@ -143,6 +179,8 @@ export interface RuntimeDependencies {
143
179
  now?: () => number;
144
180
  idGenerator?: () => string;
145
181
  logSink?: (line: string) => void;
182
+ /** Optional runtime-owned embedding provider; required for configured remote models. */
183
+ embeddingPort?: EmbeddingPort;
146
184
  }
147
185
 
148
186
  interface PreparedPrivacyContext {
@@ -249,7 +287,30 @@ export interface CreateMemoryInput {
249
287
  sourceEntryIds: string[];
250
288
  }
251
289
 
252
- 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
+ }
253
314
 
254
315
  export interface RuntimeDiagnostics {
255
316
  extensionVersion: string;
@@ -258,6 +319,7 @@ export interface RuntimeDiagnostics {
258
319
  phase: RuntimePhase;
259
320
  enabled: boolean;
260
321
  contextMode: "observer" | "managed";
322
+ adapter: RuntimeCapabilityNegotiation;
261
323
  session?: PiSessionSnapshot;
262
324
  model?: { provider: string; id: string };
263
325
  databasePath?: string;
@@ -271,6 +333,8 @@ export interface RuntimeDiagnostics {
271
333
  privacy: PrivacyDiagnostics;
272
334
  modelAwareness?: ModelAwarenessManifest;
273
335
  nativeContinuation: NativeContinuationDiagnostics;
336
+ quality: ContextQualityDiagnostics;
337
+ ranking: RuntimeRankingDiagnostics;
274
338
  artifacts: ArtifactDiagnostics;
275
339
  compaction: CompactionDiagnostics;
276
340
  lastIndexResult?: SessionIndexResult;
@@ -293,10 +357,15 @@ export class Ds4ContextRuntime {
293
357
  private pendingManifestId?: string;
294
358
  private retrievalEngine?: HistoricalRetrievalEngine;
295
359
  private lastRetrieval: RetrievalDiagnostics = emptyRetrievalDiagnostics();
360
+ private semanticIndex?: SemanticEmbeddingIndex;
361
+ private semanticFallbackReason?: string;
296
362
  private projectKnowledge?: ProjectKnowledgeManager;
297
363
  private lastProject: ProjectKnowledgeDiagnostics = emptyProjectDiagnostics();
298
364
  private projectRefreshPending = false;
365
+ private readonly projectLeaseOwnerId = randomUUID();
299
366
  private memoryManager?: MemoryManager;
367
+ private projectMemorySynchronizer?: ProjectMemorySynchronizer;
368
+ private lastCrossSessionMemory: CrossSessionMemoryDiagnostics = disabledCrossSessionMemoryDiagnostics();
300
369
  private lastMemory: MemoryDiagnostics = disabledMemoryDiagnostics();
301
370
  private privacyEngine?: PrivacyPolicyEngine;
302
371
  private lastPrivacy: PrivacyDiagnostics = disabledPrivacyDiagnostics();
@@ -312,6 +381,18 @@ export class Ds4ContextRuntime {
312
381
  private compaction?: CompactionCoordinator;
313
382
  private lastIndexResult?: SessionIndexResult;
314
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
+ }> = [];
315
396
  private lastError?: string;
316
397
  private logger: Logger = silentLogger;
317
398
  private readonly now: () => number;
@@ -327,14 +408,25 @@ export class Ds4ContextRuntime {
327
408
  }
328
409
 
329
410
  openSession(ctx: ExtensionContext): void {
411
+ this.flushContextQuality();
330
412
  this.closeDatabase();
331
413
  this.phase = "initializing";
332
414
  this.lastError = undefined;
333
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;
334
424
  this.lastIndexResult = undefined;
335
425
  this.lastManifest = undefined;
336
426
  this.pendingManifestId = undefined;
337
427
  this.retrievalEngine = undefined;
428
+ this.semanticIndex = undefined;
429
+ this.semanticFallbackReason = undefined;
338
430
  this.lastRetrieval = emptyRetrievalDiagnostics(
339
431
  this.config.context.maxRetrievedHistoryTokens,
340
432
  this.config.retrieval.maxResults,
@@ -343,6 +435,8 @@ export class Ds4ContextRuntime {
343
435
  this.lastProject = emptyProjectDiagnostics();
344
436
  this.projectRefreshPending = false;
345
437
  this.memoryManager = undefined;
438
+ this.projectMemorySynchronizer = undefined;
439
+ this.lastCrossSessionMemory = disabledCrossSessionMemoryDiagnostics();
346
440
  this.lastMemory = disabledMemoryDiagnostics();
347
441
  this.privacyEngine = undefined;
348
442
  this.lastPrivacy = disabledPrivacyDiagnostics();
@@ -357,7 +451,7 @@ export class Ds4ContextRuntime {
357
451
  this.lastArtifacts = disabledArtifactDiagnostics();
358
452
  this.compaction = undefined;
359
453
  this.observation = undefined;
360
- this.session = snapshotSession(ctx);
454
+ this.session = this.snapshotCanonicalSession(ctx);
361
455
 
362
456
  try {
363
457
  this.loadedConfig = loadConfig({
@@ -401,13 +495,22 @@ export class Ds4ContextRuntime {
401
495
  this.database = ContextDatabase.open(this.databasePath, {
402
496
  logger: this.logger,
403
497
  now: this.now(),
498
+ busyTimeoutMs: this.config.storage.busyTimeoutMs,
499
+ writeRetryTimeoutMs: this.config.storage.writeRetryTimeoutMs,
404
500
  });
501
+ this.initializeRanking();
405
502
 
406
503
  this.indexer = new PiSessionIndexer(this.database.sessionIndex, {
407
504
  logger: this.logger,
408
505
  now: this.now,
409
506
  });
410
- 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
+ );
411
514
  this.lastRetrieval = emptyRetrievalDiagnostics(
412
515
  this.config.context.maxRetrievedHistoryTokens,
413
516
  this.config.retrieval.maxResults,
@@ -692,6 +795,7 @@ export class Ds4ContextRuntime {
692
795
  const preparedPrivacy = this.preparePrivacyContext(event, ctx, pi);
693
796
  if (preparedPrivacy.changed) privacyFallback = { messages: preparedPrivacy.event.messages };
694
797
  this.syncSessionIndex(ctx);
798
+ this.refreshProjectMemorySources();
695
799
  this.refreshProjectIndexIfPending();
696
800
  if (this.config.context.mode !== "managed") {
697
801
  this.lastRetrieval = emptyRetrievalDiagnostics(
@@ -783,6 +887,7 @@ export class Ds4ContextRuntime {
783
887
  let result: { messages?: ContextEvent["messages"] } | undefined = privacyFallback;
784
888
  if (this.config.context.mode === "managed" && budget) {
785
889
  const planningStartedAt = this.now();
890
+ this.rankingCandidates.clear();
786
891
  const requestText = currentRequestText(effectiveEvent.messages);
787
892
  const activeEntryIds = new Set(ctx.sessionManager.getBranch().map((entry) => entry.id));
788
893
  const memorySelection: MemorySelection = this.memoryManager?.select(requestText, activeEntryIds) ?? {
@@ -832,6 +937,10 @@ export class Ds4ContextRuntime {
832
937
  sourceIds: [evidence.item.id],
833
938
  score: 90 + Math.min(0.999999, Math.max(0, evidence.score) / 1_000),
834
939
  reason: evidence.reason,
940
+ rankingFeatures: createRankingFeatures({
941
+ ...evidence.rankingFeatures,
942
+ priorSelected: this.priorRankingSelection.get(`memory:${evidence.item.id}`) ?? 0,
943
+ }),
835
944
  ...(evidence.item.classification ? { classification: evidence.item.classification } : {}),
836
945
  })),
837
946
  ...retrieval.selected.map((evidence) => ({
@@ -841,6 +950,10 @@ export class Ds4ContextRuntime {
841
950
  sourceIds: [evidence.entryId],
842
951
  score: 85 + Math.min(0.999999, Math.max(0, evidence.score) / 1_000),
843
952
  reason: `Historical evidence: ${evidence.reason}`,
953
+ rankingFeatures: createRankingFeatures({
954
+ ...evidence.rankingFeatures,
955
+ priorSelected: this.priorRankingSelection.get(`retrieval:${evidence.entryId}`) ?? 0,
956
+ }),
844
957
  })),
845
958
  ...project.selected.map((evidence) => ({
846
959
  id: `project:${evidence.snippetId}`,
@@ -850,6 +963,10 @@ export class Ds4ContextRuntime {
850
963
  score: 80 + Math.min(0.999999, Math.max(0, evidence.score) / 1_000),
851
964
  reason: `Project source: ${evidence.reason}`,
852
965
  projectSnippet: evidence.manifestRef,
966
+ rankingFeatures: createRankingFeatures({
967
+ ...evidence.rankingFeatures,
968
+ priorSelected: this.priorRankingSelection.get(`project:${evidence.snippetId}`) ?? 0,
969
+ }),
853
970
  })),
854
971
  ];
855
972
  const privacyExcludedSources: ExcludedContextSource[] = [];
@@ -888,13 +1005,45 @@ export class Ds4ContextRuntime {
888
1005
  ...this.lastPrivacy,
889
1006
  excludedSources: this.lastPrivacy.excludedSources + privacyExcludedSources.length,
890
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
+ });
891
1037
  const plan = planManagedContext({
892
1038
  messages: effectiveEvent.messages,
893
1039
  fixedTokens: baseline.composition.systemTokens + baseline.composition.toolTokens,
894
1040
  budget,
895
1041
  config: effectiveContextConfig,
896
1042
  pinnedMessageIndices: findPiPinnedMessageIndices(effectiveEvent, ctx),
897
- supplementalMessages,
1043
+ supplementalMessages: rankedSupplementalMessages,
1044
+ ...(ranking.diagnostics.status === "active"
1045
+ ? { supplementalSelectionOrder: ranking.ranked.map((candidate) => candidate.id) }
1046
+ : {}),
898
1047
  ...(this.config.privacy.enabled
899
1048
  ? {
900
1049
  messageClassifications: preparedPrivacy.messageClassifications,
@@ -902,6 +1051,11 @@ export class Ds4ContextRuntime {
902
1051
  }
903
1052
  : {}),
904
1053
  });
1054
+ if (plan.mode === "managed") {
1055
+ for (const candidate of learnedCandidates) {
1056
+ this.rankingCandidates.set(candidate.id, { ...candidate.features });
1057
+ }
1058
+ }
905
1059
  const selectedPinIds = new Set(
906
1060
  plan.selected.flatMap((metadata) => metadata.kind === "pin" && metadata.sourceId ? [metadata.sourceId] : []),
907
1061
  );
@@ -930,6 +1084,16 @@ export class Ds4ContextRuntime {
930
1084
  plan.selected.flatMap((metadata) => metadata.projectSnippet?.snippetId ? [metadata.projectSnippet.snippetId] : []),
931
1085
  );
932
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
+ }
933
1097
  this.lastProject = {
934
1098
  ...project,
935
1099
  plannerExcludedCount: project.selected.length - plannerSelectedProject.length,
@@ -1004,6 +1168,7 @@ export class Ds4ContextRuntime {
1004
1168
 
1005
1169
  manifest = {
1006
1170
  ...manifest,
1171
+ ranking: { ...this.lastRanking },
1007
1172
  nativeContinuation: this.nativeContinuation.initialManifest(
1008
1173
  manifest.provider,
1009
1174
  manifest.model,
@@ -1029,6 +1194,11 @@ export class Ds4ContextRuntime {
1029
1194
  observedAt,
1030
1195
  ...(budget ? { budget } : {}),
1031
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
+ }
1032
1202
  this.lastManifest = manifest;
1033
1203
  this.pendingManifestId = manifest.id;
1034
1204
  if (this.config.diagnostics.storeContextManifest && this.session?.sessionFile) {
@@ -1333,8 +1503,10 @@ export class Ds4ContextRuntime {
1333
1503
  }
1334
1504
 
1335
1505
  afterAgentSettled(ctx: ExtensionContext): void {
1506
+ this.flushContextQuality();
1336
1507
  if (this.compaction) this.compaction.afterAgentSettled(ctx);
1337
1508
  else this.syncSessionIndex(ctx);
1509
+ this.refreshProjectMemorySources();
1338
1510
  this.refreshProjectIndex();
1339
1511
  }
1340
1512
 
@@ -1368,6 +1540,107 @@ export class Ds4ContextRuntime {
1368
1540
  return this.memoryManager?.listMemories(activeOnly) ?? [];
1369
1541
  }
1370
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
+
1371
1644
  createPin(
1372
1645
  input: CreatePinInput,
1373
1646
  ctx: ExtensionContext,
@@ -1568,33 +1841,66 @@ export class Ds4ContextRuntime {
1568
1841
  }
1569
1842
 
1570
1843
  private initializeMemory(ctx: ExtensionContext): void {
1844
+ this.projectMemorySynchronizer = undefined;
1845
+ this.lastCrossSessionMemory = disabledCrossSessionMemoryDiagnostics();
1571
1846
  if (this.config.context.mode !== "managed" || !this.config.memory.enabled) {
1572
1847
  this.memoryManager = undefined;
1573
1848
  this.lastMemory = disabledMemoryDiagnostics();
1574
1849
  return;
1575
1850
  }
1576
- if (!this.database || !this.session?.sessionFile) {
1851
+ if (!this.database || !this.indexer || !this.session?.sessionFile) {
1577
1852
  this.memoryManager = undefined;
1578
1853
  this.lastMemory = disabledMemoryDiagnostics("Memory and pins require a persisted Pi session");
1579
1854
  return;
1580
1855
  }
1581
1856
  try {
1857
+ const projectPath = canonicalProjectPath(this.session.projectPath);
1858
+ const projectTrusted = ctx.isProjectTrusted();
1582
1859
  this.memoryManager = new MemoryManager(
1583
1860
  this.database.memory,
1584
1861
  this.config.memory,
1585
1862
  this.config.context.maxPinnedTokens,
1586
1863
  this.config.context.maxMemoryTokens,
1587
1864
  this.session.sessionId,
1588
- canonicalProjectPath(this.session.projectPath),
1589
- ctx.isProjectTrusted(),
1865
+ projectPath,
1866
+ projectTrusted,
1590
1867
  this.now,
1591
1868
  this.idGenerator,
1592
1869
  );
1593
1870
  if (existsSync(this.session.sessionFile)) this.reconcileMemory(ctx);
1594
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
+ }
1595
1895
  } catch (error) {
1596
1896
  const message = error instanceof Error ? error.message : String(error);
1597
1897
  this.memoryManager = undefined;
1898
+ this.projectMemorySynchronizer = undefined;
1899
+ this.lastCrossSessionMemory = {
1900
+ ...disabledCrossSessionMemoryDiagnostics(message),
1901
+ enabled: this.config.memory.crossSession,
1902
+ status: "failed",
1903
+ };
1598
1904
  this.lastMemory = {
1599
1905
  ...disabledMemoryDiagnostics(message),
1600
1906
  status: "failed",
@@ -1618,7 +1924,7 @@ export class Ds4ContextRuntime {
1618
1924
  ];
1619
1925
  if (sourceEntryIds.some((entryId) => !this.database?.sessionIndex.hasEntry(this.session!.sessionId, entryId))) {
1620
1926
  if (!this.indexer) throw new Error("Session index is unavailable for memory mutation replay");
1621
- this.session = snapshotSession(ctx);
1927
+ this.session = this.snapshotCanonicalSession(ctx);
1622
1928
  const rebuilt = this.indexer.sync(this.session, true);
1623
1929
  this.lastIndexResult = rebuilt;
1624
1930
  this.lastIndexError = undefined;
@@ -1640,6 +1946,24 @@ export class Ds4ContextRuntime {
1640
1946
  });
1641
1947
  }
1642
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
+
1643
1967
  private requireMemoryManager(): MemoryManager {
1644
1968
  if (!this.memoryManager) throw new Error("DS4 memory and pins are unavailable for this session");
1645
1969
  return this.memoryManager;
@@ -1712,6 +2036,102 @@ export class Ds4ContextRuntime {
1712
2036
  });
1713
2037
  }
1714
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
+
1715
2135
  private initializeProjectKnowledge(ctx: ExtensionContext): void {
1716
2136
  if (!this.config.project.enabled) {
1717
2137
  this.lastProject = emptyProjectDiagnostics(
@@ -1757,8 +2177,22 @@ export class Ds4ContextRuntime {
1757
2177
  this.config.project,
1758
2178
  this.config.context.maxProjectTokens,
1759
2179
  this.now,
2180
+ {
2181
+ enabled: this.config.retrieval.semantic,
2182
+ ...(this.semanticIndex ? { index: this.semanticIndex } : {}),
2183
+ ...(this.semanticFallbackReason ? { fallbackReason: this.semanticFallbackReason } : {}),
2184
+ },
1760
2185
  );
1761
- 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
+ }
1762
2196
  this.lastProject = this.projectKnowledge.diagnostics();
1763
2197
  this.logger.info("project_index.opened", {
1764
2198
  files: sync.discoveredFiles,
@@ -1786,15 +2220,78 @@ export class Ds4ContextRuntime {
1786
2220
  }
1787
2221
  }
1788
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
+
1789
2277
  private refreshProjectIndexIfPending(): void {
1790
2278
  if (this.projectRefreshPending) this.refreshProjectIndex();
1791
2279
  }
1792
2280
 
1793
2281
  private refreshProjectIndex(force = false): void {
1794
2282
  if (!this.projectKnowledge) return;
1795
- this.projectRefreshPending = false;
1796
2283
  try {
1797
- 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;
1798
2295
  this.lastProject = this.projectKnowledge.diagnostics();
1799
2296
  this.logger.debug("project_index.synced", {
1800
2297
  mode: sync.mode,
@@ -1819,7 +2316,16 @@ export class Ds4ContextRuntime {
1819
2316
  if (!this.projectKnowledge) return this.lastProject;
1820
2317
  if (!requestText) return this.projectKnowledge.diagnostics();
1821
2318
  try {
1822
- 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
+ }
1823
2329
  this.logger.debug("project_retrieval.completed", {
1824
2330
  candidates: diagnostics.candidateCount,
1825
2331
  selected: diagnostics.selected.length,
@@ -1884,14 +2390,20 @@ export class Ds4ContextRuntime {
1884
2390
  }
1885
2391
  }
1886
2392
 
2393
+ private snapshotCanonicalSession(ctx: ExtensionContext): PiSessionSnapshot {
2394
+ const snapshot = snapshotSession(ctx);
2395
+ return { ...snapshot, projectPath: canonicalProjectPath(snapshot.projectPath) };
2396
+ }
2397
+
1887
2398
  syncSessionIndex(ctx: ExtensionContext): SessionIndexResult | undefined {
1888
2399
  if (!this.indexer || this.phase === "disabled" || this.phase === "degraded" || this.phase === "closed") {
1889
2400
  return undefined;
1890
2401
  }
1891
2402
 
1892
- this.session = snapshotSession(ctx);
2403
+ this.session = this.snapshotCanonicalSession(ctx);
1893
2404
  try {
1894
2405
  this.lastIndexResult = this.indexer.sync(this.session);
2406
+ this.retrievalEngine?.syncSemantic(this.session.sessionId);
1895
2407
  this.lastIndexError = undefined;
1896
2408
  return this.lastIndexResult;
1897
2409
  } catch (error) {
@@ -1906,7 +2418,7 @@ export class Ds4ContextRuntime {
1906
2418
 
1907
2419
  rebuildIndex(ctx: ExtensionContext): SessionIndexResult {
1908
2420
  if (!this.indexer || !this.database) throw new Error("Context database is unavailable");
1909
- this.session = snapshotSession(ctx);
2421
+ this.session = this.snapshotCanonicalSession(ctx);
1910
2422
  if (!this.session.sessionFile) throw new Error("Ephemeral Pi sessions do not have a JSONL index");
1911
2423
 
1912
2424
  const result = this.indexer.sync(this.session, true);
@@ -1914,12 +2426,100 @@ export class Ds4ContextRuntime {
1914
2426
  this.lastIndexError = undefined;
1915
2427
  this.refreshProjectIndex(true);
1916
2428
  this.reconcileMemory(ctx, true);
2429
+ this.refreshProjectMemorySources();
1917
2430
  this.rebuildArtifacts(ctx);
1918
2431
  return result;
1919
2432
  }
1920
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
+
1921
2521
  diagnostics(ctx: ExtensionContext): RuntimeDiagnostics {
1922
- const currentSession = this.session ?? snapshotSession(ctx);
2522
+ const currentSession = this.session ?? this.snapshotCanonicalSession(ctx);
1923
2523
  if (this.artifactManager) {
1924
2524
  this.lastArtifacts = this.artifactManager.diagnostics(
1925
2525
  new Set(ctx.sessionManager.getBranch().map((entry) => entry.id)),
@@ -1941,6 +2541,7 @@ export class Ds4ContextRuntime {
1941
2541
  phase: this.phase,
1942
2542
  enabled: this.config.enabled,
1943
2543
  contextMode: this.config.context.mode,
2544
+ adapter: negotiatePiRuntimeCapabilities(),
1944
2545
  session: currentSession,
1945
2546
  ...(ctx.model ? { model: { provider: ctx.model.provider, id: ctx.model.id } } : {}),
1946
2547
  ...(this.databasePath ? { databasePath: this.databasePath } : {}),
@@ -1954,6 +2555,8 @@ export class Ds4ContextRuntime {
1954
2555
  privacy: this.lastPrivacy,
1955
2556
  ...(this.lastModelAwareness ? { modelAwareness: this.lastModelAwareness } : {}),
1956
2557
  nativeContinuation: this.nativeContinuation.diagnostics(),
2558
+ quality: this.contextQualityDiagnostics(),
2559
+ ranking: this.runtimeRankingDiagnostics(ctx),
1957
2560
  artifacts: this.lastArtifacts,
1958
2561
  compaction: this.getCompactionDiagnostics(ctx),
1959
2562
  ...(this.lastIndexResult ? { lastIndexResult: this.lastIndexResult } : {}),
@@ -1979,6 +2582,7 @@ export class Ds4ContextRuntime {
1979
2582
 
1980
2583
  shutdown(ctx?: ExtensionContext): void {
1981
2584
  if (ctx) this.syncSessionIndex(ctx);
2585
+ this.flushContextQuality();
1982
2586
  this.nativeContinuation.invalidate("session-shutdown");
1983
2587
  this.closeDatabase();
1984
2588
  if (ctx) this.setStatus(ctx, undefined);
@@ -2011,6 +2615,8 @@ export class Ds4ContextRuntime {
2011
2615
  this.projectKnowledge = undefined;
2012
2616
  this.projectRefreshPending = false;
2013
2617
  this.memoryManager = undefined;
2618
+ this.projectMemorySynchronizer = undefined;
2619
+ this.lastCrossSessionMemory = disabledCrossSessionMemoryDiagnostics();
2014
2620
  this.lastMemoryMutationSignature = undefined;
2015
2621
  this.artifactManager = undefined;
2016
2622
  this.indexer = undefined;