ds4-context-engine 0.4.4 → 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 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. See 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 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,9 @@ 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)
519
+ - [0.4.5 release notes](docs/releases/0.4.5.md)
520
+ - [0.4.4 release notes](docs/releases/0.4.4.md)
518
521
  - [0.4.3 release notes](docs/releases/0.4.3.md)
519
522
  - [0.4.2 release notes](docs/releases/0.4.2.md)
520
523
  - [0.4.1 release notes](docs/releases/0.4.1.md)
@@ -0,0 +1,77 @@
1
+ # 066 — Grade the summarizer quoting fallback and separate adjacent from unrelated span composition
2
+
3
+ **Date:** 2026-09-27
4
+ **Status:** Accepted
5
+ **Related:** [061](061-compaction-latency.md), [065](065-exact-value-escaped-equivalence.md)
6
+
7
+ ## Context
8
+
9
+ Compaction summary validation is fail-closed: a backticked exact value must occur
10
+ literally in the evidence, and a bounded repair removes at most eight whole
11
+ affected bullets. Production sessions kept failing closed with
12
+ `compaction.custom_fallback`, `unsupported-exact-value`, `repair=too-many-bullets`
13
+ and 30/20, 23/19, 19/15, 17/11, then 16/12 unsupported spans and affected
14
+ bullets.
15
+
16
+ The class-only report (§ 0.4.2–0.4.3) made the next observed run interpretable,
17
+ and ADR 065 removed 11 of its 16 spans as JSON-escape renderings. The production
18
+ report produced by 0.4.4 — 14 spans across 14 bullets, `spansClassifiedCheap: 0`,
19
+ `probesUsed: 11671` of 24000, `classificationComplete: true` — closed the
20
+ remaining attribution:
21
+
22
+ | Relation | Count |
23
+ | --- | --- |
24
+ | `composed-two-present-parts` | 9 |
25
+ | `no-near-miss` | 2 |
26
+ | `single-deletion-present` | 2 |
27
+ | `not-classified-length` | 1 |
28
+
29
+ No rendering class remained, so the escape path was exhausted; the dominant
30
+ residual cause is span composition, which is exactly what the 0.4.1 prompt rule
31
+ forbids. That rule is present in the prompt that produced the report and the
32
+ model still composes, which is why another repetition of the prohibition was not
33
+ the fix.
34
+
35
+ Composition mixes two situations that need opposite treatments. When the two
36
+ parts sit next to each other in one source, the span is a re-rendering of a
37
+ contiguous evidence region — the same shape of problem ADR 065 solved for
38
+ escapes. When the parts are found at unrelated positions, the span is an
39
+ association the evidence never contains, and accepting it would let a
40
+ plausible-looking but false pairing (`path: wrong-range`) through validation.
41
+
42
+ The prompt offered the model exactly one escape hatch from composing: omit the
43
+ whole bullet. Discarding a fact is expensive, so composing was the cheaper
44
+ choice. The instrumented evidence cannot by itself tell which composition kind
45
+ occurs, because the classifier only established that both parts are present
46
+ somewhere.
47
+
48
+ ## Decision
49
+
50
+ 1. The summarizer prompt keeps the prohibition but replaces the single drastic
51
+ fallback with a graded ladder: backtick only the fragments that are themselves
52
+ contiguous, with the joining text outside the backticks; if no fragment is
53
+ contiguous on its own, write the value as ordinary text without backticks and
54
+ keep the bullet; omit the bullet only when the fact itself has no support in
55
+ the evidence.
56
+ 2. Diagnostics split the composition class: `composed-adjacent-present` when one
57
+ corpus source holds the two parts with nothing but joining punctuation and
58
+ spaces between them (at most the span's own separator length plus two
59
+ characters, and no letters, digits or line breaks), and
60
+ `composed-two-present-parts` otherwise. Both are class-only, bounded, and
61
+ observation-only.
62
+ 3. The eight-bullet repair bound and the 25% removal bound are unchanged. A
63
+ larger unsupported set still fails closed to Pi default compaction: pruning
64
+ more of a summary than the operator accepts is not a fallback remedy.
65
+
66
+ ## Consequences
67
+
68
+ - The ladder can reduce the number of backticked exact values in a summary. That
69
+ is a change in what the model chooses to quote, not in what validation accepts:
70
+ the contract, the repair bounds and the fail-closed decision are untouched.
71
+ - The effect on the observed fallback is not verified by this change. The next
72
+ production report decides whether composition persists, and the new adjacency
73
+ class says whether a rendering-normalization rule on the ADR 065 pattern is
74
+ even possible: `composed-adjacent-present` is the only class for which it could
75
+ be safe, while `composed-two-present-parts` must never be accepted.
76
+ - No configuration key, no SQLite migration, no schema change, and no
77
+ provider-facing default change.
@@ -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.
@@ -69,5 +69,7 @@ The initial decisions from the development plan are accepted:
69
69
  | [063](063-fts-rowid-key-mappings.md) | Resolve FTS key deletes through rowid mapping tables | 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
