Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ jobs:
- name: Test recovery MCP product
run: pnpm --dir components/products/mcp test

- name: Test OpenCode adapter
run: pnpm --dir components/adapters/opencode test

- name: Test lightrsi CLI
run: pnpm --dir components/products/cli test

Expand Down
43 changes: 26 additions & 17 deletions components/adapters/HOSTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ TokenPilot is now structured as a reusable LightRSI component with multiple host
| `Codex CLI` | available | hooks + local Responses proxy + shared CLI | `npm --prefix components/adapters/codex run build` then `npm --prefix components/adapters/codex run install:codex` | [codex/README.md](./codex/README.md) |
| `Claude Code` | available | gateway routing + observability hooks + shared CLI | `npm --prefix components/adapters/claude-code run build` then `npm --prefix components/adapters/claude-code run install:claude-code` | [claude-code/README.md](./claude-code/README.md) |
| `DeepSeek Harness` | available | Cordis plugin + durable projection | `pnpm --filter @lightrsi/deepseek-harness-adapter compatibility:smoke -- --dsh-checkout=/absolute/path/to/deepseek-harness` | [GUA-06](../../docs/acceptance/GUA-06.md) |
| `OpenCode` | available | in-process v1 plugin + recovery MCP + shared CLI | `npm --prefix components/adapters/opencode run build` then `npm --prefix components/adapters/opencode run install:opencode` | [opencode/README.md](./opencode/README.md) |

Each full TokenPilot host adapter binds the versioned preset explicitly. OpenClaw and Codex declare Stabilizer, Reduction, and Eviction; Claude Code currently declares Stabilizer and Reduction. DeepSeek Harness is a separate compatibility adapter with a narrower projection surface. The same adapter-owned product registrations are used by the shared CLI and browser Visual surface where applicable.

Expand All @@ -21,22 +22,22 @@ Legend:
- `partial`: available, but intentionally narrower than the OpenClaw path
- `no`: not supported in the current public adapter

| Capability | OpenClaw | Codex CLI | Claude Code |
| :-- | :--: | :--: | :--: |
| Stable-prefix rewriting | yes | yes | yes |
| Before-call reduction | yes | yes | yes |
| Real MCP-backed `memory_fault_recover` | yes | yes | yes |
| Standalone `lightrsi <host> ...` CLI | yes | yes | yes |
| `status` / `doctor` / `report` | yes | yes | yes |
| `visual` | yes | yes | yes |
| `mode conservative` / `mode normal` | yes | yes | yes |
| `mode aggressive` | yes | no | no |
| Estimator-driven lifecycle eviction runtime | yes | yes | yes |
| Lifecycle eviction controls | yes | no | no |
| In-host slash commands | yes | no | no |
| Hook-based observability | partial | yes | yes |
| Local proxy / gateway runtime | yes | yes | yes |
| Session-state / ux-effects persistence | yes | yes | yes |
| Capability | OpenClaw | Codex CLI | Claude Code | OpenCode |
| :-- | :--: | :--: | :--: | :--: |
| Stable-prefix rewriting | yes | yes | yes | partial (`developer` target only) |
| Before-call reduction | yes | yes | yes | yes |
| Real MCP-backed `memory_fault_recover` | yes | yes | yes | yes |
| Standalone `lightrsi <host> ...` CLI | yes | yes | yes | yes |
| `status` / `doctor` / `report` | yes | yes | yes | yes |
| `visual` | yes | yes | yes | yes |
| `mode conservative` / `mode normal` | yes | yes | yes | yes |
| `mode aggressive` | yes | no | no | no |
| Estimator-driven lifecycle eviction runtime | yes | yes | yes | yes (durable request overlay) |
| Lifecycle eviction controls | yes | no | no | `on` / `off` / `minBlockChars` |
| In-host slash commands | yes | no | no | no |
| Hook-based observability | partial | yes | yes | in-process |
| Local proxy / gateway runtime | yes | yes | yes | not needed (in-process) |
| Session-state / ux-effects persistence | yes | yes | yes | yes |

