Merge accepted local main into PR #184 working branch

This commit is contained in:
amosblomqvist
2026-10-27 18:12:09 +02:00
28 changed files with 1464 additions and 377 deletions
+15 -2
View File
@@ -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({
+10 -4
View File
@@ -245,10 +245,17 @@ export function validateChatSettings(settings: ChatSettings): SettingsValidation
return { ok: true };
}
export function validateModelsSettings(settings: ModelsSettings): SettingsValidation {
/** Validate introduced/replaced model selections, not unchanged saved pins.
* A disconnected subscription loses its available catalog, but its stored
* selections must not prevent repairing another slot or editing thinking.
* Only the SAME slot's provider+id can pass through; runtime admission stays
* separate. Callers without a stored section retain strict catalog checking. */
export function validateModelsSettings(settings: ModelsSettings, stored?: ModelsSettings): SettingsValidation {
for (const name of MODEL_SLOTS) {
const slot = settings[name];
if (!slot) continue;
const previous = stored?.[name]?.model;
if (slot.model && previous && slot.model.provider === previous.provider && slot.model.id === previous.id) continue;
const verdict = validateSlotSpec(name, slot);
if (!verdict.ok) return verdict;
}
@@ -280,9 +287,8 @@ function validateSlotSpec(name: ModelSlotName, slot: { model?: { provider: strin
// runtime — and the clamped value is what lands in the record. The
// server is the truth: the record shows the clamped level after write.
/** Clamp one level against one model (identity when the model is unknown
* or the level already supported — the existence gate rejects unknown
* models separately, so this only ever sees real ones in practice). */
/** Clamp one level against one model. Preserve the preference when metadata
* is unavailable for an unchanged saved pin; runtime clamps after reconnect. */
function clampLevelFor(provider: string, id: string, level: ThinkingLevel): ThinkingLevel {
const model = connectionModel(provider, id);
return model ? clampThinkingLevel(model, level) : level;