{"version":3,"file":"digest.d.ts","sourceRoot":"","sources":["../../src/recorder/digest.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAG5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAMhD,MAAM,WAAW,oBAAoB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,MAAM,CAAC;IACzB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,KAAK,EAAE,YAAY,CAAC;CACpB;AAED,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,UAAU,GAAG,UAAU,GAAG,cAAc,CAAC;AAEnF,MAAM,WAAW,aAAa;IAC7B,aAAa,EAAE,CAAC,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,EAAE,gBAAgB,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,kBAAkB,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAChD,UAAU,EAAE;QACX,YAAY,EAAE,QAAQ,GAAG,QAAQ,GAAG,cAAc,CAAC;QACnD,kBAAkB,EAAE,OAAO,CAAC;QAC5B,gBAAgB,EAAE,MAAM,EAAE,CAAC;KAC3B,CAAC;IACF,OAAO,EAAE,oBAAoB,CAAC;CAC9B;AAmJD,wBAAsB,kBAAkB,CAAC,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAK/G;AAED,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAE9G;AAED,wBAAsB,kBAAkB,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAwBlF","sourcesContent":["import { readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { EvoPaths } from \"../paths.ts\";\nimport { ensureEvoLayout } from \"../paths.ts\";\nimport { atomicWriteJson, canonicalJson, readJsonIfExists, sha256 } from \"../storage.ts\";\nimport type { UsageSummary } from \"../types.ts\";\nimport type { RecordedEvent } from \"./schema.ts\";\nimport { readSessionLog } from \"./store.ts\";\n\nconst NON_SESSION_LOG_NAMES = new Set([\"capability-audit.jsonl\"]);\n\nexport interface SessionDigestMetrics {\n\ttasks: number;\n\tuserMessages: number;\n\tassistantMessages: number;\n\ttoolResults: number;\n\tfollowUpUserMessages: number;\n\ttoolCalls: number;\n\ttoolErrors: number;\n\ttoolDurationMs: number;\n\tverificationRuns: number;\n\tverificationPassed: number;\n\tverificationFailed: number;\n\tpreferenceSignals: number;\n\tcompactions: number;\n\tcompactionRetries: number;\n\tcompactionDurationMs: number;\n\tcompactionTokensBefore: number;\n\tusage: UsageSummary;\n}\n\nexport type SessionTaskClass = \"coding\" | \"research\" | \"tool-use\" | \"conversation\";\n\nexport interface SessionDigest {\n\tschemaVersion: 2;\n\tsessionId: string;\n\tbundleDigest: string | null;\n\ttaskClass: SessionTaskClass;\n\tcwd?: string;\n\tstartedAt?: string;\n\tendedAt?: string;\n\tcomplete: boolean;\n\tfirstSequence: number;\n\tlastSequence: number;\n\tsourceDigest: string;\n\tmodels: string[];\n\tpreferenceEvidence: Array<{ sequence: number }>;\n\tassessment: {\n\t\tverification: \"passed\" | \"failed\" | \"not-observed\";\n\t\tcomparisonEligible: boolean;\n\t\texclusionReasons: string[];\n\t};\n\tmetrics: SessionDigestMetrics;\n}\n\nfunction emptyUsage(): UsageSummary {\n\treturn { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0 };\n}\n\nfunction createSessionDigest(events: readonly RecordedEvent[]): SessionDigest | undefined {\n\tconst first = events[0];\n\tconst last = events.at(-1);\n\tif (!first || !last) return undefined;\n\tconst usage = emptyUsage();\n\tconst models = new Set<string>();\n\tconst preferenceEvidence: Array<{ sequence: number }> = [];\n\tlet tasks = 0;\n\tlet userMessages = 0;\n\tlet assistantMessages = 0;\n\tlet toolResults = 0;\n\tlet followUpUserMessages = 0;\n\tlet toolCalls = 0;\n\tlet toolErrors = 0;\n\tlet toolDurationMs = 0;\n\tlet verificationRuns = 0;\n\tlet verificationPassed = 0;\n\tlet verificationFailed = 0;\n\tlet compactions = 0;\n\tlet compactionRetries = 0;\n\tlet compactionDurationMs = 0;\n\tlet compactionTokensBefore = 0;\n\tlet assistantSeen = false;\n\tlet hasCodingTool = false;\n\tlet hasResearchTool = false;\n\tlet hasTool = false;\n\tlet cwd: string | undefined;\n\tlet startedAt: string | undefined;\n\tlet endedAt: string | undefined;\n\n\tfor (const event of events) {\n\t\tswitch (event.type) {\n\t\t\tcase \"session_start\":\n\t\t\t\tcwd ??= event.cwd;\n\t\t\t\tstartedAt ??= event.timestamp;\n\t\t\t\tbreak;\n\t\t\tcase \"session_end\":\n\t\t\t\tendedAt = event.timestamp;\n\t\t\t\tbreak;\n\t\t\tcase \"before_agent_start\":\n\t\t\t\ttasks += 1;\n\t\t\t\tbreak;\n\t\t\tcase \"message\":\n\t\t\t\tif (event.role === \"user\") {\n\t\t\t\t\tuserMessages += 1;\n\t\t\t\t\tif (assistantSeen) followUpUserMessages += 1;\n\t\t\t\t} else if (event.role === \"assistant\") {\n\t\t\t\t\tassistantMessages += 1;\n\t\t\t\t\tassistantSeen = true;\n\t\t\t\t} else if (event.role === \"toolResult\") {\n\t\t\t\t\ttoolResults += 1;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tcase \"tool\":\n\t\t\t\ttoolCalls += 1;\n\t\t\t\thasTool = true;\n\t\t\t\thasCodingTool ||=\n\t\t\t\t\tevent.verification !== undefined ||\n\t\t\t\t\t/(?:^|[-_])(edit|write|patch|bash|shell|test|compile)(?:$|[-_])/i.test(event.toolName);\n\t\t\t\thasResearchTool ||= /(?:search|fetch|browser|web|docs?)/i.test(event.toolName);\n\t\t\t\ttoolErrors += event.isError ? 1 : 0;\n\t\t\t\ttoolDurationMs += event.durationMs;\n\t\t\t\tif (event.verification) {\n\t\t\t\t\tverificationRuns += 1;\n\t\t\t\t\tif (event.verification.exitCode === 0) verificationPassed += 1;\n\t\t\t\t\telse verificationFailed += 1;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tcase \"usage\":\n\t\t\t\tmodels.add(`${event.provider}/${event.model}`);\n\t\t\t\tusage.input += event.usage.input;\n\t\t\t\tusage.output += event.usage.output;\n\t\t\t\tusage.cacheRead += event.usage.cacheRead;\n\t\t\t\tusage.cacheWrite += event.usage.cacheWrite;\n\t\t\t\tusage.totalTokens += event.usage.totalTokens;\n\t\t\t\tbreak;\n\t\t\tcase \"explicit_feedback\":\n\t\t\t\tpreferenceEvidence.push({ sequence: event.sequence });\n\t\t\t\tbreak;\n\t\t\tcase \"compaction\":\n\t\t\t\tcompactions += 1;\n\t\t\t\tcompactionRetries += event.willRetry ? 1 : 0;\n\t\t\t\tcompactionDurationMs += event.durationMs ?? 0;\n\t\t\t\tcompactionTokensBefore += event.tokensBefore;\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tconst taskClass: SessionTaskClass = hasCodingTool\n\t\t? \"coding\"\n\t\t: hasResearchTool\n\t\t\t? \"research\"\n\t\t\t: hasTool\n\t\t\t\t? \"tool-use\"\n\t\t\t\t: \"conversation\";\n\tconst complete = endedAt !== undefined;\n\tconst exclusionReasons = [\n\t\t...(!complete ? [\"session is incomplete\"] : []),\n\t\t...(tasks === 0 ? [\"session contains no agent task\"] : []),\n\t];\n\treturn {\n\t\tschemaVersion: 2,\n\t\tsessionId: first.sessionId,\n\t\tbundleDigest: first.bundleDigest,\n\t\ttaskClass,\n\t\t...(cwd ? { cwd } : {}),\n\t\t...(startedAt ? { startedAt } : {}),\n\t\t...(endedAt ? { endedAt } : {}),\n\t\tcomplete,\n\t\tfirstSequence: first.sequence,\n\t\tlastSequence: last.sequence,\n\t\tsourceDigest: sha256(canonicalJson(events)),\n\t\tmodels: [...models].sort(),\n\t\tpreferenceEvidence,\n\t\tassessment: {\n\t\t\tverification: verificationRuns === 0 ? \"not-observed\" : verificationFailed > 0 ? \"failed\" : \"passed\",\n\t\t\tcomparisonEligible: exclusionReasons.length === 0,\n\t\t\texclusionReasons,\n\t\t},\n\t\tmetrics: {\n\t\t\ttasks,\n\t\t\tuserMessages,\n\t\t\tassistantMessages,\n\t\t\ttoolResults,\n\t\t\tfollowUpUserMessages,\n\t\t\ttoolCalls,\n\t\t\ttoolErrors,\n\t\t\ttoolDurationMs,\n\t\t\tverificationRuns,\n\t\t\tverificationPassed,\n\t\t\tverificationFailed,\n\t\t\tpreferenceSignals: preferenceEvidence.length,\n\t\t\tcompactions,\n\t\t\tcompactionRetries,\n\t\t\tcompactionDurationMs,\n\t\t\tcompactionTokensBefore,\n\t\t\tusage,\n\t\t},\n\t};\n}\n\nexport async function buildSessionDigest(paths: EvoPaths, sessionId: string): Promise<SessionDigest | undefined> {\n\tconst digest = createSessionDigest(await readSessionLog(paths, sessionId));\n\tif (!digest) return undefined;\n\tawait atomicWriteJson(join(paths.digests, `${sessionId}.json`), digest);\n\treturn digest;\n}\n\nexport async function readSessionDigest(paths: EvoPaths, sessionId: string): Promise<SessionDigest | undefined> {\n\treturn readJsonIfExists<SessionDigest>(join(paths.digests, `${sessionId}.json`));\n}\n\nexport async function listSessionDigests(paths: EvoPaths): Promise<SessionDigest[]> {\n\tawait ensureEvoLayout(paths);\n\tconst entries = await readdir(paths.log, { withFileTypes: true });\n\tconst digests: SessionDigest[] = [];\n\tfor (const entry of entries\n\t\t.filter((item) => item.isFile() && item.name.endsWith(\".jsonl\") && !NON_SESSION_LOG_NAMES.has(item.name))\n\t\t.sort((a, b) => a.name.localeCompare(b.name))) {\n\t\tconst sessionId = entry.name.slice(0, -\".jsonl\".length);\n\t\tconst stored = await readSessionDigest(paths, sessionId);\n\t\tif (stored?.schemaVersion === 2 && stored.complete) {\n\t\t\tdigests.push(stored);\n\t\t\tcontinue;\n\t\t}\n\t\tconst current = createSessionDigest(await readSessionLog(paths, sessionId));\n\t\tif (!current) continue;\n\t\tif (stored?.sourceDigest !== current.sourceDigest) {\n\t\t\tawait atomicWriteJson(join(paths.digests, `${sessionId}.json`), current);\n\t\t}\n\t\tdigests.push(current);\n\t}\n\treturn digests.sort(\n\t\t(left, right) =>\n\t\t\t(left.startedAt ?? \"\").localeCompare(right.startedAt ?? \"\") || left.sessionId.localeCompare(right.sessionId),\n\t);\n}\n"]}