Skip to content
Open

V2 #617

Show file tree
Hide file tree
Changes from all commits
Commits
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
168 changes: 167 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,170 @@ 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/).

## 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 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 -- --update # Remember the latest release
npm run sandbox -- --opencode VERSION # Pin a release to test
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, transport, and version choices persist;
updates are explicit. `--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.
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 }
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