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.
- package/README.md +79 -13
- package/docs/ARCHITECTURE.md +33 -16
- package/docs/CONTEXT_MANIFEST.md +3 -1
- package/docs/CONTEXT_PLANNER.md +9 -1
- package/docs/CONTEXT_QUALITY.md +91 -0
- package/docs/HYBRID_RETRIEVAL.md +117 -0
- package/docs/LEARNED_RANKING.md +90 -0
- package/docs/MEMORY_AND_PINS.md +11 -5
- package/docs/PORTABLE_CORE.md +9 -5
- package/docs/PROJECT_KNOWLEDGE.md +29 -17
- package/docs/RELEASING.md +18 -13
- package/docs/RETRIEVAL.md +1 -1
- package/docs/ROADMAP_0.2.0.md +12 -0
- package/docs/RUNTIME_ADAPTER_KIT.md +143 -0
- package/docs/STORAGE.md +35 -7
- package/package.json +12 -7
- package/quality/corpus-v1.json +208 -0
- package/quality/semantic-corpus-v1.json +84 -0
- package/quality/symbol-corpus-v1.json +38 -0
- package/scripts/compare-context-quality.mjs +23 -0
- package/src/extension/commands.ts +213 -1
- package/src/extension/index.ts +1 -0
- package/src/extension/runtime.ts +658 -17
- package/src/pi-adapter/local-embedding.ts +132 -0
- package/src/pi-adapter/memory-adapter.ts +18 -8
- package/src/pi-adapter/project-memory-sync.ts +499 -0
- package/src/pi-adapter/ranking-adapter.ts +72 -0
- package/src/pi-adapter/runtime-contract.ts +86 -0
- package/src/pi-adapter/version.ts +3 -3
package/src/extension/runtime.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { existsSync, realpathSync } from "node:fs";
|
|
3
|
-
import {
|
|
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 {
|
|
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 = (
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
1571
|
-
|
|
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 =
|
|
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.
|
|
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.
|
|
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
|
|
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 =
|
|
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 =
|
|
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 ??
|
|
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;
|