ds4-context-engine 0.3.0-alpha.4 → 0.3.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.
@@ -0,0 +1,81 @@
1
+ # DS4 Context Engine 0.3.0-beta.1
2
+
3
+ Status: release candidate; publication and exact registry verification pending.
4
+
5
+ This coordinated beta contains SQLite growth caused by persisted Context Manifests and adds explicit, recoverable offline maintenance for already-large derived databases. It also hardens compaction provider retries while preserving strict grounding, privacy filtering, canonical Pi JSONL semantics, atomic tool exchanges, one final Pi compaction entry, and fail-open delegation to Pi.
6
+
7
+ ## Added
8
+
9
+ - Retains at most 128 Context Manifests globally and at most 200 calibration samples per exact provider/model/estimator profile.
10
+ - Bounds each online manifest prune to 32 rows and 8 MiB, while allowing one individually oversized oldest row to be removed so convergence cannot stall.
11
+ - Persists complete manifests up to 256 KiB; larger manifests retain complete included provenance and a deterministic excluded-only rollup with complete counts, aggregates, and digests plus at most 256 sampled excluded details.
12
+ - Skips manifest persistence above the 1 MiB hard bound without altering the active in-memory manifest or provider request.
13
+ - Exposes persisted inventory completeness through `getStored()` while legacy complete rows remain readable through `get()`.
14
+ - Adds metadata-only `/context storage` diagnostics and storage high-water integration in `/context health`.
15
+ - Adds cooperative database client leases and a two-phase maintenance lock; active or ambiguous clients block physical maintenance.
16
+ - Adds the `ds4-context-storage inspect|compact|recover --database <exact-path>` CLI. Mutating commands require a local TTY and have no non-interactive bypass.
17
+ - Adds standalone backup, working-copy transformation, validated `VACUUM INTO` candidate, protected-table full-row digests, recoverable swap state, and deterministic crash recovery.
18
+
19
+ ## Changed
20
+
21
+ - Provider usage is authoritative in scalar Context Manifest columns; `message_end` no longer rewrites `manifest_json`.
22
+ - Only compaction failures categorized as `transport` are retried, with at most three attempts, abort-aware 200 ms and 500 ms delays, fresh routing session IDs, and cumulative usage accounting.
23
+ - Runtime startup during maintenance degrades safely to Pi fallback with categorical diagnostics rather than exposing raw SQLite messages.
24
+ - SQLite lock timeout and rollback diagnostics expose bounded categories, operation names, attempt counts, and numeric SQLite codes rather than raw database errors.
25
+ - Package verification rejects database, WAL, SHM, backup, staging, retired-swap, maintenance-state, client-lease, JSONL, `.pi`, and `.serena` paths.
26
+
27
+ ## Safety and compatibility
28
+
29
+ Pi JSONL remains canonical and append-only. Physical maintenance never edits session history or project files and is unavailable to model-callable tools. Every `context_persistence` write still requires a fresh positive local UI decision, exact destructive targeting remains revision-bound, and canonical Pin/Memory mutations still append through Pi before SQLite reconciliation.
30
+
31
+ An individually oversized compaction group, unsupported exact value, non-transport provider failure, abort, or bounded-operation failure still delegates to Pi default compaction. Rejected values and provider payloads are not emitted in diagnostics.
32
+
33
+ SQLite schema remains 15. Migrations 1–15, `ds4-context-config-v1`, `runtime-adapter-v1`, `ds4-context-persistence-tool-v1`, and `ds4-context-persistence-result-v1` are unchanged. Earlier schema-15 readers can parse additive rollup metadata but may present sampled excluded details as complete, so historical excluded-inventory rendering is unsupported after downgrade once rollups have been written.
34
+
35
+ ## Package/version policy
36
+
37
+ The coordinated version is `0.3.0-beta.1` for:
38
+
39
+ ```text
40
+ ds4-context-core
41
+ ds4-context-reference-adapter
42
+ ds4-context-engine
43
+ ```
44
+
45
+ Both adapters depend exactly on `ds4-context-core@0.3.0-beta.1`. Publication uses the explicit npm `beta` dist-tag for all three packages while `latest` must remain stable `0.2.0`. GitHub Actions remains validation-only with OIDC and package-write permissions denied.
46
+
47
+ ## Candidate validation evidence
48
+
49
+ Local candidate verification on Node.js `26.5.1`:
50
+
51
+ - `npm run check`: 69 files and 341 tests passed.
52
+ - Three consecutive focused multi-process SQLite concurrency runs passed.
53
+ - `npm run quality:compare`: candidate quality `0.9875` versus baseline `0.808156`.
54
+ - `npm run schema:context-persistence`: 1,266 bytes and 317 estimated tokens; below the 1,500 absolute and 320 relative limits.
55
+ - `npm run latency:check -- <exact ds4-context-core@0.1.2>`: passed after clean install with ratio `0.93982`, at or below `1.10`, using 200 samples and 50 calls per sample.
56
+ - `npm run pack:check`: verified core (223 files), reference adapter (7 files), and Pi adapter (67 files) in a clean consumer.
57
+ - `npm pack --dry-run --json` for all three packages passed with the same inventories and no forbidden local/session/storage files.
58
+ - `git diff --check`: passed.
59
+ - Version, exact core dependencies, package-lock entries, extension constant, and reference-adapter constant are synchronized to `0.3.0-beta.1`.
60
+ - Protected compatibility golden, Pi JSONL fixture, migration definitions, canonical persistence contracts, and CI publication permissions are unchanged.
61
+
62
+ Large-database validation used a temporary private copy created from a read-only source connection; the live database was not compacted, vacuumed, rebuilt, replaced, or maintained:
63
+
64
+ - before: schema 15, `quick_check=ok`, 3,691 manifests, 2,101,666,663 manifest bytes, 2,489,868,288 database bytes;
65
+ - after: 128 manifests, 15,626,529 manifest bytes, 414 calibration samples across three profiles, 389,746,688 database bytes;
66
+ - all 128 retained oversized manifests were rewritten as explicit `excluded-rollup` projections with no irreducible oversize;
67
+ - the final candidate passed quick check and foreign-key validation while the standalone backup remained present;
68
+ - temporary copy, backup, and stage files were removed afterward, with no maintenance artifact created next to the live database or in the repository.
69
+
70
+ Publication, exact registry verification, annotated tag, and GitHub prerelease are performed only after the committed candidate passes validation-only CI.
71
+
72
+ ## Documentation
73
+
74
+ - [`../CONTEXT_MANIFEST.md`](../CONTEXT_MANIFEST.md)
75
+ - [`../STORAGE.md`](../STORAGE.md)
76
+ - [`../STORAGE_MAINTENANCE.md`](../STORAGE_MAINTENANCE.md)
77
+ - [`../COMPACTION.md`](../COMPACTION.md)
78
+ - [`../ARCHITECTURE.md`](../ARCHITECTURE.md)
79
+ - [`../DOGFOODING_0.3.0_BETA.md`](../DOGFOODING_0.3.0_BETA.md)
80
+ - [`../RELEASING.md`](../RELEASING.md)
81
+ - [`0.3.0-alpha.5.md`](0.3.0-alpha.5.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ds4-context-engine",
3
- "version": "0.3.0-alpha.4",
3
+ "version": "0.3.0-beta.1",
4
4
  "description": "Non-destructive, provider-independent context management for Pi.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -31,12 +31,16 @@
31
31
  "docs",
32
32
  "quality",
33
33
  "scripts/compare-context-quality.mjs",
34
+ "scripts/ds4-context-storage.mjs",
34
35
  "README.md",
35
36
  "LICENSE"
36
37
  ],
