feat(sf): persist escalation resolutions as durable memories

When an escalation is resolved (auto-mode accept or user override), write
the choice + rationale into the memories table with category="architecture".
The "[escalation:<task>] <question>. Chose: <option>. Rationale: ..."
prefix mirrors the decisions->memories backfill format so search and
de-duplication work the same way.

Why: getActiveMemoriesRanked auto-injects top memories into every
execute-task prompt, so a resolved escalation now travels forward as
implicit context across the whole project — not just the immediate
carry-forward into the next task. The artifact JSON stays as the audit
trail; the memory is the discoverable, semantically-ranked surface.

Best-effort write — never blocks resolution.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mikael Hugo 2026-05-02 21:53:56 +02:00
parent 7c6140517e
commit 00c13bc5a1

View file

@ -8,6 +8,7 @@ import { existsSync, mkdirSync, readFileSync } from "node:fs";
import { dirname, join } from "node:path"; import { dirname, join } from "node:path";
import { atomicWriteSync } from "./atomic-write.js"; import { atomicWriteSync } from "./atomic-write.js";
import { createMemory } from "./memory-store.js";
import { resolveSlicePath } from "./paths.js"; import { resolveSlicePath } from "./paths.js";
import type { TaskRow } from "./sf-db.js"; import type { TaskRow } from "./sf-db.js";
import { import {
@ -375,6 +376,26 @@ export function resolveEscalation(
}), }),
); );
// Persist as a durable memory so the choice + rationale auto-injects into
// future prompts via getActiveMemoriesRanked. Mirrors the decisions->memories
// backfill pattern (category="architecture", "[decision:<id>] ..." prefix).
// Best-effort — never block resolution if the memory write fails.
try {
const memoryContent = formatEscalationMemoryContent(art, chosenOption, rationale);
createMemory({
category: "architecture",
content: memoryContent,
confidence: 0.85,
source_unit_type: "execute-task",
source_unit_id: taskId,
});
} catch (memoryErr) {
logWarning(
"tool",
`escalation: failed to persist resolution as memory: ${(memoryErr as Error).message}`,
);
}
return { return {
status: "resolved", status: "resolved",
message: `Escalation resolved. Next ${sliceId} dispatch will run normally.`, message: `Escalation resolved. Next ${sliceId} dispatch will run normally.`,
@ -382,3 +403,28 @@ export function resolveEscalation(
chosenOption, chosenOption,
}; };
} }
/** Synthesize a 13 sentence memory line from a resolved escalation artifact.
* The "[escalation:<task>]" prefix mirrors the decisions->memories backfill
* format so de-duplication and search work the same way. */
function formatEscalationMemoryContent(
art: EscalationArtifact,
chosenOption: EscalationOption | undefined,
userRationale: string,
): string {
const choiceLabel = chosenOption
? `${chosenOption.label} (${chosenOption.id})`
: "unknown";
const rationale = userRationale.trim()
? userRationale.trim()
: art.recommendationRationale;
const tradeoffs = chosenOption?.tradeoffs?.trim();
return [
`[escalation:${art.taskId}] ${art.question}`,
`Chose: ${choiceLabel}.`,
`Rationale: ${rationale}`,
tradeoffs ? `Tradeoffs: ${tradeoffs}` : "",
]
.filter(Boolean)
.join(" ");
}