{"version":3,"file":"subagent-log.d.ts","sourceRoot":"","sources":["../../src/server/subagent-log.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,MAAM,WAAW,iBAAiB;IACjC,uEAAuE;IACvE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,+BAAgC,SAAQ,KAAK;IACzD,cAGC;CACD;AAyDD;;;GAGG;AACH,wBAAgB,gCAAgC,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CAQ7E;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAelF;AAyBD;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,EAAE,CAMzE","sourcesContent":["/**\n * Subagent session-log reader — turns a background agent's on-disk session\n * JSONL into the message list the client's transcript reducer consumes.\n *\n * Why disk: `background_agent_event` relays are ephemeral (they exist only on\n * SSE clients connected while the agent streams). After a browser reload the\n * reducer state is gone, so the session log is the only source of truth for\n * a subagent transcript. The child process appends entries as it works, so\n * reading the file mid-run yields the transcript up to the last completed\n * message.\n */\n\nimport { existsSync, readdirSync, readFileSync, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\nexport interface SubagentLogSource {\n\t/** Path to the agent's session JSONL (known after the child exits). */\n\tsessionFile?: string;\n\t/** Directory the child writes its session into (known at spawn time). */\n\tsessionDir?: string;\n}\n\nexport class SubagentSessionLogNotFoundError extends Error {\n\tconstructor() {\n\t\tsuper(\"No session log found for this agent — it may not have produced output yet\");\n\t\tthis.name = \"SubagentSessionLogNotFoundError\";\n\t}\n}\n\ninterface SessionFileCandidate {\n\tpath: string;\n\tmtime: number;\n}\n\nconst STEP_SESSION_DIR_RE = /^step-(\\d+)$/;\n\nfunction isExpectedFilesystemError(err: unknown): boolean {\n\treturn typeof (err as NodeJS.ErrnoException | undefined)?.code === \"string\";\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null;\n}\n\nfunction findNewestJsonlFileInDir(dir: string): SessionFileCandidate | undefined {\n\tlet best: SessionFileCandidate | undefined;\n\tfor (const entry of readdirSync(dir, { withFileTypes: true })) {\n\t\tif (!entry.name.endsWith(\".jsonl\")) continue;\n\t\tif (entry.isDirectory()) continue;\n\t\tconst path = join(dir, entry.name);\n\t\ttry {\n\t\t\tconst mtime = statSync(path).mtime.getTime();\n\t\t\tif (!best || mtime > best.mtime) best = { path, mtime };\n\t\t} catch (err) {\n\t\t\tif (isExpectedFilesystemError(err)) continue;\n\t\t\tthrow err;\n\t\t}\n\t}\n\treturn best;\n}\n\nfunction discoverStepSessionFileCandidates(sessionDir: string): SessionFileCandidate[] {\n\tconst steps: Array<{ name: string; index: number }> = [];\n\tfor (const entry of readdirSync(sessionDir, { withFileTypes: true })) {\n\t\tif (!entry.isDirectory()) continue;\n\t\tconst match = STEP_SESSION_DIR_RE.exec(entry.name);\n\t\tif (!match) continue;\n\t\tsteps.push({ name: entry.name, index: Number(match[1]) });\n\t}\n\tsteps.sort((a, b) => a.index - b.index || a.name.localeCompare(b.name));\n\n\tconst files: SessionFileCandidate[] = [];\n\tfor (const step of steps) {\n\t\ttry {\n\t\t\tconst candidate = findNewestJsonlFileInDir(join(sessionDir, step.name));\n\t\t\tif (candidate) files.push(candidate);\n\t\t} catch (err) {\n\t\t\tif (isExpectedFilesystemError(err)) continue;\n\t\t\tthrow err;\n\t\t}\n\t}\n\treturn files;\n}\n\n/**\n * Find chain step session JSONLs under step-N/ directories, ordered by numeric\n * step index. Returns an empty list for missing/non-chain directories.\n */\nexport function discoverSubagentStepSessionFiles(sessionDir: string): string[] {\n\ttry {\n\t\tif (!existsSync(sessionDir)) return [];\n\t\treturn discoverStepSessionFileCandidates(sessionDir).map((file) => file.path);\n\t} catch (err) {\n\t\tif (isExpectedFilesystemError(err)) return [];\n\t\tthrow err;\n\t}\n}\n\n/**\n * Find the most recently modified .jsonl in a session directory. Parity with\n * discoverSessionFile in @dreb/coding-agent's subagent tool. Normal subagents\n * write directly under sessionDir; chain-mode subagents register the chain root\n * but write per-step logs under step-N/ subdirectories.\n */\nexport function discoverSubagentSessionFile(sessionDir: string): string | undefined {\n\ttry {\n\t\tif (!existsSync(sessionDir)) return undefined;\n\t\tconst flatFile = findNewestJsonlFileInDir(sessionDir);\n\t\tif (flatFile) return flatFile.path;\n\n\t\tlet best: SessionFileCandidate | undefined;\n\t\tfor (const file of discoverStepSessionFileCandidates(sessionDir)) {\n\t\t\tif (!best || file.mtime > best.mtime) best = file;\n\t\t}\n\t\treturn best?.path;\n\t} catch (err) {\n\t\tif (isExpectedFilesystemError(err)) return undefined;\n\t\tthrow err;\n\t}\n}\n\nfunction readMessagesFromFile(file: string): unknown[] {\n\tconst messages: unknown[] = [];\n\tfor (const line of readFileSync(file, \"utf8\").split(\"\\n\")) {\n\t\tif (!line.trim()) continue;\n\t\ttry {\n\t\t\tconst entry = JSON.parse(line);\n\t\t\tif (isRecord(entry) && entry.type === \"message\" && entry.message) messages.push(entry.message);\n\t\t} catch (err) {\n\t\t\tif (err instanceof SyntaxError) continue;\n\t\t\tthrow err;\n\t\t}\n\t}\n\treturn messages;\n}\n\nfunction discoverMessageFiles(source: SubagentLogSource): string[] {\n\tconst stepFiles = source.sessionDir ? discoverSubagentStepSessionFiles(source.sessionDir) : [];\n\tif (stepFiles.length > 0) return stepFiles;\n\tif (source.sessionFile) return existsSync(source.sessionFile) ? [source.sessionFile] : [];\n\tconst file = source.sessionDir ? discoverSubagentSessionFile(source.sessionDir) : undefined;\n\treturn file ? [file] : [];\n}\n\n/**\n * Read the message payloads from a subagent session log.\n *\n * Subagent sessions are single-shot and linear (no branching/compaction), so\n * a straight scan of `type: \"message\"` entries reconstructs the transcript.\n * Chain-mode subagents write one linear log per step; those step logs are\n * concatenated in numeric step order. Malformed lines are skipped (the tail\n * line can be mid-write).\n *\n * @throws when no session file can be located — callers surface this loudly.\n */\nexport function readSubagentMessages(source: SubagentLogSource): unknown[] {\n\tconst files = discoverMessageFiles(source);\n\tif (files.length === 0) {\n\t\tthrow new SubagentSessionLogNotFoundError();\n\t}\n\treturn files.flatMap((file) => readMessagesFromFile(file));\n}\n"]}