37
38
  "exports": {
38
39
  ".": "./src/extension/index.ts"
39
40
  },
41
+ "bin": {
42
+ "ds4-context-storage": "./scripts/ds4-context-storage.mjs"
43
+ },
40
44
  "scripts": {
41
45
  "build:core": "npm run build --workspace ds4-context-core",
42
46
  "build:reference-adapter": "npm run build --workspace ds4-context-reference-adapter",
@@ -58,7 +62,7 @@
58
62
  ]
59
63
  },
60
64
  "dependencies": {
61
- "ds4-context-core": "0.3.0-alpha.4"
65
+ "ds4-context-core": "0.3.0-beta.1"
62
66
  },
63
67
  "peerDependencies": {
64
68
  "@earendil-works/pi-ai": "0.84.3",
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env node
2
+ import { createInterface } from "node:readline/promises";
3
+ import { stdin, stdout } from "node:process";
4
+ import { DatabaseProtocolError } from "ds4-context-core/persistence/database-client-lease";
5
+ import {
6
+ StorageMaintenanceError,
7
+ compactStorage,
8
+ inspectStorage,
9
+ recoverStorage,
10
+ storageMaintenancePaths,
11
+ } from "ds4-context-core/persistence/storage-maintenance";
12
+
13
+ function usage() {
14
+ return [
15
+ "Usage:",
16
+ " ds4-context-storage inspect --database <exact-path>",
17
+ " ds4-context-storage compact --database <exact-path>",
18
+ " ds4-context-storage recover --database <exact-path>",
19
+ "",
20
+ "compact and recover require an interactive local TTY confirmation.",
21
+ ].join("\n");
22
+ }
23
+
24
+ function parseArguments(argv) {
25
+ const command = argv[0];
26
+ if (command !== "inspect" && command !== "compact" && command !== "recover") {
27
+ throw new Error("usage");
28
+ }
29
+ let database;
30
+ for (let index = 1; index < argv.length; index++) {
31
+ const argument = argv[index];
32
+ if (argument !== "--database" || database !== undefined || !argv[index + 1]) {
33
+ throw new Error("usage");
34
+ }
35
+ database = argv[++index];
36
+ }
37
+ if (!database) throw new Error("usage");
38
+ return { command, database };
39
+ }
40
+
41
+ function formatBytes(value) {
42
+ const units = ["B", "KiB", "MiB", "GiB", "TiB"];
43
+ let scaled = value;
44
+ let unit = 0;
45
+ while (scaled >= 1024 && unit < units.length - 1) {
46
+ scaled /= 1024;
47
+ unit++;
48
+ }
49
+ const digits = unit === 0 ? 0 : scaled >= 100 ? 0 : scaled >= 10 ? 1 : 2;
50
+ return `${scaled.toFixed(digits)} ${units[unit]}`;
51
+ }
52
+
53
+ function printInspection(inspection) {
54
+ const storage = inspection.diagnostics;
55
+ console.log([
56
+ "DS4 Context Storage Inspection",
57
+ "",
58
+ `Database: ${inspection.databasePath}`,
59
+ `Schema / journal: ${inspection.schemaVersion} / ${storage.journalMode ?? "n/a"}`,
60
+ `quick_check / foreign keys: ${inspection.quickCheck} / ${inspection.foreignKeyViolations}`,
61
+ `Database / WAL: ${formatBytes(storage.databaseBytes ?? 0)} / ${formatBytes(storage.walBytes ?? 0)}`,
62
+ `Allocated / reusable: ${formatBytes(storage.allocatedBytes ?? 0)} / ${formatBytes(storage.reusableBytes ?? 0)}`,
63
+ `Manifests / target: ${storage.manifests.rows} / ${storage.manifests.retainedLimit}`,
64
+ `Manifest payload: ${formatBytes(storage.manifests.serializedBytes)}`,
65
+ `Manifests to prune: ${inspection.manifestsToPrune}`,
66
+ `Calibration to prune: ${inspection.calibrationToPrune}`,
67
+ `Available / required space: ${formatBytes(inspection.availableBytes)} / ${formatBytes(inspection.requiredBytes)}`,
68
+ `Backup: ${inspection.paths.backup}`,
69
+ `Maintenance recommended: ${storage.maintenance.recommended ? "yes" : "no"}`,
70
+ ...storage.maintenance.reasons.map((reason) => `Reason: ${reason}`),
71
+ ].join("\n"));
72
+ }
73
+
74
+ async function confirm(action, database) {
75
+ if (!stdin.isTTY || !stdout.isTTY) {
76
+ throw new StorageMaintenanceError("interactive-confirmation-required", "confirmation");
77
+ }
78
+ console.log([
79
+ "",
80
+ `Exact database path: ${database}`,
81
+ "All Pi instances must be closed before this operation.",
82
+ "The source database will not be overwritten in place.",
83
+ ].join("\n"));
84
+ const expected = action.toUpperCase();
85
+ const prompt = createInterface({ input: stdin, output: stdout });
86
+ try {
87
+ const answer = await prompt.question(`Type ${expected} to continue: `);
88
+ if (answer !== expected) throw new StorageMaintenanceError("confirmation-declined", "confirmation");
89
+ } finally {
90
+ prompt.close();
91
+ }
92
+ }
93
+
94
+ async function main() {
95
+ let parsed;
96
+ try {
97
+ parsed = parseArguments(process.argv.slice(2));
98
+ } catch {
99
+ console.error(usage());
100
+ process.exitCode = 2;
101
+ return;
102
+ }
103
+
104
+ if (parsed.command === "inspect") {
105
+ printInspection(inspectStorage(parsed.database));
106
+ return;
107
+ }
108
+
109
+ if (parsed.command === "compact") {
110
+ const inspection = inspectStorage(parsed.database);
111
+ printInspection(inspection);
112
+ await confirm("compact", inspection.databasePath);
113
+ const result = await compactStorage(inspection.databasePath);
114
+ console.log([
115
+ "",
116
+ "DS4 Context Storage Compaction Completed",
117
+ `Database: ${result.databasePath}`,
118
+ `Backup retained: ${result.backupPath}`,
119
+ `Before / after: ${formatBytes(result.beforeBytes)} / ${formatBytes(result.afterBytes)}`,
120
+ `Manifests pruned: ${result.maintenance.prunedManifests}`,
121
+ `Manifests rolled up: ${result.maintenance.rolledUpManifests}`,
122
+ `Oversize skipped: ${result.maintenance.skippedOversizeManifests}`,
123
+ `Calibration pruned: ${result.maintenance.prunedCalibrationSamples}`,
124
+ "Run /context health and /context storage after reopening Pi.",
125
+ ].join("\n"));
126
+ return;
127
+ }
128
+
129
+ const paths = storageMaintenancePaths(parsed.database);
130
+ await confirm("recover", paths.source);
131
+ const result = recoverStorage(paths.source);
132
+ console.log([
133
+ "DS4 Context Storage Recovery Completed",
134
+ `Database: ${result.databasePath}`,
135
+ `Action: ${result.action}`,
136
+ ].join("\n"));
137
+ }
138
+
139
+ try {
140
+ await main();
141
+ } catch (error) {
142
+ if (error instanceof StorageMaintenanceError || error instanceof DatabaseProtocolError) {
143
+ console.error(error.message);
144
+ } else {
145
+ console.error("Storage maintenance failed (category=unexpected-local-failure)");
146
+ }
147
+ process.exitCode = 1;
148
+ }
@@ -3,6 +3,7 @@ import {
3
3
  isPrivacyClassification,
4
4
  type PrivacyClassification,
5
5
  } from "ds4-context-core/privacy/privacy-policy";
6
+ import type { StorageDiagnostics } from "ds4-context-core/persistence/storage-diagnostics";
6
7
  import type {
7
8
  Ds4ContextRuntime,
8
9
  RuntimeDiagnostics,
@@ -33,6 +34,7 @@ const SUBCOMMANDS = [
33
34
  "artifacts",
34
35
  "compaction",
35
36
  "compact-preview",
37
+ "storage",
36
38
  "health",
37
39
  "rebuild-index",
38
40
  ] as const;
@@ -50,6 +52,19 @@ function count(value: number | undefined): string {
50
52
  return value === undefined ? "n/a" : NUMBER_FORMAT.format(value);
51
53
  }
52
54
 
55
+ function bytes(value: number | undefined): string {
56
+ if (value === undefined) return "n/a";
57
+ const units = ["B", "KiB", "MiB", "GiB", "TiB"];
58
+ let scaled = value;
59
+ let unit = 0;
60
+ while (scaled >= 1024 && unit < units.length - 1) {
61
+ scaled /= 1024;
62
+ unit++;
63
+ }
64
+ const digits = unit === 0 ? 0 : scaled >= 100 ? 0 : scaled >= 10 ? 1 : 2;
65
+ return `${scaled.toFixed(digits)} ${units[unit]}`;
66
+ }
67
+
53
68
  function present(ctx: ExtensionCommandContext, message: string, level: "info" | "warning" | "error" = "info"): void {
54
69
  if (ctx.hasUI) ctx.ui.notify(message, level);
55
70
  else console.log(message);
@@ -251,6 +266,7 @@ function formatManifest(diagnostics: RuntimeDiagnostics): string {
251
266
  const manifest = diagnostics.lastManifest;
252
267
  if (!manifest) return "No Context Manifest has been built for this session yet.";
253
268
 
269
+ const inventory = manifest.persistedInventory;
254
270
  const kinds = new Map<string, { items: number; tokens: number }>();
255
271
  for (const item of manifest.included) {
256
272
  const aggregate = kinds.get(item.kind) ?? { items: 0, tokens: 0 };
@@ -273,7 +289,11 @@ function formatManifest(diagnostics: RuntimeDiagnostics): string {
273
289
  `Actual input: ${count(manifest.actualInputTokens)}`,
274
290
  `Usage input/cache: ${count(manifest.providerUsage?.inputTokens)} / ${count(manifest.providerUsage?.cacheReadTokens)} read / ${count(manifest.providerUsage?.cacheWriteTokens)} write`,
275
291
  `Target / hard: ${count(manifest.targetInputTokens)} / ${count(manifest.hardInputLimit)}`,
276
- `Included / excluded:${count(manifest.included.length)} / ${count(manifest.excluded.length)}`,
292
+ `Included / excluded:${count(manifest.included.length)} / ${count(inventory?.excluded.total ?? manifest.excluded.length)}`,
293
+ `Persisted inventory: ${inventory?.completeness ?? "complete"}`,
294
+ ...(inventory?.completeness === "excluded-rollup" ? [
295
+ `Excluded details: ${count(inventory.excluded.retained)} / ${count(inventory.excluded.total)} retained in persisted projection`,
296
+ ] : []),
277
297
  `Planning mode: ${manifest.planning?.mode ?? "observer"}`,
278
298
  `Original messages: ${count(manifest.planning?.originalMessageCount ?? manifest.composition.messageCount)}`,
279
299
  `Selected messages: ${count(manifest.composition.messageCount)}`,
@@ -299,9 +319,13 @@ function formatManifestItems(diagnostics: RuntimeDiagnostics, type: "included" |
299
319
  const manifest = diagnostics.lastManifest;
300
320
  if (!manifest) return "No Context Manifest has been built for this session yet.";
301
321
  const items = manifest[type];
322
+ const inventory = manifest.persistedInventory;
302
323
  return [
303
324
  `DS4 Context ${type === "included" ? "Included" : "Excluded"} Items`,
304
325
  "",
326
+ ...(type === "excluded" && inventory?.completeness === "excluded-rollup"
327
+ ? [`Persisted projection: ${count(inventory.excluded.retained)} / ${count(inventory.excluded.total)} excluded details retained; this is not the complete historical inventory.`, ""]
328
+ : []),
305
329
  ...(items.length === 0
306
330
  ? ["none"]
307
331
  : items.map((item, index) => {
@@ -717,6 +741,43 @@ function formatAdapter(diagnostics: RuntimeDiagnostics): string {
717
741
  ].join("\n");
718
742
  }
719
743
 
744
+ function formatStorage(storage: StorageDiagnostics, databasePath?: string): string {
745
+ if (storage.status === "unavailable") {
746
+ return [
747
+ "DS4 Storage",
748
+ "",
749
+ "Status: unavailable",
750
+ `Database: ${databasePath ?? "unavailable"}`,
751
+ "Pi fallback remains active; no storage mutation was attempted.",
752
+ ].join("\n");
753
+ }
754
+ return [
755
+ "DS4 Storage",
756
+ "",
757
+ `Status: ${storage.status}`,
758
+ `Database: ${databasePath ?? "unavailable"}`,
759
+ `Schema / journal: ${storage.schemaVersion ?? "n/a"} / ${storage.journalMode ?? "n/a"}`,
760
+ `Database / WAL / SHM: ${bytes(storage.databaseBytes)} / ${bytes(storage.walBytes)} / ${bytes(storage.shmBytes)}`,
761
+ `Allocated / reusable: ${bytes(storage.allocatedBytes)} / ${bytes(storage.reusableBytes)}`,
762
+ `Pages total / reusable: ${count(storage.pageCount)} / ${count(storage.freePages)}`,
763
+ `Manifests: ${count(storage.manifests.rows)} / target ${count(storage.manifests.retainedLimit)}`,
764
+ `Manifest payload: ${bytes(storage.manifests.serializedBytes)}`,
765
+ `Rolled-up manifests: ${count(storage.manifests.rolledUpRows)}`,
766
+ `Calibration samples/profiles: ${count(storage.calibration.rows)} / ${count(storage.calibration.profiles)} (limit ${count(storage.calibration.retainedPerProfile)}/profile)`,
767
+ `Sessions: ${count(storage.sessions)}`,
768
+ `Artifact objects / refs: ${count(storage.artifacts.objects)} / ${count(storage.artifacts.references)}`,
769
+ `Artifact bytes: ${bytes(storage.artifacts.bytes)}`,
770
+ ...(storage.activeProject ? [
771
+ `Active project files: ${count(storage.activeProject.files)}`,
772
+ `Project snippets/stale: ${count(storage.activeProject.snippets)} / ${count(storage.activeProject.staleSnippets)}`,
773
+ `Project indexed tokens: ${count(storage.activeProject.indexedTokens)}`,
774
+ ] : []),
775
+ `Retention converged: ${storage.retention.converged ? "yes" : "no"}`,
776
+ `Offline maintenance: ${storage.maintenance.recommended ? "recommended" : "not required"}`,
777
+ ...storage.maintenance.reasons.map((reason) => `Reason: ${reason}`),
778
+ ].join("\n");
779
+ }
780
+
720
781
  function formatCompaction(diagnostics: RuntimeDiagnostics, preview: boolean): string {
721
782
  const compaction = diagnostics.compaction;
722
783
  return [
@@ -734,6 +795,11 @@ function formatCompaction(diagnostics: RuntimeDiagnostics, preview: boolean): st
734
795
  `Last trigger: ${compaction.trigger ?? "n/a"}`,
735
796
  `Summary ID: ${compaction.summaryId ?? "n/a"}`,
736
797
  `Source entries: ${count(compaction.sourceEntries)}`,
798
+ `Input budget: ${count(compaction.inputBudgetTokens)}`,
799
+ `Whole-source prompt: ${count(compaction.sourcePromptTokens)}`,
800
+ `Generated segments: ${count(compaction.segmentCount)}`,
801
+ `Aggregate calls: ${count(compaction.aggregateCalls)}`,
802
+ `Transport retries: ${count(compaction.transportRetries)}`,
737
803
  `Validation: ${compaction.validationStatus ?? "n/a"}`,
738
804
  `First kept entry: ${compaction.firstKeptEntryId ?? "n/a"}`,
739
805
  `Tokens before: ${count(compaction.tokensBefore)}`,
@@ -1003,6 +1069,17 @@ export function registerContextCommand(pi: ExtensionAPI, runtime: Ds4ContextRunt
1003
1069
  return;
1004
1070
  }
1005
1071
 
1072
+ if (subcommand === "storage") {
1073
+ const diagnostics = runtime.diagnostics(ctx);
1074
+ const storage = runtime.storageDiagnostics();
1075
+ present(
1076
+ ctx,
1077
+ formatStorage(storage, diagnostics.databasePath),
1078
+ storage.status === "ok" ? "info" : "warning",
1079
+ );
1080
+ return;
1081
+ }
1082
+
1006
1083
  if (subcommand === "rebuild-index") {
1007
1084
  await ctx.waitForIdle();
1008
1085
  const result = runtime.rebuildIndex(ctx);
@@ -1033,6 +1110,7 @@ export function registerContextCommand(pi: ExtensionAPI, runtime: Ds4ContextRunt
1033
1110
  }
1034
1111
  runtime.verifyArtifactHealth(ctx);
1035
1112
  const diagnostics = runtime.diagnostics(ctx);
1113
+ const storage = runtime.storageDiagnostics();
1036
1114
  const staleProjectSnippets = diagnostics.project.stats?.staleSnippets ?? 0;
1037
1115
  const artifactIntegrityIssues = diagnostics.artifacts.stats.missing + diagnostics.artifacts.stats.corrupt;
1038
1116
  const healthy = health.ok
@@ -1046,7 +1124,8 @@ export function registerContextCommand(pi: ExtensionAPI, runtime: Ds4ContextRunt
1046
1124
  && diagnostics.quality.lastError === undefined
1047
1125
  && diagnostics.ranking.warnings.length === 0
1048
1126
  && artifactIntegrityIssues === 0
1049
- && diagnostics.artifacts.warnings.length === 0;
1127
+ && diagnostics.artifacts.warnings.length === 0
1128
+ && storage.status === "ok";
1050
1129
  present(
1051
1130
  ctx,
1052
1131
  [
@@ -1058,6 +1137,8 @@ export function registerContextCommand(pi: ExtensionAPI, runtime: Ds4ContextRunt
1058
1137
  `Foreign keys: ${health.foreignKeys ? "enabled" : "disabled"}`,
1059
1138
  `Schema version: ${health.schemaVersion}`,
1060
1139
  `Applied migrations: ${health.appliedMigrations}`,
1140
+ `Storage status: ${storage.status}`,
1141
+ `Maintenance recommended: ${storage.maintenance.recommended ? "yes" : "no"}`,
1061
1142
  `Project stale snippets: ${count(staleProjectSnippets)}`,
1062
1143
  `Memory/pin warnings: ${count(diagnostics.memory.warnings.length)}`,
1063
1144
  `Privacy enforcement: ${diagnostics.privacy.enforcement}`,
@@ -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");