Skip to content
Merged

V2 #617

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
bbc2e6d
feat: preserve protected V2 tool and subagent results
Tarquinen Sep 16, 2026
3f16adf
feat: add a V2 server adapter alongside V1 support
Tarquinen Sep 16, 2026
83095eb
feat: support the DCP panel in the V2 terminal
Tarquinen Sep 16, 2026
3026754
test: add containerized V1 and V2 integration coverage
Tarquinen Sep 16, 2026
353ab3a
feat: add an isolated dual-version DCP sandbox
Tarquinen Sep 16, 2026
a252de2
docs: document V2 migration and sandbox workflows
Tarquinen Sep 16, 2026
cd8a324
chore: keep the migration journal local
Tarquinen Sep 16, 2026
dcd904d
docs: separate user and contributor guidance
Tarquinen Sep 16, 2026
36795a5
ci: use compatible dependency installation for both hosts
Tarquinen Sep 16, 2026
6bb9381
fix: update vulnerable Browserslist dependency
Tarquinen Sep 16, 2026
4a1a9a3
test: bundle request logging with the DCP sandbox
Tarquinen Sep 16, 2026
5e894b5
docs: describe the single-checkout sandbox setup
Tarquinen Sep 16, 2026
06e1365
feat: reuse matching OpenCode authentication in the sandbox
Tarquinen Sep 16, 2026
3bacbb3
test: cover native authentication copying and provider requests
Tarquinen Sep 16, 2026
408379e
docs: describe saved OpenCode authentication in the sandbox
Tarquinen Sep 16, 2026
f875b5f
feat: use compact message and summary IDs in V2
Tarquinen Sep 20, 2026
ffc8dc9
test: verify compact IDs across host versions and transports
Tarquinen Sep 20, 2026
babfe94
feat: use latest stable OpenCode in the sandbox
Tarquinen Sep 20, 2026
8b82542
fix: support current V2 dialog themes
Tarquinen Sep 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
cache: "npm"

- name: Install dependencies
run: npm ci
run: npm ci --legacy-peer-deps

- name: Format check
run: npm run format:check
Expand All @@ -31,6 +31,9 @@ jobs:
- name: Build
run: npm run build

- name: Test
run: npm test

- name: Security audit
run: npm audit --audit-level=high
continue-on-error: false
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ jobs:

- name: Install dependencies
if: steps.package.outputs.published == 'false'
run: npm ci
run: npm ci --legacy-peer-deps

- name: Format check
if: steps.package.outputs.published == 'false'
Expand Down
179 changes: 178 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,181 @@ This arrangement ensures the project remains Open Source while providing a path
4. Ensure all tests pass and the code is formatted.
5. Submit a Pull Request.

We look forward to your contributions!
## Development Setup

Use Node.js and npm. From your checkout:

```sh
npm ci --legacy-peer-deps
npm run build
```

The install flag allows development against the different OpenTUI peer versions
used by OpenCode V1 and V2. This also installs the bundled test logger's
dependencies through the [tests/logger](tests/logger/) npm workspace.

Run the checks relevant to your changes before submitting a pull request:

```sh
npm test # DCP and request-logger tests
npm run typecheck # TypeScript validation
npm run check:package # Build and validate the npm package
npm run format:check # Formatting
```

## Compatibility

DCP provides server and terminal integrations for OpenCode V1 and V2, using shared
package entrypoints and `dcp.jsonc` settings. Exercise both hosts when changing
shared behavior.

Use [package.json](package.json) for dependency requirements and
[the lab Dockerfile](tests/lab/Dockerfile) for pinned integration-test versions.
Host-specific behavior is implemented in [index.ts](index.ts),
[tui.tsx](tui.tsx), and [lib/v2/](lib/v2/).

V2 uses `@4@` message IDs and `@b1@` summary IDs. Compression inputs include the
whole marker; range summaries use `@b1@` placeholders for nested summaries.
Message-mode priority labels look like `@4@ [high]`; `@blocked@` marks content
that cannot be selected. V1 uses XML ID tags. Custom prompt overrides must describe
the ID format of the host they run on.

## Local Installation

After building, add this checkout's absolute path to your OpenCode configuration.

For **V2**, use `opencode.json`:

