mirror of
https://github.com/sdwolf4103/opencode-working-memory.git
synced 2026-06-02 06:19:36 +02:00
feat(memory-diag): add memory command detail
This commit is contained in:
@@ -9,7 +9,7 @@ export function usage(): string {
|
||||
memory-diag rejected [--workspace <path>] [--verbose] [--json]
|
||||
memory-diag missing [--workspace <path>] [--verbose] [--json]
|
||||
memory-diag explain [memory-id] [--workspace <path>] [--raw]
|
||||
memory-diag commands [--workspace <path>] [--verbose] [--json]
|
||||
memory-diag commands [--workspace <path>] [--verbose] [--json] [--memory <id>]
|
||||
memory-diag quality [--workspace <path>] [--verbose] [--json] [--raw] [--no-emoji]
|
||||
memory-diag revert (--memory <replacement-id> | --event <event-id>) [--workspace <path>] [--apply]
|
||||
|
||||
@@ -121,7 +121,7 @@ export function parseArgs(argv: string[]): ParsedArgs {
|
||||
if (command !== "audit" && options.migration) {
|
||||
return error(`${command} does not accept --migration`);
|
||||
}
|
||||
if (command !== "explain" && command !== "revert" && options.memory) {
|
||||
if (command !== "explain" && command !== "revert" && command !== "commands" && options.memory) {
|
||||
return error(`${command} does not accept --memory`);
|
||||
}
|
||||
if (command !== "revert" && options.event) return error(`${command} does not accept --event`);
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { queryEvidenceEvents, type EvidenceEventV1, type EvidenceOutcome } from "../../../src/evidence-log.ts";
|
||||
import { workspaceKey, workspaceMemoryPath } from "../../../src/paths.ts";
|
||||
import type { WorkspaceMemoryStore } from "../../../src/types.ts";
|
||||
import { accountWorkspaceMemoryRender } from "../../../src/workspace-memory.ts";
|
||||
import { readJSONFile } from "../io.ts";
|
||||
import { objectFromCounts, sortedCounts } from "../text.ts";
|
||||
import type { CliOptions, CommandResult } from "../types.ts";
|
||||
import { normalizedStore } from "../workspace-snapshot.ts";
|
||||
|
||||
type CommandKind = "reinforce" | "replace";
|
||||
|
||||
@@ -33,6 +38,42 @@ type MemoryCommandSummary = {
|
||||
}>;
|
||||
};
|
||||
|
||||
type MemoryCommandDetail = {
|
||||
version: 1;
|
||||
generatedAt: string;
|
||||
memoryId: string;
|
||||
current: {
|
||||
present: boolean;
|
||||
status?: string;
|
||||
renderStatus?: "rendered" | "not_rendered" | "unknown";
|
||||
type?: string;
|
||||
source?: string;
|
||||
};
|
||||
summary: {
|
||||
attempts: number;
|
||||
reinforced: number;
|
||||
rejectedOrBlocked: number;
|
||||
windowBlocked: number;
|
||||
blocksByReason: Record<string, number>;
|
||||
blockDetailsMissing: number;
|
||||
refs: string[];
|
||||
sameSessionCrossUtcDayBlocks: number;
|
||||
};
|
||||
events: Array<{
|
||||
eventId: string;
|
||||
createdAt: string;
|
||||
outcome: EvidenceOutcome;
|
||||
ref?: string;
|
||||
blockReason?: string;
|
||||
reasonCodes: string[];
|
||||
attemptedAtIso?: string;
|
||||
lastReinforcedAtIso?: string;
|
||||
crossUtcDay?: boolean | "unknown";
|
||||
producerVersion?: string;
|
||||
instrumentationVersion?: number;
|
||||
}>;
|
||||
};
|
||||
|
||||
const INVALID_COMMAND_REASONS = new Set([
|
||||
"invalid_memory_command",
|
||||
"invalid_memory_ref",
|
||||
@@ -66,6 +107,124 @@ function refFromEvent(event: EvidenceEventV1): string | undefined {
|
||||
return typeof ref === "string" ? ref : undefined;
|
||||
}
|
||||
|
||||
function isReinforcementEvent(event: EvidenceEventV1): boolean {
|
||||
return event.type === "memory_reinforced";
|
||||
}
|
||||
|
||||
function stringDetail(event: EvidenceEventV1, key: string): string | undefined {
|
||||
const value = event.details?.[key];
|
||||
return typeof value === "string" && value.length > 0 ? value : undefined;
|
||||
}
|
||||
|
||||
function isRejectedOrBlocked(event: EvidenceEventV1): boolean {
|
||||
return event.outcome === "rejected" || hasReason(event, "reinforcement_window_blocked");
|
||||
}
|
||||
|
||||
function blockReasonFor(event: EvidenceEventV1): string | undefined {
|
||||
if (!isRejectedOrBlocked(event)) return undefined;
|
||||
return stringDetail(event, "blockReason") ?? "unknown";
|
||||
}
|
||||
|
||||
function isCrossUtcDay(attemptedAtIso: string | undefined, lastReinforcedAtIso: string | undefined): boolean | "unknown" {
|
||||
if (!attemptedAtIso || !lastReinforcedAtIso) return "unknown";
|
||||
const attempted = new Date(attemptedAtIso);
|
||||
const lastReinforced = new Date(lastReinforcedAtIso);
|
||||
if (Number.isNaN(attempted.getTime()) || Number.isNaN(lastReinforced.getTime())) return "unknown";
|
||||
return attempted.toISOString().slice(0, 10) !== lastReinforced.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
async function currentMemoryStatus(root: string, memoryId: string): Promise<MemoryCommandDetail["current"]> {
|
||||
const rawStore = await readJSONFile<WorkspaceMemoryStore>(await workspaceMemoryPath(root));
|
||||
const storeRoot = rawStore?.workspace?.root ?? root;
|
||||
const storeKey = rawStore?.workspace?.key ?? await workspaceKey(root);
|
||||
const store = normalizedStore(rawStore, storeRoot, storeKey);
|
||||
const activeEntry = store.entries.find(entry => entry.id === memoryId && entry.status !== "superseded");
|
||||
const renderAccounting = accountWorkspaceMemoryRender(store);
|
||||
const renderedIds = new Set(renderAccounting.rendered.map(memory => memory.id));
|
||||
const omittedIds = new Set(renderAccounting.omitted.map(item => item.memory.id));
|
||||
const renderStatus = renderedIds.has(memoryId)
|
||||
? "rendered"
|
||||
: omittedIds.has(memoryId)
|
||||
? "not_rendered"
|
||||
: "unknown";
|
||||
|
||||
if (!activeEntry) {
|
||||
return { present: false, renderStatus };
|
||||
}
|
||||
|
||||
return {
|
||||
present: true,
|
||||
status: activeEntry.status,
|
||||
renderStatus,
|
||||
type: activeEntry.type,
|
||||
source: activeEntry.source,
|
||||
};
|
||||
}
|
||||
|
||||
function detailEventJSON(event: EvidenceEventV1): MemoryCommandDetail["events"][number] {
|
||||
const attemptedAtIso = stringDetail(event, "attemptedAtIso");
|
||||
const lastReinforcedAtIso = stringDetail(event, "lastReinforcedAtIso");
|
||||
const blocked = isRejectedOrBlocked(event);
|
||||
const blockReason = blockReasonFor(event);
|
||||
return {
|
||||
eventId: event.eventId,
|
||||
createdAt: event.createdAt,
|
||||
outcome: event.outcome,
|
||||
ref: refFromEvent(event),
|
||||
blockReason,
|
||||
reasonCodes: event.reasonCodes,
|
||||
attemptedAtIso,
|
||||
lastReinforcedAtIso,
|
||||
crossUtcDay: blocked ? isCrossUtcDay(attemptedAtIso, lastReinforcedAtIso) : undefined,
|
||||
producerVersion: event.producerVersion,
|
||||
instrumentationVersion: event.instrumentationVersion,
|
||||
};
|
||||
}
|
||||
|
||||
export async function buildMemoryCommandDetail(
|
||||
root: string,
|
||||
memoryId: string,
|
||||
events: EvidenceEventV1[],
|
||||
generatedAt = new Date().toISOString(),
|
||||
): Promise<MemoryCommandDetail> {
|
||||
const reinforcementEvents = events.filter(isReinforcementEvent);
|
||||
const blockReasonCounts = new Map<string, number>();
|
||||
const refs = new Set<string>();
|
||||
let blockDetailsMissing = 0;
|
||||
let sameSessionCrossUtcDayBlocks = 0;
|
||||
|
||||
for (const event of reinforcementEvents) {
|
||||
const ref = refFromEvent(event);
|
||||
if (ref) refs.add(ref);
|
||||
if (!isRejectedOrBlocked(event)) continue;
|
||||
|
||||
const blockReason = blockReasonFor(event) ?? "unknown";
|
||||
blockReasonCounts.set(blockReason, (blockReasonCounts.get(blockReason) ?? 0) + 1);
|
||||
if (!stringDetail(event, "blockReason")) blockDetailsMissing += 1;
|
||||
if (blockReason === "same_session" && isCrossUtcDay(stringDetail(event, "attemptedAtIso"), stringDetail(event, "lastReinforcedAtIso")) === true) {
|
||||
sameSessionCrossUtcDayBlocks += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
version: 1,
|
||||
generatedAt,
|
||||
memoryId,
|
||||
current: await currentMemoryStatus(root, memoryId),
|
||||
summary: {
|
||||
attempts: reinforcementEvents.length,
|
||||
reinforced: reinforcementEvents.filter(event => event.outcome === "reinforced").length,
|
||||
rejectedOrBlocked: reinforcementEvents.filter(isRejectedOrBlocked).length,
|
||||
windowBlocked: reinforcementEvents.filter(event => hasReason(event, "reinforcement_window_blocked")).length,
|
||||
blocksByReason: objectFromCounts(blockReasonCounts),
|
||||
blockDetailsMissing,
|
||||
refs: [...refs].sort(),
|
||||
sameSessionCrossUtcDayBlocks,
|
||||
},
|
||||
events: reinforcementEvents.map(detailEventJSON),
|
||||
};
|
||||
}
|
||||
|
||||
function latestEventJSON(event: EvidenceEventV1): MemoryCommandSummary["latestEvents"][number] {
|
||||
return {
|
||||
eventId: event.eventId,
|
||||
@@ -145,6 +304,73 @@ function formatLatestEvents(events: MemoryCommandSummary["latestEvents"]): strin
|
||||
});
|
||||
}
|
||||
|
||||
function formatInlineCounts(counts: Record<string, number>): string {
|
||||
const rows = sortedCounts(new Map(Object.entries(counts)));
|
||||
return rows.length > 0 ? rows.map(([reason, count]) => `${reason}=${count}`).join(", ") : "(none)";
|
||||
}
|
||||
|
||||
function formatCrossUtcDay(value: boolean | "unknown" | undefined): string {
|
||||
if (value === true) return "yes";
|
||||
if (value === false) return "no";
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function formatMemoryCommandDetailEvents(events: MemoryCommandDetail["events"]): string[] {
|
||||
if (events.length === 0) return [" (none)"];
|
||||
return events.map(event => {
|
||||
const ref = event.ref ? ` ref=${event.ref}` : "";
|
||||
const blockReason = event.blockReason ? ` blockReason=${event.blockReason}` : "";
|
||||
const attemptedAt = event.attemptedAtIso ? ` attemptedAt=${event.attemptedAtIso}` : "";
|
||||
const lastReinforcedAt = event.lastReinforcedAtIso ? ` lastReinforcedAt=${event.lastReinforcedAtIso}` : "";
|
||||
const crossUtcDay = event.crossUtcDay !== undefined ? ` crossUtcDay=${formatCrossUtcDay(event.crossUtcDay)}` : "";
|
||||
return ` - ${event.createdAt} outcome=${event.outcome}${ref}${blockReason}${attemptedAt}${lastReinforcedAt}${crossUtcDay} reasons=${event.reasonCodes.join(",") || "none"}`;
|
||||
});
|
||||
}
|
||||
|
||||
export function formatMemoryCommandDetail(detail: MemoryCommandDetail, options: Pick<CliOptions, "verbose"> = {}): string {
|
||||
const current = detail.current;
|
||||
const lines = [
|
||||
`Memory command diagnostics for ${detail.memoryId}`,
|
||||
"",
|
||||
"Current memory:",
|
||||
` - present: ${current.present ? "yes" : "no"}`,
|
||||
` - status: ${current.status ?? "unknown"}`,
|
||||
` - render: ${current.renderStatus ?? "unknown"}`,
|
||||
];
|
||||
|
||||
if (current.type) lines.push(` - type: ${current.type}`);
|
||||
if (current.source) lines.push(` - source: ${current.source}`);
|
||||
|
||||
lines.push("");
|
||||
if (detail.summary.attempts === 0) {
|
||||
lines.push(`No reinforcement command evidence found for ${detail.memoryId}.`);
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
lines.push(
|
||||
"Reinforcement summary:",
|
||||
` - attempts: ${detail.summary.attempts}`,
|
||||
` - reinforced: ${detail.summary.reinforced}`,
|
||||
` - rejected/blocked: ${detail.summary.rejectedOrBlocked}`,
|
||||
` - window blocked: ${detail.summary.windowBlocked}`,
|
||||
` - block reasons: ${formatInlineCounts(detail.summary.blocksByReason)}`,
|
||||
` - block details missing: ${detail.summary.blockDetailsMissing}`,
|
||||
` - same-session cross UTC day blocks: ${detail.summary.sameSessionCrossUtcDayBlocks}`,
|
||||
` - refs: ${detail.summary.refs.length > 0 ? detail.summary.refs.join(", ") : "(none)"}`,
|
||||
"",
|
||||
);
|
||||
|
||||
const eventRows = options.verbose ? detail.events : detail.events.slice(-10).reverse();
|
||||
if (!options.verbose && detail.events.length > eventRows.length) {
|
||||
lines.push(`Latest reinforcement events (showing ${eventRows.length} of ${detail.events.length}):`);
|
||||
} else {
|
||||
lines.push("Latest reinforcement events:");
|
||||
}
|
||||
lines.push(...formatMemoryCommandDetailEvents(eventRows));
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
export function formatMemoryCommandSummary(summary: MemoryCommandSummary, options: Pick<CliOptions, "verbose" | "noEmoji"> = {}): string {
|
||||
const warning = options.noEmoji ? "!" : "⚠";
|
||||
const lines = [
|
||||
@@ -176,6 +402,17 @@ export function formatMemoryCommandSummary(summary: MemoryCommandSummary, option
|
||||
|
||||
export async function runCommands(options: CliOptions): Promise<CommandResult> {
|
||||
const root = options.workspace ?? process.cwd();
|
||||
if (options.memory) {
|
||||
const events = await queryEvidenceEvents(root, { memoryId: options.memory });
|
||||
const detail = await buildMemoryCommandDetail(root, options.memory, events);
|
||||
|
||||
if (options.json) {
|
||||
return { stdout: JSON.stringify(detail, null, 2) };
|
||||
}
|
||||
|
||||
return { stdout: formatMemoryCommandDetail(detail, options) };
|
||||
}
|
||||
|
||||
const events = await queryEvidenceEvents(root);
|
||||
const summary = buildMemoryCommandSummary(events);
|
||||
|
||||
|
||||
@@ -126,6 +126,16 @@ test("commands accepts workspace json and verbose flags", () => {
|
||||
assert.equal("options" in parsed && parsed.options.verbose, true);
|
||||
});
|
||||
|
||||
test("commands accepts memory drill-down selector", () => {
|
||||
const parsed = parseArgs(["commands", "--workspace", "/tmp/workspace", "--memory", "mem-1", "--json", "--verbose"]);
|
||||
|
||||
assert.equal(parsed.ok, true);
|
||||
assert.equal("command" in parsed && parsed.command, "commands");
|
||||
assert.equal("options" in parsed && parsed.options.memory, "mem-1");
|
||||
assert.equal("options" in parsed && parsed.options.json, true);
|
||||
assert.equal("options" in parsed && parsed.options.verbose, true);
|
||||
});
|
||||
|
||||
test("revert accepts memory or event selectors and apply flag", () => {
|
||||
const byMemory = parseArgs(["revert", "--memory", "mem-new", "--workspace", "/tmp/workspace", "--apply"]);
|
||||
assert.equal(byMemory.ok, true);
|
||||
|
||||
@@ -123,6 +123,74 @@ function replacementEvidence(original: LongTermMemoryEntry, replacement: LongTer
|
||||
});
|
||||
}
|
||||
|
||||
const attributionSafetyTerms = [
|
||||
"b" + "ug",
|
||||
"fix" + "ed",
|
||||
"incor" + "rect",
|
||||
"wrong" + "ly blocked",
|
||||
"caused memory" + " loss",
|
||||
"prevent" + "ed retention",
|
||||
"should" + " allow",
|
||||
"policy" + " failure",
|
||||
"regres" + "sion",
|
||||
];
|
||||
|
||||
function assertNoAttributionSafetyTerms(text: string): void {
|
||||
for (const term of attributionSafetyTerms) {
|
||||
assert.doesNotMatch(text, new RegExp(term, "i"));
|
||||
}
|
||||
}
|
||||
|
||||
async function setupMemoryCommandDetailFixture(root: string): Promise<void> {
|
||||
await writeWorkspaceStore(root, [
|
||||
{ ...entry("mem-detail", "Detail drill-down memory remains current", "decision"), source: "explicit" as const },
|
||||
]);
|
||||
await appendEvidenceEvents(root, [
|
||||
evidence({
|
||||
type: "memory_reinforced",
|
||||
phase: "reinforcement",
|
||||
outcome: "reinforced",
|
||||
memory: { memoryId: "mem-detail", type: "decision", source: "explicit", status: "active" },
|
||||
reasonCodes: ["numbered_ref_reinforce", "reinforcement_window_allowed"],
|
||||
details: { ref: "M3", attemptedAtIso: "2026-05-12T23:55:00.000Z", reinforcedAtIso: "2026-05-12T23:55:01.000Z" },
|
||||
sessionHash: "command-session-one",
|
||||
messageHash: "command-message-one",
|
||||
}),
|
||||
evidence({
|
||||
type: "memory_reinforced",
|
||||
phase: "reinforcement",
|
||||
outcome: "rejected",
|
||||
memory: { memoryId: "mem-detail", type: "decision", source: "explicit", status: "active" },
|
||||
reasonCodes: ["numbered_ref_reinforce", "reinforcement_window_blocked", "reinforcement_block_same_session"],
|
||||
details: {
|
||||
ref: "M3",
|
||||
blockReason: "same_session",
|
||||
attemptedAtIso: "2026-05-13T00:05:00.000Z",
|
||||
lastReinforcedAtIso: "2026-05-12T23:55:01.000Z",
|
||||
},
|
||||
sessionHash: "command-session-one",
|
||||
messageHash: "command-message-two",
|
||||
}),
|
||||
evidence({
|
||||
type: "memory_reinforced",
|
||||
phase: "reinforcement",
|
||||
outcome: "rejected",
|
||||
memory: { memoryId: "mem-detail", type: "decision", source: "explicit", status: "active" },
|
||||
reasonCodes: ["numbered_ref_reinforce", "reinforcement_window_blocked"],
|
||||
details: { ref: "M3", attemptedAtIso: "2026-05-13T00:06:00.000Z" },
|
||||
sessionHash: "command-session-two",
|
||||
messageHash: "command-message-three",
|
||||
}),
|
||||
evidence({
|
||||
type: "render_selected",
|
||||
phase: "render",
|
||||
outcome: "rendered",
|
||||
memory: { memoryId: "mem-detail", type: "decision", source: "explicit", status: "active" },
|
||||
reasonCodes: ["within_caps", "within_char_budget"],
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
test("status handles missing workspace store as empty", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "opencode-memory-diag-missing-health-"));
|
||||
try {
|
||||
@@ -402,6 +470,93 @@ test("memory-diag commands json exposes protected replacement counts", async ()
|
||||
}
|
||||
});
|
||||
|
||||
test("memory-diag commands memory selector prints reinforcement detail", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "opencode-memory-diag-commands-memory-"));
|
||||
try {
|
||||
await setupMemoryCommandDetailFixture(root);
|
||||
|
||||
const stdout = await runMemoryDiag(["commands", "--memory", "mem-detail", "--workspace", root]);
|
||||
|
||||
assert.match(stdout, /Memory command diagnostics for mem-detail/);
|
||||
assert.match(stdout, /Current memory:/);
|
||||
assert.match(stdout, /status: active/);
|
||||
assert.match(stdout, /render: rendered/);
|
||||
assert.match(stdout, /Reinforcement summary:/);
|
||||
assert.match(stdout, /attempts: 3/);
|
||||
assert.match(stdout, /reinforced: 1/);
|
||||
assert.match(stdout, /rejected\/blocked: 2/);
|
||||
assert.match(stdout, /window blocked: 2/);
|
||||
assert.match(stdout, /block reasons: same_session=1, unknown=1/);
|
||||
assert.match(stdout, /block details missing: 1/);
|
||||
assert.match(stdout, /same-session cross UTC day blocks: 1/);
|
||||
assert.match(stdout, /refs: M3/);
|
||||
assert.match(stdout, /crossUtcDay=yes/);
|
||||
assert.doesNotMatch(stdout, /render_selected/);
|
||||
assertNoAttributionSafetyTerms(stdout);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("memory-diag commands memory selector emits stable JSON", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "opencode-memory-diag-commands-memory-json-"));
|
||||
try {
|
||||
await setupMemoryCommandDetailFixture(root);
|
||||
|
||||
const stdout = await runMemoryDiag(["commands", "--memory", "mem-detail", "--workspace", root, "--json"]);
|
||||
const parsed = JSON.parse(stdout) as {
|
||||
version: 1;
|
||||
memoryId: string;
|
||||
current: { present: boolean; status?: string; renderStatus?: string };
|
||||
summary: {
|
||||
attempts: number;
|
||||
reinforced: number;
|
||||
rejectedOrBlocked: number;
|
||||
blocksByReason: Record<string, number>;
|
||||
blockDetailsMissing: number;
|
||||
sameSessionCrossUtcDayBlocks: number;
|
||||
};
|
||||
events: Array<{ eventId: string; outcome: string; blockReason?: string; crossUtcDay?: boolean | "unknown" }>;
|
||||
};
|
||||
|
||||
assert.equal(parsed.version, 1);
|
||||
assert.equal(parsed.memoryId, "mem-detail");
|
||||
assert.equal(parsed.current.present, true);
|
||||
assert.equal(parsed.current.status, "active");
|
||||
assert.equal(parsed.current.renderStatus, "rendered");
|
||||
assert.equal(parsed.summary.attempts, 3);
|
||||
assert.equal(parsed.summary.reinforced, 1);
|
||||
assert.equal(parsed.summary.rejectedOrBlocked, 2);
|
||||
assert.equal(parsed.summary.blocksByReason.same_session, 1);
|
||||
assert.equal(parsed.summary.blocksByReason.unknown, 1);
|
||||
assert.equal(parsed.summary.blockDetailsMissing, 1);
|
||||
assert.equal(parsed.summary.sameSessionCrossUtcDayBlocks, 1);
|
||||
assert.equal(parsed.events.some(event => event.blockReason === "same_session" && event.crossUtcDay === true), true);
|
||||
assert.equal(parsed.events.some(event => event.blockReason === "unknown" && event.crossUtcDay === "unknown"), true);
|
||||
assert.equal(JSON.stringify(parsed).includes("command-session"), false);
|
||||
assert.equal(JSON.stringify(parsed).includes("command-message"), false);
|
||||
assert.equal(JSON.stringify(parsed).includes("Detail drill-down memory remains current"), false);
|
||||
assertNoAttributionSafetyTerms(stdout);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("memory-diag commands memory selector reports empty evidence neutrally", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "opencode-memory-diag-commands-memory-empty-"));
|
||||
try {
|
||||
await writeWorkspaceStore(root, [entry("mem-empty", "Memory without reinforcement command evidence", "feedback")]);
|
||||
|
||||
const stdout = await runMemoryDiag(["commands", "--memory", "mem-empty", "--workspace", root]);
|
||||
|
||||
assert.match(stdout, /Memory command diagnostics for mem-empty/);
|
||||
assert.match(stdout, /No reinforcement command evidence found for mem-empty\./);
|
||||
assertNoAttributionSafetyTerms(stdout);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("memory-diag revert dry-run plans changes without mutating", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "opencode-memory-diag-revert-dry-run-"));
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user