Merge compaction summary projection and incomplete-checkpoint guard
This commit is contained in:
@@ -272,9 +272,9 @@ export function newCompactionMarker(summary: string, details: CompactionDetails)
|
||||
};
|
||||
}
|
||||
|
||||
/** The ACTIVE compaction marker: the newest one in the record (chained
|
||||
* compactions — the last one rules; older markers fall inside the
|
||||
* dropped span and never render). */
|
||||
/** The ACTIVE compaction marker: the newest one with a retained-message
|
||||
* id. Its summary supersedes older markers even when their stored
|
||||
* positions remain inside its retained tail. */
|
||||
export function activeCompactionOf(messages: readonly LhMessage[]): (LhMarkerMessage & { details: CompactionDetails }) | null {
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const m = messages[i]!;
|
||||
|
||||
@@ -562,8 +562,9 @@ export function toLlmMessages(messages: readonly AgentMessage[]): Message[] {
|
||||
// THE COMPACTION CUT (owner round 14, 2026-10-05): the ACTIVE marker
|
||||
// (the newest — chained compactions rule last) names the first kept
|
||||
// message; everything before it is replaced by the marker's content
|
||||
// (header + summary) rendered ONCE, up top. The marker itself is then
|
||||
// skipped in place — it must never render twice. A cut naming a
|
||||
// (header + summary) rendered ONCE, up top. All compaction markers
|
||||
// in the retained tail are then skipped, including older summaries
|
||||
// whose stored positions still lie after the cut. A cut naming a
|
||||
// missing id degrades honestly (everything renders — never throw).
|
||||
const lhMessages = messages as readonly LhMessage[];
|
||||
const active = activeCompactionOf(lhMessages);
|
||||
@@ -604,11 +605,10 @@ export function toLlmMessages(messages: readonly AgentMessage[]): Message[] {
|
||||
// UI-only markers: plan blocks, and problem dismissals (the
|
||||
// synthesized conclude pair carries the model's face).
|
||||
if (marker.customType === "plan-block" || marker.customType === "problem-dismissed") continue;
|
||||
// The ACTIVE compaction marker already rendered as the head —
|
||||
// never twice. (Stale markers before the cut are unreachable; a
|
||||
// marker whose cut could not resolve has no head and renders as
|
||||
// its user text — honest degradation.)
|
||||
if (marker.customType === "compaction" && head !== null && marker.id === active!.id) continue;
|
||||
// The head supersedes every compaction marker, not only its own
|
||||
// stored position. Without a resolved head, preserve the existing
|
||||
// full-history fallback, including the summary text.
|
||||
if (marker.customType === "compaction" && head !== null) continue;
|
||||
out.push({
|
||||
role: "user",
|
||||
content: [{ type: "text", text: marker.content }],
|
||||
|
||||
@@ -69,7 +69,7 @@ async function subagentSummarizer(prompt: string, engine: PiEngine): Promise<{ s
|
||||
totalTokens: 0,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||
};
|
||||
const { text } = await runSubagent([], {
|
||||
const { text, newMessages } = await runSubagent([], {
|
||||
// The engine override is always present (the chat's own conversation
|
||||
// engine — owner ruling), so the role label never resolves an
|
||||
// engine; "researcher" is the least-wrong existing ModelRole label
|
||||
@@ -83,6 +83,12 @@ async function subagentSummarizer(prompt: string, engine: PiEngine): Promise<{ s
|
||||
usage = u;
|
||||
},
|
||||
});
|
||||
// A provider-declared incomplete response is not a usable checkpoint,
|
||||
// even when it contains nonempty text. Match Pi's compaction failure
|
||||
// semantics: leave the existing history and prior summary untouched.
|
||||
if (newMessages.some((message) => message.role === "assistant" && message.stopReason === "length")) {
|
||||
throw new Error("Compaction summary hit the output limit and is incomplete");
|
||||
}
|
||||
return { summary: text, usage };
|
||||
}
|
||||
|
||||
@@ -201,7 +207,14 @@ async function compact(deps: CompactionDeps, chatId: string): Promise<void> {
|
||||
spanStart = prevKept;
|
||||
priorSummary = previous.content;
|
||||
}
|
||||
const spanText = serializeForSummary(messages.slice(spanStart, cut));
|
||||
const span = messages.slice(spanStart, cut);
|
||||
// The separately supplied prior summary replaces all summary markers
|
||||
// in this span. Without a resolved prior boundary, retain the existing
|
||||
// legacy fallback rather than silently discarding summary-only context.
|
||||
const summaryMessages = priorSummary
|
||||
? span.filter((m) => m.role !== "custom" || m.customType !== "compaction")
|
||||
: span;
|
||||
const spanText = serializeForSummary(summaryMessages);
|
||||
const fullSpan = priorSummary ? `[SYSTEM NOTE — prior compaction summary, still authoritative where not superseded]\n${priorSummary}\n\n${spanText}` : spanText;
|
||||
|
||||
const prompt = buildCompactionPrompt({
|
||||
|
||||
Reference in New Issue
Block a user