+ | [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 |
72
74
 
73
75
  Each decision will receive a dedicated record when implementation pressure introduces alternatives or consequences not already covered by the development plan.
@@ -32,16 +32,18 @@ Fan-out and fan-in are bounded to 32 segment requests, 64 aggregate requests, an
32
32
  ## Critical Exact Values
33
33
  ```
34
34
 
35
- Every section must occur once, in order, and contain content or `- None`. DS4 replaces each unique `Files Read` and `Files Modified` section with one exact path per bullet from Pi's sanitized file-operation inventory before validation; missing or duplicate sections still fail. Backticked exact values must occur literally in the serialized segment source, ordered child-summary content, or those known file-operation paths, either as written or in the canonical JSON-escaped rendering of the value. A decoded value whose encoded text is present in the evidence is accepted ([ADR 065](ADR/065-exact-value-escaped-equivalence.md)); the reverse direction — an escaped span whose raw form is present — remains unsupported and is reported as `unescaped-form-present`. A bounded unsupported exact-value bullet is removed as a whole and recorded as a validation warning; unsupported prose, more than eight affected bullets, or removal above 25% still fails closed to Pi. The prompt explicitly asks the model to omit a bullet whose complete backticked span cannot be copied verbatim, and to keep every backticked span one contiguous excerpt rather than joining separately supported values (a setting name with its value, a path with a line range, a command with its flags). Every segment is validated independently against only its own sanitized source and deterministic file inventory. Every aggregate is validated independently against only the sanitized content of its ordered children and cumulative deterministic file inventory. A direct update is validated against both the sanitized previous summary and new source, plus cumulative sanitized file evidence; previous-summary-only exact values remain valid evidence. Custom focus instructions are not factual evidence. Any unrepaired failure prevents the whole graph batch from being installed.
35
+ Every section must occur once, in order, and contain content or `- None`. DS4 replaces each unique `Files Read` and `Files Modified` section with one exact path per bullet from Pi's sanitized file-operation inventory before validation; missing or duplicate sections still fail. Backticked exact values must occur literally in the serialized segment source, ordered child-summary content, or those known file-operation paths, either as written or in the canonical JSON-escaped rendering of the value. A decoded value whose encoded text is present in the evidence is accepted ([ADR 065](ADR/065-exact-value-escaped-equivalence.md)); the reverse direction — an escaped span whose raw form is present — remains unsupported and is reported as `unescaped-form-present`. A bounded unsupported exact-value bullet is removed as a whole and recorded as a validation warning; unsupported prose, more than eight affected bullets, or removal above 25% still fails closed to Pi. The prompt requires every backticked span to be one contiguous excerpt copied as-is and forbids assembling one span from separately supported values (a setting name with its value, a path with a line range, a command with its flags), and it gives the model a cheaper alternative than composing: backtick only the fragments that are themselves contiguous with the joining text outside the backticks, or write the value as ordinary text without backticks, keeping the bullet. A bullet is omitted only when the fact itself has no support in the evidence ([ADR 066](ADR/066-quoting-downgrade-and-span-adjacency.md)). Every segment is validated independently against only its own sanitized source and deterministic file inventory. Every aggregate is validated independently against only the sanitized content of its ordered children and cumulative deterministic file inventory. A direct update is validated against both the sanitized previous summary and new source, plus cumulative sanitized file evidence; previous-summary-only exact values remain valid evidence. Custom focus instructions are not factual evidence. Any unrepaired failure prevents the whole graph batch from being installed.
36
36
 
37
37
  An unrepaired exact-value failure reports only the stage, issue code, categorical repair status, unsupported-span count, and affected-bullet count. Repair statuses distinguish an unsupported location, more than eight bullets, removal above 25%, and an unexpected invalid second validation. The disputed text is intentionally absent from logs, UI notifications, and diagnostics because it may contain sensitive source material.
38
38
 
39
- The same failure carries a class-only span report on the fallback warning (`unsupportedSpanClasses`): rejected-span count, affected bullets, length buckets, character shapes (spaces, backslashes, escape sequences, separators, quotes, typographic characters, JSON punctuation), and how each distinct span relates to the evidence — escaped or unescaped rendering, collapsed whitespace, case or typographic variant, one-character deletion, two separately present values joined into one span, or no near-miss (every applicable lookup ran and found nothing). The report contains class names and counters only, never span text, and the classifier is observation-only: it cannot change validation, repair bounds, or the fail-closed decision. It exists to separate a summarizer that invents values from one whose rendering or composition rules differ from the validation domain, which require opposite fixes.
39
+ The same failure carries a class-only span report on the fallback warning (`unsupportedSpanClasses`): rejected-span count, affected bullets, length buckets, character shapes (spaces, backslashes, escape sequences, separators, quotes, typographic characters, JSON punctuation), and how each distinct span relates to the evidence — escaped or unescaped rendering, collapsed whitespace, case or typographic variant, one-character deletion, two separately present values joined into one span — adjacent in one source with nothing but joining punctuation and spaces between them (`composed-adjacent-present`) or found at unrelated positions (`composed-two-present-parts`) — or no near-miss (every applicable lookup ran and found nothing). The adjacency question separates a join that re-renders a contiguous evidence region from an association the evidence never contains: a joiner gap holds no letters, digits or line breaks, so two parts on separate lines are not adjacent. The report contains class names and counters only, never span text, and the classifier is observation-only: it cannot change validation, repair bounds, or the fail-closed decision. It exists to separate a summarizer that invents values from one whose rendering or composition rules differ from the validation domain, which require opposite fixes. The first production report produced by the 0.4.4 diagnostics attributed 9 of 14 rejected spans to composition, 2 to a one-character deviation, 2 to absence and 1 to the length limit, with no rendering class left at all.
40
40
 
41
41
  Three relations mark spans that were *not* analysed, and they must never be read as invention. `not-classified-length` means the span exceeds the near-miss length limit (96 characters); `not-classified-partial` means its share of the budget ran out; `not-classified-budget` means the shared budget was already exhausted. To keep the histogram interpretable, transformed-form lookups cover every distinct span before any near-miss analysis starts, and each near-miss candidate may spend only an equal share of what remains (at least 32 lookups, never more than the budget left). The report also carries `spansClassifiedCheap` (span occurrences attributed by a transformed-form relation), `corpusSources`, `probeBudget` and `probesUsed`, so a run can be read without guessing how much of the budget was consumed. The shared budget defaults to 24000 evidence-source scans, where one lookup scans every corpus source once. `escaped-form-present` is not expected in reports from 0.4.4 on: validation accepts the canonical escaped rendering and the classifier consumes the same predicate, so such spans no longer reach it; the class stays in the taxonomy for compatibility. `classificationComplete` is false when the shared budget or a per-span share stopped an analysis; a `not-classified-length` span does not clear it, because that limit is deterministic rather than a resource shortfall.
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;
@@ -85,3 +85,19 @@ TypeScript builds and root typecheck), deterministic `npm run quality:compare`
85
85
  (`passed: true`), and `npm run pack:check` in a clean consumer for all three
86
86
  packages at 0.4.4 (247 core files, 7 reference-adapter files, 103 extension
87
87
  files). No provider calls are involved in this release procedure.
88
+
89
+ ## Publication and registry verification
90
+
91
+ Published manually in dependency order from `10f0e0c`: `ds4-context-core@0.4.4`,
92
+ then `ds4-context-reference-adapter@0.4.4`, then `ds4-context-engine@0.4.4`, all
93
+ with the default `latest` tag, after `npm run pack:check` on the same commit.
94
+
95
+ The first seven `npm run registry:check -- 0.4.4` attempts failed while the
96
+ published metadata was still propagating; the eighth, some minutes later,
97
+ reported "Verified all DS4 registry packages at exact version 0.4.4". For all
98
+ three packages `npm view <package> dist-tags.latest` resolves to 0.4.4.
99
+
100
+ Annotated tag `v0.4.4` and the GitHub Release were created from `10f0e0c`. No
101
+ provider calls are involved in this release procedure; the classifier runs only
102
+ after a validation failure has already been decided, and no session data,
103
+ credentials, or provider payloads were read or written while preparing it.
@@ -0,0 +1,145 @@
1
+ # Release 0.4.5 — Graded quoting fallback and adjacent-vs-unrelated composition
2
+
3
+ **Coordinated packages:** `ds4-context-core`, `ds4-context-reference-adapter`, and `ds4-context-engine` 0.4.5.
4
+ **Implementation commit:** `91a700a`.
5
+
6
+ ## Summary
7
+
8
+ Behavior and diagnostics patch. The compaction summarizer prompt no longer
9
+ forces an all-or-nothing choice between quoting a composite span and dropping the
10
+ bullet: it now tells the model to backtick only the fragments that are themselves
11
+ contiguous, and otherwise to keep the fact as ordinary text without backticks.
12
+ The diagnostics report separates `composed-adjacent-present` — the two parts sit
13
+ next to each other in one evidence source with nothing but joining punctuation
14
+ and spaces between them — from `composed-two-present-parts`, where the parts are
15
+ found at unrelated positions. Validation strictness, the eight-bullet and 25%
16
+ repair bounds, the 25% removal bound, the fail-closed fallback to Pi default
17
+ compaction, the SQLite schema and every provider-facing default are unchanged.
18
+
19
+ ## Changes
20
+
21
+ - `packages/core/src/compaction/summary-contract.ts`:
22
+ - `buildSummaryPrompt` replaces the single "omit the whole bullet" escape with a
23
+ graded ladder: keep every backticked span one contiguous excerpt; when the
24
+ complete value is not one contiguous excerpt, backtick only the fragments
25
+ that are themselves contiguous with the joining text outside the backticks;
26
+ if no fragment is contiguous on its own, write the value as ordinary text
27
+ without backticks and keep the bullet; omit the bullet only when the fact
28
+ itself has no support in the evidence. The prohibition on assembling a span
29
+ from separately present values is unchanged and now carries the explicit
30
+ counter-examples (`a setting name with its value`, `a path with a line
31
+ range`, `a command with its flags`).
32
+ - The diagnostics classifier adds the relation `composed-adjacent-present`. A
33
+ composition is adjacent when one corpus source holds the two parts with at
34
+ most the span's own separator length plus two characters between them, in a
35
+ gap that contains no letters, digits or line breaks — so two parts on
36
+ separate lines are not adjacent. Adjacency lookups are bounded
37
+ (`MAX_ADJACENT_JOIN_PROBES` 8 per span, `MAX_JOIN_OCCURRENCES` 64
38
+ occurrences per source) and cost the same budget unit as every other lookup.
39
+ - `composed-two-present-parts` keeps its meaning for non-adjacent joins; both
40
+ classes are class-only and observation-only.
41
+ - `tests/unit/summary-contract.test.ts`: prompt assertions follow the new
42
+ wording, plus two cases that pin the sub-classification — a JSON-rendered pair
43
+ (`{"compaction.model": "deepseek-flash"}` against
44
+ `compaction.model: deepseek-flash`) reports `composed-adjacent-present`, and
45
+ the same span against a source holding the two parts on separate lines reports
46
+ `composed-two-present-parts`.
47
+ - `docs/COMPACTION.md`: the summary contract and the diagnostics section.
48
+ - `docs/ADR/066-quoting-downgrade-and-span-adjacency.md` records the decision.
49
+
50
+ ## Why
51
+
52
+ The class-only report added in 0.4.2, made interpretable in 0.4.3 and re-budgeted
53
+ in 0.4.4, produced its first fully analysed production run: 14 rejected spans
54
+ across 14 bullets, `spansClassifiedCheap: 0`, `probesUsed: 11671` of 24000,
55
+ `classificationComplete: true`, `trigger: manual`.
56
+
57
+ | Relation | Count |
58
+ | --- | --- |
59
+ | `composed-two-present-parts` | 9 |
60
+ | `no-near-miss` | 2 |
61
+ | `single-deletion-present` | 2 |
62
+ | `not-classified-length` | 1 |
63
+
64
+ No rendering class remained, so ADR 065 exhausted the escape path. The dominant
65
+ residual cause is composition — the exact conduct the 0.4.1 prompt rule forbids —
66
+ and that rule was in the prompt this model ran with, so repeating the prohibition
67
+ was not the fix. The remaining hypothesis is the cost asymmetry: composing was
68
+ the only alternative to discarding a whole fact. This release removes that
69
+ asymmetry and, at the same time, adds the one measurement that decides whether a
70
+ safe acceptance rule can ever exist for composition: adjacent joins are
71
+ contiguous evidence regions the span re-rendered, while unrelated joins are
72
+ associations the evidence never contains, and only the first kind could be
73
+ accepted on the ADR 065 pattern.
74
+
75
+ The eight-bullet repair bound is deliberately untouched. It was the mechanism
76
+ that turned 14 affected bullets into a total fallback, and an explicit operator
77
+ decision on 2026-09-27 kept it: removing invalid bullets without a count limit
78
+ could silently delete a large share of a summary, and with these counts the 25%
79
+ removal bound would likely refuse as well, so the change would not reliably end
80
+ the fallback.
81
+
82
+ ## Privacy and safety
83
+
84
+ - The report stays class-only. The new relation is one more fixed class name; no
85
+ span text, fragment text, evidence text or hash of either is added to logs,
86
+ warnings, or errors.
87
+ - The prompt change alters what the model is instructed to quote. It does not
88
+ change what validation accepts, the repair bounds, the fail-closed decision, or
89
+ the privacy path; no provider call, network access or configuration change is
90
+ involved in this release.
91
+ - Fewer backticked spans is a possible outcome of the ladder. That is a rendering
92
+ choice made by the summarizer, not a validator relaxation: an unsupported
93
+ backticked value is still rejected exactly as before.
94
+
95
+ ## Compatibility and persistence
96
+
97
+ - No migrations, no schema change (still 16), no new configuration key, and no
98
+ change to any provider-facing default.
99
+ - The report shape is additive: `composed-adjacent-present` is a new key in
100
+ `relations`, and existing readers keep working. A reader that previously saw
101
+ only `composed-two-present-parts` will now see the adjacent cases split out.
102
+ - `chars-v1` remains the default estimator, `modelAwareness.autoTune` and DS4
103
+ native continuation stay disabled, `storage.scope` keeps its `project` default.
104
+
105
+ ## Measured scope and limitations
106
+
107
+ The effect on the observed fallback is not verified in a real session by this
108
+ release. The change is a prompt instruction, so the outcome depends on the
109
+ summarizer model's compliance: 0.4.1 already showed that a prohibition alone is
110
+ not followed. Expected observations for the next production compaction:
111
+
112
+ | Observation | Reading |
113
+ | --- | --- |
114
+ | `compaction.custom_fallback` absent | the graded fallback ended the composition |
115
+ | composition still present, mostly `composed-adjacent-present` | composition persists and a rendering-normalization rule is technically possible |
116
+ | composition still present, mostly `composed-two-present-parts` | composition persists and no acceptance rule can be safe; only the summarizer model, its reasoning level or the repair policy remain |
117
+ | fewer backticked values overall | the ladder worked, with the expected rendering cost in the summary |
118
+
119
+ ## Validation
120
+
121
+ On Node 26.5.1 the release passed `npm run check` (99 Vitest files, 634 tests,
122
+ TypeScript builds and root typecheck), deterministic `npm run quality:compare`
123
+ (candidate `0.9875` against baseline `0.808156`), `npm run schema:context-persistence`
124
+ (`passed: true`), and `npm run pack:check` in a clean consumer for all three
125
+ packages at 0.4.5 (247 core files, 7 reference-adapter files, 104 extension
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.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.4",
66
+ "ds4-context-core": "0.4.6",
67
67
  "js-tiktoken": "1.0.21"
68
68
  },
69
69
  "peerDependencies": {
@@ -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({
@@ -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
- this.compaction = new CompactionCoordinator({
654
- config: this.config,
655
- database: this.database,
656
- sessionId: this.session.sessionId,
657
- persisted: Boolean(this.session.sessionFile),
658
- logger: this.logger,
659
- now: this.now,
660
- idGenerator: this.idGenerator,
661
- syncSessionIndex: (context) => {
662
- this.syncSessionIndex(context);
663
- },
664
- latestManifest: () => this.lastManifest,
665
- resolveModelBudget: (model) => {
666
- const resolved = this.resolveModelPolicy(model);
667
- return {
668
- budget: resolved.budget,
669
- recentTailTokens: resolved.awareness.limits.recentTailTokens,
670
- };
671
- },
672
- sanitizeContent: (text, provider) => this.privacyEngine?.sanitizeText(text, provider).value ?? text,
673
- classifyContent: (text, provider) => {
674
- const sanitized = this.privacyEngine?.sanitizeText(text, provider);
675
- return sanitized
676
- ? { value: sanitized.value, classification: sanitized.classification }
677
- : { value: text, classification: "normal" };
678
- },
679
- });
680
- this.compaction.initialize(ctx.sessionManager.getEntries());
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
+ }
@@ -1,4 +1,4 @@
1
- export const EXTENSION_VERSION = "0.4.4";
1
+ export const EXTENSION_VERSION = "0.4.6";
2
2
  export const SUPPORTED_PI_VERSION = "0.84.3";
3
3
  export const OBSERVER_PLANNER_VERSION = "observer-model-aware-v1";
4
4
  export const PLANNER_VERSION = "managed-learned-ranking-v1";