Compare commits

..

11 Commits

Author SHA1 Message Date
Ralph Chang 17258ce60f fix: use SDK API instead of sqlite3 CLI for cross-platform compatibility
- Replace sqlite3 CLI with client.session.messages() and client.session.todo()
- Fix message scan order: API returns oldest-first, now scans backwards
- Include cache.read in total token calculation
- Skip in-progress messages with zero tokens
- Silent error handling (no console.error for expected failures)
2026-02-19 13:43:26 +08:00
Ralph Chang 6fad91953c docs: improve token-usage section readability 2026-02-18 16:59:47 +08:00
Ralph Chang 7b33b51c1e docs: add token-usage guidance for session patterns 2026-02-18 16:57:40 +08:00
Ralph Chang 2edb643667 chore: bump version to v1.1.0 2026-02-18 16:44:17 +08:00
Ralph Chang d7302e531a perf: move core memory guidelines from system prompt to tool description 2026-02-18 16:39:21 +08:00
Ralph Chang bb929fc578 Clean up comments, fix docs, bump to v1.0.1 2026-02-18 14:13:37 +08:00
Ralph Chang 98e5ecaa91 Fix installation: no npm install needed, OpenCode auto-installs from config 2026-02-18 11:08:09 +08:00
Ralph Chang c008fb860f Fix Quick Start: agent prompts instead of CLI commands 2026-02-18 10:59:22 +08:00
Ralph Chang c743ddb331 Restore Quick Start example commands 2026-02-18 10:58:15 +08:00
Ralph Chang 0631286962 Improve README: simplify Quick Start and remove Phase labels 2026-02-18 10:51:01 +08:00
Ralph Chang 75c89747d8 Fix typecheck errors and update install docs for npm release 2026-02-18 10:46:03 +08:00
6 changed files with 277 additions and 405 deletions
+38 -61
View File
@@ -18,11 +18,9 @@ Stop losing context across compactions. Stop watching your agent repeat the same
## Installation
### For Humans
**Option A: Let an AI agent do it**
**Option A: Let an LLM do it**
Paste this into Claude Code, Cursor, or any AI agent:
Paste this into any AI agent:
```
Install opencode-working-memory plugin by following:
@@ -31,7 +29,7 @@ https://raw.githubusercontent.com/sdwolf4103/opencode-working-memory/main/docs/i
**Option B: Manual setup**
1. Add to your `~/.config/opencode/opencode.json`:
Add to your `~/.config/opencode/opencode.json`:
```json
{
@@ -39,42 +37,33 @@ https://raw.githubusercontent.com/sdwolf4103/opencode-working-memory/main/docs/i
}
```
2. Restart OpenCode
That's it! The plugin is now active.
Restart OpenCode. The plugin is downloaded and installed automatically.
### For LLM Agents
1. Read the OpenCode configuration file at `~/.config/opencode/opencode.json`
1. Edit `~/.config/opencode/opencode.json`
2. Add `"opencode-working-memory"` to the `plugin` array
3. Verify installation by checking the config file
3. Verify the config file was saved correctly
## Quick Start
The plugin works automatically once installed. No configuration needed.
**Use the tools:**
**Try telling your agent:**
```bash
# Update persistent memory
opencode run "use core_memory_update to set my goal"
# Read current memory state
opencode run "use core_memory_read to show me what you remember"
# Add important items to working memory
opencode run "use working_memory_add to remember this file path"
```
**The agent will automatically:**
- Track memory pressure and warn when approaching compaction
- Preserve important context during compaction
- Clean up old tool-output cache files every 20 tool calls
- Remove artifacts when sessions are deleted
Use core_memory_update to set my current goal
```
```
Use core_memory_read to show me what you remember
```
```
Use working_memory_add to remember this file path
```
## Features
### 🧠 Core Memory (Phase 1)
### 🧠 Core Memory
Persistent blocks that survive conversation resets:
@@ -82,7 +71,7 @@ Persistent blocks that survive conversation resets:
- **progress** (2000 chars) - What's done, in-progress, next steps
- **context** (1500 chars) - Key file paths, conventions, patterns
### 💡 Working Memory (Phase 3)
### 💡 Working Memory
Auto-extracts and ranks important information:
@@ -91,7 +80,7 @@ Auto-extracts and ranks important information:
- Exponential decay keeps memory fresh
- FIFO limits prevent bloat
### 🎯 Memory Pressure Monitoring (Phase 4)
### 🎯 Memory Pressure Monitoring
Real-time token tracking from session database:
@@ -99,16 +88,15 @@ Real-time token tracking from session database:
- Proactive intervention messages when pressure is high
- Pressure-aware smart pruning (adapts compression based on pressure)
### 🧹 Storage Governance (Phase 5)
### 🧹 Storage Governance
Prevents unbounded disk growth:
- **Layer 1**: Auto-cleanup on session deletion (all artifacts removed)
- **Layer 2**: Active cache management (max 300 files/session, 7-day TTL)
- Triggers every 20 tool calls
- Auto-cleanup on session deletion (all artifacts removed)
- Active cache management (max 300 files/session, 7-day TTL)
- Silent background operation
### 📊 Smart Pruning (Phase 2)
### 📊 Smart Pruning
Intelligent tool output compression:
@@ -188,30 +176,22 @@ The plugin exposes these tools to your OpenCode agent:
- ✅ Pressure-aware compression
- ✅ Zero configuration, works immediately
## Does working-memory system increase token usage? It depends.
It depends on your workflow.
- 🧹 **Clean Slate user** (for example, using DCP and frequently restarting sessions)
- ⚠️ Yes, it might add slight overhead.
- Because you keep starting fresh, automated memory persistence does not get enough time to pay off.
- 🚀 **Long Haul user** (staying in one session until token limits/compaction hit)
- ✅ This plugin is a token saver.
- Without it, compaction can cause the agent to lose the goal, forget active files, or make wrong assumptions, which creates correction loops.
- By preserving high-value context (Goals, Progress, Active Files), the agent inherits its previous state quickly. The small memory-prompt cost avoids the larger cost of the agent getting lost.
## Configuration (Optional)
The plugin works great with zero configuration. But if you want to customize:
Create `~/.config/opencode/working-memory.json`:
```json
{
"storage_governance": {
"tool_output_max_files": 300,
"tool_output_max_age_ms": 604800000,
"sweep_interval": 20
},
"memory_pressure": {
"thresholds": {
"moderate": 0.75,
"high": 0.90,
"critical": 0.95
}
}
}
```
See [Configuration Guide](docs/configuration.md) for all options.
The plugin works great with zero configuration. To customize behavior, modify the constants at the top of `index.ts`. See the [Configuration Guide](docs/configuration.md) for all tunable options.
## Requirements
@@ -223,20 +203,17 @@ See [Configuration Guide](docs/configuration.md) for all options.
MIT License - see [LICENSE](LICENSE) file for details.
## Contributing
Contributions welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) first.
## Support
- 📖 [Documentation](docs/)
- 🐛 [Report Issues](https://github.com/sdwolf4103/opencode-working-memory/issues)
- 💬 [Discussions](https://github.com/sdwolf4103/opencode-working-memory/discussions)
## Credits
Inspired by the needs of real-world OpenCode usage and built to solve actual pain points in AI-assisted development.
> This project is not affiliated with or endorsed by the OpenCode team.
---
**Made with ❤️ for the OpenCode community**
+2 -3
View File
@@ -68,9 +68,8 @@ const POOL_MAX_ITEMS = 50; // Hard limit on pool size
```typescript
const PRESSURE_THRESHOLDS = {
moderate: 70, // Warning appears in system prompt
high: 85, // Aggressive pruning activates
critical: 95, // Intervention sent to agent
moderate: 75, // Warning appears in system prompt
high: 90, // Aggressive pruning activates + intervention sent
};
```
+22 -82
View File
@@ -1,90 +1,34 @@
# Installation Guide
## Prerequisites
## Quick Install
- **OpenCode** 1.0.0 or higher
- **Node.js** 18+ (for development only)
## Quick Install (For Users)
### Option 1: Install from npm (Recommended)
```bash
npm install opencode-working-memory
```
Then add to your `.opencode/package.json`:
Add to your `~/.config/opencode/opencode.json`:
```json
{
"plugins": [
"opencode-working-memory"
]
"plugin": ["opencode-working-memory"]
}
```
### Option 2: Install from GitHub
Restart OpenCode. The plugin is downloaded and installed automatically — no `npm install` needed.
Add to your `.opencode/package.json`:
> **Note**: The correct key is `plugin` (singular), not `plugins`.
```json
{
"dependencies": {
"opencode-working-memory": "github:sdwolf4103/opencode-working-memory"
},
"plugins": [
"opencode-working-memory"
]
}
```
## For LLM Agents
Then run:
```bash
cd .opencode
npm install
```
### Option 3: Local Development Install
Clone the repository:
```bash
git clone https://github.com/sdwolf4103/opencode-working-memory.git
cd opencode-working-memory
npm install
```
Link to your OpenCode project:
```bash
cd /path/to/your/project/.opencode
npm link /path/to/opencode-working-memory
```
Add to `.opencode/package.json`:
```json
{
"plugins": [
"opencode-working-memory"
]
}
```
1. Edit `~/.config/opencode/opencode.json`
2. Add `"opencode-working-memory"` to the `plugin` array
3. Verify the config file was saved correctly
## Verification
After installation, start an OpenCode session and run:
After restarting OpenCode, ask your agent:
```
core_memory_update goal "Test installation"
Use core_memory_read to show me what you remember
```
You should see a success message. Check `.opencode/memory-core/` for the session file.
## Configuration
The plugin works out-of-the-box with sensible defaults. For advanced configuration, see [configuration.md](./configuration.md).
If the tool responds, the plugin is active.
## Troubleshooting
@@ -92,10 +36,10 @@ The plugin works out-of-the-box with sensible defaults. For advanced configurati
**Symptom**: No `core_memory_update` tool available
**Solution**:
1. Check `.opencode/package.json` includes plugin in `"plugins": []` array
2. Verify `npm install` completed successfully
3. Restart OpenCode session
**Solution**:
1. Check `~/.config/opencode/opencode.json` uses `"plugin"` (not `"plugins"`)
2. Restart OpenCode to trigger automatic installation
3. Check OpenCode logs for any download errors
### Memory Files Not Created
@@ -103,26 +47,22 @@ The plugin works out-of-the-box with sensible defaults. For advanced configurati
**Solution**:
1. Ensure OpenCode has write permissions in project directory
2. Check plugin hooks are registered (look for "Working Memory Plugin" in session logs)
3. Trigger memory operations (e.g., use `core_memory_update` tool)
2. Trigger memory operations (e.g., use `core_memory_update` tool)
### Type Errors During Development
**Symptom**: TypeScript errors when modifying plugin
**Symptom**: TypeScript errors when modifying the plugin source
**Solution**:
1. Ensure `@opencode-ai/plugin` is installed: `npm install @opencode-ai/plugin`
2. Run type checking: `npx tsc --noEmit`
1. Run `npm install` to install dev dependencies
2. Run `npm run typecheck` to check for errors
3. See [AGENTS.md](../AGENTS.md) for code style guidelines
## Uninstallation
```bash
cd .opencode
npm uninstall opencode-working-memory
```
Remove `"opencode-working-memory"` from the `plugin` array in `~/.config/opencode/opencode.json`.
Remove from `.opencode/package.json` plugins array. Memory files in `.opencode/memory-*` will persist unless manually deleted.
Memory files in `.opencode/memory-*` will persist unless manually deleted.
## Next Steps
+193 -247
View File
@@ -1,20 +1,15 @@
/**
* Working Memory Plugin for OpenCode
*
* Provides a three-tier memory system to delay/avoid compaction:
*
* Four-tier memory architecture:
* 1. Core Memory - Persistent goal/progress/context blocks (always in-context)
* 2. Working Memory - Auto-managed session-relevant information
* 3. Smart Pruning - Content-aware tool output compression
* 4. Memory Pressure Monitoring - Context usage tracking with adaptive warnings
*
* Phase 1: Core Memory Foundation (MVP) - ✅ COMPLETED
* Phase 2: Smart Pruning System - ✅ COMPLETED
* Phase 3: Working Memory Auto-Management - ✅ COMPLETED
* Phase 4: Memory Pressure Monitoring - ✅ COMPLETED
*/
import type { Plugin } from "@opencode-ai/plugin";
import { tool } from "@opencode-ai/plugin";
import { tool } from "@opencode-ai/plugin/tool";
import { existsSync } from "fs";
import { mkdir, readFile, writeFile, readdir, stat, unlink, rm } from "fs/promises";
import { join } from "path";
@@ -46,7 +41,7 @@ const CORE_MEMORY_LIMITS = {
};
// ============================================================================
// Phase 2: Smart Pruning Types
// Smart Pruning Types
// ============================================================================
type PruningStrategy =
@@ -72,7 +67,7 @@ type CachedToolOutput = {
};
// ============================================================================
// Phase 3: Working Memory Types (Slot-based Architecture)
// Working Memory Types (Slot-based Architecture)
// ============================================================================
type WorkingMemory = {
@@ -128,7 +123,7 @@ const WORKING_MEMORY_LIMITS = {
};
// ============================================================================
// Storage Governance (Layer 1 + Layer 2)
// Storage Governance
// ============================================================================
const STORAGE_GOVERNANCE = {
@@ -138,7 +133,7 @@ const STORAGE_GOVERNANCE = {
};
// ============================================================================
// Phase 4: Memory Pressure Monitoring
// Memory Pressure Monitoring
// ============================================================================
type PressureLevel = "safe" | "moderate" | "high";
@@ -169,7 +164,6 @@ type ModelPressureInfo = {
updatedAt: string;
};
// Compaction tracking (preserved from Phase 4 initial work)
type CompactionLog = {
sessionID: string;
compactionCount: number;
@@ -501,12 +495,11 @@ async function updateCoreMemoryBlock(
}
// ============================================================================
// Storage Governance Functions (Layer 1 + Layer 2)
// Storage Governance Functions
// ============================================================================
/**
* Layer 1: Clean up all artifacts for a deleted session
* Called when session.deleted event is received
* Clean up all artifacts for a deleted session.
*/
async function cleanupSessionArtifacts(
directory: string,
@@ -532,9 +525,9 @@ async function cleanupSessionArtifacts(
}
/**
* Layer 2: Sweep tool-output cache for a session
* Remove files older than TTL and enforce max file count
* Returns number of files deleted
* Sweep tool-output cache for a session.
* Removes files older than TTL and enforces max file count.
* Returns number of files deleted.
*/
async function sweepToolOutputCache(
directory: string,
@@ -606,7 +599,7 @@ async function sweepToolOutputCache(
}
// ============================================================================
// Phase 2: Smart Pruning System
// Smart Pruning System
// ============================================================================
/**
@@ -775,7 +768,7 @@ async function getCachedToolOutput(
}
// ============================================================================
// Phase 3: Working Memory Auto-Management
// Working Memory Auto-Management
// ============================================================================
/**
@@ -1057,9 +1050,7 @@ function getTopItemsForPrompt(
}
/**
* Compress file paths to save space in system prompt
* /Users/sd_wo/opencode/packages/opencode/src/foo.ts → ~/opencode/pkg/opencode/src/foo.ts
* /Users/sd_wo/work/opencode-plugins/.opencode/plugins/foo.ts → ~/work/oc-plugins/.opencode/plugins/foo.ts
* Compress file paths to save space in system prompt.
*/
function compressPath(content: string): string {
const homeDir = process.env.HOME || '/Users/' + (process.env.USER || 'user');
@@ -1131,7 +1122,7 @@ Recent session context (auto-managed, sorted by relevance):
${sections.join("\n\n")}
(${totalItems} items shown, updated: ${new Date(memory.updatedAt).toLocaleTimeString()})
(${totalItems} items shown)
</working_memory>
`.trim();
}
@@ -1145,7 +1136,7 @@ function getWorkingMemoryItemCount(memory: WorkingMemory): number {
}
// ============================================================================
// Phase 4: Compaction Tracking and State Preservation
// Compaction Tracking
// ============================================================================
/**
@@ -1207,14 +1198,13 @@ async function recordCompaction(
// ============================================================================
/**
* Calculate usable tokens using OpenCode's exact compaction formula
* Reference: packages/opencode/src/session/compaction.ts:32-48
* Calculate usable tokens using OpenCode's compaction formula.
*/
function calculateUsableTokens(model: {
limit: { context: number; input?: number; output: number };
}): number {
const OUTPUT_TOKEN_MAX = 32_000; // From transform.ts:21
const COMPACTION_BUFFER = 20_000; // From compaction.ts:33
const OUTPUT_TOKEN_MAX = 32_000;
const COMPACTION_BUFFER = 20_000;
const maxOutputTokens = Math.min(
model.limit.output || OUTPUT_TOKEN_MAX,
@@ -1222,7 +1212,6 @@ function calculateUsableTokens(model: {
);
const reserved = Math.min(COMPACTION_BUFFER, maxOutputTokens);
// Match compaction.ts:42-47
const usable = model.limit.input
? model.limit.input - reserved
: model.limit.context - maxOutputTokens;
@@ -1231,11 +1220,7 @@ function calculateUsableTokens(model: {
}
/**
* Calculate pressure level based on current tokens and usable limit
*
* Thresholds:
* - 0.75 (75%): moderate - show reminder in prompt
* - 0.9 (90%): high - send intervention message
* Calculate pressure level based on current tokens and usable limit.
*/
function calculatePressureLevel(
currentTokens: number,
@@ -1253,7 +1238,7 @@ function calculatePressureLevel(
*/
function calculateModelPressure(
sessionID: string,
model: { id: string; provider: string; limit: { context: number; input?: number; output: number } },
model: { id: string; providerID: string; limit: { context: number; input?: number; output: number } },
totalTokens: number
): ModelPressureInfo {
const OUTPUT_TOKEN_MAX = 32_000;
@@ -1272,7 +1257,7 @@ function calculateModelPressure(
return {
sessionID,
modelID: model.id,
providerID: model.provider,
providerID: model.providerID,
limits: {
context: model.limit.context,
input: model.limit.input,
@@ -1334,42 +1319,40 @@ async function loadModelPressureInfo(
}
/**
* Calculate total tokens by querying OpenCode's session database
* This is more reliable than relying on hook-provided messages
*
* Note: Only looks at last 10 messages to avoid stale data from before compaction
* Calculate total tokens by querying session messages via OpenCode SDK.
* Cross-platform compatible (no sqlite3 CLI dependency).
*/
async function calculateTotalTokensFromDB(sessionID: string): Promise<number> {
async function calculateTotalTokensFromDB(
client: ReturnType<typeof import("@opencode-ai/sdk").createOpencodeClient>,
sessionID: string
): Promise<number> {
try {
const { execSync } = await import("child_process");
const dbPath = join(process.env.HOME || "~", ".local/share/opencode/opencode.db");
const result = await client.session.messages({
path: { id: sessionID }
});
// Get tokens.total from most recent assistant message (last 10 to be safe)
// Use MAX to handle edge cases, but limit to recent messages to avoid stale pre-compaction data
const query = `
SELECT json_extract(data, '$.tokens.total') as total
FROM message
WHERE session_id = '${sessionID}'
AND json_extract(data, '$.role') = 'assistant'
AND json_extract(data, '$.tokens.total') IS NOT NULL
ORDER BY time_created DESC
LIMIT 1;
`;
if (!result.data) return 0;
const result = execSync(`sqlite3 "${dbPath}" "${query}"`, { encoding: "utf-8" }).trim();
return parseInt(result) || 0;
// Find the most recent assistant message with non-zero tokens.
// API returns messages oldest-first, so we scan backwards.
for (let i = result.data.length - 1; i >= 0; i--) {
const { info } = result.data[i];
if (info.role !== "assistant") continue;
// Total = input + output + reasoning + cache.read
const total = info.tokens.input + info.tokens.output + info.tokens.reasoning + info.tokens.cache.read;
if (total > 0) return total;
}
return 0;
} catch (error) {
console.error("[working-memory] Failed to query tokens from DB:", error);
// Silent — API call failed
return 0;
}
}
/**
* Generate pressure warning text for system prompt injection
*
* Design principles:
* - MODERATE (75%): gentle nudge, no interruption
* - HIGH (90%): actionable commands, pause and persist state
* Generate pressure warning text for system prompt injection.
*/
function generatePressureWarning(info: ModelPressureInfo): string {
const { current, calculated } = info;
@@ -1387,13 +1370,8 @@ function generatePressureWarning(info: ModelPressureInfo): string {
}
/**
* Send proactive intervention message when HIGH pressure detected (90%)
*
* This sends an independent system message to the session immediately, so the agent
* receives the task in the queue without interrupting current work. The agent will
* process it automatically when available.
*
* Design: Use promptAsync() which returns 204 immediately, non-blocking.
* Send a proactive intervention message when HIGH pressure (90%) is detected.
* Uses promptAsync() which returns immediately (non-blocking).
*/
async function sendPressureInterventionMessage(
client: any,
@@ -1421,17 +1399,14 @@ REQUIRED ACTIONS:
After completing these actions, you may resume your current task.`;
try {
// Use promptAsync to send message without waiting for response
await client.session.promptAsync({
path: { id: sessionID },
body: {
parts: [{
type: "text",
// Send actionable content directly (not log-style placeholder)
text: systemPrompt,
}],
// Keep system unset so the intervention is visible as a normal prompt
noReply: false, // We want agent to respond with actions
noReply: false,
},
});
} catch (error) {
@@ -1440,36 +1415,32 @@ After completing these actions, you may resume your current task.`;
}
/**
* Get pressure-aware pruning config based on current memory pressure
* HYPER-AGGRESSIVE MODE: pressure >= 0.90 enforces strict limits
* Get pressure-aware pruning config based on current memory pressure.
*/
function getPressureAwarePruningConfig(pressure: number): {
maxLines: number;
maxChars: number;
aggressiveTruncation: boolean;
} {
// HIGH (>= 90%): Hyper-Aggressive Mode
if (pressure >= 0.90) {
return {
maxLines: 2000, // Hard limit: 2000 lines max
maxChars: 100_000, // ~25k tokens max per tool output
aggressiveTruncation: true, // Force truncation, no exceptions
};
}
// MODERATE (>= 75%): Aggressive Mode
if (pressure >= 0.75) {
return {
maxLines: 5000,
maxChars: 200_000, // ~50k tokens max
maxLines: 2000,
maxChars: 100_000,
aggressiveTruncation: true,
};
}
if (pressure >= 0.75) {
return {
maxLines: 5000,
maxChars: 200_000,
aggressiveTruncation: true,
};
}
// SAFE (< 75%): Normal Mode
return {
maxLines: 10_000,
maxChars: 400_000, // ~100k tokens max
maxChars: 400_000,
aggressiveTruncation: false,
};
}
@@ -1498,18 +1469,9 @@ ${context.value || "[No project context set - add relevant file paths, conventio
</context>
IMPORTANT: These blocks persist across conversation resets and compaction.
Update them regularly using core_memory_update tool when:
- Goals change or new objectives are identified
- Significant progress is made or tasks are completed
- Important project context is discovered (file structures, patterns, conventions)
Update them regularly using core_memory_update tool. When blocks approach their character limits, compress or rephrase content.
When memory blocks approach their character limits, compress or rephrase content.
**Usage Discipline** (see Core Memory Usage Guidelines above for details):
- goal: ONE specific task, not project-wide goals
- progress: Checklist format, NO line numbers/commit hashes/API signatures
- context: ONLY files you're currently working on, NO type definitions/function signatures
- NEVER store: API docs, library types, function signatures (read source instead)
To mark decisions for automatic capture into working memory, write inline: [Decision: chose X over Y because Z]
</core_memory>
`.trim();
}
@@ -1523,98 +1485,38 @@ export default async function WorkingMemoryPlugin(
): Promise<ReturnType<Plugin>> {
const { directory, client } = input;
// Cache for sub-agent detection — avoids repeated API calls per session.
// Maps sessionID → parentID (string) or null (root session).
const sessionParentCache = new Map<string, string | null>();
async function isSubAgent(sessionID: string): Promise<boolean> {
if (sessionParentCache.has(sessionID)) {
return sessionParentCache.get(sessionID) !== null;
}
try {
const result = await client.session.get({ path: { id: sessionID } });
const parentID = result.data?.parentID ?? null;
sessionParentCache.set(sessionID, parentID);
return parentID !== null;
} catch {
// If we can't determine, assume it's NOT a sub-agent (safe default).
sessionParentCache.set(sessionID, null);
return false;
}
}
return {
// ========================================================================
// Phase 1: Inject Core Memory and Working Memory into System Prompt
// Phase 4: Inject Memory Pressure Warnings & Calculate Tokens from DB
// Phase 4.5: Proactive Pressure Intervention (NEW)
// Phase 5: Core Memory Usage Guidelines (AGENTS.md Enhancement)
//
// Dual-System Approach:
// 1. PASSIVE WARNING (existing): Injected into next turn's system prompt
// - Always present as reminder in system context
// - 1-turn delay but persistent
//
// 2. PROACTIVE INTERVENTION (new): Immediate async message sent to queue
// - No delay, sent immediately when HIGH (90%) detected
// - Agent processes when available (non-blocking)
// - Only sent when pressure level increases (avoids spam)
//
// 3. USAGE GUIDELINES (new): Injected after AGENTS.md, before core_memory
// - Teaches agent how to use core_memory blocks correctly
// - Prevents storing API docs/type definitions in memory
// - Ensures goal/progress/context stay focused on current task
// ========================================================================
// Inject pressure warnings, core memory, and working memory into the system prompt each turn.
// Core memory usage guidelines are in the core_memory_update tool description instead.
"experimental.chat.system.transform": async (hookInput, output) => {
const { sessionID, model } = hookInput;
if (!sessionID) return;
// Phase 5: Inject Core Memory Usage Guidelines
// This enhances AGENTS.md (if exists) with plugin-specific instructions
// Inserted early so it's read before agent sees <core_memory> block
const coreMemoryGuidelines = `
# Core Memory Usage Guidelines
// Sub-agents are short-lived — skip entire memory system.
if (await isSubAgent(sessionID)) return;
The Working Memory Plugin provides persistent core_memory blocks. **USE THEM CORRECTLY**:
## goal block (1000 chars)
**Purpose**: ONE specific task you're working on RIGHT NOW
✅ **GOOD Examples**:
- "Fix pruning bug where items with relevanceScore <0.01 are incorrectly excluded"
- "Add new tool: working_memory_search to query pool items by keyword"
- "Investigate why pressure warnings not showing in system prompt"
❌ **BAD Examples**:
- "Complete Phase 1-4 development and testing" (too broad, likely already done)
- "Build a working memory system for OpenCode" (project-level goal, not task-level)
## progress block (2000 chars)
**Purpose**: Checklist of done/in-progress/blocked items + key decisions
✅ **GOOD Examples**:
- "✅ Found bug in applyDecay() line 856\\n⏳ Testing fix with gamma=0.85\\n❓ Need to verify edge case: score=0"
- "✅ Phase 1-3 complete\\n⏳ Phase 4 intervention testing\\n⚠️ BLOCKED: Need promptAsync docs"
❌ **BAD Examples**:
- "Function sendPressureInterventionMessage() @ working-memory.ts:L1286-1354" (line numbers useless after edits)
- "Commit 2f42f1b implemented promptAsync integration" (commit hash irrelevant)
- "API: client.session.promptAsync({ path: {id}, body: {...} })" (API signature, not progress)
## context block (1500 chars)
**Purpose**: Files you're CURRENTLY editing + key patterns/conventions
✅ **GOOD Examples**:
- "Editing: .opencode/plugins/working-memory.ts (main plugin, 1706 lines)\\nRelated: WORKING_MEMORY.md, TEST_PHASE4.md"
- "Key paths: .opencode/memory-core/ (persistent blocks), memory-working/ (session data)"
- "Pattern: All async file ops use mkdir({recursive:true}) before writeFile"
❌ **BAD Examples**:
- "OpenCode SDK types: TextPartInput = { type: 'text', text: string, synthetic?: boolean }" (type definition)
- "Function signature: async function loadCoreMemory(directory: string, sessionID: string): Promise<CoreMemory | null>" (function signature)
- "Method client.session.promptAsync() returns 204 No Content" (API behavior, read docs instead)
## ⚠️ NEVER Store in Core Memory
- API documentation (read source/docs when needed)
- Type definitions from libraries (import them)
- Function signatures (read source code)
- Implementation details (belong in code comments)
- Completed goals (clear them immediately)
## ✅ Update Core Memory Immediately When
- **Starting new task**: Clear old goal, set new specific goal
- **Making progress**: Update progress checklist (keep concise)
- **Switching files**: Update context with current working files
- **Task completed**: Clear goal/progress, set next task
- **Approaching char limit**: Compress or remove outdated info
**Remember**: Core Memory is your **working scratchpad**, not a reference manual.
`.trim();
output.system.push(coreMemoryGuidelines);
// Phase 4: Check for memory pressure and inject warning
// Skip warning if model just changed (avoids false alarms with different limits)
// Check for memory pressure and inject warning into system prompt.
// Skip if model just changed (avoids false alarms with different limits).
const prevPressure = await loadModelPressureInfo(directory, sessionID);
const modelChanged = model && prevPressure && prevPressure.modelID !== model.id;
@@ -1625,29 +1527,27 @@ The Working Memory Plugin provides persistent core_memory blocks. **USE THEM COR
}
}
// Phase 4: Calculate current token usage from DB and update pressure
// Calculate current token usage from DB and update pressure info.
if (model) {
const totalTokens = await calculateTotalTokensFromDB(sessionID);
const totalTokens = await calculateTotalTokensFromDB(client, sessionID);
// Calculate pressure with current model
const updatedPressure = calculateModelPressure(
sessionID,
{
id: model.id,
provider: model.provider,
providerID: model.providerID,
limit: model.limit,
},
totalTokens
);
// Save for next turn's warning injection
// Save for next turn's warning injection.
await saveModelPressureInfo(directory, updatedPressure);
// Phase 4.5: Proactive Intervention - Send immediate message if HIGH (90%)
// This is better than waiting for next turn's passive warning
// The message goes into the queue and agent processes it when available
// Send proactive intervention if pressure just crossed into HIGH.
if (updatedPressure.current.level === "high") {
// Only send if pressure increased from previous level (avoid spam)
// Only send if pressure just escalated (avoid repeated spam).
const shouldSend = !prevPressure ||
prevPressure.current.level === "safe" ||
prevPressure.current.level === "moderate";
@@ -1658,7 +1558,7 @@ The Working Memory Plugin provides persistent core_memory blocks. **USE THEM COR
}
}
// Phase 1: Core memory
// Core memory
const coreMemory = await loadCoreMemory(directory, sessionID);
if (coreMemory) {
const hasContent =
@@ -1672,7 +1572,7 @@ The Working Memory Plugin provides persistent core_memory blocks. **USE THEM COR
}
}
// Phase 1: Working memory
// Working memory
const workingMemory = await loadWorkingMemory(directory, sessionID);
if (workingMemory && getWorkingMemoryItemCount(workingMemory) > 0) {
const workingPrompt = renderWorkingMemoryPrompt(workingMemory);
@@ -1682,15 +1582,14 @@ The Working Memory Plugin provides persistent core_memory blocks. **USE THEM COR
}
},
// ========================================================================
// Phase 2 & 3: Cache Tool Outputs and Auto-Extract to Working Memory
// Storage Governance Layer 2: Tool Output Cache Sweep Trigger
// ========================================================================
// Cache tool outputs, auto-extract to working memory, and sweep cache periodically.
"tool.execute.after": async (hookInput, hookOutput) => {
const { sessionID, callID, tool: toolName, args } = hookInput;
const { output: toolOutput } = hookOutput;
// Phase 2: Cache the full output for later smart pruning
// Sub-agents don't need working memory tracking.
if (await isSubAgent(sessionID)) return;
await cacheToolOutput(directory, {
callID,
sessionID,
@@ -1699,25 +1598,25 @@ The Working Memory Plugin provides persistent core_memory blocks. **USE THEM COR
timestamp: Date.now(),
});
// Phase 3: Auto-extract to working memory
const extractedItems = extractFromToolOutput(toolName, toolOutput);
for (const item of extractedItems) {
await addToWorkingMemory(directory, sessionID, item);
}
// Storage Governance Layer 2: Sweep tool-output cache every N calls
// Sweep tool-output cache every N tool calls.
const memory = await loadWorkingMemory(directory, sessionID);
if (memory && memory.eventCounter % STORAGE_GOVERNANCE.sweepInterval === 0) {
await sweepToolOutputCache(directory, sessionID);
}
},
// ========================================================================
// Phase 2: Apply Smart Pruning to Messages (Pressure-Aware)
// ========================================================================
// Apply smart pruning to compacted tool outputs (pressure-aware).
"experimental.chat.messages.transform": async (hookInput, output) => {
const sessionID = output.messages[0]?.info?.sessionID || "";
// Sub-agents don't need smart pruning.
if (sessionID && await isSubAgent(sessionID)) return;
// Load current pressure info to get pressure-aware pruning config
const currentPressure = await loadModelPressureInfo(directory, sessionID);
const pressureLevel = currentPressure?.current?.pressure || 0;
@@ -1754,11 +1653,32 @@ The Working Memory Plugin provides persistent core_memory blocks. **USE THEM COR
}
},
// ========================================================================
// Storage Governance Layer 1: Session Deletion Event Handler
// ========================================================================
// Auto-capture [Decision: ...] markers from agent responses.
"experimental.text.complete": async (hookInput, output) => {
const { sessionID } = hookInput;
if (!sessionID) return;
// Sub-agents are short-lived — skip decision tracking.
if (await isSubAgent(sessionID)) return;
// Extract all [Decision: ...] markers from the completed text.
const matches = output.text.matchAll(/\[Decision:\s*([^\]]+)\]/gi);
for (const match of matches) {
const description = match[1].trim();
if (!description) continue;
await addToWorkingMemory(directory, sessionID, {
type: "decision",
content: description,
source: "auto:text",
timestamp: Date.now(),
mentions: 1,
});
}
},
// Clean up all session artifacts on session deletion.
event: async ({ event }) => {
// Listen for session.deleted events and cleanup all artifacts
if (event.type === "session.deleted") {
const sessionID = event.properties?.info?.id;
if (sessionID) {
@@ -1767,12 +1687,13 @@ The Working Memory Plugin provides persistent core_memory blocks. **USE THEM COR
}
},
// ========================================================================
// Phase 4: Preserve State Before Compaction
// ========================================================================
// Preserve working memory state before compaction.
"experimental.session.compacting": async (hookInput, output) => {
const { sessionID } = hookInput;
// Sub-agents don't need compaction support.
if (await isSubAgent(sessionID)) return;
// Preserve only the most relevant working memory items
const preservedItems = await preserveRelevantItems(directory, sessionID, 0.5);
@@ -1808,28 +1729,16 @@ The Working Memory Plugin provides persistent core_memory blocks. **USE THEM COR
}
}
// SSOT Bridge: Inject OpenCode native Todos from DB into compaction context
// Inject pending OpenCode todos into compaction context.
try {
const { execSync } = await import("child_process");
const dbPath = join(process.env.HOME || "~", ".local/share/opencode/opencode.db");
const result = await client.session.todo({
path: { id: sessionID }
});
const query = `
SELECT content, status, priority
FROM todo
WHERE session_id = '${sessionID}'
AND status != 'completed'
ORDER BY position ASC;
`;
const result = execSync(`sqlite3 "${dbPath}" "${query.replace(/\n/g, ' ')}"`, {
encoding: "utf-8"
}).trim();
if (result) {
const todos = result.split('\n').map(line => {
const [content, status, priority] = line.split('|');
return `- [${status}] ${content} (${priority})`;
});
if (result.data) {
const todos = result.data
.filter(todo => todo.status !== "completed")
.map(todo => `- [${todo.status}] ${todo.content} (${todo.priority})`);
if (todos.length > 0) {
output.context.push(
@@ -1838,7 +1747,7 @@ The Working Memory Plugin provides persistent core_memory blocks. **USE THEM COR
}
}
} catch (error) {
console.error("[working-memory] Failed to inject todos from DB:", error);
// Silent — API call failed
}
// Inform about preserved working memory
@@ -1849,9 +1758,7 @@ The Working Memory Plugin provides persistent core_memory blocks. **USE THEM COR
}
},
// ========================================================================
// Tools
// ========================================================================
tool: {
core_memory_update: tool({
description: `Update persistent core memory blocks that survive compaction.
@@ -1866,16 +1773,55 @@ Operations:
- append: Add content to the end of the block (automatically adds newline)
These blocks are ALWAYS visible to you in every message, even after compaction.
Update them regularly to maintain continuity across long conversations.`,
Update them regularly to maintain continuity across long conversations.
---
## Usage Guidelines
### goal block (1000 chars)
**Purpose**: ONE specific task you're working on RIGHT NOW
✅ GOOD: "Fix pruning bug where items with relevanceScore <0.01 are incorrectly excluded"
✅ GOOD: "Add new tool: working_memory_search to query pool items by keyword"
❌ BAD: "Complete Phase 1-4 development and testing" (too broad, likely already done)
❌ BAD: "Build a working memory system for OpenCode" (project-level goal, not task-level)
### progress block (2000 chars)
**Purpose**: Checklist of done/in-progress/blocked items + key decisions
✅ GOOD: "✅ Found bug in applyDecay()\\n⏳ Testing fix with gamma=0.85\\n❓ Need to verify edge case"
✅ GOOD: "✅ Phase 1-3 complete\\n⏳ Phase 4 intervention testing\\n⚠️ BLOCKED: Need promptAsync docs"
❌ BAD: "Function foo() @ file.ts:L1286-1354" (line numbers useless after edits)
❌ BAD: "Commit 2f42f1b implemented X" (commit hash irrelevant)
❌ BAD: "API: client.session.promptAsync({ ... })" (API signatures belong in source)
### context block (1500 chars)
**Purpose**: Files you're CURRENTLY editing + key patterns/conventions
✅ GOOD: "Editing: src/plugin.ts\\nKey paths: .opencode/memory-core/ (persistent blocks)"
✅ GOOD: "Pattern: All async file ops use mkdir({recursive:true}) before writeFile"
❌ BAD: Type definitions, function signatures, API docs (read source/docs instead)
### NEVER store in core memory
- API documentation (read source/docs when needed)
- Type definitions from libraries (import them)
- Function signatures (read source code)
- Completed goals (clear them immediately)
### Update core memory immediately when
- Starting new task: clear old goal, set new specific goal
- Making progress: update progress checklist (keep concise)
- Switching files: update context with current working files
- Task completed: clear goal/progress, set next task
- Approaching char limit: compress or remove outdated info`,
args: {
block: tool.schema.enum(["goal", "progress", "context"], {
description:
"Which memory block to update (goal/progress/context)",
}),
operation: tool.schema.enum(["replace", "append"], {
description:
"Whether to replace the entire block or append to it",
}),
block: tool.schema.enum(["goal", "progress", "context"]).describe(
"Which memory block to update (goal/progress/context)"
),
operation: tool.schema.enum(["replace", "append"]).describe(
"Whether to replace the entire block or append to it"
),
content: tool.schema
.string()
.max(5000)
+10 -1
View File
@@ -1,9 +1,13 @@
{
"name": "opencode-working-memory",
"version": "1.0.0",
"version": "1.1.1",
"description": "Advanced four-tier memory architecture for OpenCode with intelligent pressure monitoring and auto-storage governance",
"type": "module",
"main": "index.ts",
"scripts": {
"build": "node -e \"console.log('No build step required: OpenCode loads index.ts directly')\"",
"typecheck": "tsc --noEmit"
},
"keywords": [
"opencode",
"plugin",
@@ -25,6 +29,11 @@
"peerDependencies": {
"@opencode-ai/plugin": "^1.2.0"
},
"devDependencies": {
"@types/node": "^24.3.0",
"bun-types": "^1.2.21",
"typescript": "^5.9.2"
},
"engines": {
"node": ">=18.0.0"
}
+12 -11
View File
@@ -1,21 +1,22 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"module": "NodeNext",
"lib": ["ES2022"],
"moduleResolution": "node",
"moduleResolution": "NodeNext",
"types": ["node", "bun-types"],
"esModuleInterop": true,
"skipLibCheck": true,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"strict": false,
"noImplicitAny": false,
"strictNullChecks": false,
"strictFunctionTypes": false,
"strictBindCallApply": false,
"strictPropertyInitialization": false,
"noImplicitThis": false,
"alwaysStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true,