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.
- package/README.md +84 -14
- package/docs/ARCHITECTURE.md +42 -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/LOCAL_KV_REUSE.md +131 -0
- package/docs/MEMORY_AND_PINS.md +11 -5
- package/docs/PORTABLE_CORE.md +15 -8
- package/docs/PROJECT_KNOWLEDGE.md +26 -16
- package/docs/RELEASING.md +18 -13
- package/docs/RETRIEVAL.md +1 -1
- package/docs/ROADMAP_0.2.0.md +15 -1
- package/docs/RUNTIME_ADAPTER_KIT.md +145 -0
- package/docs/STORAGE.md +37 -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 +622 -16
- 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
|
@@ -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 {
|
|
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 = (
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
1589
|
-
|
|
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 =
|
|
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.
|
|
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.
|
|
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
|
|
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 =
|
|
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 =
|
|
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 ??
|
|
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;
|