{"version":3,"file":"preferences.d.ts","sourceRoot":"","sources":["../../src/memory/preferences.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,eAAO,MAAM,gBAAgB,4BAA4B,CAAC;AAO1D,MAAM,WAAW,uBAAuB;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,oBAAoB;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACb;AAED,MAAM,MAAM,uBAAuB,GAAG,uBAAuB,GAAG,oBAAoB,CAAC;AAErF,MAAM,WAAW,iBAAiB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,uBAAuB,CAAC;IAChC,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAChC,aAAa,EAAE,CAAC,CAAC;IACjB,WAAW,EAAE,iBAAiB,EAAE,CAAC;CACjC;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAErE;AAcD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,gBAAgB,CAgDtE;AAgDD,wBAAsB,0BAA0B,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAKlG;AAaD,wBAAsB,kCAAkC,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAEhG","sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { sha256 } from \"../storage.ts\";\nimport type { CompiledBundle } from \"../types.ts\";\n\nexport const PREFERENCES_PATH = \"memory/preferences.json\";\nconst PREFERENCE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;\nconst SESSION_ID_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;\nconst MAX_PREFERENCES = 100;\nconst MAX_INSTRUCTION_CHARACTERS = 2_000;\nconst MAX_TOTAL_INSTRUCTION_CHARACTERS = 16_000;\n\nexport interface SessionPreferenceSource {\n\tsessionId: string;\n\tsequence: number;\n\tquote: string;\n}\n\nexport interface PackPreferenceSource {\n\tpackName: string;\n\tpackVersion: string;\n\tintegrity: string;\n\tfile: string;\n}\n\nexport type DurablePreferenceSource = SessionPreferenceSource | PackPreferenceSource;\n\nexport interface DurablePreference {\n\tid: string;\n\tinstruction: string;\n\tsource: DurablePreferenceSource;\n\taddedAt: string;\n}\n\nexport interface PreferenceMemory {\n\tschemaVersion: 1;\n\tpreferences: DurablePreference[];\n}\n\n/**\n * Canonical id for a session-sourced durable preference. Content-addressing the\n * instruction makes ids reproducible across independent writers and lets the\n * schema reject ids invented from unrelated identifiers such as inbox UUIDs.\n */\nexport function deterministicPreferenceId(instruction: string): string {\n\treturn `pref-${sha256(instruction).slice(0, 16)}`;\n}\n\nfunction asRecord(value: unknown, label: string): Record<string, unknown> {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n\t\tthrow new Error(`${label} must be an object`);\n\t}\n\treturn value as Record<string, unknown>;\n}\n\nfunction exactKeys(record: Record<string, unknown>, allowed: readonly string[], label: string): void {\n\tconst keys = new Set(allowed);\n\tfor (const key of Object.keys(record)) if (!keys.has(key)) throw new Error(`${label} has unknown key: ${key}`);\n}\n\nexport function parsePreferenceMemory(value: unknown): PreferenceMemory {\n\tconst root = asRecord(value, PREFERENCES_PATH);\n\texactKeys(root, [\"schemaVersion\", \"preferences\"], PREFERENCES_PATH);\n\tif (root.schemaVersion !== 1) throw new Error(`${PREFERENCES_PATH} schemaVersion must be 1`);\n\tif (!Array.isArray(root.preferences) || root.preferences.length > MAX_PREFERENCES) {\n\t\tthrow new Error(`${PREFERENCES_PATH} preferences must be an array with at most ${MAX_PREFERENCES} entries`);\n\t}\n\tconst preferences = root.preferences.map((value, index): DurablePreference => {\n\t\tconst label = `${PREFERENCES_PATH}.preferences[${index}]`;\n\t\tconst entry = asRecord(value, label);\n\t\texactKeys(entry, [\"id\", \"instruction\", \"source\", \"addedAt\"], label);\n\t\tif (typeof entry.id !== \"string\" || !PREFERENCE_ID_PATTERN.test(entry.id)) {\n\t\t\tthrow new Error(`${label}.id is invalid`);\n\t\t}\n\t\tif (\n\t\t\ttypeof entry.instruction !== \"string\" ||\n\t\t\t!entry.instruction.trim() ||\n\t\t\tentry.instruction !== entry.instruction.trim() ||\n\t\t\tentry.instruction.length > MAX_INSTRUCTION_CHARACTERS\n\t\t) {\n\t\t\tthrow new Error(`${label}.instruction must be trimmed and contain 1-${MAX_INSTRUCTION_CHARACTERS} characters`);\n\t\t}\n\t\tif (typeof entry.addedAt !== \"string\" || !Number.isFinite(Date.parse(entry.addedAt))) {\n\t\t\tthrow new Error(`${label}.addedAt is invalid`);\n\t\t}\n\t\tconst source = parsePreferenceSource(entry.source, entry.instruction, `${label}.source`);\n\t\tif (\"sessionId\" in source && entry.id !== deterministicPreferenceId(entry.instruction)) {\n\t\t\tthrow new Error(\n\t\t\t\t`${label}.id must be the deterministic instruction hash ${deterministicPreferenceId(entry.instruction)}`,\n\t\t\t);\n\t\t}\n\t\treturn {\n\t\t\tid: entry.id,\n\t\t\tinstruction: entry.instruction,\n\t\t\tsource,\n\t\t\taddedAt: entry.addedAt,\n\t\t};\n\t});\n\tif (new Set(preferences.map((entry) => entry.id)).size !== preferences.length) {\n\t\tthrow new Error(`${PREFERENCES_PATH} contains duplicate ids`);\n\t}\n\tif (new Set(preferences.map((entry) => entry.instruction)).size !== preferences.length) {\n\t\tthrow new Error(`${PREFERENCES_PATH} contains duplicate instructions`);\n\t}\n\tif (preferences.reduce((total, entry) => total + entry.instruction.length, 0) > MAX_TOTAL_INSTRUCTION_CHARACTERS) {\n\t\tthrow new Error(`${PREFERENCES_PATH} active instructions exceed ${MAX_TOTAL_INSTRUCTION_CHARACTERS} characters`);\n\t}\n\treturn { schemaVersion: 1, preferences };\n}\n\nfunction parsePreferenceSource(value: unknown, instruction: string, label: string): DurablePreferenceSource {\n\tconst source = asRecord(value, label);\n\tif (\"sessionId\" in source) {\n\t\texactKeys(source, [\"sessionId\", \"sequence\", \"quote\"], label);\n\t\tif (typeof source.sessionId !== \"string\" || !SESSION_ID_PATTERN.test(source.sessionId)) {\n\t\t\tthrow new Error(`${label}.sessionId is invalid`);\n\t\t}\n\t\tif (!Number.isSafeInteger(source.sequence) || (source.sequence as number) <= 0) {\n\t\t\tthrow new Error(`${label}.sequence must be a positive integer`);\n\t\t}\n\t\tif (typeof source.quote !== \"string\" || !source.quote.includes(instruction) || source.quote.length > 16_000) {\n\t\t\tthrow new Error(`${label}.quote must contain the exact instruction`);\n\t\t}\n\t\treturn {\n\t\t\tsessionId: source.sessionId,\n\t\t\tsequence: source.sequence as number,\n\t\t\tquote: source.quote,\n\t\t};\n\t}\n\texactKeys(source, [\"packName\", \"packVersion\", \"integrity\", \"file\"], label);\n\tif (typeof source.packName !== \"string\" || !/^[a-z0-9][a-z0-9._-]{0,127}$/.test(source.packName)) {\n\t\tthrow new Error(`${label}.packName is invalid`);\n\t}\n\tif (typeof source.packVersion !== \"string\" || !/^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$/.test(source.packVersion)) {\n\t\tthrow new Error(`${label}.packVersion is invalid`);\n\t}\n\tif (typeof source.integrity !== \"string\" || !/^sha256:[0-9a-f]{64}$/.test(source.integrity)) {\n\t\tthrow new Error(`${label}.integrity is invalid`);\n\t}\n\tif (\n\t\ttypeof source.file !== \"string\" ||\n\t\t!source.file ||\n\t\tsource.file.startsWith(\"/\") ||\n\t\tsource.file.startsWith(\"\\\\\") ||\n\t\tsource.file.split(/[/\\\\]/).includes(\"..\")\n\t) {\n\t\tthrow new Error(`${label}.file is invalid`);\n\t}\n\treturn {\n\t\tpackName: source.packName,\n\t\tpackVersion: source.packVersion,\n\t\tintegrity: source.integrity,\n\t\tfile: source.file,\n\t};\n}\n\nexport async function readBundlePreferenceMemory(bundle: CompiledBundle): Promise<PreferenceMemory> {\n\tif (!bundle.manifest.files.some((file) => file.path === PREFERENCES_PATH)) {\n\t\treturn { schemaVersion: 1, preferences: [] };\n\t}\n\treturn parsePreferenceMemory(JSON.parse(await readFile(join(bundle.directory, PREFERENCES_PATH), \"utf8\")));\n}\n\nfunction renderPreferenceInstructions(memory: PreferenceMemory): string {\n\tif (memory.preferences.length === 0) return \"\";\n\treturn [\n\t\t\"## Durable user preferences\",\n\t\t\"\",\n\t\t\"Treat these as the user's cross-task defaults. A newer explicit instruction in the current conversation may override them.\",\n\t\t\"\",\n\t\t...memory.preferences.map((preference) => `- ${preference.instruction}`),\n\t].join(\"\\n\");\n}\n\nexport async function renderBundlePreferenceInstructions(bundle: CompiledBundle): Promise<string> {\n\treturn renderPreferenceInstructions(await readBundlePreferenceMemory(bundle));\n}\n"]}