```jsonc
{
"plugins": [{ "package": "/absolute/path/to/opencode-dynamic-context-pruning" }],
"permissions": [{ "action": "compress", "resource": "*", "effect": "allow" }],
}
```

For **V1**, add the following to both `opencode.json` (server plugin) and the
separate `tui.json` (panel):

```jsonc
{ "plugin": ["/absolute/path/to/opencode-dynamic-context-pruning"] }
```

## Manual Sandbox

The sandbox requires Docker, Node/npm, and saved OpenCode authentication.
The request logger is included in [tests/logger](tests/logger/); only the DCP
checkout is needed. Complete [Development Setup](#development-setup), then run:

```sh
npm run sandbox # OpenCode V2
npm run sandbox -- --v1 # OpenCode V1
```

Each launch uses the latest stable OpenCode release for the selected major version,
rebuilds DCP and the test logger, and prepares a clean Docker image. Run
`npm run sandbox -- --help` for available options and defaults. Each launch copies
all saved authentication from the matching host version: V1's `auth.json`, or V2's
credential records and account selections. OpenCode handles provider authentication
normally inside the container; copied credentials can be refreshed there without
writing back to the host.

V1's auth file is under `$XDG_DATA_HOME/opencode` (normally
`~/.local/share/opencode`). V2's database is located with `opencode2 debug paths db`,
or the standard data directory when that command is unavailable. Set `DCP_AUTH_PATH`
to select a different V1 auth file or V2 database. Credentials embedded in host
configuration or environment variables are not copied. Custom provider definitions
can be added to `opencode.json` in the sandbox's scratch workspace.

The sandbox has its own sessions, scratch workspace, and configuration under
`~/.local/state/dcp-sandbox/`. V1 uses the `v1/` subdirectory, with a separate
database. Your host project and normal OpenCode configuration are not mounted.
Try `/dcp` for the panel or `/dcp-compress` for a compression pass.

```sh
npm run sandbox -- --fresh # New profile; keep old runs
npm run sandbox -- --logs # Latest log paths and capture counts
npm run sandbox -- --path # Current profile's host directory
npm run sandbox -- -- --continue # Resume a session
npm run sandbox -- --opencode VERSION # Use an exact release for this launch
npm run sandbox -- --transport http # Select V2's transport
npm run sandbox -- --model PROVIDER/MODEL # Select a model available to your account
```

Replace `VERSION`, `PROVIDER`, and `MODEL` with the release and model you want to test.
Without a saved model choice, OpenCode selects its default. A V2 transport override
applies to the selected model.
Add `--v1` to manage the V1 sandbox. Model and transport choices persist.
An exact version override applies only to that launch; otherwise the latest stable
release is selected. `--fresh` selects a new profile for subsequent launches.
You can edit `dcp.jsonc` and CLI preferences; `opencode.json` is launcher-managed.
Set `DCP_SANDBOX_DIR` to choose another state directory.

For a shortcut on Linux, run from the checkout:

```sh
mkdir -p ~/.local/bin
ln -s "$PWD/scripts/sandbox.mjs" ~/.local/bin/dcp-sandbox
dcp-sandbox
```

### Request Logs

The logger is development-only tooling and is excluded from DCP's published npm
package. Each launch has `raw/` and `readable/` directories under its timestamped log folder.
The launcher manages the WebSocket relay and readable-log watcher. Requests appear
as they are sent; assembled responses appear when they finish, while the session
stays open. `--logs` only shows paths and capture counts.

Start at `readable/index.json`, then a session's numbered request folders:

```text
readable/<session>/0001_primary_websocket/
request.json # Pretty-printed body actually sent
response.json # Assistant content, parsed tool calls, token totals, errors
meta.json # Timing, completion, transport, raw source, continuation ID
```

V2's full pre-transport snapshots are in each session's `context/` directory.
WebSocket continuation requests remain deltas with `previous_response_id`.
Partial and failed responses are marked in metadata. Full provider metadata,
original HTTP bytes, and WebSocket frames remain available in `raw/`.

## Integration Tests

The containerized lab exercises packed plugins on V1 and V2, including saved-auth
copying, HTTP and WebSocket compression, commands, permissions, concurrent sessions,
persistence, and native compaction. It uses a local mock provider without live credentials.

