|
|
|
@@ -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,18 +1319,14 @@ 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 OpenCode's session database.
|
|
|
|
|
*/
|
|
|
|
|
async function calculateTotalTokensFromDB(sessionID: string): Promise<number> {
|
|
|
|
|
try {
|
|
|
|
|
const { execSync } = await import("child_process");
|
|
|
|
|
const dbPath = join(process.env.HOME || "~", ".local/share/opencode/opencode.db");
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
// Get tokens.total from most recent assistant message
|
|
|
|
|
const query = `
|
|
|
|
|
SELECT json_extract(data, '$.tokens.total') as total
|
|
|
|
|
FROM message
|
|
|
|
@@ -1365,11 +1346,7 @@ async function calculateTotalTokensFromDB(sessionID: string): Promise<number> {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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 +1364,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 +1393,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 +1409,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 +1463,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 +1479,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,7 +1521,7 @@ 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);
|
|
|
|
|
|
|
|
|
@@ -1634,20 +1530,18 @@ The Working Memory Plugin provides persistent core_memory blocks. **USE THEM COR
|
|
|
|
|
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 +1552,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 +1566,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 +1576,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 +1592,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 +1647,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 +1681,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,7 +1723,7 @@ 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");
|
|
|
|
@@ -1849,9 +1764,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 +1779,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)
|
|
|
|
|