ds4-context-engine 0.3.0-alpha.5 → 0.3.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.
@@ -66,6 +66,11 @@ import type {
66
66
  PrivacyManifest,
67
67
  ProviderUsageManifest,
68
68
  } from "ds4-context-core/manifest/context-manifest";
69
+ import { HARD_PERSISTED_MANIFEST_BYTES } from "ds4-context-core/manifest/context-manifest-storage";
70
+ import {
71
+ unavailableStorageDiagnostics,
72
+ type StorageDiagnostics,
73
+ } from "ds4-context-core/persistence/storage-diagnostics";
69
74
  import type { ExcludedContextSource, ObservedTool } from "ds4-context-core/manifest/observer";
70
75
  import {
71
76
  disabledCrossSessionMemoryDiagnostics,
@@ -133,6 +138,7 @@ import {
133
138
  } from "ds4-context-core/retrieval/retrieval-engine";
134
139
  import { currentRequestText } from "ds4-context-core/retrieval/task-descriptor";
135
140
  import { LocalFeatureHashEmbedding } from "../pi-adapter/local-embedding.ts";
141
+ import { DatabaseProtocolError } from "ds4-context-core/persistence/database-client-lease";
136
142
  import { ContextDatabase, type DatabaseHealth, type SessionIndexStats } from "ds4-context-core/persistence/sqlite";
137
143
  import {
138
144
  activeTools,
@@ -371,6 +377,7 @@ export class Ds4ContextRuntime {
371
377
  private observation?: ContextObservation;
372
378
  private lastManifest?: ContextManifest;
373
379
  private pendingManifestId?: string;
380
+ private pendingManifestPersisted = false;
374
381
  private retrievalEngine?: HistoricalRetrievalEngine;
375
382
  private lastRetrieval: RetrievalDiagnostics = emptyRetrievalDiagnostics();
376
383
  private semanticIndex?: SemanticEmbeddingIndex;
@@ -440,6 +447,7 @@ export class Ds4ContextRuntime {
440
447
  this.lastIndexResult = undefined;
441
448
  this.lastManifest = undefined;
442
449
  this.pendingManifestId = undefined;
450
+ this.pendingManifestPersisted = false;
443
451
  this.retrievalEngine = undefined;
444
452
  this.semanticIndex = undefined;
445
453
  this.semanticFallbackReason = undefined;
@@ -1228,9 +1236,7 @@ export class Ds4ContextRuntime {
1228
1236
  }
1229
1237
  this.lastManifest = manifest;
1230
1238
  this.pendingManifestId = manifest.id;
1231
- if (this.config.diagnostics.storeContextManifest && this.session?.sessionFile) {
1232
- this.database?.manifests.save(manifest);
1233
- }
1239
+ this.pendingManifestPersisted = this.persistManifest(manifest);
1234
1240
 
1235
1241
  this.logger.trace("context.planned", {
1236
1242
  sessionId: this.session?.sessionId,
@@ -1311,6 +1317,47 @@ export class Ds4ContextRuntime {
1311
1317
  }
1312
1318
  }
1313
1319
 
1320
+ private persistManifest(manifest: ContextManifest): boolean {
1321
+ if (!this.config.diagnostics.storeContextManifest || !this.session?.sessionFile || !this.database) {
1322
+ return false;
1323
+ }
1324
+ try {
1325
+ const result = this.database.manifests.save(manifest);
1326
+ if (result.status === "skipped-oversize") {
1327
+ this.logger.warn("context.manifest_persistence_skipped", {
1328
+ category: "oversize",
1329
+ sourceBytes: result.sourceBytes,
1330
+ projectedBytes: result.projectedBytes,
1331
+ includedCount: manifest.included.length,
1332
+ excludedCount: manifest.excluded.length,
1333
+ hardLimitBytes: HARD_PERSISTED_MANIFEST_BYTES,
1334
+ });
1335
+ return false;
1336
+ }
1337
+ this.logger.debug("context.manifest_persisted", {
1338
+ manifestId: manifest.id,
1339
+ completeness: result.completeness,
1340
+ sourceBytes: result.sourceBytes,
1341
+ storedBytes: result.storedBytes,
1342
+ });
1343
+ if (result.prunedManifests > 0) {
1344
+ this.logger.debug("database.retention_progress", {
1345
+ category: "context-manifest",
1346
+ prunedRows: result.prunedManifests,
1347
+ prunedBytes: result.prunedBytes,
1348
+ });
1349
+ }
1350
+ return true;
1351
+ } catch (error) {
1352
+ this.logger.warn("context.manifest_persistence_skipped", {
1353
+ category: "storage-write",
1354
+ manifestId: manifest.id,
1355
+ errorType: error instanceof Error ? error.name : "UnknownError",
1356
+ });
1357
+ return false;
1358
+ }
1359
+ }
1360
+
1314
1361
  private updateManifestPrivacy(): void {
1315
1362
  if (!this.lastManifest?.privacy) return;
1316
1363
  const updatedPrivacy = this.privacyManifest(
@@ -1322,8 +1369,9 @@ export class Ds4ContextRuntime {
1322
1369
  );
1323
1370
  if (!updatedPrivacy) return;
1324
1371
  this.lastManifest = { ...this.lastManifest, privacy: updatedPrivacy };
1325
- if (this.config.diagnostics.storeContextManifest && this.session?.sessionFile) {
1326
- this.database?.manifests.save(this.lastManifest);
1372
+ const persisted = this.persistManifest(this.lastManifest);
1373
+ if (this.pendingManifestId === this.lastManifest.id) {
1374
+ this.pendingManifestPersisted = this.pendingManifestPersisted || persisted;
1327
1375
  }
1328
1376
  }
1329
1377
 
@@ -1453,14 +1501,9 @@ export class Ds4ContextRuntime {
1453
1501
  ...this.lastManifest,
1454
1502
  nativeContinuation: { ...continuation },
1455
1503
  };
1456
- if (!this.config.diagnostics.storeContextManifest || !this.session?.sessionFile) return;
1457
- try {
1458
- this.database?.manifests.save(this.lastManifest);
1459
- } catch (error) {
1460
- this.logger.warn("native_continuation.manifest_persistence_failed", {
1461
- manifestId,
1462
- errorType: error instanceof Error ? error.name : "UnknownError",
1463
- });
1504
+ const persisted = this.persistManifest(this.lastManifest);
1505
+ if (this.pendingManifestId === manifestId) {
1506
+ this.pendingManifestPersisted = this.pendingManifestPersisted || persisted;
1464
1507
  }
1465
1508
  }
1466
1509
 
@@ -1470,7 +1513,9 @@ export class Ds4ContextRuntime {
1470
1513
  if (record.role !== "assistant") return;
1471
1514
 
1472
1515
  const manifestId = this.pendingManifestId;
1516
+ const manifestPersisted = this.pendingManifestPersisted;
1473
1517
  this.pendingManifestId = undefined;
1518
+ this.pendingManifestPersisted = false;
1474
1519
  if (record.stopReason === "error" || record.stopReason === "aborted") return;
1475
1520
  const usage = record.usage;
1476
1521
  if (!usage || typeof usage !== "object") return;
@@ -1491,19 +1536,17 @@ export class Ds4ContextRuntime {
1491
1536
  };
1492
1537
  }
1493
1538
  const createdAt = this.now();
1494
- const persisted = Boolean(
1495
- this.config.diagnostics.storeContextManifest
1496
- && this.session?.sessionFile
1497
- && this.database,
1498
- );
1499
1539
  try {
1500
- if (persisted) {
1501
- const updated = this.database?.manifests.recordProviderUsage(
1540
+ if (manifestPersisted && this.database) {
1541
+ const updated = this.database.manifests.recordProviderUsage(
1502
1542
  manifestId,
1503
1543
  providerUsage,
1504
1544
  createdAt,
1545
+ manifest?.modelAwareness?.calibration.estimator ?? "chars-v1",
1505
1546
  );
1506
- if (updated && this.lastManifest?.id === manifestId) this.lastManifest = updated;
1547
+ if (!updated && manifest?.estimatedInputTokens) {
1548
+ this.rememberVolatileCalibration(manifest, providerUsage, createdAt);
1549
+ }
1507
1550
  } else if (manifest?.estimatedInputTokens) {
1508
1551
  this.rememberVolatileCalibration(manifest, providerUsage, createdAt);
1509
1552
  }
@@ -1513,7 +1556,8 @@ export class Ds4ContextRuntime {
1513
1556
  }
1514
1557
  this.logger.warn("context.usage_persistence_failed", {
1515
1558
  manifestId,
1516
- error: error instanceof Error ? error.message : String(error),
1559
+ category: "storage-write",
1560
+ errorType: error instanceof Error ? error.name : "UnknownError",
1517
1561
  });
1518
1562
  }
1519
1563
  this.logger.debug("context.actual_usage_recorded", {
@@ -2913,6 +2957,11 @@ export class Ds4ContextRuntime {
2913
2957
  return this.database.health();
2914
2958
  }
2915
2959
 
2960
+ storageDiagnostics(): StorageDiagnostics {
2961
+ return this.database?.storageDiagnostics(this.session?.projectPath)
2962
+ ?? unavailableStorageDiagnostics();
2963
+ }
2964
+
2916
2965
  shutdown(ctx?: ExtensionContext): void {
2917
2966
  if (ctx) this.syncSessionIndex(ctx);
2918
2967
  this.flushContextQuality();
@@ -2931,8 +2980,16 @@ export class Ds4ContextRuntime {
2931
2980
  this.nativeContinuation.invalidate("runtime-degraded");
2932
2981
  this.closeDatabase();
2933
2982
  this.phase = "degraded";
2934
- this.lastError = error instanceof Error ? error.message : String(error);
2935
- this.logger.error("runtime.degraded", { stage, error: this.lastError });
2983
+ const maintenanceActive = error instanceof DatabaseProtocolError && error.category === "maintenance-active";
2984
+ const errorType = error instanceof Error ? error.name : "UnknownError";
2985
+ this.lastError = maintenanceActive
2986
+ ? "Database maintenance is active (category=maintenance-active)"
2987
+ : `Database startup unavailable (category=storage-unavailable; errorType=${errorType})`;
2988
+ if (maintenanceActive) {
2989
+ this.logger.warn("database.maintenance_active", { category: "maintenance-active" });
2990
+ } else {
2991
+ this.logger.error("runtime.degraded", { stage, category: "storage-unavailable", errorType });
2992
+ }
2936
2993
  this.setStatus(ctx, "DS4 ctx: Pi fallback");
2937
2994
  if (ctx.hasUI) {
2938
2995
  ctx.ui.notify(`DS4 Context Engine unavailable; using Pi context. ${this.lastError}`, "warning");
@@ -81,6 +81,7 @@ export interface CompactionDiagnostics {
81
81
  sourcePromptTokens?: number;
82
82
  segmentCount?: number;
83
83
  aggregateCalls?: number;
84
+ transportRetries?: number;
84
85
  contextTokens?: number;
85
86
  softLimitTokens?: number;
86
87
  proactiveThresholdTokens?: number;
@@ -224,6 +225,7 @@ export class CompactionCoordinator {
224
225
  requestedAt,
225
226
  firstKeptEntryId: event.preparation.firstKeptEntryId,
226
227
  tokensBefore: event.preparation.tokensBefore,
228
+ transportRetries: 0,
227
229
  };
228
230
 
229
231
  try {
@@ -357,6 +359,7 @@ export class CompactionCoordinator {
357
359
  sourcePromptTokens: wholePlan.promptTokens,
358
360
  segmentCount: segmentPlans.length,
359
361
  aggregateCalls: aggregated.aggregateCalls,
362
+ transportRetries: this.state.transportRetries ?? 0,
360
363
  };
361
364
  this.dependencies.logger.debug("compaction.summary_graph_prepared", {
362
365
  activeSummaryId: activeNode.id,
@@ -369,6 +372,7 @@ export class CompactionCoordinator {
369
372
  sourcePromptTokens: wholePlan.promptTokens,
370
373
  segmentCount: segmentPlans.length,
371
374
  aggregateCalls: aggregated.aggregateCalls,
375
+ transportRetries: this.state.transportRetries ?? 0,
372
376
  trigger,
373
377
  });
374
378
  const activeRecord = records.at(-1);
@@ -435,6 +439,7 @@ export class CompactionCoordinator {
435
439
  ...(attempt.sourcePromptTokens !== undefined ? { sourcePromptTokens: attempt.sourcePromptTokens } : {}),
436
440
  ...(attempt.segmentCount !== undefined ? { segmentCount: attempt.segmentCount } : {}),
437
441
  ...(attempt.aggregateCalls !== undefined ? { aggregateCalls: attempt.aggregateCalls } : {}),
442
+ ...(attempt.transportRetries !== undefined ? { transportRetries: attempt.transportRetries } : {}),
438
443
  ...(customError ? { lastError: `Custom fallback: ${customError}` } : {}),
439
444
  };
440
445
  return;
@@ -475,6 +480,9 @@ export class CompactionCoordinator {
475
480
  : {}),
476
481
  ...(this.state.segmentCount !== undefined ? { segmentCount: this.state.segmentCount } : {}),
477
482
  ...(this.state.aggregateCalls !== undefined ? { aggregateCalls: this.state.aggregateCalls } : {}),
483
+ ...(this.state.transportRetries !== undefined
484
+ ? { transportRetries: this.state.transportRetries }
485
+ : {}),
478
486
  };
479
487
  this.dependencies.logger.debug("compaction.summary_graph_committed", {
480
488
  activeSummaryId: metadata.summaryId,
@@ -950,6 +958,10 @@ export class CompactionCoordinator {
950
958
  validate: this.dependencies.config.compaction.validate,
951
959
  maxSummaryTokens: this.dependencies.config.context.maxSummaryTokens,
952
960
  now: this.dependencies.now,
961
+ onTransportRetry: (diagnostic) => {
962
+ this.state.transportRetries = (this.state.transportRetries ?? 0) + 1;
963
+ this.dependencies.logger.debug("compaction.transport_retry", { ...diagnostic });
964
+ },
953
965
  });
954
966
  }
955
967
 
@@ -12,6 +12,17 @@ import {
12
12
  type SummaryValidationResult,
13
13
  } from "ds4-context-core/compaction/summary-contract";
14
14
 
15
+ export const COMPACTION_TRANSPORT_MAX_ATTEMPTS = 3;
16
+ const COMPACTION_TRANSPORT_RETRY_DELAYS_MS = [200, 500] as const;
17
+
18
+ export interface CompactionTransportRetryDiagnostic {
19
+ stage: "segment" | "aggregate";
20
+ failedAttempt: number;
21
+ nextAttempt: number;
22
+ maxAttempts: number;
23
+ delayMs: number;
24
+ }
25
+
15
26
  export interface GenerateValidatedSummaryInput {
16
27
  stage: "segment" | "aggregate";
17
28
  prompt: string;
@@ -23,6 +34,7 @@ export interface GenerateValidatedSummaryInput {
23
34
  event: SessionBeforeCompactEvent;
24
35
  ctx: ExtensionContext;
25
36
  now: () => number;
37
+ onTransportRetry?: (diagnostic: CompactionTransportRetryDiagnostic) => void;
26
38
  }
27
39
 
28
40
  export interface GeneratedSummary {
@@ -55,60 +67,134 @@ function responseErrorMessage(response: unknown): string | undefined {
55
67
  return typeof response.errorMessage === "string" ? response.errorMessage : undefined;
56
68
  }
57
69
 
58
- function providerFailureCategory(value: unknown): string {
70
+ type ProviderFailureCategory =
71
+ | "aborted"
72
+ | "usage-limit"
73
+ | "rate-limit"
74
+ | "input-limit"
75
+ | "authentication"
76
+ | "transport"
77
+ | "provider-error";
78
+
79
+ function providerFailureCategory(value: unknown): ProviderFailureCategory {
59
80
  const message = value instanceof Error
60
81
  ? value.message
61
82
  : typeof value === "string"
62
83
  ? value
63
84
  : "";
64
- if (/abort|cancel/iu.test(message)) return "aborted";
85
+ if (value instanceof Error && value.name === "AbortError") return "aborted";
65
86
  if (/usage|quota|credit|billing/iu.test(message)) return "usage-limit";
66
87
  if (/rate|too many requests|429/iu.test(message)) return "rate-limit";
67
88
  if (/(?:context|prompt|input).{0,48}(?:exceed|limit|maximum|too (?:long|large)|tokens?)|tokens?.{0,48}(?:exceed|limit|maximum|too many)|maximum (?:context|input|prompt|length)/iu.test(message)) {
68
89
  return "input-limit";
69
90
  }
70
91
  if (/auth|credential|api.?key|permission|forbidden|401|403/iu.test(message)) return "authentication";
71
- if (/timeout|network|connection|socket|dns/iu.test(message)) return "transport";
92
+ if (/timeout|timed out|network|connection|socket|dns|fetch failed|econn(?:reset|refused|aborted)|etimedout|eai_again|enotfound|und_err/iu.test(message)) {
93
+ return "transport";
94
+ }
95
+ if (/abort|cancel/iu.test(message)) return "aborted";
72
96
  return "provider-error";
73
97
  }
74
98
 
99
+ function abortedError(): Error {
100
+ return new Error("Compaction summary generation aborted");
101
+ }
102
+
103
+ async function waitForTransportRetry(signal: AbortSignal, milliseconds: number): Promise<void> {
104
+ if (signal.aborted) throw abortedError();
105
+ await new Promise<void>((resolve, reject) => {
106
+ const onAbort = (): void => {
107
+ clearTimeout(timer);
108
+ reject(abortedError());
109
+ };
110
+ const timer = setTimeout(() => {
111
+ signal.removeEventListener("abort", onAbort);
112
+ resolve();
113
+ }, milliseconds);
114
+ signal.addEventListener("abort", onAbort, { once: true });
115
+ });
116
+ }
117
+
118
+ function transportFailureSuffix(category: ProviderFailureCategory, attempts: number): string {
119
+ return category === "transport"
120
+ ? `category=${category}; attempts=${attempts}`
121
+ : `category=${category}`;
122
+ }
123
+
75
124
  export async function generateValidatedSummary(
76
125
  input: GenerateValidatedSummaryInput,
77
126
  ): Promise<GeneratedSummary> {
78
127
  const model = input.ctx.model;
79
128
  if (!model) throw new Error("Compaction summary generation requires an active model");
80
129
  const maxTokens = Math.max(1, Math.min(input.maxSummaryTokens, model.maxTokens ?? input.maxSummaryTokens));
130
+ const retryUsages: Usage[] = [];
81
131
  let response: Awaited<ReturnType<typeof input.ctx.modelRegistry.complete>>;
82
- try {
83
- response = await input.ctx.modelRegistry.complete(
84
- model,
85
- {
86
- messages: [{
87
- role: "user",
88
- content: [{ type: "text", text: input.prompt }],
89
- timestamp: input.now(),
90
- }],
91
- },
92
- {
93
- maxTokens,
94
- signal: input.event.signal,
95
- cacheRetention: "none",
96
- sessionId: randomUUID(),
97
- },
98
- );
99
- } catch (error) {
100
- throw new Error(
101
- `Compaction ${input.stage} request failed (category=${providerFailureCategory(error)})`,
102
- );
103
- }
104
- if (input.event.signal.aborted) throw new Error("Compaction summary generation aborted");
105
- const stopReason = responseStopReason(response);
106
- if (stopReason === "length") throw new Error("Compaction summary hit the model output limit");
107
- if (stopReason === "error" || stopReason === "aborted") {
108
- const category = stopReason === "aborted"
109
- ? "aborted"
110
- : providerFailureCategory(responseErrorMessage(response));
111
- throw new Error(`Compaction ${input.stage} summary stopped with ${stopReason} (category=${category})`);
132
+ let attempt = 0;
133
+ for (;;) {
134
+ if (input.event.signal.aborted) throw abortedError();
135
+ attempt++;
136
+ try {
137
+ response = await input.ctx.modelRegistry.complete(
138
+ model,
139
+ {
140
+ messages: [{
141
+ role: "user",
142
+ content: [{ type: "text", text: input.prompt }],
143
+ timestamp: input.now(),
144
+ }],
145
+ },
146
+ {
147
+ maxTokens,
148
+ signal: input.event.signal,
149
+ cacheRetention: "none",
150
+ sessionId: randomUUID(),
151
+ },
152
+ );
153
+ } catch (error) {
154
+ if (input.event.signal.aborted) throw abortedError();
155
+ const category = providerFailureCategory(error);
156
+ if (category !== "transport" || attempt >= COMPACTION_TRANSPORT_MAX_ATTEMPTS) {
157
+ throw new Error(
158
+ `Compaction ${input.stage} request failed (${transportFailureSuffix(category, attempt)})`,
159
+ );
160
+ }
161
+ const delayMs = COMPACTION_TRANSPORT_RETRY_DELAYS_MS[attempt - 1] ?? 500;
162
+ await waitForTransportRetry(input.event.signal, delayMs);
163
+ input.onTransportRetry?.({
164
+ stage: input.stage,
165
+ failedAttempt: attempt,
166
+ nextAttempt: attempt + 1,
167
+ maxAttempts: COMPACTION_TRANSPORT_MAX_ATTEMPTS,
168
+ delayMs,
169
+ });
170
+ continue;
171
+ }
172
+ if (input.event.signal.aborted) throw abortedError();
173
+ const stopReason = responseStopReason(response);
174
+ if (stopReason === "error") {
175
+ const category = providerFailureCategory(responseErrorMessage(response));
176
+ if (category === "transport" && attempt < COMPACTION_TRANSPORT_MAX_ATTEMPTS) {
177
+ retryUsages.push(response.usage);
178
+ const delayMs = COMPACTION_TRANSPORT_RETRY_DELAYS_MS[attempt - 1] ?? 500;
179
+ await waitForTransportRetry(input.event.signal, delayMs);
180
+ input.onTransportRetry?.({
181
+ stage: input.stage,
182
+ failedAttempt: attempt,
183
+ nextAttempt: attempt + 1,
184
+ maxAttempts: COMPACTION_TRANSPORT_MAX_ATTEMPTS,
185
+ delayMs,
186
+ });
187
+ continue;
188
+ }
189
+ throw new Error(
190
+ `Compaction ${input.stage} summary stopped with error (${transportFailureSuffix(category, attempt)})`,
191
+ );
192
+ }
193
+ if (stopReason === "aborted") {
194
+ throw new Error(`Compaction ${input.stage} summary stopped with aborted (category=aborted)`);
195
+ }
196
+ if (stopReason === "length") throw new Error("Compaction summary hit the model output limit");
197
+ break;
112
198
  }
113
199
  if (response.content.some((block) => block.type === "toolCall")) {
114
200
  throw new Error("Compaction summarizer attempted to call a tool");
@@ -172,7 +258,7 @@ export async function generateValidatedSummary(
172
258
  : "";
173
259
  throw new Error(`Compaction ${input.stage} summary validation failed: ${codes}${repairDiagnostics}`);
174
260
  }
175
- return { content, validation, usage: response.usage };
261
+ return { content, validation, usage: sumUsage([...retryUsages, response.usage]) };
176
262
  }
177
263
 
178
264
  export function sumUsage(usages: readonly Usage[]): Usage {
@@ -1,4 +1,4 @@
1
- export const EXTENSION_VERSION = "0.3.0-alpha.5";
1
+ export const EXTENSION_VERSION = "0.3.0-beta.2";
2
2
  export const SUPPORTED_PI_VERSION = "0.84.3";
3
3
  export const OBSERVER_PLANNER_VERSION = "observer-model-aware-v1";
4
4
  export const PLANNER_VERSION = "managed-learned-ranking-v1";