mirror of
https://github.com/sdwolf4103/opencode-working-memory.git
synced 2026-07-17 12:56:42 +02:00
1bba0511bb
## Task 1: Fix exitCode undefined false positive - Add `typeof exitCode !== "number"` check in plugin.ts - Only extract errors when exitCode is explicitly non-zero - Prevent git-log/cat with "errors" text from creating false positives ## Task 2: Fix workspace memory XML truncation - Budget-aware line-by-line rendering - Always include closing </workspace_memory> tag - Return empty string when budget too small - Bonus: canonical exact deduplication with source priority ## Task 3: Remove "always" as trigger - Replace "always" with "going forward" in patterns - Add word boundary via `g` flag and matchAll loop - "from now on" still works as expected ## Task 4: Verification - 22 tests passing - typecheck passing Tests cover: - git log/cat with loose "errors" ignored - TS2345/TypeError strong signals captured - undefined exitCode: no create, no clear - exitCode 0: clears errors - exitCode non-zero: creates error - XML never truncated mid-tag - "always" not a trigger
177 lines
4.5 KiB
TypeScript
177 lines
4.5 KiB
TypeScript
/**
|
|
* OpenCode SDK helper functions for memory plugin.
|
|
*
|
|
* These functions wrap OpenCode client API calls to extract:
|
|
* - Latest user message text (for explicit memory extraction)
|
|
* - Latest compaction summary (for memory candidate parsing)
|
|
* - Pending todos (for compaction context)
|
|
*/
|
|
|
|
/**
|
|
* Extract the latest user message text from a session.
|
|
* Returns { id, text } or null if no user message found.
|
|
*/
|
|
export async function latestUserText(
|
|
client: unknown,
|
|
sessionID: string
|
|
): Promise<{ id: string; text: string } | null> {
|
|
try {
|
|
// Cast client to access session.messages API
|
|
const api = client as {
|
|
session: {
|
|
messages: (params: { path: { id: string } }) => Promise<{
|
|
data?: Array<{
|
|
info?: {
|
|
role?: string;
|
|
id?: string;
|
|
};
|
|
parts?: Array<{
|
|
type?: string;
|
|
text?: string;
|
|
}>;
|
|
}>;
|
|
}>;
|
|
};
|
|
};
|
|
|
|
const result = await api.session.messages({ path: { id: sessionID } });
|
|
const messages = result.data ?? [];
|
|
|
|
// Scan backwards from most recent messages to find the latest user message
|
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
const msg = messages[i];
|
|
if (msg.info?.role !== "user") continue;
|
|
|
|
// Concatenate all text parts
|
|
const text = (msg.parts ?? [])
|
|
.filter((p: { type?: string }) => p.type === "text")
|
|
.map((p: { text?: string }) => p.text ?? "")
|
|
.join("\n");
|
|
|
|
if (text.trim()) {
|
|
return {
|
|
id: msg.info?.id ?? "",
|
|
text: text.trim(),
|
|
};
|
|
}
|
|
}
|
|
|
|
return null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract the latest compaction summary from a session.
|
|
* Compaction summaries are assistant messages marked with summary=true.
|
|
*/
|
|
export async function latestCompactionSummary(
|
|
client: unknown,
|
|
sessionID: string
|
|
): Promise<string | null> {
|
|
try {
|
|
const api = client as {
|
|
session: {
|
|
messages: (params: { path: { id: string } }) => Promise<{
|
|
data?: Array<{
|
|
info?: {
|
|
role?: string;
|
|
summary?: boolean;
|
|
};
|
|
parts?: Array<{
|
|
type?: string;
|
|
text?: string;
|
|
}>;
|
|
}>;
|
|
}>;
|
|
};
|
|
};
|
|
|
|
const result = await api.session.messages({ path: { id: sessionID } });
|
|
const messages = result.data ?? [];
|
|
|
|
// Scan backwards to find the most recent summary (compaction)
|
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
const msg = messages[i];
|
|
if (msg.info?.role !== "assistant" || msg.info?.summary !== true) continue;
|
|
|
|
const text = (msg.parts ?? [])
|
|
.filter((p: { type?: string }) => p.type === "text")
|
|
.map((p: { text?: string }) => p.text ?? "")
|
|
.join("\n");
|
|
|
|
if (text.trim()) {
|
|
return text.trim();
|
|
}
|
|
}
|
|
|
|
return null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch pending todos from a session.
|
|
* Returns todos that are not marked as completed.
|
|
*/
|
|
export async function pendingTodos(
|
|
client: unknown,
|
|
sessionID: string
|
|
): Promise<Array<{ content: string; status: string; priority?: string }>> {
|
|
try {
|
|
const api = client as {
|
|
session: {
|
|
todo: (params: { path: { id: string } }) => Promise<{
|
|
data?: Array<{
|
|
content?: string;
|
|
status?: string;
|
|
priority?: string;
|
|
}>;
|
|
}>;
|
|
};
|
|
};
|
|
|
|
const result = await api.session.todo({ path: { id: sessionID } });
|
|
const todos = result.data ?? [];
|
|
|
|
// Filter out completed todos
|
|
return todos
|
|
.filter((todo: { status?: string }) => todo.status !== "completed")
|
|
.map((todo: { content?: string; status?: string; priority?: string }) => ({
|
|
content: todo.content ?? "",
|
|
status: todo.status ?? "pending",
|
|
priority: todo.priority,
|
|
}));
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if a session is a sub-agent (has a parent session).
|
|
* Sub-agents are short-lived and should not have their own memory tracking.
|
|
*/
|
|
export async function isSubAgent(
|
|
client: unknown,
|
|
sessionID: string
|
|
): Promise<boolean> {
|
|
try {
|
|
const api = client as {
|
|
session: {
|
|
get: (params: { path: { id: string } }) => Promise<{
|
|
data?: {
|
|
parentID?: string | null;
|
|
};
|
|
}>;
|
|
};
|
|
};
|
|
|
|
const result = await api.session.get({ path: { id: sessionID } });
|
|
return result.data?.parentID != null;
|
|
} catch {
|
|
// If we can't determine, assume it's NOT a sub-agent (safe default)
|
|
return false;
|
|
}
|
|
} |