@caupulican/pi-adaptative 0.81.23 → 0.81.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/dist/core/agent-session.d.ts +2 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +4 -3
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/autonomy/status.d.ts +2 -3
- package/dist/core/autonomy/status.d.ts.map +1 -1
- package/dist/core/autonomy/status.js +3 -12
- package/dist/core/autonomy/status.js.map +1 -1
- package/dist/core/autonomy-telemetry.d.ts +3 -8
- package/dist/core/autonomy-telemetry.d.ts.map +1 -1
- package/dist/core/autonomy-telemetry.js +15 -20
- package/dist/core/autonomy-telemetry.js.map +1 -1
- package/dist/core/cost/cost-summary.d.ts +26 -0
- package/dist/core/cost/cost-summary.d.ts.map +1 -0
- package/dist/core/cost/cost-summary.js +85 -0
- package/dist/core/cost/cost-summary.js.map +1 -0
- package/dist/core/cost/session-usage.d.ts.map +1 -1
- package/dist/core/cost/session-usage.js +8 -2
- package/dist/core/cost/session-usage.js.map +1 -1
- package/dist/core/session-analytics.d.ts +3 -1
- package/dist/core/session-analytics.d.ts.map +1 -1
- package/dist/core/session-analytics.js +33 -18
- package/dist/core/session-analytics.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +4 -20
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/report-commands.d.ts.map +1 -1
- package/dist/modes/interactive/report-commands.js +7 -8
- package/dist/modes/interactive/report-commands.js.map +1 -1
- package/dist/modes/interactive/session-flow-commands.d.ts +1 -2
- package/dist/modes/interactive/session-flow-commands.d.ts.map +1 -1
- package/dist/modes/interactive/session-flow-commands.js +8 -4
- package/dist/modes/interactive/session-flow-commands.js.map +1 -1
- package/docs/usage.md +6 -0
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +12 -12
- package/package.json +4 -4
|
@@ -105,6 +105,7 @@ export function aggregateCumulativeUsageFromSessionEntries(entries) {
|
|
|
105
105
|
costCacheWrite += usage.cost.cacheWrite;
|
|
106
106
|
costTotal += usage.cost.total;
|
|
107
107
|
};
|
|
108
|
+
const seenSpawnedReportIds = new Set();
|
|
108
109
|
for (const entry of entries) {
|
|
109
110
|
if (entry.type === "message" && entry.message.role === "assistant") {
|
|
110
111
|
const usage = entry.message.usage;
|
|
@@ -114,9 +115,14 @@ export function aggregateCumulativeUsageFromSessionEntries(entries) {
|
|
|
114
115
|
}
|
|
115
116
|
else if (entry.type === "custom" && entry.customType === SPAWNED_USAGE_CUSTOM_TYPE) {
|
|
116
117
|
const data = entry.data;
|
|
117
|
-
if (data?.usage
|
|
118
|
-
|
|
118
|
+
if (!data?.usage || !isUsage(data.usage))
|
|
119
|
+
continue;
|
|
120
|
+
if (data.reportId) {
|
|
121
|
+
if (seenSpawnedReportIds.has(data.reportId))
|
|
122
|
+
continue;
|
|
123
|
+
seenSpawnedReportIds.add(data.reportId);
|
|
119
124
|
}
|
|
125
|
+
add(data.usage);
|
|
120
126
|
}
|
|
121
127
|
}
|
|
122
128
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-usage.js","sourceRoot":"","sources":["../../../src/core/cost/session-usage.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from \"node:fs\";\nimport { basename, join } from \"node:path\";\nimport { loadEntriesFromFile, type SessionEntry } from \"@caupulican/pi-agent-core/node\";\nimport type { AssistantMessage, Usage } from \"@caupulican/pi-ai\";\nimport { SPAWNED_USAGE_CUSTOM_TYPE, type SpawnedUsageReport } from \"../agent-session.ts\";\n\nfunction isUsage(value: unknown): value is Usage {\n\tif (!value || typeof value !== \"object\") return false;\n\tconst usage = value as Partial<Usage>;\n\tconst cost = usage.cost as Partial<Usage[\"cost\"]> | undefined;\n\treturn (\n\t\ttypeof usage.input === \"number\" &&\n\t\ttypeof usage.output === \"number\" &&\n\t\ttypeof usage.cacheRead === \"number\" &&\n\t\ttypeof usage.cacheWrite === \"number\" &&\n\t\ttypeof usage.totalTokens === \"number\" &&\n\t\t!!cost &&\n\t\ttypeof cost.input === \"number\" &&\n\t\ttypeof cost.output === \"number\" &&\n\t\ttypeof cost.cacheRead === \"number\" &&\n\t\ttypeof cost.cacheWrite === \"number\" &&\n\t\ttypeof cost.total === \"number\"\n\t);\n}\n\nexport function readAutoLearnSessionIdFromFile(filePath: string): string | undefined {\n\tlet fd: number | undefined;\n\ttry {\n\t\tfd = fs.openSync(filePath, \"r\");\n\t\tconst buffer = Buffer.alloc(64 * 1024);\n\t\tconst bytesRead = fs.readSync(fd, buffer, 0, buffer.length, 0);\n\t\tconst firstLine = buffer.toString(\"utf8\", 0, bytesRead).split(\"\\n\", 1)[0]?.trim();\n\t\tif (!firstLine) return undefined;\n\t\tconst header = JSON.parse(firstLine) as Record<string, unknown>;\n\t\treturn header.type === \"session\" && typeof header.id === \"string\" ? header.id : undefined;\n\t} catch {\n\t\treturn undefined;\n\t} finally {\n\t\tif (fd !== undefined) {\n\t\t\ttry {\n\t\t\t\tfs.closeSync(fd);\n\t\t\t} catch {\n\t\t\t\t// Ignore close errors\n\t\t\t}\n\t\t}\n\t}\n}\n\nexport function findChildSessionFile(sessionDir: string, sessionId: string): string | undefined {\n\tif (!fs.existsSync(sessionDir)) return undefined;\n\n\tconst jsonlFiles: string[] = [];\n\n\tfunction collectJsonlFiles(dirPath: string) {\n\t\tlet currentEntries: fs.Dirent[];\n\t\ttry {\n\t\t\tcurrentEntries = fs.readdirSync(dirPath, { withFileTypes: true });\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\t\tfor (const entry of currentEntries) {\n\t\t\tconst fullPath = join(dirPath, entry.name);\n\t\t\tif (entry.isDirectory()) {\n\t\t\t\tcollectJsonlFiles(fullPath);\n\t\t\t} else if (entry.isFile() && entry.name.endsWith(\".jsonl\")) {\n\t\t\t\tjsonlFiles.push(fullPath);\n\t\t\t}\n\t\t}\n\t}\n\n\tcollectJsonlFiles(sessionDir);\n\n\tfor (const file of jsonlFiles) {\n\t\tconst base = basename(file);\n\t\tif (base === `${sessionId}.jsonl` || base.endsWith(`_${sessionId}.jsonl`)) {\n\t\t\treturn file;\n\t\t}\n\t}\n\n\tfor (const file of jsonlFiles) {\n\t\tconst headerId = readAutoLearnSessionIdFromFile(file);\n\t\tif (headerId === sessionId) {\n\t\t\treturn file;\n\t\t}\n\t}\n\n\treturn undefined;\n}\n\nexport function aggregateCumulativeUsageFromSessionEntries(entries: SessionEntry[]): Usage {\n\tlet input = 0;\n\tlet output = 0;\n\tlet cacheRead = 0;\n\tlet cacheWrite = 0;\n\tlet totalTokens = 0;\n\tlet costInput = 0;\n\tlet costOutput = 0;\n\tlet costCacheRead = 0;\n\tlet costCacheWrite = 0;\n\tlet costTotal = 0;\n\n\tconst add = (usage: Usage) => {\n\t\tinput += usage.input;\n\t\toutput += usage.output;\n\t\tcacheRead += usage.cacheRead;\n\t\tcacheWrite += usage.cacheWrite;\n\t\ttotalTokens += usage.totalTokens;\n\t\tcostInput += usage.cost.input;\n\t\tcostOutput += usage.cost.output;\n\t\tcostCacheRead += usage.cost.cacheRead;\n\t\tcostCacheWrite += usage.cost.cacheWrite;\n\t\tcostTotal += usage.cost.total;\n\t};\n\n\tfor (const entry of entries) {\n\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\tconst usage = (entry.message as AssistantMessage).usage;\n\t\t\tif (usage && isUsage(usage)) {\n\t\t\t\tadd(usage);\n\t\t\t}\n\t\t} else if (entry.type === \"custom\" && entry.customType === SPAWNED_USAGE_CUSTOM_TYPE) {\n\t\t\tconst data = entry.data as SpawnedUsageReport | undefined;\n\t\t\tif (data?.usage && isUsage(data.usage)) {\n\t\t\t\tadd(data.usage);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\tinput,\n\t\toutput,\n\t\tcacheRead,\n\t\tcacheWrite,\n\t\ttotalTokens,\n\t\tcost: {\n\t\t\tinput: costInput,\n\t\t\toutput: costOutput,\n\t\t\tcacheRead: costCacheRead,\n\t\t\tcacheWrite: costCacheWrite,\n\t\t\ttotal: costTotal,\n\t\t},\n\t};\n}\n\nexport function reportCompletedAutoLearnUsageHelper(args: {\n\trunId: string;\n\tsessionDir: string;\n\tsessionId: string;\n\tlogPath?: string;\n\tparentSession: {\n\t\taddSpawnedUsage: (\n\t\t\tusage: Usage,\n\t\t\topts: { label: string; sourceSessionId: string; reportId: string },\n\t\t) => string | undefined;\n\t};\n\tappendLog?: (logPath: string, message: string) => void;\n}): void {\n\tconst { runId, sessionDir, sessionId, logPath, parentSession, appendLog } = args;\n\tconst sessionFile = findChildSessionFile(sessionDir, sessionId);\n\tif (!sessionFile) {\n\t\tif (logPath && appendLog) {\n\t\t\tappendLog(logPath, `Auto Learn usage report skipped: no child session file found for ${sessionId}.`);\n\t\t}\n\t\treturn;\n\t}\n\tconst fileEntries = loadEntriesFromFile(sessionFile);\n\tconst sessionEntries = fileEntries.filter((entry): entry is SessionEntry => entry.type !== \"session\");\n\tconst usage = aggregateCumulativeUsageFromSessionEntries(sessionEntries);\n\tif (usage.cost.total === 0 && usage.totalTokens === 0) {\n\t\tif (logPath && appendLog) {\n\t\t\tappendLog(logPath, `Auto Learn usage report skipped: child session had no usage for ${sessionId}.`);\n\t\t}\n\t\treturn;\n\t}\n\tparentSession.addSpawnedUsage(usage, {\n\t\tlabel: \"auto-learn\",\n\t\tsourceSessionId: sessionId,\n\t\treportId: `auto-learn:${runId}:${sessionId}`,\n\t});\n\tif (logPath && appendLog) {\n\t\tappendLog(logPath, `Auto Learn usage reported: ${sessionId}.`);\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"session-usage.js","sourceRoot":"","sources":["../../../src/core/cost/session-usage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,mBAAmB,EAAqB,MAAM,gCAAgC,CAAC;AAExF,OAAO,EAAE,yBAAyB,EAA2B,MAAM,qBAAqB,CAAC;AAEzF,SAAS,OAAO,CAAC,KAAc,EAAkB;IAChD,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IACtD,MAAM,KAAK,GAAG,KAAuB,CAAC;IACtC,MAAM,IAAI,GAAG,KAAK,CAAC,IAA0C,CAAC;IAC9D,OAAO,CACN,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ;QAC/B,OAAO,KAAK,CAAC,MAAM,KAAK,QAAQ;QAChC,OAAO,KAAK,CAAC,SAAS,KAAK,QAAQ;QACnC,OAAO,KAAK,CAAC,UAAU,KAAK,QAAQ;QACpC,OAAO,KAAK,CAAC,WAAW,KAAK,QAAQ;QACrC,CAAC,CAAC,IAAI;QACN,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ;QAC9B,OAAO,IAAI,CAAC,MAAM,KAAK,QAAQ;QAC/B,OAAO,IAAI,CAAC,SAAS,KAAK,QAAQ;QAClC,OAAO,IAAI,CAAC,UAAU,KAAK,QAAQ;QACnC,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,CAC9B,CAAC;AAAA,CACF;AAED,MAAM,UAAU,8BAA8B,CAAC,QAAgB,EAAsB;IACpF,IAAI,EAAsB,CAAC;IAC3B,IAAI,CAAC;QACJ,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;QAChC,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC;QACvC,MAAM,SAAS,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QAC/D,MAAM,SAAS,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,SAAS,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;QAClF,IAAI,CAAC,SAAS;YAAE,OAAO,SAAS,CAAC;QACjC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAA4B,CAAC;QAChE,OAAO,MAAM,CAAC,IAAI,KAAK,SAAS,IAAI,OAAO,MAAM,CAAC,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;IAC3F,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,SAAS,CAAC;IAClB,CAAC;YAAS,CAAC;QACV,IAAI,EAAE,KAAK,SAAS,EAAE,CAAC;YACtB,IAAI,CAAC;gBACJ,EAAE,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;YAClB,CAAC;YAAC,MAAM,CAAC;gBACR,sBAAsB;YACvB,CAAC;QACF,CAAC;IACF,CAAC;AAAA,CACD;AAED,MAAM,UAAU,oBAAoB,CAAC,UAAkB,EAAE,SAAiB,EAAsB;IAC/F,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC;QAAE,OAAO,SAAS,CAAC;IAEjD,MAAM,UAAU,GAAa,EAAE,CAAC;IAEhC,SAAS,iBAAiB,CAAC,OAAe,EAAE;QAC3C,IAAI,cAA2B,CAAC;QAChC,IAAI,CAAC;YACJ,cAAc,GAAG,EAAE,CAAC,WAAW,CAAC,OAAO,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QACnE,CAAC;QAAC,MAAM,CAAC;YACR,OAAO;QACR,CAAC;QACD,KAAK,MAAM,KAAK,IAAI,cAAc,EAAE,CAAC;YACpC,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;YAC3C,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;gBACzB,iBAAiB,CAAC,QAAQ,CAAC,CAAC;YAC7B,CAAC;iBAAM,IAAI,KAAK,CAAC,MAAM,EAAE,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAC5D,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAC3B,CAAC;QACF,CAAC;IAAA,CACD;IAED,iBAAiB,CAAC,UAAU,CAAC,CAAC;IAE9B,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;QAC/B,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC5B,IAAI,IAAI,KAAK,GAAG,SAAS,QAAQ,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,SAAS,QAAQ,CAAC,EAAE,CAAC;YAC3E,OAAO,IAAI,CAAC;QACb,CAAC;IACF,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;QAC/B,MAAM,QAAQ,GAAG,8BAA8B,CAAC,IAAI,CAAC,CAAC;QACtD,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC5B,OAAO,IAAI,CAAC;QACb,CAAC;IACF,CAAC;IAED,OAAO,SAAS,CAAC;AAAA,CACjB;AAED,MAAM,UAAU,0CAA0C,CAAC,OAAuB,EAAS;IAC1F,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,IAAI,SAAS,GAAG,CAAC,CAAC;IAClB,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,IAAI,WAAW,GAAG,CAAC,CAAC;IACpB,IAAI,SAAS,GAAG,CAAC,CAAC;IAClB,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,IAAI,aAAa,GAAG,CAAC,CAAC;IACtB,IAAI,cAAc,GAAG,CAAC,CAAC;IACvB,IAAI,SAAS,GAAG,CAAC,CAAC;IAElB,MAAM,GAAG,GAAG,CAAC,KAAY,EAAE,EAAE,CAAC;QAC7B,KAAK,IAAI,KAAK,CAAC,KAAK,CAAC;QACrB,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC;QACvB,SAAS,IAAI,KAAK,CAAC,SAAS,CAAC;QAC7B,UAAU,IAAI,KAAK,CAAC,UAAU,CAAC;QAC/B,WAAW,IAAI,KAAK,CAAC,WAAW,CAAC;QACjC,SAAS,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC;QAC9B,UAAU,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC;QAChC,aAAa,IAAI,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC;QACtC,cAAc,IAAI,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC;QACxC,SAAS,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC;IAAA,CAC9B,CAAC;IAEF,MAAM,oBAAoB,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/C,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;YACpE,MAAM,KAAK,GAAI,KAAK,CAAC,OAA4B,CAAC,KAAK,CAAC;YACxD,IAAI,KAAK,IAAI,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC7B,GAAG,CAAC,KAAK,CAAC,CAAC;YACZ,CAAC;QACF,CAAC;aAAM,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,UAAU,KAAK,yBAAyB,EAAE,CAAC;YACtF,MAAM,IAAI,GAAG,KAAK,CAAC,IAAsC,CAAC;YAC1D,IAAI,CAAC,IAAI,EAAE,KAAK,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC;gBAAE,SAAS;YACnD,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACnB,IAAI,oBAAoB,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC;oBAAE,SAAS;gBACtD,oBAAoB,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACzC,CAAC;YACD,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACjB,CAAC;IACF,CAAC;IAED,OAAO;QACN,KAAK;QACL,MAAM;QACN,SAAS;QACT,UAAU;QACV,WAAW;QACX,IAAI,EAAE;YACL,KAAK,EAAE,SAAS;YAChB,MAAM,EAAE,UAAU;YAClB,SAAS,EAAE,aAAa;YACxB,UAAU,EAAE,cAAc;YAC1B,KAAK,EAAE,SAAS;SAChB;KACD,CAAC;AAAA,CACF;AAED,MAAM,UAAU,mCAAmC,CAAC,IAYnD,EAAQ;IACR,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC;IACjF,MAAM,WAAW,GAAG,oBAAoB,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;IAChE,IAAI,CAAC,WAAW,EAAE,CAAC;QAClB,IAAI,OAAO,IAAI,SAAS,EAAE,CAAC;YAC1B,SAAS,CAAC,OAAO,EAAE,oEAAoE,SAAS,GAAG,CAAC,CAAC;QACtG,CAAC;QACD,OAAO;IACR,CAAC;IACD,MAAM,WAAW,GAAG,mBAAmB,CAAC,WAAW,CAAC,CAAC;IACrD,MAAM,cAAc,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,KAAK,EAAyB,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC;IACtG,MAAM,KAAK,GAAG,0CAA0C,CAAC,cAAc,CAAC,CAAC;IACzE,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,KAAK,CAAC,IAAI,KAAK,CAAC,WAAW,KAAK,CAAC,EAAE,CAAC;QACvD,IAAI,OAAO,IAAI,SAAS,EAAE,CAAC;YAC1B,SAAS,CAAC,OAAO,EAAE,mEAAmE,SAAS,GAAG,CAAC,CAAC;QACrG,CAAC;QACD,OAAO;IACR,CAAC;IACD,aAAa,CAAC,eAAe,CAAC,KAAK,EAAE;QACpC,KAAK,EAAE,YAAY;QACnB,eAAe,EAAE,SAAS;QAC1B,QAAQ,EAAE,cAAc,KAAK,IAAI,SAAS,EAAE;KAC5C,CAAC,CAAC;IACH,IAAI,OAAO,IAAI,SAAS,EAAE,CAAC;QAC1B,SAAS,CAAC,OAAO,EAAE,8BAA8B,SAAS,GAAG,CAAC,CAAC;IAChE,CAAC;AAAA,CACD","sourcesContent":["import * as fs from \"node:fs\";\nimport { basename, join } from \"node:path\";\nimport { loadEntriesFromFile, type SessionEntry } from \"@caupulican/pi-agent-core/node\";\nimport type { AssistantMessage, Usage } from \"@caupulican/pi-ai\";\nimport { SPAWNED_USAGE_CUSTOM_TYPE, type SpawnedUsageReport } from \"../agent-session.ts\";\n\nfunction isUsage(value: unknown): value is Usage {\n\tif (!value || typeof value !== \"object\") return false;\n\tconst usage = value as Partial<Usage>;\n\tconst cost = usage.cost as Partial<Usage[\"cost\"]> | undefined;\n\treturn (\n\t\ttypeof usage.input === \"number\" &&\n\t\ttypeof usage.output === \"number\" &&\n\t\ttypeof usage.cacheRead === \"number\" &&\n\t\ttypeof usage.cacheWrite === \"number\" &&\n\t\ttypeof usage.totalTokens === \"number\" &&\n\t\t!!cost &&\n\t\ttypeof cost.input === \"number\" &&\n\t\ttypeof cost.output === \"number\" &&\n\t\ttypeof cost.cacheRead === \"number\" &&\n\t\ttypeof cost.cacheWrite === \"number\" &&\n\t\ttypeof cost.total === \"number\"\n\t);\n}\n\nexport function readAutoLearnSessionIdFromFile(filePath: string): string | undefined {\n\tlet fd: number | undefined;\n\ttry {\n\t\tfd = fs.openSync(filePath, \"r\");\n\t\tconst buffer = Buffer.alloc(64 * 1024);\n\t\tconst bytesRead = fs.readSync(fd, buffer, 0, buffer.length, 0);\n\t\tconst firstLine = buffer.toString(\"utf8\", 0, bytesRead).split(\"\\n\", 1)[0]?.trim();\n\t\tif (!firstLine) return undefined;\n\t\tconst header = JSON.parse(firstLine) as Record<string, unknown>;\n\t\treturn header.type === \"session\" && typeof header.id === \"string\" ? header.id : undefined;\n\t} catch {\n\t\treturn undefined;\n\t} finally {\n\t\tif (fd !== undefined) {\n\t\t\ttry {\n\t\t\t\tfs.closeSync(fd);\n\t\t\t} catch {\n\t\t\t\t// Ignore close errors\n\t\t\t}\n\t\t}\n\t}\n}\n\nexport function findChildSessionFile(sessionDir: string, sessionId: string): string | undefined {\n\tif (!fs.existsSync(sessionDir)) return undefined;\n\n\tconst jsonlFiles: string[] = [];\n\n\tfunction collectJsonlFiles(dirPath: string) {\n\t\tlet currentEntries: fs.Dirent[];\n\t\ttry {\n\t\t\tcurrentEntries = fs.readdirSync(dirPath, { withFileTypes: true });\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\t\tfor (const entry of currentEntries) {\n\t\t\tconst fullPath = join(dirPath, entry.name);\n\t\t\tif (entry.isDirectory()) {\n\t\t\t\tcollectJsonlFiles(fullPath);\n\t\t\t} else if (entry.isFile() && entry.name.endsWith(\".jsonl\")) {\n\t\t\t\tjsonlFiles.push(fullPath);\n\t\t\t}\n\t\t}\n\t}\n\n\tcollectJsonlFiles(sessionDir);\n\n\tfor (const file of jsonlFiles) {\n\t\tconst base = basename(file);\n\t\tif (base === `${sessionId}.jsonl` || base.endsWith(`_${sessionId}.jsonl`)) {\n\t\t\treturn file;\n\t\t}\n\t}\n\n\tfor (const file of jsonlFiles) {\n\t\tconst headerId = readAutoLearnSessionIdFromFile(file);\n\t\tif (headerId === sessionId) {\n\t\t\treturn file;\n\t\t}\n\t}\n\n\treturn undefined;\n}\n\nexport function aggregateCumulativeUsageFromSessionEntries(entries: SessionEntry[]): Usage {\n\tlet input = 0;\n\tlet output = 0;\n\tlet cacheRead = 0;\n\tlet cacheWrite = 0;\n\tlet totalTokens = 0;\n\tlet costInput = 0;\n\tlet costOutput = 0;\n\tlet costCacheRead = 0;\n\tlet costCacheWrite = 0;\n\tlet costTotal = 0;\n\n\tconst add = (usage: Usage) => {\n\t\tinput += usage.input;\n\t\toutput += usage.output;\n\t\tcacheRead += usage.cacheRead;\n\t\tcacheWrite += usage.cacheWrite;\n\t\ttotalTokens += usage.totalTokens;\n\t\tcostInput += usage.cost.input;\n\t\tcostOutput += usage.cost.output;\n\t\tcostCacheRead += usage.cost.cacheRead;\n\t\tcostCacheWrite += usage.cost.cacheWrite;\n\t\tcostTotal += usage.cost.total;\n\t};\n\n\tconst seenSpawnedReportIds = new Set<string>();\n\tfor (const entry of entries) {\n\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\tconst usage = (entry.message as AssistantMessage).usage;\n\t\t\tif (usage && isUsage(usage)) {\n\t\t\t\tadd(usage);\n\t\t\t}\n\t\t} else if (entry.type === \"custom\" && entry.customType === SPAWNED_USAGE_CUSTOM_TYPE) {\n\t\t\tconst data = entry.data as SpawnedUsageReport | undefined;\n\t\t\tif (!data?.usage || !isUsage(data.usage)) continue;\n\t\t\tif (data.reportId) {\n\t\t\t\tif (seenSpawnedReportIds.has(data.reportId)) continue;\n\t\t\t\tseenSpawnedReportIds.add(data.reportId);\n\t\t\t}\n\t\t\tadd(data.usage);\n\t\t}\n\t}\n\n\treturn {\n\t\tinput,\n\t\toutput,\n\t\tcacheRead,\n\t\tcacheWrite,\n\t\ttotalTokens,\n\t\tcost: {\n\t\t\tinput: costInput,\n\t\t\toutput: costOutput,\n\t\t\tcacheRead: costCacheRead,\n\t\t\tcacheWrite: costCacheWrite,\n\t\t\ttotal: costTotal,\n\t\t},\n\t};\n}\n\nexport function reportCompletedAutoLearnUsageHelper(args: {\n\trunId: string;\n\tsessionDir: string;\n\tsessionId: string;\n\tlogPath?: string;\n\tparentSession: {\n\t\taddSpawnedUsage: (\n\t\t\tusage: Usage,\n\t\t\topts: { label: string; sourceSessionId: string; reportId: string },\n\t\t) => string | undefined;\n\t};\n\tappendLog?: (logPath: string, message: string) => void;\n}): void {\n\tconst { runId, sessionDir, sessionId, logPath, parentSession, appendLog } = args;\n\tconst sessionFile = findChildSessionFile(sessionDir, sessionId);\n\tif (!sessionFile) {\n\t\tif (logPath && appendLog) {\n\t\t\tappendLog(logPath, `Auto Learn usage report skipped: no child session file found for ${sessionId}.`);\n\t\t}\n\t\treturn;\n\t}\n\tconst fileEntries = loadEntriesFromFile(sessionFile);\n\tconst sessionEntries = fileEntries.filter((entry): entry is SessionEntry => entry.type !== \"session\");\n\tconst usage = aggregateCumulativeUsageFromSessionEntries(sessionEntries);\n\tif (usage.cost.total === 0 && usage.totalTokens === 0) {\n\t\tif (logPath && appendLog) {\n\t\t\tappendLog(logPath, `Auto Learn usage report skipped: child session had no usage for ${sessionId}.`);\n\t\t}\n\t\treturn;\n\t}\n\tparentSession.addSpawnedUsage(usage, {\n\t\tlabel: \"auto-learn\",\n\t\tsourceSessionId: sessionId,\n\t\treportId: `auto-learn:${runId}:${sessionId}`,\n\t});\n\tif (logPath && appendLog) {\n\t\tappendLog(logPath, `Auto Learn usage reported: ${sessionId}.`);\n\t}\n}\n"]}
|
|
@@ -11,6 +11,7 @@ import type { AgentMessage, AgentState } from "@caupulican/pi-agent-core";
|
|
|
11
11
|
import { type SessionManager } from "@caupulican/pi-agent-core/node";
|
|
12
12
|
import type { Model, ToolArgumentExecutionOutcome, ToolArgumentTeachState, ToolArgumentValidationTelemetryEvent, Usage } from "@caupulican/pi-ai";
|
|
13
13
|
import { type SessionStats, type SpawnedUsageTotals } from "./agent-session.ts";
|
|
14
|
+
import { type SessionCostSummary } from "./cost/cost-summary.ts";
|
|
14
15
|
import { type DailyUsageTotals } from "./cost/daily-usage.ts";
|
|
15
16
|
import type { ContextUsage, ToolDefinition } from "./extensions/index.ts";
|
|
16
17
|
import type { SettingsManager } from "./settings-manager.ts";
|
|
@@ -55,7 +56,7 @@ export interface SessionAnalyticsDeps {
|
|
|
55
56
|
export declare class SessionAnalytics {
|
|
56
57
|
/** Memoized spawned-usage totals, keyed by session entry count (Bug #22: O(1) between turns). */
|
|
57
58
|
private _spawnedUsageCache?;
|
|
58
|
-
/** Memoized daily usage totals with a short TTL, keyed by the resolved scope dir. */
|
|
59
|
+
/** Memoized daily usage totals with a short TTL, keyed by the resolved scope dir and local-day window. */
|
|
59
60
|
private _dailyUsageCache?;
|
|
60
61
|
/** Live recovery telemetry records already snapshotted for the worker; keyed to dedupe sidecar reads. */
|
|
61
62
|
private readonly _toolArgumentValidationRecords;
|
|
@@ -99,6 +100,7 @@ export declare class SessionAnalytics {
|
|
|
99
100
|
* instead of an O(N) scan on every keystroke (Bug #22). Recomputes only when entries change.
|
|
100
101
|
*/
|
|
101
102
|
getSpawnedUsage(): SpawnedUsageTotals;
|
|
103
|
+
getCostSummary(now?: Date): SessionCostSummary;
|
|
102
104
|
getDailyUsageTotals(now?: Date): DailyUsageTotals;
|
|
103
105
|
getDailyUsageBreakdown(formatLabel?: (label: string) => string, now?: Date): string;
|
|
104
106
|
getContextUsage(): ContextUsage | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-analytics.d.ts","sourceRoot":"","sources":["../../src/core/session-analytics.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Session usage / cost / stats accounting, context-window usage, and session export.\n *\n * Extracted verbatim from agent-session.ts (god-file decomposition). Read-only over the session\n * except for its two owned memo caches (`_spawnedUsageCache`, `_dailyUsageCache`) — it never mutates\n * agent or session state. Single source of truth for \"how much did this session and its spawned\n * subtree spend\" (footer roll-up, print-mode child reporting), the daily cross-session totals, the\n * /context window estimate, and HTML/JSONL export of the current branch.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport type { AgentMessage, AgentState } from \"@caupulican/pi-agent-core\";\nimport {\n\tCURRENT_SESSION_VERSION,\n\tcalculateContextTokens,\n\testimateContextTokens,\n\tgetLatestCompactionEntry,\n\ttype SessionHeader,\n\ttype SessionManager,\n} from \"@caupulican/pi-agent-core/node\";\nimport type {\n\tAssistantMessage,\n\tModel,\n\tToolArgumentExecutionOutcome,\n\tToolArgumentTeachState,\n\tToolArgumentValidationTelemetryEvent,\n\tUsage,\n} from \"@caupulican/pi-ai\";\nimport { getSessionsDir } from \"../config.ts\";\nimport { theme } from \"../modes/interactive/theme/theme.ts\";\nimport { resolvePath } from \"../utils/paths.ts\";\nimport {\n\ttype SessionStats,\n\tSPAWNED_USAGE_CUSTOM_TYPE,\n\ttype SpawnedUsageReport,\n\ttype SpawnedUsageTotals,\n} from \"./agent-session.ts\";\nimport {\n\taggregateDailyUsageFromSessionFiles,\n\taggregateDailyUsageFromSessionRoot,\n\ttype DailyUsageTotals,\n\tformatDailyUsageBreakdown,\n\tgetLocalDayWindow,\n} from \"./cost/daily-usage.ts\";\nimport { exportSessionToHtml, type ToolHtmlRenderer } from \"./export-html/index.ts\";\nimport { createToolHtmlRenderer } from \"./export-html/tool-renderer.ts\";\nimport type { ContextUsage, ToolDefinition } from \"./extensions/index.ts\";\nimport type { SettingsManager } from \"./settings-manager.ts\";\nimport {\n\tisToolArgumentValidationLogRecord,\n\ttype ToolArgumentValidationLogRecord,\n} from \"./tool-recovery-log-records.ts\";\n\nexport const TOOL_ARGUMENT_VALIDATION_CUSTOM_TYPE = \"tool_argument_validation\";\n\nexport interface ToolArgumentValidationTeachEfficacy {\n\trecurrenceBefore: number;\n\trecurrenceAfter: number;\n\trepairedThenSucceeded: number;\n\trepairedThenFailed: number;\n\trepairedThenNotRun: number;\n}\n\nexport interface ToolArgumentValidationStats {\n\tclean: number;\n\trepaired: number;\n\tbounced: number;\n\tfailureModes: Record<string, number>;\n\trepairsApplied: Record<string, number>;\n\ttaught: Record<ToolArgumentTeachState, number>;\n\texecutionOutcome: Record<ToolArgumentExecutionOutcome, number>;\n\tteachEfficacy: Record<string, ToolArgumentValidationTeachEfficacy>;\n}\n\nexport interface ToolArgumentValidationRecord extends ToolArgumentValidationTelemetryEvent {\n\tversion: 1;\n}\n\nexport interface SessionAnalyticsDeps {\n\t/** Live agent state — assistant-message usage and message counts are read from here. */\n\tgetState(): AgentState;\n\t/** All messages (agent state view) — used for context-window estimation and last-assistant text. */\n\tgetMessages(): AgentMessage[];\n\t/** Current session model — its context window bounds the /context estimate. */\n\tgetModel(): Model<any> | undefined;\n\t/** Session log — entries feed spawned-usage roll-up, daily totals, branch export. */\n\tgetSessionManager(): SessionManager;\n\t/** Settings — the export theme is read here. */\n\tgetSettingsManager(): SettingsManager;\n\t/** Resolve a tool definition for the HTML export's custom-tool renderer. */\n\tgetToolDefinition(name: string): ToolDefinition | undefined;\n\t/** Sidecar recovery telemetry log; read on demand so turn handling never writes session custom entries. */\n\tgetToolRecoveryEventLogPath(): string;\n}\n\nexport class SessionAnalytics {\n\t/** Memoized spawned-usage totals, keyed by session entry count (Bug #22: O(1) between turns). */\n\tprivate _spawnedUsageCache?: { entryCount: number; totals: SpawnedUsageTotals };\n\t/** Memoized daily usage totals with a short TTL, keyed by the resolved scope dir. */\n\tprivate _dailyUsageCache?: { sessionDir: string; expiresAt: number; totals: DailyUsageTotals };\n\t/** Live recovery telemetry records already snapshotted for the worker; keyed to dedupe sidecar reads. */\n\tprivate readonly _toolArgumentValidationRecords = new Map<string, ToolArgumentValidationLogRecord>();\n\n\tprivate readonly deps: SessionAnalyticsDeps;\n\n\tconstructor(deps: SessionAnalyticsDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\tgetSessionStats(): SessionStats {\n\t\tconst state = this.deps.getState();\n\t\tconst userMessages = state.messages.filter((m) => m.role === \"user\").length;\n\t\tconst assistantMessages = state.messages.filter((m) => m.role === \"assistant\").length;\n\t\tconst toolResults = state.messages.filter((m) => m.role === \"toolResult\").length;\n\n\t\tlet toolCalls = 0;\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\n\t\tfor (const message of state.messages) {\n\t\t\tif (message.role === \"assistant\") {\n\t\t\t\tconst assistantMsg = message as AssistantMessage;\n\t\t\t\ttoolCalls += assistantMsg.content.filter((c) => c.type === \"toolCall\").length;\n\t\t\t\ttotalInput += assistantMsg.usage.input;\n\t\t\t\ttotalOutput += assistantMsg.usage.output;\n\t\t\t\ttotalCacheRead += assistantMsg.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += assistantMsg.usage.cacheWrite;\n\t\t\t\ttotalCost += assistantMsg.usage.cost.total;\n\t\t\t}\n\t\t}\n\n\t\tconst toolArgumentValidation = this.getToolArgumentValidationStats();\n\n\t\treturn {\n\t\t\tsessionFile: this.deps.getSessionManager().getSessionFile(),\n\t\t\tsessionId: this.deps.getSessionManager().getSessionId(),\n\t\t\tuserMessages,\n\t\t\tassistantMessages,\n\t\t\ttoolCalls,\n\t\t\ttoolResults,\n\t\t\ttotalMessages: state.messages.length,\n\t\t\ttokens: {\n\t\t\t\tinput: totalInput,\n\t\t\t\toutput: totalOutput,\n\t\t\t\tcacheRead: totalCacheRead,\n\t\t\t\tcacheWrite: totalCacheWrite,\n\t\t\t\ttotal: totalInput + totalOutput + totalCacheRead + totalCacheWrite,\n\t\t\t},\n\t\t\tcost: totalCost,\n\t\t\tcontextUsage: this.getContextUsage(),\n\t\t\ttoolArgumentValidation,\n\t\t};\n\t}\n\n\trecordToolArgumentValidation(record: ToolArgumentValidationLogRecord): void {\n\t\tthis._toolArgumentValidationRecords.set(record.recordId, record);\n\t}\n\n\tprivate readToolArgumentValidationSidecarRecords(): ToolArgumentValidationLogRecord[] {\n\t\tconst filePath = this.deps.getToolRecoveryEventLogPath();\n\t\tif (!existsSync(filePath)) return [];\n\t\tconst records: ToolArgumentValidationLogRecord[] = [];\n\t\ttry {\n\t\t\tconst sessionId = this.deps.getSessionManager().getSessionId();\n\t\t\tfor (const line of readFileSync(filePath, \"utf-8\").split(\"\\n\")) {\n\t\t\t\tif (line.trim().length === 0) continue;\n\t\t\t\tconst parsed = JSON.parse(line) as unknown;\n\t\t\t\tif (!isToolArgumentValidationLogRecord(parsed) || parsed.sessionId !== sessionId) continue;\n\t\t\t\trecords.push(parsed);\n\t\t\t}\n\t\t} catch {\n\t\t\treturn [];\n\t\t}\n\t\treturn records;\n\t}\n\n\tgetToolArgumentValidationStats(): ToolArgumentValidationStats {\n\t\tconst stats: ToolArgumentValidationStats = {\n\t\t\tclean: 0,\n\t\t\trepaired: 0,\n\t\t\tbounced: 0,\n\t\t\tfailureModes: {},\n\t\t\trepairsApplied: {},\n\t\t\ttaught: { none: 0, note: 0, rule: 0 },\n\t\t\texecutionOutcome: { not_run: 0, succeeded: 0, failed: 0 },\n\t\t\tteachEfficacy: {},\n\t\t};\n\n\t\tconst getEfficacy = (record: ToolArgumentValidationRecord, mode: string): ToolArgumentValidationTeachEfficacy => {\n\t\t\tconst key = `${record.provider ?? \"unknown\"}/${record.model ?? \"unknown\"}:${mode}`;\n\t\t\tstats.teachEfficacy[key] ??= {\n\t\t\t\trecurrenceBefore: 0,\n\t\t\t\trecurrenceAfter: 0,\n\t\t\t\trepairedThenSucceeded: 0,\n\t\t\t\trepairedThenFailed: 0,\n\t\t\t\trepairedThenNotRun: 0,\n\t\t\t};\n\t\t\treturn stats.teachEfficacy[key];\n\t\t};\n\n\t\tconst seen = new Set<string>();\n\t\tconst consume = (record: ToolArgumentValidationRecord, key: string): void => {\n\t\t\tif (seen.has(key)) return;\n\t\t\tseen.add(key);\n\t\t\tstats[record.outcome] += 1;\n\t\t\tconst taught = record.taught ?? \"none\";\n\t\t\tconst executionOutcome = record.executionOutcome ?? \"not_run\";\n\t\t\tstats.taught[taught] += 1;\n\t\t\tstats.executionOutcome[executionOutcome] += 1;\n\t\t\tconst modes = new Set([...record.failureModes, ...record.repairsApplied]);\n\t\t\tfor (const mode of record.failureModes) {\n\t\t\t\tstats.failureModes[mode] = (stats.failureModes[mode] ?? 0) + 1;\n\t\t\t}\n\t\t\tfor (const repair of record.repairsApplied) {\n\t\t\t\tstats.repairsApplied[repair] = (stats.repairsApplied[repair] ?? 0) + 1;\n\t\t\t}\n\t\t\tfor (const mode of modes) {\n\t\t\t\tconst efficacy = getEfficacy(record, mode);\n\t\t\t\tif (taught === \"none\") {\n\t\t\t\t\tefficacy.recurrenceBefore++;\n\t\t\t\t} else {\n\t\t\t\t\tefficacy.recurrenceAfter++;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (record.outcome === \"repaired\") {\n\t\t\t\tfor (const repair of record.repairsApplied) {\n\t\t\t\t\tconst efficacy = getEfficacy(record, repair);\n\t\t\t\t\tif (executionOutcome === \"succeeded\") efficacy.repairedThenSucceeded++;\n\t\t\t\t\tif (executionOutcome === \"failed\") efficacy.repairedThenFailed++;\n\t\t\t\t\tif (executionOutcome === \"not_run\") efficacy.repairedThenNotRun++;\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\n\t\tfor (const entry of this.deps.getSessionManager().getEntries()) {\n\t\t\tif (entry.type !== \"custom\" || entry.customType !== TOOL_ARGUMENT_VALIDATION_CUSTOM_TYPE) continue;\n\t\t\tconst record = entry.data as ToolArgumentValidationRecord | undefined;\n\t\t\tif (!record || record.version !== 1) continue;\n\t\t\tconsume(record, `legacy:${entry.id}`);\n\t\t}\n\t\tfor (const record of this.readToolArgumentValidationSidecarRecords()) {\n\t\t\tconsume(record, record.recordId);\n\t\t}\n\t\tfor (const record of this._toolArgumentValidationRecords.values()) {\n\t\t\tconsume(record, record.recordId);\n\t\t}\n\n\t\treturn stats;\n\t}\n\n\t/**\n\t * Cumulative usage (full breakdown) for this session's entire spawn subtree: its own\n\t * assistant messages PLUS every `spawned_usage` report it has rolled up. Single source of\n\t * truth for \"how much did this session and everything it spawned spend\" — used by print-mode\n\t * to emit a child's total so a spawner can roll it up via {@link addSpawnedUsage}.\n\t *\n\t * Including the `spawned_usage` reports is what keeps the single-hop invariant intact: a child\n\t * that itself spawned grandchildren must report own + sub-usage in one number, or the parent\n\t * silently under-counts the grandchildren.\n\t */\n\tgetCumulativeUsage(): Usage {\n\t\tlet input = 0;\n\t\tlet output = 0;\n\t\tlet cacheRead = 0;\n\t\tlet cacheWrite = 0;\n\t\tlet totalTokens = 0;\n\t\tlet costInput = 0;\n\t\tlet costOutput = 0;\n\t\tlet costCacheRead = 0;\n\t\tlet costCacheWrite = 0;\n\t\tlet costTotal = 0;\n\t\tconst add = (usage: Usage) => {\n\t\t\tinput += usage.input;\n\t\t\toutput += usage.output;\n\t\t\tcacheRead += usage.cacheRead;\n\t\t\tcacheWrite += usage.cacheWrite;\n\t\t\ttotalTokens += usage.totalTokens;\n\t\t\tcostInput += usage.cost.input;\n\t\t\tcostOutput += usage.cost.output;\n\t\t\tcostCacheRead += usage.cost.cacheRead;\n\t\t\tcostCacheWrite += usage.cost.cacheWrite;\n\t\t\tcostTotal += usage.cost.total;\n\t\t};\n\t\tfor (const message of this.deps.getState().messages) {\n\t\t\tif (message.role !== \"assistant\") continue;\n\t\t\tconst usage = (message as AssistantMessage).usage;\n\t\t\tif (!usage) continue;\n\t\t\tadd(usage);\n\t\t}\n\t\t// Roll up usage this session attributed to its own spawned children (single-hop).\n\t\tfor (const entry of this.deps.getSessionManager().getEntries()) {\n\t\t\tif (entry.type !== \"custom\" || entry.customType !== SPAWNED_USAGE_CUSTOM_TYPE) continue;\n\t\t\tconst data = entry.data as SpawnedUsageReport | undefined;\n\t\t\tif (data?.usage) add(data.usage);\n\t\t}\n\t\treturn {\n\t\t\tinput,\n\t\t\toutput,\n\t\t\tcacheRead,\n\t\t\tcacheWrite,\n\t\t\ttotalTokens,\n\t\t\tcost: {\n\t\t\t\tinput: costInput,\n\t\t\t\toutput: costOutput,\n\t\t\t\tcacheRead: costCacheRead,\n\t\t\t\tcacheWrite: costCacheWrite,\n\t\t\t\ttotal: costTotal,\n\t\t\t},\n\t\t};\n\t}\n\n\t/**\n\t * Record usage spent by a spawned/subagent session so the footer can roll it into the\n\t * displayed cost. Persisted as a `CustomEntry` (`customType: \"spawned_usage\"`, Model A) so\n\t * it survives reload and is reconstructed exactly like main usage; a new/forked session\n\t * starts fresh because it owns a new log file.\n\t *\n\t * Idempotent on `opts.reportId`: a re-report (retry, duplicate `agent_end`) with a\n\t * previously-seen id is ignored, so cost cannot be double-counted. Honors the single-hop\n\t * invariant documented on {@link SpawnedUsageReport}.\n\t *\n\t * @returns the id of the appended entry, or `undefined` if the report was a duplicate.\n\t */\n\taddSpawnedUsage(\n\t\tusage: Usage,\n\t\topts?: { label?: string; sourceSessionId?: string; reportId?: string },\n\t): string | undefined {\n\t\tconst reportId = opts?.reportId;\n\t\tif (reportId) {\n\t\t\tfor (const entry of this.deps.getSessionManager().getEntries()) {\n\t\t\t\tif (\n\t\t\t\t\tentry.type === \"custom\" &&\n\t\t\t\t\tentry.customType === SPAWNED_USAGE_CUSTOM_TYPE &&\n\t\t\t\t\t(entry.data as SpawnedUsageReport | undefined)?.reportId === reportId\n\t\t\t\t) {\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tconst report: SpawnedUsageReport = {\n\t\t\tusage,\n\t\t\tlabel: opts?.label,\n\t\t\tsourceSessionId: opts?.sourceSessionId,\n\t\t\treportId,\n\t\t};\n\t\treturn this.deps.getSessionManager().appendCustomEntry(SPAWNED_USAGE_CUSTOM_TYPE, report);\n\t}\n\n\t/**\n\t * Aggregate all recorded spawned-usage reports (see {@link addSpawnedUsage}). Cached by the session\n\t * entry count so the interactive footer (which calls this every render frame) is O(1) between turns\n\t * instead of an O(N) scan on every keystroke (Bug #22). Recomputes only when entries change.\n\t */\n\tgetSpawnedUsage(): SpawnedUsageTotals {\n\t\tconst sessionManager = this.deps.getSessionManager();\n\t\tconst entryCount = sessionManager.getEntryCount?.() ?? sessionManager.getEntries().length;\n\t\tif (this._spawnedUsageCache?.entryCount === entryCount) return this._spawnedUsageCache.totals;\n\t\tlet cost = 0;\n\t\tlet reports = 0;\n\t\tfor (const entry of sessionManager.getEntries()) {\n\t\t\tif (entry.type !== \"custom\" || entry.customType !== SPAWNED_USAGE_CUSTOM_TYPE) continue;\n\t\t\tconst data = entry.data as SpawnedUsageReport | undefined;\n\t\t\tif (!data?.usage) continue;\n\t\t\tcost += data.usage.cost.total;\n\t\t\treports += 1;\n\t\t}\n\t\tconst totals: SpawnedUsageTotals = { cost, reports };\n\t\tthis._spawnedUsageCache = { entryCount, totals };\n\t\treturn totals;\n\t}\n\n\tgetDailyUsageTotals(now = new Date()): DailyUsageTotals {\n\t\tconst sessionManager = this.deps.getSessionManager();\n\t\tconst sessionDir = sessionManager.getSessionDir();\n\t\tconst scope = sessionManager.usesDefaultSessionDir() ? getSessionsDir() : sessionDir;\n\t\tconst nowMs = now.getTime();\n\t\tif (this._dailyUsageCache?.sessionDir === scope && this._dailyUsageCache.expiresAt > nowMs) {\n\t\t\treturn this._dailyUsageCache.totals;\n\t\t}\n\t\tconst window = getLocalDayWindow(now);\n\t\tconst totals = sessionManager.usesDefaultSessionDir()\n\t\t\t? aggregateDailyUsageFromSessionRoot(scope, window)\n\t\t\t: aggregateDailyUsageFromSessionFiles(sessionDir, window);\n\t\tthis._dailyUsageCache = { sessionDir: scope, expiresAt: nowMs + 10_000, totals };\n\t\treturn totals;\n\t}\n\n\tgetDailyUsageBreakdown(formatLabel?: (label: string) => string, now = new Date()): string {\n\t\treturn formatDailyUsageBreakdown(this.getDailyUsageTotals(now), formatLabel);\n\t}\n\n\tgetContextUsage(): ContextUsage | undefined {\n\t\tconst model = this.deps.getModel();\n\t\tif (!model) return undefined;\n\n\t\tconst contextWindow = model.contextWindow ?? 0;\n\t\tif (contextWindow <= 0) return undefined;\n\n\t\t// After compaction, the last assistant usage reflects pre-compaction context size.\n\t\t// We can only trust usage from an assistant that responded after the latest compaction.\n\t\t// If no such assistant exists, context token count is unknown until the next LLM response.\n\t\tconst branchEntries = this.deps.getSessionManager().getBranch();\n\t\tconst latestCompaction = getLatestCompactionEntry(branchEntries);\n\n\t\tif (latestCompaction) {\n\t\t\t// Check if there's a valid assistant usage after the compaction boundary\n\t\t\tconst compactionIndex = branchEntries.lastIndexOf(latestCompaction);\n\t\t\tlet hasPostCompactionUsage = false;\n\t\t\tfor (let i = branchEntries.length - 1; i > compactionIndex; i--) {\n\t\t\t\tconst entry = branchEntries[i];\n\t\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\t\tconst assistant = entry.message;\n\t\t\t\t\tif (assistant.stopReason !== \"aborted\" && assistant.stopReason !== \"error\") {\n\t\t\t\t\t\tconst contextTokens = calculateContextTokens(assistant.usage);\n\t\t\t\t\t\tif (contextTokens > 0) {\n\t\t\t\t\t\t\thasPostCompactionUsage = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!hasPostCompactionUsage) {\n\t\t\t\treturn { tokens: null, contextWindow, percent: null };\n\t\t\t}\n\t\t}\n\n\t\tconst estimate = estimateContextTokens(this.deps.getMessages());\n\t\tconst percent = (estimate.tokens / contextWindow) * 100;\n\n\t\treturn {\n\t\t\ttokens: estimate.tokens,\n\t\t\tcontextWindow,\n\t\t\tpercent,\n\t\t};\n\t}\n\n\t/**\n\t * Export session to HTML.\n\t * @param outputPath Optional output path (defaults to session directory)\n\t * @returns Path to exported file\n\t */\n\tasync exportToHtml(outputPath?: string): Promise<string> {\n\t\tconst sessionManager = this.deps.getSessionManager();\n\t\tconst themeName = this.deps.getSettingsManager().getTheme();\n\n\t\t// Create tool renderer if we have an extension runner (for custom tool HTML rendering)\n\t\tconst toolRenderer: ToolHtmlRenderer = createToolHtmlRenderer({\n\t\t\tgetToolDefinition: (name) => this.deps.getToolDefinition(name),\n\t\t\ttheme,\n\t\t\tcwd: sessionManager.getCwd(),\n\t\t});\n\n\t\treturn await exportSessionToHtml(sessionManager, this.deps.getState(), {\n\t\t\toutputPath,\n\t\t\tthemeName,\n\t\t\ttoolRenderer,\n\t\t});\n\t}\n\n\t/**\n\t * Export the current session branch to a JSONL file.\n\t * Writes the session header followed by all entries on the current branch path.\n\t * @param outputPath Target file path. If omitted, generates a timestamped file in cwd.\n\t * @returns The resolved output file path.\n\t */\n\texportToJsonl(outputPath?: string): string {\n\t\tconst sessionManager = this.deps.getSessionManager();\n\t\tconst filePath = resolvePath(\n\t\t\toutputPath ?? `session-${new Date().toISOString().replace(/[:.]/g, \"-\")}.jsonl`,\n\t\t\tprocess.cwd(),\n\t\t);\n\t\tconst dir = dirname(filePath);\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true });\n\t\t}\n\n\t\tconst header: SessionHeader = {\n\t\t\ttype: \"session\",\n\t\t\tversion: CURRENT_SESSION_VERSION,\n\t\t\tid: sessionManager.getSessionId(),\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tcwd: sessionManager.getCwd(),\n\t\t};\n\n\t\tconst branchEntries = sessionManager.getBranch();\n\t\tconst lines = [JSON.stringify(header)];\n\n\t\t// Re-chain parentIds to form a linear sequence\n\t\tlet prevId: string | null = null;\n\t\tfor (const entry of branchEntries) {\n\t\t\tconst linear = { ...entry, parentId: prevId };\n\t\t\tlines.push(JSON.stringify(linear));\n\t\t\tprevId = entry.id;\n\t\t}\n\n\t\twriteFileSync(filePath, `${lines.join(\"\\n\")}\\n`);\n\t\treturn filePath;\n\t}\n\n\t/**\n\t * Get text content of last assistant message.\n\t * Useful for /copy command.\n\t * @returns Text content, or undefined if no assistant message exists\n\t */\n\tgetLastAssistantText(): string | undefined {\n\t\tconst lastAssistant = this.deps\n\t\t\t.getMessages()\n\t\t\t.slice()\n\t\t\t.reverse()\n\t\t\t.find((m) => {\n\t\t\t\tif (m.role !== \"assistant\") return false;\n\t\t\t\tconst msg = m as AssistantMessage;\n\t\t\t\t// Skip aborted messages with no content\n\t\t\t\tif (msg.stopReason === \"aborted\" && msg.content.length === 0) return false;\n\t\t\t\treturn true;\n\t\t\t});\n\n\t\tif (!lastAssistant) return undefined;\n\n\t\tlet text = \"\";\n\t\tfor (const content of (lastAssistant as AssistantMessage).content) {\n\t\t\tif (content.type === \"text\") {\n\t\t\t\ttext += content.text;\n\t\t\t}\n\t\t}\n\n\t\treturn text.trim() || undefined;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"session-analytics.d.ts","sourceRoot":"","sources":["../../src/core/session-analytics.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Session usage / cost / stats accounting, context-window usage, and session export.\n *\n * Extracted verbatim from agent-session.ts (god-file decomposition). Read-only over the session\n * except for its two owned memo caches (`_spawnedUsageCache`, `_dailyUsageCache`) — it never mutates\n * agent or session state. Single source of truth for \"how much did this session and its spawned\n * subtree spend\" (footer roll-up, print-mode child reporting), the daily cross-session totals, the\n * /context window estimate, and HTML/JSONL export of the current branch.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport type { AgentMessage, AgentState } from \"@caupulican/pi-agent-core\";\nimport {\n\tCURRENT_SESSION_VERSION,\n\tcalculateContextTokens,\n\testimateContextTokens,\n\tgetLatestCompactionEntry,\n\ttype SessionHeader,\n\ttype SessionManager,\n} from \"@caupulican/pi-agent-core/node\";\nimport type {\n\tAssistantMessage,\n\tModel,\n\tToolArgumentExecutionOutcome,\n\tToolArgumentTeachState,\n\tToolArgumentValidationTelemetryEvent,\n\tUsage,\n} from \"@caupulican/pi-ai\";\nimport { getSessionsDir } from \"../config.ts\";\nimport { theme } from \"../modes/interactive/theme/theme.ts\";\nimport { resolvePath } from \"../utils/paths.ts\";\nimport {\n\ttype SessionStats,\n\tSPAWNED_USAGE_CUSTOM_TYPE,\n\ttype SpawnedUsageReport,\n\ttype SpawnedUsageTotals,\n} from \"./agent-session.ts\";\nimport {\n\taggregateCurrentSessionCostsFromEntries,\n\tcreateSessionCostSummary,\n\ttype SessionCostSummary,\n} from \"./cost/cost-summary.ts\";\nimport {\n\taggregateDailyUsageFromSessionFiles,\n\taggregateDailyUsageFromSessionRoot,\n\ttype DailyUsageTotals,\n\tformatDailyUsageBreakdown,\n\tgetLocalDayWindow,\n} from \"./cost/daily-usage.ts\";\nimport { exportSessionToHtml, type ToolHtmlRenderer } from \"./export-html/index.ts\";\nimport { createToolHtmlRenderer } from \"./export-html/tool-renderer.ts\";\nimport type { ContextUsage, ToolDefinition } from \"./extensions/index.ts\";\nimport type { SettingsManager } from \"./settings-manager.ts\";\nimport {\n\tisToolArgumentValidationLogRecord,\n\ttype ToolArgumentValidationLogRecord,\n} from \"./tool-recovery-log-records.ts\";\n\nexport const TOOL_ARGUMENT_VALIDATION_CUSTOM_TYPE = \"tool_argument_validation\";\n\nexport interface ToolArgumentValidationTeachEfficacy {\n\trecurrenceBefore: number;\n\trecurrenceAfter: number;\n\trepairedThenSucceeded: number;\n\trepairedThenFailed: number;\n\trepairedThenNotRun: number;\n}\n\nexport interface ToolArgumentValidationStats {\n\tclean: number;\n\trepaired: number;\n\tbounced: number;\n\tfailureModes: Record<string, number>;\n\trepairsApplied: Record<string, number>;\n\ttaught: Record<ToolArgumentTeachState, number>;\n\texecutionOutcome: Record<ToolArgumentExecutionOutcome, number>;\n\tteachEfficacy: Record<string, ToolArgumentValidationTeachEfficacy>;\n}\n\nexport interface ToolArgumentValidationRecord extends ToolArgumentValidationTelemetryEvent {\n\tversion: 1;\n}\n\nexport interface SessionAnalyticsDeps {\n\t/** Live agent state — assistant-message usage and message counts are read from here. */\n\tgetState(): AgentState;\n\t/** All messages (agent state view) — used for context-window estimation and last-assistant text. */\n\tgetMessages(): AgentMessage[];\n\t/** Current session model — its context window bounds the /context estimate. */\n\tgetModel(): Model<any> | undefined;\n\t/** Session log — entries feed spawned-usage roll-up, daily totals, branch export. */\n\tgetSessionManager(): SessionManager;\n\t/** Settings — the export theme is read here. */\n\tgetSettingsManager(): SettingsManager;\n\t/** Resolve a tool definition for the HTML export's custom-tool renderer. */\n\tgetToolDefinition(name: string): ToolDefinition | undefined;\n\t/** Sidecar recovery telemetry log; read on demand so turn handling never writes session custom entries. */\n\tgetToolRecoveryEventLogPath(): string;\n}\n\nexport class SessionAnalytics {\n\t/** Memoized spawned-usage totals, keyed by session entry count (Bug #22: O(1) between turns). */\n\tprivate _spawnedUsageCache?: { entryCount: number; totals: SpawnedUsageTotals };\n\t/** Memoized daily usage totals with a short TTL, keyed by the resolved scope dir and local-day window. */\n\tprivate _dailyUsageCache?: {\n\t\tsessionDir: string;\n\t\twindowStartMs: number;\n\t\twindowEndMs: number;\n\t\texpiresAt: number;\n\t\ttotals: DailyUsageTotals;\n\t};\n\t/** Live recovery telemetry records already snapshotted for the worker; keyed to dedupe sidecar reads. */\n\tprivate readonly _toolArgumentValidationRecords = new Map<string, ToolArgumentValidationLogRecord>();\n\n\tprivate readonly deps: SessionAnalyticsDeps;\n\n\tconstructor(deps: SessionAnalyticsDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\tgetSessionStats(): SessionStats {\n\t\tconst state = this.deps.getState();\n\t\tconst userMessages = state.messages.filter((m) => m.role === \"user\").length;\n\t\tconst assistantMessages = state.messages.filter((m) => m.role === \"assistant\").length;\n\t\tconst toolResults = state.messages.filter((m) => m.role === \"toolResult\").length;\n\n\t\tlet toolCalls = 0;\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\n\t\tfor (const message of state.messages) {\n\t\t\tif (message.role === \"assistant\") {\n\t\t\t\tconst assistantMsg = message as AssistantMessage;\n\t\t\t\ttoolCalls += assistantMsg.content.filter((c) => c.type === \"toolCall\").length;\n\t\t\t\ttotalInput += assistantMsg.usage.input;\n\t\t\t\ttotalOutput += assistantMsg.usage.output;\n\t\t\t\ttotalCacheRead += assistantMsg.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += assistantMsg.usage.cacheWrite;\n\t\t\t\ttotalCost += assistantMsg.usage.cost.total;\n\t\t\t}\n\t\t}\n\n\t\tconst toolArgumentValidation = this.getToolArgumentValidationStats();\n\n\t\treturn {\n\t\t\tsessionFile: this.deps.getSessionManager().getSessionFile(),\n\t\t\tsessionId: this.deps.getSessionManager().getSessionId(),\n\t\t\tuserMessages,\n\t\t\tassistantMessages,\n\t\t\ttoolCalls,\n\t\t\ttoolResults,\n\t\t\ttotalMessages: state.messages.length,\n\t\t\ttokens: {\n\t\t\t\tinput: totalInput,\n\t\t\t\toutput: totalOutput,\n\t\t\t\tcacheRead: totalCacheRead,\n\t\t\t\tcacheWrite: totalCacheWrite,\n\t\t\t\ttotal: totalInput + totalOutput + totalCacheRead + totalCacheWrite,\n\t\t\t},\n\t\t\tcost: totalCost,\n\t\t\tcontextUsage: this.getContextUsage(),\n\t\t\ttoolArgumentValidation,\n\t\t};\n\t}\n\n\trecordToolArgumentValidation(record: ToolArgumentValidationLogRecord): void {\n\t\tthis._toolArgumentValidationRecords.set(record.recordId, record);\n\t}\n\n\tprivate readToolArgumentValidationSidecarRecords(): ToolArgumentValidationLogRecord[] {\n\t\tconst filePath = this.deps.getToolRecoveryEventLogPath();\n\t\tif (!existsSync(filePath)) return [];\n\t\tconst records: ToolArgumentValidationLogRecord[] = [];\n\t\ttry {\n\t\t\tconst sessionId = this.deps.getSessionManager().getSessionId();\n\t\t\tfor (const line of readFileSync(filePath, \"utf-8\").split(\"\\n\")) {\n\t\t\t\tif (line.trim().length === 0) continue;\n\t\t\t\tconst parsed = JSON.parse(line) as unknown;\n\t\t\t\tif (!isToolArgumentValidationLogRecord(parsed) || parsed.sessionId !== sessionId) continue;\n\t\t\t\trecords.push(parsed);\n\t\t\t}\n\t\t} catch {\n\t\t\treturn [];\n\t\t}\n\t\treturn records;\n\t}\n\n\tgetToolArgumentValidationStats(): ToolArgumentValidationStats {\n\t\tconst stats: ToolArgumentValidationStats = {\n\t\t\tclean: 0,\n\t\t\trepaired: 0,\n\t\t\tbounced: 0,\n\t\t\tfailureModes: {},\n\t\t\trepairsApplied: {},\n\t\t\ttaught: { none: 0, note: 0, rule: 0 },\n\t\t\texecutionOutcome: { not_run: 0, succeeded: 0, failed: 0 },\n\t\t\tteachEfficacy: {},\n\t\t};\n\n\t\tconst getEfficacy = (record: ToolArgumentValidationRecord, mode: string): ToolArgumentValidationTeachEfficacy => {\n\t\t\tconst key = `${record.provider ?? \"unknown\"}/${record.model ?? \"unknown\"}:${mode}`;\n\t\t\tstats.teachEfficacy[key] ??= {\n\t\t\t\trecurrenceBefore: 0,\n\t\t\t\trecurrenceAfter: 0,\n\t\t\t\trepairedThenSucceeded: 0,\n\t\t\t\trepairedThenFailed: 0,\n\t\t\t\trepairedThenNotRun: 0,\n\t\t\t};\n\t\t\treturn stats.teachEfficacy[key];\n\t\t};\n\n\t\tconst seen = new Set<string>();\n\t\tconst consume = (record: ToolArgumentValidationRecord, key: string): void => {\n\t\t\tif (seen.has(key)) return;\n\t\t\tseen.add(key);\n\t\t\tstats[record.outcome] += 1;\n\t\t\tconst taught = record.taught ?? \"none\";\n\t\t\tconst executionOutcome = record.executionOutcome ?? \"not_run\";\n\t\t\tstats.taught[taught] += 1;\n\t\t\tstats.executionOutcome[executionOutcome] += 1;\n\t\t\tconst modes = new Set([...record.failureModes, ...record.repairsApplied]);\n\t\t\tfor (const mode of record.failureModes) {\n\t\t\t\tstats.failureModes[mode] = (stats.failureModes[mode] ?? 0) + 1;\n\t\t\t}\n\t\t\tfor (const repair of record.repairsApplied) {\n\t\t\t\tstats.repairsApplied[repair] = (stats.repairsApplied[repair] ?? 0) + 1;\n\t\t\t}\n\t\t\tfor (const mode of modes) {\n\t\t\t\tconst efficacy = getEfficacy(record, mode);\n\t\t\t\tif (taught === \"none\") {\n\t\t\t\t\tefficacy.recurrenceBefore++;\n\t\t\t\t} else {\n\t\t\t\t\tefficacy.recurrenceAfter++;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (record.outcome === \"repaired\") {\n\t\t\t\tfor (const repair of record.repairsApplied) {\n\t\t\t\t\tconst efficacy = getEfficacy(record, repair);\n\t\t\t\t\tif (executionOutcome === \"succeeded\") efficacy.repairedThenSucceeded++;\n\t\t\t\t\tif (executionOutcome === \"failed\") efficacy.repairedThenFailed++;\n\t\t\t\t\tif (executionOutcome === \"not_run\") efficacy.repairedThenNotRun++;\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\n\t\tfor (const entry of this.deps.getSessionManager().getEntries()) {\n\t\t\tif (entry.type !== \"custom\" || entry.customType !== TOOL_ARGUMENT_VALIDATION_CUSTOM_TYPE) continue;\n\t\t\tconst record = entry.data as ToolArgumentValidationRecord | undefined;\n\t\t\tif (!record || record.version !== 1) continue;\n\t\t\tconsume(record, `legacy:${entry.id}`);\n\t\t}\n\t\tfor (const record of this.readToolArgumentValidationSidecarRecords()) {\n\t\t\tconsume(record, record.recordId);\n\t\t}\n\t\tfor (const record of this._toolArgumentValidationRecords.values()) {\n\t\t\tconsume(record, record.recordId);\n\t\t}\n\n\t\treturn stats;\n\t}\n\n\t/**\n\t * Cumulative usage (full breakdown) for this session's entire spawn subtree: its own\n\t * assistant messages PLUS every `spawned_usage` report it has rolled up. Single source of\n\t * truth for \"how much did this session and everything it spawned spend\" — used by print-mode\n\t * to emit a child's total so a spawner can roll it up via {@link addSpawnedUsage}.\n\t *\n\t * Including the `spawned_usage` reports is what keeps the single-hop invariant intact: a child\n\t * that itself spawned grandchildren must report own + sub-usage in one number, or the parent\n\t * silently under-counts the grandchildren.\n\t */\n\tgetCumulativeUsage(): Usage {\n\t\tlet input = 0;\n\t\tlet output = 0;\n\t\tlet cacheRead = 0;\n\t\tlet cacheWrite = 0;\n\t\tlet totalTokens = 0;\n\t\tlet costInput = 0;\n\t\tlet costOutput = 0;\n\t\tlet costCacheRead = 0;\n\t\tlet costCacheWrite = 0;\n\t\tlet costTotal = 0;\n\t\tconst add = (usage: Usage) => {\n\t\t\tinput += usage.input;\n\t\t\toutput += usage.output;\n\t\t\tcacheRead += usage.cacheRead;\n\t\t\tcacheWrite += usage.cacheWrite;\n\t\t\ttotalTokens += usage.totalTokens;\n\t\t\tcostInput += usage.cost.input;\n\t\t\tcostOutput += usage.cost.output;\n\t\t\tcostCacheRead += usage.cost.cacheRead;\n\t\t\tcostCacheWrite += usage.cost.cacheWrite;\n\t\t\tcostTotal += usage.cost.total;\n\t\t};\n\t\tfor (const message of this.deps.getState().messages) {\n\t\t\tif (message.role !== \"assistant\") continue;\n\t\t\tconst usage = (message as AssistantMessage).usage;\n\t\t\tif (!usage) continue;\n\t\t\tadd(usage);\n\t\t}\n\t\t// Roll up usage this session attributed to its own spawned children (single-hop).\n\t\tconst seenSpawnedReportIds = new Set<string>();\n\t\tfor (const entry of this.deps.getSessionManager().getEntries()) {\n\t\t\tif (entry.type !== \"custom\" || entry.customType !== SPAWNED_USAGE_CUSTOM_TYPE) continue;\n\t\t\tconst data = entry.data as SpawnedUsageReport | undefined;\n\t\t\tif (!data?.usage) continue;\n\t\t\tif (data.reportId) {\n\t\t\t\tif (seenSpawnedReportIds.has(data.reportId)) continue;\n\t\t\t\tseenSpawnedReportIds.add(data.reportId);\n\t\t\t}\n\t\t\tadd(data.usage);\n\t\t}\n\t\treturn {\n\t\t\tinput,\n\t\t\toutput,\n\t\t\tcacheRead,\n\t\t\tcacheWrite,\n\t\t\ttotalTokens,\n\t\t\tcost: {\n\t\t\t\tinput: costInput,\n\t\t\t\toutput: costOutput,\n\t\t\t\tcacheRead: costCacheRead,\n\t\t\t\tcacheWrite: costCacheWrite,\n\t\t\t\ttotal: costTotal,\n\t\t\t},\n\t\t};\n\t}\n\n\t/**\n\t * Record usage spent by a spawned/subagent session so the footer can roll it into the\n\t * displayed cost. Persisted as a `CustomEntry` (`customType: \"spawned_usage\"`, Model A) so\n\t * it survives reload and is reconstructed exactly like main usage; a new/forked session\n\t * starts fresh because it owns a new log file.\n\t *\n\t * Idempotent on `opts.reportId`: a re-report (retry, duplicate `agent_end`) with a\n\t * previously-seen id is ignored, so cost cannot be double-counted. Honors the single-hop\n\t * invariant documented on {@link SpawnedUsageReport}.\n\t *\n\t * @returns the id of the appended entry, or `undefined` if the report was a duplicate.\n\t */\n\taddSpawnedUsage(\n\t\tusage: Usage,\n\t\topts?: { label?: string; sourceSessionId?: string; reportId?: string },\n\t): string | undefined {\n\t\tconst reportId = opts?.reportId;\n\t\tif (reportId) {\n\t\t\tfor (const entry of this.deps.getSessionManager().getEntries()) {\n\t\t\t\tif (\n\t\t\t\t\tentry.type === \"custom\" &&\n\t\t\t\t\tentry.customType === SPAWNED_USAGE_CUSTOM_TYPE &&\n\t\t\t\t\t(entry.data as SpawnedUsageReport | undefined)?.reportId === reportId\n\t\t\t\t) {\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tconst report: SpawnedUsageReport = {\n\t\t\tusage,\n\t\t\tlabel: opts?.label,\n\t\t\tsourceSessionId: opts?.sourceSessionId,\n\t\t\treportId,\n\t\t};\n\t\treturn this.deps.getSessionManager().appendCustomEntry(SPAWNED_USAGE_CUSTOM_TYPE, report);\n\t}\n\n\t/**\n\t * Aggregate all recorded spawned-usage reports (see {@link addSpawnedUsage}). Cached by the session\n\t * entry count so the interactive footer (which calls this every render frame) is O(1) between turns\n\t * instead of an O(N) scan on every keystroke (Bug #22). Recomputes only when entries change.\n\t */\n\tgetSpawnedUsage(): SpawnedUsageTotals {\n\t\tconst sessionManager = this.deps.getSessionManager();\n\t\tconst entryCount = sessionManager.getEntryCount?.() ?? sessionManager.getEntries().length;\n\t\tif (this._spawnedUsageCache?.entryCount === entryCount) return this._spawnedUsageCache.totals;\n\t\tconst current = aggregateCurrentSessionCostsFromEntries(sessionManager.getEntries());\n\t\tconst totals: SpawnedUsageTotals = { cost: current.subagentCost, reports: current.subagentReports };\n\t\tthis._spawnedUsageCache = { entryCount, totals };\n\t\treturn totals;\n\t}\n\n\tgetCostSummary(now = new Date()): SessionCostSummary {\n\t\tconst window = getLocalDayWindow(now);\n\t\treturn createSessionCostSummary({\n\t\t\tentries: this.deps.getSessionManager().getEntries(),\n\t\t\tdailyTotals: this.getDailyUsageTotals(now),\n\t\t\ttodayWindow: window,\n\t\t});\n\t}\n\n\tgetDailyUsageTotals(now = new Date()): DailyUsageTotals {\n\t\tconst sessionManager = this.deps.getSessionManager();\n\t\tconst sessionDir = sessionManager.getSessionDir();\n\t\tconst scope = sessionManager.usesDefaultSessionDir() ? getSessionsDir() : sessionDir;\n\t\tconst nowMs = now.getTime();\n\t\tconst window = getLocalDayWindow(now);\n\t\tif (\n\t\t\tthis._dailyUsageCache?.sessionDir === scope &&\n\t\t\tthis._dailyUsageCache.windowStartMs === window.startMs &&\n\t\t\tthis._dailyUsageCache.windowEndMs === window.endMs &&\n\t\t\tthis._dailyUsageCache.expiresAt > nowMs\n\t\t) {\n\t\t\treturn this._dailyUsageCache.totals;\n\t\t}\n\t\tconst totals = sessionManager.usesDefaultSessionDir()\n\t\t\t? aggregateDailyUsageFromSessionRoot(scope, window)\n\t\t\t: aggregateDailyUsageFromSessionFiles(sessionDir, window);\n\t\tthis._dailyUsageCache = {\n\t\t\tsessionDir: scope,\n\t\t\twindowStartMs: window.startMs,\n\t\t\twindowEndMs: window.endMs,\n\t\t\texpiresAt: Math.min(nowMs + 10_000, window.endMs),\n\t\t\ttotals,\n\t\t};\n\t\treturn totals;\n\t}\n\n\tgetDailyUsageBreakdown(formatLabel?: (label: string) => string, now = new Date()): string {\n\t\treturn formatDailyUsageBreakdown(this.getDailyUsageTotals(now), formatLabel);\n\t}\n\n\tgetContextUsage(): ContextUsage | undefined {\n\t\tconst model = this.deps.getModel();\n\t\tif (!model) return undefined;\n\n\t\tconst contextWindow = model.contextWindow ?? 0;\n\t\tif (contextWindow <= 0) return undefined;\n\n\t\t// After compaction, the last assistant usage reflects pre-compaction context size.\n\t\t// We can only trust usage from an assistant that responded after the latest compaction.\n\t\t// If no such assistant exists, context token count is unknown until the next LLM response.\n\t\tconst branchEntries = this.deps.getSessionManager().getBranch();\n\t\tconst latestCompaction = getLatestCompactionEntry(branchEntries);\n\n\t\tif (latestCompaction) {\n\t\t\t// Check if there's a valid assistant usage after the compaction boundary\n\t\t\tconst compactionIndex = branchEntries.lastIndexOf(latestCompaction);\n\t\t\tlet hasPostCompactionUsage = false;\n\t\t\tfor (let i = branchEntries.length - 1; i > compactionIndex; i--) {\n\t\t\t\tconst entry = branchEntries[i];\n\t\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\t\tconst assistant = entry.message;\n\t\t\t\t\tif (assistant.stopReason !== \"aborted\" && assistant.stopReason !== \"error\") {\n\t\t\t\t\t\tconst contextTokens = calculateContextTokens(assistant.usage);\n\t\t\t\t\t\tif (contextTokens > 0) {\n\t\t\t\t\t\t\thasPostCompactionUsage = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!hasPostCompactionUsage) {\n\t\t\t\treturn { tokens: null, contextWindow, percent: null };\n\t\t\t}\n\t\t}\n\n\t\tconst estimate = estimateContextTokens(this.deps.getMessages());\n\t\tconst percent = (estimate.tokens / contextWindow) * 100;\n\n\t\treturn {\n\t\t\ttokens: estimate.tokens,\n\t\t\tcontextWindow,\n\t\t\tpercent,\n\t\t};\n\t}\n\n\t/**\n\t * Export session to HTML.\n\t * @param outputPath Optional output path (defaults to session directory)\n\t * @returns Path to exported file\n\t */\n\tasync exportToHtml(outputPath?: string): Promise<string> {\n\t\tconst sessionManager = this.deps.getSessionManager();\n\t\tconst themeName = this.deps.getSettingsManager().getTheme();\n\n\t\t// Create tool renderer if we have an extension runner (for custom tool HTML rendering)\n\t\tconst toolRenderer: ToolHtmlRenderer = createToolHtmlRenderer({\n\t\t\tgetToolDefinition: (name) => this.deps.getToolDefinition(name),\n\t\t\ttheme,\n\t\t\tcwd: sessionManager.getCwd(),\n\t\t});\n\n\t\treturn await exportSessionToHtml(sessionManager, this.deps.getState(), {\n\t\t\toutputPath,\n\t\t\tthemeName,\n\t\t\ttoolRenderer,\n\t\t});\n\t}\n\n\t/**\n\t * Export the current session branch to a JSONL file.\n\t * Writes the session header followed by all entries on the current branch path.\n\t * @param outputPath Target file path. If omitted, generates a timestamped file in cwd.\n\t * @returns The resolved output file path.\n\t */\n\texportToJsonl(outputPath?: string): string {\n\t\tconst sessionManager = this.deps.getSessionManager();\n\t\tconst filePath = resolvePath(\n\t\t\toutputPath ?? `session-${new Date().toISOString().replace(/[:.]/g, \"-\")}.jsonl`,\n\t\t\tprocess.cwd(),\n\t\t);\n\t\tconst dir = dirname(filePath);\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true });\n\t\t}\n\n\t\tconst header: SessionHeader = {\n\t\t\ttype: \"session\",\n\t\t\tversion: CURRENT_SESSION_VERSION,\n\t\t\tid: sessionManager.getSessionId(),\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tcwd: sessionManager.getCwd(),\n\t\t};\n\n\t\tconst branchEntries = sessionManager.getBranch();\n\t\tconst lines = [JSON.stringify(header)];\n\n\t\t// Re-chain parentIds to form a linear sequence\n\t\tlet prevId: string | null = null;\n\t\tfor (const entry of branchEntries) {\n\t\t\tconst linear = { ...entry, parentId: prevId };\n\t\t\tlines.push(JSON.stringify(linear));\n\t\t\tprevId = entry.id;\n\t\t}\n\n\t\twriteFileSync(filePath, `${lines.join(\"\\n\")}\\n`);\n\t\treturn filePath;\n\t}\n\n\t/**\n\t * Get text content of last assistant message.\n\t * Useful for /copy command.\n\t * @returns Text content, or undefined if no assistant message exists\n\t */\n\tgetLastAssistantText(): string | undefined {\n\t\tconst lastAssistant = this.deps\n\t\t\t.getMessages()\n\t\t\t.slice()\n\t\t\t.reverse()\n\t\t\t.find((m) => {\n\t\t\t\tif (m.role !== \"assistant\") return false;\n\t\t\t\tconst msg = m as AssistantMessage;\n\t\t\t\t// Skip aborted messages with no content\n\t\t\t\tif (msg.stopReason === \"aborted\" && msg.content.length === 0) return false;\n\t\t\t\treturn true;\n\t\t\t});\n\n\t\tif (!lastAssistant) return undefined;\n\n\t\tlet text = \"\";\n\t\tfor (const content of (lastAssistant as AssistantMessage).content) {\n\t\t\tif (content.type === \"text\") {\n\t\t\t\ttext += content.text;\n\t\t\t}\n\t\t}\n\n\t\treturn text.trim() || undefined;\n\t}\n}\n"]}
|
|
@@ -14,6 +14,7 @@ import { getSessionsDir } from "../config.js";
|
|
|
14
14
|
import { theme } from "../modes/interactive/theme/theme.js";
|
|
15
15
|
import { resolvePath } from "../utils/paths.js";
|
|
16
16
|
import { SPAWNED_USAGE_CUSTOM_TYPE, } from "./agent-session.js";
|
|
17
|
+
import { aggregateCurrentSessionCostsFromEntries, createSessionCostSummary, } from "./cost/cost-summary.js";
|
|
17
18
|
import { aggregateDailyUsageFromSessionFiles, aggregateDailyUsageFromSessionRoot, formatDailyUsageBreakdown, getLocalDayWindow, } from "./cost/daily-usage.js";
|
|
18
19
|
import { exportSessionToHtml } from "./export-html/index.js";
|
|
19
20
|
import { createToolHtmlRenderer } from "./export-html/tool-renderer.js";
|
|
@@ -22,7 +23,7 @@ export const TOOL_ARGUMENT_VALIDATION_CUSTOM_TYPE = "tool_argument_validation";
|
|
|
22
23
|
export class SessionAnalytics {
|
|
23
24
|
/** Memoized spawned-usage totals, keyed by session entry count (Bug #22: O(1) between turns). */
|
|
24
25
|
_spawnedUsageCache;
|
|
25
|
-
/** Memoized daily usage totals with a short TTL, keyed by the resolved scope dir. */
|
|
26
|
+
/** Memoized daily usage totals with a short TTL, keyed by the resolved scope dir and local-day window. */
|
|
26
27
|
_dailyUsageCache;
|
|
27
28
|
/** Live recovery telemetry records already snapshotted for the worker; keyed to dedupe sidecar reads. */
|
|
28
29
|
_toolArgumentValidationRecords = new Map();
|
|
@@ -215,12 +216,19 @@ export class SessionAnalytics {
|
|
|
215
216
|
add(usage);
|
|
216
217
|
}
|
|
217
218
|
// Roll up usage this session attributed to its own spawned children (single-hop).
|
|
219
|
+
const seenSpawnedReportIds = new Set();
|
|
218
220
|
for (const entry of this.deps.getSessionManager().getEntries()) {
|
|
219
221
|
if (entry.type !== "custom" || entry.customType !== SPAWNED_USAGE_CUSTOM_TYPE)
|
|
220
222
|
continue;
|
|
221
223
|
const data = entry.data;
|
|
222
|
-
if (data?.usage)
|
|
223
|
-
|
|
224
|
+
if (!data?.usage)
|
|
225
|
+
continue;
|
|
226
|
+
if (data.reportId) {
|
|
227
|
+
if (seenSpawnedReportIds.has(data.reportId))
|
|
228
|
+
continue;
|
|
229
|
+
seenSpawnedReportIds.add(data.reportId);
|
|
230
|
+
}
|
|
231
|
+
add(data.usage);
|
|
224
232
|
}
|
|
225
233
|
return {
|
|
226
234
|
input,
|
|
@@ -278,34 +286,41 @@ export class SessionAnalytics {
|
|
|
278
286
|
const entryCount = sessionManager.getEntryCount?.() ?? sessionManager.getEntries().length;
|
|
279
287
|
if (this._spawnedUsageCache?.entryCount === entryCount)
|
|
280
288
|
return this._spawnedUsageCache.totals;
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
for (const entry of sessionManager.getEntries()) {
|
|
284
|
-
if (entry.type !== "custom" || entry.customType !== SPAWNED_USAGE_CUSTOM_TYPE)
|
|
285
|
-
continue;
|
|
286
|
-
const data = entry.data;
|
|
287
|
-
if (!data?.usage)
|
|
288
|
-
continue;
|
|
289
|
-
cost += data.usage.cost.total;
|
|
290
|
-
reports += 1;
|
|
291
|
-
}
|
|
292
|
-
const totals = { cost, reports };
|
|
289
|
+
const current = aggregateCurrentSessionCostsFromEntries(sessionManager.getEntries());
|
|
290
|
+
const totals = { cost: current.subagentCost, reports: current.subagentReports };
|
|
293
291
|
this._spawnedUsageCache = { entryCount, totals };
|
|
294
292
|
return totals;
|
|
295
293
|
}
|
|
294
|
+
getCostSummary(now = new Date()) {
|
|
295
|
+
const window = getLocalDayWindow(now);
|
|
296
|
+
return createSessionCostSummary({
|
|
297
|
+
entries: this.deps.getSessionManager().getEntries(),
|
|
298
|
+
dailyTotals: this.getDailyUsageTotals(now),
|
|
299
|
+
todayWindow: window,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
296
302
|
getDailyUsageTotals(now = new Date()) {
|
|
297
303
|
const sessionManager = this.deps.getSessionManager();
|
|
298
304
|
const sessionDir = sessionManager.getSessionDir();
|
|
299
305
|
const scope = sessionManager.usesDefaultSessionDir() ? getSessionsDir() : sessionDir;
|
|
300
306
|
const nowMs = now.getTime();
|
|
301
|
-
|
|
307
|
+
const window = getLocalDayWindow(now);
|
|
308
|
+
if (this._dailyUsageCache?.sessionDir === scope &&
|
|
309
|
+
this._dailyUsageCache.windowStartMs === window.startMs &&
|
|
310
|
+
this._dailyUsageCache.windowEndMs === window.endMs &&
|
|
311
|
+
this._dailyUsageCache.expiresAt > nowMs) {
|
|
302
312
|
return this._dailyUsageCache.totals;
|
|
303
313
|
}
|
|
304
|
-
const window = getLocalDayWindow(now);
|
|
305
314
|
const totals = sessionManager.usesDefaultSessionDir()
|
|
306
315
|
? aggregateDailyUsageFromSessionRoot(scope, window)
|
|
307
316
|
: aggregateDailyUsageFromSessionFiles(sessionDir, window);
|
|
308
|
-
this._dailyUsageCache = {
|
|
317
|
+
this._dailyUsageCache = {
|
|
318
|
+
sessionDir: scope,
|
|
319
|
+
windowStartMs: window.startMs,
|
|
320
|
+
windowEndMs: window.endMs,
|
|
321
|
+
expiresAt: Math.min(nowMs + 10_000, window.endMs),
|
|
322
|
+
totals,
|
|
323
|
+
};
|
|
309
324
|
return totals;
|
|
310
325
|
}
|
|
311
326
|
getDailyUsageBreakdown(formatLabel, now = new Date()) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-analytics.js","sourceRoot":"","sources":["../../src/core/session-analytics.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Session usage / cost / stats accounting, context-window usage, and session export.\n *\n * Extracted verbatim from agent-session.ts (god-file decomposition). Read-only over the session\n * except for its two owned memo caches (`_spawnedUsageCache`, `_dailyUsageCache`) — it never mutates\n * agent or session state. Single source of truth for \"how much did this session and its spawned\n * subtree spend\" (footer roll-up, print-mode child reporting), the daily cross-session totals, the\n * /context window estimate, and HTML/JSONL export of the current branch.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport type { AgentMessage, AgentState } from \"@caupulican/pi-agent-core\";\nimport {\n\tCURRENT_SESSION_VERSION,\n\tcalculateContextTokens,\n\testimateContextTokens,\n\tgetLatestCompactionEntry,\n\ttype SessionHeader,\n\ttype SessionManager,\n} from \"@caupulican/pi-agent-core/node\";\nimport type {\n\tAssistantMessage,\n\tModel,\n\tToolArgumentExecutionOutcome,\n\tToolArgumentTeachState,\n\tToolArgumentValidationTelemetryEvent,\n\tUsage,\n} from \"@caupulican/pi-ai\";\nimport { getSessionsDir } from \"../config.ts\";\nimport { theme } from \"../modes/interactive/theme/theme.ts\";\nimport { resolvePath } from \"../utils/paths.ts\";\nimport {\n\ttype SessionStats,\n\tSPAWNED_USAGE_CUSTOM_TYPE,\n\ttype SpawnedUsageReport,\n\ttype SpawnedUsageTotals,\n} from \"./agent-session.ts\";\nimport {\n\taggregateDailyUsageFromSessionFiles,\n\taggregateDailyUsageFromSessionRoot,\n\ttype DailyUsageTotals,\n\tformatDailyUsageBreakdown,\n\tgetLocalDayWindow,\n} from \"./cost/daily-usage.ts\";\nimport { exportSessionToHtml, type ToolHtmlRenderer } from \"./export-html/index.ts\";\nimport { createToolHtmlRenderer } from \"./export-html/tool-renderer.ts\";\nimport type { ContextUsage, ToolDefinition } from \"./extensions/index.ts\";\nimport type { SettingsManager } from \"./settings-manager.ts\";\nimport {\n\tisToolArgumentValidationLogRecord,\n\ttype ToolArgumentValidationLogRecord,\n} from \"./tool-recovery-log-records.ts\";\n\nexport const TOOL_ARGUMENT_VALIDATION_CUSTOM_TYPE = \"tool_argument_validation\";\n\nexport interface ToolArgumentValidationTeachEfficacy {\n\trecurrenceBefore: number;\n\trecurrenceAfter: number;\n\trepairedThenSucceeded: number;\n\trepairedThenFailed: number;\n\trepairedThenNotRun: number;\n}\n\nexport interface ToolArgumentValidationStats {\n\tclean: number;\n\trepaired: number;\n\tbounced: number;\n\tfailureModes: Record<string, number>;\n\trepairsApplied: Record<string, number>;\n\ttaught: Record<ToolArgumentTeachState, number>;\n\texecutionOutcome: Record<ToolArgumentExecutionOutcome, number>;\n\tteachEfficacy: Record<string, ToolArgumentValidationTeachEfficacy>;\n}\n\nexport interface ToolArgumentValidationRecord extends ToolArgumentValidationTelemetryEvent {\n\tversion: 1;\n}\n\nexport interface SessionAnalyticsDeps {\n\t/** Live agent state — assistant-message usage and message counts are read from here. */\n\tgetState(): AgentState;\n\t/** All messages (agent state view) — used for context-window estimation and last-assistant text. */\n\tgetMessages(): AgentMessage[];\n\t/** Current session model — its context window bounds the /context estimate. */\n\tgetModel(): Model<any> | undefined;\n\t/** Session log — entries feed spawned-usage roll-up, daily totals, branch export. */\n\tgetSessionManager(): SessionManager;\n\t/** Settings — the export theme is read here. */\n\tgetSettingsManager(): SettingsManager;\n\t/** Resolve a tool definition for the HTML export's custom-tool renderer. */\n\tgetToolDefinition(name: string): ToolDefinition | undefined;\n\t/** Sidecar recovery telemetry log; read on demand so turn handling never writes session custom entries. */\n\tgetToolRecoveryEventLogPath(): string;\n}\n\nexport class SessionAnalytics {\n\t/** Memoized spawned-usage totals, keyed by session entry count (Bug #22: O(1) between turns). */\n\tprivate _spawnedUsageCache?: { entryCount: number; totals: SpawnedUsageTotals };\n\t/** Memoized daily usage totals with a short TTL, keyed by the resolved scope dir. */\n\tprivate _dailyUsageCache?: { sessionDir: string; expiresAt: number; totals: DailyUsageTotals };\n\t/** Live recovery telemetry records already snapshotted for the worker; keyed to dedupe sidecar reads. */\n\tprivate readonly _toolArgumentValidationRecords = new Map<string, ToolArgumentValidationLogRecord>();\n\n\tprivate readonly deps: SessionAnalyticsDeps;\n\n\tconstructor(deps: SessionAnalyticsDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\tgetSessionStats(): SessionStats {\n\t\tconst state = this.deps.getState();\n\t\tconst userMessages = state.messages.filter((m) => m.role === \"user\").length;\n\t\tconst assistantMessages = state.messages.filter((m) => m.role === \"assistant\").length;\n\t\tconst toolResults = state.messages.filter((m) => m.role === \"toolResult\").length;\n\n\t\tlet toolCalls = 0;\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\n\t\tfor (const message of state.messages) {\n\t\t\tif (message.role === \"assistant\") {\n\t\t\t\tconst assistantMsg = message as AssistantMessage;\n\t\t\t\ttoolCalls += assistantMsg.content.filter((c) => c.type === \"toolCall\").length;\n\t\t\t\ttotalInput += assistantMsg.usage.input;\n\t\t\t\ttotalOutput += assistantMsg.usage.output;\n\t\t\t\ttotalCacheRead += assistantMsg.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += assistantMsg.usage.cacheWrite;\n\t\t\t\ttotalCost += assistantMsg.usage.cost.total;\n\t\t\t}\n\t\t}\n\n\t\tconst toolArgumentValidation = this.getToolArgumentValidationStats();\n\n\t\treturn {\n\t\t\tsessionFile: this.deps.getSessionManager().getSessionFile(),\n\t\t\tsessionId: this.deps.getSessionManager().getSessionId(),\n\t\t\tuserMessages,\n\t\t\tassistantMessages,\n\t\t\ttoolCalls,\n\t\t\ttoolResults,\n\t\t\ttotalMessages: state.messages.length,\n\t\t\ttokens: {\n\t\t\t\tinput: totalInput,\n\t\t\t\toutput: totalOutput,\n\t\t\t\tcacheRead: totalCacheRead,\n\t\t\t\tcacheWrite: totalCacheWrite,\n\t\t\t\ttotal: totalInput + totalOutput + totalCacheRead + totalCacheWrite,\n\t\t\t},\n\t\t\tcost: totalCost,\n\t\t\tcontextUsage: this.getContextUsage(),\n\t\t\ttoolArgumentValidation,\n\t\t};\n\t}\n\n\trecordToolArgumentValidation(record: ToolArgumentValidationLogRecord): void {\n\t\tthis._toolArgumentValidationRecords.set(record.recordId, record);\n\t}\n\n\tprivate readToolArgumentValidationSidecarRecords(): ToolArgumentValidationLogRecord[] {\n\t\tconst filePath = this.deps.getToolRecoveryEventLogPath();\n\t\tif (!existsSync(filePath)) return [];\n\t\tconst records: ToolArgumentValidationLogRecord[] = [];\n\t\ttry {\n\t\t\tconst sessionId = this.deps.getSessionManager().getSessionId();\n\t\t\tfor (const line of readFileSync(filePath, \"utf-8\").split(\"\\n\")) {\n\t\t\t\tif (line.trim().length === 0) continue;\n\t\t\t\tconst parsed = JSON.parse(line) as unknown;\n\t\t\t\tif (!isToolArgumentValidationLogRecord(parsed) || parsed.sessionId !== sessionId) continue;\n\t\t\t\trecords.push(parsed);\n\t\t\t}\n\t\t} catch {\n\t\t\treturn [];\n\t\t}\n\t\treturn records;\n\t}\n\n\tgetToolArgumentValidationStats(): ToolArgumentValidationStats {\n\t\tconst stats: ToolArgumentValidationStats = {\n\t\t\tclean: 0,\n\t\t\trepaired: 0,\n\t\t\tbounced: 0,\n\t\t\tfailureModes: {},\n\t\t\trepairsApplied: {},\n\t\t\ttaught: { none: 0, note: 0, rule: 0 },\n\t\t\texecutionOutcome: { not_run: 0, succeeded: 0, failed: 0 },\n\t\t\tteachEfficacy: {},\n\t\t};\n\n\t\tconst getEfficacy = (record: ToolArgumentValidationRecord, mode: string): ToolArgumentValidationTeachEfficacy => {\n\t\t\tconst key = `${record.provider ?? \"unknown\"}/${record.model ?? \"unknown\"}:${mode}`;\n\t\t\tstats.teachEfficacy[key] ??= {\n\t\t\t\trecurrenceBefore: 0,\n\t\t\t\trecurrenceAfter: 0,\n\t\t\t\trepairedThenSucceeded: 0,\n\t\t\t\trepairedThenFailed: 0,\n\t\t\t\trepairedThenNotRun: 0,\n\t\t\t};\n\t\t\treturn stats.teachEfficacy[key];\n\t\t};\n\n\t\tconst seen = new Set<string>();\n\t\tconst consume = (record: ToolArgumentValidationRecord, key: string): void => {\n\t\t\tif (seen.has(key)) return;\n\t\t\tseen.add(key);\n\t\t\tstats[record.outcome] += 1;\n\t\t\tconst taught = record.taught ?? \"none\";\n\t\t\tconst executionOutcome = record.executionOutcome ?? \"not_run\";\n\t\t\tstats.taught[taught] += 1;\n\t\t\tstats.executionOutcome[executionOutcome] += 1;\n\t\t\tconst modes = new Set([...record.failureModes, ...record.repairsApplied]);\n\t\t\tfor (const mode of record.failureModes) {\n\t\t\t\tstats.failureModes[mode] = (stats.failureModes[mode] ?? 0) + 1;\n\t\t\t}\n\t\t\tfor (const repair of record.repairsApplied) {\n\t\t\t\tstats.repairsApplied[repair] = (stats.repairsApplied[repair] ?? 0) + 1;\n\t\t\t}\n\t\t\tfor (const mode of modes) {\n\t\t\t\tconst efficacy = getEfficacy(record, mode);\n\t\t\t\tif (taught === \"none\") {\n\t\t\t\t\tefficacy.recurrenceBefore++;\n\t\t\t\t} else {\n\t\t\t\t\tefficacy.recurrenceAfter++;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (record.outcome === \"repaired\") {\n\t\t\t\tfor (const repair of record.repairsApplied) {\n\t\t\t\t\tconst efficacy = getEfficacy(record, repair);\n\t\t\t\t\tif (executionOutcome === \"succeeded\") efficacy.repairedThenSucceeded++;\n\t\t\t\t\tif (executionOutcome === \"failed\") efficacy.repairedThenFailed++;\n\t\t\t\t\tif (executionOutcome === \"not_run\") efficacy.repairedThenNotRun++;\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\n\t\tfor (const entry of this.deps.getSessionManager().getEntries()) {\n\t\t\tif (entry.type !== \"custom\" || entry.customType !== TOOL_ARGUMENT_VALIDATION_CUSTOM_TYPE) continue;\n\t\t\tconst record = entry.data as ToolArgumentValidationRecord | undefined;\n\t\t\tif (!record || record.version !== 1) continue;\n\t\t\tconsume(record, `legacy:${entry.id}`);\n\t\t}\n\t\tfor (const record of this.readToolArgumentValidationSidecarRecords()) {\n\t\t\tconsume(record, record.recordId);\n\t\t}\n\t\tfor (const record of this._toolArgumentValidationRecords.values()) {\n\t\t\tconsume(record, record.recordId);\n\t\t}\n\n\t\treturn stats;\n\t}\n\n\t/**\n\t * Cumulative usage (full breakdown) for this session's entire spawn subtree: its own\n\t * assistant messages PLUS every `spawned_usage` report it has rolled up. Single source of\n\t * truth for \"how much did this session and everything it spawned spend\" — used by print-mode\n\t * to emit a child's total so a spawner can roll it up via {@link addSpawnedUsage}.\n\t *\n\t * Including the `spawned_usage` reports is what keeps the single-hop invariant intact: a child\n\t * that itself spawned grandchildren must report own + sub-usage in one number, or the parent\n\t * silently under-counts the grandchildren.\n\t */\n\tgetCumulativeUsage(): Usage {\n\t\tlet input = 0;\n\t\tlet output = 0;\n\t\tlet cacheRead = 0;\n\t\tlet cacheWrite = 0;\n\t\tlet totalTokens = 0;\n\t\tlet costInput = 0;\n\t\tlet costOutput = 0;\n\t\tlet costCacheRead = 0;\n\t\tlet costCacheWrite = 0;\n\t\tlet costTotal = 0;\n\t\tconst add = (usage: Usage) => {\n\t\t\tinput += usage.input;\n\t\t\toutput += usage.output;\n\t\t\tcacheRead += usage.cacheRead;\n\t\t\tcacheWrite += usage.cacheWrite;\n\t\t\ttotalTokens += usage.totalTokens;\n\t\t\tcostInput += usage.cost.input;\n\t\t\tcostOutput += usage.cost.output;\n\t\t\tcostCacheRead += usage.cost.cacheRead;\n\t\t\tcostCacheWrite += usage.cost.cacheWrite;\n\t\t\tcostTotal += usage.cost.total;\n\t\t};\n\t\tfor (const message of this.deps.getState().messages) {\n\t\t\tif (message.role !== \"assistant\") continue;\n\t\t\tconst usage = (message as AssistantMessage).usage;\n\t\t\tif (!usage) continue;\n\t\t\tadd(usage);\n\t\t}\n\t\t// Roll up usage this session attributed to its own spawned children (single-hop).\n\t\tfor (const entry of this.deps.getSessionManager().getEntries()) {\n\t\t\tif (entry.type !== \"custom\" || entry.customType !== SPAWNED_USAGE_CUSTOM_TYPE) continue;\n\t\t\tconst data = entry.data as SpawnedUsageReport | undefined;\n\t\t\tif (data?.usage) add(data.usage);\n\t\t}\n\t\treturn {\n\t\t\tinput,\n\t\t\toutput,\n\t\t\tcacheRead,\n\t\t\tcacheWrite,\n\t\t\ttotalTokens,\n\t\t\tcost: {\n\t\t\t\tinput: costInput,\n\t\t\t\toutput: costOutput,\n\t\t\t\tcacheRead: costCacheRead,\n\t\t\t\tcacheWrite: costCacheWrite,\n\t\t\t\ttotal: costTotal,\n\t\t\t},\n\t\t};\n\t}\n\n\t/**\n\t * Record usage spent by a spawned/subagent session so the footer can roll it into the\n\t * displayed cost. Persisted as a `CustomEntry` (`customType: \"spawned_usage\"`, Model A) so\n\t * it survives reload and is reconstructed exactly like main usage; a new/forked session\n\t * starts fresh because it owns a new log file.\n\t *\n\t * Idempotent on `opts.reportId`: a re-report (retry, duplicate `agent_end`) with a\n\t * previously-seen id is ignored, so cost cannot be double-counted. Honors the single-hop\n\t * invariant documented on {@link SpawnedUsageReport}.\n\t *\n\t * @returns the id of the appended entry, or `undefined` if the report was a duplicate.\n\t */\n\taddSpawnedUsage(\n\t\tusage: Usage,\n\t\topts?: { label?: string; sourceSessionId?: string; reportId?: string },\n\t): string | undefined {\n\t\tconst reportId = opts?.reportId;\n\t\tif (reportId) {\n\t\t\tfor (const entry of this.deps.getSessionManager().getEntries()) {\n\t\t\t\tif (\n\t\t\t\t\tentry.type === \"custom\" &&\n\t\t\t\t\tentry.customType === SPAWNED_USAGE_CUSTOM_TYPE &&\n\t\t\t\t\t(entry.data as SpawnedUsageReport | undefined)?.reportId === reportId\n\t\t\t\t) {\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tconst report: SpawnedUsageReport = {\n\t\t\tusage,\n\t\t\tlabel: opts?.label,\n\t\t\tsourceSessionId: opts?.sourceSessionId,\n\t\t\treportId,\n\t\t};\n\t\treturn this.deps.getSessionManager().appendCustomEntry(SPAWNED_USAGE_CUSTOM_TYPE, report);\n\t}\n\n\t/**\n\t * Aggregate all recorded spawned-usage reports (see {@link addSpawnedUsage}). Cached by the session\n\t * entry count so the interactive footer (which calls this every render frame) is O(1) between turns\n\t * instead of an O(N) scan on every keystroke (Bug #22). Recomputes only when entries change.\n\t */\n\tgetSpawnedUsage(): SpawnedUsageTotals {\n\t\tconst sessionManager = this.deps.getSessionManager();\n\t\tconst entryCount = sessionManager.getEntryCount?.() ?? sessionManager.getEntries().length;\n\t\tif (this._spawnedUsageCache?.entryCount === entryCount) return this._spawnedUsageCache.totals;\n\t\tlet cost = 0;\n\t\tlet reports = 0;\n\t\tfor (const entry of sessionManager.getEntries()) {\n\t\t\tif (entry.type !== \"custom\" || entry.customType !== SPAWNED_USAGE_CUSTOM_TYPE) continue;\n\t\t\tconst data = entry.data as SpawnedUsageReport | undefined;\n\t\t\tif (!data?.usage) continue;\n\t\t\tcost += data.usage.cost.total;\n\t\t\treports += 1;\n\t\t}\n\t\tconst totals: SpawnedUsageTotals = { cost, reports };\n\t\tthis._spawnedUsageCache = { entryCount, totals };\n\t\treturn totals;\n\t}\n\n\tgetDailyUsageTotals(now = new Date()): DailyUsageTotals {\n\t\tconst sessionManager = this.deps.getSessionManager();\n\t\tconst sessionDir = sessionManager.getSessionDir();\n\t\tconst scope = sessionManager.usesDefaultSessionDir() ? getSessionsDir() : sessionDir;\n\t\tconst nowMs = now.getTime();\n\t\tif (this._dailyUsageCache?.sessionDir === scope && this._dailyUsageCache.expiresAt > nowMs) {\n\t\t\treturn this._dailyUsageCache.totals;\n\t\t}\n\t\tconst window = getLocalDayWindow(now);\n\t\tconst totals = sessionManager.usesDefaultSessionDir()\n\t\t\t? aggregateDailyUsageFromSessionRoot(scope, window)\n\t\t\t: aggregateDailyUsageFromSessionFiles(sessionDir, window);\n\t\tthis._dailyUsageCache = { sessionDir: scope, expiresAt: nowMs + 10_000, totals };\n\t\treturn totals;\n\t}\n\n\tgetDailyUsageBreakdown(formatLabel?: (label: string) => string, now = new Date()): string {\n\t\treturn formatDailyUsageBreakdown(this.getDailyUsageTotals(now), formatLabel);\n\t}\n\n\tgetContextUsage(): ContextUsage | undefined {\n\t\tconst model = this.deps.getModel();\n\t\tif (!model) return undefined;\n\n\t\tconst contextWindow = model.contextWindow ?? 0;\n\t\tif (contextWindow <= 0) return undefined;\n\n\t\t// After compaction, the last assistant usage reflects pre-compaction context size.\n\t\t// We can only trust usage from an assistant that responded after the latest compaction.\n\t\t// If no such assistant exists, context token count is unknown until the next LLM response.\n\t\tconst branchEntries = this.deps.getSessionManager().getBranch();\n\t\tconst latestCompaction = getLatestCompactionEntry(branchEntries);\n\n\t\tif (latestCompaction) {\n\t\t\t// Check if there's a valid assistant usage after the compaction boundary\n\t\t\tconst compactionIndex = branchEntries.lastIndexOf(latestCompaction);\n\t\t\tlet hasPostCompactionUsage = false;\n\t\t\tfor (let i = branchEntries.length - 1; i > compactionIndex; i--) {\n\t\t\t\tconst entry = branchEntries[i];\n\t\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\t\tconst assistant = entry.message;\n\t\t\t\t\tif (assistant.stopReason !== \"aborted\" && assistant.stopReason !== \"error\") {\n\t\t\t\t\t\tconst contextTokens = calculateContextTokens(assistant.usage);\n\t\t\t\t\t\tif (contextTokens > 0) {\n\t\t\t\t\t\t\thasPostCompactionUsage = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!hasPostCompactionUsage) {\n\t\t\t\treturn { tokens: null, contextWindow, percent: null };\n\t\t\t}\n\t\t}\n\n\t\tconst estimate = estimateContextTokens(this.deps.getMessages());\n\t\tconst percent = (estimate.tokens / contextWindow) * 100;\n\n\t\treturn {\n\t\t\ttokens: estimate.tokens,\n\t\t\tcontextWindow,\n\t\t\tpercent,\n\t\t};\n\t}\n\n\t/**\n\t * Export session to HTML.\n\t * @param outputPath Optional output path (defaults to session directory)\n\t * @returns Path to exported file\n\t */\n\tasync exportToHtml(outputPath?: string): Promise<string> {\n\t\tconst sessionManager = this.deps.getSessionManager();\n\t\tconst themeName = this.deps.getSettingsManager().getTheme();\n\n\t\t// Create tool renderer if we have an extension runner (for custom tool HTML rendering)\n\t\tconst toolRenderer: ToolHtmlRenderer = createToolHtmlRenderer({\n\t\t\tgetToolDefinition: (name) => this.deps.getToolDefinition(name),\n\t\t\ttheme,\n\t\t\tcwd: sessionManager.getCwd(),\n\t\t});\n\n\t\treturn await exportSessionToHtml(sessionManager, this.deps.getState(), {\n\t\t\toutputPath,\n\t\t\tthemeName,\n\t\t\ttoolRenderer,\n\t\t});\n\t}\n\n\t/**\n\t * Export the current session branch to a JSONL file.\n\t * Writes the session header followed by all entries on the current branch path.\n\t * @param outputPath Target file path. If omitted, generates a timestamped file in cwd.\n\t * @returns The resolved output file path.\n\t */\n\texportToJsonl(outputPath?: string): string {\n\t\tconst sessionManager = this.deps.getSessionManager();\n\t\tconst filePath = resolvePath(\n\t\t\toutputPath ?? `session-${new Date().toISOString().replace(/[:.]/g, \"-\")}.jsonl`,\n\t\t\tprocess.cwd(),\n\t\t);\n\t\tconst dir = dirname(filePath);\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true });\n\t\t}\n\n\t\tconst header: SessionHeader = {\n\t\t\ttype: \"session\",\n\t\t\tversion: CURRENT_SESSION_VERSION,\n\t\t\tid: sessionManager.getSessionId(),\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tcwd: sessionManager.getCwd(),\n\t\t};\n\n\t\tconst branchEntries = sessionManager.getBranch();\n\t\tconst lines = [JSON.stringify(header)];\n\n\t\t// Re-chain parentIds to form a linear sequence\n\t\tlet prevId: string | null = null;\n\t\tfor (const entry of branchEntries) {\n\t\t\tconst linear = { ...entry, parentId: prevId };\n\t\t\tlines.push(JSON.stringify(linear));\n\t\t\tprevId = entry.id;\n\t\t}\n\n\t\twriteFileSync(filePath, `${lines.join(\"\\n\")}\\n`);\n\t\treturn filePath;\n\t}\n\n\t/**\n\t * Get text content of last assistant message.\n\t * Useful for /copy command.\n\t * @returns Text content, or undefined if no assistant message exists\n\t */\n\tgetLastAssistantText(): string | undefined {\n\t\tconst lastAssistant = this.deps\n\t\t\t.getMessages()\n\t\t\t.slice()\n\t\t\t.reverse()\n\t\t\t.find((m) => {\n\t\t\t\tif (m.role !== \"assistant\") return false;\n\t\t\t\tconst msg = m as AssistantMessage;\n\t\t\t\t// Skip aborted messages with no content\n\t\t\t\tif (msg.stopReason === \"aborted\" && msg.content.length === 0) return false;\n\t\t\t\treturn true;\n\t\t\t});\n\n\t\tif (!lastAssistant) return undefined;\n\n\t\tlet text = \"\";\n\t\tfor (const content of (lastAssistant as AssistantMessage).content) {\n\t\t\tif (content.type === \"text\") {\n\t\t\t\ttext += content.text;\n\t\t\t}\n\t\t}\n\n\t\treturn text.trim() || undefined;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"session-analytics.js","sourceRoot":"","sources":["../../src/core/session-analytics.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Session usage / cost / stats accounting, context-window usage, and session export.\n *\n * Extracted verbatim from agent-session.ts (god-file decomposition). Read-only over the session\n * except for its two owned memo caches (`_spawnedUsageCache`, `_dailyUsageCache`) — it never mutates\n * agent or session state. Single source of truth for \"how much did this session and its spawned\n * subtree spend\" (footer roll-up, print-mode child reporting), the daily cross-session totals, the\n * /context window estimate, and HTML/JSONL export of the current branch.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport type { AgentMessage, AgentState } from \"@caupulican/pi-agent-core\";\nimport {\n\tCURRENT_SESSION_VERSION,\n\tcalculateContextTokens,\n\testimateContextTokens,\n\tgetLatestCompactionEntry,\n\ttype SessionHeader,\n\ttype SessionManager,\n} from \"@caupulican/pi-agent-core/node\";\nimport type {\n\tAssistantMessage,\n\tModel,\n\tToolArgumentExecutionOutcome,\n\tToolArgumentTeachState,\n\tToolArgumentValidationTelemetryEvent,\n\tUsage,\n} from \"@caupulican/pi-ai\";\nimport { getSessionsDir } from \"../config.ts\";\nimport { theme } from \"../modes/interactive/theme/theme.ts\";\nimport { resolvePath } from \"../utils/paths.ts\";\nimport {\n\ttype SessionStats,\n\tSPAWNED_USAGE_CUSTOM_TYPE,\n\ttype SpawnedUsageReport,\n\ttype SpawnedUsageTotals,\n} from \"./agent-session.ts\";\nimport {\n\taggregateCurrentSessionCostsFromEntries,\n\tcreateSessionCostSummary,\n\ttype SessionCostSummary,\n} from \"./cost/cost-summary.ts\";\nimport {\n\taggregateDailyUsageFromSessionFiles,\n\taggregateDailyUsageFromSessionRoot,\n\ttype DailyUsageTotals,\n\tformatDailyUsageBreakdown,\n\tgetLocalDayWindow,\n} from \"./cost/daily-usage.ts\";\nimport { exportSessionToHtml, type ToolHtmlRenderer } from \"./export-html/index.ts\";\nimport { createToolHtmlRenderer } from \"./export-html/tool-renderer.ts\";\nimport type { ContextUsage, ToolDefinition } from \"./extensions/index.ts\";\nimport type { SettingsManager } from \"./settings-manager.ts\";\nimport {\n\tisToolArgumentValidationLogRecord,\n\ttype ToolArgumentValidationLogRecord,\n} from \"./tool-recovery-log-records.ts\";\n\nexport const TOOL_ARGUMENT_VALIDATION_CUSTOM_TYPE = \"tool_argument_validation\";\n\nexport interface ToolArgumentValidationTeachEfficacy {\n\trecurrenceBefore: number;\n\trecurrenceAfter: number;\n\trepairedThenSucceeded: number;\n\trepairedThenFailed: number;\n\trepairedThenNotRun: number;\n}\n\nexport interface ToolArgumentValidationStats {\n\tclean: number;\n\trepaired: number;\n\tbounced: number;\n\tfailureModes: Record<string, number>;\n\trepairsApplied: Record<string, number>;\n\ttaught: Record<ToolArgumentTeachState, number>;\n\texecutionOutcome: Record<ToolArgumentExecutionOutcome, number>;\n\tteachEfficacy: Record<string, ToolArgumentValidationTeachEfficacy>;\n}\n\nexport interface ToolArgumentValidationRecord extends ToolArgumentValidationTelemetryEvent {\n\tversion: 1;\n}\n\nexport interface SessionAnalyticsDeps {\n\t/** Live agent state — assistant-message usage and message counts are read from here. */\n\tgetState(): AgentState;\n\t/** All messages (agent state view) — used for context-window estimation and last-assistant text. */\n\tgetMessages(): AgentMessage[];\n\t/** Current session model — its context window bounds the /context estimate. */\n\tgetModel(): Model<any> | undefined;\n\t/** Session log — entries feed spawned-usage roll-up, daily totals, branch export. */\n\tgetSessionManager(): SessionManager;\n\t/** Settings — the export theme is read here. */\n\tgetSettingsManager(): SettingsManager;\n\t/** Resolve a tool definition for the HTML export's custom-tool renderer. */\n\tgetToolDefinition(name: string): ToolDefinition | undefined;\n\t/** Sidecar recovery telemetry log; read on demand so turn handling never writes session custom entries. */\n\tgetToolRecoveryEventLogPath(): string;\n}\n\nexport class SessionAnalytics {\n\t/** Memoized spawned-usage totals, keyed by session entry count (Bug #22: O(1) between turns). */\n\tprivate _spawnedUsageCache?: { entryCount: number; totals: SpawnedUsageTotals };\n\t/** Memoized daily usage totals with a short TTL, keyed by the resolved scope dir and local-day window. */\n\tprivate _dailyUsageCache?: {\n\t\tsessionDir: string;\n\t\twindowStartMs: number;\n\t\twindowEndMs: number;\n\t\texpiresAt: number;\n\t\ttotals: DailyUsageTotals;\n\t};\n\t/** Live recovery telemetry records already snapshotted for the worker; keyed to dedupe sidecar reads. */\n\tprivate readonly _toolArgumentValidationRecords = new Map<string, ToolArgumentValidationLogRecord>();\n\n\tprivate readonly deps: SessionAnalyticsDeps;\n\n\tconstructor(deps: SessionAnalyticsDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\tgetSessionStats(): SessionStats {\n\t\tconst state = this.deps.getState();\n\t\tconst userMessages = state.messages.filter((m) => m.role === \"user\").length;\n\t\tconst assistantMessages = state.messages.filter((m) => m.role === \"assistant\").length;\n\t\tconst toolResults = state.messages.filter((m) => m.role === \"toolResult\").length;\n\n\t\tlet toolCalls = 0;\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\n\t\tfor (const message of state.messages) {\n\t\t\tif (message.role === \"assistant\") {\n\t\t\t\tconst assistantMsg = message as AssistantMessage;\n\t\t\t\ttoolCalls += assistantMsg.content.filter((c) => c.type === \"toolCall\").length;\n\t\t\t\ttotalInput += assistantMsg.usage.input;\n\t\t\t\ttotalOutput += assistantMsg.usage.output;\n\t\t\t\ttotalCacheRead += assistantMsg.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += assistantMsg.usage.cacheWrite;\n\t\t\t\ttotalCost += assistantMsg.usage.cost.total;\n\t\t\t}\n\t\t}\n\n\t\tconst toolArgumentValidation = this.getToolArgumentValidationStats();\n\n\t\treturn {\n\t\t\tsessionFile: this.deps.getSessionManager().getSessionFile(),\n\t\t\tsessionId: this.deps.getSessionManager().getSessionId(),\n\t\t\tuserMessages,\n\t\t\tassistantMessages,\n\t\t\ttoolCalls,\n\t\t\ttoolResults,\n\t\t\ttotalMessages: state.messages.length,\n\t\t\ttokens: {\n\t\t\t\tinput: totalInput,\n\t\t\t\toutput: totalOutput,\n\t\t\t\tcacheRead: totalCacheRead,\n\t\t\t\tcacheWrite: totalCacheWrite,\n\t\t\t\ttotal: totalInput + totalOutput + totalCacheRead + totalCacheWrite,\n\t\t\t},\n\t\t\tcost: totalCost,\n\t\t\tcontextUsage: this.getContextUsage(),\n\t\t\ttoolArgumentValidation,\n\t\t};\n\t}\n\n\trecordToolArgumentValidation(record: ToolArgumentValidationLogRecord): void {\n\t\tthis._toolArgumentValidationRecords.set(record.recordId, record);\n\t}\n\n\tprivate readToolArgumentValidationSidecarRecords(): ToolArgumentValidationLogRecord[] {\n\t\tconst filePath = this.deps.getToolRecoveryEventLogPath();\n\t\tif (!existsSync(filePath)) return [];\n\t\tconst records: ToolArgumentValidationLogRecord[] = [];\n\t\ttry {\n\t\t\tconst sessionId = this.deps.getSessionManager().getSessionId();\n\t\t\tfor (const line of readFileSync(filePath, \"utf-8\").split(\"\\n\")) {\n\t\t\t\tif (line.trim().length === 0) continue;\n\t\t\t\tconst parsed = JSON.parse(line) as unknown;\n\t\t\t\tif (!isToolArgumentValidationLogRecord(parsed) || parsed.sessionId !== sessionId) continue;\n\t\t\t\trecords.push(parsed);\n\t\t\t}\n\t\t} catch {\n\t\t\treturn [];\n\t\t}\n\t\treturn records;\n\t}\n\n\tgetToolArgumentValidationStats(): ToolArgumentValidationStats {\n\t\tconst stats: ToolArgumentValidationStats = {\n\t\t\tclean: 0,\n\t\t\trepaired: 0,\n\t\t\tbounced: 0,\n\t\t\tfailureModes: {},\n\t\t\trepairsApplied: {},\n\t\t\ttaught: { none: 0, note: 0, rule: 0 },\n\t\t\texecutionOutcome: { not_run: 0, succeeded: 0, failed: 0 },\n\t\t\tteachEfficacy: {},\n\t\t};\n\n\t\tconst getEfficacy = (record: ToolArgumentValidationRecord, mode: string): ToolArgumentValidationTeachEfficacy => {\n\t\t\tconst key = `${record.provider ?? \"unknown\"}/${record.model ?? \"unknown\"}:${mode}`;\n\t\t\tstats.teachEfficacy[key] ??= {\n\t\t\t\trecurrenceBefore: 0,\n\t\t\t\trecurrenceAfter: 0,\n\t\t\t\trepairedThenSucceeded: 0,\n\t\t\t\trepairedThenFailed: 0,\n\t\t\t\trepairedThenNotRun: 0,\n\t\t\t};\n\t\t\treturn stats.teachEfficacy[key];\n\t\t};\n\n\t\tconst seen = new Set<string>();\n\t\tconst consume = (record: ToolArgumentValidationRecord, key: string): void => {\n\t\t\tif (seen.has(key)) return;\n\t\t\tseen.add(key);\n\t\t\tstats[record.outcome] += 1;\n\t\t\tconst taught = record.taught ?? \"none\";\n\t\t\tconst executionOutcome = record.executionOutcome ?? \"not_run\";\n\t\t\tstats.taught[taught] += 1;\n\t\t\tstats.executionOutcome[executionOutcome] += 1;\n\t\t\tconst modes = new Set([...record.failureModes, ...record.repairsApplied]);\n\t\t\tfor (const mode of record.failureModes) {\n\t\t\t\tstats.failureModes[mode] = (stats.failureModes[mode] ?? 0) + 1;\n\t\t\t}\n\t\t\tfor (const repair of record.repairsApplied) {\n\t\t\t\tstats.repairsApplied[repair] = (stats.repairsApplied[repair] ?? 0) + 1;\n\t\t\t}\n\t\t\tfor (const mode of modes) {\n\t\t\t\tconst efficacy = getEfficacy(record, mode);\n\t\t\t\tif (taught === \"none\") {\n\t\t\t\t\tefficacy.recurrenceBefore++;\n\t\t\t\t} else {\n\t\t\t\t\tefficacy.recurrenceAfter++;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (record.outcome === \"repaired\") {\n\t\t\t\tfor (const repair of record.repairsApplied) {\n\t\t\t\t\tconst efficacy = getEfficacy(record, repair);\n\t\t\t\t\tif (executionOutcome === \"succeeded\") efficacy.repairedThenSucceeded++;\n\t\t\t\t\tif (executionOutcome === \"failed\") efficacy.repairedThenFailed++;\n\t\t\t\t\tif (executionOutcome === \"not_run\") efficacy.repairedThenNotRun++;\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\n\t\tfor (const entry of this.deps.getSessionManager().getEntries()) {\n\t\t\tif (entry.type !== \"custom\" || entry.customType !== TOOL_ARGUMENT_VALIDATION_CUSTOM_TYPE) continue;\n\t\t\tconst record = entry.data as ToolArgumentValidationRecord | undefined;\n\t\t\tif (!record || record.version !== 1) continue;\n\t\t\tconsume(record, `legacy:${entry.id}`);\n\t\t}\n\t\tfor (const record of this.readToolArgumentValidationSidecarRecords()) {\n\t\t\tconsume(record, record.recordId);\n\t\t}\n\t\tfor (const record of this._toolArgumentValidationRecords.values()) {\n\t\t\tconsume(record, record.recordId);\n\t\t}\n\n\t\treturn stats;\n\t}\n\n\t/**\n\t * Cumulative usage (full breakdown) for this session's entire spawn subtree: its own\n\t * assistant messages PLUS every `spawned_usage` report it has rolled up. Single source of\n\t * truth for \"how much did this session and everything it spawned spend\" — used by print-mode\n\t * to emit a child's total so a spawner can roll it up via {@link addSpawnedUsage}.\n\t *\n\t * Including the `spawned_usage` reports is what keeps the single-hop invariant intact: a child\n\t * that itself spawned grandchildren must report own + sub-usage in one number, or the parent\n\t * silently under-counts the grandchildren.\n\t */\n\tgetCumulativeUsage(): Usage {\n\t\tlet input = 0;\n\t\tlet output = 0;\n\t\tlet cacheRead = 0;\n\t\tlet cacheWrite = 0;\n\t\tlet totalTokens = 0;\n\t\tlet costInput = 0;\n\t\tlet costOutput = 0;\n\t\tlet costCacheRead = 0;\n\t\tlet costCacheWrite = 0;\n\t\tlet costTotal = 0;\n\t\tconst add = (usage: Usage) => {\n\t\t\tinput += usage.input;\n\t\t\toutput += usage.output;\n\t\t\tcacheRead += usage.cacheRead;\n\t\t\tcacheWrite += usage.cacheWrite;\n\t\t\ttotalTokens += usage.totalTokens;\n\t\t\tcostInput += usage.cost.input;\n\t\t\tcostOutput += usage.cost.output;\n\t\t\tcostCacheRead += usage.cost.cacheRead;\n\t\t\tcostCacheWrite += usage.cost.cacheWrite;\n\t\t\tcostTotal += usage.cost.total;\n\t\t};\n\t\tfor (const message of this.deps.getState().messages) {\n\t\t\tif (message.role !== \"assistant\") continue;\n\t\t\tconst usage = (message as AssistantMessage).usage;\n\t\t\tif (!usage) continue;\n\t\t\tadd(usage);\n\t\t}\n\t\t// Roll up usage this session attributed to its own spawned children (single-hop).\n\t\tconst seenSpawnedReportIds = new Set<string>();\n\t\tfor (const entry of this.deps.getSessionManager().getEntries()) {\n\t\t\tif (entry.type !== \"custom\" || entry.customType !== SPAWNED_USAGE_CUSTOM_TYPE) continue;\n\t\t\tconst data = entry.data as SpawnedUsageReport | undefined;\n\t\t\tif (!data?.usage) continue;\n\t\t\tif (data.reportId) {\n\t\t\t\tif (seenSpawnedReportIds.has(data.reportId)) continue;\n\t\t\t\tseenSpawnedReportIds.add(data.reportId);\n\t\t\t}\n\t\t\tadd(data.usage);\n\t\t}\n\t\treturn {\n\t\t\tinput,\n\t\t\toutput,\n\t\t\tcacheRead,\n\t\t\tcacheWrite,\n\t\t\ttotalTokens,\n\t\t\tcost: {\n\t\t\t\tinput: costInput,\n\t\t\t\toutput: costOutput,\n\t\t\t\tcacheRead: costCacheRead,\n\t\t\t\tcacheWrite: costCacheWrite,\n\t\t\t\ttotal: costTotal,\n\t\t\t},\n\t\t};\n\t}\n\n\t/**\n\t * Record usage spent by a spawned/subagent session so the footer can roll it into the\n\t * displayed cost. Persisted as a `CustomEntry` (`customType: \"spawned_usage\"`, Model A) so\n\t * it survives reload and is reconstructed exactly like main usage; a new/forked session\n\t * starts fresh because it owns a new log file.\n\t *\n\t * Idempotent on `opts.reportId`: a re-report (retry, duplicate `agent_end`) with a\n\t * previously-seen id is ignored, so cost cannot be double-counted. Honors the single-hop\n\t * invariant documented on {@link SpawnedUsageReport}.\n\t *\n\t * @returns the id of the appended entry, or `undefined` if the report was a duplicate.\n\t */\n\taddSpawnedUsage(\n\t\tusage: Usage,\n\t\topts?: { label?: string; sourceSessionId?: string; reportId?: string },\n\t): string | undefined {\n\t\tconst reportId = opts?.reportId;\n\t\tif (reportId) {\n\t\t\tfor (const entry of this.deps.getSessionManager().getEntries()) {\n\t\t\t\tif (\n\t\t\t\t\tentry.type === \"custom\" &&\n\t\t\t\t\tentry.customType === SPAWNED_USAGE_CUSTOM_TYPE &&\n\t\t\t\t\t(entry.data as SpawnedUsageReport | undefined)?.reportId === reportId\n\t\t\t\t) {\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tconst report: SpawnedUsageReport = {\n\t\t\tusage,\n\t\t\tlabel: opts?.label,\n\t\t\tsourceSessionId: opts?.sourceSessionId,\n\t\t\treportId,\n\t\t};\n\t\treturn this.deps.getSessionManager().appendCustomEntry(SPAWNED_USAGE_CUSTOM_TYPE, report);\n\t}\n\n\t/**\n\t * Aggregate all recorded spawned-usage reports (see {@link addSpawnedUsage}). Cached by the session\n\t * entry count so the interactive footer (which calls this every render frame) is O(1) between turns\n\t * instead of an O(N) scan on every keystroke (Bug #22). Recomputes only when entries change.\n\t */\n\tgetSpawnedUsage(): SpawnedUsageTotals {\n\t\tconst sessionManager = this.deps.getSessionManager();\n\t\tconst entryCount = sessionManager.getEntryCount?.() ?? sessionManager.getEntries().length;\n\t\tif (this._spawnedUsageCache?.entryCount === entryCount) return this._spawnedUsageCache.totals;\n\t\tconst current = aggregateCurrentSessionCostsFromEntries(sessionManager.getEntries());\n\t\tconst totals: SpawnedUsageTotals = { cost: current.subagentCost, reports: current.subagentReports };\n\t\tthis._spawnedUsageCache = { entryCount, totals };\n\t\treturn totals;\n\t}\n\n\tgetCostSummary(now = new Date()): SessionCostSummary {\n\t\tconst window = getLocalDayWindow(now);\n\t\treturn createSessionCostSummary({\n\t\t\tentries: this.deps.getSessionManager().getEntries(),\n\t\t\tdailyTotals: this.getDailyUsageTotals(now),\n\t\t\ttodayWindow: window,\n\t\t});\n\t}\n\n\tgetDailyUsageTotals(now = new Date()): DailyUsageTotals {\n\t\tconst sessionManager = this.deps.getSessionManager();\n\t\tconst sessionDir = sessionManager.getSessionDir();\n\t\tconst scope = sessionManager.usesDefaultSessionDir() ? getSessionsDir() : sessionDir;\n\t\tconst nowMs = now.getTime();\n\t\tconst window = getLocalDayWindow(now);\n\t\tif (\n\t\t\tthis._dailyUsageCache?.sessionDir === scope &&\n\t\t\tthis._dailyUsageCache.windowStartMs === window.startMs &&\n\t\t\tthis._dailyUsageCache.windowEndMs === window.endMs &&\n\t\t\tthis._dailyUsageCache.expiresAt > nowMs\n\t\t) {\n\t\t\treturn this._dailyUsageCache.totals;\n\t\t}\n\t\tconst totals = sessionManager.usesDefaultSessionDir()\n\t\t\t? aggregateDailyUsageFromSessionRoot(scope, window)\n\t\t\t: aggregateDailyUsageFromSessionFiles(sessionDir, window);\n\t\tthis._dailyUsageCache = {\n\t\t\tsessionDir: scope,\n\t\t\twindowStartMs: window.startMs,\n\t\t\twindowEndMs: window.endMs,\n\t\t\texpiresAt: Math.min(nowMs + 10_000, window.endMs),\n\t\t\ttotals,\n\t\t};\n\t\treturn totals;\n\t}\n\n\tgetDailyUsageBreakdown(formatLabel?: (label: string) => string, now = new Date()): string {\n\t\treturn formatDailyUsageBreakdown(this.getDailyUsageTotals(now), formatLabel);\n\t}\n\n\tgetContextUsage(): ContextUsage | undefined {\n\t\tconst model = this.deps.getModel();\n\t\tif (!model) return undefined;\n\n\t\tconst contextWindow = model.contextWindow ?? 0;\n\t\tif (contextWindow <= 0) return undefined;\n\n\t\t// After compaction, the last assistant usage reflects pre-compaction context size.\n\t\t// We can only trust usage from an assistant that responded after the latest compaction.\n\t\t// If no such assistant exists, context token count is unknown until the next LLM response.\n\t\tconst branchEntries = this.deps.getSessionManager().getBranch();\n\t\tconst latestCompaction = getLatestCompactionEntry(branchEntries);\n\n\t\tif (latestCompaction) {\n\t\t\t// Check if there's a valid assistant usage after the compaction boundary\n\t\t\tconst compactionIndex = branchEntries.lastIndexOf(latestCompaction);\n\t\t\tlet hasPostCompactionUsage = false;\n\t\t\tfor (let i = branchEntries.length - 1; i > compactionIndex; i--) {\n\t\t\t\tconst entry = branchEntries[i];\n\t\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\t\tconst assistant = entry.message;\n\t\t\t\t\tif (assistant.stopReason !== \"aborted\" && assistant.stopReason !== \"error\") {\n\t\t\t\t\t\tconst contextTokens = calculateContextTokens(assistant.usage);\n\t\t\t\t\t\tif (contextTokens > 0) {\n\t\t\t\t\t\t\thasPostCompactionUsage = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!hasPostCompactionUsage) {\n\t\t\t\treturn { tokens: null, contextWindow, percent: null };\n\t\t\t}\n\t\t}\n\n\t\tconst estimate = estimateContextTokens(this.deps.getMessages());\n\t\tconst percent = (estimate.tokens / contextWindow) * 100;\n\n\t\treturn {\n\t\t\ttokens: estimate.tokens,\n\t\t\tcontextWindow,\n\t\t\tpercent,\n\t\t};\n\t}\n\n\t/**\n\t * Export session to HTML.\n\t * @param outputPath Optional output path (defaults to session directory)\n\t * @returns Path to exported file\n\t */\n\tasync exportToHtml(outputPath?: string): Promise<string> {\n\t\tconst sessionManager = this.deps.getSessionManager();\n\t\tconst themeName = this.deps.getSettingsManager().getTheme();\n\n\t\t// Create tool renderer if we have an extension runner (for custom tool HTML rendering)\n\t\tconst toolRenderer: ToolHtmlRenderer = createToolHtmlRenderer({\n\t\t\tgetToolDefinition: (name) => this.deps.getToolDefinition(name),\n\t\t\ttheme,\n\t\t\tcwd: sessionManager.getCwd(),\n\t\t});\n\n\t\treturn await exportSessionToHtml(sessionManager, this.deps.getState(), {\n\t\t\toutputPath,\n\t\t\tthemeName,\n\t\t\ttoolRenderer,\n\t\t});\n\t}\n\n\t/**\n\t * Export the current session branch to a JSONL file.\n\t * Writes the session header followed by all entries on the current branch path.\n\t * @param outputPath Target file path. If omitted, generates a timestamped file in cwd.\n\t * @returns The resolved output file path.\n\t */\n\texportToJsonl(outputPath?: string): string {\n\t\tconst sessionManager = this.deps.getSessionManager();\n\t\tconst filePath = resolvePath(\n\t\t\toutputPath ?? `session-${new Date().toISOString().replace(/[:.]/g, \"-\")}.jsonl`,\n\t\t\tprocess.cwd(),\n\t\t);\n\t\tconst dir = dirname(filePath);\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true });\n\t\t}\n\n\t\tconst header: SessionHeader = {\n\t\t\ttype: \"session\",\n\t\t\tversion: CURRENT_SESSION_VERSION,\n\t\t\tid: sessionManager.getSessionId(),\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tcwd: sessionManager.getCwd(),\n\t\t};\n\n\t\tconst branchEntries = sessionManager.getBranch();\n\t\tconst lines = [JSON.stringify(header)];\n\n\t\t// Re-chain parentIds to form a linear sequence\n\t\tlet prevId: string | null = null;\n\t\tfor (const entry of branchEntries) {\n\t\t\tconst linear = { ...entry, parentId: prevId };\n\t\t\tlines.push(JSON.stringify(linear));\n\t\t\tprevId = entry.id;\n\t\t}\n\n\t\twriteFileSync(filePath, `${lines.join(\"\\n\")}\\n`);\n\t\treturn filePath;\n\t}\n\n\t/**\n\t * Get text content of last assistant message.\n\t * Useful for /copy command.\n\t * @returns Text content, or undefined if no assistant message exists\n\t */\n\tgetLastAssistantText(): string | undefined {\n\t\tconst lastAssistant = this.deps\n\t\t\t.getMessages()\n\t\t\t.slice()\n\t\t\t.reverse()\n\t\t\t.find((m) => {\n\t\t\t\tif (m.role !== \"assistant\") return false;\n\t\t\t\tconst msg = m as AssistantMessage;\n\t\t\t\t// Skip aborted messages with no content\n\t\t\t\tif (msg.stopReason === \"aborted\" && msg.content.length === 0) return false;\n\t\t\t\treturn true;\n\t\t\t});\n\n\t\tif (!lastAssistant) return undefined;\n\n\t\tlet text = \"\";\n\t\tfor (const content of (lastAssistant as AssistantMessage).content) {\n\t\t\tif (content.type === \"text\") {\n\t\t\t\ttext += content.text;\n\t\t\t}\n\t\t}\n\n\t\treturn text.trim() || undefined;\n\t}\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"footer.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/footer.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,SAAS,EAAiC,MAAM,oBAAoB,CAAC;AACnF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,uCAAuC,CAAC;AAqFxF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAYhF;AAoBD,qBAAa,eAAgB,YAAW,SAAS;IAChD,OAAO,CAAC,kBAAkB,CAAQ;IAClC,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,UAAU,CAA6B;IAC/C,OAAO,CAAC,aAAa,CAAC,CAAsB;IAE5C,YAAY,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,0BAA0B,EAGxE;IAED,UAAU,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAGtC;IAED,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE5C;IAED;;OAEG;IACH,UAAU,IAAI,IAAI,CAEjB;IAED;;;OAGG;IACH,OAAO,IAAI,IAAI,CAEd;IAED,OAAO,CAAC,gBAAgB;IAsDxB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CA8J9B;CACD","sourcesContent":["import { isAbsolute, relative, resolve, sep } from \"node:path\";\nimport { type Component, truncateToWidth, visibleWidth } from \"@caupulican/pi-tui\";\nimport type { AgentSession } from \"../../../core/agent-session.ts\";\nimport type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.ts\";\nimport { theme } from \"../theme/theme.ts\";\n\n/**\n * Sanitize text for display in a single-line status.\n * Removes newlines, tabs, carriage returns, and other control characters.\n */\nfunction sanitizeStatusText(text: string): string {\n\t// Replace newlines, tabs, carriage returns with space, then collapse multiple spaces\n\treturn text\n\t\t.replace(/[\\r\\n\\t]/g, \" \")\n\t\t.replace(/ +/g, \" \")\n\t\t.trim();\n}\n\nfunction stripAnsi(text: string): string {\n\treturn text.replace(/\\u001b\\[[0-?]*[ -/]*[@-~]/g, \"\");\n}\n\nfunction normalizeLearningPhase(phase: string): string {\n\tconst normalized = phase\n\t\t.toLowerCase()\n\t\t.replace(/[^a-z0-9_-]+/g, \"\")\n\t\t.trim();\n\tif (!normalized) return \"active\";\n\tif (normalized === \"starting\") return \"start\";\n\tif (normalized === \"mapping\") return \"map\";\n\tif (normalized === \"scanning\") return \"scan\";\n\tif (normalized === \"auditing\") return \"audit\";\n\tif (normalized === \"learning\") return \"run\";\n\tif (normalized === \"pruning\") return \"prune\";\n\treturn normalized.slice(0, 16);\n}\n\nfunction formatExtensionStatuses(statuses: ReadonlyMap<string, string>): string[] {\n\tconst regularStatuses: string[] = [];\n\tconst learningPhases = new Set<string>();\n\tlet sawLearningStatus = false;\n\n\tfor (const [key, rawText] of Array.from(statuses.entries()).sort(([a], [b]) => a.localeCompare(b))) {\n\t\tconst text = sanitizeStatusText(rawText);\n\t\tconst plain = stripAnsi(text).trim();\n\t\tconst plainLower = plain.toLowerCase();\n\t\tlet phase: string | undefined;\n\n\t\tif (plainLower.startsWith(\"(learning)\")) {\n\t\t\tphase = plain.slice(\"(learning)\".length).trim();\n\t\t} else if (plainLower === \"learning\") {\n\t\t\tphase = \"active\";\n\t\t} else if (/^learn(?:ing)?\\s*[: ]/.test(plainLower)) {\n\t\t\tphase = plain.replace(/^learn(?:ing)?\\s*[: ]/i, \"\").trim();\n\t\t}\n\n\t\tif (phase !== undefined) {\n\t\t\tsawLearningStatus = true;\n\t\t\tlearningPhases.add(normalizeLearningPhase(phase));\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (key === \"auto-learn\" || key === \"continuous-learning\") {\n\t\t\tsawLearningStatus = true;\n\t\t\tlearningPhases.add(\"active\");\n\t\t\tcontinue;\n\t\t}\n\n\t\tregularStatuses.push(text);\n\t}\n\n\tif (!sawLearningStatus) return regularStatuses;\n\tconst phases = Array.from(learningPhases).filter((phase) => phase !== \"active\");\n\tconst phaseText = phases.length > 0 ? phases.join(\"/\") : \"active\";\n\treturn [theme.fg(\"warning\", \"learn\") + theme.fg(\"dim\", `:${phaseText}`), ...regularStatuses];\n}\n\n/**\n * Format token counts for compact footer display.\n */\nfunction formatTokens(count: number): string {\n\tif (count < 1000) return count.toString();\n\tif (count < 10000) return `${(count / 1000).toFixed(1)}k`;\n\tif (count < 1000000) return `${Math.round(count / 1000)}k`;\n\tif (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;\n\treturn `${Math.round(count / 1000000)}M`;\n}\n\nexport function formatCwdForFooter(cwd: string, home: string | undefined): string {\n\tif (!home) return cwd;\n\n\tconst resolvedCwd = resolve(cwd);\n\tconst resolvedHome = resolve(home);\n\tconst relativeToHome = relative(resolvedHome, resolvedCwd);\n\tconst isInsideHome =\n\t\trelativeToHome === \"\" ||\n\t\t(relativeToHome !== \"..\" && !relativeToHome.startsWith(`..${sep}`) && !isAbsolute(relativeToHome));\n\n\tif (!isInsideHome) return cwd;\n\treturn relativeToHome === \"\" ? \"~\" : `~${sep}${relativeToHome}`;\n}\n\n/**\n * Footer component that shows pwd, token stats, and context usage.\n * Computes token/context stats from session, gets git branch and extension statuses from provider.\n */\ntype FooterUsageSnapshot = {\n\tentryCount: number;\n\tmessageCount: number;\n\ttotalInput: number;\n\ttotalOutput: number;\n\ttotalCacheRead: number;\n\ttotalCacheWrite: number;\n\ttotalCost: number;\n\t/** Rolled-up cost of spawned/subagent sessions (Cost Aggregation). 0 when none. */\n\ttotalSpawnedCost: number;\n\tdailyTotalCost: number;\n\tcontextUsage: ReturnType<AgentSession[\"getContextUsage\"]>;\n};\n\nexport class FooterComponent implements Component {\n\tprivate autoCompactEnabled = true;\n\tprivate session: AgentSession;\n\tprivate footerData: ReadonlyFooterDataProvider;\n\tprivate usageSnapshot?: FooterUsageSnapshot;\n\n\tconstructor(session: AgentSession, footerData: ReadonlyFooterDataProvider) {\n\t\tthis.session = session;\n\t\tthis.footerData = footerData;\n\t}\n\n\tsetSession(session: AgentSession): void {\n\t\tthis.session = session;\n\t\tthis.usageSnapshot = undefined;\n\t}\n\n\tsetAutoCompactEnabled(enabled: boolean): void {\n\t\tthis.autoCompactEnabled = enabled;\n\t}\n\n\t/**\n\t * Invalidate cached footer stats when session state changes.\n\t */\n\tinvalidate(): void {\n\t\tthis.usageSnapshot = undefined;\n\t}\n\n\t/**\n\t * Clean up resources.\n\t * Git watcher cleanup now handled by provider.\n\t */\n\tdispose(): void {\n\t\t// Git watcher cleanup handled by provider\n\t}\n\n\tprivate getUsageSnapshot(messageCount: number): FooterUsageSnapshot {\n\t\tconst sessionManager = this.session.sessionManager as AgentSession[\"sessionManager\"] & {\n\t\t\tgetEntryCount?: () => number;\n\t\t};\n\t\tconst entryCount = sessionManager.getEntryCount?.() ?? sessionManager.getEntries().length;\n\t\tconst cached = this.usageSnapshot;\n\t\tif (cached && cached.entryCount === entryCount && cached.messageCount === messageCount) {\n\t\t\treturn cached;\n\t\t}\n\n\t\t// Calculate cumulative usage from ALL session entries in one batched pass.\n\t\t// This avoids per-frame defensive array allocation when only the TUI redraws.\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\n\t\tconst entries = this.session.sessionManager.getEntries();\n\t\tfor (let i = 0; i < entries.length; i++) {\n\t\t\tconst entry = entries[i];\n\t\t\tif (entry.type !== \"message\" || entry.message.role !== \"assistant\") continue;\n\t\t\tconst usage = entry.message.usage;\n\t\t\tif (!usage) continue;\n\t\t\ttotalInput += usage.input;\n\t\t\ttotalOutput += usage.output;\n\t\t\ttotalCacheRead += usage.cacheRead;\n\t\t\ttotalCacheWrite += usage.cacheWrite;\n\t\t\ttotalCost += usage.cost.total;\n\t\t}\n\n\t\t// Roll up spawned/subagent cost (Cost Aggregation, Model A). Derived from the same\n\t\t// session entries, so the {entryCount} cache key above busts when new reports land.\n\t\tconst totalSpawnedCost = this.session.getSpawnedUsage().cost;\n\t\tconst dailyTotalCost = this.session.getDailyUsageTotals?.().totalCost ?? 0;\n\n\t\tconst snapshot: FooterUsageSnapshot = {\n\t\t\tentryCount,\n\t\t\tmessageCount,\n\t\t\ttotalInput,\n\t\t\ttotalOutput,\n\t\t\ttotalCacheRead,\n\t\t\ttotalCacheWrite,\n\t\t\ttotalCost,\n\t\t\ttotalSpawnedCost,\n\t\t\tdailyTotalCost,\n\t\t\t// Calculate context usage from session (handles compaction correctly).\n\t\t\t// After compaction, tokens are unknown until the next LLM response.\n\t\t\tcontextUsage: this.session.getContextUsage(),\n\t\t};\n\t\tthis.usageSnapshot = snapshot;\n\t\treturn snapshot;\n\t}\n\n\trender(width: number): string[] {\n\t\tconst state = this.session.state;\n\t\tconst usageSnapshot = this.getUsageSnapshot(state.messages?.length ?? 0);\n\t\tconst {\n\t\t\ttotalInput,\n\t\t\ttotalOutput,\n\t\t\ttotalCacheRead,\n\t\t\ttotalCacheWrite,\n\t\t\ttotalCost,\n\t\t\ttotalSpawnedCost,\n\t\t\tdailyTotalCost,\n\t\t\tcontextUsage,\n\t\t} = usageSnapshot;\n\t\tconst contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;\n\t\tconst contextPercentValue = contextUsage?.percent ?? 0;\n\t\tconst contextPercent = contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : \"?\";\n\n\t\t// Replace home directory with ~\n\t\tlet pwd = formatCwdForFooter(this.session.sessionManager.getCwd(), process.env.HOME || process.env.USERPROFILE);\n\n\t\t// Add git branch if available\n\t\tconst branch = this.footerData.getGitBranch();\n\t\tif (branch) {\n\t\t\tpwd = `${pwd} (${branch})`;\n\t\t}\n\n\t\t// Add session name if set\n\t\tconst sessionName = this.session.sessionManager.getSessionName();\n\t\tif (sessionName) {\n\t\t\tpwd = `${pwd} • ${sessionName}`;\n\t\t}\n\n\t\t// Build stats line\n\t\tconst statsParts = [];\n\t\tif (totalInput) statsParts.push(`↑${formatTokens(totalInput)}`);\n\t\tif (totalOutput) statsParts.push(`↓${formatTokens(totalOutput)}`);\n\t\tif (totalCacheRead) statsParts.push(`R${formatTokens(totalCacheRead)}`);\n\t\tif (totalCacheWrite) statsParts.push(`W${formatTokens(totalCacheWrite)}`);\n\n\t\t// Show cost with \"(sub)\" indicator if using OAuth subscription\n\t\tconst usingSubscription = state.model ? this.session.modelRegistry.isUsingOAuth(state.model) : false;\n\t\tconst grandTotalCost = totalCost + totalSpawnedCost;\n\t\tif (grandTotalCost || usingSubscription) {\n\t\t\tconst costStr = `$${grandTotalCost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`;\n\t\t\tstatsParts.push(costStr);\n\t\t}\n\t\tif (dailyTotalCost) {\n\t\t\tstatsParts.push(`day:$${dailyTotalCost.toFixed(3)}`);\n\t\t}\n\n\t\t// Colorize context percentage based on usage\n\t\tlet contextPercentStr: string;\n\t\tconst autoIndicator = this.autoCompactEnabled ? \" (auto)\" : \"\";\n\t\tconst contextPercentDisplay =\n\t\t\tcontextPercent === \"?\"\n\t\t\t\t? `?/${formatTokens(contextWindow)}${autoIndicator}`\n\t\t\t\t: `${contextPercent}%/${formatTokens(contextWindow)}${autoIndicator}`;\n\t\tif (contextPercentValue > 90) {\n\t\t\tcontextPercentStr = theme.fg(\"error\", contextPercentDisplay);\n\t\t} else if (contextPercentValue > 70) {\n\t\t\tcontextPercentStr = theme.fg(\"warning\", contextPercentDisplay);\n\t\t} else {\n\t\t\tcontextPercentStr = contextPercentDisplay;\n\t\t}\n\t\tstatsParts.push(contextPercentStr);\n\n\t\tlet statsLeft = statsParts.join(\" \");\n\n\t\t// Add model display name on the right side, plus thinking level if model supports it\n\t\tconst modelName = state.model?.name || state.model?.id || \"no-model\";\n\n\t\tlet statsLeftWidth = visibleWidth(statsLeft);\n\n\t\t// If statsLeft is too wide, truncate it\n\t\tif (statsLeftWidth > width) {\n\t\t\tstatsLeft = truncateToWidth(statsLeft, width, \"...\");\n\t\t\tstatsLeftWidth = visibleWidth(statsLeft);\n\t\t}\n\n\t\t// Calculate available space for padding (minimum 2 spaces between stats and model)\n\t\tconst minPadding = 2;\n\n\t\t// Add thinking level indicator if model supports reasoning\n\t\tlet rightSideWithoutProvider = modelName;\n\t\tif (state.model?.reasoning) {\n\t\t\tconst thinkingLevel = state.thinkingLevel || \"off\";\n\t\t\trightSideWithoutProvider =\n\t\t\t\tthinkingLevel === \"off\" ? `${modelName} • thinking off` : `${modelName} • ${thinkingLevel}`;\n\t\t}\n\n\t\t// Prepend the provider in parentheses if there are multiple providers and there's enough room\n\t\tlet rightSide = rightSideWithoutProvider;\n\t\tif (this.footerData.getAvailableProviderCount() > 1 && state.model) {\n\t\t\trightSide = `(${state.model!.provider}) ${rightSideWithoutProvider}`;\n\t\t\tif (statsLeftWidth + minPadding + visibleWidth(rightSide) > width) {\n\t\t\t\t// Too wide, fall back\n\t\t\t\trightSide = rightSideWithoutProvider;\n\t\t\t}\n\t\t}\n\n\t\tconst rightSideWidth = visibleWidth(rightSide);\n\t\tconst totalNeeded = statsLeftWidth + minPadding + rightSideWidth;\n\n\t\tlet statsLine: string;\n\t\tif (totalNeeded <= width) {\n\t\t\t// Both fit - add padding to right-align model\n\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - rightSideWidth);\n\t\t\tstatsLine = statsLeft + padding + rightSide;\n\t\t} else {\n\t\t\t// Need to truncate right side\n\t\t\tconst availableForRight = width - statsLeftWidth - minPadding;\n\t\t\tif (availableForRight > 0) {\n\t\t\t\tconst truncatedRight = truncateToWidth(rightSide, availableForRight, \"\");\n\t\t\t\tconst truncatedRightWidth = visibleWidth(truncatedRight);\n\t\t\t\tconst padding = \" \".repeat(Math.max(0, width - statsLeftWidth - truncatedRightWidth));\n\t\t\t\tstatsLine = statsLeft + padding + truncatedRight;\n\t\t\t} else {\n\t\t\t\t// Not enough space for right side at all\n\t\t\t\tstatsLine = statsLeft;\n\t\t\t}\n\t\t}\n\n\t\t// Apply dim to each part separately. statsLeft may contain color codes (for context %)\n\t\t// that end with a reset, which would clear an outer dim wrapper. So we dim the parts\n\t\t// before and after the colored section independently.\n\t\tconst dimStatsLeft = theme.fg(\"dim\", statsLeft);\n\t\tconst remainder = statsLine.slice(statsLeft.length); // padding + rightSide\n\t\tconst dimRemainder = theme.fg(\"dim\", remainder);\n\n\t\tconst pwdLine = truncateToWidth(theme.fg(\"dim\", pwd), width, theme.fg(\"dim\", \"...\"));\n\t\tconst lines = [pwdLine, dimStatsLeft + dimRemainder];\n\n\t\t// Add extension statuses on a single line. Learning-related statuses are\n\t\t// folded into one compact chip so independent learning systems do not render\n\t\t// brittle duplicates like \"(learning) (learning) auto\".\n\t\tconst extensionStatuses = this.footerData.getExtensionStatuses();\n\t\tconst autonomyStatus = this.footerData.getAutonomyStatus();\n\n\t\tconst statusParts: string[] = [];\n\t\tif (autonomyStatus) {\n\t\t\tconst sanitizedAutonomyStatus = sanitizeStatusText(autonomyStatus);\n\t\t\tif (sanitizedAutonomyStatus) {\n\t\t\t\tstatusParts.push(sanitizedAutonomyStatus);\n\t\t\t}\n\t\t}\n\t\tif (extensionStatuses.size > 0) {\n\t\t\tconst extLine = formatExtensionStatuses(extensionStatuses).join(\" \");\n\t\t\tif (extLine) {\n\t\t\t\tstatusParts.push(extLine);\n\t\t\t}\n\t\t}\n\n\t\tif (statusParts.length > 0) {\n\t\t\tconst statusLine = statusParts.join(\" \");\n\t\t\tlines.push(truncateToWidth(statusLine, width, theme.fg(\"dim\", \"...\")));\n\t\t}\n\n\t\treturn lines;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"footer.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/footer.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,SAAS,EAAiC,MAAM,oBAAoB,CAAC;AACnF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAEnE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,uCAAuC,CAAC;AAqFxF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAYhF;AAgBD,qBAAa,eAAgB,YAAW,SAAS;IAChD,OAAO,CAAC,kBAAkB,CAAQ;IAClC,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,UAAU,CAA6B;IAC/C,OAAO,CAAC,aAAa,CAAC,CAAsB;IAE5C,YAAY,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,0BAA0B,EAGxE;IAED,UAAU,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAGtC;IAED,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE5C;IAED;;OAEG;IACH,UAAU,IAAI,IAAI,CAEjB;IAED;;;OAGG;IACH,OAAO,IAAI,IAAI,CAEd;IAED,OAAO,CAAC,gBAAgB;IA4CxB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CA6I9B;CACD","sourcesContent":["import { isAbsolute, relative, resolve, sep } from \"node:path\";\nimport { type Component, truncateToWidth, visibleWidth } from \"@caupulican/pi-tui\";\nimport type { AgentSession } from \"../../../core/agent-session.ts\";\nimport { formatFooterCostParts } from \"../../../core/cost/cost-summary.ts\";\nimport type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.ts\";\nimport { theme } from \"../theme/theme.ts\";\n\n/**\n * Sanitize text for display in a single-line status.\n * Removes newlines, tabs, carriage returns, and other control characters.\n */\nfunction sanitizeStatusText(text: string): string {\n\t// Replace newlines, tabs, carriage returns with space, then collapse multiple spaces\n\treturn text\n\t\t.replace(/[\\r\\n\\t]/g, \" \")\n\t\t.replace(/ +/g, \" \")\n\t\t.trim();\n}\n\nfunction stripAnsi(text: string): string {\n\treturn text.replace(/\\u001b\\[[0-?]*[ -/]*[@-~]/g, \"\");\n}\n\nfunction normalizeLearningPhase(phase: string): string {\n\tconst normalized = phase\n\t\t.toLowerCase()\n\t\t.replace(/[^a-z0-9_-]+/g, \"\")\n\t\t.trim();\n\tif (!normalized) return \"active\";\n\tif (normalized === \"starting\") return \"start\";\n\tif (normalized === \"mapping\") return \"map\";\n\tif (normalized === \"scanning\") return \"scan\";\n\tif (normalized === \"auditing\") return \"audit\";\n\tif (normalized === \"learning\") return \"run\";\n\tif (normalized === \"pruning\") return \"prune\";\n\treturn normalized.slice(0, 16);\n}\n\nfunction formatExtensionStatuses(statuses: ReadonlyMap<string, string>): string[] {\n\tconst regularStatuses: string[] = [];\n\tconst learningPhases = new Set<string>();\n\tlet sawLearningStatus = false;\n\n\tfor (const [key, rawText] of Array.from(statuses.entries()).sort(([a], [b]) => a.localeCompare(b))) {\n\t\tconst text = sanitizeStatusText(rawText);\n\t\tconst plain = stripAnsi(text).trim();\n\t\tconst plainLower = plain.toLowerCase();\n\t\tlet phase: string | undefined;\n\n\t\tif (plainLower.startsWith(\"(learning)\")) {\n\t\t\tphase = plain.slice(\"(learning)\".length).trim();\n\t\t} else if (plainLower === \"learning\") {\n\t\t\tphase = \"active\";\n\t\t} else if (/^learn(?:ing)?\\s*[: ]/.test(plainLower)) {\n\t\t\tphase = plain.replace(/^learn(?:ing)?\\s*[: ]/i, \"\").trim();\n\t\t}\n\n\t\tif (phase !== undefined) {\n\t\t\tsawLearningStatus = true;\n\t\t\tlearningPhases.add(normalizeLearningPhase(phase));\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (key === \"auto-learn\" || key === \"continuous-learning\") {\n\t\t\tsawLearningStatus = true;\n\t\t\tlearningPhases.add(\"active\");\n\t\t\tcontinue;\n\t\t}\n\n\t\tregularStatuses.push(text);\n\t}\n\n\tif (!sawLearningStatus) return regularStatuses;\n\tconst phases = Array.from(learningPhases).filter((phase) => phase !== \"active\");\n\tconst phaseText = phases.length > 0 ? phases.join(\"/\") : \"active\";\n\treturn [theme.fg(\"warning\", \"learn\") + theme.fg(\"dim\", `:${phaseText}`), ...regularStatuses];\n}\n\n/**\n * Format token counts for compact footer display.\n */\nfunction formatTokens(count: number): string {\n\tif (count < 1000) return count.toString();\n\tif (count < 10000) return `${(count / 1000).toFixed(1)}k`;\n\tif (count < 1000000) return `${Math.round(count / 1000)}k`;\n\tif (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;\n\treturn `${Math.round(count / 1000000)}M`;\n}\n\nexport function formatCwdForFooter(cwd: string, home: string | undefined): string {\n\tif (!home) return cwd;\n\n\tconst resolvedCwd = resolve(cwd);\n\tconst resolvedHome = resolve(home);\n\tconst relativeToHome = relative(resolvedHome, resolvedCwd);\n\tconst isInsideHome =\n\t\trelativeToHome === \"\" ||\n\t\t(relativeToHome !== \"..\" && !relativeToHome.startsWith(`..${sep}`) && !isAbsolute(relativeToHome));\n\n\tif (!isInsideHome) return cwd;\n\treturn relativeToHome === \"\" ? \"~\" : `~${sep}${relativeToHome}`;\n}\n\n/**\n * Footer component that shows pwd, token stats, and context usage.\n * Computes token/context stats from session, gets git branch and extension statuses from provider.\n */\ntype FooterUsageSnapshot = {\n\tentryCount: number;\n\tmessageCount: number;\n\ttotalInput: number;\n\ttotalOutput: number;\n\ttotalCacheRead: number;\n\ttotalCacheWrite: number;\n\tcontextUsage: ReturnType<AgentSession[\"getContextUsage\"]>;\n};\n\nexport class FooterComponent implements Component {\n\tprivate autoCompactEnabled = true;\n\tprivate session: AgentSession;\n\tprivate footerData: ReadonlyFooterDataProvider;\n\tprivate usageSnapshot?: FooterUsageSnapshot;\n\n\tconstructor(session: AgentSession, footerData: ReadonlyFooterDataProvider) {\n\t\tthis.session = session;\n\t\tthis.footerData = footerData;\n\t}\n\n\tsetSession(session: AgentSession): void {\n\t\tthis.session = session;\n\t\tthis.usageSnapshot = undefined;\n\t}\n\n\tsetAutoCompactEnabled(enabled: boolean): void {\n\t\tthis.autoCompactEnabled = enabled;\n\t}\n\n\t/**\n\t * Invalidate cached footer stats when session state changes.\n\t */\n\tinvalidate(): void {\n\t\tthis.usageSnapshot = undefined;\n\t}\n\n\t/**\n\t * Clean up resources.\n\t * Git watcher cleanup now handled by provider.\n\t */\n\tdispose(): void {\n\t\t// Git watcher cleanup handled by provider\n\t}\n\n\tprivate getUsageSnapshot(messageCount: number): FooterUsageSnapshot {\n\t\tconst sessionManager = this.session.sessionManager as AgentSession[\"sessionManager\"] & {\n\t\t\tgetEntryCount?: () => number;\n\t\t};\n\t\tconst entryCount = sessionManager.getEntryCount?.() ?? sessionManager.getEntries().length;\n\t\tconst cached = this.usageSnapshot;\n\t\tif (cached && cached.entryCount === entryCount && cached.messageCount === messageCount) {\n\t\t\treturn cached;\n\t\t}\n\n\t\t// Calculate cumulative usage from ALL session entries in one batched pass.\n\t\t// This avoids per-frame defensive array allocation when only the TUI redraws.\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\n\t\tconst entries = this.session.sessionManager.getEntries();\n\t\tfor (let i = 0; i < entries.length; i++) {\n\t\t\tconst entry = entries[i];\n\t\t\tif (entry.type !== \"message\" || entry.message.role !== \"assistant\") continue;\n\t\t\tconst usage = entry.message.usage;\n\t\t\tif (!usage) continue;\n\t\t\ttotalInput += usage.input;\n\t\t\ttotalOutput += usage.output;\n\t\t\ttotalCacheRead += usage.cacheRead;\n\t\t\ttotalCacheWrite += usage.cacheWrite;\n\t\t}\n\n\t\tconst snapshot: FooterUsageSnapshot = {\n\t\t\tentryCount,\n\t\t\tmessageCount,\n\t\t\ttotalInput,\n\t\t\ttotalOutput,\n\t\t\ttotalCacheRead,\n\t\t\ttotalCacheWrite,\n\t\t\t// Calculate context usage from session (handles compaction correctly).\n\t\t\t// After compaction, tokens are unknown until the next LLM response.\n\t\t\tcontextUsage: this.session.getContextUsage(),\n\t\t};\n\t\tthis.usageSnapshot = snapshot;\n\t\treturn snapshot;\n\t}\n\n\trender(width: number): string[] {\n\t\tconst state = this.session.state;\n\t\tconst usageSnapshot = this.getUsageSnapshot(state.messages?.length ?? 0);\n\t\tconst { totalInput, totalOutput, totalCacheRead, totalCacheWrite, contextUsage } = usageSnapshot;\n\t\tconst costSummary = this.session.getCostSummary();\n\t\tconst contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;\n\t\tconst contextPercentValue = contextUsage?.percent ?? 0;\n\t\tconst contextPercent = contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : \"?\";\n\n\t\t// Replace home directory with ~\n\t\tlet pwd = formatCwdForFooter(this.session.sessionManager.getCwd(), process.env.HOME || process.env.USERPROFILE);\n\n\t\t// Add git branch if available\n\t\tconst branch = this.footerData.getGitBranch();\n\t\tif (branch) {\n\t\t\tpwd = `${pwd} (${branch})`;\n\t\t}\n\n\t\t// Add session name if set\n\t\tconst sessionName = this.session.sessionManager.getSessionName();\n\t\tif (sessionName) {\n\t\t\tpwd = `${pwd} • ${sessionName}`;\n\t\t}\n\n\t\t// Build stats line\n\t\tconst statsParts = [];\n\t\tif (totalInput) statsParts.push(`↑${formatTokens(totalInput)}`);\n\t\tif (totalOutput) statsParts.push(`↓${formatTokens(totalOutput)}`);\n\t\tif (totalCacheRead) statsParts.push(`R${formatTokens(totalCacheRead)}`);\n\t\tif (totalCacheWrite) statsParts.push(`W${formatTokens(totalCacheWrite)}`);\n\n\t\tstatsParts.push(...formatFooterCostParts(costSummary));\n\n\t\t// Colorize context percentage based on usage\n\t\tlet contextPercentStr: string;\n\t\tconst autoIndicator = this.autoCompactEnabled ? \" (auto)\" : \"\";\n\t\tconst contextPercentDisplay =\n\t\t\tcontextPercent === \"?\"\n\t\t\t\t? `?/${formatTokens(contextWindow)}${autoIndicator}`\n\t\t\t\t: `${contextPercent}%/${formatTokens(contextWindow)}${autoIndicator}`;\n\t\tif (contextPercentValue > 90) {\n\t\t\tcontextPercentStr = theme.fg(\"error\", contextPercentDisplay);\n\t\t} else if (contextPercentValue > 70) {\n\t\t\tcontextPercentStr = theme.fg(\"warning\", contextPercentDisplay);\n\t\t} else {\n\t\t\tcontextPercentStr = contextPercentDisplay;\n\t\t}\n\t\tstatsParts.push(contextPercentStr);\n\n\t\tlet statsLeft = statsParts.join(\" \");\n\n\t\t// Add model display name on the right side, plus thinking level if model supports it\n\t\tconst modelName = state.model?.name || state.model?.id || \"no-model\";\n\n\t\tlet statsLeftWidth = visibleWidth(statsLeft);\n\n\t\t// If statsLeft is too wide, truncate it\n\t\tif (statsLeftWidth > width) {\n\t\t\tstatsLeft = truncateToWidth(statsLeft, width, \"...\");\n\t\t\tstatsLeftWidth = visibleWidth(statsLeft);\n\t\t}\n\n\t\t// Calculate available space for padding (minimum 2 spaces between stats and model)\n\t\tconst minPadding = 2;\n\n\t\t// Add thinking level indicator if model supports reasoning\n\t\tlet rightSideWithoutProvider = modelName;\n\t\tif (state.model?.reasoning) {\n\t\t\tconst thinkingLevel = state.thinkingLevel || \"off\";\n\t\t\trightSideWithoutProvider =\n\t\t\t\tthinkingLevel === \"off\" ? `${modelName} • thinking off` : `${modelName} • ${thinkingLevel}`;\n\t\t}\n\n\t\t// Prepend the provider in parentheses if there are multiple providers and there's enough room\n\t\tlet rightSide = rightSideWithoutProvider;\n\t\tif (this.footerData.getAvailableProviderCount() > 1 && state.model) {\n\t\t\trightSide = `(${state.model!.provider}) ${rightSideWithoutProvider}`;\n\t\t\tif (statsLeftWidth + minPadding + visibleWidth(rightSide) > width) {\n\t\t\t\t// Too wide, fall back\n\t\t\t\trightSide = rightSideWithoutProvider;\n\t\t\t}\n\t\t}\n\n\t\tconst rightSideWidth = visibleWidth(rightSide);\n\t\tconst totalNeeded = statsLeftWidth + minPadding + rightSideWidth;\n\n\t\tlet statsLine: string;\n\t\tif (totalNeeded <= width) {\n\t\t\t// Both fit - add padding to right-align model\n\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - rightSideWidth);\n\t\t\tstatsLine = statsLeft + padding + rightSide;\n\t\t} else {\n\t\t\t// Need to truncate right side\n\t\t\tconst availableForRight = width - statsLeftWidth - minPadding;\n\t\t\tif (availableForRight > 0) {\n\t\t\t\tconst truncatedRight = truncateToWidth(rightSide, availableForRight, \"\");\n\t\t\t\tconst truncatedRightWidth = visibleWidth(truncatedRight);\n\t\t\t\tconst padding = \" \".repeat(Math.max(0, width - statsLeftWidth - truncatedRightWidth));\n\t\t\t\tstatsLine = statsLeft + padding + truncatedRight;\n\t\t\t} else {\n\t\t\t\t// Not enough space for right side at all\n\t\t\t\tstatsLine = statsLeft;\n\t\t\t}\n\t\t}\n\n\t\t// Apply dim to each part separately. statsLeft may contain color codes (for context %)\n\t\t// that end with a reset, which would clear an outer dim wrapper. So we dim the parts\n\t\t// before and after the colored section independently.\n\t\tconst dimStatsLeft = theme.fg(\"dim\", statsLeft);\n\t\tconst remainder = statsLine.slice(statsLeft.length); // padding + rightSide\n\t\tconst dimRemainder = theme.fg(\"dim\", remainder);\n\n\t\tconst pwdLine = truncateToWidth(theme.fg(\"dim\", pwd), width, theme.fg(\"dim\", \"...\"));\n\t\tconst lines = [pwdLine, dimStatsLeft + dimRemainder];\n\n\t\t// Add extension statuses on a single line. Learning-related statuses are\n\t\t// folded into one compact chip so independent learning systems do not render\n\t\t// brittle duplicates like \"(learning) (learning) auto\".\n\t\tconst extensionStatuses = this.footerData.getExtensionStatuses();\n\t\tconst autonomyStatus = this.footerData.getAutonomyStatus();\n\n\t\tconst statusParts: string[] = [];\n\t\tif (autonomyStatus) {\n\t\t\tconst sanitizedAutonomyStatus = sanitizeStatusText(autonomyStatus);\n\t\t\tif (sanitizedAutonomyStatus) {\n\t\t\t\tstatusParts.push(sanitizedAutonomyStatus);\n\t\t\t}\n\t\t}\n\t\tif (extensionStatuses.size > 0) {\n\t\t\tconst extLine = formatExtensionStatuses(extensionStatuses).join(\" \");\n\t\t\tif (extLine) {\n\t\t\t\tstatusParts.push(extLine);\n\t\t\t}\n\t\t}\n\n\t\tif (statusParts.length > 0) {\n\t\t\tconst statusLine = statusParts.join(\" \");\n\t\t\tlines.push(truncateToWidth(statusLine, width, theme.fg(\"dim\", \"...\")));\n\t\t}\n\n\t\treturn lines;\n\t}\n}\n"]}
|