Compare commits

..

4 Commits

Author SHA1 Message Date
Ralph Chang 4832b382b6 fix: silence memory load errors in working-memory 2026-02-19 21:13:20 +08:00
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
3 changed files with 48 additions and 43 deletions
+13
View File
@@ -176,6 +176,19 @@ The plugin exposes these tools to your OpenCode agent:
- ✅ Pressure-aware compression - ✅ Pressure-aware compression
- ✅ Zero configuration, works immediately - ✅ 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) ## Configuration (Optional)
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. 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.
+34 -42
View File
@@ -249,8 +249,7 @@ async function loadCoreMemory(
try { try {
const content = await readFile(path, "utf-8"); const content = await readFile(path, "utf-8");
return JSON.parse(content) as CoreMemory; return JSON.parse(content) as CoreMemory;
} catch (error) { } catch {
console.error("Failed to load core memory:", error);
return null; return null;
} }
} }
@@ -356,8 +355,7 @@ async function loadWorkingMemory(
} }
return data as WorkingMemory; return data as WorkingMemory;
} catch (error) { } catch {
console.error("Failed to load working memory:", error);
return null; return null;
} }
} }
@@ -1319,28 +1317,34 @@ async function loadModelPressureInfo(
} }
/** /**
* Calculate total tokens by querying OpenCode's session database. * 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 { try {
const { execSync } = await import("child_process"); const result = await client.session.messages({
const dbPath = join(process.env.HOME || "~", ".local/share/opencode/opencode.db"); path: { id: sessionID }
});
// Get tokens.total from most recent assistant message if (!result.data) return 0;
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;
`;
const result = execSync(`sqlite3 "${dbPath}" "${query}"`, { encoding: "utf-8" }).trim(); // Find the most recent assistant message with non-zero tokens.
return parseInt(result) || 0; // 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) { } catch (error) {
console.error("[working-memory] Failed to query tokens from DB:", error); // Silent — API call failed
return 0; return 0;
} }
} }
@@ -1523,7 +1527,7 @@ export default async function WorkingMemoryPlugin(
// Calculate current token usage from DB and update pressure info. // Calculate current token usage from DB and update pressure info.
if (model) { if (model) {
const totalTokens = await calculateTotalTokensFromDB(sessionID); const totalTokens = await calculateTotalTokensFromDB(client, sessionID);
// Calculate pressure with current model // Calculate pressure with current model
const updatedPressure = calculateModelPressure( const updatedPressure = calculateModelPressure(
@@ -1725,26 +1729,14 @@ export default async function WorkingMemoryPlugin(
// Inject pending OpenCode todos into compaction context. // Inject pending OpenCode todos into compaction context.
try { try {
const { execSync } = await import("child_process"); const result = await client.session.todo({
const dbPath = join(process.env.HOME || "~", ".local/share/opencode/opencode.db"); path: { id: sessionID }
});
const query = ` if (result.data) {
SELECT content, status, priority const todos = result.data
FROM todo .filter(todo => todo.status !== "completed")
WHERE session_id = '${sessionID}' .map(todo => `- [${todo.status}] ${todo.content} (${todo.priority})`);
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 (todos.length > 0) { if (todos.length > 0) {
output.context.push( output.context.push(
@@ -1753,7 +1745,7 @@ export default async function WorkingMemoryPlugin(
} }
} }
} catch (error) { } catch (error) {
console.error("[working-memory] Failed to inject todos from DB:", error); // Silent — API call failed
} }
// Inform about preserved working memory // Inform about preserved working memory
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "opencode-working-memory", "name": "opencode-working-memory",
"version": "1.1.0", "version": "1.1.2",
"description": "Advanced four-tier memory architecture for OpenCode with intelligent pressure monitoring and auto-storage governance", "description": "Advanced four-tier memory architecture for OpenCode with intelligent pressure monitoring and auto-storage governance",
"type": "module", "type": "module",
"main": "index.ts", "main": "index.ts",