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 +1 -0
- package/docs/CONTEXT_PLANNER.md +3 -1
- package/docs/releases/0.3.5.md +18 -2
- package/docs/releases/0.3.6.md +53 -0
- package/package.json +2 -2
- package/src/extension/commands.ts +6 -0
- package/src/extension/runtime.ts +76 -4
- package/src/pi-adapter/context-observer.ts +16 -0
- package/src/pi-adapter/version.ts +1 -1
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,
|
package/docs/CONTEXT_PLANNER.md
CHANGED
|
@@ -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
|
package/docs/releases/0.3.5.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# DS4 Context Engine 0.3.5
|
|
2
2
|
|
|
3
|
-
Status:
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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`}`,
|
package/src/extension/runtime.ts
CHANGED
|
@@ -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
|
|
1147
|
+
fixedTokens,
|
|
1114
1148
|
budget,
|
|
1115
1149
|
config: effectiveContextConfig,
|
|
1116
|
-
pinnedMessageIndices
|
|
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:
|
|
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);
|