{"version":3,"file":"inbox.d.ts","sourceRoot":"","sources":["../src/inbox.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,OAAO,KAAK,EAAE,kBAAkB,EAAqB,MAAM,sBAAsB,CAAC;AAElF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,MAAM,MAAM,SAAS,GAAG,cAAc,GAAG,YAAY,GAAG,SAAS,GAAG,MAAM,GAAG,YAAY,CAAC;AAC1F,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,QAAQ,GAAG,cAAc,GAAG,WAAW,GAAG,WAAW,GAAG,YAAY,CAAC;AAExG,MAAM,WAAW,mBAAmB;IACnC,aAAa,EAAE,CAAC,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,WAAW,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,mBAAoB,SAAQ,mBAAmB;IAC/D,WAAW,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,YAAY,GAAG,SAAS,GAAG,MAAM,GAAG,YAAY,CAAC;IACvD,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,aAAa;IAC7B,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,EAAE,MAAM,EAAE,CAAC;CAChB;AAmDD,wBAAsB,cAAc,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAG/F;AAyDD,wBAAsB,wBAAwB,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC,CAGzG;AA+CD,wBAAsB,wBAAwB,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAU1G;AAED,wBAAsB,mBAAmB,CACxC,KAAK,EAAE,QAAQ,EACf,SAAS,EAAE,SAAS,aAAa,EAAE,EACnC,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,GAC/B,OAAO,CAAC,IAAI,CAAC,CA8Bf;AAED,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAY1F;AAED,wBAAsB,mBAAmB,CAAC,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAgB5F;AAED,wBAAsB,iBAAiB,CACtC,KAAK,EAAE,QAAQ,EACf,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,cAAc,GAAG,WAAW,GAAG,WAAW,GAAG,YAA0B,GAC7E,OAAO,CAAC,mBAAmB,CAAC,CAQ9B;AAED,wBAAsB,mBAAmB,CAAC,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAY5G;AAED,wBAAsB,mBAAmB,CAAC,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAQ,GAAG,OAAO,CAAC,aAAa,CAAC,CA8BjG","sourcesContent":["import { readdir, readFile } from \"node:fs/promises\";\nimport { basename, join } from \"node:path\";\nimport type { EvoPaths } from \"./paths.ts\";\nimport { ensureEvoLayout } from \"./paths.ts\";\nimport type { RecorderInboxEntry, RecorderInboxKind } from \"./recorder/schema.ts\";\nimport { appendJsonLine, durableUnlink, sha256, withFileLock } from \"./storage.ts\";\nimport type { Proposal } from \"./types.ts\";\n\nexport type InboxKind = \"unclassified\" | \"preference\" | \"request\" | \"note\" | \"task-local\";\nexport type InboxStatus = \"open\" | \"linked\" | \"materialized\" | \"fulfilled\" | \"dismissed\" | \"superseded\";\n\nexport interface InboxLifecycleEvent {\n\tschemaVersion: 1;\n\tid: string;\n\tfile: string;\n\ttimestamp: string;\n\tkind: InboxKind;\n\tstatus: InboxStatus;\n\treason: string;\n\tsummary?: string;\n\t/** Exact user-authored durable instruction after preference classification. */\n\tinstruction?: string;\n\tproposalId?: string;\n\tbundleDigest?: string;\n\tsourceDigest?: string;\n\tpayloadDeletedAt?: string;\n}\n\nexport interface InboxLifecycleState extends InboxLifecycleEvent {\n\tinitialized: boolean;\n}\n\nexport interface InboxDecision {\n\tfile: string;\n\tkind: \"preference\" | \"request\" | \"note\" | \"task-local\";\n\treason: string;\n\tinstruction?: string;\n}\n\nexport interface InboxGcResult {\n\tdryRun: boolean;\n\tfiles: string[];\n}\n\nconst TERMINAL_STATUSES = new Set<InboxStatus>([\"materialized\", \"fulfilled\", \"dismissed\", \"superseded\"]);\nconst FILE_PATTERN = /^[^/\\\\\\0]+\\.json$/;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction assertFileName(file: string): void {\n\tif (!FILE_PATTERN.test(file) || basename(file) !== file) throw new Error(`Invalid inbox file: ${file}`);\n}\n\nfunction inferEntryKind(entry: RecorderInboxEntry): RecorderInboxKind {\n\tif (entry.kind) return entry.kind;\n\tconst text = entry.text.trimStart();\n\tif (text.startsWith(\"REQUEST:\")) return \"request\";\n\tif (text.startsWith(\"NOTE:\")) return \"note\";\n\tif (text.startsWith(\"PREFERENCE:\")) return \"preference\";\n\treturn \"candidate\";\n}\n\nfunction lifecycleKind(kind: RecorderInboxKind): InboxKind {\n\treturn kind === \"candidate\" ? \"unclassified\" : kind;\n}\n\nfunction parseRecorderInboxEntry(value: unknown, file: string): RecorderInboxEntry {\n\tassertFileName(file);\n\tif (!isRecord(value)) throw new Error(`Inbox entry is invalid: ${file}`);\n\tif (\n\t\tvalue.schemaVersion !== 1 ||\n\t\ttypeof value.id !== \"string\" ||\n\t\t!value.id ||\n\t\ttypeof value.timestamp !== \"string\" ||\n\t\t!Number.isFinite(Date.parse(value.timestamp)) ||\n\t\ttypeof value.sessionId !== \"string\" ||\n\t\t!value.sessionId ||\n\t\t(value.source !== \"interactive\" && value.source !== \"rpc\" && value.source !== \"extension\") ||\n\t\ttypeof value.text !== \"string\" ||\n\t\t!value.text.trim() ||\n\t\t(value.kind !== undefined &&\n\t\t\tvalue.kind !== \"candidate\" &&\n\t\t\tvalue.kind !== \"preference\" &&\n\t\t\tvalue.kind !== \"request\" &&\n\t\t\tvalue.kind !== \"note\")\n\t) {\n\t\tthrow new Error(`Inbox entry is invalid: ${file}`);\n\t}\n\treturn value as unknown as RecorderInboxEntry;\n}\n\nexport async function readInboxEntry(paths: EvoPaths, file: string): Promise<RecorderInboxEntry> {\n\tassertFileName(file);\n\treturn parseRecorderInboxEntry(JSON.parse(await readFile(join(paths.inbox, file), \"utf8\")), file);\n}\n\nfunction parseLifecycleEvent(value: unknown, line: number): InboxLifecycleEvent {\n\tif (!isRecord(value)) throw new Error(`Invalid inbox history event at line ${line}`);\n\tconst kind = value.kind;\n\tconst status = value.status;\n\tif (\n\t\tvalue.schemaVersion !== 1 ||\n\t\ttypeof value.id !== \"string\" ||\n\t\t!value.id ||\n\t\ttypeof value.file !== \"string\" ||\n\t\ttypeof value.timestamp !== \"string\" ||\n\t\t!Number.isFinite(Date.parse(value.timestamp)) ||\n\t\t(kind !== \"unclassified\" &&\n\t\t\tkind !== \"preference\" &&\n\t\t\tkind !== \"request\" &&\n\t\t\tkind !== \"note\" &&\n\t\t\tkind !== \"task-local\") ||\n\t\t(status !== \"open\" &&\n\t\t\tstatus !== \"linked\" &&\n\t\t\tstatus !== \"materialized\" &&\n\t\t\tstatus !== \"fulfilled\" &&\n\t\t\tstatus !== \"dismissed\" &&\n\t\t\tstatus !== \"superseded\") ||\n\t\ttypeof value.reason !== \"string\" ||\n\t\t!value.reason.trim() ||\n\t\t(value.summary !== undefined && (typeof value.summary !== \"string\" || !value.summary.trim())) ||\n\t\t(value.instruction !== undefined && (typeof value.instruction !== \"string\" || !value.instruction.trim())) ||\n\t\t(value.proposalId !== undefined && typeof value.proposalId !== \"string\") ||\n\t\t(value.bundleDigest !== undefined && typeof value.bundleDigest !== \"string\") ||\n\t\t(value.sourceDigest !== undefined && typeof value.sourceDigest !== \"string\") ||\n\t\t(value.payloadDeletedAt !== undefined &&\n\t\t\t(typeof value.payloadDeletedAt !== \"string\" || !Number.isFinite(Date.parse(value.payloadDeletedAt))))\n\t) {\n\t\tthrow new Error(`Invalid inbox history event at line ${line}`);\n\t}\n\tassertFileName(value.file);\n\treturn value as unknown as InboxLifecycleEvent;\n}\n\nasync function readStatesUnlocked(paths: EvoPaths): Promise<Map<string, InboxLifecycleState>> {\n\tlet content: string;\n\ttry {\n\t\tcontent = await readFile(paths.inboxHistory, \"utf8\");\n\t} catch (error) {\n\t\tif (isRecord(error) && error.code === \"ENOENT\") return new Map();\n\t\tthrow error;\n\t}\n\tconst states = new Map<string, InboxLifecycleState>();\n\tfor (const [index, line] of content.split(\"\\n\").entries()) {\n\t\tif (!line.trim()) continue;\n\t\tconst event = parseLifecycleEvent(JSON.parse(line) as unknown, index + 1);\n\t\tstates.set(event.file, { ...event, initialized: true });\n\t}\n\treturn states;\n}\n\nexport async function readInboxLifecycleStates(paths: EvoPaths): Promise<Map<string, InboxLifecycleState>> {\n\tawait ensureEvoLayout(paths);\n\treturn withFileLock(paths, \"inbox-lifecycle\", () => readStatesUnlocked(paths));\n}\n\nasync function appendState(\n\tpaths: EvoPaths,\n\tfile: string,\n\tupdate: {\n\t\tkind: InboxKind;\n\t\tstatus: InboxStatus;\n\t\treason: string;\n\t\tsummary?: string;\n\t\tinstruction?: string;\n\t\tproposalId?: string;\n\t\tbundleDigest?: string;\n\t\tsourceDigest?: string;\n\t\tpayloadDeletedAt?: string;\n\t},\n): Promise<InboxLifecycleState> {\n\tassertFileName(file);\n\treturn withFileLock(paths, \"inbox-lifecycle\", async () => {\n\t\tconst states = await readStatesUnlocked(paths);\n\t\tconst previous = states.get(file);\n\t\tconst entry = await readInboxEntry(paths, file).catch((error: unknown) => {\n\t\t\tif (previous?.payloadDeletedAt) return { id: previous.id } as RecorderInboxEntry;\n\t\t\tthrow error;\n\t\t});\n\t\tconst event: InboxLifecycleEvent = {\n\t\t\tschemaVersion: 1,\n\t\t\tid: entry.id,\n\t\t\tfile,\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tkind: update.kind,\n\t\t\tstatus: update.status,\n\t\t\treason: update.reason,\n\t\t\t...((update.summary ?? previous?.summary) ? { summary: update.summary ?? previous?.summary } : {}),\n\t\t\t...((update.instruction ?? previous?.instruction)\n\t\t\t\t? { instruction: update.instruction ?? previous?.instruction }\n\t\t\t\t: {}),\n\t\t\t...(update.proposalId ? { proposalId: update.proposalId } : {}),\n\t\t\t...(update.bundleDigest ? { bundleDigest: update.bundleDigest } : {}),\n\t\t\t...(update.sourceDigest ? { sourceDigest: update.sourceDigest } : {}),\n\t\t\t...(update.payloadDeletedAt ? { payloadDeletedAt: update.payloadDeletedAt } : {}),\n\t\t};\n\t\tawait appendJsonLine(paths.inboxHistory, event);\n\t\treturn { ...event, initialized: true };\n\t});\n}\n\nexport async function initializeInboxLifecycle(paths: EvoPaths, file: string): Promise<InboxLifecycleState> {\n\tconst existing = (await readInboxLifecycleStates(paths)).get(file);\n\tif (existing) return existing;\n\tconst entry = await readInboxEntry(paths, file);\n\treturn appendState(paths, file, {\n\t\tkind: lifecycleKind(inferEntryKind(entry)),\n\t\tstatus: \"open\",\n\t\treason: \"Inbox entry recorded\",\n\t\tsummary: entry.text.replace(/^(?:CANDIDATE|PREFERENCE|REQUEST|NOTE):\\s*/i, \"\").trim(),\n\t});\n}\n\nexport async function applyInboxDecisions(\n\tpaths: EvoPaths,\n\tdecisions: readonly InboxDecision[],\n\tallowedFiles: ReadonlySet<string>,\n): Promise<void> {\n\tconst existingStates = await readInboxLifecycleStates(paths);\n\tconst seen = new Set<string>();\n\tfor (const decision of decisions) {\n\t\tif (!allowedFiles.has(decision.file))\n\t\t\tthrow new Error(`Inbox decision references unavailable file: ${decision.file}`);\n\t\tif (seen.has(decision.file)) throw new Error(`Duplicate inbox decision: ${decision.file}`);\n\t\tseen.add(decision.file);\n\t\tconst entry = await readInboxEntry(paths, decision.file);\n\t\tif (decision.kind === \"preference\") {\n\t\t\tif (!decision.instruction?.trim() || !entry.text.includes(decision.instruction)) {\n\t\t\t\tthrow new Error(`Preference decision must quote an exact instruction from ${decision.file}`);\n\t\t\t}\n\t\t} else if (decision.instruction !== undefined) {\n\t\t\tthrow new Error(`Only preference decisions may contain instruction: ${decision.file}`);\n\t\t}\n\t\tawait appendState(paths, decision.file, {\n\t\t\tkind: decision.kind,\n\t\t\tstatus: decision.kind === \"note\" || decision.kind === \"task-local\" ? \"dismissed\" : \"open\",\n\t\t\treason: decision.reason,\n\t\t\tsummary: entry.text.replace(/^(?:CANDIDATE|PREFERENCE|REQUEST|NOTE):\\s*/i, \"\").trim(),\n\t\t\t...(decision.instruction ? { instruction: decision.instruction } : {}),\n\t\t});\n\t}\n\tfor (const file of allowedFiles) {\n\t\tconst state = existingStates.get(file);\n\t\tif ((!state || state.kind === \"unclassified\") && !seen.has(file)) {\n\t\t\tthrow new Error(`ResearchPlanner did not classify active inbox file: ${file}`);\n\t\t}\n\t}\n}\n\nexport async function linkProposalInbox(paths: EvoPaths, proposal: Proposal): Promise<void> {\n\tconst states = await readInboxLifecycleStates(paths);\n\tfor (const file of proposal.inboxReferences) {\n\t\tconst state = states.get(file) ?? (await initializeInboxLifecycle(paths, file));\n\t\tif (TERMINAL_STATUSES.has(state.status)) continue;\n\t\tawait appendState(paths, file, {\n\t\t\tkind: state.kind,\n\t\t\tstatus: \"linked\",\n\t\t\treason: \"Inbox entry linked to proposal\",\n\t\t\tproposalId: proposal.id,\n\t\t});\n\t}\n}\n\nexport async function settleProposalInbox(paths: EvoPaths, proposal: Proposal): Promise<void> {\n\tconst states = await readInboxLifecycleStates(paths);\n\tfor (const file of proposal.inboxReferences) {\n\t\tconst state = states.get(file) ?? (await initializeInboxLifecycle(paths, file));\n\t\tif (TERMINAL_STATUSES.has(state.status)) continue;\n\t\tconst materializedPreference = state.kind === \"preference\";\n\t\tawait appendState(paths, file, {\n\t\t\tkind: state.kind,\n\t\t\tstatus: materializedPreference ? \"materialized\" : \"fulfilled\",\n\t\t\treason: materializedPreference\n\t\t\t\t? \"Preference materialized in an active bundle\"\n\t\t\t\t: \"Request fulfilled by a kept proposal\",\n\t\t\tproposalId: proposal.id,\n\t\t\t...(proposal.candidateDigest ? { bundleDigest: proposal.candidateDigest } : {}),\n\t\t});\n\t}\n}\n\nexport async function resolveInboxEntry(\n\tpaths: EvoPaths,\n\tfile: string,\n\treason: string,\n\tstatus: \"materialized\" | \"fulfilled\" | \"dismissed\" | \"superseded\" = \"fulfilled\",\n): Promise<InboxLifecycleState> {\n\tconst state = (await readInboxLifecycleStates(paths)).get(file) ?? (await initializeInboxLifecycle(paths, file));\n\tif (TERMINAL_STATUSES.has(state.status)) return state;\n\treturn appendState(paths, file, {\n\t\tkind: state.kind,\n\t\tstatus,\n\t\treason,\n\t});\n}\n\nexport async function reopenProposalInbox(paths: EvoPaths, proposal: Proposal, reason: string): Promise<void> {\n\tconst states = await readInboxLifecycleStates(paths);\n\tfor (const file of proposal.inboxReferences) {\n\t\tconst state = states.get(file);\n\t\tif (!state || state.payloadDeletedAt) continue;\n\t\tawait appendState(paths, file, {\n\t\t\tkind: state.kind,\n\t\t\tstatus: \"open\",\n\t\t\treason,\n\t\t\tproposalId: proposal.id,\n\t\t});\n\t}\n}\n\nexport async function garbageCollectInbox(paths: EvoPaths, dryRun = false): Promise<InboxGcResult> {\n\tawait ensureEvoLayout(paths);\n\tconst states = await readInboxLifecycleStates(paths);\n\tconst available = new Set(\n\t\t(await readdir(paths.inbox, { withFileTypes: true }))\n\t\t\t.filter((entry) => entry.isFile() && entry.name.endsWith(\".json\"))\n\t\t\t.map((entry) => entry.name),\n\t);\n\tconst files = [...states.values()]\n\t\t.filter((state) => TERMINAL_STATUSES.has(state.status) && available.has(state.file))\n\t\t.map((state) => state.file)\n\t\t.sort();\n\tif (dryRun) return { dryRun: true, files };\n\tfor (const file of files) {\n\t\tconst state = states.get(file);\n\t\tif (!state) continue;\n\t\tconst source = await readFile(join(paths.inbox, file));\n\t\tconst deletedAt = new Date().toISOString();\n\t\tawait appendState(paths, file, {\n\t\t\tkind: state.kind,\n\t\t\tstatus: state.status,\n\t\t\treason: \"Terminal inbox payload garbage-collected\",\n\t\t\t...(state.proposalId ? { proposalId: state.proposalId } : {}),\n\t\t\t...(state.bundleDigest ? { bundleDigest: state.bundleDigest } : {}),\n\t\t\tsourceDigest: sha256(source),\n\t\t\tpayloadDeletedAt: deletedAt,\n\t\t});\n\t\tawait durableUnlink(join(paths.inbox, file));\n\t}\n\treturn { dryRun: false, files };\n}\n"]}