ds4-context-engine 0.3.5 → 0.3.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
@@ -264,6 +264,7 @@ The following example shows the main configuration groups. Omitted values use th
264
264
  "minimumOutputReserve": 8192,
265
265
  "preferredOutputReserve": 32768,
266
266
  "recentTailTokens": 64000,
267
+ "rescueImmediatePredecessor": true,
267
268
  "maxPinnedTokens": 16000,
268
269
  "maxMemoryTokens": 8000,
269
270
  "maxRetrievedHistoryTokens": 16000,
@@ -12,7 +12,7 @@ The managed planner is synchronous, deterministic, provider-independent, and doe
12
12
  6. Merge groups linked by assistant tool calls and every matching tool result.
13
13
  7. Select the current request, labelled pin groups, and applicable allowed persistent pins as mandatory.
14
14
  8. Enforce `maxPinnedTokens`, then fit relevant allowed durable memory under `maxMemoryTokens`.
15
- 9. Walk older turns newest-first, stopping at the first group that would break the contiguous recent tail, target, or hard limit.
15
+ 9. Walk older turns newest-first, stopping at the first group that would break the contiguous recent tail, target, or hard limit. When the immediate-predecessor turn itself exceeds the recent-tail cap but still fits the target and hard budgets, it is kept verbatim (`context.rescueImmediatePredecessor`, default true) and the tail then closes.
16
16
  10. Privacy-filter and fit source-labelled historical retrieval groups under `maxRetrievedHistoryTokens`.
17
17
  11. Privacy-filter and fit hash-current project snippets under `maxProjectTokens`.
18
18
  12. Fit active allowed Pi compaction/branch summaries in the remaining summary and input budgets.
@@ -41,6 +41,8 @@ Artifact condensation occurs before atomic grouping. It preserves `toolCallId`,
41
41
 
42
42
  The retrieval engine produces independent synthetic user-role evidence groups. They are never mandatory: recent turns have priority 100, durable memory 90, retrieved history 85, project snippets 80, and active summaries 75. Each group is selected or excluded whole, carries its original Pi entry ID, and is represented as `retrieval` in the Context Manifest. If planner validation falls back, every synthetic evidence message is discarded and Pi receives its original `AgentMessage[]` unchanged.
43
43
 
44
+ Retrieval deduplicates against the entries the managed plan actually commits, not against Pi's native context: before retrieving, the runtime plans the context with mandatory supplements only, maps the committed messages back to Pi session entry IDs, and passes those IDs as the retrieval exclusion set. An entry that Pi still exposes but the planner excludes (for example a turn larger than the recent-tail cap) therefore remains retrievable and reappears as bounded `retrieval` evidence instead of being silently lost. The Context Manifest planning block records `rescuedImmediatePredecessor` and `oversizedTurnExclusions`; an oversized exclusion also emits a `context.excluded_oversized_turn` warning, and `/context explain` surfaces both counters.
45
+
44
46
  Evidence text is a JSON-quoted historical excerpt with an explicit data-only boundary. It is inserted immediately before the latest real user request, so the current task remains the final message and provider conversation order stays deterministic.
45
47
 
46
48
  ## Project snippets
@@ -1,6 +1,6 @@
1
1
  # DS4 Context Engine 0.3.5
2
2
 
3
- Status: local release gates passed; commit, push and coordinated publication authorized. Validation-only CI, publication and registry verification are pending.
3
+ Status: published as stable on npm under `latest`; annotated tag `v0.3.5` points to validated release commit `8b1d5f4`.
4
4
 
5
5
  ## Added since 0.3.4
6
6
 
@@ -65,6 +65,22 @@ Candidate validation on Node.js `26.5.1`, from a sanitized release source with a
65
65
 
66
66
  Pre-existing local `allowScripts` additions and `.serena/` are excluded from release commits and public packages.
67
67
 
