@@ -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
* / U s e r s / s d _ w o / o p e n c o d e / p a c k a g e s / o p e n c o d e / s r c / f o o . t s → ~ / o p e n c o d e / p k g / o p e n c o d e / s r c / f o o . t s
* / U s e r s / s d _ w o / w o r k / o p e n c o d e - p l u g i n s / . o p e n c o d e / p l u g i n s / f o o . t s → ~ / w o r k / o c - p l u g i n s / . o p e n c o d e / p l u g i n s / f o o . t s
* 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 M emory 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 m emory 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 Co re M emory 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 enti re m emory 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: P roactive I ntervention - 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 p roactive i ntervention if pressure just crossed into HIGH.
if ( updatedPressure . current . level === "high" ) {
// Only send if pressure increased from previous level (avoi d spam)
// Only send if pressure just escalated (avoid repeate d 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 prun ing
// Sub-agents don't need working memory track ing.
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 )