Skip to content
Open
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
49 changes: 35 additions & 14 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,24 @@ Tests verify:
### File Structure
```
opencode-working-memory/
├── index.ts # Plugin entry point (exports PluginModule)
├── index.ts # Dual V1/V2 server entry point
├── tui.ts # TUI entry point discovered beside index.ts
├── src/
│ ├── plugin.ts # Main plugin implementation
│ ├── runtime.ts # Host-agnostic memory logic
│ ├── plugin.ts # V1 adapter
│ ├── plugin-v2.ts # V2 adapter
│ ├── tui-plugin.ts # V2 TUI + V1 TUI dual export
│ ├── tui-plugin-v1.ts # Legacy V1 TUI implementation
│ ├── extractors.ts # Memory extraction logic
│ ├── workspace-memory.ts # Workspace memory management
│ ├── session-state.ts # Session state tracking
│ ├── storage.ts # File storage utilities
│ ├── paths.ts # Path utilities
│ ├── opencode.ts # OpenCode SDK types
│ ├── opencode.ts # V1 OpenCode SDK helpers
│ └── types.ts # Type definitions
├── tests/
│ ├── plugin.test.ts # Plugin hook tests
│ ├── plugin.test.ts # V1 adapter and runtime hook tests
│ ├── plugin-v2.test.ts # V2 adapter and TUI tests
│ ├── extractors.test.ts # Extractor tests
│ └── workspace-memory.test.ts # Workspace memory tests
├── package.json # Plugin manifest
Expand Down Expand Up @@ -221,16 +227,26 @@ const canonical = normalizeText(text); // Lowercase, strip punctuation, collaps

### Plugin Entry Point

`index.ts` exposes both host adapters from one default export:

```typescript
// index.ts
import { WorkingMemoryPlugin } from "./src/plugin-v2.ts";
import { MemoryV2Plugin } from "./src/plugin.ts";