68
+ The final clean committed worktree repeated fresh `npm ci`, all **508 tests**, clean-consumer package verification and tarball inspection successfully. Validation-only CI run [`33970061602`](https://github.com/Alucard24/ds4-context-engine/actions/runs/33970061602) on `8b1d5f4` passed both Node `22.19.0` and `24.x`, including the full suite and package checks.
69
+
68
70
  ## Registry verification and release
69
71
 
70
- Pending successful local gates, validation-only CI, manual publication and exact registry verification. No release tag has been created yet.
72
+ Published manually as `alucard_24`, in dependency order, from reviewed tarballs built in the clean committed worktree:
73
+
74
+ - `ds4-context-core@0.3.5`: shasum `71105c666a8c88f1abfe5ba3efefe63fc32bba74`;
75
+ - `ds4-context-reference-adapter@0.3.5`: shasum `8eca29472ea4721606d947818eba0aa53a6c6f63`;
76
+ - `ds4-context-engine@0.3.5`: shasum `304b61c21e28f89e82edbee39880210c2c9dc868`.
77
+
78
+ `npm run registry:check -- 0.3.5` passed: fresh exact-version installation, matching adapter/core dependencies, public core exports, compiled reference conformance, packaged quality corpus, storage CLI usage probe and isolated offline Pi extension startup. Registry SHA-1 and SHA-512 integrity values match the local tarballs for all three packages.
79
+
80
+ The immediate first post-publication install returned `ETARGET` for the engine. Once exact-version registry metadata was available, verification was repeated with npm `prefer-online` and passed; no package was republished or tag moved.
81
+
82
+ Verified dist-tags for all three: `latest=0.3.5`, `beta=0.3.0-beta.3`, `alpha=0.3.0-alpha.5`, `rc=0.2.0-rc.1`.
83
+
84
+ Annotated tag `v0.3.5` targets `8b1d5f4`, the tested/published source. This post-publication evidence update is documentation-only.
85
+
86
+ GitHub Release: https://github.com/Alucard24/ds4-context-engine/releases/tag/v0.3.5
@@ -0,0 +1,53 @@
1
+ # DS4 Context Engine 0.3.6
2
+
3
+ Status: published release on 2026-09-05; tag `v0.3.6`.
4
+
5
+ This coordinated release fixes a managed-context selection defect: a single oversized atomic conversation turn could cause the planner to discard turns that are still essential to the current request, and its own retrieval second pass could not recover them.
6
+
7
+ ## Fixed
8
+
9
+ - **Immediate-predecessor rescue**: when the turn immediately preceding the current request exceeds the recent-tail cap but still fits the active target and hard budgets, the planner keeps it verbatim instead of closing the entire recent tail at that group. The tail closes after the rescue, so older turns keep their prior bounded behavior. Controlled by `context.rescueImmediatePredecessor` (default `true`).
10
+ - **Retrieval second chance**: retrieval now deduplicates against the entries the managed plan actually commits, not against Pi's native context. Before the retrieval pass, the runtime plans the context with mandatory supplements only, maps the committed messages back to Pi session entry IDs, and passes those IDs as the retrieval exclusion set. Entries the planner excludes (for example an oversized turn) can therefore reappear as bounded, source-labelled `retrieval` evidence instead of being silently lost.
11
+ - **Diagnostics**: a `context.excluded_oversized_turn` warning reports how many turn groups at or above the recent-tail cap were excluded, whether the immediate predecessor was rescued, and the active limits. The Context Manifest planning block records optional `rescuedImmediatePredecessor` and `oversizedTurnExclusions` counters, and `/context explain` surfaces both.
12
+
13
+ No validation, privacy, provenance, cancellation, fallback, canonical-history, or bounded-retention behavior was weakened. The runtime retry policy, compaction pipeline, and storage formats are unchanged.
14
+
15
+ ## Compatibility and persistence
16
+
17
+ The context manifest adds only optional metadata fields. `ds4-context-config-v1`, `runtime-adapter-v1`, `ds4-context-persistence-tool-v1`, `ds4-context-persistence-result-v1`, SQLite schema 15, and migration checksums are unchanged.
18
+
19
+ ## Package/version policy
20
+
21
+ The coordinated version is `0.3.6` for:
22
+
23
+ ```text
24
+ ds4-context-core
25
+ ds4-context-reference-adapter
26
+ ds4-context-engine
27
+ ```
28
+
29
+ Both adapters depend exactly on `ds4-context-core@0.3.6`. The packages were published manually under npm `latest`. GitHub Actions remains validation-only with OIDC and package-write permissions denied.
30
+
31
+ ## Validation evidence
32
+
33
+ Local candidate verification on Node.js `26.5.1`:
34
+
35
+ - `npm run check`: 81 files and 518 tests passed (including planner rescue cases, second-chance retrieval integration, and entry-id mapping).
36
+ - `npm run quality:compare`: candidate quality `0.9875` versus baseline `0.808156`.
37
+ - `npm run schema:context-persistence`: 1,266 bytes and 317 estimated tokens; below the 1,500 absolute and 320 relative limits.
38
+ - `npm run latency:check -- <exact ds4-context-core@0.1.2>`: passed with ratio `1.082064`, at or below `1.10`.
39
+ - `npm run pack:check`: verified core (235 files), reference adapter (7 files), and Pi adapter (87 files) in a clean consumer.
40
+ - `npm pack --dry-run --json` for all three packages: passed with the same bounded inventories.
41
+ - `git diff --check`: passed.
42
+ - Protected CI, compatibility golden, Pi fixture, migration, canonical Pin/Memory, and persistence-tool contract files: unchanged except the intentional `compatibility-0.2.0.json` default addition.
43
+
44
+ - `npm run registry:check -- 0.3.6`: passed against all three exact published versions; `latest` resolves to `0.3.6` for every package.
45
+
46
+ Exact registry verification passed before the annotated tag and GitHub release were created.
47
+
48
+ ## Documentation
49
+
50
+ - [`../CONTEXT_PLANNER.md`](../CONTEXT_PLANNER.md)
51
+ - [`../COMPACTION.md`](../COMPACTION.md)
52
+ - [`../RELEASING.md`](../RELEASING.md)
53
+ - [`0.3.5.md`](0.3.5.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ds4-context-engine",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "description": "Non-destructive, provider-independent context management for Pi.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -62,7 +62,7 @@
62
62
  ]
63
63
  },
