ds4-context-engine 0.4.5 → 0.4.6
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 +2 -1
- package/docs/ADR/067-core-version-guard.md +73 -0
- package/docs/ADR/README.md +1 -0
- package/docs/COMPACTION.md +2 -0
- package/docs/releases/0.4.5.md +19 -0
- package/docs/releases/0.4.6.md +137 -0
- package/package.json +2 -2
- package/src/extension/index.ts +1 -0
- package/src/extension/runtime.ts +57 -28
- package/src/pi-adapter/compaction-coordinator.ts +14 -1
- package/src/pi-adapter/core-compatibility.ts +98 -0
- 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.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`. Patch `0.4.1` states the contiguous-span rule for compaction summaries in the summarizer prompt while leaving validation strictness, the eight-bullet repair bound and every provider-facing default unchanged. Patch `0.4.2` adds class-only diagnostics for rejected exact-value spans: the existing `custom_fallback` warning carries `unsupportedSpanClasses` as class names and counters, never span text, with validation, repair bounds and provider-facing defaults unchanged. Patch `0.4.3` makes those diagnostics interpretable: every distinct span receives the cheap transformed-form lookups before the bounded near-miss analysis starts, each near-miss candidate may spend only an equal share of what remains, and spans that were not analysed report `not-classified-length`, `not-classified-partial` or `not-classified-budget` instead of `no-near-miss`. Patch `0.4.4` accepts a backticked exact value whose canonical JSON-escaped rendering is literally present in the evidence — the decoded rendering a summarizer produces when it quotes serialized JSON — while absent values, the reverse escape direction, span composition and single-character variants stay rejected; the diagnostics probe budget rises from 4000 to 24000 evidence-source scans. Patch `0.4.5` grades the summarizer quoting fallback: the prompt requires one contiguous excerpt per backticked span, tells the model to backtick only the fragments that are themselves contiguous or to keep the fact as ordinary text without backticks instead of composing a span, and reserves bullet omission for facts with no support in the evidence; diagnostics split composition into `composed-adjacent-present` (both parts next to each other in one source, so the span may be a re-rendering of a contiguous region) and `composed-two-present-parts` (parts found at unrelated positions), with validation strictness, the eight-bullet and 25% repair bounds and every provider-facing default unchanged. See the [0.4.5 release record](docs/releases/0.4.5.md), the [0.4.4 release record](docs/releases/0.4.4.md), the [0.4.3 release record](docs/releases/0.4.3.md), the [0.4.2 release record](docs/releases/0.4.2.md), the [0.4.1 release record](docs/releases/0.4.1.md) and the [0.4.0 release record](docs/releases/0.4.0.md), [ADR 066](docs/ADR/066-quoting-downgrade-and-span-adjacency.md), [ADR 065](docs/ADR/065-exact-value-escaped-equivalence.md), [ADR 064](docs/ADR/064-per-project-databases-with-shared-calibration.md) and [model-awareness validation](docs/MODEL_AWARENESS.md).
|
|
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`. Patch `0.4.1` states the contiguous-span rule for compaction summaries in the summarizer prompt while leaving validation strictness, the eight-bullet repair bound and every provider-facing default unchanged. Patch `0.4.2` adds class-only diagnostics for rejected exact-value spans: the existing `custom_fallback` warning carries `unsupportedSpanClasses` as class names and counters, never span text, with validation, repair bounds and provider-facing defaults unchanged. Patch `0.4.3` makes those diagnostics interpretable: every distinct span receives the cheap transformed-form lookups before the bounded near-miss analysis starts, each near-miss candidate may spend only an equal share of what remains, and spans that were not analysed report `not-classified-length`, `not-classified-partial` or `not-classified-budget` instead of `no-near-miss`. Patch `0.4.4` accepts a backticked exact value whose canonical JSON-escaped rendering is literally present in the evidence — the decoded rendering a summarizer produces when it quotes serialized JSON — while absent values, the reverse escape direction, span composition and single-character variants stay rejected; the diagnostics probe budget rises from 4000 to 24000 evidence-source scans. Patch `0.4.5` grades the summarizer quoting fallback: the prompt requires one contiguous excerpt per backticked span, tells the model to backtick only the fragments that are themselves contiguous or to keep the fact as ordinary text without backticks instead of composing a span, and reserves bullet omission for facts with no support in the evidence; diagnostics split composition into `composed-adjacent-present` (both parts next to each other in one source, so the span may be a re-rendering of a contiguous region) and `composed-two-present-parts` (parts found at unrelated positions), with validation strictness, the eight-bullet and 25% repair bounds and every provider-facing default unchanged. Patch `0.4.6` makes an engine/core artifact mismatch visible instead of a missing function: the core exports `CORE_VERSION` and the engine verifies at session start and at every compaction attempt that the loaded core matches its own version and exposes the entry points it calls, so a core rebuilt under a running Pi — which a `/reload` does not pick up — reports one actionable line, keeps the DS4 compaction layer inert instead of failing mid-generation, and never blocks Pi; validation strictness, repair bounds and every provider-facing default stay unchanged. See the [0.4.6 release record](docs/releases/0.4.6.md), the [0.4.5 release record](docs/releases/0.4.5.md), the [0.4.4 release record](docs/releases/0.4.4.md), the [0.4.3 release record](docs/releases/0.4.3.md), the [0.4.2 release record](docs/releases/0.4.2.md), the [0.4.1 release record](docs/releases/0.4.1.md) and the [0.4.0 release record](docs/releases/0.4.0.md), [ADR 067](docs/ADR/067-core-version-guard.md), [ADR 066](docs/ADR/066-quoting-downgrade-and-span-adjacency.md), [ADR 065](docs/ADR/065-exact-value-escaped-equivalence.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
|
|
|
@@ -515,6 +515,7 @@ scripts package and release-readiness checks
|
|
|
515
515
|
- [Roadmap 0.2.0](docs/ROADMAP_0.2.0.md)
|
|
516
516
|
- [Release process](docs/RELEASING.md)
|
|
517
517
|
- [0.2.0 release readiness](docs/RELEASE_READINESS_0.2.0.md)
|
|
518
|
+
- [0.4.6 release notes](docs/releases/0.4.6.md)
|
|
518
519
|
- [0.4.5 release notes](docs/releases/0.4.5.md)
|
|
519
520
|
- [0.4.4 release notes](docs/releases/0.4.4.md)
|
|
520
521
|
- [0.4.3 release notes](docs/releases/0.4.3.md)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# 067 — Detect an engine/core artifact mismatch before compaction runs
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-09-27
|
|
4
|
+
**Status:** Accepted
|
|
5
|
+
**Related:** [`docs/RELEASING.md`](../RELEASING.md)
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
A machine reported, right after a Pi `/reload`:
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
Warning: DS4 compaction unavailable; using Pi default.
|
|
13
|
+
(0 , _summaryContract.classifyUnsupportedExactValueSpans) is not a function
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The message was read as an installation mismatch, but every artifact on disk was
|
|
17
|
+
current: `packages/core/dist/compaction/summary-contract.js` in the checkout and
|
|
18
|
+
the npm-installed `ds4-context-core@0.4.3` both export the symbol (the core is
|
|
19
|
+
ESM, so the export is an `export function` declaration, not an
|
|
20
|
+
`exports.<name>` assignment). No stale file existed.
|
|
21
|
+
|
|
22
|
+
The remaining explanation is process-local module state. Pi loads extension
|
|
23
|
+
sources through jiti (`dist/core/extensions/loader.js`) and re-imports the
|
|
24
|
+
extension entry when its cache generation changes — which is what `/reload`
|
|
25
|
+
does — while modules that were already evaluated for that process, including the
|
|
26
|
+
`ds4-context-core` dependency, keep their loaded instance. An extension source
|
|
27
|
+
newer than the core module instance it resolved therefore calls a symbol that
|
|
28
|
+
the cached core does not have. The failure costs the whole compaction attempt and
|
|
29
|
+
reports a symptom (`is not a function`) that points at DS4 rather than at the
|
|
30
|
+
process state.
|
|
31
|
+
|
|
32
|
+
## Decision
|
|
33
|
+
|
|
34
|
+
- `packages/core/src/version.ts` exports `CORE_VERSION`, bumped with the two
|
|
35
|
+
adapters by the coordinated release process.
|
|
36
|
+
- `src/pi-adapter/core-compatibility.ts` inspects the engine↔core contract at
|
|
37
|
+
runtime: version equality plus the presence of the core entry points this
|
|
38
|
+
extension calls (`buildSummaryPrompt`,
|
|
39
|
+
`classifyUnsupportedExactValueSpans`). The inspection is a pure function, so
|
|
40
|
+
it is testable without a broken installation.
|
|
41
|
+
- The guard runs on session start — so a `/reload` reports the state
|
|
42
|
+
immediately, before any compaction is attempted — and again as the first
|
|
43
|
+
statement of the guarded compaction attempt.
|
|
44
|
+
- On a mismatch the engine logs `runtime.core_incompatible`, notifies once with
|
|
45
|
+
a single-line actionable message, sets the runtime `lastError`, and **does not
|
|
46
|
+
create the compaction coordinator**: DS4 compaction stays inert, `/context
|
|
47
|
+
compaction` reports `enabled: false` with the reason, and Pi compacting with
|
|
48
|
+
its own default is the only behaviour left rather than a half-finished DS4
|
|
49
|
+
run.
|
|
50
|
+
- The guard never throws from the extension load path: Pi treats an extension
|
|
51
|
+
load error as fatal (`main.js` exits with code 1), so a stale core must not
|
|
52
|
+
prevent Pi from starting.
|
|
53
|
+
- The message carries both remedies in order: rebuild the checkout
|
|
54
|
+
(`npm ci && npm run build:core && npm run build:adapters`) or update the
|
|
55
|
+
installed package (`pi update --extensions`), then **restart Pi**, because a
|
|
56
|
+
`/reload` keeps the already-loaded core module.
|
|
57
|
+
|
|
58
|
+
## Consequences
|
|
59
|
+
|
|
60
|
+
- A stale core no longer surfaces as `... is not a function`; it surfaces as one
|
|
61
|
+
line naming both versions, the missing entry point and the exact command to
|
|
62
|
+
run.
|
|
63
|
+
- `CORE_VERSION` must stay imported from the package root. A *missing file* is a
|
|
64
|
+
module-resolution failure, while a *missing named export* of an existing module
|
|
65
|
+
arrives as `undefined` through the CommonJS interop Pi uses for extension
|
|
66
|
+
sources — which is the only form the guard can inspect.
|
|
67
|
+
- Version equality is strict, which matches the synchronized versioning rule:
|
|
68
|
+
after a version bump the core must be rebuilt before the extension is used.
|
|
69
|
+
- The guard detects and bounds the failure; it does not invalidate the module
|
|
70
|
+
cache. A rebuilt core still requires a restarted Pi process, and `/reload`
|
|
71
|
+
alone remains insufficient for core changes. Shipping the extension as a
|
|
72
|
+
self-contained artifact would remove that dependency, and is left as a
|
|
73
|
+
separate packaging decision.
|
package/docs/ADR/README.md
CHANGED
|
@@ -70,5 +70,6 @@ The initial decisions from the development plan are accepted:
|
|
|
70
70
|
| [064](064-per-project-databases-with-shared-calibration.md) | Split project state into per-project databases and keep token calibration shared | Accepted |
|
|
71
71
|
| [065](065-exact-value-escaped-equivalence.md) | Accept canonical JSON-escaped exact values in compaction summary validation | Accepted |
|
|
72
72
|
| [066](066-quoting-downgrade-and-span-adjacency.md) | Grade the summarizer quoting fallback and separate adjacent from unrelated span composition | Accepted |
|
|
73
|
+
| [067](067-core-version-guard.md) | Detect an engine/core artifact mismatch before compaction runs | Accepted |
|
|
73
74
|
|
|
74
75
|
Each decision will receive a dedicated record when implementation pressure introduces alternatives or consequences not already covered by the development plan.
|
package/docs/COMPACTION.md
CHANGED
|
@@ -42,6 +42,8 @@ Three relations mark spans that were *not* analysed, and they must never be read
|
|
|
42
42
|
|
|
43
43
|
## Provenance and recovery
|
|
44
44
|
|
|
45
|
+
The engine checks its own contract with the loaded core at session start and at the start of every compaction attempt: `CORE_VERSION` must equal the extension version and the core entry points the extension calls must be present. Because Pi loads extension sources from TypeScript but keeps dependency modules loaded for the life of the process, a core rebuilt under a running Pi stays stale in memory and a `/reload` is not enough to pick it up; without the check the first missing export surfaced as `... is not a function` deep inside generation. A mismatch now logs `runtime.core_incompatible`, notifies once with the versions and the failing entry point, and leaves the compaction coordinator uncreated, so `/context compaction` reports `enabled: false` with that reason and Pi's own compaction is the only behaviour left. The guard is detection only: it never changes validation, repair bounds or the fail-closed decision, and it never throws while the extension is loading, because Pi treats an extension load error as fatal ([ADR 067](ADR/067-core-version-guard.md)).
|
|
46
|
+
|
|
45
47
|
`CompactionEntry.details` contains cumulative `readFiles` and `modifiedFiles` plus:
|
|
46
48
|
|
|
47
49
|
- summary and contract versions;
|
package/docs/releases/0.4.5.md
CHANGED
|
@@ -124,3 +124,22 @@ TypeScript builds and root typecheck), deterministic `npm run quality:compare`
|
|
|
124
124
|
(`passed: true`), and `npm run pack:check` in a clean consumer for all three
|
|
125
125
|
packages at 0.4.5 (247 core files, 7 reference-adapter files, 104 extension
|
|
126
126
|
files). No provider calls are involved in this release procedure.
|
|
127
|
+
|
|
128
|
+
## Publication and registry verification
|
|
129
|
+
|
|
130
|
+
Published manually in dependency order from `2366c98`: `ds4-context-core@0.4.5`,
|
|
131
|
+
then `ds4-context-reference-adapter@0.4.5`, then `ds4-context-engine@0.4.5`, all
|
|
132
|
+
with the default `latest` tag, after `npm run pack:check` on the same commit.
|
|
133
|
+
|
|
134
|
+
`npm run registry:check -- 0.4.5`, run with a fresh npm cache, reported
|
|
135
|
+
"Verified all DS4 registry packages at exact version 0.4.5" on the first
|
|
136
|
+
attempt: all three packages install in a clean consumer, the exact
|
|
137
|
+
adapter/core dependencies resolve, the public core and KV exports import, the
|
|
138
|
+
compiled reference conformance and packaged quality corpus run, the
|
|
139
|
+
`ds4-context-storage` CLI shim responds, and the published Pi extension starts
|
|
140
|
+
against isolated offline RPC state. For all three packages
|
|
141
|
+
`npm view <package> dist-tags.latest` resolves to 0.4.5.
|
|
142
|
+
|
|
143
|
+
Annotated tag `v0.4.5` and the GitHub Release were created from `2366c98`. No
|
|
144
|
+
provider calls are involved in this release procedure; no session data,
|
|
145
|
+
credentials, or provider payloads were read or written while preparing it.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Release 0.4.6 — Detect an engine/core artifact mismatch before compaction runs
|
|
2
|
+
|
|
3
|
+
**Coordinated packages:** `ds4-context-core`, `ds4-context-reference-adapter`, and `ds4-context-engine` 0.4.6.
|
|
4
|
+
**Implementation commit:** `225078f`.
|
|
5
|
+
|
|
6
|
+
## Summary
|
|
7
|
+
|
|
8
|
+
Diagnostic and packaging patch. The core now exports `CORE_VERSION` and the
|
|
9
|
+
extension verifies, at session start and at the beginning of every compaction
|
|
10
|
+
attempt, that the core it actually loaded matches its own version and exposes the
|
|
11
|
+
entry points it calls. A mismatch — typically a core rebuilt in a checkout while
|
|
12
|
+
Pi kept the previously loaded module instance, which a `/reload` does not
|
|
13
|
+
replace — no longer surfaces deep inside summary generation as
|
|
14
|
+
`(0 , _summaryContract.x) is not a function`. It reports one actionable line,
|
|
15
|
+
keeps the DS4 compaction layer inert instead of failing mid-generation, and
|
|
16
|
+
leaves Pi compacting with its own default. Validation strictness, the
|
|
17
|
+
eight-bullet and 25% repair bounds, the fail-closed fallback, the SQLite schema,
|
|
18
|
+
canonical JSONL history and every provider-facing default are unchanged.
|
|
19
|
+
|
|
20
|
+
## Changes
|
|
21
|
+
|
|
22
|
+
- `packages/core/src/version.ts` (new): `CORE_VERSION`, re-exported from the core
|
|
23
|
+
package root and bumped with the two adapters by the coordinated release
|
|
24
|
+
process.
|
|
25
|
+
- `src/pi-adapter/core-compatibility.ts` (new): `inspectCoreCompatibility`
|
|
26
|
+
(pure), `coreCompatibilityIssues` (real probe), `coreCompatibilityMessage` and
|
|
27
|
+
`assertCoreCompatibility`. The probe compares `CORE_VERSION` with
|
|
28
|
+
`EXTENSION_VERSION` and checks that `buildSummaryPrompt` and
|
|
29
|
+
`classifyUnsupportedExactValueSpans` are functions, naming the exact missing
|
|
30
|
+
entry point.
|
|
31
|
+
- `src/extension/runtime.ts`: session start runs the probe once. On a mismatch it
|
|
32
|
+
logs `runtime.core_incompatible`, notifies a single line and sets the runtime
|
|
33
|
+
`lastError`; the compaction coordinator is then not created, so `/context
|
|
34
|
+
compaction` reports `enabled: false` with that reason instead of a half-run
|
|
35
|
+
batch. `RuntimeDependencies` gained an optional `coreCompatibility` seam for
|
|
36
|
+
tests.
|
|
37
|
+
- `src/pi-adapter/compaction-coordinator.ts`: the same guard runs as the first
|
|
38
|
+
statement of the guarded compaction attempt, so the existing coordinator catch
|
|
39
|
+
turns it into the ordinary `compaction.custom_fallback` warning. The
|
|
40
|
+
coordinator gained an optional `checkCoreCompatibility` seam.
|
|
41
|
+
- `tests/unit/core-compatibility.test.ts` (new, 6 cases): synchronized core
|
|
42
|
+
accepted, older version named on both sides, missing `CORE_VERSION` reported,
|
|
43
|
+
missing entry point named instead of an `is not a function`, single-line remedy,
|
|
44
|
+
and the real probe passing against the core this repository resolves.
|
|
45
|
+
- `tests/unit/compaction-optimizations.test.ts`: a simulated stale core reaches
|
|
46
|
+
`beforeCompact` as a fallback warning with no provider call.
|
|
47
|
+
- `tests/integration/extension.test.ts`: with a simulated stale core, session
|
|
48
|
+
start warns with the remedy, `/context compaction` diagnostics report
|
|
49
|
+
`enabled: false` and the reason, and the compaction hook stays inert.
|
|
50
|
+
- `docs/COMPACTION.md`, `docs/ADR/067-core-version-guard.md` and the ADR index
|
|
51
|
+
record the decision.
|
|
52
|
+
|
|
53
|
+
## Why
|
|
54
|
+
|
|
55
|
+
A production machine reported, immediately after a Pi `/reload`:
|
|
56
|
+
|
|
57
|
+
```text
|
|
58
|
+
Warning: DS4 compaction unavailable; using Pi default.
|
|
59
|
+
(0 , _summaryContract.classifyUnsupportedExactValueSpans) is not a function
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Every artifact on disk was current: the checkout's `packages/core/dist` and the
|
|
63
|
+
npm-installed `ds4-context-core@0.4.3` both export the symbol. Pi loads extension
|
|
64
|
+
sources through jiti and re-imports the extension entry when its cache generation
|
|
65
|
+
changes, while modules already evaluated for that process keep their loaded
|
|
66
|
+
instance, so an extension source newer than the core module it resolved calls a
|
|
67
|
+
symbol the loaded core does not have. The failure consumed the whole compaction
|
|
68
|
+
attempt and pointed at DS4 rather than at the process state.
|
|
69
|
+
|
|
70
|
+
The guard converts that class of mismatch into a single line that names both
|
|
71
|
+
versions, the missing entry point and the two remedies — rebuild the checkout
|
|
72
|
+
(`npm ci && npm run build:core && npm run build:adapters`) or update the installed
|
|
73
|
+
package (`pi update --extensions`) — followed by the step that actually makes a
|
|
74
|
+
rebuilt core visible: restart Pi. It is deliberately not on the extension load
|
|
75
|
+
path, because Pi treats an extension load error as fatal and a stale core must
|
|
76
|
+
never keep Pi from starting, and it is detection only: nothing about validation,
|
|
77
|
+
repair bounds or the fail-closed decision changes
|
|
78
|
+
([ADR 067](ADR/067-core-version-guard.md)).
|
|
79
|
+
|
|
80
|
+
## Privacy and safety
|
|
81
|
+
|
|
82
|
+
- The guard reads module state only: two version strings and the presence of two
|
|
83
|
+
exported functions. It never reads session JSONL, the SQLite projection,
|
|
84
|
+
credentials, provider payloads or prompt text, and the message contains no
|
|
85
|
+
session content.
|
|
86
|
+
- `runtime.core_incompatible` and the fallback warning carry the same message
|
|
87
|
+
that the user sees; no span text, file content or provider data is logged.
|
|
88
|
+
- No provider call is introduced by this release, and none was made to prepare
|
|
89
|
+
it.
|
|
90
|
+
|
|
91
|
+
## Compatibility and persistence
|
|
92
|
+
|
|
93
|
+
- No configuration key is added or default changed. `CORE_VERSION` is additive;
|
|
94
|
+
a core older than 0.4.6 simply fails the version probe, which is what the guard
|
|
95
|
+
reports.
|
|
96
|
+
- SQLite schema 16, migrations, the Pi JSONL boundary, tool contracts and the
|
|
97
|
+
reference adapter's public surface are untouched.
|
|
98
|
+
- Version equality is strict because the three packages are released in lockstep:
|
|
99
|
+
after bumping versions, the core must be rebuilt before the extension runs.
|
|
100
|
+
|
|
101
|
+
## Measured scope and limitations
|
|
102
|
+
|
|
103
|
+
- Detection only. The guard cannot invalidate Pi's module cache: on a machine
|
|
104
|
+
whose running Pi already holds a stale core, the next `/reload` reports the
|
|
105
|
+
mismatch and disables the DS4 compaction layer, and the DS4 core in that
|
|
106
|
+
process is only replaced by restarting Pi.
|
|
107
|
+
- The guard covers the entry points the extension already calls. A future core
|
|
108
|
+
symbol the engine starts requiring must be added to `REQUIRED_CORE_EXPORTS`
|
|
109
|
+
(or fail through the version check when versions differ).
|
|
110
|
+
- Shipping the extension as a self-contained artifact would remove the
|
|
111
|
+
process-cache dependency entirely; that packaging decision is left open in
|
|
112
|
+
ADR 067.
|
|
113
|
+
|
|
114
|
+
## Validation
|
|
115
|
+
|
|
116
|
+
- `npm run check` on the release commit: builds, `tsc --noEmit`, and
|
|
117
|
+
**100 test files / 642 tests passed** (8 new tests: 6 unit guard cases, 1
|
|
118
|
+
coordinator case, 1 extension integration case).
|
|
119
|
+
- `npm run pack:check` in a clean consumer: `ds4-context-core@0.4.6`
|
|
120
|
+
(251 files), `ds4-context-reference-adapter@0.4.6` (7 files) and
|
|
121
|
+
`ds4-context-engine@0.4.6` (108 files).
|
|
122
|
+
- `npm run quality:compare`: candidate `task-weighted-v0.2-candidate` 0.9875
|
|
123
|
+
against the frozen baseline `static-ranking-v0.1` 0.808156, unchanged from
|
|
124
|
+
0.4.5.
|
|
125
|
+
- `npm run schema:context-persistence` (`passed: true`).
|
|
126
|
+
- No provider calls are involved in this release procedure. `jev_verify` is not
|
|
127
|
+
available in this environment (project verification disabled), so the checks
|
|
128
|
+
above were run directly.
|
|
129
|
+
|
|
130
|
+
## Publication and registry verification
|
|
131
|
+
|
|
132
|
+
Published manually in dependency order: `ds4-context-core@0.4.6`, then
|
|
133
|
+
`ds4-context-reference-adapter@0.4.6`, then `ds4-context-engine@0.4.6`, all with
|
|
134
|
+
the default `latest` tag, after `npm run pack:check` on the same commit, followed
|
|
135
|
+
by `npm run registry:check -- 0.4.6` with a fresh npm cache, the annotated tag
|
|
136
|
+
`v0.4.6` and the GitHub Release. No session data, credentials, provider payloads
|
|
137
|
+
or prompt text were read or written while preparing this release.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ds4-context-engine",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
4
|
"description": "Non-destructive, provider-independent context management for Pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
]
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
|
-
"ds4-context-core": "0.4.
|
|
66
|
+
"ds4-context-core": "0.4.6",
|
|
67
67
|
"js-tiktoken": "1.0.21"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
package/src/extension/index.ts
CHANGED
|
@@ -27,6 +27,7 @@ export function registerDs4ContextEngine(
|
|
|
27
27
|
...(dependencies.idGenerator ? { idGenerator: dependencies.idGenerator } : {}),
|
|
28
28
|
...(dependencies.logSink ? { logSink: dependencies.logSink } : {}),
|
|
29
29
|
...(dependencies.embeddingPort ? { embeddingPort: dependencies.embeddingPort } : {}),
|
|
30
|
+
...(dependencies.coreCompatibility ? { coreCompatibility: dependencies.coreCompatibility } : {}),
|
|
30
31
|
});
|
|
31
32
|
|
|
32
33
|
const continuationStream = createOpenAIResponsesContinuationStream({
|
package/src/extension/runtime.ts
CHANGED
|
@@ -31,6 +31,11 @@ import {
|
|
|
31
31
|
type SessionCompactFailedLike,
|
|
32
32
|
type SummaryGraphDiagnostics,
|
|
33
33
|
} from "../pi-adapter/compaction-coordinator.ts";
|
|
34
|
+
import {
|
|
35
|
+
coreCompatibilityIssues,
|
|
36
|
+
coreCompatibilityMessage,
|
|
37
|
+
type CoreCompatibilityIssue,
|
|
38
|
+
} from "../pi-adapter/core-compatibility.ts";
|
|
34
39
|
import {
|
|
35
40
|
NativeContinuationManager,
|
|
36
41
|
type NativeContinuationAttempt,
|
|
@@ -224,6 +229,8 @@ export interface RuntimeDependencies {
|
|
|
224
229
|
logSink?: (line: string) => void;
|
|
225
230
|
/** Optional runtime-owned embedding provider; required for configured remote models. */
|
|
226
231
|
embeddingPort?: EmbeddingPort;
|
|
232
|
+
/** Test seam for the engine↔core guard; defaults to the real module probe. */
|
|
233
|
+
coreCompatibility?: () => readonly CoreCompatibilityIssue[];
|
|
227
234
|
}
|
|
228
235
|
|
|
229
236
|
interface PreparedPrivacyContext {
|
|
@@ -489,6 +496,7 @@ export class Ds4ContextRuntime {
|
|
|
489
496
|
recordedAt: number;
|
|
490
497
|
}> = [];
|
|
491
498
|
private lastError?: string;
|
|
499
|
+
private coreIncompatibilityMessage?: string;
|
|
492
500
|
private logger: Logger = silentLogger;
|
|
493
501
|
private readonly now: () => number;
|
|
494
502
|
private readonly idGenerator: () => string;
|
|
@@ -547,6 +555,7 @@ export class Ds4ContextRuntime {
|
|
|
547
555
|
this.artifactManager = undefined;
|
|
548
556
|
this.lastArtifacts = disabledArtifactDiagnostics();
|
|
549
557
|
this.compaction = undefined;
|
|
558
|
+
this.coreIncompatibilityMessage = undefined;
|
|
550
559
|
this.observation = undefined;
|
|
551
560
|
this.session = this.snapshotCanonicalSession(ctx);
|
|
552
561
|
|
|
@@ -650,34 +659,47 @@ export class Ds4ContextRuntime {
|
|
|
650
659
|
}
|
|
651
660
|
this.initializeMemory(ctx);
|
|
652
661
|
this.initializeArtifacts(ctx);
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
662
|
+
// Pi re-imports extension sources on /reload but keeps already-loaded
|
|
663
|
+
// dependency modules, so a core rebuilt under a running Pi stays stale in
|
|
664
|
+
// memory. Report that instead of leaving compaction to fail on a missing
|
|
665
|
+
// export, and skip only the compaction layer.
|
|
666
|
+
const coreIssues = (this.dependencies.coreCompatibility ?? coreCompatibilityIssues)();
|
|
667
|
+
if (coreIssues.length > 0) {
|
|
668
|
+
const message = coreCompatibilityMessage(coreIssues);
|
|
669
|
+
this.coreIncompatibilityMessage = message;
|
|
670
|
+
this.lastError = message;
|
|
671
|
+
this.logger.warn("runtime.core_incompatible", { error: message });
|
|
672
|
+
if (ctx.hasUI) ctx.ui.notify(`DS4 Context Engine: ${message}`, "warning");
|
|
673
|
+
} else {
|
|
674
|
+
this.compaction = new CompactionCoordinator({
|
|
675
|
+
config: this.config,
|
|
676
|
+
database: this.database,
|
|
677
|
+
sessionId: this.session.sessionId,
|
|
678
|
+
persisted: Boolean(this.session.sessionFile),
|
|
679
|
+
logger: this.logger,
|
|
680
|
+
now: this.now,
|
|
681
|
+
idGenerator: this.idGenerator,
|
|
682
|
+
syncSessionIndex: (context) => {
|
|
683
|
+
this.syncSessionIndex(context);
|
|
684
|
+
},
|
|
685
|
+
latestManifest: () => this.lastManifest,
|
|
686
|
+
resolveModelBudget: (model) => {
|
|
687
|
+
const resolved = this.resolveModelPolicy(model);
|
|
688
|
+
return {
|
|
689
|
+
budget: resolved.budget,
|
|
690
|
+
recentTailTokens: resolved.awareness.limits.recentTailTokens,
|
|
691
|
+
};
|
|
692
|
+
},
|
|
693
|
+
sanitizeContent: (text, provider) => this.privacyEngine?.sanitizeText(text, provider).value ?? text,
|
|
694
|
+
classifyContent: (text, provider) => {
|
|
695
|
+
const sanitized = this.privacyEngine?.sanitizeText(text, provider);
|
|
696
|
+
return sanitized
|
|
697
|
+
? { value: sanitized.value, classification: sanitized.classification }
|
|
698
|
+
: { value: text, classification: "normal" };
|
|
699
|
+
},
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
this.compaction?.initialize(ctx.sessionManager.getEntries());
|
|
681
703
|
|
|
682
704
|
this.phase = this.config.context.mode;
|
|
683
705
|
this.setStatus(ctx, `DS4 ctx: ${this.config.context.mode}`);
|
|
@@ -3501,6 +3523,13 @@ export class Ds4ContextRuntime {
|
|
|
3501
3523
|
}
|
|
3502
3524
|
|
|
3503
3525
|
private getCompactionDiagnostics(ctx: ExtensionContext): CompactionDiagnostics {
|
|
3526
|
+
if (this.coreIncompatibilityMessage) {
|
|
3527
|
+
return {
|
|
3528
|
+
...defaultCompactionDiagnostics(this.config),
|
|
3529
|
+
enabled: false,
|
|
3530
|
+
lastError: this.coreIncompatibilityMessage,
|
|
3531
|
+
};
|
|
3532
|
+
}
|
|
3504
3533
|
return this.compaction?.diagnostics(ctx) ?? defaultCompactionDiagnostics(this.config);
|
|
3505
3534
|
}
|
|
3506
3535
|
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
type PreparedCompactionSource,
|
|
30
30
|
type PreparedCompactionSourceSlice,
|
|
31
31
|
} from "./compaction-adapter.ts";
|
|
32
|
+
import { assertCoreCompatibility } from "./core-compatibility.ts";
|
|
32
33
|
import { buildCompactionAtomicGroups } from "ds4-context-core/compaction/segmentation";
|
|
33
34
|
import { estimateMessageTokens } from "ds4-context-core/core/token-estimator";
|
|
34
35
|
import { adaptiveRecentTailLimit } from "ds4-context-core/planner/context-planner";
|
|
@@ -195,6 +196,11 @@ interface CompactionCoordinatorDependencies {
|
|
|
195
196
|
text: string,
|
|
196
197
|
provider: string,
|
|
197
198
|
) => { value: string; classification: PrivacyClassification };
|
|
199
|
+
/**
|
|
200
|
+
* Engine↔core compatibility guard. Defaults to `assertCoreCompatibility`; the
|
|
201
|
+
* seam exists so tests can simulate a stale core artifact.
|
|
202
|
+
*/
|
|
203
|
+
checkCoreCompatibility?: () => void;
|
|
198
204
|
}
|
|
199
205
|
|
|
200
206
|
type MutableCompactionState = Omit<
|
|
@@ -238,8 +244,11 @@ export class CompactionCoordinator {
|
|
|
238
244
|
private proactiveRequested = false;
|
|
239
245
|
private lastProactiveLeafId?: string;
|
|
240
246
|
private readonly graphRecords = new Map<string, SummaryRecord>();
|
|
247
|
+
private readonly checkCoreCompatibility: () => void;
|
|
241
248
|
|
|
242
|
-
constructor(private readonly dependencies: CompactionCoordinatorDependencies) {
|
|
249
|
+
constructor(private readonly dependencies: CompactionCoordinatorDependencies) {
|
|
250
|
+
this.checkCoreCompatibility = dependencies.checkCoreCompatibility ?? assertCoreCompatibility;
|
|
251
|
+
}
|
|
243
252
|
|
|
244
253
|
initialize(entries: readonly SessionEntry[]): void {
|
|
245
254
|
if (!this.dependencies.database || !this.dependencies.persisted) return;
|
|
@@ -308,6 +317,10 @@ export class CompactionCoordinator {
|
|
|
308
317
|
};
|
|
309
318
|
|
|
310
319
|
try {
|
|
320
|
+
// A stale core module (a core rebuilt under a running Pi keeps its cached
|
|
321
|
+
// instance there) would otherwise fail later as a missing function deep in
|
|
322
|
+
// generation; this catch turns the guard message into the ordinary fallback.
|
|
323
|
+
this.checkCoreCompatibility();
|
|
311
324
|
const { source, inputBudgetTokens, requestInputLimitTokens, wholePlan, directPlan, segmentPlans } = await measure("preparationMs", () => {
|
|
312
325
|
if (event.signal.aborted) throw new Error("Compaction summary generation aborted");
|
|
313
326
|
this.dependencies.syncSessionIndex(ctx);
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { CORE_VERSION } from "ds4-context-core";
|
|
2
|
+
import {
|
|
3
|
+
buildSummaryPrompt,
|
|
4
|
+
classifyUnsupportedExactValueSpans,
|
|
5
|
+
} from "ds4-context-core/compaction/summary-contract";
|
|
6
|
+
import { EXTENSION_VERSION } from "./version.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Runtime guard for the engine↔core contract.
|
|
10
|
+
*
|
|
11
|
+
* Pi loads this extension from TypeScript source while `ds4-context-core` is a
|
|
12
|
+
* prebuilt package resolved through normal module resolution. When those two
|
|
13
|
+
* artifacts drift apart — a checkout that pulled new sources without rebuilding
|
|
14
|
+
* `packages/core/dist`, or an npm update that moved only one of the two packages
|
|
15
|
+
* — the extension keeps loading, and the first missing core export surfaces deep
|
|
16
|
+
* inside compaction as `(0, _summaryContract.x) is not a function`, which reads
|
|
17
|
+
* like a DS4 bug instead of an installation problem.
|
|
18
|
+
*
|
|
19
|
+
* The guard is deliberately kept off the load path: Pi treats an extension load
|
|
20
|
+
* failure as fatal (`process.exit(1)`), so throwing at module scope would block
|
|
21
|
+
* the whole runtime. It runs on the compaction path instead, where the mismatch
|
|
22
|
+
* already breaks the run, and where the coordinator turns the thrown message
|
|
23
|
+
* into the ordinary "DS4 compaction unavailable; using Pi default" warning.
|
|
24
|
+
*
|
|
25
|
+
* `CORE_VERSION` is imported from the package root rather than a new subpath so
|
|
26
|
+
* that a stale core without the marker still resolves: a missing *file* would be
|
|
27
|
+
* a module-resolution failure, while a missing named export simply arrives as
|
|
28
|
+
* `undefined` through Pi's CommonJS interop.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
export interface CoreCompatibilityIssue {
|
|
32
|
+
kind: "version" | "export";
|
|
33
|
+
detail: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Pure inspection, so the guard is testable without a broken installation. */
|
|
37
|
+
export function inspectCoreCompatibility(input: {
|
|
38
|
+
extensionVersion: string;
|
|
39
|
+
coreVersion: unknown;
|
|
40
|
+
requiredExports: readonly (readonly [string, unknown])[];
|
|
41
|
+
}): CoreCompatibilityIssue[] {
|
|
42
|
+
const issues: CoreCompatibilityIssue[] = [];
|
|
43
|
+
if (typeof input.coreVersion !== "string" || input.coreVersion.length === 0) {
|
|
44
|
+
issues.push({
|
|
45
|
+
kind: "version",
|
|
46
|
+
detail:
|
|
47
|
+
`the resolved ds4-context-core does not export CORE_VERSION, so it predates ${input.extensionVersion}`,
|
|
48
|
+
});
|
|
49
|
+
} else if (input.coreVersion !== input.extensionVersion) {
|
|
50
|
+
issues.push({
|
|
51
|
+
kind: "version",
|
|
52
|
+
detail:
|
|
53
|
+
`ds4-context-core resolves to ${input.coreVersion} while DS4 Context Engine is ${input.extensionVersion}`,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
for (const [name, value] of input.requiredExports) {
|
|
57
|
+
if (typeof value !== "function") {
|
|
58
|
+
issues.push({
|
|
59
|
+
kind: "export",
|
|
60
|
+
detail: `ds4-context-core does not export ${name}()`,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return issues;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Core entry points this extension calls. Extend this list whenever the engine
|
|
69
|
+
* starts requiring a newer core symbol; `CORE_VERSION` equality already covers
|
|
70
|
+
* whole-artifact drift, and these probes name the exact missing symbol.
|
|
71
|
+
*/
|
|
72
|
+
const REQUIRED_CORE_EXPORTS: readonly (readonly [string, unknown])[] = [
|
|
73
|
+
["buildSummaryPrompt", buildSummaryPrompt],
|
|
74
|
+
["classifyUnsupportedExactValueSpans", classifyUnsupportedExactValueSpans],
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
export function coreCompatibilityIssues(): CoreCompatibilityIssue[] {
|
|
78
|
+
return inspectCoreCompatibility({
|
|
79
|
+
extensionVersion: EXTENSION_VERSION,
|
|
80
|
+
coreVersion: CORE_VERSION,
|
|
81
|
+
requiredExports: REQUIRED_CORE_EXPORTS,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Single-line message: it is appended to the existing fallback notification. */
|
|
86
|
+
export function coreCompatibilityMessage(
|
|
87
|
+
issues: readonly CoreCompatibilityIssue[],
|
|
88
|
+
): string {
|
|
89
|
+
return `${issues.map((issue) => issue.detail).join("; ")}. Fix: rebuild the checkout `
|
|
90
|
+
+ "(\"npm ci && npm run build:core && npm run build:adapters\") or update the installed package "
|
|
91
|
+
+ "(\"pi update --extensions\"), then restart Pi: a /reload can keep the old core module cached.";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Throws an actionable error instead of a downstream `is not a function`. */
|
|
95
|
+
export function assertCoreCompatibility(): void {
|
|
96
|
+
const issues = coreCompatibilityIssues();
|
|
97
|
+
if (issues.length > 0) throw new Error(coreCompatibilityMessage(issues));
|
|
98
|
+
}
|