## Host Notes

Expand Down Expand Up @@ -69,10 +70,18 @@ Legend:
- supports TokenPilot status projection and compatibility smoke verification
- canonical surface eviction remains opt-in and is guarded by estimator, registry, safety, and revision checks

### OpenCode

- in-process v1 plugin (verified on OpenCode 1.18.33) plus the shared recovery MCP server in `opencode.json`
- stable prefix in `experimental.chat.system.transform`; reduction and opt-in eviction (durable request overlay) in `experimental.chat.messages.transform`, which runs before every model step
- `dynamicContextTarget=user` is not available (messages are transformed before the system prompt in 1.18.33)
- never changes OpenCode's native `compaction.prune`; `doctor` reports it
- design note: [docs/adapters/opencode-design.md](../../docs/adapters/opencode-design.md)

### Shared Visual Surface

- `lightrsi visual` now provides a standalone browser visual entrypoint
- the shared visual can switch between `openclaw`, `codex`, and `claude-code` hosts
- the shared visual can switch between `openclaw`, `codex`, `claude-code`, and `opencode` hosts
- today, the browser visual is backed by snapshot data; OpenClaw still has the richest dataset, while Codex and Claude Code now route their `visual` commands into the shared browser surface

## Boundary
Expand Down
5 changes: 5 additions & 0 deletions components/adapters/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ Adapter inventory:
- adapter for Claude Code
- `deepseek-harness/`
- Cordis adapter and compatibility smoke path for DeepSeek Harness
- `opencode/`
- in-process v1 plugin adapter for OpenCode
- `shared/canonical/`
- host glue for in-process transcript adapters (canonical reduction, stabilizer application, recovery protocol, eviction surface, fail-open logging)
- future adapters
- other host-specific integrations

Expand Down Expand Up @@ -126,3 +130,4 @@ This keeps the first working version small and makes boundary mistakes easier to
- [codex/README.md](./codex/README.md)
- [claude-code/README.md](./claude-code/README.md)
- [deepseek-harness/](./deepseek-harness/)
- [opencode/README.md](./opencode/README.md)
134 changes: 134 additions & 0 deletions components/adapters/opencode/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# TokenPilot OpenCode Adapter