64
64
  "dependencies": {
65
- "ds4-context-core": "0.3.5"
65
+ "ds4-context-core": "0.3.6"
66
66
  },
67
67
  "peerDependencies": {
68
68
  "@earendil-works/pi-ai": "0.84.3",
@@ -403,6 +403,12 @@ function formatExplain(diagnostics: RuntimeDiagnostics): string {
403
403
  `Message target: ${count(planning.messageTargetTokens)}`,
404
404
  `Message hard limit: ${count(planning.messageHardLimitTokens)}`,
405
405
  `Recent-tail limit: ${count(planning.recentTailTokenLimit)}`,
406
+ ...(planning.rescuedImmediatePredecessor
407
+ ? ["Rescued predecessor: yes (immediate previous turn kept beyond the recent-tail cap within the input budget)"]
408
+ : []),
409
+ ...(planning.oversizedTurnExclusions
410
+ ? [`Oversized turn excl: ${count(planning.oversizedTurnExclusions)} (turn group(s) at/above the recent-tail cap; recovered by retrieval only if it fits)`]
411
+ : []),
406
412
  `Selected groups: ${count(planning.selectedGroupCount)}`,
407
413
  `Excluded groups: ${count(planning.excludedGroupCount)}`,
408
414
  `Duration: ${planning.durationMs === undefined ? "n/a" : `${planning.durationMs.toFixed(1)} ms`}`,
@@ -97,7 +97,7 @@ import {
97
97
  type PinScope,
98
98
  type ProjectMemorySource,
99
99
  } from "ds4-context-core/memory/memory-types";
100
- import { planManagedContext, type SupplementalContextMessage } from "ds4-context-core/planner/context-planner";
100
+ import { planManagedContext, type ManagedContextPlan, type SupplementalContextMessage } from "ds4-context-core/planner/context-planner";
101
101
  import {
102
102
  disabledContextQualityDiagnostics,
103
103
  evaluateManifestQuality,
@@ -151,6 +151,7 @@ import {
151
151
  buildPiObserverManifest,
152
152
  findExactPiMessageSourceIds,
153
153
  findPiPinnedMessageIndices,
154
+ findPiSourceEntryIds,
154
155
  } from "../pi-adapter/context-observer.ts";
155
156
  import { projectSessionFileMutations } from "../pi-adapter/memory-adapter.ts";
156
157
  import { ProjectMemorySynchronizer } from "../pi-adapter/project-memory-sync.ts";
@@ -973,10 +974,43 @@ export class Ds4ContextRuntime {
973
974
  memoryTokens: 0,
974
975
  };
975
976
  if (this.memoryManager) this.lastMemory = this.memoryManager.diagnostics();
977
+ const fixedTokens = baseline.composition.systemTokens + baseline.composition.toolTokens;
978
+ const pinnedMessageIndices = findPiPinnedMessageIndices(effectiveEvent, ctx);
979
+ const retrievalEnabled = this.config.retrieval.exact
980
+ || this.config.retrieval.fts
981
+ || this.config.retrieval.semantic;
982
+ const retrievalActiveContextEntryIds = retrievalEnabled
983
+ ? this.plannedContextEntryIds(ctx, planManagedContext({
984
+ messages: effectiveEvent.messages,
985
+ fixedTokens,
986
+ budget,
987
+ config: effectiveContextConfig,
988
+ pinnedMessageIndices,
989
+ supplementalMessages: [
990
+ ...memorySelection.pins.map((evidence) => ({
991
+ id: `pin:${evidence.item.id}`,
992
+ message: evidence.message,
993
+ kind: "pin" as const,
994
+ sourceIds: [evidence.item.id],
995
+ score: 950,
996
+ reason: evidence.reason,
997
+ })),
998
+ ...memorySelection.memories.map((evidence) => ({
999
+ id: `memory:${evidence.item.id}`,
1000
+ message: evidence.message,
1001
+ kind: "memory" as const,
1002
+ sourceIds: [evidence.item.id],
1003
+ score: 90 + Math.min(0.999999, Math.max(0, evidence.score) / 1_000),
1004
+ reason: evidence.reason,
1005
+ })),
1006
+ ],
1007
+ }))
1008
+ : undefined;
976
1009
  const retrieval = this.retrieveHistory(
977
1010
  effectiveEvent,
978
1011
  ctx,
979
1012
  effectiveContextConfig.maxRetrievedHistoryTokens,
1013
+ retrievalActiveContextEntryIds,
980
1014
  );
981
1015
  const project = this.retrieveProjectKnowledge(
982
1016
  requestText,
@@ -1110,10 +1144,10 @@ export class Ds4ContextRuntime {
1110
1144
  });
1111
1145
  const plan = planManagedContext({
1112
1146
  messages: effectiveEvent.messages,
1113
- fixedTokens: baseline.composition.systemTokens + baseline.composition.toolTokens,
1147
+ fixedTokens,
1114
1148
  budget,
1115
1149
  config: effectiveContextConfig,
1116
- pinnedMessageIndices: findPiPinnedMessageIndices(effectiveEvent, ctx),
1150
+ pinnedMessageIndices,
1117
1151
  supplementalMessages: rankedSupplementalMessages,
1118
1152
  ...(ranking.diagnostics.status === "active"
1119
1153
  ? { supplementalSelectionOrder: ranking.ranked.map((candidate) => candidate.id) }
@@ -1175,6 +1209,17 @@ export class Ds4ContextRuntime {
1175
1209
  selected: plannerSelectedProject,
1176
1210
  };
1177
1211
  plan.planning.durationMs = Math.max(0, this.now() - planningStartedAt);
1212
+ const oversizedTurnExclusions = plan.planning.oversizedTurnExclusions ?? 0;
1213
+ if (plan.mode === "managed" && oversizedTurnExclusions > 0) {
1214
+ this.logger.warn("context.excluded_oversized_turn", {
1215
+ oversizedTurnCount: oversizedTurnExclusions,
1216
+ rescuedImmediatePredecessor: plan.planning.rescuedImmediatePredecessor ?? false,
1217
+ recentTailTokenLimit: plan.planning.recentTailTokenLimit,
1218
+ messageTargetTokens: plan.planning.messageTargetTokens,
1219
+ selectedGroupCount: plan.planning.selectedGroupCount,
1220
+ excludedGroupCount: plan.planning.excludedGroupCount,
1221
+ });
1222
+ }
1178
1223
  const plannedEvent: ContextEvent = { type: "context", messages: plan.messages };
1179
1224
  const selectedArtifactReferences = plan.mode === "managed"
1180
1225
  ? plan.selected.flatMap((metadata) => {
@@ -2758,10 +2803,36 @@ export class Ds4ContextRuntime {
2758
2803
  }
2759
2804
  }
2760
2805
 
2806
+ private plannedContextEntryIds(
2807
+ ctx: ExtensionContext,
2808
+ plan: ManagedContextPlan<ContextEvent["messages"][number]>,
2809
+ ): Set<string> {
2810
+ if (plan.mode === "fallback") {
2811
+ return new Set(ctx.sessionManager.buildContextEntries().map((entry) => entry.id));
2812
+ }
2813
+ const syntheticIndices = new Set(
2814
+ [...plan.selected, ...plan.excluded]
2815
+ .filter((metadata) =>
2816
+ metadata.kind === "memory"
2817
+ || (metadata.kind === "pin" && metadata.sourceId !== undefined)
2818
+ )
2819
+ .map((metadata) => metadata.originalIndex),
2820
+ );
2821
+ const selectedIndices = new Set(plan.selected.map((metadata) => metadata.originalIndex));
2822
+ const sources = findPiSourceEntryIds(plan.originalMessages, ctx, syntheticIndices);
2823
+ const entryIds = new Set<string>();
2824
+ for (const index of selectedIndices) {
2825
+ const sourceId = sources[index];
2826
+ if (sourceId) entryIds.add(sourceId);
2827
+ }
2828
+ return entryIds;
2829
+ }
2830
+
2761
2831
  private retrieveHistory(
2762
2832
  event: ContextEvent,
2763
2833
  ctx: ExtensionContext,
2764
2834
  maxTokens = this.config.context.maxRetrievedHistoryTokens,
2835
+ activeContextEntryIds?: ReadonlySet<string>,
2765
2836
  ): RetrievalDiagnostics {
2766
2837
  const requestText = currentRequestText(event.messages);
2767
2838
  if (!this.retrievalEngine || !this.session?.sessionFile || !requestText) {
@@ -2775,7 +2846,8 @@ export class Ds4ContextRuntime {
2775
2846
  sessionId: this.session.sessionId,
2776
2847
  requestText,
2777
2848
  activeBranchEntryIds: new Set(ctx.sessionManager.getBranch().map((entry) => entry.id)),
2778
- activeContextEntryIds: new Set(ctx.sessionManager.buildContextEntries().map((entry) => entry.id)),
2849
+ activeContextEntryIds: activeContextEntryIds
2850
+ ?? new Set(ctx.sessionManager.buildContextEntries().map((entry) => entry.id)),
2779
2851
  exact: this.config.retrieval.exact,
2780
2852
  fts: this.config.retrieval.fts,
2781
2853
  semantic: this.config.retrieval.semantic,
@@ -244,6 +244,22 @@ export function findExactPiMessageSourceIds(
244
244
  });
245
245
  }
246
246
 
247
+ /**
248
+ * Maps managed-plan messages back to Pi session entry ids. Synthetic evidence
249
+ * indices are skipped so role/order fallbacks stay aligned with real messages.
250
+ */
251
+ export function findPiSourceEntryIds(
252
+ messages: readonly unknown[],
253
+ ctx: Pick<ExtensionContext, "sessionManager">,
254
+ syntheticIndices: ReadonlySet<number> = new Set(),
255
+ ): Array<string | undefined> {
256
+ return mapMessageSources(
257
+ messages,
258
+ sourceCandidates(ctx.sessionManager.buildContextEntries()),
259
+ syntheticIndices,
260
+ ).map((source) => source.sourceId);
261
+ }
262
+
247
263
  export function findPiPinnedMessageIndices(event: ContextEvent, ctx: ExtensionContext): number[] {
248
264
  const candidates = sourceCandidates(ctx.sessionManager.buildContextEntries());
249
265
  const sources = mapMessageSources(event.messages, candidates);
@@ -1,4 +1,4 @@
1
- export const EXTENSION_VERSION = "0.3.5";
1
+ export const EXTENSION_VERSION = "0.3.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";