import { createHash, randomUUID } from "node:crypto"; import type { ExtensionContext } from "@earendil-works/pi-coding-agent"; import { reconstructFromEntries } from "./replay.ts"; import { loadStateFile, resolveStorePath, type StoreFileResult } from "./store-file.ts"; import { atomicWriteText, withFileLock } from "./transaction-file.ts"; import { assembleBoundedContext, selectSummarySnapshot, validateSummary, type SummaryPolicy, type SummarySnapshot } from "./context-policy.ts"; export type Grounding = { capturedAt: string; model: string; contextInfo: string }; export type BtwAttempt = { id: string; mode: "quick" | "deep"; answer: string; grounding: Grounding; toolsUsed?: string[]; error?: string; }; export type BtwEntry = { id: string; question: string; attempts: BtwAttempt[]; promotedAttemptIds?: string[]; }; export type BtwSummarySourceRef = { entryId: string; attemptId: string }; export type BtwUsageTotals = { input: number; output: number; cacheRead: number; cacheWrite: number; totalTokens: number; reasoning?: number; cacheWrite1h?: number; cost: { input: number; output: number; cacheRead: number; cacheWrite: number; total: number }; }; export type BtwSummary = { text: string; throughEntryId: string; source: BtwSummarySourceRef[]; sourceHash: string; createdAt: string; model: string; }; export type BtwSummaryMeter = { runIds: string[]; requests: number; committed: number; stale: number; failed: number; aborted: number; usageKnownRuns: number; usage: BtwUsageTotals; }; export type BtwThread = { id: string; createdAt: string; entries: BtwEntry[]; summary?: BtwSummary; summaryMeter?: BtwSummaryMeter }; export type BtwThreadV4 = { id: string; createdAt: string; entries: BtwEntry[] }; /** Strict pre-v4 flat records, retained only for old file and session replay input. */ export type FlatEntry = { id: string; mode: "quick" | "deep"; question: string; answer: string; grounding: Grounding; toolsUsed?: string[]; promoted?: boolean; error?: string; }; export type FlatThread = { id: string; createdAt: string; entries: FlatEntry[] }; export type BtwStateV1 = { version: 1; threads: FlatThread[]; activeThreadId?: string }; export type BtwStateV2 = { version: 2; revision: number; threads: FlatThread[]; activeThreadId?: string }; export type BtwScopeV3 = { id: string; sessionId: string | null; startedAtLeafId: string | null; kind: "session" | "tree" | "legacy"; createdAt: string; threads: FlatThread[]; activeThreadId?: string; }; export type BtwStateV3 = { version: 3; revision: number; scopes: BtwScopeV3[] }; export type BtwScopeV4 = Omit & { threads: BtwThreadV4[] }; export type BtwStateV4 = { version: 4; revision: number; scopes: BtwScopeV4[] }; export type BtwScope = Omit & { threads: BtwThread[] }; export type BtwState = { version: 5; revision: number; scopes: BtwScope[] }; export type ScopeToken = { scopeId: string; epoch: number }; export type HistorySnapshot = { scopeId: string; sessionId: string | null; scopeKind: BtwScope["kind"]; scopeCreatedAt: string; thread: BtwThread; }; export type PromotionRef = { entryId: string; attemptId: string }; export type ThreadStats = { storedThreads: number; active: boolean; questions: number; attempts: number; successful: number; errored: number; promoted: number; summary: { present: boolean; covered: number }; meter: BtwSummaryMeter; }; export const latestAttempt = (entry: BtwEntry): BtwAttempt => entry.attempts.at(-1)!; export const isAttemptPromoted = (entry: BtwEntry, attemptId: string): boolean => entry.promotedAttemptIds?.includes(attemptId) ?? false; export type ThreadStoreOptions = { path?: string; id?: () => string; onLockContention?: () => void; }; export interface ThreadStore { reconstruct(ctx: ExtensionContext): Promise; startSession(ctx: ExtensionContext, reason: "startup" | "reload" | "new" | "resume" | "fork"): Promise; tree(ctx: ExtensionContext, oldLeafId: string | null, newLeafId: string | null): Promise; shutdown(): Promise; flush(): Promise; listThreads(): BtwThread[]; getActive(): BtwThread | null; setActive(id: string): void; newThread(): BtwThread; deleteThread(id: string): void; appendEntry(threadId: string, entry: BtwEntry): void; appendEntryIfCurrent(token: ScopeToken, threadId: string, entry: BtwEntry): boolean; /** @deprecated M3 name retained for compatibility; entry must use v4 shape. */ append(threadId: string, entry: BtwEntry): void; /** @deprecated M3 name retained for compatibility; entry must use v4 shape. */ appendIfCurrent(token: ScopeToken, threadId: string, entry: BtwEntry): boolean; appendAttempt(threadId: string, entryId: string, question: string, attempt: BtwAttempt): void; appendAttemptIfCurrent(token: ScopeToken, threadId: string, entryId: string, question: string, attempt: BtwAttempt): boolean; invalidateSummary(threadId: string, entryId: string): boolean; invalidateSummaryIfCurrent(token: ScopeToken, threadId: string, entryId: string): boolean; getSummarySnapshot(threadId: string, policy: SummaryPolicy): SummarySnapshot | null; getSummarySnapshotIfCurrent(token: ScopeToken, threadId: string, policy: SummaryPolicy): SummarySnapshot | null; buildBoundedContext(threadId: string, budget: number, excludeEntryId?: string): string; buildBoundedContextIfCurrent(token: ScopeToken, threadId: string, budget: number, excludeEntryId?: string): string; finishSummary(scopeId: string, threadId: string, runId: string, outcome: "failed" | "aborted" | { summary: BtwSummary }, usage?: BtwUsageTotals): "committed" | "stale" | "failed" | "aborted" | "duplicate"; tailDigest(threadId: string, excludeEntryId?: string): string; markAttemptPromoted(threadId: string, entryId: string, attemptId: string): void; markAttemptPromotedIfCurrent(token: ScopeToken, threadId: string, entryId: string, attemptId: string): boolean; /** Atomically marks every referenced current attempt. Existing marks are idempotent. */ markAttemptsPromotedIfCurrent(token: ScopeToken, threadId: string, refs: PromotionRef[]): boolean; stats?(): ThreadStats; /** @deprecated Promotes the entry's latest attempt. */ markPromoted(threadId: string, entryId: string): void; /** @deprecated Promotes the entry's latest attempt. */ markPromotedIfCurrent(token: ScopeToken, threadId: string, entryId: string): boolean; getAttemptIfCurrent(token: ScopeToken, threadId: string, entryId: string, attemptId: string): BtwAttempt | null; captureScopeToken(): ScopeToken | null; isCurrent(token: ScopeToken): boolean; onScopeChange(listener: () => void): () => void; history(): HistorySnapshot[]; continueThread(token: ScopeToken, snapshot: HistorySnapshot): BtwThread | null; nextId(): string; } type DeleteKnown = { id: string; attemptIds: string[] }; type Operation = | { type: "scope"; scope: BtwScope } | { type: "create"; scopeId: string; thread: BtwThread } | { type: "append-entry"; scopeId: string; threadId: string; entry: BtwEntry } | { type: "append-attempt"; scopeId: string; threadId: string; entryId: string; question: string; attempt: BtwAttempt } | { type: "delete"; scopeId: string; threadId: string; knownEntries: DeleteKnown[] } | { type: "active"; scopeId: string; threadId: string } | { type: "promote-attempt"; scopeId: string; threadId: string; entryId: string; attemptId: string } | { type: "promote-attempts"; scopeId: string; threadId: string; refs: PromotionRef[] } | { type: "invalidate-summary"; scopeId: string; threadId: string; entryId: string } | { type: "finish-summary"; scopeId: string; threadId: string; runId: string; outcome: "failed" | "aborted" | { summary: BtwSummary }; usage?: BtwUsageTotals } | { type: "seed"; scope: BtwScope } | { type: "migrate" }; class StorePersistenceError extends Error { constructor(message: string) { super(message); this.name = "StorePersistenceError"; } } const clone = (value: T): T => structuredClone(value); const emptyUsage = (): BtwUsageTotals => ({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } }); const emptyMeter = (): BtwSummaryMeter => ({ runIds: [], requests: 0, committed: 0, stale: 0, failed: 0, aborted: 0, usageKnownRuns: 0, usage: emptyUsage() }); const emptyState = (): BtwState => ({ version: 5, revision: 0, scopes: [] }); const findScope = (state: BtwState, id: string) => state.scopes.find((scope) => scope.id === id); const findThread = (scope: BtwScope | undefined, id: string) => scope?.threads.find((thread) => thread.id === id); const findEntry = (thread: BtwThread | undefined, id: string) => thread?.entries.find((entry) => entry.id === id); const sessionScopeId = (sessionId: string) => `session-${createHash("sha256").update(sessionId).digest("hex").slice(0, 24)}`; function newScope(id: string, sessionId: string | null, leaf: string | null, kind: BtwScope["kind"]): BtwScope { return { id, sessionId, startedAtLeafId: leaf, kind, createdAt: new Date().toISOString(), threads: [] }; } function flatEntryToV4(entry: FlatEntry): BtwEntry { const attemptId = `legacy-${entry.id}`; return { id: entry.id, question: entry.question, attempts: [{ id: attemptId, mode: entry.mode, answer: entry.answer, grounding: clone(entry.grounding), ...(entry.toolsUsed === undefined ? {} : { toolsUsed: [...entry.toolsUsed] }), ...(entry.error === undefined ? {} : { error: entry.error }), }], ...(entry.promoted ? { promotedAttemptIds: [attemptId] } : {}), }; } function flatThreadToV4(thread: FlatThread): BtwThread { return { id: thread.id, createdAt: thread.createdAt, entries: thread.entries.map(flatEntryToV4) }; } function v3ToV5(state: BtwStateV3): BtwState { return { version: 5, revision: state.revision, scopes: state.scopes.map((scope) => ({ ...scope, threads: scope.threads.map(flatThreadToV4) })), }; } function v4ToV5(state: BtwStateV4): BtwState { return { version: 5, revision: state.revision, scopes: state.scopes.map((scope) => ({ ...scope, threads: scope.threads.map((thread) => clone(thread)) })) }; } function legacyState(version: 1 | 2, state: BtwStateV1 | BtwStateV2): BtwState { return { version: 5, revision: version === 2 ? (state as BtwStateV2).revision : 0, scopes: state.threads.length ? [{ id: `legacy-v${version}`, sessionId: null, startedAtLeafId: null, kind: "legacy", createdAt: "", threads: state.threads.map(flatThreadToV4), ...(state.activeThreadId === undefined ? {} : { activeThreadId: state.activeThreadId }), }] : [], }; } function cleanPromotions(entry: BtwEntry): void { if (!entry.promotedAttemptIds) return; entry.promotedAttemptIds = entry.promotedAttemptIds.filter((attemptId) => entry.attempts.some((attempt) => attempt.id === attemptId)); if (!entry.promotedAttemptIds.length) delete entry.promotedAttemptIds; } function invalidateSummaryIfCovered(thread: BtwThread, entryId: string): boolean { if (!thread.summary?.source.some((ref) => ref.entryId === entryId)) return false; delete thread.summary; return true; } function revalidateSummary(thread: BtwThread): boolean { if (thread.summary && !validateSummary(thread)) { delete thread.summary; return true; } return false; } function finiteNonnegative(value: unknown): value is number { return typeof value === "number" && Number.isFinite(value) && value >= 0; } function validUsage(usage: BtwUsageTotals | undefined): usage is BtwUsageTotals { if (!usage || !usage.cost || typeof usage.cost !== "object") return false; const required = [ usage.input, usage.output, usage.cacheRead, usage.cacheWrite, usage.totalTokens, usage.cost.input, usage.cost.output, usage.cost.cacheRead, usage.cost.cacheWrite, usage.cost.total, ]; return required.every(finiteNonnegative) && (usage.reasoning === undefined || finiteNonnegative(usage.reasoning)) && (usage.cacheWrite1h === undefined || finiteNonnegative(usage.cacheWrite1h)); } /** Add atomically only when every resulting total stays serializable. */ function addUsage(target: BtwUsageTotals, usage: BtwUsageTotals): boolean { const next: BtwUsageTotals = { input: target.input + usage.input, output: target.output + usage.output, cacheRead: target.cacheRead + usage.cacheRead, cacheWrite: target.cacheWrite + usage.cacheWrite, totalTokens: target.totalTokens + usage.totalTokens, ...(target.reasoning === undefined && usage.reasoning === undefined ? {} : { reasoning: (target.reasoning ?? 0) + (usage.reasoning ?? 0) }), ...(target.cacheWrite1h === undefined && usage.cacheWrite1h === undefined ? {} : { cacheWrite1h: (target.cacheWrite1h ?? 0) + (usage.cacheWrite1h ?? 0) }), cost: { input: target.cost.input + usage.cost.input, output: target.cost.output + usage.cost.output, cacheRead: target.cost.cacheRead + usage.cost.cacheRead, cacheWrite: target.cost.cacheWrite + usage.cost.cacheWrite, total: target.cost.total + usage.cost.total, }, }; if (!validUsage(next)) return false; Object.assign(target, next); return true; } function mergeEntry(target: BtwEntry, source: BtwEntry): boolean { let changed = false; for (const attempt of source.attempts) { if (!target.attempts.some((current) => current.id === attempt.id)) { target.attempts.push(clone(attempt)); changed = true; } } for (const attemptId of source.promotedAttemptIds ?? []) { if (target.attempts.some((attempt) => attempt.id === attemptId) && !isAttemptPromoted(target, attemptId)) { (target.promotedAttemptIds ??= []).push(attemptId); changed = true; } } return changed; } function mergeThread(target: BtwThread, source: BtwThread): boolean { let changed = false; for (const incomingEntry of source.entries) { const existingEntry = findEntry(target, incomingEntry.id); if (!existingEntry) { target.entries.push(clone(incomingEntry)); changed = true; } else { changed = mergeEntry(existingEntry, incomingEntry) || changed; } } // A summary is meaningful only if it remains an exact local prefix. Prefer // the candidate that covers farther, never replace equal/later coverage. if (source.summary && validateSummary(source) && (!target.summary || !validateSummary(target) || source.summary.source.length > target.summary.source.length)) { target.summary = clone(source.summary); changed = true; } if (source.summaryMeter && !target.summaryMeter) { target.summaryMeter = clone(source.summaryMeter); changed = true; } changed = revalidateSummary(target) || changed; return changed; } function applyDelete(scope: BtwScope, operation: Extract): boolean { const index = scope.threads.findIndex((thread) => thread.id === operation.threadId); if (index < 0) return false; const thread = scope.threads[index]!; if (!thread.entries.length) { scope.threads.splice(index, 1); if (scope.activeThreadId === thread.id) delete scope.activeThreadId; return true; } const knownByEntry = new Map(operation.knownEntries.map((entry) => [entry.id, new Set(entry.attemptIds)])); let changed = false; for (const entry of [...thread.entries]) { const knownAttempts = knownByEntry.get(entry.id); if (!knownAttempts) continue; const originalLength = entry.attempts.length; entry.attempts = entry.attempts.filter((attempt) => !knownAttempts.has(attempt.id)); changed ||= originalLength !== entry.attempts.length; cleanPromotions(entry); if (!entry.attempts.length) thread.entries.splice(thread.entries.indexOf(entry), 1); } const unknownRemains = thread.entries.some((entry) => { const knownAttempts = knownByEntry.get(entry.id); return !knownAttempts || entry.attempts.some((attempt) => !knownAttempts.has(attempt.id)); }); const active = scope.activeThreadId === thread.id; if (!thread.entries.length) { if (!changed) return false; scope.threads.splice(index, 1); if (active) delete scope.activeThreadId; return true; } changed = revalidateSummary(thread) || changed; if (unknownRemains && (thread.createdAt !== "" || active)) { thread.createdAt = ""; if (active) delete scope.activeThreadId; return true; } return changed; } function apply(state: BtwState, operation: Operation): boolean { if (operation.type === "migrate") return false; if (operation.type === "scope") { if (findScope(state, operation.scope.id)) return false; state.scopes.push(clone(operation.scope)); return true; } if (operation.type === "seed") { const existingScope = findScope(state, operation.scope.id); if (!existingScope) { state.scopes.push(clone(operation.scope)); return true; } let changed = false; for (const incomingThread of operation.scope.threads) { const existingThread = findThread(existingScope, incomingThread.id); if (!existingThread) { existingScope.threads.push(clone(incomingThread)); changed = true; continue; } changed = mergeThread(existingThread, incomingThread) || changed; } return changed; } const scope = findScope(state, operation.scopeId); if (!scope) return false; switch (operation.type) { case "create": if (findThread(scope, operation.thread.id)) return false; // Keep the synchronous local object retained by callers; persistence receives // a frozen clone separately at enqueue time. scope.threads.push(operation.thread); scope.activeThreadId = operation.thread.id; return true; case "append-entry": { let thread = findThread(scope, operation.threadId); if (!thread) { thread = { id: operation.threadId, createdAt: "", entries: [] }; scope.threads.push(thread); } const existingEntry = findEntry(thread, operation.entry.id); if (!existingEntry) { // As with thread creation, retain the synchronous object supplied to the // local UI; queued persistence gets its own cloned operation. thread.entries.push(operation.entry); revalidateSummary(thread); return true; } const changed = mergeEntry(existingEntry, operation.entry); return revalidateSummary(thread) || changed; } case "append-attempt": { let thread = findThread(scope, operation.threadId); if (!thread) { thread = { id: operation.threadId, createdAt: "", entries: [] }; scope.threads.push(thread); } let entry = findEntry(thread, operation.entryId); if (!entry) { entry = { id: operation.entryId, question: operation.question, attempts: [] }; thread.entries.push(entry); } if (entry.attempts.some((attempt) => attempt.id === operation.attempt.id)) return false; entry.attempts.push(clone(operation.attempt)); revalidateSummary(thread); return true; } case "delete": return applyDelete(scope, operation); case "active": if (!findThread(scope, operation.threadId) || scope.activeThreadId === operation.threadId) return false; scope.activeThreadId = operation.threadId; return true; case "promote-attempt": { const entry = findEntry(findThread(scope, operation.threadId), operation.entryId); if (!entry || !entry.attempts.some((attempt) => attempt.id === operation.attemptId) || isAttemptPromoted(entry, operation.attemptId)) return false; (entry.promotedAttemptIds ??= []).push(operation.attemptId); return true; } case "promote-attempts": { const thread = findThread(scope, operation.threadId); const uniqueRefs = new Set(operation.refs.map((ref) => `${ref.entryId}\u0000${ref.attemptId}`)); // Validate the complete batch before changing any local state. Duplicate // refs are malformed rather than silently becoming multiple operations. if (!thread || !operation.refs.length || uniqueRefs.size !== operation.refs.length || operation.refs.some((ref) => !findEntry(thread, ref.entryId)?.attempts.some((attempt) => attempt.id === ref.attemptId))) return false; let changed = false; for (const ref of operation.refs) { const entry = findEntry(thread, ref.entryId)!; if (!isAttemptPromoted(entry, ref.attemptId)) { (entry.promotedAttemptIds ??= []).push(ref.attemptId); changed = true; } } return changed; } case "invalidate-summary": { const thread = findThread(scope, operation.threadId); return thread ? invalidateSummaryIfCovered(thread, operation.entryId) : false; } case "finish-summary": { const thread = findThread(scope, operation.threadId); if (!thread) return false; const meter = thread.summaryMeter ??= emptyMeter(); if (meter.runIds.includes(operation.runId)) return false; meter.runIds.push(operation.runId); meter.requests++; if (validUsage(operation.usage) && addUsage(meter.usage, operation.usage)) { meter.usageKnownRuns++; } if (operation.outcome === "failed") meter.failed++; else if (operation.outcome === "aborted") meter.aborted++; else { const candidate = operation.outcome.summary; const currentLength = validateSummary(thread) ? thread.summary!.source.length : 0; const valid = candidate.text.length > 0 && candidate.source.length > currentLength && candidate.source.length <= thread.entries.length && candidate.throughEntryId === candidate.source.at(-1)?.entryId && candidate.source.every((ref, index) => thread.entries[index]?.id === ref.entryId && thread.entries[index]?.attempts.at(-1)?.id === ref.attemptId) && // Source hash is rechecked through the policy module's summary validator. validateSummary({ entries: thread.entries, summary: candidate } as BtwThread); if (valid) { thread.summary = clone(candidate); meter.committed++; } else meter.stale++; } return true; } } } function diskState(result: StoreFileResult): { state: BtwState; migrated: boolean } | null { switch (result.kind) { case "v5": return { state: clone(result.state), migrated: false }; case "v4": return { state: v4ToV5(result.state), migrated: true }; case "v3": return { state: v3ToV5(result.state), migrated: true }; case "v2": return { state: legacyState(2, result.state), migrated: true }; case "v1": return { state: legacyState(1, result.state), migrated: true }; default: return null; } } export function createThreadStore(options: ThreadStoreOptions = {}): ThreadStore { let state = emptyState(); let currentScopeId: string | null = null; let epoch = 0; let storePath = options.path ?? null; let queue: Promise = Promise.resolve(); let failure: unknown = null; let migrationPending = false; let notify: ((message: string) => void) | null = null; let warned = false; const nextRandomId = options.id ?? randomUUID; const listeners = new Set<() => void>(); const current = () => currentScopeId ? findScope(state, currentScopeId) : undefined; const invalidate = () => { if (currentScopeId === null) return; currentScopeId = null; epoch++; for (const listener of [...listeners]) { try { listener(); } catch { /* listener failures are observational */ } } }; const select = (scopeId: string) => { if (currentScopeId === scopeId) return; invalidate(); currentScopeId = scopeId; }; const noteFailure = (error: unknown) => { if (!failure) failure = error; if (!warned) { warned = true; try { notify?.("btw: couldn't save threads; keeping this session's in-memory threads only"); } catch {} } }; const targetPath = () => storePath ?? resolveStorePath(process.cwd()); const write = async (path: string, operation: Operation): Promise => { await withFileLock(path, async () => { const loaded = loadStateFile(path); if (loaded.kind === "corrupt" || loaded.kind === "future") { throw new StorePersistenceError(`refusing to overwrite ${loaded.kind} btw store`); } const source = diskState(loaded); const disk = source?.state ?? emptyState(); const changed = apply(disk, operation); if (!changed && !source?.migrated) return; if (disk.revision >= Number.MAX_SAFE_INTEGER) { throw new StorePersistenceError("btw store revision is exhausted; refusing to overwrite the store"); } disk.revision++; await atomicWriteText(path, JSON.stringify(disk)); }, { onContention: options.onLockContention }); }; const enqueue = (operation: Operation) => { const path = targetPath(); const frozen = clone(operation); migrationPending = false; if (failure) return; queue = queue.then(() => failure ? undefined : write(path, frozen), () => undefined).catch(noteFailure); }; const mutate = (operation: Operation) => { if (apply(state, operation)) enqueue(operation); }; const drain = async () => { while (true) { const observed = queue; await observed; if (observed === queue) return; } }; const load = async (ctx: ExtensionContext) => { invalidate(); await drain(); storePath = options.path ?? resolveStorePath((ctx as { cwd?: string }).cwd ?? process.cwd()); notify = (message) => ctx.ui.notify(message, "error"); failure = null; warned = false; migrationPending = false; const loaded = loadStateFile(storePath); const source = diskState(loaded); if (source) { state = source.state; migrationPending = source.migrated; return; } state = emptyState(); if (loaded.kind === "missing") { try { const replay = reconstructFromEntries(ctx.sessionManager.getEntries()).state; if (!replay.threads.length) return; const sessionId = ctx.sessionManager.getSessionId(); const seeded = newScope( `legacy-session-${createHash("sha256").update(sessionId).digest("hex").slice(0, 24)}`, sessionId, ctx.sessionManager.getLeafId(), "legacy", ); seeded.threads = replay.threads.map(flatThreadToV4); if (replay.activeThreadId) seeded.activeThreadId = replay.activeThreadId; apply(state, { type: "seed", scope: seeded }); enqueue({ type: "seed", scope: seeded }); } catch { // Legacy session replay is best effort and never poisons fresh storage. } } else if (loaded.kind === "corrupt" || loaded.kind === "future") { noteFailure(new StorePersistenceError(`refusing to overwrite ${loaded.kind} btw store`)); } }; return { reconstruct: load, async startSession(ctx, reason) { await load(ctx); const sessionId = ctx.sessionManager.getSessionId(); if (reason === "startup" || reason === "reload" || reason === "resume") { const existing = state.scopes.filter((scope) => scope.sessionId === sessionId && scope.kind !== "legacy").at(-1); if (existing) { select(existing.id); return; } } const suffix = reason === "new" || reason === "fork" ? `-${nextRandomId()}` : ""; const fresh = newScope( sessionScopeId(sessionId) + suffix, sessionId, ctx.sessionManager.getLeafId(), "session", ); mutate({ type: "scope", scope: fresh }); select(fresh.id); }, async tree(ctx, oldLeafId, newLeafId) { if (oldLeafId === newLeafId) return; invalidate(); const fresh = newScope(`tree-${nextRandomId()}`, ctx.sessionManager.getSessionId(), newLeafId, "tree"); mutate({ type: "scope", scope: fresh }); select(fresh.id); }, async shutdown() { invalidate(); await this.flush().catch(() => {}); }, async flush() { if (migrationPending && !failure) enqueue({ type: "migrate" }); await drain(); if (failure) throw failure; }, listThreads: () => current()?.threads ?? [], getActive: () => { const scope = current(); return scope?.activeThreadId ? findThread(scope, scope.activeThreadId) ?? null : null; }, setActive(threadId) { if (currentScopeId) mutate({ type: "active", scopeId: currentScopeId, threadId }); }, newThread() { if (!currentScopeId) throw new StorePersistenceError("no writable btw session scope"); const thread: BtwThread = { id: `t${nextRandomId()}`, createdAt: new Date().toISOString(), entries: [] }; mutate({ type: "create", scopeId: currentScopeId, thread }); return thread; }, deleteThread(threadId) { const thread = findThread(current(), threadId); if (!thread || !currentScopeId) return; mutate({ type: "delete", scopeId: currentScopeId, threadId, knownEntries: thread.entries.map((entry) => ({ id: entry.id, attemptIds: entry.attempts.map((attempt) => attempt.id) })), }); }, appendEntry(threadId, entry) { if (currentScopeId) mutate({ type: "append-entry", scopeId: currentScopeId, threadId, entry }); }, appendEntryIfCurrent(token, threadId, entry) { if (!this.isCurrent(token) || !findThread(current(), threadId)) return false; this.appendEntry(threadId, entry); return true; }, append(threadId, entry) { this.appendEntry(threadId, entry); }, appendIfCurrent(token, threadId, entry) { return this.appendEntryIfCurrent(token, threadId, entry); }, appendAttempt(threadId, entryId, question, attempt) { if (currentScopeId) mutate({ type: "append-attempt", scopeId: currentScopeId, threadId, entryId, question, attempt }); }, appendAttemptIfCurrent(token, threadId, entryId, question, attempt) { if (!this.isCurrent(token) || !findEntry(findThread(current(), threadId), entryId)) return false; this.appendAttempt(threadId, entryId, question, attempt); return true; }, invalidateSummary(threadId, entryId) { if (!currentScopeId) return false; const thread = findThread(current(), threadId); if (!thread?.summary?.source.some((ref) => ref.entryId === entryId)) return false; mutate({ type: "invalidate-summary", scopeId: currentScopeId, threadId, entryId }); return true; }, invalidateSummaryIfCurrent(token, threadId, entryId) { return this.isCurrent(token) ? this.invalidateSummary(threadId, entryId) : false; }, getSummarySnapshot(threadId, policy) { const thread = findThread(current(), threadId); return thread ? selectSummarySnapshot(thread, policy) : null; }, getSummarySnapshotIfCurrent(token, threadId, policy) { return this.isCurrent(token) ? this.getSummarySnapshot(threadId, policy) : null; }, buildBoundedContext(threadId, budget, excludeEntryId) { const thread = findThread(current(), threadId); return thread ? assembleBoundedContext(thread, budget, excludeEntryId) : ""; }, buildBoundedContextIfCurrent(token, threadId, budget, excludeEntryId) { return this.isCurrent(token) ? this.buildBoundedContext(threadId, budget, excludeEntryId) : ""; }, finishSummary(scopeId, threadId, runId, outcome, usage) { const thread = findThread(findScope(state, scopeId), threadId); if (!thread) return "stale"; if (thread.summaryMeter?.runIds.includes(runId)) return "duplicate"; let status: "committed" | "stale" | "failed" | "aborted" = outcome === "failed" ? "failed" : outcome === "aborted" ? "aborted" : "stale"; if (typeof outcome === "object") { const candidate = outcome.summary; const currentLength = validateSummary(thread) ? thread.summary!.source.length : 0; const valid = candidate.source.length > currentLength && candidate.text.length > 0 && candidate.throughEntryId === candidate.source.at(-1)?.entryId && validateSummary({ entries: thread.entries, summary: candidate } as BtwThread); status = valid ? "committed" : "stale"; } mutate({ type: "finish-summary", scopeId, threadId, runId, outcome, ...(usage ? { usage } : {}) }); return status; }, tailDigest(threadId, excludeEntryId) { return findThread(current(), threadId)?.entries .filter((entry) => entry.id !== excludeEntryId) .map((entry) => { const attempt = latestAttempt(entry); return `Q: ${entry.question}\nA: ${attempt.error ? `(error: ${attempt.error})` : attempt.answer}`; }) .join("\n\n") ?? ""; }, markAttemptPromoted(threadId, entryId, attemptId) { if (currentScopeId) mutate({ type: "promote-attempt", scopeId: currentScopeId, threadId, entryId, attemptId }); }, markAttemptPromotedIfCurrent(token, threadId, entryId, attemptId) { return this.markAttemptsPromotedIfCurrent(token, threadId, [{ entryId, attemptId }]); }, markAttemptsPromotedIfCurrent(token, threadId, refs) { if (!this.isCurrent(token) || !currentScopeId || !refs.length || new Set(refs.map((ref) => `${ref.entryId}\u0000${ref.attemptId}`)).size !== refs.length) return false; const thread = findThread(current(), threadId); // Successful/nonempty/unpromoted is a send-side precondition too. Keep this // guard here so a late UI completion cannot mark stale or failed attempts. if (!thread || refs.some((ref) => { const entry = findEntry(thread, ref.entryId); const attempt = entry?.attempts.find((item) => item.id === ref.attemptId); return !entry || !attempt || Boolean(attempt.error) || !attempt.answer.trim(); })) return false; mutate({ type: "promote-attempts", scopeId: currentScopeId, threadId, refs }); return true; }, stats() { const scope = current(); const active = this.getActive(); if (!scope || !active) return { storedThreads: scope?.threads.length ?? 0, active: false, questions: 0, attempts: 0, successful: 0, errored: 0, promoted: 0, summary: { present: false, covered: 0 }, meter: emptyMeter() }; const attempts = active.entries.flatMap((entry) => entry.attempts); return { storedThreads: scope.threads.length, active: true, questions: active.entries.length, attempts: attempts.length, successful: attempts.filter((attempt) => !attempt.error && Boolean(attempt.answer.trim())).length, errored: attempts.filter((attempt) => Boolean(attempt.error)).length, promoted: active.entries.reduce((total, entry) => total + (entry.promotedAttemptIds?.length ?? 0), 0), summary: { present: Boolean(active.summary), covered: active.summary?.source.length ?? 0 }, meter: clone(active.summaryMeter ?? emptyMeter()) }; }, markPromoted(threadId, entryId) { const entry = findEntry(findThread(current(), threadId), entryId); if (entry) this.markAttemptPromoted(threadId, entryId, latestAttempt(entry).id); }, markPromotedIfCurrent(token, threadId, entryId) { const entry = findEntry(findThread(current(), threadId), entryId); return entry ? this.markAttemptPromotedIfCurrent(token, threadId, entryId, latestAttempt(entry).id) : false; }, getAttemptIfCurrent(token, threadId, entryId, attemptId) { if (!this.isCurrent(token)) return null; return findEntry(findThread(current(), threadId), entryId)?.attempts.find((attempt) => attempt.id === attemptId) ?? null; }, captureScopeToken: () => currentScopeId ? { scopeId: currentScopeId, epoch } : null, isCurrent: (token) => token.scopeId === currentScopeId && token.epoch === epoch, onScopeChange(listener) { listeners.add(listener); return () => listeners.delete(listener); }, history: () => state.scopes .flatMap((scope) => scope.threads .filter((thread) => thread.entries.length && !(scope.id === currentScopeId && thread.id === scope.activeThreadId)) .map((thread) => ({ scopeId: scope.id, sessionId: scope.sessionId, scopeKind: scope.kind, scopeCreatedAt: scope.createdAt, thread: clone(thread), }))) .sort((left, right) => right.thread.createdAt.localeCompare(left.thread.createdAt)), continueThread(token, snapshot) { if (!this.isCurrent(token) || !currentScopeId) return null; const scope = current(); if (!scope) return null; const thread = clone(snapshot.thread); thread.id = `t${nextRandomId()}`; thread.createdAt = new Date().toISOString(); mutate({ type: "create", scopeId: scope.id, thread }); return thread; }, nextId: () => `e${nextRandomId()}`, }; }