This package integrates TokenPilot with [OpenCode](https://opencode.ai) (verified on **1.18.33**) as an in-process v1 plugin, plus the shared recovery MCP server registered in OpenCode's own config.

This adapter explicitly binds the TokenPilot `stabilizer`, `reduction`, and `eviction` features. Its product registration provides OpenCode state discovery to the shared CLI and Visual surface.

Design, host facts and gaps: [`docs/adapters/opencode-design.md`](../../../docs/adapters/opencode-design.md).

## Supports

| Feature | OpenCode hook | Notes |
| :-- | :-- | :-- |
| Stable prefix | `experimental.chat.system.transform` | Shared stabilizer: volatile lines (e.g. `Today's date: …`) move to the tail of the system prompt. |
| Reduction | `experimental.chat.messages.transform` | Runs before every model step (verified in 1.18.33). Request-time, over the whole history. Stored message parts keep the originals. |
| Recovery | shared recovery MCP server | Registered as `mcp.tokenpilot_memory_fault_recover`. OpenCode shows the tool as `tokenpilot_memory_fault_recover_memory_fault_recover`. The recovery protocol is appended to the system prompt. |
| Eviction (opt-in, off by default) | `experimental.chat.messages.transform` + durable overlay | OpenCode has no persistent part-edit hook for plugins, so evictions are stored per session and re-applied identically on every request (Claude Code's overlay approach). Estimator failures never disable reduction. |
| Modes | | `conservative`, `normal` (default). `aggressive` is not exposed. |

Current limitations:

- `hooks.dynamicContextTarget=user` runs as `developer`: in 1.18.33, `messages.transform` runs before `system.transform`, so volatile lines cannot be moved into the same request's first user message. `doctor` reports this.
- the v2 plugin API is not used: in 1.18.33 it has no message, tool or session hooks
- no in-host slash commands; use the shared `lightrsi opencode ...` CLI
- the Context Cleaner is not integrated yet (see the design note)

### Native tool-output pruning

OpenCode can prune old tool outputs itself (`compaction.prune`: keeps roughly the last 40k tokens of tool output and clears older ones to `[Old tool result content cleared]`). It is **off by default in 1.18.33**. TokenPilot never changes this setting. `lightrsi opencode doctor` reports it as `native tool-output pruning: on|off`.

- Off: TokenPilot reduction is the only thing trimming old outputs, apart from OpenCode's own truncation at 2000 lines / 50 KiB (`tool_output.max_lines|max_bytes`).
- On: pruned outputs are skipped by TokenPilot, since they are below the reduction thresholds and are never evicted. Pruning rewrites earlier history persistently, which moves the cached-prefix boundary; that trade-off is OpenCode's and yours to choose.

## Install

```bash
pnpm install
pnpm --filter @lightrsi/mcp build # recovery MCP server
npm --prefix components/adapters/opencode run build
npm --prefix components/products/cli run build # for the lightrsi CLI
npm --prefix components/adapters/opencode run install:opencode
```

Install writes, in OpenCode's global config dir (`$XDG_CONFIG_HOME/opencode`, default `~/.config/opencode`):

- `plugins/tokenpilot.js`: a marker-tagged ESM loader re-exporting `components/adapters/opencode/dist/plugin.mjs`. OpenCode auto-loads `plugins/*.js`. A foreign file at that path is moved to `tokenpilot.js.bak-<timestamp>` first.
- `opencode.json`: adds exactly one key, `mcp.tokenpilot_memory_fault_recover`, after copying the file to `opencode.json.tokenpilot-backup-<timestamp>`. The file is created if it does not exist.
- `tokenpilot.json`: runtime config in `normal` mode, created only if missing and never containing credentials.
- `tokenpilot-install.json`: install manifest used by uninstall.

If you only have an `opencode.jsonc`, or your `opencode.json` is not plain JSON, install **does not rewrite it**. It prints the MCP snippet for you to paste, so your comments are never lost. `doctor` flags the missing entry.

Uninstall removes the loader and exactly that MCP key (and an `opencode.json` it created, once nothing else is in it):

```bash
npm --prefix components/adapters/opencode run uninstall:opencode
npm --prefix components/adapters/opencode run uninstall:opencode -- --purge # also created config + state
```

`TOKENPILOT_OPENCODE_CONFIG_DIR` and `TOKENPILOT_OPENCODE_CONFIG` override the locations.

## Verify

1. Restart OpenCode so the plugin and the MCP server load.
2. `lightrsi opencode doctor`: the loader, bundle and MCP entry should be present, and the MCP probe should be `ok`.
3. After a few turns: `lightrsi opencode report`

## Commands

```bash
lightrsi opencode status
lightrsi opencode report
lightrsi opencode doctor
lightrsi opencode visual
lightrsi opencode mode conservative|normal
lightrsi opencode stabilizer on|off
lightrsi opencode reduction on|off
lightrsi opencode reduction pass <name> on|off
lightrsi opencode eviction status|on|off
lightrsi opencode eviction set minBlockChars <number>
```

## Configuration

`~/.config/opencode/tokenpilot.json` uses the same keys and defaults as the Claude Code adapter. Config edits, including `lightrsi opencode mode ...`, apply on the next request without restarting OpenCode.

| Key | Default | Notes |
| :-- | :-- | :-- |
| `enabled` | `true` | Master switch; `false` makes every hook a no-op. |
| `modules.stabilizer` | `true` | |
| `hooks.dynamicContextTarget` | `developer` | `user` is not available on OpenCode 1.18.33 (see Supports above); it runs as `developer`. |
| `modules.reduction` | `true` | |
| `reduction.triggerMinChars` / `maxToolChars` | `2200` / `1200` | `conservative`: `4000` / `1800`. |
| `reduction.passes.*` | all `true` | |
| `modules.eviction` + `eviction.enabled` | `false` | Both must be on, plus the estimator settings below. |
| `eviction.minBlockChars` | `4000` | |
| `taskStateEstimator.baseUrl` / `model` / `apiKey` | unset | OpenAI-compatible endpoint for the task-state estimator. Keep the key out of checked-in files. |

Enabling eviction:

```bash
lightrsi opencode eviction on
# then set taskStateEstimator.baseUrl / model / apiKey in ~/.config/opencode/tokenpilot.json
lightrsi opencode doctor # "eviction: active" once complete
```

## Runtime Files

```text
~/.config/opencode/tokenpilot-state/tokenpilot/
```

- `tokenpilot/adapter.log`: fail-open reasons. OpenCode does not catch plugin errors, so every hook catches its own and continues unmodified.
- `tokenpilot/tool-result-archives/<session>/`: recovery archives
- `tokenpilot/reduction-memo/<session>.json`: which read disclosed which file, so a restarted OpenCode (`opencode run --continue`) still honours deliberate re-reads
- `tokenpilot/eviction-overlay/<session>.json`: durable eviction decisions (when eviction is on)
- `session-state/…`, `ux-effects/…`: report and visual data

## Debugging

- `lightrsi opencode doctor`
- `tail ~/.config/opencode/tokenpilot-state/tokenpilot/tokenpilot/adapter.log`
- `opencode run --print-logs --log-level DEBUG "…"` shows plugin and MCP loading.
- `opencode run --pure "…"` runs without external plugins, to compare with TokenPilot off.

## Package Scripts

```bash
npm --prefix components/adapters/opencode run build
npm --prefix components/adapters/opencode run typecheck
npm --prefix components/adapters/opencode test
npm --prefix components/adapters/opencode run install:opencode
npm --prefix components/adapters/opencode run uninstall:opencode
npm --prefix components/adapters/opencode run doctor:opencode
```
51 changes: 51 additions & 0 deletions components/adapters/opencode/build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { build } from "esbuild";
import { join } from "node:path";

const shared = {
bundle: true,
// ../shared/canonical has no node_modules of its own; resolve workspace deps from here.
nodePaths: [join(__dirname, "node_modules")],
platform: "node" as const,
target: "node20",
sourcemap: true,
minify: false,
logLevel: "info" as const,
logOverride: { "empty-import-meta": "silent" as const },
};

async function main() {
// OpenCode (Bun) imports plugins as ES modules. Bundled CommonJS dependencies
// still need `require`, `__filename` and `__dirname`, so provide them.
await build({
...shared,
entryPoints: { plugin: "src/plugin.ts" },
outdir: "dist",
format: "esm",
outExtension: { ".js": ".mjs" },
banner: {
js: [
'import { createRequire as __tpCreateRequire } from "node:module";',
'import { fileURLToPath as __tpFileURLToPath } from "node:url";',
'import { dirname as __tpDirname } from "node:path";',
"const require = __tpCreateRequire(import.meta.url);",
"const __filename = __tpFileURLToPath(import.meta.url);",
"const __dirname = __tpDirname(__filename);",
].join("\n"),
},
});
await build({
...shared,
entryPoints: {
"install-opencode": "scripts/install-opencode.ts",
"uninstall-opencode": "scripts/uninstall-opencode.ts",
"doctor-opencode": "scripts/doctor-opencode.ts",
},
outdir: "dist",
format: "cjs",
});
}

main().catch((err) => {
console.error(err);
process.exit(1);
});
44 changes: 44 additions & 0 deletions components/adapters/opencode/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "@lightrsi/opencode-adapter",
"version": "0.1.0-beta.1",
"description": "TokenPilot adapter for OpenCode (in-process v1 plugin + recovery MCP).",
"lightrsi": {
"role": "adapter"
},
"main": "dist/plugin.js",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/zjunlp/LightRSI.git",
"directory": "components/adapters/opencode"
},
"engines": {
"node": ">=20.0.0"
},
"dependencies": {
"@lightrsi/artifact-store": "workspace:*",
"@lightrsi/eviction": "workspace:*",
"@lightrsi/history": "workspace:*",
"@lightrsi/host-adapter": "workspace:*",
"@lightrsi/kernel": "workspace:*",
"@lightrsi/mcp": "workspace:*",
"@lightrsi/product-surface": "workspace:*",
"@lightrsi/reduction": "workspace:*",
"@lightrsi/stabilizer": "workspace:*",
"@lightrsi/tokenpilot": "workspace:*"
},
"scripts": {
"build": "tsx build.ts",
"doctor:opencode": "node --import tsx scripts/doctor-opencode.ts",
"install:opencode": "node --import tsx scripts/install-opencode.ts",
"uninstall:opencode": "node --import tsx scripts/uninstall-opencode.ts",
"test": "node --import tsx --test tests/*.test.ts",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"devDependencies": {
"esbuild": "^0.25.10",
"tsx": "^4.21.0",
"typescript": "^5.9.3"
},
"keywords": ["opencode", "tokenpilot", "context-management", "plugin"]
}
16 changes: 16 additions & 0 deletions components/adapters/opencode/scripts/doctor-opencode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env node
import { defaultTokenPilotOpenCodeConfigPath, loadTokenPilotOpenCodeConfig } from "../src/config.js";
import { formatOpenCodeDoctorReport, inspectOpenCodeDoctor } from "../src/doctor.js";

async function main() {
const configPath = process.env.TOKENPILOT_OPENCODE_CONFIG ?? defaultTokenPilotOpenCodeConfigPath();
const config = await loadTokenPilotOpenCodeConfig(configPath);
const report = await inspectOpenCodeDoctor({ config, configPath, configDir: process.env.TOKENPILOT_OPENCODE_CONFIG_DIR });
console.log(formatOpenCodeDoctorReport(report));
if (!report.healthy) process.exitCode = 1;
}

main().catch((error) => {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
});
33 changes: 33 additions & 0 deletions components/adapters/opencode/scripts/install-opencode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/usr/bin/env node
import { installOpenCodeTokenPilot } from "../src/install.js";

async function main() {
const result = await installOpenCodeTokenPilot({
configDir: process.env.TOKENPILOT_OPENCODE_CONFIG_DIR,
tokenPilotConfigPath: process.env.TOKENPILOT_OPENCODE_CONFIG,
});
const lines = [
"TokenPilot OpenCode install complete:",
`- plugin loader: ${result.paths.loaderPath}`,
`- plugin bundle: ${result.bundlePath}`,
...(result.loaderBackupPath ? [`- previous loader backed up to: ${result.loaderBackupPath}`] : []),
`- tokenpilot config: ${result.paths.tokenPilotConfigPath} (${result.tokenPilotConfigCreated ? "created, normal mode" : "kept existing"})`,
`- state dir: ${result.stateDir}`,
`- recovery MCP: ${result.mcp.registered ? `${result.mcp.reason} in ${result.paths.opencodeJsonPath}` : `NOT registered (${result.mcp.reason})`}`,
...(result.mcp.backupPath ? [`- opencode.json backup: ${result.mcp.backupPath}`] : []),
...(!result.mcp.serverBuilt ? ["- recovery MCP server is not built yet: run `pnpm --filter @lightrsi/mcp build`"] : []),
...(!result.mcp.registered ? ["- add this to your OpenCode config manually:", result.mcp.snippet] : []),
result.cliBin?.installed
? `- lightrsi CLI bin: installed at ${result.cliBin.launcherPath ?? result.cliBin.binPath}`
: "- lightrsi CLI bin: skipped (build components/products/cli first)",
...(result.cliBin && !result.cliBin.binDirOnPath ? [`- lightrsi CLI PATH note: add ${result.cliBin.binDir} to PATH if 'lightrsi' is unavailable.`] : []),
"- next step: restart OpenCode so the plugin and MCP server load",
"- verify: lightrsi opencode doctor",
];
console.log(lines.join("\n"));
}

main().catch((error) => {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
});
Loading
Loading