ds4-context-engine 0.3.3 → 0.3.5
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 +47 -6
- package/docs/ADR/059-optional-anchored-editing.md +51 -0
- package/docs/ADR/060-optional-portable-agent-tools.md +38 -0
- package/docs/ADR/061-compaction-latency.md +21 -0
- package/docs/ADR/README.md +3 -0
- package/docs/ANCHORED_EDITING.md +144 -0
- package/docs/ARCHITECTURE.md +38 -0
- package/docs/COMPACTION.md +33 -11
- package/docs/PORTABLE_AGENT_TOOLS.md +86 -0
- package/docs/releases/0.3.3.md +22 -2
- package/docs/releases/0.3.4.md +81 -0
- package/docs/releases/0.3.5.md +70 -0
- package/package.json +2 -2
- package/src/extension/adaptive-read-tool.ts +47 -0
- package/src/extension/anchored-edit-tool.ts +157 -0
- package/src/extension/anchored-edit.ts +53 -0
- package/src/extension/bash-job-tool.ts +149 -0
- package/src/extension/commands.ts +12 -0
- package/src/extension/index.ts +18 -0
- package/src/extension/post-edit-report.ts +134 -0
- package/src/extension/runtime.ts +10 -5
- package/src/pi-adapter/compaction-coordinator.ts +168 -76
- package/src/pi-adapter/compaction-workers.ts +43 -0
- package/src/pi-adapter/summary-generator.ts +5 -6
- package/src/pi-adapter/version.ts +1 -1
- package/src/tools/bash-job-manager.ts +176 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Optional portable agent tools
|
|
2
|
+
|
|
3
|
+
These features are separate opt-ins, also gated by the DS4 master `enabled` switch. They do not implement inference rewind, forced sampling, or native KV-cache integration. Pi remains the agent runtime.
|
|
4
|
+
|
|
5
|
+
Enable only the features you want, then reload:
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
/context config set editing.postEditReport true
|
|
9
|
+
/context config set reading.adaptive true
|
|
10
|
+
/context config set artifacts.adaptiveBudget true
|
|
11
|
+
/context config set jobs.enabled true
|
|
12
|
+
/reload
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
All four default to `false`. Configuration writes apply on the next session load, not immediately. The default target is the trusted project configuration; add `--global` for the agent-directory file. Disable with the same commands using `false`, then reload.
|
|
16
|
+
|
|
17
|
+
## Post-edit reports
|
|
18
|
+
|
|
19
|
+
`editing.postEditReport` works with native ordinary edits, independently of `editing.anchored`. Enable both to combine reports with [`[upto]` ranges](ANCHORED_EDITING.md).
|
|
20
|
+
|
|
21
|
+
After a successful edit, the tool result content and details include:
|
|
22
|
+
|
|
23
|
+
- actual old/new changed ranges from Pi's native unified patch;
|
|
24
|
+
- total line delta and cumulative shift for subsequent lines;
|
|
25
|
+
- bounded numbered context from the updated content, encoded as quoted JSON data.
|
|
26
|
+
|
|
27
|
+
Regions combine nearby changes in the same native patch hunk. Zero-length ranges identify insertion/deletion boundaries. Coordinates refer to the completed write, not future file state. Reports include at most six regions and six short context lines per region; rendered report text stays below 6000 characters. The native patch/diff remains available and the TUI continues to display the native diff. There is no separate file read after the write and no change to matching or mutation queues. If reporting fails, the successful edit result is retained rather than encouraging a duplicate write.
|
|
28
|
+
|
|
29
|
+
## Adaptive reads
|
|
30
|
+
|
|
31
|
+
`reading.adaptive` changes only the default line limit of the native `read` tool:
|
|
32
|
+
|
|
33
|
+
| Execution-time model window | Default lines |
|
|
34
|
+
| --- | ---: |
|
|
35
|
+
| <= 8192 tokens | 120 |
|
|
36
|
+
| <= 16384 tokens | 240 |
|
|
37
|
+
| larger | 500 |
|
|
38
|
+
| missing/invalid model window | native default |
|
|
39
|
+
|
|
40
|
+
Explicit positive integer `offset`/`limit` values are honored; native byte limits can still shorten a response. Images, path resolution, access errors and cancellation remain native. The model is checked on each execution, so switching models does not require reload. Arguments are copied, not rewritten in canonical tool calls.
|
|
41
|
+
|
|
42
|
+
Use the native continuation notice, e.g. `offset=121`, to read more. There is deliberately no shared `more` cursor, which could become ambiguous after concurrent reads, branching or compaction. This does not reduce the native reader's full-file I/O or memory use.
|
|
43
|
+
|
|
44
|
+
Both file overrides skip initial registration if their built-in tool is missing or visibly overridden. Disabling an already-enabled instance restores the native definition; fresh disabled loads register nothing. Compatibility is tested against the project's Pi dependency, currently `0.84.3`. Custom SDK/remote filesystem integrations are not covered by these local wrappers.
|
|
45
|
+
|
|
46
|
+
## Adaptive tool-result/artifact budget
|
|
47
|
+
|
|
48
|
+
`artifacts.adaptiveBudget` operates only where artifact offload already operates: DS4 enabled, managed context, and artifact storage enabled/available.
|
|
49
|
+
|
|
50
|
+
The per-context policy uses the same calibrated active input budget as the planner, after output reserve and safety margin. It subtracts estimated system/tool-schema overhead, other messages and non-text blocks, then shares 75% of remaining capacity among candidate text results. It can only lower `artifacts.maxInlineToolResultChars` and `artifacts.excerptChars`, never raise them. The reference-metadata floor is 1600 characters or the configured inline maximum, whichever is smaller.
|
|
51
|
+
|
|
52
|
+
This is estimated budgeting, not a tokenizer-exact fit guarantee. Irreducible metadata, errors, missing provenance or unavailable storage can still leave a context oversized. The existing planner and fallback behavior remain responsible; there is no recursive `ctx.compact()` call inside the context hook.
|
|
53
|
+
|
|
54
|
+
Canonical Pi messages stay unchanged. Only privacy-prepared provider-facing text is transformed, with the original tool identity, source IDs and non-text blocks preserved. Artifact storage/search caps and branch/privacy checks remain unchanged. Rebuild uses the conservative adaptive floor so earlier small-budget artifacts remain reconstructible; consequently it may reconstruct more small artifacts than the fixed-threshold policy. A result is not replaced with a larger estimated result.
|
|
55
|
+
|
|
56
|
+
## Managed local bash jobs
|
|
57
|
+
|
|
58
|
+
The `bash_job` tool is implemented in a separate optional module, not in the portable core. It does not replace `bash` and is not a new agent loop or a separate published package.
|
|
59
|
+
|
|
60
|
+
Examples of tool arguments:
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{"action":"start","command":"npm test","timeout":300}
|
|
64
|
+
{"action":"list"}
|
|
65
|
+
{"action":"status","id":"<returned-job-id>"}
|
|
66
|
+
{"action":"stop","id":"<returned-job-id>"}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Authorization and execution
|
|
70
|
+
|
|
71
|
+
- Requires an active built-in local `bash`, an unclaimed `bash_job` name, a trusted project, and local UI confirmation for every start. Missing UI or refused confirmation means no command starts. Tool-call arguments are revalidated, and the confirmed command/cwd/timeout are snapshotted.
|
|
72
|
+
- Executes in the current working directory using Pi's public local BashOperations. It **does not inherit** custom bash-only permission hooks, remote/sandbox behavior, SDK shell settings/command prefixes, or session-specific `PI_*` injection. It must not be used to bypass those controls. The confirmation explicitly identifies the local execution boundary.
|
|
73
|
+
- Status/stop/list use opaque IDs owned by the current session and originating branch entry, never arbitrary PIDs. Stop remains available for owned jobs even if bash is later deactivated.
|
|
74
|
+
- Four running jobs maximum; sixteen retained records maximum. Oldest completed records/logs are evicted first. Timeout defaults to 300 seconds; valid range is 1–3600 seconds.
|
|
75
|
+
|
|
76
|
+
### Output and lifecycle
|
|
77
|
+
|
|
78
|
+
Logs combine stdout/stderr in native arrival order. Each private local log is capped at 8 MiB; reaching the cap or a write error stops the job. Status returns at most 512 bytes of head plus 512 bytes of non-overlapping tail, quoted in JSON, and the local output path. For larger logs, read that path while the session remains open. `outputTruncated` also indicates omitted middle output; `status=output-limit` distinguishes a capped log. A nonzero exit code is reported, not hidden.
|
|
79
|
+
|
|
80
|
+
Successful start returns immediately; subsequent agent cancellation does not silently kill the independently running job. Cancelling a status request does not stop it. Use `stop` explicitly. Completion marks project indexing dirty but does not trigger another model turn.
|
|
81
|
+
|
|
82
|
+
Jobs survive compaction. The extension appends a bounded canonical **metadata snapshot** after compaction, without command/output text or a follow-up model request. Refresh status before acting on any old snapshot. Branch navigation stops jobs no longer visible from the new branch; ancestor-owned jobs remain visible. Session replacement, reload and graceful shutdown stop jobs and remove their logs. State is not persisted in SQLite, Memory or Pins and is not restored after restart.
|
|
83
|
+
|
|
84
|
+
Native Pi handles process-tree cancellation. There is no rollback of command side effects and no guarantee of controlling deliberately escaped daemons. Forced process termination/crashes can leave temporary logs; abrupt-exit cleanup is not guaranteed. Raw logs can contain secrets: do not publish or persist them blindly. Linux tests do not establish Windows execution success.
|
|
85
|
+
|
|
86
|
+
Architecture: [ADR 060](ADR/060-optional-portable-agent-tools.md).
|
package/docs/releases/0.3.3.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# DS4 Context Engine 0.3.3
|
|
2
2
|
|
|
3
|
-
Status:
|
|
3
|
+
Status: published 2026-09-04 as stable, printed under the npm `latest` dist-tag, tagged `v0.3.3`.
|
|
4
4
|
|
|
5
5
|
This release adds a configurable transport retry policy for compaction summary requests on top of the 0.3.2 stable candidate. It carries forward 0.3.2 without changing canonical records, SQLite schema, runtime contracts, retention limits, privacy policy, compaction validation, or fallback semantics.
|
|
6
6
|
|
|
@@ -43,7 +43,7 @@ ds4-context-reference-adapter
|
|
|
43
43
|
ds4-context-engine
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
Both adapters depend exactly on `ds4-context-core@0.3.3`. Publication uses npm's default `latest` tag for all three packages. GitHub Actions remains validation-only with OIDC and package-write permissions denied.
|
|
46
|
+
Both adapters depend exactly on `ds4-context-core@0.3.3`. Publication uses npm's default `latest` tag for all three packages: `latest` now resolves to `0.3.3` (previously `0.3.2`), `beta` remains `0.3.0-beta.3`, `alpha` remains `0.3.0-alpha.5`, and `rc` remains `0.2.0-rc.1`. GitHub Actions remains validation-only with OIDC and package-write permissions denied.
|
|
47
47
|
|
|
48
48
|
## Candidate validation
|
|
49
49
|
|
|
@@ -58,3 +58,23 @@ Local candidate verification on Node.js `26.5.1`:
|
|
|
58
58
|
- Version, exact core dependencies, package-lock entries, extension constant, and reference-adapter constant are synchronized to `0.3.3`.
|
|
59
59
|
|
|
60
60
|
Validation-only CI is recorded below with the release commit. Exact registry verification and the annotated tag are recorded after execution.
|
|
61
|
+
|
|
62
|
+
## Validation-only CI
|
|
63
|
+
|
|
64
|
+
Release commit `5d636cc` (chore(release): prepare 0.3.3); CI run `33921584110` on Node `22.19.0` and `24.x`. The first Node 22.19.0 attempt hit a one-off `upgrade-rebuild` migration test timeout (5 s default) on the shared runner; the exact same test passes locally and on the re-run, and the rerun completed `success` on both Node versions.
|
|
65
|
+
|
|
66
|
+
## Registry verification and tag
|
|
67
|
+
|
|
68
|
+
Published manually in dependency order with npm's default `latest` tag after `npm whoami` confirmed `alucard_24`:
|
|
69
|
+
|
|
70
|
+
- `ds4-context-core@0.3.3`; shasum `5db4c81a9c6a8861bf6612d499abec7fb576e411`;
|
|
71
|
+
- `ds4-context-reference-adapter@0.3.3`; shasum `7a251de9735968776d21ed16a2e10786ea2bc2ef`;
|
|
72
|
+
- `ds4-context-engine@0.3.3`; shasum `cb107f9e28d89e860b3b05118f374a9c9969b58c`.
|
|
73
|
+
|
|
74
|
+
`npm run registry:check -- 0.3.3` passed: fresh-install exact core/adapters, public core/KV exports, compiled reference conformance, packaged quality corpus, installed `ds4-context-storage` CLI shim probe, and published Pi extension startup through isolated offline RPC state. The first check attempt hit a stale npm cache ETARGET for `ds4-context-engine@0.3.3`; a direct registry query confirmed all three artifacts and dist-tags, and the retry passed.
|
|
75
|
+
|
|
76
|
+
npm dist-tags for all three packages: `latest=0.3.3`, `beta=0.3.0-beta.3`, `alpha=0.3.0-alpha.5`, `rc=0.2.0-rc.1`.
|
|
77
|
+
|
|
78
|
+
Annotated tag `v0.3.3` was pushed and the GitHub Release was created:
|
|
79
|
+
|
|
80
|
+
https://github.com/Alucard24/ds4-context-engine/releases/tag/v0.3.3
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# DS4 Context Engine 0.3.4
|
|
2
|
+
|
|
3
|
+
Status: published as stable on npm under `latest`; annotated tag `v0.3.4` points to validated release commit `5845a64`.
|
|
4
|
+
|
|
5
|
+
## Added since 0.3.3
|
|
6
|
+
|
|
7
|
+
Five independent opt-ins, all default-off and gated by the master `enabled` switch:
|
|
8
|
+
|
|
9
|
+
- **`editing.anchored`**: exact, inclusive `head[upto]tail` replacements, expanded under Pi's native mutation queue. Literal escaping, mixed-batch exactness, native cancellation/line endings and real result diffs are preserved. No generation-time marker forcing.
|
|
10
|
+
- **`editing.postEditReport`**: bounded changed-line ranges, line delta and numbered updated context derived from the actual native patch, without rereading the file. Works independently of anchored editing.
|
|
11
|
+
- **`reading.adaptive`**: execution-time model-aware default read windows of 120/240/500 lines. Explicit limits, images and native byte caps remain native.
|
|
12
|
+
- **`artifacts.adaptiveBudget`**: per-context estimated caps can only lower configured inline/excerpt limits. Privacy-prepared provider context, source provenance, non-expanding replacements and conservative artifact rebuild are covered.
|
|
13
|
+
- **`jobs.enabled`**: separate local `bash_job` start/status/stop/list tool with trusted-project and local UI confirmation for every start, session/branch ownership, bounded output/concurrency/timeouts and lifecycle cleanup. Jobs survive compaction with a metadata-only reminder, not session replacement/reload/shutdown.
|
|
14
|
+
|
|
15
|
+
See [anchored editing](../ANCHORED_EDITING.md), [portable agent tools](../PORTABLE_AGENT_TOOLS.md), [ADR 059](../ADR/059-optional-anchored-editing.md) and [ADR 060](../ADR/060-optional-portable-agent-tools.md).
|
|
16
|
+
|
|
17
|
+
## Safety and compatibility
|
|
18
|
+
|
|
19
|
+
Pi JSONL remains canonical and append-only; SQLite remains disposable/rebuildable with schema `15`. No live database intervention, provider/backend integration or Pi upgrade is part of this release.
|
|
20
|
+
|
|
21
|
+
Unchanged contracts: `ds4-context-config-v1`, `runtime-adapter-v1`, `ds4-context-persistence-tool-v1`, `ds4-context-persistence-result-v1`. Pi remains pinned to `0.84.3`; Node.js requirement remains `>=22.19.0`.
|
|
22
|
+
|
|
23
|
+
Local tool wrappers must not be combined with remote/sandbox replacements. `bash_job` does not inherit bash-only permission hooks or SDK shell settings; its confirmation explicitly identifies this boundary. Stop does not roll back shell side effects or guarantee ownership of escaped daemons. Linux execution tests do not establish Windows correctness. Adaptive budgets are estimates; real-provider token savings, latency and reliability have not been measured. No operational KV reuse, rewind, forced sampling or marker insertion is added.
|
|
24
|
+
|
|
25
|
+
## Update and enable
|
|
26
|
+
|
|
27
|
+
Install the exact package and fully restart Pi to load the matching compiled core:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pi install npm:ds4-context-engine@0.3.4
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
In a trusted project, enable only the features you want:
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
/context config set editing.anchored true
|
|
37
|
+
/context config set editing.postEditReport true
|
|
38
|
+
/context config set reading.adaptive true
|
|
39
|
+
/context config set artifacts.adaptiveBudget true
|
|
40
|
+
/context config set jobs.enabled true
|
|
41
|
+
/reload
|
|
42
|
+
/context config show
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Add `--global` to each `set` to target the agent-directory configuration instead of the project file. Trusted project settings take precedence. Configuration writes do not change the active session until reload/startup. Use `false` to disable. The DS4 master `enabled` switch must also be true.
|
|
46
|
+
|
|
47
|
+
## Package policy and validation
|
|
48
|
+
|
|
49
|
+
All three packages use `0.3.4`: `ds4-context-core`, `ds4-context-reference-adapter`, `ds4-context-engine`. Both adapters depend exactly on `ds4-context-core@0.3.4`. Publication is manual in that order using npm's stable `latest` tag. GitHub Actions remains validation-only, with OIDC and package-write permissions denied.
|
|
50
|
+
|
|
51
|
+
Candidate validation on Node.js `26.5.1`, from a sanitized release source with a fresh `npm ci`:
|
|
52
|
+
|
|
53
|
+
- `npm run check`: **78 files / 485 tests passed**, including native edit queue/matching regressions, independent opt-ins and real local bash execution/stop/timeout.
|
|
54
|
+
- `npm run quality:compare`: passed; frozen-corpus candidate score `0.9875` versus baseline `0.808156`.
|
|
55
|
+
- `npm run schema:context-persistence`: passed, `1266` bytes / `317` estimated tokens (limits `1500` / `320`).
|
|
56
|
+
- `npm run latency:check` against exact `ds4-context-core@0.1.2`: passed; disabled-planning p95 ratio `0.885863`, maximum `1.1`. This is not a provider latency/savings measurement.
|
|
57
|
+
- `npm run pack:check`: clean-consumer core **231 files**, reference adapter **7**, engine **84**, with eight real offline Pi registry scenarios.
|
|
58
|
+
- All three `npm pack --dry-run --json` inventories and `git diff --check` passed.
|
|
59
|
+
- Versions, exact core dependencies, lockfile and runtime version constants are synchronized. No dependency upgrade was performed.
|
|
60
|
+
|
|
61
|
+
Pre-existing local `allowScripts` additions and `.serena/` are excluded from release commits and public packages.
|
|
62
|
+
|
|
63
|
+
The initial CI run `33965143704` on candidate `4e665e3` passed Node `22.19.0`. Node `24.x` exceeded the default 5-second timeout in the existing disk-backed schema-v10 upgrade fixture (484 tests passed, one timeout). That individual correctness test now has a bounded 15-second timeout, with all assertions unchanged; no runtime/storage behavior changed.
|
|
64
|
+
|
|
65
|
+
Final validation-only CI run [`33965385188`](https://github.com/Alucard24/ds4-context-engine/actions/runs/33965385188) on `5845a64` passed both Node `22.19.0` and `24.x`, including the full test suite and clean-consumer package checks.
|
|
66
|
+
|
|
67
|
+
## Registry verification and release
|
|
68
|
+
|
|
69
|
+
Published manually as `alucard_24`, in dependency order, from reviewed tarballs built in a clean committed worktree:
|
|
70
|
+
|
|
71
|
+
- `ds4-context-core@0.3.4`: shasum `c1f1f6c5eec392e43f8b6a0917581e5d02141c84`;
|
|
72
|
+
- `ds4-context-reference-adapter@0.3.4`: shasum `61402dc240af5963f4cbbf5f69752fe9a451170d`;
|
|
73
|
+
- `ds4-context-engine@0.3.4`: shasum `99b9ba52c2bc628f5fb197ea5356459963994b89`.
|
|
74
|
+
|
|
75
|
+
`npm run registry:check -- 0.3.4` 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 integrity values match the local tarballs for all three packages.
|
|
76
|
+
|
|
77
|
+
Verified dist-tags for all three: `latest=0.3.4`, `beta=0.3.0-beta.3`, `alpha=0.3.0-alpha.5`, `rc=0.2.0-rc.1`.
|
|
78
|
+
|
|
79
|
+
Annotated tag `v0.3.4` targets `5845a64`, the tested/published source. This post-publication evidence update is documentation-only.
|
|
80
|
+
|
|
81
|
+
GitHub Release: https://github.com/Alucard24/ds4-context-engine/releases/tag/v0.3.4
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# DS4 Context Engine 0.3.5
|
|
2
|
+
|
|
3
|
+
Status: local release gates passed; commit, push and coordinated publication authorized. Validation-only CI, publication and registry verification are pending.
|
|
4
|
+
|
|
5
|
+
## Added since 0.3.4
|
|
6
|
+
|
|
7
|
+
Bounded compaction optimizations, enabled by default when DS4 custom compaction is enabled:
|
|
8
|
+
|
|
9
|
+
- **`compaction.directUpdate=true`**: one validated previous-summary plus new-source request when the complete sanitized prompt fits. The immutable `task-state` node retains predecessor edges, transitive source IDs and a source hash bound to both inputs. Oversized updates retain hierarchical segmentation/aggregation; no synthetic segment or source truncation is introduced.
|
|
10
|
+
- **`compaction.inputBudget="summary"`**: use the calibrated hard input limit rather than the ordinary context fill target, additionally capped by model context minus safety margin and actual summary output headroom. Configured/model hard limits and calibration remain authoritative; ordinary context planning is unchanged.
|
|
11
|
+
- **`compaction.maxConcurrentSegments=2`** (integer 1–2): overlap only independent segment requests. Node identities, sources, graph edges and usage follow source order, never completion order. Failure or cancellation stops scheduling, aborts siblings and drains every started worker before Pi fallback; no partial graph is installed. Aggregation stays ordered and bounded.
|
|
12
|
+
- **Metadata-only diagnostics**: `/context compaction` reports path, effective provider/model, full-update prompt size, logical calls, concurrency, retries and monotonic wall timings for preparation, generation, aggregation, persistence and total DS4 hook duration. Timings are process-local, exclude native fallback, and are not persisted in canonical JSONL.
|
|
13
|
+
|
|
14
|
+
Transport documentation now explicitly distinguishes **three total attempts** from three retries; runtime transport policy is unchanged.
|
|
15
|
+
|
|
16
|
+
See [compaction controls](../COMPACTION.md#latency-controls) and [ADR-061](../ADR/061-compaction-latency.md).
|
|
17
|
+
|
|
18
|
+
## Compatibility and limitations
|
|
19
|
+
|
|
20
|
+
Pi cut points, atomic tool exchanges, summary validation and bounded exact-value repair, privacy classification, immutable provenance and JSONL rebuild remain in place. Schema-v2 compaction metadata reuses the existing `task-state` kind. Pi JSONL remains canonical and append-only; SQLite remains disposable/rebuildable at schema `15`.
|
|
21
|
+
|
|
22
|
+
Unchanged contracts: `ds4-context-config-v1`, `runtime-adapter-v1`, `ds4-context-persistence-tool-v1`, `ds4-context-persistence-result-v1`. The new configuration keys are additive; absent keys use the new defaults. The five optional portable tools/features introduced in 0.3.4 remain default-off. Pi stays pinned to `0.84.3`; Node.js remains `>=22.19.0`. No dependency upgrade, live database maintenance or Pi upgrade is part of this release.
|
|
23
|
+
|
|
24
|
+
Mock-provider tests establish call counts, bounded overlap and deterministic safety properties, not real-provider speedups, semantic equivalence or a guaranteed compaction duration. A larger prompt may take longer to process. Rate-limit errors are not transport-retried; providers with restrictive concurrency should use `maxConcurrentSegments=1`. Cancellation is cooperative: accepted work may still be billed and providers ignoring abort may delay settlement. Linux validation does not establish Windows execution correctness.
|
|
25
|
+
|
|
26
|
+
## Update and configure
|
|
27
|
+
|
|
28
|
+
Install the exact coordinated package and **fully restart Pi** to load its matching compiled core:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pi install npm:ds4-context-engine@0.3.5
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
No extra opt-in is needed for the three new defaults when DS4 and custom compaction are enabled. Inspect configuration and subsequent compaction diagnostics with:
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
/context config show
|
|
38
|
+
/context compaction
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
For a legacy-path comparison in a trusted project:
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
/context config set compaction.directUpdate false
|
|
45
|
+
/context config set compaction.inputBudget context
|
|
46
|
+
/context config set compaction.maxConcurrentSegments 1
|
|
47
|
+
/reload
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Use `true`, `summary`, and `2` to restore the new defaults. Add `--global` to each `set` for agent-directory configuration; trusted project settings take precedence. Disabling DS4 custom compaction still delegates to Pi.
|
|
51
|
+
|
|
52
|
+
## Package policy and validation
|
|
53
|
+
|
|
54
|
+
All three packages use `0.3.5`: `ds4-context-core`, `ds4-context-reference-adapter`, `ds4-context-engine`. Both adapters depend exactly on `ds4-context-core@0.3.5`. Publication is manual in that order under npm's stable `latest` tag. GitHub Actions remains validation-only with OIDC and package-write permissions denied.
|
|
55
|
+
|
|
56
|
+
Candidate validation on Node.js `26.5.1`, from a sanitized release source with a fresh `npm ci`:
|
|
57
|
+
|
|
58
|
+
- `npm run check`: **80 files / 508 tests passed**, including exact full-prompt budget boundaries, direct-update evidence/privacy, immutable graph rebuild, out-of-order segments, abort/failure drainage and transport retry cancellation.
|
|
59
|
+
- `npm run quality:compare`: passed; frozen-corpus candidate score `0.9875` versus baseline `0.808156`. This is a planner corpus, not a real-provider summary-quality comparison.
|
|
60
|
+
- `npm run schema:context-persistence`: passed, `1266` bytes / `317` estimated tokens (limits `1500` / `320`).
|
|
61
|
+
- `npm run latency:check` against freshly installed exact `ds4-context-core@0.1.2`: passed; disabled-planning p95 ratio `0.903863`, maximum `1.1`. This is not a compaction/provider latency measurement.
|
|
62
|
+
- `npm run pack:check`: passed in a clean consumer, core **235 files**, reference adapter **7**, engine **87**, including isolated offline Pi extension startup and registry scenarios.
|
|
63
|
+
- All three `npm pack --dry-run --json` inventories and `git diff --check` passed. Tarball inventories exclude sessions, databases, credentials, test state and `.serena/`.
|
|
64
|
+
- Manifests, exact core dependencies, lockfile and runtime version constants are synchronized. The lockfile contains only coordinated version changes; no dependency upgrade was performed.
|
|
65
|
+
|
|
66
|
+
Pre-existing local `allowScripts` additions and `.serena/` are excluded from release commits and public packages.
|
|
67
|
+
|
|
68
|
+
## Registry verification and release
|
|
69
|
+
|
|
70
|
+
Pending successful local gates, validation-only CI, manual publication and exact registry verification. No release tag has been created yet.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ds4-context-engine",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
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.5"
|
|
66
66
|
},
|
|
67
67
|
"peerDependencies": {
|
|
68
68
|
"@earendil-works/pi-ai": "0.84.3",
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createReadToolDefinition,
|
|
3
|
+
defineTool,
|
|
4
|
+
type ExtensionAPI,
|
|
5
|
+
type ExtensionContext,
|
|
6
|
+
type ReadToolOptions,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
8
|
+
|
|
9
|
+
export function adaptiveReadLimit(contextWindow: number | undefined): number | undefined {
|
|
10
|
+
if (contextWindow === undefined || !Number.isFinite(contextWindow) || contextWindow <= 0) return undefined;
|
|
11
|
+
return contextWindow <= 8192 ? 120 : contextWindow <= 16384 ? 240 : 500;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function createAdaptiveReadTool(cwd: string, options?: ReadToolOptions) {
|
|
15
|
+
const base = createReadToolDefinition(cwd, options);
|
|
16
|
+
return defineTool({
|
|
17
|
+
...base,
|
|
18
|
+
description: `${base.description} Without an explicit limit, DS4 uses 120/240/500 lines for small/medium/large model context windows.`,
|
|
19
|
+
async execute(id, input, signal, onUpdate, ctx) {
|
|
20
|
+
// Tool-call hooks can mutate arguments after schema validation.
|
|
21
|
+
if (!input || typeof input.path !== "string"
|
|
22
|
+
|| (input.offset !== undefined && (!Number.isSafeInteger(input.offset) || input.offset < 1))
|
|
23
|
+
|| (input.limit !== undefined && (!Number.isSafeInteger(input.limit) || input.limit < 1))) {
|
|
24
|
+
throw new Error("Read requires path and optional positive integer offset/limit");
|
|
25
|
+
}
|
|
26
|
+
const limit = input.limit ?? adaptiveReadLimit(ctx.model?.contextWindow);
|
|
27
|
+
// Private copy only. Images and all byte/line-ending handling stay native.
|
|
28
|
+
return createReadToolDefinition(ctx.cwd || cwd, options).execute(
|
|
29
|
+
id, { ...input, ...(limit !== undefined ? { limit } : {}) }, signal, onUpdate, ctx,
|
|
30
|
+
);
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function createAdaptiveReadRegistration(pi: ExtensionAPI) {
|
|
36
|
+
let registered = false;
|
|
37
|
+
return (enabled: boolean, ctx: ExtensionContext): void => {
|
|
38
|
+
if (!enabled && !registered) return;
|
|
39
|
+
const existing = pi.getAllTools().find((tool) => tool.name === "read");
|
|
40
|
+
if (!registered && existing?.sourceInfo.source !== "builtin") {
|
|
41
|
+
if (ctx.hasUI) ctx.ui.notify("DS4 adaptive read not registered: native read is unavailable or already overridden.", "warning");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
pi.registerTool(enabled ? createAdaptiveReadTool(ctx.cwd) : createReadToolDefinition(ctx.cwd));
|
|
45
|
+
registered = true;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { access, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { Type, type Static } from "@earendil-works/pi-ai";
|
|
4
|
+
import {
|
|
5
|
+
createEditToolDefinition,
|
|
6
|
+
defineTool,
|
|
7
|
+
type EditOperations,
|
|
8
|
+
type EditToolOptions,
|
|
9
|
+
type ExtensionAPI,
|
|
10
|
+
type ExtensionContext,
|
|
11
|
+
} from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { normalizeEditText, resolveAnchoredEdit, UPTO_MARKER } from "./anchored-edit.ts";
|
|
13
|
+
import { addPostEditReport, createReportingEditTool } from "./post-edit-report.ts";
|
|
14
|
+
|
|
15
|
+
export const ANCHORED_EDIT_PARAMS = Type.Object({
|
|
16
|
+
path: Type.String({ description: "Path to the file to edit (relative or absolute)" }),
|
|
17
|
+
edits: Type.Array(Type.Object({
|
|
18
|
+
oldText: Type.String({
|
|
19
|
+
description: "Unique old text, or head[upto]tail for an inclusive range: head unique in the original file, tail unique after head. Anchors match exactly; newlines immediately after [upto] are separators.",
|
|
20
|
+
}),
|
|
21
|
+
newText: Type.String({ description: "Replacement for the entire old span, including both anchors." }),
|
|
22
|
+
literal: Type.Optional(Type.Boolean({
|
|
23
|
+
description: "Treat [upto] as ordinary text instead of a range marker. Default false.",
|
|
24
|
+
})),
|
|
25
|
+
})),
|
|
26
|
+
});
|
|
27
|
+
export type AnchoredEditInput = Static<typeof ANCHORED_EDIT_PARAMS>;
|
|
28
|
+
|
|
29
|
+
const localOperations: EditOperations = {
|
|
30
|
+
access: (path) => access(path, constants.R_OK | constants.W_OK),
|
|
31
|
+
readFile: (path) => readFile(path),
|
|
32
|
+
writeFile: (path, content) => writeFile(path, content, "utf8"),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function usesAnchors(edit: Partial<AnchoredEditInput["edits"][number]>): boolean {
|
|
36
|
+
return edit.literal !== true && typeof edit.oldText === "string" && edit.oldText.includes(UPTO_MARKER);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// tool_call handlers can mutate already-validated arguments. Recheck before I/O.
|
|
40
|
+
function validateInput(input: AnchoredEditInput): void {
|
|
41
|
+
if (!input || typeof input.path !== "string" || !Array.isArray(input.edits) || input.edits.length === 0) {
|
|
42
|
+
throw new Error("Edit input requires a path and at least one replacement in edits");
|
|
43
|
+
}
|
|
44
|
+
for (const [index, edit] of input.edits.entries()) {
|
|
45
|
+
if (!edit || typeof edit.oldText !== "string" || typeof edit.newText !== "string"
|
|
46
|
+
|| (edit.literal !== undefined && typeof edit.literal !== "boolean")) {
|
|
47
|
+
throw new Error(`Invalid edits[${index}]: expected oldText/newText strings and optional literal boolean`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Expand inside native edit's read operation, NOT in prepareArguments/tool_call.
|
|
54
|
+
* Native edit holds its shared mutation queue from access/read through write.
|
|
55
|
+
* Per-call copies keep expanded file contents out of canonical tool arguments.
|
|
56
|
+
*/
|
|
57
|
+
export function createAnchoredEditTool(cwd: string, options?: EditToolOptions, postEditReport = false) {
|
|
58
|
+
const base = createEditToolDefinition(cwd, options);
|
|
59
|
+
const operations = options?.operations ?? localOperations;
|
|
60
|
+
return defineTool({
|
|
61
|
+
...base,
|
|
62
|
+
description: "Edit a single file with unique, non-overlapping replacements against its original content. For large old spans, use head[upto]tail in oldText to replace the inclusive range between exact anchors. Use literal: true to match [upto] literally.",
|
|
63
|
+
promptSnippet: "Edit files with exact text or compact [upto] anchored ranges; supports disjoint edits[]",
|
|
64
|
+
promptGuidelines: [
|
|
65
|
+
"Use edit with edits[] for precise file changes. Match every edit against the original file; reject overlaps and merge nearby changes instead of overlapping edits.",
|
|
66
|
+
"For large old spans, prefer oldText containing first lines, [upto], then final lines. The head must be unique in the file and the tail unique after the head; include both anchors in newText if you want to keep them.",
|
|
67
|
+
"Copy anchors exactly from inspected content. Newlines immediately after [upto] are separators, not part of the tail anchor. Never omit the tail or use multiple markers. Use literal: true when oldText contains literal [upto] text.",
|
|
68
|
+
"All oldText in a batch containing anchors must match exactly. Native fuzzy fallback is available only in calls without anchored edits.",
|
|
69
|
+
"Keep ordinary oldText as small as possible while unique. Re-read after anchor or overlap errors; do not guess or broaden a destructive range.",
|
|
70
|
+
],
|
|
71
|
+
parameters: ANCHORED_EDIT_PARAMS,
|
|
72
|
+
// Native argument preparation also supports JSON-string/single-object edits
|
|
73
|
+
// and legacy top-level oldText/newText. It preserves optional literal flags.
|
|
74
|
+
prepareArguments: base.prepareArguments,
|
|
75
|
+
async execute(toolCallId, input, signal, onUpdate, ctx) {
|
|
76
|
+
validateInput(input);
|
|
77
|
+
// The targeted Pi version binds cwd at construction.
|
|
78
|
+
const executionCwd = ctx.cwd || cwd;
|
|
79
|
+
if (!input.edits.some(usesAnchors)) {
|
|
80
|
+
const ordinary = await createEditToolDefinition(executionCwd, options).execute(toolCallId, input, signal, onUpdate, ctx);
|
|
81
|
+
return postEditReport ? addPostEditReport(ordinary) : ordinary;
|
|
82
|
+
}
|
|
83
|
+
const requests = input.edits.map((edit) => ({ ...edit }));
|
|
84
|
+
const edits = requests.map((edit) => ({ oldText: edit.oldText, newText: edit.newText }));
|
|
85
|
+
const ranges: Array<{ editIndex: number; startLine: number; endLine: number }> = [];
|
|
86
|
+
const delegate = createEditToolDefinition(executionCwd, {
|
|
87
|
+
...options,
|
|
88
|
+
operations: {
|
|
89
|
+
access: (path) => operations.access(path),
|
|
90
|
+
writeFile: (path, content) => operations.writeFile(path, content),
|
|
91
|
+
async readFile(path) {
|
|
92
|
+
const buffer = await operations.readFile(path);
|
|
93
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
94
|
+
const original = normalizeEditText(buffer.toString("utf8").replace(/^\uFEFF/, ""));
|
|
95
|
+
for (const [index, edit] of requests.entries()) {
|
|
96
|
+
if (!usesAnchors(edit)) {
|
|
97
|
+
// Native fuzzy fallback changes the coordinate space for the
|
|
98
|
+
// WHOLE batch. Prevent it from relocating an exact anchored span.
|
|
99
|
+
if (!original.includes(normalizeEditText(edit.oldText))) {
|
|
100
|
+
throw new Error(`edits[${index}] in ${input.path}: anchored batches require exact ordinary oldText; re-read the file or use a separate marker-free call`);
|
|
101
|
+
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const span = resolveAnchoredEdit(original, edit.oldText);
|
|
106
|
+
edits[index]!.oldText = span.oldText;
|
|
107
|
+
ranges.push({ editIndex: index, startLine: span.startLine, endLine: span.endLine });
|
|
108
|
+
} catch (error) {
|
|
109
|
+
throw new Error(`edits[${index}] in ${input.path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return buffer;
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
// Native batch validation, overlap/no-op checks, cancellation, BOM/EOL
|
|
117
|
+
// restoration, write and diff generation all remain on the native path.
|
|
118
|
+
const nativeResult = await delegate.execute(toolCallId, { path: input.path, edits }, signal, onUpdate, ctx);
|
|
119
|
+
const result = postEditReport ? addPostEditReport(nativeResult) : nativeResult;
|
|
120
|
+
return {
|
|
121
|
+
...result,
|
|
122
|
+
content: [...result.content, {
|
|
123
|
+
type: "text" as const,
|
|
124
|
+
text: `Anchored replacements (original lines): ${ranges.map((range) => `edits[${range.editIndex}] ${range.startLine}-${range.endLine}`).join(", ")}.`,
|
|
125
|
+
}],
|
|
126
|
+
details: result.details && { ...result.details, anchoredRanges: ranges },
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
renderCall(args, theme, context) {
|
|
130
|
+
// The native preview matcher does not understand markers. Do not run a
|
|
131
|
+
// misleading pre-execution preview; renderResult supplies the actual diff.
|
|
132
|
+
const anchored = Array.isArray(args.edits) && args.edits.some((edit) => edit && usesAnchors(edit));
|
|
133
|
+
return base.renderCall!(args, theme, anchored ? { ...context, argsComplete: false } : context);
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Session-bound opt-in. Do not claim tools reported as extension-/SDK-owned. */
|
|
139
|
+
export function createAnchoredEditRegistration(pi: ExtensionAPI) {
|
|
140
|
+
let registered = false;
|
|
141
|
+
return (features: boolean | { anchored: boolean; postEditReport: boolean }, ctx: ExtensionContext): void => {
|
|
142
|
+
const anchored = typeof features === "boolean" ? features : features.anchored;
|
|
143
|
+
const postEditReport = typeof features === "boolean" ? false : features.postEditReport;
|
|
144
|
+
const enabled = anchored || postEditReport;
|
|
145
|
+
if (!enabled && !registered) return;
|
|
146
|
+
const existing = pi.getAllTools().find((tool) => tool.name === "edit");
|
|
147
|
+
if (!registered && existing?.sourceInfo.source !== "builtin") {
|
|
148
|
+
if (ctx.hasUI) ctx.ui.notify("DS4 anchored edit not registered: native edit is unavailable or already overridden.", "warning");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
// Pi has no unregisterTool API. After an enabled session, restore the native
|
|
152
|
+
// definition on disable. A fresh reload while disabled registers nothing.
|
|
153
|
+
pi.registerTool(anchored ? createAnchoredEditTool(ctx.cwd, undefined, postEditReport)
|
|
154
|
+
: postEditReport ? createReportingEditTool(ctx.cwd) : createEditToolDefinition(ctx.cwd));
|
|
155
|
+
registered = true;
|
|
156
|
+
};
|
|
157
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** Portable text semantics; no filesystem access or inference-time forcing. */
|
|
2
|
+
export const UPTO_MARKER = "[upto]";
|
|
3
|
+
|
|
4
|
+
export interface AnchoredEditSpan {
|
|
5
|
+
/** Offsets in LF-normalized, BOM-free original content (UTF-16 code units). */
|
|
6
|
+
start: number;
|
|
7
|
+
end: number;
|
|
8
|
+
oldText: string;
|
|
9
|
+
startLine: number;
|
|
10
|
+
endLine: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function normalizeEditText(text: string): string {
|
|
14
|
+
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Count overlapping occurrences as ambiguous too. Tail uniqueness is suffix-only. */
|
|
18
|
+
function uniqueAnchor(content: string, anchor: string, from: number, label: string): number {
|
|
19
|
+
if (!anchor.trim()) throw new Error(`${label} anchor must contain non-whitespace text`);
|
|
20
|
+
const position = content.indexOf(anchor, from);
|
|
21
|
+
if (position < 0) throw new Error(`${label} anchor not found${from > 0 ? " after head" : ""}`);
|
|
22
|
+
if (content.indexOf(anchor, position + 1) >= 0) {
|
|
23
|
+
throw new Error(`${label} anchor is not unique${from > 0 ? " after head" : ""}`);
|
|
24
|
+
}
|
|
25
|
+
return position;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolve one DS4-style range. Both anchors are included in the replacement.
|
|
30
|
+
* As in DS4, newlines immediately after the marker are separators, not part of
|
|
31
|
+
* the tail needle. No trimming of anchor spaces, fuzzy matching or automatic
|
|
32
|
+
* forcer's size/line thresholds. The caller supplies the original normalized file.
|
|
33
|
+
*/
|
|
34
|
+
export function resolveAnchoredEdit(content: string, oldText: string): AnchoredEditSpan {
|
|
35
|
+
const old = normalizeEditText(oldText);
|
|
36
|
+
const marker = old.indexOf(UPTO_MARKER);
|
|
37
|
+
if (marker < 0) throw new Error("Anchored oldText must contain one [upto] marker");
|
|
38
|
+
if (old.indexOf(UPTO_MARKER, marker + UPTO_MARKER.length) >= 0) {
|
|
39
|
+
throw new Error("Anchored oldText contains more than one [upto] marker; use literal: true for literal text");
|
|
40
|
+
}
|
|
41
|
+
const head = old.slice(0, marker);
|
|
42
|
+
const tail = old.slice(marker + UPTO_MARKER.length).replace(/^\n+/, "");
|
|
43
|
+
const start = uniqueAnchor(content, head, 0, "Head");
|
|
44
|
+
const tailStart = uniqueAnchor(content, tail, start + head.length, "Tail");
|
|
45
|
+
const end = tailStart + tail.length;
|
|
46
|
+
return {
|
|
47
|
+
start,
|
|
48
|
+
end,
|
|
49
|
+
oldText: content.slice(start, end),
|
|
50
|
+
startLine: content.slice(0, start).split("\n").length,
|
|
51
|
+
endLine: content.slice(0, end - 1).split("\n").length,
|
|
52
|
+
};
|
|
53
|
+
}
|