export default {
id: "working-memory",
server: MemoryV2Plugin,
...WorkingMemoryPlugin, // V2: { id, setup }
server: MemoryV2Plugin, // V1: server(input, options)
};
```

Shared behavior lives in `src/runtime.ts`. The adapters only translate host
payloads:

- `src/plugin.ts` — V1 string hooks and `client.session.*` calls.
- `src/plugin-v2.ts` — `ctx.session.hook`, `ctx.tool.hook`, `ctx.event.subscribe`.
- `src/tui-plugin.ts` — V2 keymap/slash command plus the V1 TUI export.

### Workspace Memory Files

- **Location**: `~/.local/share/opencode-working-memory/workspaces/{workspaceKey}/workspace-memory.json`
Expand Down Expand Up @@ -261,25 +277,30 @@ export default {

## Plugin Hooks

### `experimental.chat.system.transform`
### `experimental.chat.system.transform` (V1) / `session.hook("context")` (V2)

Injects workspace memory and hot session state into system prompt.
Injects workspace memory and hot session state into the system prompt. V2 pushes
one `{ type: "text", text }` part per memory block.

### `tool.execute.after`
### `tool.execute.after` (V1) / `tool.hook("execute.after")` (V2)

- Tracks active files (read, grep, edit, write actions)
- Tracks open errors from failed commands
- Clears errors when commands succeed
- Ignores `exitCode === undefined`
- V2 reads `metadata.exit` and also records `todowrite` snapshots

### `experimental.session.compacting`
### `experimental.session.compacting` (V1) / `session.hook("compaction")` (V2)

Extracts workspace memory candidates from conversation, applies quality gate and deduplication.
V1 replaces the compaction prompt with a `---`-free template. V2 keeps
OpenCode's own compaction prompt and appends the memory rules and numbered refs
as a system part.

### `event`
### `event` (V1) / `ctx.event.subscribe()` (V2)

- `session.compacted`: Promote session decisions to workspace memory
- `session.deleted`: Clean up session state files
- `session.compacted` (V1) / `session.compaction.ended` (V2): parse the summary
and promote memory candidates to workspace memory
- `session.deleted`: promote owned pending memories, then clean up session state

## Debugging & Testing

Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.7.0] - 2026-10-21

### Added

- Added an OpenCode V2 plugin adapter (`src/plugin-v2.ts`) using `Plugin.define`, `ctx.session.hook("context"|"compaction")`, `ctx.tool.hook("execute.after")`, and `ctx.event.subscribe()`.
- Added a V2 TUI plugin that registers the `/memory` slash command through `ctx.keymap.layer` and native dialogs.
- Added `tui.ts` beside `index.ts` so V2 discovers the TUI entrypoint for local directory plugins; published packages expose it through `exports["./tui"]`.
- Added `tests/plugin-v2.test.ts` covering hook registration, system-part injection, shell error tracking, `todowrite` snapshots, compaction summaries, session cleanup, and the TUI command.

### Changed

- Extracted host-agnostic memory behavior into `src/runtime.ts`; `src/plugin.ts` is now the V1 adapter.
- `index.ts` exports both shapes (`setup` for V2, `server` for V1) so one package works on both hosts.
- V2 keeps OpenCode's own compaction prompt and appends memory rules and numbered refs as a system part instead of replacing the prompt.
- V2 records todo snapshots from the `todowrite` tool because V2 has no session todo read API.
- Workspace memory and session state paths are unchanged, so existing memory survives the upgrade.

## [1.6.7] - 2026-05-31

### Fixed
Expand Down
38 changes: 31 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,33 @@ compaction ─────► reinforce / replace ──► selective prompt con

## Installation

New users: add OpenCode Working Memory to both OpenCode plugin configs.
### OpenCode V2 (recommended)

Add the package to the `plugins` array in `opencode.json`:

```json
{
"$schema": "https://opencode.ai/config.json",
"plugins": ["opencode-working-memory"]
}
```

V2 discovers the CLI/TUI entrypoint from the package's `./tui` export automatically, so there is no separate `tui.json` step. Restart OpenCode after changing the config.

To run a local checkout, point at the plugin directory (V2 requires a directory, not an `index.ts` file):

```json
{
"$schema": "https://opencode.ai/config.json",
"plugins": ["/home/mirro/coding/opencode-working-memory"]
}
```

Local directory plugins keep the server and TUI entrypoints side by side: V2 discovers `tui.ts` next to `index.ts`. The published package exposes the same module through `exports["./tui"]`.

### OpenCode V1

V1 keeps the original two-file setup:

`.opencode/opencode.json`:

Expand All @@ -62,13 +88,11 @@ New users: add OpenCode Working Memory to both OpenCode plugin configs.
}
```

Existing users: keep your current `.opencode/opencode.json` config and add only the `.opencode/tui.json` block above to enable the native `/memory` TUI menu.

Then restart OpenCode. Memory activates automatically, and `/memory` appears in the TUI slash command menu.
Both adapters share the same on-disk memory format, so switching between V1 and V2 keeps existing workspace memory.

### Strict OpenAI-Compatible Providers
### Strict OpenAI-Compatible Providers (V1 only)

Some providers, including GWDG/Qwen endpoints, reject requests with multiple system messages. If your provider returns `System message must be at the beginning`, enable merged system prompts in `.opencode/opencode.json`:
Some V1 providers, including GWDG/Qwen endpoints, reject requests with multiple system messages. If your provider returns `System message must be at the beginning`, enable merged system prompts in `.opencode/opencode.json`:

```json
{
Expand All @@ -77,7 +101,7 @@ Some providers, including GWDG/Qwen endpoints, reject requests with multiple sys
}
```

This keeps memory context available by appending workspace memory and hot session state into the first system message. Leave this option off for providers that already accept multiple system messages; the default separated prompts are more cache-friendly.
This keeps memory context available by appending workspace memory and hot session state into the first system message. Leave this option off for providers that already accept multiple system messages; the default separated prompts are more cache-friendly. V2 injects memory as separate system parts and ignores this option.

Fallback for older OpenCode/plugin hosts that do not pass plugin tuple options:

Expand Down
21 changes: 14 additions & 7 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,27 +286,34 @@ Delegate task tracking to OpenCode's native features.

## Plugin Hooks

OpenCode Working Memory hooks into OpenCode lifecycle events:
OpenCode Working Memory hooks into OpenCode lifecycle events. The shared
behavior lives in `src/runtime.ts`; `src/plugin.ts` and `src/plugin-v2.ts` are
thin host adapters.

### `experimental.chat.system.transform`
### `experimental.chat.system.transform` (V1) / `session.hook("context")` (V2)

Injects cached frozen workspace memory and cached frozen hot snapshot prompts into the system prompt. Normal tool/user churn updates storage but does not mutate these pre-history prompts until a new epoch starts.
Injects cached frozen workspace memory and cached frozen hot snapshot prompts into the system prompt. Normal tool/user churn updates storage but does not mutate these pre-history prompts until a new epoch starts. V2 pushes each block as its own text system part.

### `tool.execute.after`
### `tool.execute.after` (V1) / `tool.hook("execute.after")` (V2)

- Tracks active files (read, grep, edit, write actions)
- Tracks open errors from failed commands
- Clears errors when commands succeed
- Ignores `exitCode === undefined` (successful commands without explicit exit codes)
- V2 reads the shell tool's `metadata.exit` and records `todowrite` snapshots in session state

### `experimental.session.compacting`
### `experimental.session.compacting` (V1) / `session.hook("compaction")` (V2)

Extracts workspace memory candidates from conversation.
Applies quality gate, redaction, migration, consolidation accounting, deduplication, and source priority.

### `event` (session.compacted, session.deleted)
V1 replaces the compaction prompt so it can remove `---` separators. V2 keeps
OpenCode's own compaction prompt and appends the memory rules and numbered
`[M#]` refs as a system part.

- `session.compacted`: Promote session decisions to workspace memory
### `event` (V1) / `ctx.event.subscribe()` (V2)

- `session.compacted` (V1) / `session.compaction.ended` (V2): Promote session decisions to workspace memory
- `session.deleted`: Clean up session state files

Promotion uses accounting results from workspace memory normalization. Pending memories that are kept are promoted; duplicate memories are absorbed; exact decision replacements can be superseded; over-capacity compaction memories are rejected. Stale-marked memories are not hard-pruned by age; they lose rendered space through retention strength and cap competition.
Expand Down
54 changes: 45 additions & 9 deletions docs/installation.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,57 @@
# Installation Guide

## Quick Install
## OpenCode V2

Add to your `~/.config/opencode/opencode.json`:
Add to your `~/.config/opencode/opencode.json` (or project `opencode.json`):

```json
{
"plugin": ["opencode-working-memory"]
"$schema": "https://opencode.ai/config.json",
"plugins": ["opencode-working-memory"]
}
```

For a local checkout, use the plugin directory:

```json
{
"plugins": ["/home/mirro/coding/opencode-working-memory"]
}
```

V2 requires a directory, not an `index.ts` file. The package's `./tui` export is
loaded automatically, so no separate `tui.json` is needed.

Restart OpenCode. OpenCode Working Memory activates automatically — no manual setup needed.

> **Note**: The correct key is `plugin` (singular), not `plugins`.
## OpenCode V1

Add to `~/.config/opencode/opencode.json`:

```json
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-working-memory"]
}
```

For the `/memory` TUI menu, also add the package to `~/.config/opencode/tui.json`:

```json
{
"$schema": "https://opencode.ai/tui.json",
"plugin": ["opencode-working-memory"]
}
```

> **Note**: V1 uses `plugin` (singular); V2 uses `plugins` (plural).

## For LLM Agents

1. Edit `~/.config/opencode/opencode.json`
2. Add `"opencode-working-memory"` to the `plugin` array
2. On V2, add the package or local directory to the `plugins` array; on V1, add it to `plugin`
3. Verify the config file was saved correctly
4. Restart OpenCode (V2 requires a server restart after adding a local plugin)

## Verification

Expand Down Expand Up @@ -59,12 +92,13 @@ Tracks current session:

