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:
parent
7c6140517e
commit
00c13bc5a1
1 changed files with 46 additions and 0 deletions
|
|
@ -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 1–3 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(" ");
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue