ds4-context-engine 0.3.10 → 0.4.0
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 +4 -2
- package/docs/ADR/064-per-project-databases-with-shared-calibration.md +88 -0
- package/docs/ADR/README.md +1 -0
- package/docs/MODEL_AWARENESS.md +2 -0
- package/docs/STORAGE.md +38 -3
- package/docs/STORAGE_MAINTENANCE.md +1 -1
- package/docs/releases/0.3.10.md +1 -1
- package/docs/releases/0.4.0.md +86 -0
- package/package.json +3 -2
- package/src/extension/commands.ts +10 -2
- package/src/extension/runtime.ts +100 -23
- package/src/pi-adapter/version.ts +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ bounded active context with provenance
|
|
|
16
16
|
Pi provider
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
> **Project status:** The coordinated `0.
|
|
19
|
+
> **Project status:** The coordinated `0.4.0` release adds `storage.scope: "agent" | "project"`, now defaulting to per-project SQLite projections with shared token calibration in the agent database (opt out with `storage.scope: "agent"`). The opt-in BPE estimation and bounded auto-tuning from `0.3.10` keep `chars-v1` and disabled auto-tuning as their defaults. The bounded compaction controls from `0.3.9` remain in place; canonical history, SQLite schema 16 and runtime contracts are unchanged. Pi remains pinned to `0.84.3`. See the [0.4.0 release record](docs/releases/0.4.0.md), [ADR 064](docs/ADR/064-per-project-databases-with-shared-calibration.md) and [model-awareness validation](docs/MODEL_AWARENESS.md).
|
|
20
20
|
|
|
21
21
|
**Current compaction defaults:** `compaction.directUpdate=true`, `compaction.inputBudget="context"`, `compaction.segmentTargetTokens=30000`, `compaction.maxRequestInputTokens=64000`, `compaction.maxOperationInputTokens=2000000`, `compaction.maxConcurrentSegments=2`. Every DS4 provider attempt is bounded by the effective request limit, and the operation limit includes retries; `inputBudget="summary"` remains an explicit throughput-oriented opt-in. Existing compaction/master switches still apply. See [latency controls and compatibility](docs/COMPACTION.md#latency-controls). No real-provider speedup is claimed from mock tests. The five optional editing/reading/artifact/job features introduced in `0.3.4` remain default-off.
|
|
22
22
|
|
|
@@ -385,6 +385,7 @@ The following example shows the main configuration groups. Omitted values use th
|
|
|
385
385
|
"logLevel": "info"
|
|
386
386
|
},
|
|
387
387
|
"storage": {
|
|
388
|
+
"scope": "project",
|
|
388
389
|
"databasePath": "ds4-context/context.db",
|
|
389
390
|
"busyTimeoutMs": 5000,
|
|
390
391
|
"writeRetryTimeoutMs": 30000,
|
|
@@ -514,6 +515,7 @@ scripts package and release-readiness checks
|
|
|
514
515
|
- [Roadmap 0.2.0](docs/ROADMAP_0.2.0.md)
|
|
515
516
|
- [Release process](docs/RELEASING.md)
|
|
516
517
|
- [0.2.0 release readiness](docs/RELEASE_READINESS_0.2.0.md)
|
|
518
|
+
- [0.4.0 release notes](docs/releases/0.4.0.md)
|
|
517
519
|
- [0.3.10 release notes](docs/releases/0.3.10.md)
|
|
518
520
|
- [0.3.9 release notes](docs/releases/0.3.9.md)
|
|
519
521
|
- [0.3.8 release notes](docs/releases/0.3.8.md)
|
|
@@ -542,7 +544,7 @@ scripts package and release-readiness checks
|
|
|
542
544
|
|
|
543
545
|
The original M0–M13 roadmap is complete. `ds4-context-core` contains the compiled runtime-neutral implementation. M14 context-quality metrics, M15 rich symbol indexing, M16 hybrid semantic retrieval, M17 cross-session project memory, M18 learned-ranking shadow evaluation, M19's runtime adapter/conformance kit, and M20 opt-in local KV eligibility/replay are implemented on `main`. Learned active ranking remains promotion-gated, Pi reports local KV as unsupported, and static ranking/native completion stay authoritative on every failure.
|
|
544
546
|
|
|
545
|
-
The [0.2.0 roadmap](docs/ROADMAP_0.2.0.md) is complete. The stable 0.3 line carries forward the [context persistence tool](docs/CONTEXT_PERSISTENCE_TOOL.md), privacy-safe [compaction](docs/COMPACTION.md), bounded persisted manifests, cooperative client leases and recoverable offline maintenance. Version 0.3.10 adds opt-in BPE estimation and bounded model-budget auto-tuning while retaining the `chars-v1` default. Version 0.3.9 extends the bounded compaction updates, summary input headroom, concurrent segments and phase timings introduced in 0.3.5 with per-request and cumulative operation input limits; 0.3.8 adds indexed FTS key deletion without changing search results. The opt-in [anchored editing](docs/ANCHORED_EDITING.md) and [portable agent tools](docs/PORTABLE_AGENT_TOOLS.md) from 0.3.4 remain default-off, without backend rewind, forced sampling or operational KV integration. Confirmation, provenance, Pi fallback and canonical/configuration/SQLite/runtime contracts remain unchanged. The [0.2 readiness record](docs/RELEASE_READINESS_0.2.0.md) remains the compatibility baseline; the lexical planner stays available as the deterministic fallback.
|
|
547
|
+
The [0.2.0 roadmap](docs/ROADMAP_0.2.0.md) is complete. The stable 0.3 line carries forward the [context persistence tool](docs/CONTEXT_PERSISTENCE_TOOL.md), privacy-safe [compaction](docs/COMPACTION.md), bounded persisted manifests, cooperative client leases and recoverable offline maintenance. Version 0.4.0 introduces `storage.scope` (`"agent" | "project"`, default `"project"`): one rebuildable SQLite projection per trusted canonical project root, with token calibration shared in the agent database; `storage.scope: "agent"` restores the previous single-file layout. Version 0.3.10 adds opt-in BPE estimation and bounded model-budget auto-tuning while retaining the `chars-v1` default. Version 0.3.9 extends the bounded compaction updates, summary input headroom, concurrent segments and phase timings introduced in 0.3.5 with per-request and cumulative operation input limits; 0.3.8 adds indexed FTS key deletion without changing search results. The opt-in [anchored editing](docs/ANCHORED_EDITING.md) and [portable agent tools](docs/PORTABLE_AGENT_TOOLS.md) from 0.3.4 remain default-off, without backend rewind, forced sampling or operational KV integration. Confirmation, provenance, Pi fallback and canonical/configuration/SQLite/runtime contracts remain unchanged. The [0.2 readiness record](docs/RELEASE_READINESS_0.2.0.md) remains the compatibility baseline; the lexical planner stays available as the deterministic fallback.
|
|
546
548
|
|
|
547
549
|
## Contributing
|
|
548
550
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# 064 — Per-project databases with shared token calibration
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-09-25
|
|
4
|
+
**Status:** Accepted
|
|
5
|
+
**Related:** [002](002-pi-jsonl-canonical-sqlite-rebuildable.md), [058](058-bounded-manifest-storage.md), [063](063-fts-rowid-key-mappings.md)
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
The storage plan decided **D1**: keep one shared SQLite projection at
|
|
10
|
+
`~/.pi/agent/ds4-context/context.db` for every Pi session and explicitly avoid
|
|
11
|
+
per-session *or per-project* databases. That projection is deliberately
|
|
12
|
+
derived, rebuildable and disposable.
|
|
13
|
+
|
|
14
|
+
The session index (`entries`, `entries_fts`) is the table that actually grows
|
|
15
|
+
with total indexed history, and automatic eviction remains deferred until an
|
|
16
|
+
on-demand rehydration path is verified. A single file therefore also means a
|
|
17
|
+
single growth boundary, a single write lock and a single point of physical
|
|
18
|
+
reset shared by unrelated projects. Content is already partitioned logically
|
|
19
|
+
by `sessions.project_path`, `project_states`, `project_files` and
|
|
20
|
+
`project_memory_sessions`; the file boundary was the only thing missing.
|
|
21
|
+
|
|
22
|
+
The one piece of genuinely global learning is token calibration. Schema v10's
|
|
23
|
+
`token_calibration` is keyed by exact `provider + model + estimator_version`
|
|
24
|
+
and has **no project column**: a naive per-project split would restart
|
|
25
|
+
calibration for every project (minimum 3 samples, 8 accepted for the opt-in
|
|
26
|
+
`autoTune` expansion), weakening exactly the BPE/auto-tuning path it should
|
|
27
|
+
protect.
|
|
28
|
+
|
|
29
|
+
## Decision
|
|
30
|
+
|
|
31
|
+
Add `storage.scope: "agent" | "project"` (default `"project"`) to the storage
|
|
32
|
+
configuration:
|
|
33
|
+
|
|
34
|
+
- `agent` keeps the previous shared-database behavior and remains selectable.
|
|
35
|
+
- `project` derives one database per trusted canonical project root:
|
|
36
|
+
`projects/<sha256(canonicalRoot)[0..32]>.db`, next to the configured agent
|
|
37
|
+
database. Untrusted projects, broad roots (home directory, filesystem root)
|
|
38
|
+
and any resolution failure fall back to the agent database.
|
|
39
|
+
|
|
40
|
+
Both files receive the **same schema and the same migrations**; there is no
|
|
41
|
+
schema fork and migrations 1–15 are untouched. The split changes only which
|
|
42
|
+
repository each handle is used for:
|
|
43
|
+
|
|
44
|
+
- the agent database keeps `token_calibration` (shared learning);
|
|
45
|
+
- the project database keeps the session index, project index, context
|
|
46
|
+
manifests, summary graph, memory/pin projections, embeddings, quality
|
|
47
|
+
samples and artifact metadata; artifact object bytes move to
|
|
48
|
+
`projects/artifacts/<project-digest>/` so the orphan garbage collector,
|
|
49
|
+
which only sees references in the current database, can never delete
|
|
50
|
+
another project's objects;
|
|
51
|
+
- `resource_leases` and the client lease stay per file, protecting each
|
|
52
|
+
database independently.
|
|
53
|
+
|
|
54
|
+
With `project`, a calibration sample is derived from the project manifest and
|
|
55
|
+
inserted into the agent database with `manifest_id = NULL` (the column and its
|
|
56
|
+
partial unique index already allow this). The two writes are intentionally
|
|
57
|
+
**not** one cross-database transaction: losing one calibration sample is
|
|
58
|
+
harmless, whereas losing manifest/usage consistency is not. In `agent` scope
|
|
59
|
+
the previous single transaction is unchanged. Manifest pruning only detaches
|
|
60
|
+
calibration rows in `agent` scope; the project database's calibration table
|
|
61
|
+
stays empty.
|
|
62
|
+
|
|
63
|
+
Every project database starts empty and is rebuilt from canonical Pi JSONL,
|
|
64
|
+
project files and memory/pin `CustomEntry` records. The previous shared
|
|
65
|
+
database is left untouched: with the default change, existing users cold-start
|
|
66
|
+
their per-project indexes while their existing calibration remains available
|
|
67
|
+
in the agent database.
|
|
68
|
+
|
|
69
|
+
## Consequences
|
|
70
|
+
|
|
71
|
+
- Physical isolation per project: separate growth, separate write lock,
|
|
72
|
+
"reset project state" = remove one file, and no eviction needed to bound a
|
|
73
|
+
single project's index.
|
|
74
|
+
- Calibration stays global: a sample learned in project A is immediately
|
|
75
|
+
visible in project B for the same provider/model/estimator.
|
|
76
|
+
- Default behavior changes. A pre-existing shared database becomes the agent
|
|
77
|
+
database (calibration and old manifests) and is no longer the active
|
|
78
|
+
projection for new sessions. This requires a minor release and release
|
|
79
|
+
notes; `storage.scope: "agent"` restores the old layout.
|
|
80
|
+
- Maintenance and diagnostics become per file: `/context storage` reports the
|
|
81
|
+
active project database and, when split, the shared agent database;
|
|
82
|
+
`ds4-context-storage inspect|compact|recover --database <path>` must be
|
|
83
|
+
pointed at each file.
|
|
84
|
+
- `storage.databasePath` now names the agent database; project databases
|
|
85
|
+
derive from its directory. A manually configured per-project path keeps
|
|
86
|
+
working, but the derived `projects/` directory is the supported layout.
|
|
87
|
+
- D1 is superseded by this ADR; the development plan keeps the original text
|
|
88
|
+
with an explicit amendment pointer.
|
package/docs/ADR/README.md
CHANGED
|
@@ -67,5 +67,6 @@ The initial decisions from the development plan are accepted:
|
|
|
67
67
|
| [061](061-compaction-latency.md) | Bound compaction update calls, input budgets, concurrent segments and phase timings | Accepted |
|
|
68
68
|
| [062](062-cache-aware-context-planning.md) | Opt-in cache-aware tail planning using model pricing and observed cache shares | Accepted |
|
|
69
69
|
| [063](063-fts-rowid-key-mappings.md) | Resolve FTS key deletes through rowid mapping tables | Accepted |
|
|
70
|
+
| [064](064-per-project-databases-with-shared-calibration.md) | Split project state into per-project databases and keep token calibration shared | Accepted |
|
|
70
71
|
|
|
71
72
|
Each decision will receive a dedicated record when implementation pressure introduces alternatives or consequences not already covered by the development plan.
|
package/docs/MODEL_AWARENESS.md
CHANGED
|
@@ -86,6 +86,8 @@ Until enough samples exist, the multiplier remains `1.0`. The default window is
|
|
|
86
86
|
|
|
87
87
|
Provider-token capacities are computed first from context window, output reserve, safety margin, and policy ratios. Global hard/soft/preferred input limits are converted into local-estimator units using **at least** a multiplier of 1: observed underestimation may reduce them, but apparent overestimation on short prompts never raises them above nominal model limits. Raw manifest estimates remain uncalibrated so future samples do not feed a corrected estimate back into itself. Adaptive tail/history/project budgets use the accepted multiplier but remain capped by the configured `context.*` maxima even after conversion.
|
|
88
88
|
|
|
89
|
+
Calibration samples are global learning, not project data: with the default `storage.scope: "project"` the manifests live in the per-project database while `token_calibration` stays in the shared agent database (see [ADR 064](ADR/064-per-project-databases-with-shared-calibration.md)). A sample learned in one project therefore applies to every other project for the same provider/model/estimator.
|
|
90
|
+
|
|
89
91
|
### Optional BPE estimator and measured budget tuning
|
|
90
92
|
|
|
91
93
|
The default remains `chars-v1`. To opt a profile into local OpenAI `o200k_base` BPE text counting (without fetching vocabulary over the network):
|
package/docs/STORAGE.md
CHANGED
|
@@ -10,6 +10,19 @@ Pi's session JSONL is canonical for conversations and live project files are can
|
|
|
10
10
|
|
|
11
11
|
The extension never edits or rewrites Pi JSONL or project source files. Manual memory/pin commands, confirmed `context_persistence` canonical writes, and learned-ranking feedback append versioned classified Pi `CustomEntry` records through Pi's official `appendEntry()` API. The tool does not write SQLite as a substitute for a canonical Pin or Memory append.
|
|
12
12
|
|
|
13
|
+
## Storage scope
|
|
14
|
+
|
|
15
|
+
`storage.scope` selects where the disposable projection lives (see [ADR 064](ADR/064-per-project-databases-with-shared-calibration.md)):
|
|
16
|
+
|
|
17
|
+
- `agent` — the previous behavior: one shared `context.db` for every session and project.
|
|
18
|
+
- `project` (**default**) — one database per trusted canonical project root, derived as `projects/<sha256(root)[0..32]>.db` next to the agent database. Untrusted projects and broad roots (home directory, filesystem root) fall back to the agent database.
|
|
19
|
+
|
|
20
|
+
Both files receive the same schema and migrations. The agent database keeps only `token_calibration`, so a sample learned in one project is visible to every other project for the same provider/model/estimator; project databases keep the session index, project index, manifests, summary graph, memory/pin projections, embeddings, quality samples and artifact metadata. Project artifact object bytes move under `projects/artifacts/<project-digest>/` so garbage collection stays scoped to one project. A project database starts empty and is rebuilt from canonical JSONL and project files; the split never rewrites the previous shared database.
|
|
21
|
+
|
|
22
|
+
Calibration and manifest writes are intentionally not one cross-database transaction: a failed calibration insert loses at most one sample, while manifest/usage consistency stays inside the project database. Manifest pruning detaches calibration rows only in `agent` scope; in `project` scope the project database's calibration table stays empty. `storage.databasePath` names the agent database; the derived `projects/` directory is the supported layout.
|
|
23
|
+
|
|
24
|
+
Maintenance and diagnostics are per file: `/context storage` reports the active project database and, when split, the shared agent database; `ds4-context-storage inspect|compact|recover --database <path>` must be pointed at each file.
|
|
25
|
+
|
|
13
26
|
The M19 non-Pi reference adapter owns a separate `ds4-runtime-session-v1` JSONL source selected by its host runtime. Its header binds runtime/session identity and the exact canonical project root; following records contain provenance-checked canonical messages. DS4 snapshots and capability diagnostics are disposable. `createReferenceHistory()` refuses overwrite, append uses a dedicated provenance-checked operation, files are mode `0600` where supported, and rebuild never edits this runtime-owned canonical file. Reference JSONL is not imported into Pi or `context.db`.
|
|
14
27
|
|
|
15
28
|
M20 local KV state is entirely runtime-owned and volatile. Core returns only an in-memory eligibility fingerprint to the runtime port; it has no cache-handle field or serialization API. Prefixes, fingerprints, handles and provider outputs are absent from Pi/reference JSONL, Context Manifests, ranking artifacts and every SQLite table. Aggregate hit/miss/prefill counters live only on the adapter controller, and a restart safely resets them with the runtime cache. M20 adds no database migration.
|
|
@@ -83,7 +96,7 @@ Custom entries have empty lexical search text and never enter Pi context directl
|
|
|
83
96
|
|
|
84
97
|
Schema v10 extends `context_manifests` and `token_calibration` with separate uncached-input, cache-read, and cache-write token columns. New calibration rows also carry the correlated manifest ID and explicit estimator version. Legacy pre-v10 samples migrate as `chars-v1` with their prior total stored as uncached input and zero cache fields; this preserves historical ratio behavior without inventing cache hits.
|
|
85
98
|
|
|
86
|
-
Calibration rows are derived telemetry, isolated by exact provider/model and bounded to the latest configured window at read time. The runtime recomputes median/MAD outlier filtering deterministically; no learned model or mutable provider state is stored. Deleting the database loses calibration and cache history but never session content. Ephemeral sessions and configurations that disable manifest persistence keep only a bounded in-memory window.
|
|
99
|
+
Calibration rows are derived telemetry, isolated by exact provider/model and bounded to the latest configured window at read time. The runtime recomputes median/MAD outlier filtering deterministically; no learned model or mutable provider state is stored. Deleting the database loses calibration and cache history but never session content. Ephemeral sessions and configurations that disable manifest persistence keep only a bounded in-memory window. With `storage.scope: "project"`, calibration lives in the agent database while the manifest that produced the sample lives in the project database; the sample therefore carries no `manifest_id` and the two writes are separate transactions.
|
|
87
100
|
|
|
88
101
|
## Context quality samples
|
|
89
102
|
|
|
@@ -107,7 +120,7 @@ The volatile state is cleared on lifecycle/model/branch/compaction boundaries an
|
|
|
107
120
|
|
|
108
121
|
Schema v8 splits content objects from source references. `artifact_objects` is keyed by SHA-256 and stores the private file path, MIME, byte size, verification timestamps, and integrity status. `artifacts` is keyed by a deterministic source-specific ID and references session/entry/tool identity plus original/condensed token estimates and an optional derived privacy classification in `metadata_json`. Equal bytes across calls or sessions deduplicate to one object while retaining independent provenance.
|
|
109
122
|
|
|
110
|
-
Objects live under `ds4-context/artifacts/<sha-prefix>/<sha256>` with private permissions and atomic writes. Pi's full JSONL tool result remains canonical; the object file is a rebuildable local cache. No artifact content is stored in Context Manifests. Search recomputes SHA-256 and returns only bounded, redacted, JSON-quoted literal-match windows for a current-branch reference. The runtime reapplies the stored artifact classification before returning excerpts to the active provider; prohibited remote searches return no content.
|
|
123
|
+
Objects live under `ds4-context/artifacts/<sha-prefix>/<sha256>` with private permissions and atomic writes. With `storage.scope: "project"` the store root becomes `projects/artifacts/<project-digest>/...` so artifact bytes and their metadata share one boundary; the orphan garbage collector only sees references in the current database and must never delete another project's objects. Pi's full JSONL tool result remains canonical; the object file is a rebuildable local cache. No artifact content is stored in Context Manifests. Search recomputes SHA-256 and returns only bounded, redacted, JSON-quoted literal-match windows for a current-branch reference. The runtime reapplies the stored artifact classification before returning excerpts to the active provider; prohibited remote searches return no content.
|
|
111
124
|
|
|
112
125
|
A full index rebuild replays all message entries, recreates missing qualifying objects, removes stale session references, and garbage-collects object rows/files with no references. Missing/corrupt states are reported by `/context health` without blocking Pi.
|
|
113
126
|
|
|
@@ -117,7 +130,7 @@ For persisted sessions, each `context` hook stores a metadata-only manifest cont
|
|
|
117
130
|
|
|
118
131
|
`before_provider_request` updates the pending in-memory manifest with final-check/redaction counters but never the provider payload. The following finalized assistant response updates only the existing scalar usage columns (`actual_tokens`, `input_tokens`, `cache_read_tokens`, and `cache_write_tokens`) and adds at most one exact-model calibration sample. It does not read or rewrite `manifest_json`. Repository reads hydrate authoritative usage from those columns. Ephemeral, oversize-skipped, concurrently pruned, and otherwise uncorrelated manifests retain bounded calibration only in memory.
|
|
119
132
|
|
|
120
|
-
Retention is bounded without a schema change: SQLite keeps the latest 128 manifests globally and at most 200 calibration samples for each provider/model/estimator profile. A manifest prune first detaches its small calibration row, then removes the large diagnostic JSON; calibration has its own per-profile retention. Save and prune are one transaction. Existing oversized stores are reduced incrementally by at most 32 rows and 8 MiB of serialized manifest payload per subsequent manifest write; one individually oversized oldest row may be removed to guarantee progress. There is no startup purge.
|
|
133
|
+
Retention is bounded without a schema change: SQLite keeps the latest 128 manifests globally and at most 200 calibration samples for each provider/model/estimator profile. A manifest prune first detaches its small calibration row, then removes the large diagnostic JSON; calibration has its own per-profile retention. Save and prune are one transaction. In `project` scope the manifest transaction runs in the project database while the calibration sample is inserted separately into the agent database. Existing oversized stores are reduced incrementally by at most 32 rows and 8 MiB of serialized manifest payload per subsequent manifest write; one individually oversized oldest row may be removed to guarantee progress. There is no startup purge.
|
|
121
134
|
|
|
122
135
|
New manifest persistence is byte-bounded. Payloads up to 256 KiB remain complete. Larger payloads preserve all `included` provenance and replace only the `excluded` inventory with a deterministic first/last sample of at most 256 details plus explicit `ds4-context-manifest-inventory-v1` counts, token/classification/kind rollups, and digests. The wrapper returned by `getStored()` declares `complete` or `excluded-rollup`; the live runtime manifest remains complete. A projected payload over 1 MiB is skipped without affecting the model request. Deleted pages become reusable by SQLite but do not promise an immediate reduction in filesystem size. Manifests and calibration remain disposable; Pi JSONL and project files are untouched.
|
|
123
136
|
|
|
@@ -152,3 +165,25 @@ A full rebuild does not blindly delete unchanged entries. It upserts all observe
|
|
|
152
165
|
Session reconciliation is transactional. Memory/pin mutation replacement, checkpoint update, source exclusion and full materialization each occur under the shared write coordinator. Each manifest upsert and dual-bound incremental retention prune share one transaction; each scalar usage/calibration update and its independent per-profile prune do the same. Each quality upsert and bounded-retention prune also share one transaction; quality failures do not affect manifests or planning. Each changed project file is replaced transactionally with its snippets and FTS rows; embedding upserts and canonical-source pruning are transactional; artifact object/reference metadata and project deletion batches are atomic. A filesystem artifact write precedes its metadata transaction, so an interrupted metadata write may leave only an unreferenced content-addressed cache file; canonical JSONL remains sufficient for recovery. If manifest serialization, projection, retention, or SQLite writing fails, the complete current manifest remains in memory and the provider request is unchanged. Other artifact/project failures contribute no replacement/snippets; planner failures discard all synthetic evidence; Pi continues with its native context.
|
|
153
166
|
|
|
154
167
|
After bounded busy-aware replay is exhausted, DS4 emits `database.write_lock_timeout` with only the coordinator operation name, attempt count, elapsed/configured waits, and SQLite primary code. The thrown error repeats the operation and categorical lock status but never includes SQL, bound values, provider content, or the raw SQLite message. Retry and rollback diagnostics follow the same metadata-only rule.
|
|
168
|
+
|
|
169
|
+
## Growth measurements
|
|
170
|
+
|
|
171
|
+
`tests/benchmarks/storage-scale.bench.ts` seeds session indexes of 5,000 / 50,000 / 200,000 entries and measures the paths a session actually pays. Run it on demand:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
npx vitest bench tests/benchmarks/storage-scale.bench.ts
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Measured means on this development machine (Node.js 26.5.1, `node:sqlite`, one session per database):
|
|
178
|
+
|
|
179
|
+
| Path | 5k entries | 50k entries | 200k entries |
|
|
180
|
+
| --- | ---: | ---: | ---: |
|
|
181
|
+
| Exact identifier scan (`instr` over one session) | 0.97 ms | 12.5 ms | 52.7 ms |
|
|
182
|
+
| Exact phrase scan | 0.98 ms | 12.8 ms | 53.2 ms |
|
|
183
|
+
| FTS retrieval, common token | 0.14 ms | 2.3 ms | 9.0 ms |
|
|
184
|
+
| FTS retrieval, rare token | 0.06 ms | 0.15 ms | 0.94 ms |
|
|
185
|
+
| Per-session stats (`COUNT`/`SUM` for one session) | 0.33 ms | 4.8 ms | 21.8 ms |
|
|
186
|
+
| Storage diagnostics | 0.10 ms | 0.10 ms | 0.10 ms |
|
|
187
|
+
| Append-only unchanged re-check (1,000 entries) | 1.4 ms | 2.9 ms | 7.2 ms |
|
|
188
|
+
|
|
189
|
+
Growth is **real but bounded**: exact identifier and phrase scans are literal `instr()` scans over the session's rows and grow roughly linearly, passing the 50 ms typical-operation target only around 200k indexed entries in a single session. FTS retrieval and per-session aggregate SQL grow sublinearly and stay in single-digit milliseconds; bounded manifest/calibration diagnostics are flat. The dominant cost tracks a single session's size, not the file size, so `storage.scope: "project"` bounds physical growth, lock scope and reset per project but does not by itself change this per-session scan profile. Long single sessions near or above the 200k-entry range are the case where exact-identifier retrieval latency becomes measurable.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Offline SQLite Storage Maintenance
|
|
2
2
|
|
|
3
|
-
DS4 keeps `context.db` as disposable derived state, while Pi session JSONL and live project files remain canonical. Normal runtime retention stops unbounded manifest growth and makes deleted pages reusable. It does not promise that an existing high-water SQLite file shrinks physically.
|
|
3
|
+
DS4 keeps `context.db` as disposable derived state, while Pi session JSONL and live project files remain canonical. Normal runtime retention stops unbounded manifest growth and makes deleted pages reusable. It does not promise that an existing high-water SQLite file shrinks physically. With the default `storage.scope: "project"` each trusted project has its own database under `ds4-context/projects/` and the agent database keeps shared token calibration: run the maintenance commands per file, not only on the agent database (see [`STORAGE.md`](STORAGE.md)).
|
|
4
4
|
|
|
5
5
|
Physical compaction is therefore an explicit offline operation. It is never model-callable, never runs at startup, and never edits Pi JSONL or project files.
|
|
6
6
|
|
package/docs/releases/0.3.10.md
CHANGED
|
@@ -26,4 +26,4 @@ No new defaults are enabled: `chars-v1`, disabled `autoTune`, and disabled DS4 n
|
|
|
26
26
|
|
|
27
27
|
## Validation and publication
|
|
28
28
|
|
|
29
|
-
On Node 26.5.1, the coordinated release passed a clean `npm ci`, `npm run check` (97 Vitest files, 607 tests, TypeScript builds and root typecheck), deterministic `npm run quality:compare`, the `npm run schema:context-persistence` size bound, and `npm run pack:check` in a clean consumer after synchronizing both exported runtime version constants. All typecheck/tests were also rerun after that correction. Dry-run tarball review contained 243 core files, 7 reference-adapter files, and 97 extension files; none included untracked local state.
|
|
29
|
+
On Node 26.5.1, the coordinated release passed a clean `npm ci`, `npm run check` (97 Vitest files, 607 tests, TypeScript builds and root typecheck), deterministic `npm run quality:compare`, the `npm run schema:context-persistence` size bound, and `npm run pack:check` in a clean consumer after synchronizing both exported runtime version constants. All typecheck/tests were also rerun after that correction. Dry-run tarball review contained 243 core files, 7 reference-adapter files, and 97 extension files; none included untracked local state. All three packages were then published to npm at 0.3.10 in dependency order. After registry propagation, `npm run registry:check -- 0.3.10` verified all three exact-version artifacts in a fresh consumer.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Release 0.4.0 — Per-project databases with shared token calibration
|
|
2
|
+
|
|
3
|
+
**Coordinated packages:** `ds4-context-core`, `ds4-context-reference-adapter`, and `ds4-context-engine` 0.4.0.
|
|
4
|
+
**Implementation commit:** `2c2e248`.
|
|
5
|
+
**Decision record:** [ADR 064](../ADR/064-per-project-databases-with-shared-calibration.md).
|
|
6
|
+
|
|
7
|
+
## Summary
|
|
8
|
+
|
|
9
|
+
Adds the opt-out `storage.scope: "agent" | "project"` setting and defaults it to
|
|
10
|
+
`project`. Each trusted canonical project root now gets its own SQLite
|
|
11
|
+
projection next to the configured agent database, while token calibration stays
|
|
12
|
+
in the agent database and remains shared across projects. `storage.scope:
|
|
13
|
+
"agent"` restores the previous single-file layout. The reference adapter and Pi
|
|
14
|
+
extension continue to depend exactly on the matching core version.
|
|
15
|
+
|
|
16
|
+
## Changes
|
|
17
|
+
|
|
18
|
+
- `storage.scope: "project"` (new default) derives one database per trusted
|
|
19
|
+
canonical project root at `projects/<sha256(root)[0..32]>.db`, next to
|
|
20
|
+
`storage.databasePath`. Untrusted projects, broad roots (home directory,
|
|
21
|
+
filesystem root) and any resolution failure fall back to the agent database.
|
|
22
|
+
- Both files receive the same schema and the same migrations; migrations 1–15
|
|
23
|
+
are untouched and there is no schema fork. The project database holds the
|
|
24
|
+
session index, project index, context manifests, summary graph, memory/pin
|
|
25
|
+
projections, embeddings, quality samples and artifact metadata; the agent
|
|
26
|
+
database holds `token_calibration`.
|
|
27
|
+
- With the split, a calibration sample is written to the agent database with
|
|
28
|
+
`manifest_id = NULL`, exactly once per manifest. The two writes are
|
|
29
|
+
intentionally not one cross-database transaction: a lost calibration sample is
|
|
30
|
+
harmless, a lost manifest/usage consistency is not. In `agent` scope the
|
|
31
|
+
previous single transaction is unchanged.
|
|
32
|
+
- Artifact object bytes move to `projects/artifacts/<project-digest>/` under
|
|
33
|
+
project scope so the orphan garbage collector, which only sees references in
|
|
34
|
+
the current database, can never delete another project's objects.
|
|
35
|
+
- `/context storage` and `/context diagnostics` report the active project
|
|
36
|
+
database and, when split, the shared agent database. Storage maintenance
|
|
37
|
+
stays per file: `ds4-context-storage inspect|compact|recover --database <path>`
|
|
38
|
+
must be pointed at each database.
|
|
39
|
+
- The previous shared database is left untouched. With the new default, existing
|
|
40
|
+
users cold-start per-project indexes while existing calibration remains
|
|
41
|
+
available in the agent database; project indexes rebuild from canonical Pi
|
|
42
|
+
JSONL, project files and memory/pin `CustomEntry` records.
|
|
43
|
+
|
|
44
|
+
## Breaking behavior
|
|
45
|
+
|
|
46
|
+
The default storage layout changes. A pre-existing
|
|
47
|
+
`~/.pi/agent/ds4-context/context.db` becomes the agent database and is no longer
|
|
48
|
+
the active projection for new sessions. `storage.scope: "agent"` restores the
|
|
49
|
+
old single-database behavior. Rows are not migrated between files; the project
|
|
50
|
+
databases are derived and rebuildable. Decision D1 of the storage plan ("no
|
|
51
|
+
per-session or per-project databases") is superseded by ADR 064 and the plan
|
|
52
|
+
keeps the original text with an explicit amendment pointer.
|
|
53
|
+
|
|
54
|
+
## Measured scope and limitations
|
|
55
|
+
|
|
56
|
+
`tests/benchmarks/storage-scale.bench.ts` seeds one session with 5,000 /
|
|
57
|
+
50,000 / 200,000 entries and measures the paths a session pays. Measured means
|
|
58
|
+
on the development machine (Node.js 26.5.1, `node:sqlite`): exact identifier
|
|
59
|
+
scan 0.97 / 12.5 / 52.7 ms, exact phrase scan 0.98 / 12.8 / 53.2 ms, FTS
|
|
60
|
+
common token 0.14 / 2.3 / 9.0 ms, per-session stats 0.33 / 4.8 / 21.8 ms,
|
|
61
|
+
storage diagnostics flat at 0.10 ms, unchanged re-check 1.4 / 2.9 / 7.2 ms.
|
|
62
|
+
Growth is real but bounded and tracks single-session size, not file size:
|
|
63
|
+
project scope bounds physical growth, lock scope and reset per project, but does
|
|
64
|
+
not by itself change the per-session exact-scan profile. See
|
|
65
|
+
[storage growth measurements](../STORAGE.md#growth-measurements).
|
|
66
|
+
|
|
67
|
+
These are local measurements on one host, not portable guarantees. No provider
|
|
68
|
+
calls are involved in this release procedure.
|
|
69
|
+
|
|
70
|
+
## Compatibility
|
|
71
|
+
|
|
72
|
+
No provider-facing default changes: `chars-v1` remains the default estimator,
|
|
73
|
+
`modelAwareness.autoTune` and DS4 native continuation stay disabled. No SQLite
|
|
74
|
+
migration, no change to canonical Pi JSONL, privacy consent, native continuation
|
|
75
|
+
or the portable runtime-adapter contract. Restart Pi after upgrading so the new
|
|
76
|
+
compiled core and session configuration are loaded.
|
|
77
|
+
|
|
78
|
+
## Validation and publication
|
|
79
|
+
|
|
80
|
+
On Node 26.5.1, the coordinated release passed `npm run check` (99 Vitest files,
|
|
81
|
+
615 tests, TypeScript builds and root typecheck), deterministic
|
|
82
|
+
`npm run quality:compare`, the `npm run schema:context-persistence` size bound,
|
|
83
|
+
and `npm run pack:check` in a clean consumer after synchronizing both exported
|
|
84
|
+
runtime version constants. `git diff --check` was clean and dry-run tarball
|
|
85
|
+
review contained 247 core files, 7 reference-adapter files and 98 extension
|
|
86
|
+
files, none including untracked local state.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ds4-context-engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Non-destructive, provider-independent context management for Pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"quality:compare": "node scripts/compare-context-quality.mjs",
|
|
53
53
|
"schema:context-persistence": "node scripts/measure-context-persistence-schema.mjs",
|
|
54
54
|
"latency:check": "npm run build:core && node scripts/compare-disabled-planning-latency.mjs",
|
|
55
|
+
"storage:scale": "npm run build:core && vitest bench tests/benchmarks/storage-scale.bench.ts",
|
|
55
56
|
"pack:check": "node scripts/verify-packages.mjs",
|
|
56
57
|
"registry:check": "node scripts/verify-registry-packages.mjs",
|
|
57
58
|
"prepare": "npm run build:core && npm run build:adapters"
|
|
@@ -62,7 +63,7 @@
|
|
|
62
63
|
]
|
|
63
64
|
},
|
|
64
65
|
"dependencies": {
|
|
65
|
-
"ds4-context-core": "0.
|
|
66
|
+
"ds4-context-core": "0.4.0",
|
|
66
67
|
"js-tiktoken": "1.0.21"
|
|
67
68
|
},
|
|
68
69
|
"peerDependencies": {
|
|
@@ -835,13 +835,20 @@ function formatAdapter(diagnostics: RuntimeDiagnostics): string {
|
|
|
835
835
|
].join("\n");
|
|
836
836
|
}
|
|
837
837
|
|
|
838
|
-
function formatStorage(
|
|
838
|
+
function formatStorage(
|
|
839
|
+
storage: StorageDiagnostics,
|
|
840
|
+
databasePath?: string,
|
|
841
|
+
agentDatabasePath?: string,
|
|
842
|
+
): string {
|
|
843
|
+
const agentLine = agentDatabasePath && agentDatabasePath !== databasePath
|
|
844
|
+
? [`Agent database (shared): ${agentDatabasePath}`] : [];
|
|
839
845
|
if (storage.status === "unavailable") {
|
|
840
846
|
return [
|
|
841
847
|
"DS4 Storage",
|
|
842
848
|
"",
|
|
843
849
|
"Status: unavailable",
|
|
844
850
|
`Database: ${databasePath ?? "unavailable"}`,
|
|
851
|
+
...agentLine,
|
|
845
852
|
"Pi fallback remains active; no storage mutation was attempted.",
|
|
846
853
|
].join("\n");
|
|
847
854
|
}
|
|
@@ -850,6 +857,7 @@ function formatStorage(storage: StorageDiagnostics, databasePath?: string): stri
|
|
|
850
857
|
"",
|
|
851
858
|
`Status: ${storage.status}`,
|
|
852
859
|
`Database: ${databasePath ?? "unavailable"}`,
|
|
860
|
+
...agentLine,
|
|
853
861
|
`Schema / journal: ${storage.schemaVersion ?? "n/a"} / ${storage.journalMode ?? "n/a"}`,
|
|
854
862
|
`Database / WAL / SHM: ${bytes(storage.databaseBytes)} / ${bytes(storage.walBytes)} / ${bytes(storage.shmBytes)}`,
|
|
855
863
|
`Allocated / reusable: ${bytes(storage.allocatedBytes)} / ${bytes(storage.reusableBytes)}`,
|
|
@@ -1234,7 +1242,7 @@ export function registerContextCommand(pi: ExtensionAPI, runtime: Ds4ContextRunt
|
|
|
1234
1242
|
const storage = runtime.storageDiagnostics();
|
|
1235
1243
|
present(
|
|
1236
1244
|
ctx,
|
|
1237
|
-
formatStorage(storage, diagnostics.databasePath),
|
|
1245
|
+
formatStorage(storage, diagnostics.databasePath, diagnostics.agentDatabasePath),
|
|
1238
1246
|
storage.status === "ok" ? "info" : "warning",
|
|
1239
1247
|
);
|
|
1240
1248
|
return;
|
package/src/extension/runtime.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import { dirname, join, parse, resolve } from "node:path";
|
|
4
|
+
import { basename, dirname, join, parse, resolve } from "node:path";
|
|
5
5
|
import type {
|
|
6
6
|
Api,
|
|
7
7
|
AssistantMessage,
|
|
@@ -46,11 +46,12 @@ export type {
|
|
|
46
46
|
import {
|
|
47
47
|
loadConfig,
|
|
48
48
|
resolveDatabasePath,
|
|
49
|
+
resolveProjectDatabasePath as deriveProjectDatabasePath,
|
|
49
50
|
resolveRankingModelPath,
|
|
50
51
|
validateConfigFile,
|
|
51
52
|
type LoadedConfig,
|
|
52
53
|
} from "ds4-context-core/config/config-loader";
|
|
53
|
-
import { CONFIG_SCHEMA_VERSION, createDefaultConfig, type Ds4ContextConfig } from "ds4-context-core/config/config";
|
|
54
|
+
import { CONFIG_SCHEMA_VERSION, createDefaultConfig, type Ds4ContextConfig, type StorageScope } from "ds4-context-core/config/config";
|
|
54
55
|
import {
|
|
55
56
|
applyConfigValue,
|
|
56
57
|
findConfigField,
|
|
@@ -387,6 +388,8 @@ export interface RuntimeDiagnostics {
|
|
|
387
388
|
session?: PiSessionSnapshot;
|
|
388
389
|
model?: { provider: string; id: string };
|
|
389
390
|
databasePath?: string;
|
|
391
|
+
/** Present only when storage.scope splits calibration from project state. */
|
|
392
|
+
agentDatabasePath?: string;
|
|
390
393
|
databaseSchemaVersion?: number;
|
|
391
394
|
indexed?: SessionIndexStats;
|
|
392
395
|
observation?: ContextObservation;
|
|
@@ -426,8 +429,10 @@ export class Ds4ContextRuntime {
|
|
|
426
429
|
private loadedConfig?: LoadedConfig;
|
|
427
430
|
private session?: PiSessionSnapshot;
|
|
428
431
|
private database?: ContextDatabase;
|
|
432
|
+
private agentDatabase?: ContextDatabase;
|
|
429
433
|
private indexer?: PiSessionIndexer;
|
|
430
434
|
private databasePath?: string;
|
|
435
|
+
private agentDatabasePath?: string;
|
|
431
436
|
private observation?: ContextObservation;
|
|
432
437
|
private lastManifest?: ContextManifest;
|
|
433
438
|
private lastPersistedInventory?: PersistedManifestInventory;
|
|
@@ -579,17 +584,33 @@ export class Ds4ContextRuntime {
|
|
|
579
584
|
return;
|
|
580
585
|
}
|
|
581
586
|
|
|
582
|
-
|
|
587
|
+
const agentDatabasePath = resolveDatabasePath(
|
|
583
588
|
this.config.storage.databasePath,
|
|
584
589
|
this.dependencies.agentDir,
|
|
585
590
|
this.dependencies.homeDir,
|
|
586
591
|
);
|
|
592
|
+
this.agentDatabasePath = agentDatabasePath;
|
|
593
|
+
const projectDatabasePath = this.resolveProjectDatabasePath(
|
|
594
|
+
this.config.storage.scope,
|
|
595
|
+
ctx,
|
|
596
|
+
agentDatabasePath,
|
|
597
|
+
);
|
|
598
|
+
this.databasePath = projectDatabasePath ?? agentDatabasePath;
|
|
599
|
+
if (projectDatabasePath) {
|
|
600
|
+
this.agentDatabase = ContextDatabase.open(agentDatabasePath, {
|
|
601
|
+
logger: this.logger,
|
|
602
|
+
now: this.now(),
|
|
603
|
+
busyTimeoutMs: this.config.storage.busyTimeoutMs,
|
|
604
|
+
writeRetryTimeoutMs: this.config.storage.writeRetryTimeoutMs,
|
|
605
|
+
});
|
|
606
|
+
}
|
|
587
607
|
this.database = ContextDatabase.open(this.databasePath, {
|
|
588
608
|
logger: this.logger,
|
|
589
609
|
now: this.now(),
|
|
590
610
|
busyTimeoutMs: this.config.storage.busyTimeoutMs,
|
|
591
611
|
writeRetryTimeoutMs: this.config.storage.writeRetryTimeoutMs,
|
|
592
612
|
});
|
|
613
|
+
this.agentDatabase ??= this.database;
|
|
593
614
|
this.initializeRanking();
|
|
594
615
|
|
|
595
616
|
this.indexer = new PiSessionIndexer(this.database.sessionIndex, {
|
|
@@ -765,10 +786,25 @@ export class Ds4ContextRuntime {
|
|
|
765
786
|
? O200K_ESTIMATOR : CHARS_ESTIMATOR;
|
|
766
787
|
}
|
|
767
788
|
|
|
789
|
+
private resolveProjectDatabasePath(
|
|
790
|
+
scope: StorageScope,
|
|
791
|
+
ctx: ExtensionContext,
|
|
792
|
+
agentDatabasePath: string,
|
|
793
|
+
): string | undefined {
|
|
794
|
+
if (scope !== "project") return undefined;
|
|
795
|
+
// Untrusted projects ignore project configuration; they also fall back to
|
|
796
|
+
// the shared agent database instead of creating a new physical boundary.
|
|
797
|
+
if (!ctx.isProjectTrusted()) return undefined;
|
|
798
|
+
const projectRoot = canonicalProjectPath(ctx.cwd);
|
|
799
|
+
if (isBroadProjectRoot(projectRoot, this.dependencies.homeDir ?? homedir())) return undefined;
|
|
800
|
+
const projectDatabasePath = deriveProjectDatabasePath(agentDatabasePath, projectRoot);
|
|
801
|
+
return projectDatabasePath === agentDatabasePath ? undefined : projectDatabasePath;
|
|
802
|
+
}
|
|
803
|
+
|
|
768
804
|
private calibrationSamples(model: ModelDescriptor): TokenCalibrationSample[] {
|
|
769
805
|
const version = this.estimatorForModel(model).version;
|
|
770
806
|
if (this.database && this.session?.sessionFile && this.config.diagnostics.storeContextManifest) {
|
|
771
|
-
return this.database.
|
|
807
|
+
return (this.agentDatabase ?? this.database).calibrations.list(
|
|
772
808
|
model.provider,
|
|
773
809
|
model.id,
|
|
774
810
|
this.config.modelAwareness.calibrationWindow,
|
|
@@ -1884,13 +1920,39 @@ export class Ds4ContextRuntime {
|
|
|
1884
1920
|
const createdAt = this.now();
|
|
1885
1921
|
try {
|
|
1886
1922
|
if (manifestPersisted && this.database) {
|
|
1887
|
-
const
|
|
1923
|
+
const estimatorVersion = manifest?.modelAwareness?.calibration.estimator ?? "chars-v1";
|
|
1924
|
+
// With storage.scope=project the manifest lives in the project database
|
|
1925
|
+
// while calibration stays shared in the agent database. The two writes
|
|
1926
|
+
// are intentionally not one transaction: losing one sample is harmless,
|
|
1927
|
+
// losing manifest/usage consistency is not.
|
|
1928
|
+
const calibrationDatabase = this.agentDatabase !== this.database
|
|
1929
|
+
? this.agentDatabase : undefined;
|
|
1930
|
+
const updated = this.database.manifests.recordProviderUsageOutcome(
|
|
1888
1931
|
manifestId,
|
|
1889
1932
|
providerUsage,
|
|
1890
1933
|
createdAt,
|
|
1891
|
-
|
|
1934
|
+
estimatorVersion,
|
|
1935
|
+
{ writeCalibration: calibrationDatabase === undefined },
|
|
1892
1936
|
);
|
|
1893
|
-
if (
|
|
1937
|
+
if (updated.outcome === "recorded" && calibrationDatabase) {
|
|
1938
|
+
const source = this.database.manifests.calibrationSource(manifestId);
|
|
1939
|
+
const recorded = source
|
|
1940
|
+
? calibrationDatabase.calibrations.record({
|
|
1941
|
+
provider: source.provider,
|
|
1942
|
+
model: source.model,
|
|
1943
|
+
estimatedTokens: source.estimatedTokens,
|
|
1944
|
+
actualInputTokens: providerUsage.totalInputTokens,
|
|
1945
|
+
inputTokens: providerUsage.inputTokens,
|
|
1946
|
+
cacheReadTokens: providerUsage.cacheReadTokens,
|
|
1947
|
+
cacheWriteTokens: providerUsage.cacheWriteTokens,
|
|
1948
|
+
createdAt,
|
|
1949
|
+
estimatorVersion,
|
|
1950
|
+
})
|
|
1951
|
+
: false;
|
|
1952
|
+
if (!recorded && manifest?.estimatedInputTokens) {
|
|
1953
|
+
this.rememberVolatileCalibration(manifest, providerUsage, createdAt);
|
|
1954
|
+
}
|
|
1955
|
+
} else if (!updated.manifest && manifest?.estimatedInputTokens) {
|
|
1894
1956
|
this.rememberVolatileCalibration(manifest, providerUsage, createdAt);
|
|
1895
1957
|
}
|
|
1896
1958
|
} else if (manifest?.estimatedInputTokens) {
|
|
@@ -2707,10 +2769,13 @@ export class Ds4ContextRuntime {
|
|
|
2707
2769
|
return;
|
|
2708
2770
|
}
|
|
2709
2771
|
try {
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2772
|
+
// Artifact bytes and their metadata must share one boundary: the orphan
|
|
2773
|
+
// garbage collector only sees references in the current database, so a
|
|
2774
|
+
// shared store would let one project delete another project's objects.
|
|
2775
|
+
const storeRoot = this.agentDatabase !== this.database && this.databasePath
|
|
2776
|
+
? join(dirname(this.databasePath), "artifacts", basename(this.databasePath, ".db"))
|
|
2777
|
+
: join(this.dependencies.agentDir, "ds4-context", "artifacts");
|
|
2778
|
+
const store = new FileArtifactStore(storeRoot, this.now);
|
|
2714
2779
|
this.artifactManager = new ArtifactManager(
|
|
2715
2780
|
store,
|
|
2716
2781
|
this.database.artifacts,
|
|
@@ -3295,6 +3360,8 @@ export class Ds4ContextRuntime {
|
|
|
3295
3360
|
session: currentSession,
|
|
3296
3361
|
...(ctx.model ? { model: { provider: ctx.model.provider, id: ctx.model.id } } : {}),
|
|
3297
3362
|
...(this.databasePath ? { databasePath: this.databasePath } : {}),
|
|
3363
|
+
...(this.agentDatabasePath && this.agentDatabasePath !== this.databasePath
|
|
3364
|
+
? { agentDatabasePath: this.agentDatabasePath } : {}),
|
|
3298
3365
|
...(this.database ? { databaseSchemaVersion: this.database.schemaVersion } : {}),
|
|
3299
3366
|
...(indexed ? { indexed } : {}),
|
|
3300
3367
|
...(this.observation ? { observation: this.observation } : {}),
|
|
@@ -3332,7 +3399,9 @@ export class Ds4ContextRuntime {
|
|
|
3332
3399
|
}
|
|
3333
3400
|
|
|
3334
3401
|
storageDiagnostics(): StorageDiagnostics {
|
|
3335
|
-
|
|
3402
|
+
const calibrationDatabase = this.agentDatabase && this.agentDatabase !== this.database
|
|
3403
|
+
? this.agentDatabase : undefined;
|
|
3404
|
+
return this.database?.storageDiagnostics(this.session?.projectPath, calibrationDatabase)
|
|
3336
3405
|
?? unavailableStorageDiagnostics();
|
|
3337
3406
|
}
|
|
3338
3407
|
|
|
@@ -3459,17 +3528,25 @@ export class Ds4ContextRuntime {
|
|
|
3459
3528
|
try {
|
|
3460
3529
|
this.database?.close();
|
|
3461
3530
|
} finally {
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3531
|
+
try {
|
|
3532
|
+
if (this.agentDatabase && this.agentDatabase !== this.database) {
|
|
3533
|
+
this.agentDatabase.close();
|
|
3534
|
+
}
|
|
3535
|
+
} finally {
|
|
3536
|
+
this.agentDatabase = undefined;
|
|
3537
|
+
this.agentDatabasePath = undefined;
|
|
3538
|
+
this.compaction = undefined;
|
|
3539
|
+
this.retrievalEngine = undefined;
|
|
3540
|
+
this.projectKnowledge = undefined;
|
|
3541
|
+
this.projectRefreshPending = false;
|
|
3542
|
+
this.memoryManager = undefined;
|
|
3543
|
+
this.projectMemorySynchronizer = undefined;
|
|
3544
|
+
this.lastCrossSessionMemory = disabledCrossSessionMemoryDiagnostics();
|
|
3545
|
+
this.lastMemoryMutationSignature = undefined;
|
|
3546
|
+
this.artifactManager = undefined;
|
|
3547
|
+
this.indexer = undefined;
|
|
3548
|
+
this.database = undefined;
|
|
3549
|
+
}
|
|
3473
3550
|
}
|
|
3474
3551
|
}
|
|
3475
3552
|
|