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.
- package/README.md +24 -10
- package/docs/ADR/058-bounded-manifest-storage.md +31 -0
- package/docs/ADR/README.md +1 -0
- package/docs/ARCHITECTURE.md +9 -5
- package/docs/COMPACTION.md +3 -3
- package/docs/CONTEXT_MANIFEST.md +10 -1
- package/docs/CONTEXT_PERSISTENCE_TOOL.md +2 -2
- package/docs/DOGFOODING_0.3.0_ALPHA.md +34 -9
- package/docs/DOGFOODING_0.3.0_BETA.md +331 -0
- package/docs/RELEASE_READINESS_0.2.0.md +1 -1
- package/docs/RELEASING.md +6 -6
- package/docs/STORAGE.md +16 -2
- package/docs/STORAGE_MAINTENANCE.md +129 -0
- package/docs/releases/0.3.0-alpha.5.md +5 -3
- package/docs/releases/0.3.0-beta.1.md +90 -0
- package/docs/releases/0.3.0-beta.2.md +84 -0
- package/package.json +6 -2
- package/scripts/ds4-context-storage.mjs +148 -0
- package/src/extension/commands.ts +79 -2
- package/src/extension/runtime.ts +81 -24
- package/src/pi-adapter/compaction-coordinator.ts +12 -0
- package/src/pi-adapter/summary-generator.ts +120 -34
- package/src/pi-adapter/version.ts +1 -1
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# DS4 Context Engine 0.3.0-beta.1
|
|
2
|
+
|
|
3
|
+
Status: published prerelease on 2026-09-03; exact registry verification passed.
|
|
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
|
+
Validation-only CI run [`33752225991`](https://github.com/Alucard24/ds4-context-engine/actions/runs/33752225991) passed on Node.js `22.19.0` and `24.x` for committed candidate `4ef936f5a7e5c6afb63fd42d0b745d9201a7b87f` before publication.
|
|
71
|
+
|
|
72
|
+
## Registry evidence
|
|
73
|
+
|
|
74
|
+
- `npm run registry:check -- 0.3.0-beta.1`: passed against all three exact published versions, including exact adapter/core dependencies, public core exports, compiled reference conformance, packaged quality corpus, packaged Pi extension startup, and the installed `ds4-context-storage` CLI shim/usage probe.
|
|
75
|
+
- npm `beta` resolves to `0.3.0-beta.1` for all three packages.
|
|
76
|
+
- npm `latest` remains `0.2.0`, `alpha` remains `0.3.0-alpha.5`, and `rc` remains `0.2.0-rc.1` for all three packages.
|
|
77
|
+
- Published SHA-1 values are `fc5d6aaf2304291dc147ecc639281fb947390a51` (core), `7bfe0cf61a0f1213db058d5f89c1b7feb25db54a` (reference adapter), and `6abad04e2a2c30566c042f1e0829930c6546ed54` (Pi adapter).
|
|
78
|
+
|
|
79
|
+
Exact registry verification has passed; the annotated tag and GitHub prerelease remain pending.
|
|
80
|
+
|
|
81
|
+
## Documentation
|
|
82
|
+
|
|
83
|
+
- [`../CONTEXT_MANIFEST.md`](../CONTEXT_MANIFEST.md)
|
|
84
|
+
- [`../STORAGE.md`](../STORAGE.md)
|
|
85
|
+
- [`../STORAGE_MAINTENANCE.md`](../STORAGE_MAINTENANCE.md)
|
|
86
|
+
- [`../COMPACTION.md`](../COMPACTION.md)
|
|
87
|
+
- [`../ARCHITECTURE.md`](../ARCHITECTURE.md)
|
|
88
|
+
- [`../DOGFOODING_0.3.0_BETA.md`](../DOGFOODING_0.3.0_BETA.md)
|
|
89
|
+
- [`../RELEASING.md`](../RELEASING.md)
|
|
90
|
+
- [`0.3.0-alpha.5.md`](0.3.0-alpha.5.md)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# DS4 Context Engine 0.3.0-beta.2
|
|
2
|
+
|
|
3
|
+
Status: release candidate; publication pending.
|
|
4
|
+
|
|
5
|
+
This coordinated beta carries forward beta.1 without changing canonical records, SQLite schema, runtime contracts, retention limits, privacy policy, compaction validation, or fallback semantics. It incorporates the operational evidence from a successful maintenance of the live derived SQLite projection and documents how to retain an interactive confirmation when recording a maintenance transcript.
|
|
6
|
+
|
|
7
|
+
## Changed since beta.1
|
|
8
|
+
|
|
9
|
+
- Recorded successful offline maintenance of a live schema-15 database after all Pi holders closed and the exact local `COMPACT` confirmation was entered.
|
|
10
|
+
- Clarified that `compact` and `recover` require both stdin and stdout to be local TTYs. Directly piping a mutating command through `tee` fails safely before maintenance; transcript capture must allocate a pseudo-TTY and must not bypass confirmation.
|
|
11
|
+
- Updated the beta dogfooding target and coordinated package versions to `0.3.0-beta.2`.
|
|
12
|
+
|
|
13
|
+
There are no functional storage-format or provider-path changes relative to beta.1.
|
|
14
|
+
|
|
15
|
+
## Live maintenance evidence
|
|
16
|
+
|
|
17
|
+
The authorized beta.1 maintenance run completed backup, working-copy transformation, candidate validation, recoverable swap, and post-swap inspection:
|
|
18
|
+
|
|
19
|
+
- database size: 2.42 GiB before, 377 MiB immediately after compaction;
|
|
20
|
+
- Context Manifests: 1,630 before, 128 after;
|
|
21
|
+
- manifest payload: 1.02 GiB before, 13.5 MiB after;
|
|
22
|
+
- 1,502 manifests pruned, 123 retained manifests rewritten as bounded excluded rollups, and no irreducible oversize skipped;
|
|
23
|
+
- schema 15, `quick_check=ok`, and zero foreign-key violations;
|
|
24
|
+
- no working, candidate, retired, lock, or maintenance-state artifact remained;
|
|
25
|
+
- after Pi reopened, the database returned to WAL mode with 128 manifests, no pending prune, and no maintenance recommendation;
|
|
26
|
+
- the fixed standalone backup remains retained with private file permissions during the observation period.
|
|
27
|
+
|
|
28
|
+
The first transcript-capturing launcher redirected stdout through `tee`, so the CLI rejected it with `interactive-confirmation-required` at `stage=confirmation`. The confirmation precedes `compactStorage()`, and no backup or staging artifact was created by that failed attempt. The successful retry used a pseudo-TTY and preserved the exact local confirmation requirement.
|
|
29
|
+
|
|
30
|
+
## Safety and compatibility
|
|
31
|
+
|
|
32
|
+
Pi JSONL remains canonical and append-only. SQLite remains disposable and rebuildable. Physical maintenance remains explicit, offline, local-TTY-only, and unavailable to model-callable tools. Every `context_persistence` write still requires a fresh positive local UI decision; destructive Pin and Memory operations still require an exact target and current revision; canonical mutations still append through Pi before SQLite reconciliation.
|
|
33
|
+
|
|
34
|
+
Strict exact-value grounding, bounded repair, privacy filtering, atomic tool exchanges, overflow-safe hierarchical compaction, transport-only retries, and Pi fallback behavior are unchanged. Rejected exact values, provider payloads, SQL, bound values, and raw SQLite messages remain absent from diagnostics.
|
|
35
|
+
|
|
36
|
+
Compatibility remains unchanged:
|
|
37
|
+
|
|
38
|
+
- SQLite schema: `15`;
|
|
39
|
+
- configuration: `ds4-context-config-v1`;
|
|
40
|
+
- runtime adapter: `runtime-adapter-v1`;
|
|
41
|
+
- persistence tool: `ds4-context-persistence-tool-v1`;
|
|
42
|
+
- persistence result: `ds4-context-persistence-result-v1`;
|
|
43
|
+
- Pi: `0.84.3`;
|
|
44
|
+
- Node.js: `>=22.19.0`.
|
|
45
|
+
|
|
46
|
+
Historical excluded-inventory rendering remains unsupported after downgrade once bounded `excluded-rollup` rows have been written, because earlier readers may present sampled excluded details as complete.
|
|
47
|
+
|
|
48
|
+
## Package/version policy
|
|
49
|
+
|
|
50
|
+
The coordinated version is `0.3.0-beta.2` for:
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
ds4-context-core
|
|
54
|
+
ds4-context-reference-adapter
|
|
55
|
+
ds4-context-engine
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Both adapters depend exactly on `ds4-context-core@0.3.0-beta.2`. Publication must use the explicit npm `beta` dist-tag for all three packages. npm `latest` must remain stable `0.2.0`, and GitHub Actions remains validation-only with OIDC and package-write permissions denied.
|
|
59
|
+
|
|
60
|
+
## Candidate validation
|
|
61
|
+
|
|
62
|
+
Local candidate verification on Node.js `26.5.1`:
|
|
63
|
+
|
|
64
|
+
- `npm run check`: 69 files and 341 tests passed.
|
|
65
|
+
- Three consecutive focused multi-process SQLite concurrency runs passed.
|
|
66
|
+
- `npm run quality:compare`: candidate quality `0.9875` versus baseline `0.808156`.
|
|
67
|
+
- `npm run schema:context-persistence`: 1,266 bytes and 317 estimated tokens; below the 1,500 absolute and 320 relative limits.
|
|
68
|
+
- Three feature-disabled latency comparisons against exact `ds4-context-core@0.1.2` passed with ratios `1.075625`, `0.899241`, and `1.008488`, each at or below `1.10`, using 200 samples and 50 calls per sample.
|
|
69
|
+
- `npm run pack:check`: verified core (223 files), reference adapter (7 files), and Pi adapter (68 files) in a clean consumer.
|
|
70
|
+
- `npm pack --dry-run --json` for all three packages passed with the same inventories and no forbidden local/session/storage files.
|
|
71
|
+
- `git diff --check` passed; the only pre-existing untracked path is `.serena/`, which is excluded from commits and package inventories.
|
|
72
|
+
- Version, exact core dependencies, package-lock entries, extension constant, and reference-adapter constant are synchronized to `0.3.0-beta.2`.
|
|
73
|
+
|
|
74
|
+
Validation-only CI on the committed candidate remains required before publication. Exact post-publication registry verification, the annotated tag, and the GitHub prerelease remain pending until the candidate and published artifacts pass their respective gates.
|
|
75
|
+
|
|
76
|
+
## Documentation
|
|
77
|
+
|
|
78
|
+
- [`../DOGFOODING_0.3.0_BETA.md`](../DOGFOODING_0.3.0_BETA.md)
|
|
79
|
+
- [`../STORAGE_MAINTENANCE.md`](../STORAGE_MAINTENANCE.md)
|
|
80
|
+
- [`../STORAGE.md`](../STORAGE.md)
|
|
81
|
+
- [`../CONTEXT_MANIFEST.md`](../CONTEXT_MANIFEST.md)
|
|
82
|
+
- [`../COMPACTION.md`](../COMPACTION.md)
|
|
83
|
+
- [`../RELEASING.md`](../RELEASING.md)
|
|
84
|
+
- [`0.3.0-beta.1.md`](0.3.0-beta.1.md)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ds4-context-engine",
|
|
3
|
-
"version": "0.3.0-
|
|
3
|
+
"version": "0.3.0-beta.2",
|
|
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-
|
|
65
|
+
"ds4-context-core": "0.3.0-beta.2"
|
|
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 [
|
|
@@ -738,6 +799,7 @@ function formatCompaction(diagnostics: RuntimeDiagnostics, preview: boolean): st
|
|
|
738
799
|
`Whole-source prompt: ${count(compaction.sourcePromptTokens)}`,
|
|
739
800
|
`Generated segments: ${count(compaction.segmentCount)}`,
|
|
740
801
|
`Aggregate calls: ${count(compaction.aggregateCalls)}`,
|
|
802
|
+
`Transport retries: ${count(compaction.transportRetries)}`,
|
|
741
803
|
`Validation: ${compaction.validationStatus ?? "n/a"}`,
|
|
742
804
|
`First kept entry: ${compaction.firstKeptEntryId ?? "n/a"}`,
|
|
743
805
|
`Tokens before: ${count(compaction.tokensBefore)}`,
|
|
@@ -1007,6 +1069,17 @@ export function registerContextCommand(pi: ExtensionAPI, runtime: Ds4ContextRunt
|
|
|
1007
1069
|
return;
|
|
1008
1070
|
}
|
|
1009
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
|
+
|
|
1010
1083
|
if (subcommand === "rebuild-index") {
|
|
1011
1084
|
await ctx.waitForIdle();
|
|
1012
1085
|
const result = runtime.rebuildIndex(ctx);
|
|
@@ -1037,6 +1110,7 @@ export function registerContextCommand(pi: ExtensionAPI, runtime: Ds4ContextRunt
|
|
|
1037
1110
|
}
|
|
1038
1111
|
runtime.verifyArtifactHealth(ctx);
|
|
1039
1112
|
const diagnostics = runtime.diagnostics(ctx);
|
|
1113
|
+
const storage = runtime.storageDiagnostics();
|
|
1040
1114
|
const staleProjectSnippets = diagnostics.project.stats?.staleSnippets ?? 0;
|
|
1041
1115
|
const artifactIntegrityIssues = diagnostics.artifacts.stats.missing + diagnostics.artifacts.stats.corrupt;
|
|
1042
1116
|
const healthy = health.ok
|
|
@@ -1050,7 +1124,8 @@ export function registerContextCommand(pi: ExtensionAPI, runtime: Ds4ContextRunt
|
|
|
1050
1124
|
&& diagnostics.quality.lastError === undefined
|
|
1051
1125
|
&& diagnostics.ranking.warnings.length === 0
|
|
1052
1126
|
&& artifactIntegrityIssues === 0
|
|
1053
|
-
&& diagnostics.artifacts.warnings.length === 0
|
|
1127
|
+
&& diagnostics.artifacts.warnings.length === 0
|
|
1128
|
+
&& storage.status === "ok";
|
|
1054
1129
|
present(
|
|
1055
1130
|
ctx,
|
|
1056
1131
|
[
|
|
@@ -1062,6 +1137,8 @@ export function registerContextCommand(pi: ExtensionAPI, runtime: Ds4ContextRunt
|
|
|
1062
1137
|
`Foreign keys: ${health.foreignKeys ? "enabled" : "disabled"}`,
|
|
1063
1138
|
`Schema version: ${health.schemaVersion}`,
|
|
1064
1139
|
`Applied migrations: ${health.appliedMigrations}`,
|
|
1140
|
+
`Storage status: ${storage.status}`,
|
|
1141
|
+
`Maintenance recommended: ${storage.maintenance.recommended ? "yes" : "no"}`,
|
|
1065
1142
|
`Project stale snippets: ${count(staleProjectSnippets)}`,
|
|
1066
1143
|
`Memory/pin warnings: ${count(diagnostics.memory.warnings.length)}`,
|
|
1067
1144
|
`Privacy enforcement: ${diagnostics.privacy.enforcement}`,
|