{"version":3,"file":"fork-context.d.ts","sourceRoot":"","sources":["../../../src/shared/fork-context.ts"],"names":[],"mappings":"AAIA,OAAO,EAAiB,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEhE,KAAK,wBAAwB,GAAG,OAAO,GAAG,MAAM,CAAC;AAEjD,UAAU,kBAAkB;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE;QACT,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,KAAK,CAAC,EAAE,MAAM,CAAC;KACf,CAAC;IACF,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,UAAU,oBAAoB;IAC7B,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IAC1D,SAAS,CAAC,EAAE,MAAM,kBAAkB,GAAG,IAAI,CAAC;IAC5C,UAAU,CAAC,EAAE,MAAM,kBAAkB,EAAE,CAAC;CACxC;AAED,UAAU,sBAAsB;IAC/B,cAAc,IAAI,MAAM,GAAG,SAAS,CAAC;IACrC,SAAS,IAAI,MAAM,GAAG,IAAI,CAAC;IAC3B,aAAa,CAAC,IAAI,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,KAAK,oBAAoB,CAAC;CAC1E;AAED,UAAU,0BAA0B;IACnC,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,KAAK,oBAAoB,CAAC;IAC1E;2FACuF;IACvF,wBAAwB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;CACtD;AAOD,UAAU,mBAAmB;IAC5B,mBAAmB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IACxD,wBAAwB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,SAAS,CAAC;CAC5D;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,wBAAwB,CAE/E;AAED;wFACwF;AACxF,wBAAgB,8BAA8B,CAC7C,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,eAAe,CAAC,EAAE,SAAS,EAAE,EAC7B,iBAAiB,CAAC,EAAE,MAAM,GACxB,OAAO,CAKT;AAqED,wBAAgB,yBAAyB,CACxC,cAAc,EAAE,sBAAsB,EACtC,gBAAgB,EAAE,OAAO,EACzB,OAAO,GAAE,0BAA+B,GACtC,mBAAmB,CAuFrB","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { SessionManager } from \"@lpb-work/pi-coding-agent\";\nimport { findModelInfo, type ModelInfo } from \"./model-info.ts\";\n\ntype SubagentExecutionContext = \"fresh\" | \"fork\";\n\ninterface BranchSessionEntry {\n\ttype: string;\n\tid?: string;\n\tparentId?: string | null;\n\ttimestamp?: string;\n\tmessage?: {\n\t\trole?: string;\n\t\tcontent?: unknown;\n\t\tprovider?: string;\n\t\tapi?: string;\n\t\tmodel?: string;\n\t};\n\tthinkingLevel?: string;\n}\n\ninterface BranchSessionManager {\n\tcreateBranchedSession(leafId: string): string | undefined;\n\tgetHeader?: () => BranchSessionEntry | null;\n\tgetEntries?: () => BranchSessionEntry[];\n}\n\ninterface ForkableSessionManager {\n\tgetSessionFile(): string | undefined;\n\tgetLeafId(): string | null;\n\tgetSessionDir?(): string;\n\topenSession?: (path: string, sessionDir?: string) => BranchSessionManager;\n}\n\ninterface ForkContextResolverOptions {\n\topenSession?: (path: string, sessionDir?: string) => BranchSessionManager;\n\t/** Decide per child index whether a sanitized transcript must also disable the child's\n\t * thinking. Defaults to true (the pre-existing conservative behavior) when omitted. */\n\tforceThinkingOffForIndex?: (index: number) => boolean;\n}\n\ninterface ForkContextResolution {\n\tsessionFile: string;\n\tthinkingOverride?: \"off\";\n}\n\ninterface ForkContextResolver {\n\tsessionFileForIndex(index?: number): string | undefined;\n\tthinkingOverrideForIndex(index?: number): \"off\" | undefined;\n}\n\nexport function resolveSubagentContext(value: unknown): SubagentExecutionContext {\n\treturn value === \"fork\" ? \"fork\" : \"fresh\";\n}\n\n/** Decide whether a resolved child model uses Anthropic's provider or message API, which\n * requires the sanitized fork to disable thinking. Unknown models stay conservative. */\nexport function forkedChildRequiresThinkingOff(\n\tmodel: string | undefined,\n\tavailableModels?: ModelInfo[],\n\tpreferredProvider?: string,\n): boolean {\n\tif (!model) return true;\n\tconst info = findModelInfo(model, availableModels, preferredProvider);\n\tif (!info) return true;\n\treturn info.provider.toLowerCase() === \"anthropic\" || info.api?.toLowerCase() === \"anthropic-messages\";\n}\n\nfunction isUnsafeAnthropicThinkingBlock(message: BranchSessionEntry[\"message\"], block: unknown): boolean {\n\tif (!message || !block || typeof block !== \"object\" || !(\"type\" in block)) return false;\n\tconst provider = typeof message.provider === \"string\" ? message.provider.toLowerCase() : \"\";\n\tconst api = typeof message.api === \"string\" ? message.api.toLowerCase() : \"\";\n\tconst model = typeof message.model === \"string\" ? message.model.toLowerCase() : \"\";\n\tconst isAnthropic = provider === \"anthropic\" || api === \"anthropic-messages\" || model.startsWith(\"anthropic/\");\n\tif (block.type === \"redacted_thinking\") return true;\n\tif (block.type !== \"thinking\" || !isAnthropic) return false;\n\tconst record = block as Record<string, unknown>;\n\tconst signature =\n\t\t\"thinkingSignature\" in record ? record.thinkingSignature : \"signature\" in record ? record.signature : undefined;\n\treturn record.redacted === true || (typeof signature === \"string\" && signature.length > 0);\n}\n\nfunction createEntryId(entries: BranchSessionEntry[]): string {\n\tconst ids = new Set(entries.map((entry) => entry.id).filter((id): id is string => typeof id === \"string\"));\n\tfor (let attempt = 0; attempt < 100; attempt++) {\n\t\tconst id = randomUUID().slice(0, 8);\n\t\tif (!ids.has(id)) return id;\n\t}\n\treturn randomUUID();\n}\n\nfunction appendThinkingOffEntry(entries: BranchSessionEntry[]): void {\n\tconst last = entries[entries.length - 1];\n\tif (last?.type === \"thinking_level_change\" && last.thinkingLevel === \"off\") return;\n\tconst parent = [...entries].reverse().find((entry) => typeof entry.id === \"string\");\n\tentries.push({\n\t\ttype: \"thinking_level_change\",\n\t\tid: createEntryId(entries),\n\t\tparentId: parent?.id ?? null,\n\t\ttimestamp: new Date().toISOString(),\n\t\tthinkingLevel: \"off\",\n\t});\n}\n\nfunction sanitizeUnsafeThinkingBlocks(entries: BranchSessionEntry[]): boolean {\n\tlet sanitized = false;\n\tfor (const entry of entries) {\n\t\tif (entry.type !== \"message\" || entry.message?.role !== \"assistant\" || !Array.isArray(entry.message.content))\n\t\t\tcontinue;\n\t\tconst filtered = entry.message.content.filter((block) => !isUnsafeAnthropicThinkingBlock(entry.message, block));\n\t\tif (filtered.length === entry.message.content.length) continue;\n\t\tentry.message.content = filtered;\n\t\tsanitized = true;\n\t}\n\treturn sanitized;\n}\n\nfunction readSessionEntries(sessionFile: string): BranchSessionEntry[] {\n\tconst lines = fs\n\t\t.readFileSync(sessionFile, \"utf-8\")\n\t\t.split(\"\\n\")\n\t\t.filter((line) => line.trim().length > 0);\n\treturn lines.map((line, index) => {\n\t\ttry {\n\t\t\treturn JSON.parse(line) as BranchSessionEntry;\n\t\t} catch (error) {\n\t\t\tconst cause = error instanceof Error ? error : new Error(String(error));\n\t\t\tthrow new Error(\n\t\t\t\t`Unable to inspect forked session ${sessionFile}: invalid JSONL on line ${index + 1}: ${cause.message}`,\n\t\t\t\t{ cause },\n\t\t\t);\n\t\t}\n\t});\n}\n\nexport function createForkContextResolver(\n\tsessionManager: ForkableSessionManager,\n\trequestedContext: unknown,\n\toptions: ForkContextResolverOptions = {},\n): ForkContextResolver {\n\tif (resolveSubagentContext(requestedContext) !== \"fork\") {\n\t\treturn {\n\t\t\tsessionFileForIndex: () => undefined,\n\t\t\tthinkingOverrideForIndex: () => undefined,\n\t\t};\n\t}\n\n\tconst parentSessionFile = sessionManager.getSessionFile();\n\tif (!parentSessionFile) {\n\t\tthrow new Error(\"Forked subagent context requires a persisted parent session.\");\n\t}\n\n\tconst leafId = sessionManager.getLeafId();\n\tif (!leafId) {\n\t\tthrow new Error(\"Forked subagent context requires a current leaf to fork from.\");\n\t}\n\n\tconst openSession =\n\t\toptions.openSession ??\n\t\tsessionManager.openSession ??\n\t\t((file: string, dir?: string) => SessionManager.open(file, dir));\n\tconst sessionDir = sessionManager.getSessionDir?.();\n\tconst cachedResolutions = new Map<number, ForkContextResolution>();\n\n\tconst resolveFork = (index = 0): ForkContextResolution => {\n\t\tconst cached = cachedResolutions.get(index);\n\t\tif (cached) return cached;\n\t\ttry {\n\t\t\tif (!fs.existsSync(parentSessionFile)) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Parent session file does not exist: ${parentSessionFile}. Pi has not persisted enough history to fork yet.`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst sourceManager = openSession(parentSessionFile, sessionDir);\n\t\t\tconst sessionFile = sourceManager.createBranchedSession(leafId);\n\t\t\tif (!sessionFile) {\n\t\t\t\tthrow new Error(\"Session manager did not return a forked session file.\");\n\t\t\t}\n\t\t\tconst forceThinkingOff = (sanitized: boolean): boolean =>\n\t\t\t\tsanitized && (options.forceThinkingOffForIndex?.(index) ?? true);\n\t\t\tlet thinkingOverride: \"off\" | undefined;\n\t\t\tif (!fs.existsSync(sessionFile)) {\n\t\t\t\tconst header = sourceManager.getHeader?.();\n\t\t\t\tconst entries = sourceManager.getEntries?.();\n\t\t\t\tif (!header || !entries) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Session manager returned a forked session file that does not exist and cannot be persisted by fallback: ${sessionFile}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (forceThinkingOff(sanitizeUnsafeThinkingBlocks(entries))) {\n\t\t\t\t\tappendThinkingOffEntry(entries);\n\t\t\t\t\tthinkingOverride = \"off\";\n\t\t\t\t}\n\t\t\t\tfs.mkdirSync(path.dirname(sessionFile), { recursive: true });\n\t\t\t\tfs.writeFileSync(\n\t\t\t\t\tsessionFile,\n\t\t\t\t\t`${[header, ...entries].map((entry) => JSON.stringify(entry)).join(\"\\n\")}\\n`,\n\t\t\t\t\t\"utf-8\",\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tconst entries = readSessionEntries(sessionFile);\n\t\t\t\tif (sanitizeUnsafeThinkingBlocks(entries)) {\n\t\t\t\t\tif (forceThinkingOff(true)) {\n\t\t\t\t\t\tappendThinkingOffEntry(entries);\n\t\t\t\t\t\tthinkingOverride = \"off\";\n\t\t\t\t\t}\n\t\t\t\t\tfs.writeFileSync(sessionFile, `${entries.map((entry) => JSON.stringify(entry)).join(\"\\n\")}\\n`, \"utf-8\");\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst resolution = { sessionFile, ...(thinkingOverride ? { thinkingOverride } : {}) };\n\t\t\tcachedResolutions.set(index, resolution);\n\t\t\treturn resolution;\n\t\t} catch (error) {\n\t\t\tconst cause = error instanceof Error ? error : new Error(String(error));\n\t\t\tthrow new Error(`Failed to create forked subagent session: ${cause.message}`, { cause });\n\t\t}\n\t};\n\n\treturn {\n\t\tsessionFileForIndex(index = 0): string | undefined {\n\t\t\treturn resolveFork(index).sessionFile;\n\t\t},\n\t\tthinkingOverrideForIndex(index = 0): \"off\" | undefined {\n\t\t\treturn resolveFork(index).thinkingOverride;\n\t\t},\n\t};\n}\n"]}