{"version":3,"file":"evidence.d.ts","sourceRoot":"","sources":["../../src/reflect/evidence.ts"],"names":[],"mappings":"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","sourcesContent":["import { lstat, readdir, readFile } from \"node:fs/promises\";\nimport { basename, join } from \"node:path\";\nimport type { AgentMessage } from \"@ch1nyzzz/pi-agent-core\";\nimport { loadCompiledBundle } from \"../bundle/compile.ts\";\nimport { readInboxLifecycleStates } from \"../inbox.ts\";\nimport { type EvoPaths, ensureEvoLayout } from \"../paths.ts\";\nimport type { DraftProposal } from \"../proposal.ts\";\nimport { listSessionDigests } from \"../recorder/digest.ts\";\nimport type { RecordedEvent, RecorderInboxEntry } from \"../recorder/schema.ts\";\nimport { readSessionLog, resolveStoredPayload } from \"../recorder/store.ts\";\nimport { BundleRegistry } from \"../registry/registry.ts\";\nimport { atomicWriteJson, canonicalJson, readJsonIfExists, sha256, withFileLock } from \"../storage.ts\";\nimport type { EvidenceReference, ReplayScenario } from \"../types.ts\";\n\n// Safe default for consumers that inline the corpus text into a prompt.\nconst DEFAULT_CORPUS_BYTES = 1024 * 1024;\n// Budget for the materialized on-disk corpus (prompts carry only an index there);\n// raising it costs disk, never context-window tokens. Callers opt in explicitly.\nexport const MATERIALIZED_CORPUS_BYTES = 8 * 1024 * 1024;\nconst REVIEW_CURSOR_SCHEMA_VERSION = 1;\nconst REVIEW_CURSOR_FILE = \"review-cursor.json\";\nconst SOURCE_SEPARATOR = \"\\n\\n\";\n// Percentage caps for the fixed sources. Sessions — the behavioral evidence — has no\n// weight: it receives the entire budget the fixed sources leave unused.\nconst SOURCE_WEIGHTS = {\n\tbundle: 30,\n\thistory: 15,\n\tinbox: 20,\n\tsessions: 35,\n} as const;\nconst FIXED_SOURCES = [\"bundle\", \"history\", \"inbox\"] as const;\n\nexport type EvidenceSource = keyof typeof SOURCE_WEIGHTS;\n\nexport interface EvidenceSourceSummary {\n\tbytes: number;\n\tmaxBytes: number;\n\ttruncated: boolean;\n}\n\n/** One corpus section with its structured value, used to materialize the corpus on disk. */\nexport interface EvidenceFragment {\n\tsource: EvidenceSource;\n\theading: string;\n\tvalue: unknown;\n\tsessionId?: string;\n}\n\nexport interface EvidenceReviewCursor {\n\tschemaVersion: 1;\n\tupdatedAt: string;\n\tinboxFiles: string[];\n\tsessionSequences: Record<string, number>;\n}\n\nexport interface CollectEvidenceCorpusOptions {\n\tmaxBytes?: number;\n\tmode?: \"full\" | \"incremental\";\n\treviewCursor?: EvidenceReviewCursor;\n\tcompletedSessionsOnly?: boolean;\n\tnow?: () => Date;\n}\n\nexport interface EvidenceCorpus {\n\ttext: string;\n\tbytes: number;\n\tmaxBytes: number;\n\ttruncated: boolean;\n\tmode: \"full\" | \"incremental\";\n\tevidenceDigest: string;\n\tsources: Record<EvidenceSource, EvidenceSourceSummary>;\n\tfragments: EvidenceFragment[];\n\tsessionIds: string[];\n\tinboxFiles: string[];\n\tnextReviewCursor: EvidenceReviewCursor;\n\tbundleDigest?: string;\n}\n\nexport interface LoadedReplayScenario {\n\thistory: AgentMessage[];\n\ttargetPrompt: string;\n\toldSystemPrompt: string;\n\tsystemPromptOptions: unknown;\n\tsessionIdentity: string;\n\tcwd: string;\n}\n\ninterface EvidenceFile {\n\tname: string;\n\tmtimeMs: number;\n}\n\ninterface SourceCollector {\n\tappend(heading: string, value: unknown, sessionId?: string): boolean;\n\ttext(): string;\n\tsummary(): EvidenceSourceSummary;\n\tfragments(): EvidenceFragment[];\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isMissingFile(error: unknown): boolean {\n\treturn isRecord(error) && error.code === \"ENOENT\";\n}\n\nfunction assertSafeFileName(name: string, label: string): void {\n\tif (!name || basename(name) !== name || name.includes(\"\\\\\") || name.includes(\"\\0\")) {\n\t\tthrow new Error(`${label} must be a plain file name`);\n\t}\n}\n\nfunction parseReviewCursor(value: unknown): EvidenceReviewCursor {\n\tif (!isRecord(value)) throw new Error(\"Stored evidence review cursor must be an object\");\n\tconst allowedKeys = new Set([\"schemaVersion\", \"updatedAt\", \"inboxFiles\", \"sessionSequences\"]);\n\tif (Object.keys(value).some((key) => !allowedKeys.has(key))) {\n\t\tthrow new Error(\"Stored evidence review cursor has unknown fields\");\n\t}\n\tif (\n\t\tvalue.schemaVersion !== REVIEW_CURSOR_SCHEMA_VERSION ||\n\t\ttypeof value.updatedAt !== \"string\" ||\n\t\t!Number.isFinite(Date.parse(value.updatedAt)) ||\n\t\t!Array.isArray(value.inboxFiles) ||\n\t\tvalue.inboxFiles.some((file) => typeof file !== \"string\") ||\n\t\t!isRecord(value.sessionSequences)\n\t) {\n\t\tthrow new Error(\"Stored evidence review cursor is invalid\");\n\t}\n\tconst inboxFiles = value.inboxFiles as string[];\n\tfor (const file of inboxFiles) {\n\t\tassertSafeFileName(file, \"Stored evidence review cursor inbox file\");\n\t\tif (!file.endsWith(\".json\")) throw new Error(\"Stored evidence review cursor inbox file must be JSON\");\n\t}\n\tif (new Set(inboxFiles).size !== inboxFiles.length) {\n\t\tthrow new Error(\"Stored evidence review cursor contains duplicate inbox files\");\n\t}\n\tconst sessionSequences: Record<string, number> = {};\n\tfor (const [sessionId, sequence] of Object.entries(value.sessionSequences)) {\n\t\tif (!/^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/.test(sessionId)) {\n\t\t\tthrow new Error(\"Stored evidence review cursor contains an invalid session id\");\n\t\t}\n\t\tif (typeof sequence !== \"number\" || !Number.isSafeInteger(sequence) || sequence <= 0) {\n\t\t\tthrow new Error(\"Stored evidence review cursor contains an invalid session sequence\");\n\t\t}\n\t\tsessionSequences[sessionId] = sequence;\n\t}\n\treturn {\n\t\tschemaVersion: 1,\n\t\tupdatedAt: value.updatedAt,\n\t\tinboxFiles: [...inboxFiles].sort(),\n\t\tsessionSequences: Object.fromEntries(\n\t\t\tObject.entries(sessionSequences).sort(([left], [right]) => left.localeCompare(right)),\n\t\t),\n\t};\n}\n\nfunction reviewCursorPath(paths: EvoPaths): string {\n\treturn join(paths.registry, REVIEW_CURSOR_FILE);\n}\n\nexport async function readEvidenceReviewCursor(paths: EvoPaths): Promise<EvidenceReviewCursor | undefined> {\n\tawait ensureEvoLayout(paths);\n\tconst cursor = await readJsonIfExists<unknown>(reviewCursorPath(paths));\n\treturn cursor === undefined ? undefined : parseReviewCursor(cursor);\n}\n\nfunction mergeReviewCursors(\n\texisting: EvidenceReviewCursor | undefined,\n\tcandidate: EvidenceReviewCursor,\n): EvidenceReviewCursor {\n\tconst inboxFiles = new Set([...(existing?.inboxFiles ?? []), ...candidate.inboxFiles]);\n\tconst sessionSequences: Record<string, number> = { ...(existing?.sessionSequences ?? {}) };\n\tfor (const [sessionId, sequence] of Object.entries(candidate.sessionSequences)) {\n\t\tsessionSequences[sessionId] = Math.max(sessionSequences[sessionId] ?? 0, sequence);\n\t}\n\tconst updatedAt =\n\t\texisting && Date.parse(existing.updatedAt) > Date.parse(candidate.updatedAt)\n\t\t\t? existing.updatedAt\n\t\t\t: candidate.updatedAt;\n\treturn parseReviewCursor({\n\t\tschemaVersion: 1,\n\t\tupdatedAt,\n\t\tinboxFiles: [...inboxFiles],\n\t\tsessionSequences,\n\t});\n}\n\nexport async function advanceEvidenceReviewCursor(\n\tpaths: EvoPaths,\n\tcandidate: EvidenceReviewCursor,\n): Promise<EvidenceReviewCursor> {\n\tconst validatedCandidate = parseReviewCursor(candidate);\n\treturn withFileLock(paths, \"evidence-review-cursor\", async () => {\n\t\tconst existing = await readEvidenceReviewCursor(paths);\n\t\tconst merged = mergeReviewCursors(existing, validatedCandidate);\n\t\tawait atomicWriteJson(reviewCursorPath(paths), merged);\n\t\treturn merged;\n\t});\n}\n\nfunction allocateSourceBudgets(maxBytes: number): Record<(typeof FIXED_SOURCES)[number], number> {\n\tconst separatorBytes = Buffer.byteLength(SOURCE_SEPARATOR, \"utf8\") * FIXED_SOURCES.length;\n\tconst available = Math.max(0, maxBytes - Math.min(maxBytes, separatorBytes));\n\treturn Object.fromEntries(\n\t\tFIXED_SOURCES.map((source) => [source, Math.floor((available * SOURCE_WEIGHTS[source]) / 100)]),\n\t) as Record<(typeof FIXED_SOURCES)[number], number>;\n}\n\nfunction createSourceCollector(source: EvidenceSource, maxBytes: number): SourceCollector {\n\tlet sourceText = \"\";\n\tlet truncated = false;\n\tconst collected: EvidenceFragment[] = [];\n\treturn {\n\t\tappend(heading, value, sessionId) {\n\t\t\tconst fragment = `${heading}\\n${typeof value === \"string\" ? value : JSON.stringify(value, undefined, \"\\t\")}`;\n\t\t\tconst candidate = sourceText ? `${sourceText}${SOURCE_SEPARATOR}${fragment}` : fragment;\n\t\t\tif (Buffer.byteLength(candidate, \"utf8\") > maxBytes) {\n\t\t\t\ttruncated = true;\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tsourceText = candidate;\n\t\t\tcollected.push({ source, heading, value, ...(sessionId ? { sessionId } : {}) });\n\t\t\treturn true;\n\t\t},\n\t\ttext: () => sourceText,\n\t\tsummary: () => ({ bytes: Buffer.byteLength(sourceText, \"utf8\"), maxBytes, truncated }),\n\t\tfragments: () => [...collected],\n\t};\n}\n\nasync function listEvidenceFiles(\n\tdirectory: string,\n\tsuffix: string,\n\torder: \"oldest\" | \"newest\",\n): Promise<EvidenceFile[]> {\n\tconst entries = await readdir(directory, { withFileTypes: true }).catch((error: unknown) => {\n\t\tif (isMissingFile(error)) return [];\n\t\tthrow error;\n\t});\n\tconst files = await Promise.all(\n\t\tentries\n\t\t\t.filter((entry) => entry.isFile() && entry.name.endsWith(suffix))\n\t\t\t.map(async (entry): Promise<EvidenceFile | undefined> => {\n\t\t\t\ttry {\n\t\t\t\t\tconst metadata = await lstat(join(directory, entry.name));\n\t\t\t\t\tif (!metadata.isFile()) return undefined;\n\t\t\t\t\treturn { name: entry.name, mtimeMs: metadata.mtimeMs };\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (isMissingFile(error)) return undefined;\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t}),\n\t);\n\tconst direction = order === \"oldest\" ? 1 : -1;\n\treturn files\n\t\t.filter((file): file is EvidenceFile => file !== undefined)\n\t\t.sort((left, right) => direction * (left.mtimeMs - right.mtimeMs || left.name.localeCompare(right.name)));\n}\n\nasync function restoreEvent(event: RecordedEvent, paths: EvoPaths): Promise<Record<string, unknown>> {\n\tconst restored: Record<string, unknown> = { ...event };\n\tswitch (event.type) {\n\t\tcase \"before_agent_start\":\n\t\t\trestored.prompt = await resolveStoredPayload(paths, event.prompt);\n\t\t\trestored.systemPrompt = await resolveStoredPayload(paths, event.systemPrompt);\n\t\t\trestored.systemPromptOptions = await resolveStoredPayload(paths, event.systemPromptOptions);\n\t\t\tif (event.images) restored.images = await resolveStoredPayload(paths, event.images);\n\t\t\tbreak;\n\t\tcase \"message\":\n\t\t\trestored.message = await resolveStoredPayload(paths, event.message);\n\t\t\tbreak;\n\t\tcase \"tool\":\n\t\t\trestored.input = await resolveStoredPayload(paths, event.input);\n\t\t\trestored.result = await resolveStoredPayload(paths, event.result);\n\t\t\tbreak;\n\t\tcase \"git_diff\":\n\t\t\trestored.diff = await resolveStoredPayload(paths, event.diff);\n\t\t\tbreak;\n\t\tcase \"explicit_feedback\":\n\t\t\trestored.text = await resolveStoredPayload(paths, event.text);\n\t\t\tbreak;\n\t}\n\treturn restored;\n}\n\nfunction containsQuote(value: unknown, quote: string): boolean {\n\tif (typeof value === \"string\") return value.includes(quote);\n\tif (Array.isArray(value)) return value.some((entry) => containsQuote(entry, quote));\n\tif (!isRecord(value)) return false;\n\treturn Object.values(value).some((entry) => containsQuote(entry, quote));\n}\n\nfunction parseInboxEntry(value: unknown, fileName: string): RecorderInboxEntry {\n\tif (\n\t\t!isRecord(value) ||\n\t\tvalue.schemaVersion !== 1 ||\n\t\ttypeof value.id !== \"string\" ||\n\t\t!value.id ||\n\t\ttypeof value.timestamp !== \"string\" ||\n\t\ttypeof value.sessionId !== \"string\" ||\n\t\t!value.sessionId ||\n\t\t!([\"interactive\", \"rpc\", \"extension\"] as const).includes(value.source as \"interactive\" | \"rpc\" | \"extension\") ||\n\t\ttypeof value.text !== \"string\"\n\t) {\n\t\tthrow new Error(`Inbox reference is invalid: ${fileName}`);\n\t}\n\treturn value as unknown as RecorderInboxEntry;\n}\n\nasync function readInboxEntry(paths: EvoPaths, fileName: string): Promise<RecorderInboxEntry> {\n\tassertSafeFileName(fileName, \"Inbox reference\");\n\tif (!fileName.endsWith(\".json\")) throw new Error(`Inbox reference must be a JSON file: ${fileName}`);\n\tlet source: string;\n\ttry {\n\t\tsource = await readFile(join(paths.inbox, fileName), \"utf8\");\n\t} catch (error) {\n\t\tif (isMissingFile(error)) throw new Error(`Inbox reference does not exist: ${fileName}`);\n\t\tthrow error;\n\t}\n\tlet value: unknown;\n\ttry {\n\t\tvalue = JSON.parse(source);\n\t} catch {\n\t\tthrow new Error(`Inbox reference is invalid JSON: ${fileName}`);\n\t}\n\treturn parseInboxEntry(value, fileName);\n}\n\nasync function fullEvidenceStateDigest(\n\tpaths: EvoPaths,\n\tbundleDigest: string | undefined,\n\tdigests: Awaited<ReturnType<typeof listSessionDigests>>,\n): Promise<string> {\n\tconst sessionDigests = digests.map((digest) => ({\n\t\tsessionId: digest.sessionId,\n\t\tsourceDigest: digest.sourceDigest,\n\t}));\n\tconst inbox = await Promise.all(\n\t\t(await listEvidenceFiles(paths.inbox, \".json\", \"oldest\")).map(async (file) => ({\n\t\t\tfile: file.name,\n\t\t\tsha256: sha256(await readFile(join(paths.inbox, file.name), \"utf8\")),\n\t\t})),\n\t);\n\tlet history = \"\";\n\tlet inboxHistory = \"\";\n\ttry {\n\t\thistory = await readFile(paths.history, \"utf8\");\n\t} catch (error) {\n\t\tif (!isMissingFile(error)) throw error;\n\t}\n\ttry {\n\t\tinboxHistory = await readFile(paths.inboxHistory, \"utf8\");\n\t} catch (error) {\n\t\tif (!isMissingFile(error)) throw error;\n\t}\n\treturn sha256(\n\t\tcanonicalJson({\n\t\t\tfullEvidenceSchemaVersion: 1,\n\t\t\tbundleDigest: bundleDigest ?? null,\n\t\t\thistory: sha256(history),\n\t\t\tinboxHistory: sha256(inboxHistory),\n\t\t\tinbox,\n\t\t\tsessions: sessionDigests,\n\t\t}),\n\t);\n}\n\nfunction compactSessionDigest(digest: Awaited<ReturnType<typeof listSessionDigests>>[number]) {\n\tconst metrics = Object.fromEntries(\n\t\tObject.entries(digest.metrics).filter(([key, value]) =>\n\t\t\tkey === \"usage\" ? digest.metrics.usage.totalTokens > 0 : value !== 0,\n\t\t),\n\t);\n\treturn {\n\t\tschemaVersion: digest.schemaVersion,\n\t\tsessionId: digest.sessionId,\n\t\tbundleDigest: digest.bundleDigest,\n\t\ttaskClass: digest.taskClass,\n\t\tstartedAt: digest.startedAt,\n\t\tendedAt: digest.endedAt,\n\t\tcomplete: digest.complete,\n\t\tlastSequence: digest.lastSequence,\n\t\tsourceDigest: digest.sourceDigest,\n\t\tpreferenceEvidence: digest.preferenceEvidence,\n\t\tassessment: digest.assessment,\n\t\tmetrics,\n\t};\n}\n\nfunction extractMessageText(value: unknown, label: string): string {\n\tif (!isRecord(value) || value.role !== \"user\") throw new Error(`${label} must contain a user message`);\n\tif (typeof value.content === \"string\") return value.content;\n\tif (!Array.isArray(value.content)) throw new Error(`${label} has no textual prompt`);\n\tconst parts = value.content.flatMap((entry) => {\n\t\tif (!isRecord(entry) || entry.type !== \"text\" || typeof entry.text !== \"string\") return [];\n\t\treturn [entry.text];\n\t});\n\tif (parts.length === 0) throw new Error(`${label} has no textual prompt`);\n\treturn parts.join(\"\\n\");\n}\n\nfunction asAgentMessage(value: unknown, label: string): AgentMessage {\n\tif (!isRecord(value) || typeof value.role !== \"string\" || !(\"content\" in value)) {\n\t\tthrow new Error(`${label} is not an agent message`);\n\t}\n\tif (!([\"user\", \"assistant\", \"toolResult\"] as const).includes(value.role as \"user\" | \"assistant\" | \"toolResult\")) {\n\t\tthrow new Error(`${label} has an unsupported role: ${value.role}`);\n\t}\n\treturn value as unknown as AgentMessage;\n}\n\nexport async function collectEvidenceCorpus(\n\tpaths: EvoPaths,\n\toptions: CollectEvidenceCorpusOptions = {},\n): Promise<EvidenceCorpus> {\n\tconst maxBytes = options.maxBytes ?? DEFAULT_CORPUS_BYTES;\n\tif (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) throw new Error(\"maxBytes must be a positive safe integer\");\n\tconst mode = options.mode ?? \"full\";\n\tif (mode === \"full\" && options.reviewCursor !== undefined) {\n\t\tthrow new Error(\"A review cursor can only be used with incremental evidence collection\");\n\t}\n\tawait ensureEvoLayout(paths);\n\n\tconst sourceBudgets = allocateSourceBudgets(maxBytes);\n\tconst collectors = {\n\t\tbundle: createSourceCollector(\"bundle\", sourceBudgets.bundle),\n\t\thistory: createSourceCollector(\"history\", sourceBudgets.history),\n\t\tinbox: createSourceCollector(\"inbox\", sourceBudgets.inbox),\n\t} as Record<EvidenceSource, SourceCollector>;\n\tconst previousCursor =\n\t\tmode === \"incremental\" && options.reviewCursor ? parseReviewCursor(options.reviewCursor) : undefined;\n\tconst reviewedInboxFiles = new Set(previousCursor?.inboxFiles ?? []);\n\tconst inboxLifecycle = await readInboxLifecycleStates(paths);\n\tconst nextInboxFiles = new Set(previousCursor?.inboxFiles ?? []);\n\tconst nextSessionSequences: Record<string, number> = { ...(previousCursor?.sessionSequences ?? {}) };\n\tconst sessionIds = new Set<string>();\n\tconst inboxFiles: string[] = [];\n\n\tlet bundleDigest: string | undefined;\n\tconst registry = new BundleRegistry(paths);\n\tbundleDigest = await registry.readStableDigest();\n\tif (bundleDigest) {\n\t\tconst bundle = await loadCompiledBundle(paths, bundleDigest);\n\t\tif (!collectors.bundle.append(`## Current bundle ${bundleDigest} manifest`, bundle.manifest)) {\n\t\t\tcollectors.bundle.append(`## Current bundle ${bundleDigest}`, {\n\t\t\t\tsummary: bundle.manifest.summary,\n\t\t\t\tparentDigest: bundle.manifest.parentDigest,\n\t\t\t\tmanifestOmittedByBudget: true,\n\t\t\t});\n\t\t}\n\t\tfor (const file of bundle.manifest.files) {\n\t\t\tcollectors.bundle.append(\n\t\t\t\t`## Current bundle file ${file.path}`,\n\t\t\t\tawait readFile(join(bundle.directory, file.path), \"utf8\"),\n\t\t\t);\n\t\t}\n\t}\n\n\tfor (const [path, heading] of [\n\t\t[paths.history, \"Registry history\"],\n\t\t[paths.inboxHistory, \"Inbox lifecycle\"],\n\t] as const) {\n\t\ttry {\n\t\t\tconst lines = (await readFile(path, \"utf8\"))\n\t\t\t\t.split(\"\\n\")\n\t\t\t\t.map((line) => line.trim())\n\t\t\t\t.filter(Boolean)\n\t\t\t\t.reverse();\n\t\t\tfor (const [index, line] of lines.entries()) {\n\t\t\t\tlet entry: unknown;\n\t\t\t\ttry {\n\t\t\t\t\tentry = JSON.parse(line);\n\t\t\t\t} catch {\n\t\t\t\t\tthrow new Error(`${heading} line ${lines.length - index} is invalid JSON`);\n\t\t\t\t}\n\t\t\t\tcollectors.history.append(`## ${heading} entry ${lines.length - index}`, entry);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tif (!isMissingFile(error)) throw error;\n\t\t}\n\t}\n\n\tfor (const file of await listEvidenceFiles(paths.inbox, \".json\", mode === \"incremental\" ? \"oldest\" : \"newest\")) {\n\t\tconst lifecycle = inboxLifecycle.get(file.name);\n\t\tif (\n\t\t\tlifecycle?.status === \"materialized\" ||\n\t\t\tlifecycle?.status === \"fulfilled\" ||\n\t\t\tlifecycle?.status === \"dismissed\" ||\n\t\t\tlifecycle?.status === \"superseded\"\n\t\t) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (!lifecycle && reviewedInboxFiles.has(file.name)) continue;\n\t\tconst entry = await readInboxEntry(paths, file.name);\n\t\tif (!collectors.inbox.append(`## Explicit input ${file.name}`, { entry, lifecycle: lifecycle ?? null })) break;\n\t\tinboxFiles.push(file.name);\n\t\tnextInboxFiles.add(file.name);\n\t}\n\n\t// Soft quota: sessions (the behavioral evidence) receive every byte the fixed\n\t// sources did not use, not a fixed share.\n\tconst fixedBytes = FIXED_SOURCES.reduce((sum, source) => sum + collectors[source].summary().bytes, 0);\n\tconst separatorReserve = Buffer.byteLength(SOURCE_SEPARATOR, \"utf8\") * FIXED_SOURCES.length;\n\tcollectors.sessions = createSourceCollector(\"sessions\", Math.max(0, maxBytes - fixedBytes - separatorReserve));\n\n\tconst allDigests = await listSessionDigests(paths);\n\tconst digests = options.completedSessionsOnly ? allDigests.filter((digest) => digest.complete) : allDigests;\n\tconst orderedDigests = mode === \"incremental\" ? digests : [...digests].reverse();\n\tlet sessionBudgetFull = false;\n\tfor (const digest of orderedDigests) {\n\t\tconst reviewedSequence = previousCursor?.sessionSequences[digest.sessionId] ?? 0;\n\t\tif (digest.lastSequence <= reviewedSequence) continue;\n\t\tlet events: RecordedEvent[];\n\t\ttry {\n\t\t\tevents = await readSessionLog(paths, digest.sessionId);\n\t\t} catch (error) {\n\t\t\tthrow new Error(`Cannot collect session log ${digest.sessionId}.jsonl`, { cause: error });\n\t\t}\n\t\tif (reviewedSequence === 0) {\n\t\t\tif (\n\t\t\t\t!collectors.sessions.append(\n\t\t\t\t\t`## Session digest ${digest.sessionId}`,\n\t\t\t\t\tcompactSessionDigest(digest),\n\t\t\t\t\tdigest.sessionId,\n\t\t\t\t)\n\t\t\t) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tsessionIds.add(digest.sessionId);\n\t\t}\n\t\tfor (const event of events) {\n\t\t\tif (event.sequence <= reviewedSequence) continue;\n\t\t\tconst restored = await restoreEvent(event, paths);\n\t\t\tif (\n\t\t\t\tcollectors.sessions.append(\n\t\t\t\t\t`## Session ${digest.sessionId}, sequence ${event.sequence}`,\n\t\t\t\t\trestored,\n\t\t\t\t\tdigest.sessionId,\n\t\t\t\t)\n\t\t\t) {\n\t\t\t\tsessionIds.add(digest.sessionId);\n\t\t\t\tnextSessionSequences[digest.sessionId] = event.sequence;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (\n\t\t\t\tcollectors.sessions.append(\n\t\t\t\t\t`## Session ${digest.sessionId}, sequence ${event.sequence} (artifact omitted by budget)`,\n\t\t\t\t\tevent,\n\t\t\t\t\tdigest.sessionId,\n\t\t\t\t)\n\t\t\t) {\n\t\t\t\tsessionIds.add(digest.sessionId);\n\t\t\t\tnextSessionSequences[digest.sessionId] = event.sequence;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tsessionBudgetFull = true;\n\t\t\tbreak;\n\t\t}\n\t\tif (sessionBudgetFull) break;\n\t}\n\n\tconst sourceOrder = Object.keys(SOURCE_WEIGHTS) as EvidenceSource[];\n\tconst sourceTexts = sourceOrder.map((source) => collectors[source].text()).filter(Boolean);\n\tconst text = sourceTexts.join(SOURCE_SEPARATOR);\n\tconst sources = Object.fromEntries(sourceOrder.map((source) => [source, collectors[source].summary()])) as Record<\n\t\tEvidenceSource,\n\t\tEvidenceSourceSummary\n\t>;\n\tconst nextReviewCursor = parseReviewCursor({\n\t\tschemaVersion: 1,\n\t\tupdatedAt: (options.now ?? (() => new Date()))().toISOString(),\n\t\tinboxFiles: [...nextInboxFiles],\n\t\tsessionSequences: nextSessionSequences,\n\t});\n\tconst evidenceDigest =\n\t\tmode === \"full\"\n\t\t\t? await fullEvidenceStateDigest(paths, bundleDigest, digests)\n\t\t\t: sha256(\n\t\t\t\t\tcanonicalJson({\n\t\t\t\t\t\tbundleDigest: bundleDigest ?? null,\n\t\t\t\t\t\tcutoff: {\n\t\t\t\t\t\t\tinboxFiles: nextReviewCursor.inboxFiles,\n\t\t\t\t\t\t\tsessionSequences: nextReviewCursor.sessionSequences,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tsourceDigests: Object.fromEntries(\n\t\t\t\t\t\t\tsourceOrder.map((source) => [source, sha256(collectors[source].text())]),\n\t\t\t\t\t\t),\n\t\t\t\t\t}),\n\t\t\t\t);\n\treturn {\n\t\ttext,\n\t\tbytes: Buffer.byteLength(text, \"utf8\"),\n\t\tmaxBytes,\n\t\ttruncated: sourceOrder.some((source) => sources[source].truncated),\n\t\tmode,\n\t\tevidenceDigest,\n\t\tsources,\n\t\tfragments: sourceOrder.flatMap((source) => collectors[source].fragments()),\n\t\tsessionIds: [...sessionIds],\n\t\tinboxFiles,\n\t\tnextReviewCursor,\n\t\t...(bundleDigest ? { bundleDigest } : {}),\n\t};\n}\n\nexport async function validateEvidenceReferences(\n\tpaths: EvoPaths,\n\treferences: readonly EvidenceReference[],\n): Promise<EvidenceReference[]> {\n\tconst logs = new Map<string, Promise<RecordedEvent[]>>();\n\tconst verified: EvidenceReference[] = [];\n\tfor (const reference of references) {\n\t\tif (!reference.sessionId || !Number.isSafeInteger(reference.sequence) || reference.sequence <= 0) {\n\t\t\tthrow new Error(\"Observation evidence must contain a sessionId and positive sequence\");\n\t\t}\n\t\tlet events = logs.get(reference.sessionId);\n\t\tif (!events) {\n\t\t\tevents = readSessionLog(paths, reference.sessionId);\n\t\t\tlogs.set(reference.sessionId, events);\n\t\t}\n\t\tconst event = (await events).find((candidate) => candidate.sequence === reference.sequence);\n\t\tif (!event) throw new Error(`Observation evidence does not exist: ${reference.sessionId}:${reference.sequence}`);\n\t\tif (reference.quote !== undefined) {\n\t\t\tif (!reference.quote)\n\t\t\t\tthrow new Error(`Observation evidence quote is empty: ${reference.sessionId}:${reference.sequence}`);\n\t\t\tconst restored = await restoreEvent(event, paths);\n\t\t\tif (!containsQuote(restored, reference.quote)) {\n\t\t\t\tthrow new Error(`Observation evidence quote was not found: ${reference.sessionId}:${reference.sequence}`);\n\t\t\t}\n\t\t}\n\t\tverified.push({ ...reference });\n\t}\n\treturn verified;\n}\n\nexport async function validateDraftGrounding(paths: EvoPaths, draft: DraftProposal): Promise<EvidenceReference[]> {\n\tconst logs = new Map<string, Promise<RecordedEvent[]>>();\n\tconst loadLog = (sessionId: string): Promise<RecordedEvent[]> => {\n\t\tconst existing = logs.get(sessionId);\n\t\tif (existing) return existing;\n\t\tconst pending = readSessionLog(paths, sessionId);\n\t\tlogs.set(sessionId, pending);\n\t\treturn pending;\n\t};\n\tconst findEvent = async (reference: EvidenceReference | ReplayScenario): Promise<RecordedEvent> => {\n\t\tconst events = await loadLog(reference.sessionId);\n\t\tconst event = events.find((candidate) => candidate.sequence === reference.sequence);\n\t\tif (!event) throw new Error(`Evidence reference does not exist: ${reference.sessionId}:${reference.sequence}`);\n\t\treturn event;\n\t};\n\n\tlet hasExplicitFeedbackEvidence = false;\n\tlet hasExplicitUserRequestEvidence = false;\n\tconst verified: EvidenceReference[] = [];\n\tfor (const reference of draft.evidence) {\n\t\tconst event = await findEvent(reference);\n\t\tconst restored = await restoreEvent(event, paths);\n\t\tlet quoteVerified = false;\n\t\tif (reference.quote !== undefined) {\n\t\t\tif (!reference.quote) throw new Error(`Evidence quote is empty: ${reference.sessionId}:${reference.sequence}`);\n\t\t\tif (!containsQuote(restored, reference.quote)) {\n\t\t\t\tthrow new Error(`Evidence quote was not found: ${reference.sessionId}:${reference.sequence}`);\n\t\t\t}\n\t\t\tquoteVerified = true;\n\t\t}\n\t\thasExplicitFeedbackEvidence ||= event.type === \"explicit_feedback\" && quoteVerified;\n\t\thasExplicitUserRequestEvidence ||= event.type === \"message\" && event.role === \"user\" && quoteVerified;\n\t\tverified.push({ ...reference });\n\t}\n\n\tfor (const scenario of draft.replayScenarios) await findEvent(scenario);\n\tlet hasExplicitInboxInput = false;\n\tfor (const fileName of draft.inboxReferences) {\n\t\tconst entry = await readInboxEntry(paths, fileName);\n\t\thasExplicitInboxInput ||=\n\t\t\tentry.kind === \"request\" ||\n\t\t\tentry.kind === \"preference\" ||\n\t\t\tentry.text.trimStart().startsWith(\"REQUEST:\") ||\n\t\t\tentry.text.trimStart().startsWith(\"PREFERENCE:\");\n\t}\n\n\tif (draft.source === \"pattern\") {\n\t\tconst unique = new Map(\n\t\t\tverified.map((reference) => [`${reference.sessionId}:${reference.sequence}`, reference] as const),\n\t\t);\n\t\tconst sessions = new Set([...unique.values()].map((reference) => reference.sessionId));\n\t\tlet independent = sessions.size >= 2;\n\t\tif (!independent) {\n\t\t\tconst references = [...unique.values()].sort((left, right) => left.sequence - right.sequence);\n\t\t\tindependent = references.some((reference, index) => {\n\t\t\t\tconst next = references[index + 1];\n\t\t\t\treturn (\n\t\t\t\t\tnext !== undefined &&\n\t\t\t\t\treference.quote !== undefined &&\n\t\t\t\t\tnext.quote !== undefined &&\n\t\t\t\t\tnext.sequence - reference.sequence >= 2\n\t\t\t\t);\n\t\t\t});\n\t\t}\n\t\tif (!independent) throw new Error(\"Pattern proposals require at least two independent evidence references\");\n\t} else if (!hasExplicitInboxInput && !hasExplicitFeedbackEvidence && !hasExplicitUserRequestEvidence) {\n\t\tthrow new Error(\"Explicit-request proposals require a REQUEST inbox entry or quoted user request\");\n\t}\n\treturn verified;\n}\n\nexport async function loadReplayScenario(paths: EvoPaths, scenario: ReplayScenario): Promise<LoadedReplayScenario> {\n\tconst events = await readSessionLog(paths, scenario.sessionId);\n\tconst target = events.find((event) => event.sequence === scenario.sequence);\n\tif (!target) throw new Error(`Replay target does not exist: ${scenario.sessionId}:${scenario.sequence}`);\n\tif (target.type !== \"message\" || target.role !== \"user\") {\n\t\tthrow new Error(`Replay target must be a user message: ${scenario.sessionId}:${scenario.sequence}`);\n\t}\n\tconst targetMessage = await resolveStoredPayload(paths, target.message);\n\tconst targetPrompt = extractMessageText(targetMessage, \"Replay target\");\n\n\tconst beforeStart = [...events]\n\t\t.reverse()\n\t\t.find((event) => event.sequence < target.sequence && event.type === \"before_agent_start\");\n\tif (!beforeStart || beforeStart.type !== \"before_agent_start\") {\n\t\tthrow new Error(`Replay target has no preceding system prompt: ${scenario.sessionId}:${scenario.sequence}`);\n\t}\n\tconst oldSystemPrompt = await resolveStoredPayload(paths, beforeStart.systemPrompt);\n\tif (typeof oldSystemPrompt !== \"string\") throw new Error(\"Recorded system prompt must be a string\");\n\tconst systemPromptOptions = await resolveStoredPayload(paths, beforeStart.systemPromptOptions);\n\n\tconst sessionStart = [...events]\n\t\t.reverse()\n\t\t.find((event) => event.sequence <= target.sequence && event.type === \"session_start\");\n\tif (!sessionStart || sessionStart.type !== \"session_start\") {\n\t\tthrow new Error(`Replay target has no session start: ${scenario.sessionId}:${scenario.sequence}`);\n\t}\n\n\tconst history: AgentMessage[] = [];\n\tfor (const event of events) {\n\t\tif (event.sequence >= target.sequence) break;\n\t\tif (event.type !== \"message\") continue;\n\t\tconst message = await resolveStoredPayload(paths, event.message);\n\t\thistory.push(asAgentMessage(message, `Recorded message ${event.sessionId}:${event.sequence}`));\n\t}\n\n\treturn {\n\t\thistory,\n\t\ttargetPrompt,\n\t\toldSystemPrompt,\n\t\tsystemPromptOptions,\n\t\tsessionIdentity: scenario.sessionId,\n\t\tcwd: sessionStart.cwd,\n\t};\n}\n"]}