### Plugin Not Loading

**Symptom**: No memory context in system prompt
**Symptom**: No memory context in system prompt, or `1 plugin failed`

**Solution**:
1. Check `~/.config/opencode/opencode.json` uses `"plugin"` (not `"plugins"`)
1. Check the config key for your host: V2 uses `"plugins"` with a directory path; V1 uses `"plugin"` with the package name
2. Restart OpenCode to trigger automatic installation
3. Check OpenCode logs for any download errors
3. Check OpenCode logs for plugin load errors (`~/.local/share/opencode/log/opencode.log`)
4. V2: confirm the plugin appears as `working-memory` with `state.status: active` in `opencode api get /api/plugin`

### Memory Files Not Created

Expand Down Expand Up @@ -95,7 +129,9 @@ Tracks current session:

## Uninstallation

Remove `"opencode-working-memory"` from the `plugin` array in `~/.config/opencode/opencode.json`.
V2: remove `"opencode-working-memory"` (or the local directory) from the `plugins` array in `~/.config/opencode/opencode.json`.

V1: remove `"opencode-working-memory"` from the `plugin` arrays in `opencode.json` and `tui.json`.

Memory files in `~/.local/share/opencode-working-memory/` persist unless manually deleted.

Expand Down
11 changes: 9 additions & 2 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { WorkingMemoryPlugin } from "./src/plugin-v2.ts";
import { MemoryV2Plugin } from "./src/plugin.ts";

/**
* Dual entrypoint:
* - OpenCode V2 calls `setup(ctx)`.
* - OpenCode V1 (>= 1.18.29) calls `server(input, options)`.
* - Older V1 releases can import the `./server` export directly.
*/
export default {
id: "working-memory",
...WorkingMemoryPlugin,
server: MemoryV2Plugin,
};
};
14 changes: 12 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "opencode-working-memory",
"version": "1.6.9",
"version": "1.7.0",
"description": "Three-layer memory architecture for OpenCode with workspace memory and hot session state",
"type": "module",
"main": "dist/index.js",
Expand All @@ -14,6 +14,7 @@
},
"files": [
"index.ts",
"tui.ts",
"src/",
"scripts/memory-diag.ts",
"scripts/memory-diag/",
Expand Down Expand Up @@ -54,9 +55,18 @@
},
"homepage": "https://github.com/sdwolf4103/opencode-working-memory#readme",
"peerDependencies": {
"@opencode-ai/plugin": ">=1.2.0 <2.0.0"
"@opencode-ai/plugin": ">=1.18.29 <2.0.0"
},
"peerDependenciesMeta": {
"@opencode-ai/plugin": {
"optional": true
}
},
"dependencies": {
"@opencode/plugin": "^2.1.1"
},
"devDependencies": {
"@opencode-ai/plugin": "^1.19.3",
"@types/node": "^24.3.0",
"bun-types": "^1.2.21",
"typescript": "^5.9.2"
Expand Down
13 changes: 10 additions & 3 deletions src/extractors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,9 +220,16 @@ export function extractActiveFiles(
args: Record<string, unknown>,
output: string,
): Array<{ path: string; action: ActiveFile["action"] }> {
if (toolName === "read" && typeof args.filePath === "string") return [{ path: args.filePath, action: "read" }];
if (toolName === "edit" && typeof args.filePath === "string") return [{ path: args.filePath, action: "edit" }];
if (toolName === "write" && typeof args.filePath === "string") return [{ path: args.filePath, action: "write" }];
// V1 tool inputs use `filePath`; V2 uses `path`.
const filePath = typeof args.filePath === "string"
? args.filePath
: typeof args.path === "string"
? args.path
: undefined;

if (toolName === "read" && filePath) return [{ path: filePath, action: "read" }];
if (toolName === "edit" && filePath) return [{ path: filePath, action: "edit" }];
if (toolName === "write" && filePath) return [{ path: filePath, action: "write" }];
if (toolName === "grep") return extractGrepPaths(output).map(path => ({ path, action: "grep" as const }));
return [];
}
Expand Down
Loading