From b99e02c76af2f45768e6eb548ce7cc8f4eec49e9 Mon Sep 17 00:00:00 2001
From: iceteaSA <171169159+iceteaSA@users.noreply.github.com>
Date: Fri, 6 Nov 2026 16:39:18 +0000
Subject: [PATCH 1/8] feat(sidebar): expose option B data and bound refreshes
Keep historian observations in bounded session memory and use indexed cache-delta counts. Share the refresh scheduler across OpenCode sidebar paths without changing their layout.
---
.../magic-context/memory/storage-memory.ts | 25 ++
.../compartment-runner-historian.test.ts | 124 ++++++++++
.../compartment-runner-historian.ts | 173 ++++++++++---
.../compartment-runner-incremental.ts | 4 +
.../compartment-runner-partial-recomp.ts | 4 +
.../compartment-runner-recomp.ts | 5 +
.../magic-context/compartment-runner-types.ts | 10 +-
.../magic-context/compartment-runner.test.ts | 8 +
.../magic-context/compartment-trigger.test.ts | 2 +
.../magic-context/compartment-trigger.ts | 7 +
.../hooks/magic-context/event-handler.test.ts | 3 +
.../src/hooks/magic-context/event-handler.ts | 2 +
.../sidebar-observations.test.ts | 59 +++++
.../magic-context/sidebar-observations.ts | 53 ++++
.../rpc-handlers-sidebar-option-b.test.ts | 148 +++++++++++
.../plugin/src/plugin/rpc-handlers.test.ts | 26 ++
packages/plugin/src/plugin/rpc-handlers.ts | 62 ++++-
packages/plugin/src/shared/rpc-types.ts | 24 +-
.../src/tui-compiled/data/context-db.ts | 11 +-
.../tui-compiled/data/refresh-scheduler.ts | 97 ++++++++
.../tui-compiled/slots/sidebar-content.tsx | 195 ++-------------
.../plugin/src/tui/data/context-db.test.ts | 19 ++
packages/plugin/src/tui/data/context-db.ts | 11 +-
.../src/tui/data/refresh-scheduler.test.ts | 149 +++++++++++
.../plugin/src/tui/data/refresh-scheduler.ts | 97 ++++++++
.../plugin/src/tui/slots/sidebar-content.tsx | 233 +++---------------
packages/plugin/src/v2/hooks/context.ts | 2 +
.../plugin/src/v2/hooks/event-loop.test.ts | 6 +
packages/plugin/src/v2/tui/index.ts | 68 +++--
.../src/v2/tui/sidebar-fallback.test.ts | 96 +++++++-
30 files changed, 1270 insertions(+), 453 deletions(-)
create mode 100644 packages/plugin/src/hooks/magic-context/sidebar-observations.test.ts
create mode 100644 packages/plugin/src/hooks/magic-context/sidebar-observations.ts
create mode 100644 packages/plugin/src/plugin/rpc-handlers-sidebar-option-b.test.ts
create mode 100644 packages/plugin/src/tui-compiled/data/refresh-scheduler.ts
create mode 100644 packages/plugin/src/tui/data/refresh-scheduler.test.ts
create mode 100644 packages/plugin/src/tui/data/refresh-scheduler.ts
diff --git a/packages/plugin/src/features/magic-context/memory/storage-memory.ts b/packages/plugin/src/features/magic-context/memory/storage-memory.ts
index e8fc82bb62..6d3b9c94a0 100644
--- a/packages/plugin/src/features/magic-context/memory/storage-memory.ts
+++ b/packages/plugin/src/features/magic-context/memory/storage-memory.ts
@@ -943,6 +943,31 @@ export function getMaxMemoryIdForProjects(
return typeof row?.max_id === "number" ? row.max_id : 0;
}
+export function countNewMemoriesForM1Union(
+ db: Database,
+ projectPaths: readonly string[],
+ afterId: number,
+ expiryCutoff: number,
+ ownIdentities?: readonly string[],
+ shareCategories?: readonly string[] | null,
+): number {
+ const identities = uniqueValues(projectPaths);
+ if (identities.length === 0) return 0;
+ const sharingFilter = buildWorkspaceMemorySqlFilter({
+ identities,
+ ownIdentities,
+ shareCategories,
+ includeClassificationFields: hasMemoryShareableColumn(db) && hasMemoryScopeColumn(db),
+ });
+ const row = db
+ .prepare(`SELECT COUNT(*) AS count FROM memories
+ WHERE project_path IN (${sqlPlaceholders(identities)}) AND id > ?
+ AND status IN ('active', 'permanent')
+ AND (expires_at IS NULL OR expires_at > ?)${sharingFilter.clause}`)
+ .get(...identities, afterId, expiryCutoff, ...sharingFilter.params) as { count: number };
+ return row.count;
+}
+
export function readNewMemoriesForM1Union(
db: Database,
projectPaths: readonly string[],
diff --git a/packages/plugin/src/hooks/magic-context/compartment-runner-historian.test.ts b/packages/plugin/src/hooks/magic-context/compartment-runner-historian.test.ts
index d322ba0fff..6d0b8aff01 100644
--- a/packages/plugin/src/hooks/magic-context/compartment-runner-historian.test.ts
+++ b/packages/plugin/src/hooks/magic-context/compartment-runner-historian.test.ts
@@ -4,6 +4,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { closeDatabase, openDatabase } from "../../features/magic-context/storage";
import { getSubagentInvocations } from "../../features/magic-context/storage-subagent-invocations";
+import { buildSidebarSnapshot } from "../../plugin/rpc-handlers";
import type { PluginContext } from "../../plugin/types";
import { clearModelsDevCache, refreshModelLimitsFromApi } from "../../shared/models-dev-cache";
import { recordPromptSessionError } from "../../shared/prompt-async-transport";
@@ -13,10 +14,90 @@ import {
runValidatedHistorianPass,
} from "./compartment-runner-historian";
import type { HiddenCompletionExecutor } from "./compartment-runner-types";
+import { clearSidebarObservations } from "./sidebar-observations";
const tempDirs: string[] = [];
const originalXdgDataHome = process.env.XDG_DATA_HOME;
+test("N1 primary rejected twice then fallback publishes; only a clean primary resets the counters", async () => {
+ const directory = createTestTempDirFromPath(join(tmpdir(), "mc-sidebar-historian-"));
+ tempDirs.push(directory);
+ process.env.XDG_DATA_HOME = directory;
+ const db = openDatabase();
+ let attempt = 0;
+ let clean = false;
+ const executor: HiddenCompletionExecutor = {
+ capabilities: { tools: false, harness: "opencode2" },
+ open: async () => ({ id: `sidebar-child-${++attempt}` }),
+ attempt: async () => {},
+ collect: async () => ({
+ text:
+ clean || attempt % 3 === 0
+ ? ''
+ : "not usable compartments",
+ reasoning: null,
+ lengthCapped: false,
+ providerId: "actual",
+ modelId: clean ? "primary" : attempt % 3 === 0 ? "fallback" : "primary",
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
+ }),
+ close: async () => {},
+ };
+ const run = () =>
+ runValidatedHistorianPass({
+ client: undefined,
+ hiddenCompletionExecutor: executor,
+ db,
+ parentSessionId: "sidebar-historian",
+ sessionDirectory: directory,
+ prompt: "Messages 1-1:\n1: U: Keep this.",
+ chunk: { startIndex: 1, endIndex: 1, lines: [{ ordinal: 1, messageId: "m1" }] },
+ priorCompartments: [],
+ sequenceOffset: 0,
+ dumpLabelBase: "sidebar",
+ model: "configured/primary",
+ fallbackModels: ["configured/fallback"],
+ });
+ for (const runs of [1, 2]) {
+ const result = await run();
+ expect(result.ok).toBe(true);
+ result.finishSidebarRun?.(true);
+ const snapshot = buildSidebarSnapshot(db, "sidebar-historian", process.cwd());
+ expect(snapshot.historianRun).toMatchObject({
+ outcome: "fallback",
+ model: "actual/fallback",
+ fallback: true,
+ primaryRejections: 2,
+ primaryAttempts: 2,
+ sinceLastClean: {
+ primaryRejections: runs * 2,
+ primaryAttempts: runs * 2,
+ fallbackRuns: runs,
+ },
+ });
+ expect(snapshot.historianRun?.finishedAt).toBeGreaterThan(0);
+ }
+ clean = true;
+ const result = await run();
+ result.finishSidebarRun?.(true);
+ expect(buildSidebarSnapshot(db, "sidebar-historian", process.cwd()).historianRun).toMatchObject(
+ {
+ outcome: "ok",
+ model: "actual/primary",
+ fallback: false,
+ primaryRejections: 0,
+ primaryAttempts: 1,
+ sinceLastClean: { primaryRejections: 0, primaryAttempts: 0, fallbackRuns: 0 },
+ },
+ );
+ const deletedRun = await run();
+ clearSidebarObservations("sidebar-historian");
+ deletedRun.finishSidebarRun?.(true);
+ expect(
+ buildSidebarSnapshot(db, "sidebar-historian", process.cwd()).historianRun,
+ ).toBeUndefined();
+});
+
afterEach(() => {
clearModelsDevCache();
closeDatabase();
@@ -41,6 +122,49 @@ test("missing v2 hidden executor names the historian or dream entry point", () =
);
});
+test("N1 recognizes a configured fallback selected inside the host transport", async () => {
+ const directory = createTestTempDirFromPath(join(tmpdir(), "mc-sidebar-host-fallback-"));
+ tempDirs.push(directory);
+ process.env.XDG_DATA_HOME = directory;
+ const db = openDatabase();
+ const executor: HiddenCompletionExecutor = {
+ capabilities: { tools: false, harness: "opencode2" },
+ open: async () => ({ id: "host-fallback" }),
+ attempt: async () => {},
+ close: async () => {},
+ collect: async () => ({
+ text: '',
+ reasoning: null,
+ lengthCapped: false,
+ providerId: "actual",
+ modelId: "fallback",
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
+ }),
+ };
+ const result = await runValidatedHistorianPass({
+ client: undefined,
+ hiddenCompletionExecutor: executor,
+ db,
+ parentSessionId: "host-fallback-parent",
+ sessionDirectory: directory,
+ prompt: "Messages 1-1:\n1: U: Keep this.",
+ chunk: { startIndex: 1, endIndex: 1, lines: [{ ordinal: 1, messageId: "m1" }] },
+ priorCompartments: [],
+ sequenceOffset: 0,
+ dumpLabelBase: "host-fallback",
+ fallbackModels: ["actual/fallback"],
+ });
+ result.finishSidebarRun?.(true);
+ expect(
+ buildSidebarSnapshot(db, "host-fallback-parent", process.cwd()).historianRun,
+ ).toMatchObject({
+ outcome: "fallback",
+ fallback: true,
+ model: "actual/fallback",
+ sinceLastClean: { fallbackRuns: 1 },
+ });
+});
+
test("historian ledger distinguishes empty, reasoning-only, length-capped and valid output", async () => {
const directory = createTestTempDirFromPath(join(tmpdir(), "mc-historian-ledger-"));
tempDirs.push(directory);
diff --git a/packages/plugin/src/hooks/magic-context/compartment-runner-historian.ts b/packages/plugin/src/hooks/magic-context/compartment-runner-historian.ts
index bd9e98befd..3ce5473ff6 100644
--- a/packages/plugin/src/hooks/magic-context/compartment-runner-historian.ts
+++ b/packages/plugin/src/hooks/magic-context/compartment-runner-historian.ts
@@ -70,6 +70,7 @@ import {
producerPromptFailureReason,
} from "./producer-window-guard";
import { estimateTokens } from "./read-session-formatting";
+import { beginSidebarHistorianRun } from "./sidebar-observations";
// Intentionally kept: historian validation failure dumps are preserved for
// debugging. They land in the project-local historian dir
@@ -317,6 +318,8 @@ export function createV1HiddenCompletionExecutor(
return {
messages,
text,
+ providerId: typeof info.providerID === "string" ? info.providerID : undefined,
+ modelId: typeof info.modelID === "string" ? info.modelID : undefined,
reasoning: text ? null : extractLatestHistorianReasoning(messages),
lengthCapped: hasLengthCappedOutput(messages),
usage: sumTokensFromChildMessages(messages),
@@ -356,44 +359,87 @@ export function historianReasoningBudgetDiagnostic(
return `historian ran out of output budget while reasoning (length-capped at ${outputTokens} tokens, no text${tokens ? `; ${formatRunTokenLog(tokens)}` : ""}) — set historian.maxTokens or route historian.model to a low-reasoning lane/variant`;
}
-export async function runValidatedHistorianPass(args: {
- client: PluginContext["client"] | undefined;
- hiddenCompletionExecutor?: HiddenCompletionExecutor;
- db: Database;
- parentSessionId: string;
- sessionDirectory: string;
- prompt: string;
- /** Use `write` to retain message-block cache markers for a later conversational turn. */
- cacheTail?: import("../../shared/historian-cache").HistorianCacheTail;
- chunk: HistorianValidationChunk;
- priorCompartments: StoredCompartmentRange[];
- sequenceOffset: number;
- dumpLabelBase: string;
- timeoutMs?: number;
- maxOutputTokens?: number;
- /** Active OpenCode historian entry, including its outbound request variant. */
- model?: ModelInput;
- fallbackModelId?: string;
- /**
- * Resolved historian fallback chain ("provider/modelID" entries). When the
- * primary historian model fails (auth, model-not-found, transient network),
- * each fallback is tried in order. The active session model is eligible
- * only when no historian primary was configured.
- */
- fallbackModels?: readonly ModelInput[];
- callbacks?: HistorianProgressCallbacks;
- /** When true, run a second editor pass after successful historian output
- * to clean low-signal U: lines and cross-compartment duplicates. If editor
- * validation fails, falls back to the draft (first-pass) result. */
- twoPass?: boolean;
- subagentKind?: SubagentKind;
- agentId?: string;
- language?: string;
-}): Promise {
+export async function runValidatedHistorianPass(
+ args: Parameters[0],
+): Promise {
const selected = selectHiddenModelStart(args.model, args.fallbackModels, (message) =>
shared.sessionLog(args.parentSessionId, message),
);
args = { ...args, model: selected.primary, fallbackModels: selected.fallbacks };
+ const attempts = { primaryAttempts: 0, primaryRejections: 0 };
+ const finishObservation = beginSidebarHistorianRun(args.parentSessionId);
+ // The sidebar names the model the run started with, after the start selection.
+ const primaryModel = toModelEntry(args.model)?.model ?? null;
+ const result = await runValidatedHistorianPassImpl(args, attempts).catch((error: unknown) => {
+ finishObservation({
+ ...attempts,
+ primaryModel,
+ outcome: "failed",
+ model: null,
+ fallback: false,
+ });
+ throw error;
+ });
+ const usedFallback =
+ result.usedFallback === true ||
+ (result.producingModel !== primaryModel &&
+ (args.fallbackModels ?? []).some(
+ (candidate) => toModelEntry(candidate)?.model === result.producingModel,
+ ));
+ let finished = false;
+ const finishSidebarRun = (published: boolean) => {
+ if (finished) return;
+ finished = true;
+ finishObservation({
+ ...attempts,
+ primaryModel,
+ outcome: published && result.ok ? (usedFallback ? "fallback" : "ok") : "failed",
+ model: published && result.ok ? (result.producingModel ?? null) : null,
+ fallback: published && result.ok && usedFallback,
+ });
+ };
+ if (!result.ok) finishSidebarRun(false);
+ return { ...result, finishSidebarRun };
+}
+
+async function runValidatedHistorianPassImpl(
+ args: {
+ client: PluginContext["client"] | undefined;
+ hiddenCompletionExecutor?: HiddenCompletionExecutor;
+ db: Database;
+ parentSessionId: string;
+ sessionDirectory: string;
+ prompt: string;
+ /** Use `write` to retain message-block cache markers for a later conversational turn. */
+ cacheTail?: import("../../shared/historian-cache").HistorianCacheTail;
+ chunk: HistorianValidationChunk;
+ priorCompartments: StoredCompartmentRange[];
+ sequenceOffset: number;
+ dumpLabelBase: string;
+ timeoutMs?: number;
+ maxOutputTokens?: number;
+ /** Active OpenCode historian entry, including its outbound request variant. */
+ model?: ModelInput;
+ fallbackModelId?: string;
+ /**
+ * Resolved historian fallback chain ("provider/modelID" entries). When the
+ * primary historian model fails (auth, model-not-found, transient network),
+ * each fallback is tried in order. The active session model is eligible
+ * only when no historian primary was configured.
+ */
+ fallbackModels?: readonly ModelInput[];
+ callbacks?: HistorianProgressCallbacks;
+ /** When true, run a second editor pass after successful historian output
+ * to clean low-signal U: lines and cross-compartment duplicates. If editor
+ * validation fails, falls back to the draft (first-pass) result. */
+ twoPass?: boolean;
+ subagentKind?: SubagentKind;
+ agentId?: string;
+ language?: string;
+ },
+ attempts: { primaryAttempts: number; primaryRejections: number },
+): Promise {
+ attempts.primaryAttempts += 1;
const firstRun = await runHistorianPrompt({
...args,
dumpLabel: `${args.dumpLabelBase}-initial`,
@@ -401,6 +447,7 @@ export async function runValidatedHistorianPass(args: {
agentId: args.agentId,
});
if (!firstRun.ok || !firstRun.result) {
+ attempts.primaryRejections += 1;
if (firstRun.refusal?.terminal)
return {
ok: false,
@@ -431,12 +478,18 @@ export async function runValidatedHistorianPass(args: {
draftValidation: firstValidation,
draftDumpPath: firstRun.dumpPath,
draftInvocationId: firstRun.invocationId ?? null,
+ draftProducingModel: firstRun.producingModel,
})
- : { ...firstValidation, invocationId: firstRun.invocationId ?? null };
+ : {
+ ...firstValidation,
+ invocationId: firstRun.invocationId ?? null,
+ producingModel: firstRun.producingModel,
+ };
cleanupHistorianDump(args.parentSessionId, firstRun.dumpPath);
return finalResult;
}
+ attempts.primaryRejections += 1;
await args.callbacks?.onRepairRetry?.(firstValidation.error ?? "invalid compartment output");
const repairPrompt = buildHistorianRepairPrompt(
args.prompt,
@@ -444,6 +497,7 @@ export async function runValidatedHistorianPass(args: {
firstValidation.error ?? "invalid compartment output",
args.language,
);
+ attempts.primaryAttempts += 1;
const repairRun = await runHistorianPrompt({
...args,
prompt: repairPrompt,
@@ -452,6 +506,7 @@ export async function runValidatedHistorianPass(args: {
agentId: args.agentId,
});
if (!repairRun.ok || !repairRun.result) {
+ attempts.primaryRejections += 1;
if (repairRun.refusal?.terminal)
return {
ok: false,
@@ -482,14 +537,20 @@ export async function runValidatedHistorianPass(args: {
draftValidation: repairValidation,
draftDumpPath: repairRun.dumpPath,
draftInvocationId: repairRun.invocationId ?? null,
+ draftProducingModel: repairRun.producingModel,
})
- : { ...repairValidation, invocationId: repairRun.invocationId ?? null };
+ : {
+ ...repairValidation,
+ invocationId: repairRun.invocationId ?? null,
+ producingModel: repairRun.producingModel,
+ };
// Keep firstRun.dumpPath (initial failure) for debugging.
// Only cleanup the successful repair run's dump.
cleanupHistorianDump(args.parentSessionId, repairRun.dumpPath);
return finalResult;
}
+ attempts.primaryRejections += 1;
return runFallbackHistorianPass({
...args,
prompt: repairPrompt,
@@ -536,6 +597,7 @@ async function runEditorPassOrFallback(args: {
draftValidation: ValidatedHistorianPassResult;
draftDumpPath?: string;
draftInvocationId?: number | null;
+ draftProducingModel?: string | null;
model?: ModelInput;
}): Promise {
shared.sessionLog(args.parentSessionId, "historian two-pass: running editor on draft");
@@ -560,7 +622,11 @@ async function runEditorPassOrFallback(args: {
error: editorRun.error,
});
// Editor failed → keep the validated draft; FK links to the draft run.
- return { ...args.draftValidation, invocationId: args.draftInvocationId ?? null };
+ return {
+ ...args.draftValidation,
+ invocationId: args.draftInvocationId ?? null,
+ producingModel: args.draftProducingModel,
+ };
}
const editorValidation = validateHistorianOutput(
@@ -577,12 +643,20 @@ async function runEditorPassOrFallback(args: {
{ error: editorValidation.error },
);
// Editor output was bad — keep editor dump for debugging.
- return { ...args.draftValidation, invocationId: args.draftInvocationId ?? null };
+ return {
+ ...args.draftValidation,
+ invocationId: args.draftInvocationId ?? null,
+ producingModel: args.draftProducingModel,
+ };
}
cleanupHistorianDump(args.parentSessionId, editorRun.dumpPath);
shared.sessionLog(args.parentSessionId, "historian two-pass: editor accepted");
- return { ...editorValidation, invocationId: editorRun.invocationId ?? null };
+ return {
+ ...editorValidation,
+ invocationId: editorRun.invocationId ?? null,
+ producingModel: editorRun.producingModel,
+ };
}
async function runHistorianPrompt(args: {
@@ -894,7 +968,19 @@ async function runHistorianPrompt(args: {
dumpLabel ?? "historian-response",
result,
);
- return { ok: true, result, dumpPath, invocationId: invocationId ?? undefined };
+ return {
+ ok: true,
+ result,
+ dumpPath,
+ invocationId: invocationId ?? undefined,
+ producingModel:
+ completion.providerId && completion.modelId
+ ? `${completion.providerId}/${completion.modelId}`
+ : (completion.attemptedModel ??
+ handle.attemptedModel ??
+ toModelEntry(modelOverride)?.model ??
+ null),
+ };
} catch (modelError: unknown) {
hiddenModelCooldowns.record(
handle?.attemptedModel ?? toModelEntry(modelOverride)?.model,
@@ -1052,7 +1138,12 @@ async function runFallbackHistorianPass(args: {
// Only cleanup the successful run's dump. Prior failed dumps
// (args.dumpPaths + earlier chain attempts) are kept for debugging.
cleanupHistorianDump(args.parentSessionId, fallbackRun.dumpPath);
- return { ...fallbackValidation, invocationId: fallbackRun.invocationId ?? null };
+ return {
+ ...fallbackValidation,
+ invocationId: fallbackRun.invocationId ?? null,
+ producingModel: fallbackRun.producingModel,
+ usedFallback: true,
+ };
}
lastError = fallbackValidation.error ?? lastError;
lastFailureReason = fallbackValidation.error ?? "parse_failed";
diff --git a/packages/plugin/src/hooks/magic-context/compartment-runner-incremental.ts b/packages/plugin/src/hooks/magic-context/compartment-runner-incremental.ts
index ec7d7281bc..b364dc918b 100644
--- a/packages/plugin/src/hooks/magic-context/compartment-runner-incremental.ts
+++ b/packages/plugin/src/hooks/magic-context/compartment-runner-incremental.ts
@@ -341,6 +341,7 @@ export async function runCompartmentAgent(deps: HiddenCompartmentRunnerDeps): Pr
// durable and already signaled, so a later throw is a failed side step, not a
// failed historian run.
let publishCommitted = false;
+ let finishSidebarRun: ((published: boolean) => void) | undefined;
let retainDrainReservationForRetryThrottle = false;
let issueNotified = false;
let drainReservation: ReturnType["reservation"] = null;
@@ -1008,6 +1009,7 @@ export async function runCompartmentAgent(deps: HiddenCompartmentRunnerDeps): Pr
twoPass: deps.historianTwoPass,
language: deps.language,
});
+ finishSidebarRun = validatedPass.finishSidebarRun;
// A window refusal here comes from a fallback too small to hold even a
// minimal prompt (every other chain model was fitted above). Treat it like
// any other failed pass: count it, notify, and keep
@@ -1414,6 +1416,7 @@ export async function runCompartmentAgent(deps: HiddenCompartmentRunnerDeps): Pr
// drop queue) is already in the transaction above; embedding registration
// and provider calls below are post-commit best-effort and must never leave
// a committed publish marked failed or unsignaled.
+ finishSidebarRun?.(true);
deps.onCompartmentStatePublished?.(sessionId);
// Inject compaction marker into OpenCode's DB.
@@ -1657,6 +1660,7 @@ export async function runCompartmentAgent(deps: HiddenCompartmentRunnerDeps): Pr
await notifyHistorianIssue(buildHistorianFailureNotice(failCount, desc.brief));
}
} finally {
+ finishSidebarRun?.(publishCommitted);
if (!completedSuccessfully) {
if (!retainDrainReservationForRetryThrottle) {
rollbackDrainReservation();
diff --git a/packages/plugin/src/hooks/magic-context/compartment-runner-partial-recomp.ts b/packages/plugin/src/hooks/magic-context/compartment-runner-partial-recomp.ts
index 0ee495898f..78bab22f8f 100644
--- a/packages/plugin/src/hooks/magic-context/compartment-runner-partial-recomp.ts
+++ b/packages/plugin/src/hooks/magic-context/compartment-runner-partial-recomp.ts
@@ -153,6 +153,7 @@ export async function executePartialRecompInternal(
}
const leaseHolderId = holderId;
updateSessionMeta(db, sessionId, { compartmentInProgress: true });
+ const sidebarRuns: ((published: boolean) => void)[] = [];
try {
// ── Snap to compartment boundaries ─────────────────────────────────
@@ -328,6 +329,7 @@ export async function executePartialRecompInternal(
}
// Clear partial-range marker — staging is now empty.
+ for (const finish of sidebarRuns.splice(0)) finish(true);
setRecompPartialRange(db, sessionId, null);
// Reset depth counters for rebuilt range so fresh compartments start
// at depth 0. Prior/tail depth is preserved.
@@ -514,6 +516,7 @@ export async function executePartialRecompInternal(
return `## Magic Recomp — Failed\n\n${renderUserFacingFailure("recomp_unavailable")}`;
}
+ if (validatedPass.finishSidebarRun) sidebarRuns.push(validatedPass.finishSidebarRun);
candidateCompartments = [
...candidateCompartments,
...(validatedPass.compartments ?? []),
@@ -558,6 +561,7 @@ export async function executePartialRecompInternal(
);
return `## Magic Recomp — Failed\n\n${renderUserFacingFailure("recomp_unavailable")}`;
} finally {
+ for (const finish of sidebarRuns) finish(false);
updateSessionMeta(db, sessionId, { compartmentInProgress: false });
// Best-effort cleanup: if staging is somehow left over without a matching
// range marker, clear it. Normal success already cleared via promoteFinal.
diff --git a/packages/plugin/src/hooks/magic-context/compartment-runner-recomp.ts b/packages/plugin/src/hooks/magic-context/compartment-runner-recomp.ts
index 3e903e2989..8d949dbb73 100644
--- a/packages/plugin/src/hooks/magic-context/compartment-runner-recomp.ts
+++ b/packages/plugin/src/hooks/magic-context/compartment-runner-recomp.ts
@@ -200,6 +200,7 @@ export async function executeContextRecompInternal(deps: CompartmentRunnerDeps):
};
// State file for the current pass — hoisted to be accessible in finally{}
updateSessionMeta(db, sessionId, { compartmentInProgress: true });
+ const sidebarRuns: ((published: boolean) => void)[] = [];
try {
const rawMessageCount = getRawSessionMessageCount(sessionId);
@@ -303,6 +304,7 @@ export async function executeContextRecompInternal(deps: CompartmentRunnerDeps):
const promoted = promoteRecompStagingWithM0Mutation(db, sessionId, leaseHolderId);
if (!promoted) return null;
+ for (const finish of sidebarRuns.splice(0)) finish(true);
// Full recomp rebuilds every compartment from message 1 onward, so
// all pre-existing compression-depth rows are stale — the compressor
@@ -599,6 +601,7 @@ export async function executeContextRecompInternal(deps: CompartmentRunnerDeps):
return `## Magic Recomp — Failed\n\n${renderUserFacingFailure("recomp_unavailable")}`;
}
+ if (validatedPass.finishSidebarRun) sidebarRuns.push(validatedPass.finishSidebarRun);
// historian_runs telemetry: one row per SUCCESSFUL recomp pass. Failure
// early-returns above are already captured in subagent_invocations; we
// keep recomp instrumentation to the clean per-pass success point to
@@ -683,6 +686,7 @@ export async function executeContextRecompInternal(deps: CompartmentRunnerDeps):
sessionLog(sessionId, "recomp publish skipped: compartment lease no longer held");
return "## Magic Recomp — Skipped\n\nAnother process acquired the compartment-state lease before recomp could publish. No state was written.";
}
+ for (const finish of sidebarRuns.splice(0)) finish(true);
// Full recomp rebuilds every compartment, so all pre-existing depth
// rows are stale. Matches partial recomp's behavior for rebuilt ranges.
afterPublish("compression-depth", () => clearCompressionDepth(db, sessionId));
@@ -776,6 +780,7 @@ export async function executeContextRecompInternal(deps: CompartmentRunnerDeps):
);
return `## Magic Recomp — Failed\n\n${renderUserFacingFailure("recomp_unavailable")}`;
} finally {
+ for (const finish of sidebarRuns) finish(false);
updateSessionMeta(db, sessionId, { compartmentInProgress: false });
}
}
diff --git a/packages/plugin/src/hooks/magic-context/compartment-runner-types.ts b/packages/plugin/src/hooks/magic-context/compartment-runner-types.ts
index caefe5f6a0..a29d83f613 100644
--- a/packages/plugin/src/hooks/magic-context/compartment-runner-types.ts
+++ b/packages/plugin/src/hooks/magic-context/compartment-runner-types.ts
@@ -280,9 +280,14 @@ export interface HistorianRunResult {
failureReason?: string;
dumpPath?: string;
invocationId?: number;
+ producingModel?: string | null;
}
-export type ValidatedHistorianPassResult =
+export type ValidatedHistorianPassResult = {
+ producingModel?: string | null;
+ usedFallback?: boolean;
+ finishSidebarRun?: (published: boolean) => void;
+} & (
| {
ok: true;
compartments: CandidateCompartment[];
@@ -312,7 +317,8 @@ export type ValidatedHistorianPassResult =
error: string;
failureReason?: string;
invocationId?: number | null;
- };
+ }
+);
export interface StoredCompartmentRange {
startMessage: number;
diff --git a/packages/plugin/src/hooks/magic-context/compartment-runner.test.ts b/packages/plugin/src/hooks/magic-context/compartment-runner.test.ts
index 56c17f9612..7f3b3e2e91 100644
--- a/packages/plugin/src/hooks/magic-context/compartment-runner.test.ts
+++ b/packages/plugin/src/hooks/magic-context/compartment-runner.test.ts
@@ -46,6 +46,7 @@ import {
} from "../../features/magic-context/storage-meta-persisted";
import { rebaseSessionCoordinates } from "../../features/magic-context/store-generation-rebase";
import { createTagger } from "../../features/magic-context/tagger";
+import { buildSidebarSnapshot } from "../../plugin/rpc-handlers";
import type { PluginContext } from "../../plugin/types";
import * as shared from "../../shared";
import { resolveHistorianModel } from "../../shared/model-resolution";
@@ -1597,6 +1598,7 @@ describe("runCompartmentAgent", () => {
const meta = getOrCreateSessionMeta(db, "ses-1");
expect(meta.compartmentInProgress).toBe(false);
+ expect(buildSidebarSnapshot(db, "ses-1", process.cwd()).historianRun?.outcome).toBe("ok");
const compartments = getCompartments(db, "ses-1");
expect(compartments).toHaveLength(1);
expect(compartments[0]?.startMessageId).toBe("m-1");
@@ -2018,6 +2020,12 @@ describe("runCompartmentAgent", () => {
).toEqual([]);
expect(loadProtectedTailMeta(db, "ses-fact-insert-rollback").priorBoundaryOrdinal).toBe(1);
expect(onPublished).not.toHaveBeenCalled();
+ expect(
+ buildSidebarSnapshot(db, "ses-fact-insert-rollback", process.cwd()).historianRun,
+ ).toMatchObject({
+ outcome: "failed",
+ model: null,
+ });
expect(
db
.prepare(
diff --git a/packages/plugin/src/hooks/magic-context/compartment-trigger.test.ts b/packages/plugin/src/hooks/magic-context/compartment-trigger.test.ts
index e4ea46bc1c..b0a8710009 100644
--- a/packages/plugin/src/hooks/magic-context/compartment-trigger.test.ts
+++ b/packages/plugin/src/hooks/magic-context/compartment-trigger.test.ts
@@ -18,6 +18,7 @@ import {
} from "../../features/magic-context/storage";
import { recordOverflowDetected } from "../../features/magic-context/storage-meta-persisted";
import type { SessionMeta } from "../../features/magic-context/types";
+import { buildSidebarSnapshot } from "../../plugin/rpc-handlers";
import * as logger from "../../shared/logger";
import { Database } from "../../shared/sqlite";
import { closeQuietly } from "../../shared/sqlite-helpers";
@@ -109,6 +110,7 @@ it("does not fire at 88.3% for a tiny capped head even when later eligible histo
expect(result.shouldFire).toBe(false);
}
expect(getHistorianFailureState(db, sessionId).failureCount).toBe(0);
+ expect(buildSidebarSnapshot(db, sessionId, process.cwd()).waitingMessages).toBe(1);
});
} finally {
if (oldNodeEnv === undefined) delete process.env.NODE_ENV;
diff --git a/packages/plugin/src/hooks/magic-context/compartment-trigger.ts b/packages/plugin/src/hooks/magic-context/compartment-trigger.ts
index 20a0e4e145..e634950ea0 100644
--- a/packages/plugin/src/hooks/magic-context/compartment-trigger.ts
+++ b/packages/plugin/src/hooks/magic-context/compartment-trigger.ts
@@ -48,6 +48,7 @@ import {
import { estimateTrueRawMessageTokens } from "./read-session-true-raw-tokens";
import { reasoningBudgetCutoff } from "./reasoning-budget";
import { modelAcceptsEmptyContent } from "./sentinel";
+import { rememberWaitingMessages } from "./sidebar-observations";
const PROACTIVE_TRIGGER_OFFSET_PERCENTAGE = 2;
const POST_DROP_TARGET_RATIO = 0.75;
@@ -398,6 +399,7 @@ function getUnsummarizedTailInfo(
const rawEligibility = getRawHistoryEligibility(db, sessionId);
if (!rawEligibility.hasRawBeyondLastCompartment) {
+ rememberWaitingMessages(sessionId, 0);
return { ...TAIL_INFO_DEFAULTS, nextStartOrdinal: rawEligibility.offset };
}
@@ -418,6 +420,11 @@ function getUnsummarizedTailInfo(
const hasProtectedEligibleHead =
boundary.offset <
Math.min(boundary.protectedTailStart, boundary.eligibleEndOrdinal);
+ rememberWaitingMessages(
+ sessionId,
+ Math.min(boundary.protectedTailStart, boundary.eligibleEndOrdinal) -
+ boundary.offset,
+ );
if (!hasProtectedEligibleHead) {
return {
diff --git a/packages/plugin/src/hooks/magic-context/event-handler.test.ts b/packages/plugin/src/hooks/magic-context/event-handler.test.ts
index 7e3dae32d4..f63c2d7a91 100644
--- a/packages/plugin/src/hooks/magic-context/event-handler.test.ts
+++ b/packages/plugin/src/hooks/magic-context/event-handler.test.ts
@@ -1,6 +1,7 @@
import { promptAsyncAndWaitForIdle } from "../../shared/prompt-async-transport";
import { drainNotifications } from "../../shared/rpc-notifications";
import { createTestTempDirFromPath } from "../../shared/test-temp-dir";
+import { rememberWaitingMessages, sidebarObservations } from "./sidebar-observations";
///
import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test";
@@ -1473,6 +1474,7 @@ describe("createEventHandler", () => {
useTempDataHome("context-event-explicit-delete-");
const deps = createDeps(new Map());
const sessionId = "ses-explicit-delete";
+ rememberWaitingMessages(sessionId, 12);
seedRetainedSession(deps.db, sessionId, Date.now() - 25 * 60 * 60 * 1000);
const before = snapshotSessionRows(deps.db, sessionId);
expect(before.compartments).toHaveLength(40);
@@ -1486,6 +1488,7 @@ describe("createEventHandler", () => {
for (const [table, rows] of Object.entries(snapshotSessionRows(deps.db, sessionId))) {
expect(rows, `${table} retained deleted session data`).toEqual([]);
}
+ expect(sidebarObservations(sessionId).waitingMessages).toBeUndefined();
});
it("retries a failed deleted-session cleanup from its durable marker", async () => {
diff --git a/packages/plugin/src/hooks/magic-context/event-handler.ts b/packages/plugin/src/hooks/magic-context/event-handler.ts
index eebec19ce1..f10f4cb54a 100644
--- a/packages/plugin/src/hooks/magic-context/event-handler.ts
+++ b/packages/plugin/src/hooks/magic-context/event-handler.ts
@@ -97,6 +97,7 @@ import { forgetRawSessionOrdinalWatermark } from "./read-session-raw";
import { invalidateTrueRawTokenCache } from "./read-session-true-raw-tokens";
import { type NotificationParams, sendStatusNotification } from "./send-session-notification";
import { isAnthropicFamilyRoute } from "./sentinel";
+import { clearSidebarObservations } from "./sidebar-observations";
import { clearMessageTokensCache } from "./transform";
import { resetDegradedCacheCount } from "./transform-postprocess-phase";
@@ -1278,6 +1279,7 @@ export function createEventHandler(deps: EventHandlerDeps) {
return;
}
+ clearSidebarObservations(sessionId);
dropSlot(sessionId, "session.deleted");
clearLkgMeasuredRequest(sessionId);
clearTrackedOpenCodeSession(sessionId);
diff --git a/packages/plugin/src/hooks/magic-context/sidebar-observations.test.ts b/packages/plugin/src/hooks/magic-context/sidebar-observations.test.ts
new file mode 100644
index 0000000000..2fecf016d7
--- /dev/null
+++ b/packages/plugin/src/hooks/magic-context/sidebar-observations.test.ts
@@ -0,0 +1,59 @@
+import { expect, test } from "bun:test";
+import {
+ beginSidebarHistorianRun,
+ clearSidebarObservations,
+ rememberWaitingMessages,
+ sidebarObservations,
+} from "./sidebar-observations";
+
+test("sidebar observations evict old sessions and deletion clears both readings", () => {
+ for (let i = 0; i < 257; i++) {
+ const id = `bounded-sidebar-${i}`;
+ rememberWaitingMessages(id, i);
+ beginSidebarHistorianRun(id)({
+ outcome: "failed",
+ model: null,
+ primaryModel: "primary",
+ fallback: false,
+ primaryAttempts: 2,
+ primaryRejections: 2,
+ });
+ }
+ expect(sidebarObservations("bounded-sidebar-0")).toEqual({
+ historianRun: undefined,
+ waitingMessages: undefined,
+ });
+ expect(sidebarObservations("bounded-sidebar-256").waitingMessages).toBe(256);
+ clearSidebarObservations("bounded-sidebar-256");
+ expect(sidebarObservations("bounded-sidebar-256")).toEqual({
+ historianRun: undefined,
+ waitingMessages: undefined,
+ });
+ for (let i = 0; i < 257; i++) clearSidebarObservations(`bounded-sidebar-${i}`);
+});
+
+test("a failed run cannot reset degraded counters even when its primary output had no rejection", () => {
+ const id = "sidebar-failed-not-clean";
+ beginSidebarHistorianRun(id)({
+ outcome: "fallback",
+ model: "fallback",
+ primaryModel: "primary",
+ fallback: true,
+ primaryAttempts: 2,
+ primaryRejections: 2,
+ });
+ beginSidebarHistorianRun(id)({
+ outcome: "failed",
+ model: null,
+ primaryModel: "primary",
+ fallback: false,
+ primaryAttempts: 1,
+ primaryRejections: 0,
+ });
+ expect(sidebarObservations(id).historianRun).toMatchObject({
+ outcome: "failed",
+ model: null,
+ sinceLastClean: { primaryAttempts: 3, primaryRejections: 2, fallbackRuns: 1 },
+ });
+ clearSidebarObservations(id);
+});
diff --git a/packages/plugin/src/hooks/magic-context/sidebar-observations.ts b/packages/plugin/src/hooks/magic-context/sidebar-observations.ts
new file mode 100644
index 0000000000..f4e4bf64e7
--- /dev/null
+++ b/packages/plugin/src/hooks/magic-context/sidebar-observations.ts
@@ -0,0 +1,53 @@
+import { BoundedSessionMap } from "../../shared/bounded-session-map";
+import type { SidebarHistorianRun } from "../../shared/rpc-types";
+
+const historianRuns = new BoundedSessionMap(256);
+const waitingMessages = new BoundedSessionMap(256);
+const generations = new BoundedSessionMap