After [Development Setup](#development-setup), build
[tests/lab/Dockerfile](tests/lab/Dockerfile) using the image tag expected by
[scripts/lab.mjs](scripts/lab.mjs), then run:

```sh
node scripts/lab.mjs
```

The runner prints its output directory under `/tmp/opencode/dcp-lab/`. Set
`DCP_LAB_DIR` to override it. Add `--built` to reuse an existing DCP build.
For real-provider checks, `node scripts/lab.mjs --live` uses the current build and
saved V2 authentication. Its OpenAI Responses scenarios require access to the model
configured in [tests/lab/live.mjs](tests/lab/live.mjs).

Inspect capture summaries without opening large transcripts:

```sh
node tests/lab/inspect.mjs <log-directory>
```

Terminal-panel checks require `uv` and reuse a completed lab run:

```sh
uv run --with pexpect --with pyte tests/lab/ui.py <lab-output-directory> v2
uv run --with pexpect --with pyte tests/lab/ui.py <lab-output-directory> v1
```

These check the panel, Context, Stats, persisted manual-mode toggle, and closing
the dialog. Terminal transcripts and screen snapshots are saved in the lab output.

To check another V2 release, build the lab image with `--build-arg V2=VERSION`
and pass that image's tag as the final argument to `ui.py`. Use a separate copy of
the lab output when testing different releases so their databases stay independent.
32 changes: 16 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,10 @@ opencode plugin @tarquinen/opencode-dcp@latest --global

This installs the package and adds it to your global OpenCode config.

## Project Status
## Related Project

Development on DCP has slowed because most new context-management work has moved to [Sleev](https://sleev.ai) and the `sleev` CLI. Sleev is a local proxy for Claude Code, Codex, and OpenCode that builds on DCP's core ideas with newer context-management features and will work with any harness/client.

DCP remains available for OpenCode plugin users, but new features are landing in Sleev first. If you are starting fresh, we recommend trying Sleev:
[Sleev](https://sleev.ai) is a local proxy for coding agents, including Claude Code,
Codex, and OpenCode. It provides context management through the `sleev` CLI:

```bash
npm i -g sleev
Expand Down Expand Up @@ -49,7 +48,7 @@ Identifies repeated tool calls (same tool, same arguments) and keeps only the mo

### Purge Errors

Prunes inputs from errored tool calls after a configurable number of turns (default: 4). Error messages are preserved; only the potentially large input content is removed. Recalculated on compress tool use.
Prunes inputs from errored tool calls after a configurable number of turns. Error messages are preserved; only the potentially large input content is removed. Recalculated on compress tool use.

## Configuration

Expand All @@ -62,7 +61,7 @@ DCP uses its own config file, searched in order:
Each level overrides the previous, so project settings take priority over global. Restart OpenCode after making config changes.

> [!NOTE]
> If you use models with smaller context windows, such as GitHub Copilot models or local models, lower `compress.minContextLimit` and `compress.maxContextLimit` in your configuration to match the available context.
> If your model has a smaller context window, lower `compress.minContextLimit` and `compress.maxContextLimit` in your configuration to match the available context.

> [!IMPORTANT]
> Defaults are applied automatically. Expand this if you want to review or override settings.
Expand Down Expand Up @@ -138,14 +137,12 @@ Each level overrides the previous, so project settings take priority over global
// Accepts: number or "X%".
// Example:
// "modelMaxLimits": {
// "openai/gpt-5.3-codex": 120000,
// "anthropic/claude-sonnet-4.6": "80%"
// "provider/model": "80%"
// },
// Optional per-model override for minContextLimit.
// If present, this wins over the global minContextLimit.
// "modelMinLimits": {
// "openai/gpt-5.3-codex": 50000,
// "anthropic/claude-sonnet-4.6": "25%"
// "provider/model": "25%"
// },
// How often the context-limit nudge fires (1 = every fetch, 5 = every 5th)
"nudgeFrequency": 5,
Expand Down Expand Up @@ -194,7 +191,7 @@ DCP provides a TUI panel and one prompt-producing slash command:

### Prompt Overrides

DCP exposes six editable prompts:
DCP exposes the following editable prompts:

- `system`
- `compress-range`
Expand Down Expand Up @@ -224,15 +221,18 @@ For the `compress` tool, `compress.protectedTools` ensures specific tool outputs

LLM providers cache prompts based on exact prefix matching. When DCP prunes content, it changes messages, which invalidates cached prefixes from that point forward.

**Trade-off:** You lose some cache reads but gain token savings from reduced context size and fewer hallucinations from stale context. In most cases, especially in long sessions, the savings outweigh the cache miss cost.

> [!NOTE]
> In testing, cache hit rates were approximately 85% with DCP vs 90% without.
**Trade-off:** Pruning reduces context size but can increase cache misses. The cost
balance depends on your conversation, compression frequency, and provider pricing.

**No impact for:**

- **Request-based billing** — Some providers charge per request, not tokens.
- **Uniform token pricing** — Providers like Cerebras that bill cached and uncached tokens at the same rate.
- **Uniform token pricing** — Providers that bill cached and uncached tokens at the same rate.

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, local installation,
and testing with the V1/V2 sandbox.

## License

Expand Down
3 changes: 2 additions & 1 deletion index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from "./lib/hooks"
import { configureClientAuth, isSecureMode } from "./lib/auth"
import { startAutoUpdate } from "./lib/update"
import { setup } from "./lib/v2"

const server: Plugin = (async (ctx) => {
const config = getConfig(ctx)
Expand Down Expand Up @@ -134,4 +135,4 @@ const server: Plugin = (async (ctx) => {
}
}) satisfies Plugin

export default server
export default { id: "opencode-dcp", setup, server }
2 changes: 1 addition & 1 deletion lib/commands/decompress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export interface DecompressCommandContext {

function parseBlockIdArg(arg: string): number | null {
const normalized = arg.trim().toLowerCase()
const blockRef = parseBlockRef(normalized)
const blockRef = parseBlockRef(normalized) ?? parseBlockRef(normalized, "compact")
if (blockRef !== null) {
return blockRef
}
Expand Down
2 changes: 1 addition & 1 deletion lib/commands/recompress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export interface RecompressCommandContext {

function parseBlockIdArg(arg: string): number | null {
const normalized = arg.trim().toLowerCase()
const blockRef = parseBlockRef(normalized)
const blockRef = parseBlockRef(normalized) ?? parseBlockRef(normalized, "compact")
if (blockRef !== null) {
return blockRef
}
Expand Down
36 changes: 17 additions & 19 deletions lib/commands/sweep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,7 @@ import { isIgnoredUserMessage } from "../messages/query"
import { buildToolIdList } from "../messages/utils"
import { saveSessionState } from "../state/persistence"
import { isMessageCompacted } from "../state/utils"
import {
getFilePathsFromParameters,
isFilePathProtected,
isToolNameProtected,
} from "../protected-patterns"
import { isToolProtected } from "../protected-patterns"
import { syncToolCache } from "../state/tool-cache"

export interface SweepCommandContext {
Expand Down Expand Up @@ -176,15 +172,18 @@ export async function handleSweepCommand(ctx: SweepCommandContext): Promise<void
if (!entry) {
return true
}
if (isToolNameProtected(entry.tool, protectedTools)) {
if (
isToolProtected(
entry.tool,
entry.parameters,
protectedTools,
config.protectedFilePatterns,
entry.metadata,
)
) {
logger.debug(`Sweep: skipping protected tool ${entry.tool} (${id})`)
return false
}
const filePaths = getFilePathsFromParameters(entry.tool, entry.parameters)
if (isFilePathProtected(filePaths, config.protectedFilePatterns)) {
logger.debug(`Sweep: skipping protected file path(s) ${filePaths.join(", ")} (${id})`)
return false
}
return true
})

Expand All @@ -194,14 +193,13 @@ export async function handleSweepCommand(ctx: SweepCommandContext): Promise<void
if (!entry) {
return false
}
if (isToolNameProtected(entry.tool, protectedTools)) {
return true
}
const filePaths = getFilePathsFromParameters(entry.tool, entry.parameters)
if (isFilePathProtected(filePaths, config.protectedFilePatterns)) {
return true
}
return false
return isToolProtected(
entry.tool,
entry.parameters,
protectedTools,
config.protectedFilePatterns,
entry.metadata,
)
}).length

if (newToolIds.length === 0) {
Expand Down
Loading
Loading