@caupulican/pi-adaptative 0.81.22 → 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.
Files changed (73) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/core/agent-session.d.ts +5 -1
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js +28 -20
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/autonomy/status.d.ts +2 -3
  7. package/dist/core/autonomy/status.d.ts.map +1 -1
  8. package/dist/core/autonomy/status.js +3 -12
  9. package/dist/core/autonomy/status.js.map +1 -1
  10. package/dist/core/autonomy-telemetry.d.ts +3 -8
  11. package/dist/core/autonomy-telemetry.d.ts.map +1 -1
  12. package/dist/core/autonomy-telemetry.js +15 -20
  13. package/dist/core/autonomy-telemetry.js.map +1 -1
  14. package/dist/core/cost/cost-summary.d.ts +26 -0
  15. package/dist/core/cost/cost-summary.d.ts.map +1 -0
  16. package/dist/core/cost/cost-summary.js +85 -0
  17. package/dist/core/cost/cost-summary.js.map +1 -0
  18. package/dist/core/cost/session-usage.d.ts.map +1 -1
  19. package/dist/core/cost/session-usage.js +8 -2
  20. package/dist/core/cost/session-usage.js.map +1 -1
  21. package/dist/core/failure-corpus.d.ts +10 -1
  22. package/dist/core/failure-corpus.d.ts.map +1 -1
  23. package/dist/core/failure-corpus.js +32 -26
  24. package/dist/core/failure-corpus.js.map +1 -1
  25. package/dist/core/session-analytics.d.ts +10 -2
  26. package/dist/core/session-analytics.d.ts.map +1 -1
  27. package/dist/core/session-analytics.js +75 -29
  28. package/dist/core/session-analytics.js.map +1 -1
  29. package/dist/core/settings-manager.d.ts +1 -1
  30. package/dist/core/settings-manager.d.ts.map +1 -1
  31. package/dist/core/settings-manager.js.map +1 -1
  32. package/dist/core/tool-recovery-log-records.d.ts +24 -0
  33. package/dist/core/tool-recovery-log-records.d.ts.map +1 -0
  34. package/dist/core/tool-recovery-log-records.js +63 -0
  35. package/dist/core/tool-recovery-log-records.js.map +1 -0
  36. package/dist/core/tool-recovery-log-worker.d.ts +2 -0
  37. package/dist/core/tool-recovery-log-worker.d.ts.map +1 -0
  38. package/dist/core/tool-recovery-log-worker.js +44 -0
  39. package/dist/core/tool-recovery-log-worker.js.map +1 -0
  40. package/dist/core/tool-recovery-logger.d.ts +59 -0
  41. package/dist/core/tool-recovery-logger.d.ts.map +1 -0
  42. package/dist/core/tool-recovery-logger.js +197 -0
  43. package/dist/core/tool-recovery-logger.js.map +1 -0
  44. package/dist/core/tool-repair-health.d.ts +2 -1
  45. package/dist/core/tool-repair-health.d.ts.map +1 -1
  46. package/dist/core/tool-repair-health.js +13 -4
  47. package/dist/core/tool-repair-health.js.map +1 -1
  48. package/dist/core/tool-repair-settings.d.ts +1 -0
  49. package/dist/core/tool-repair-settings.d.ts.map +1 -1
  50. package/dist/core/tool-repair-settings.js +2 -1
  51. package/dist/core/tool-repair-settings.js.map +1 -1
  52. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  53. package/dist/modes/interactive/components/footer.js +4 -20
  54. package/dist/modes/interactive/components/footer.js.map +1 -1
  55. package/dist/modes/interactive/report-commands.d.ts.map +1 -1
  56. package/dist/modes/interactive/report-commands.js +7 -8
  57. package/dist/modes/interactive/report-commands.js.map +1 -1
  58. package/dist/modes/interactive/session-flow-commands.d.ts +1 -2
  59. package/dist/modes/interactive/session-flow-commands.d.ts.map +1 -1
  60. package/dist/modes/interactive/session-flow-commands.js +8 -4
  61. package/dist/modes/interactive/session-flow-commands.js.map +1 -1
  62. package/docs/settings.md +6 -4
  63. package/docs/tool-repair.md +8 -7
  64. package/docs/usage.md +6 -0
  65. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  66. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  67. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  68. package/examples/extensions/sandbox/package-lock.json +2 -2
  69. package/examples/extensions/sandbox/package.json +1 -1
  70. package/examples/extensions/with-deps/package-lock.json +2 -2
  71. package/examples/extensions/with-deps/package.json +1 -1
  72. package/npm-shrinkwrap.json +12 -12
  73. 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 && isUsage(data.usage)) {
118
- add(data.usage);
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":"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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\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"]}
@@ -40,6 +40,16 @@ export interface FailureCorpusFs {
40
40
  size: number;
41
41
  };
42
42
  }
43
+ export declare function createToolValidationFailureCorpusRecord(args: {
44
+ provider?: string;
45
+ modelId?: string;
46
+ tool: string;
47
+ failureModes: readonly string[];
48
+ shape: readonly ToolValidationFailureShapeEntry[];
49
+ errorKeywords?: readonly string[];
50
+ ts: string;
51
+ }): ToolValidationFailureCorpusRecord;
52
+ export declare function writeFailureCorpusRecord(filePath: string, record: FailureCorpusRecord, fs?: FailureCorpusFs): void;
43
53
  export declare class FailureCorpusRecorder {
44
54
  private total;
45
55
  private unknown;
@@ -69,7 +79,6 @@ export declare class FailureCorpusRecorder {
69
79
  }): void;
70
80
  stats(): FailureCorpusStats;
71
81
  private appendRecord;
72
- private rotateIfNeeded;
73
82
  }
74
83
  export declare function redactSecrets(message: string): string;
75
84
  //# sourceMappingURL=failure-corpus.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"failure-corpus.d.ts","sourceRoot":"","sources":["../../src/core/failure-corpus.ts"],"names":[],"mappings":"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","sourcesContent":["import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport type { ClassifiedError } from \"@caupulican/pi-agent-core\";\n\nexport interface ProviderFailureCorpusRecord {\n\tts: string;\n\tprovider?: string;\n\tmodelId?: string;\n\treason: ClassifiedError[\"reason\"];\n\tretryable: boolean;\n\tmessage: string;\n}\n\nexport interface ToolValidationFailureShapeEntry {\n\tpath: string;\n\texpectedType: string;\n\treceivedType: string;\n\tkeyword?: string;\n}\n\nexport interface ToolValidationFailureCorpusRecord {\n\tkind: \"tool_validation\";\n\tts: string;\n\tprovider?: string;\n\tmodelId?: string;\n\ttool: string;\n\tfailureModes: string[];\n\tshape: ToolValidationFailureShapeEntry[];\n\terrorKeywords: string[];\n}\n\nexport type FailureCorpusRecord = ProviderFailureCorpusRecord | ToolValidationFailureCorpusRecord;\n\nexport interface FailureCorpusStats {\n\ttotal: number;\n\tunknown: number;\n}\n\nexport interface FailureCorpusFs {\n\texistsSync(path: string): boolean;\n\tmkdirSync(path: string, options: { recursive: true }): void;\n\tappendFileSync(path: string, data: string, encoding: \"utf-8\"): void;\n\treadFileSync(path: string, encoding: \"utf-8\"): string;\n\twriteFileSync(path: string, data: string, encoding: \"utf-8\"): void;\n\tstatSync(path: string): { size: number };\n}\n\nconst DEFAULT_FS: FailureCorpusFs = { existsSync, mkdirSync, appendFileSync, readFileSync, writeFileSync, statSync };\nconst MAX_MESSAGE_CHARS = 500;\nconst MAX_FILE_BYTES = 512 * 1024;\nconst MAX_ROTATED_RECORDS = 1000;\n\nexport class FailureCorpusRecorder {\n\tprivate total = 0;\n\tprivate unknown = 0;\n\tprivate readonly filePath: string;\n\tprivate readonly fs: FailureCorpusFs;\n\tprivate readonly now: () => Date;\n\tprivate readonly debug: (message: string) => void;\n\n\tconstructor(args: { filePath: string; fs?: FailureCorpusFs; now?: () => Date; debug?: (message: string) => void }) {\n\t\tthis.filePath = args.filePath;\n\t\tthis.fs = args.fs ?? DEFAULT_FS;\n\t\tthis.now = args.now ?? (() => new Date());\n\t\tthis.debug = args.debug ?? (() => {});\n\t}\n\n\trecord(args: { provider?: string; modelId?: string; message: string; classified: ClassifiedError }): void {\n\t\tthis.total += 1;\n\t\tif (args.classified.reason === \"unknown\") this.unknown += 1;\n\t\tthis.appendRecord({\n\t\t\tts: this.now().toISOString(),\n\t\t\tprovider: args.provider,\n\t\t\tmodelId: args.modelId,\n\t\t\treason: args.classified.reason,\n\t\t\tretryable: args.classified.retryable,\n\t\t\tmessage: redactSecrets(args.message).slice(0, MAX_MESSAGE_CHARS),\n\t\t});\n\t}\n\n\trecordToolValidation(args: {\n\t\tprovider?: string;\n\t\tmodelId?: string;\n\t\ttool: string;\n\t\tfailureModes: readonly string[];\n\t\tshape: readonly ToolValidationFailureShapeEntry[];\n\t\terrorKeywords?: readonly string[];\n\t}): void {\n\t\tthis.appendRecord({\n\t\t\tkind: \"tool_validation\",\n\t\t\tts: this.now().toISOString(),\n\t\t\tprovider: args.provider,\n\t\t\tmodelId: args.modelId,\n\t\t\ttool: args.tool,\n\t\t\tfailureModes: [...new Set(args.failureModes)].sort(),\n\t\t\tshape: sanitizeToolValidationShape(args.shape),\n\t\t\terrorKeywords: [...new Set(args.errorKeywords ?? [])].sort(),\n\t\t});\n\t}\n\n\tstats(): FailureCorpusStats {\n\t\treturn { total: this.total, unknown: this.unknown };\n\t}\n\n\tprivate appendRecord(record: FailureCorpusRecord): void {\n\t\ttry {\n\t\t\tthis.fs.mkdirSync(dirname(this.filePath), { recursive: true });\n\t\t\tthis.fs.appendFileSync(this.filePath, `${JSON.stringify(record)}\\n`, \"utf-8\");\n\t\t\tthis.rotateIfNeeded();\n\t\t} catch (error) {\n\t\t\tthis.debug(`failure corpus write skipped: ${error instanceof Error ? error.message : String(error)}`);\n\t\t}\n\t}\n\n\tprivate rotateIfNeeded(): void {\n\t\tif (!this.fs.existsSync(this.filePath) || this.fs.statSync(this.filePath).size <= MAX_FILE_BYTES) return;\n\t\tconst lines = this.fs\n\t\t\t.readFileSync(this.filePath, \"utf-8\")\n\t\t\t.split(\"\\n\")\n\t\t\t.filter((line) => line.trim().length > 0)\n\t\t\t.slice(-MAX_ROTATED_RECORDS);\n\t\tthis.fs.writeFileSync(this.filePath, `${lines.join(\"\\n\")}\\n`, \"utf-8\");\n\t}\n}\n\nfunction sanitizeToolValidationShape(\n\tshape: readonly ToolValidationFailureShapeEntry[],\n): ToolValidationFailureShapeEntry[] {\n\treturn shape.map((entry) => ({\n\t\tpath: entry.path,\n\t\texpectedType: entry.expectedType,\n\t\treceivedType: entry.receivedType,\n\t\t...(entry.keyword ? { keyword: entry.keyword } : {}),\n\t}));\n}\n\nexport function redactSecrets(message: string): string {\n\treturn message\n\t\t.replace(/sk-(?:proj-|ant-)?[A-Za-z0-9._-]{8,}/g, \"[redacted]\")\n\t\t.replace(/Bearer\\s+[A-Za-z0-9._~+/=-]+/gi, \"Bearer [redacted]\")\n\t\t.replace(/[A-Za-z0-9+/]{40,}={0,2}/g, \"[redacted]\");\n}\n"]}
1
+ {"version":3,"file":"failure-corpus.d.ts","sourceRoot":"","sources":["../../src/core/failure-corpus.ts"],"names":[],"mappings":"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","sourcesContent":["import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport type { ClassifiedError } from \"@caupulican/pi-agent-core\";\n\nexport interface ProviderFailureCorpusRecord {\n\tts: string;\n\tprovider?: string;\n\tmodelId?: string;\n\treason: ClassifiedError[\"reason\"];\n\tretryable: boolean;\n\tmessage: string;\n}\n\nexport interface ToolValidationFailureShapeEntry {\n\tpath: string;\n\texpectedType: string;\n\treceivedType: string;\n\tkeyword?: string;\n}\n\nexport interface ToolValidationFailureCorpusRecord {\n\tkind: \"tool_validation\";\n\tts: string;\n\tprovider?: string;\n\tmodelId?: string;\n\ttool: string;\n\tfailureModes: string[];\n\tshape: ToolValidationFailureShapeEntry[];\n\terrorKeywords: string[];\n}\n\nexport type FailureCorpusRecord = ProviderFailureCorpusRecord | ToolValidationFailureCorpusRecord;\n\nexport interface FailureCorpusStats {\n\ttotal: number;\n\tunknown: number;\n}\n\nexport interface FailureCorpusFs {\n\texistsSync(path: string): boolean;\n\tmkdirSync(path: string, options: { recursive: true }): void;\n\tappendFileSync(path: string, data: string, encoding: \"utf-8\"): void;\n\treadFileSync(path: string, encoding: \"utf-8\"): string;\n\twriteFileSync(path: string, data: string, encoding: \"utf-8\"): void;\n\tstatSync(path: string): { size: number };\n}\n\nconst DEFAULT_FS: FailureCorpusFs = { existsSync, mkdirSync, appendFileSync, readFileSync, writeFileSync, statSync };\nconst MAX_MESSAGE_CHARS = 500;\nconst MAX_FILE_BYTES = 512 * 1024;\nconst MAX_ROTATED_RECORDS = 1000;\n\nexport function createToolValidationFailureCorpusRecord(args: {\n\tprovider?: string;\n\tmodelId?: string;\n\ttool: string;\n\tfailureModes: readonly string[];\n\tshape: readonly ToolValidationFailureShapeEntry[];\n\terrorKeywords?: readonly string[];\n\tts: string;\n}): ToolValidationFailureCorpusRecord {\n\treturn {\n\t\tkind: \"tool_validation\",\n\t\tts: args.ts,\n\t\tprovider: args.provider,\n\t\tmodelId: args.modelId,\n\t\ttool: args.tool,\n\t\tfailureModes: [...new Set(args.failureModes)].sort(),\n\t\tshape: sanitizeToolValidationShape(args.shape),\n\t\terrorKeywords: [...new Set(args.errorKeywords ?? [])].sort(),\n\t};\n}\n\nexport function writeFailureCorpusRecord(\n\tfilePath: string,\n\trecord: FailureCorpusRecord,\n\tfs: FailureCorpusFs = DEFAULT_FS,\n): void {\n\tfs.mkdirSync(dirname(filePath), { recursive: true });\n\tfs.appendFileSync(filePath, `${JSON.stringify(record)}\\n`, \"utf-8\");\n\trotateFailureCorpusIfNeeded(filePath, fs);\n}\n\nfunction rotateFailureCorpusIfNeeded(filePath: string, fs: FailureCorpusFs): void {\n\tif (!fs.existsSync(filePath) || fs.statSync(filePath).size <= MAX_FILE_BYTES) return;\n\tconst lines = fs\n\t\t.readFileSync(filePath, \"utf-8\")\n\t\t.split(\"\\n\")\n\t\t.filter((line) => line.trim().length > 0)\n\t\t.slice(-MAX_ROTATED_RECORDS);\n\tfs.writeFileSync(filePath, `${lines.join(\"\\n\")}\\n`, \"utf-8\");\n}\n\nexport class FailureCorpusRecorder {\n\tprivate total = 0;\n\tprivate unknown = 0;\n\tprivate readonly filePath: string;\n\tprivate readonly fs: FailureCorpusFs;\n\tprivate readonly now: () => Date;\n\tprivate readonly debug: (message: string) => void;\n\n\tconstructor(args: { filePath: string; fs?: FailureCorpusFs; now?: () => Date; debug?: (message: string) => void }) {\n\t\tthis.filePath = args.filePath;\n\t\tthis.fs = args.fs ?? DEFAULT_FS;\n\t\tthis.now = args.now ?? (() => new Date());\n\t\tthis.debug = args.debug ?? (() => {});\n\t}\n\n\trecord(args: { provider?: string; modelId?: string; message: string; classified: ClassifiedError }): void {\n\t\tthis.total += 1;\n\t\tif (args.classified.reason === \"unknown\") this.unknown += 1;\n\t\tthis.appendRecord({\n\t\t\tts: this.now().toISOString(),\n\t\t\tprovider: args.provider,\n\t\t\tmodelId: args.modelId,\n\t\t\treason: args.classified.reason,\n\t\t\tretryable: args.classified.retryable,\n\t\t\tmessage: redactSecrets(args.message).slice(0, MAX_MESSAGE_CHARS),\n\t\t});\n\t}\n\n\trecordToolValidation(args: {\n\t\tprovider?: string;\n\t\tmodelId?: string;\n\t\ttool: string;\n\t\tfailureModes: readonly string[];\n\t\tshape: readonly ToolValidationFailureShapeEntry[];\n\t\terrorKeywords?: readonly string[];\n\t}): void {\n\t\tthis.appendRecord(createToolValidationFailureCorpusRecord({ ...args, ts: this.now().toISOString() }));\n\t}\n\n\tstats(): FailureCorpusStats {\n\t\treturn { total: this.total, unknown: this.unknown };\n\t}\n\n\tprivate appendRecord(record: FailureCorpusRecord): void {\n\t\ttry {\n\t\t\twriteFailureCorpusRecord(this.filePath, record, this.fs);\n\t\t} catch (error) {\n\t\t\tthis.debug(`failure corpus write skipped: ${error instanceof Error ? error.message : String(error)}`);\n\t\t}\n\t}\n}\n\nfunction sanitizeToolValidationShape(\n\tshape: readonly ToolValidationFailureShapeEntry[],\n): ToolValidationFailureShapeEntry[] {\n\treturn shape.map((entry) => ({\n\t\tpath: entry.path,\n\t\texpectedType: entry.expectedType,\n\t\treceivedType: entry.receivedType,\n\t\t...(entry.keyword ? { keyword: entry.keyword } : {}),\n\t}));\n}\n\nexport function redactSecrets(message: string): string {\n\treturn message\n\t\t.replace(/sk-(?:proj-|ant-)?[A-Za-z0-9._-]{8,}/g, \"[REDACTED]\")\n\t\t.replace(/Bearer\\s+[A-Za-z0-9._~+/=-]+/gi, \"Bearer [REDACTED]\")\n\t\t.replace(/[A-Za-z0-9+/]{40,}={0,2}/g, \"[REDACTED]\");\n}\n"]}
@@ -4,6 +4,33 @@ const DEFAULT_FS = { existsSync, mkdirSync, appendFileSync, readFileSync, writeF
4
4
  const MAX_MESSAGE_CHARS = 500;
5
5
  const MAX_FILE_BYTES = 512 * 1024;
6
6
  const MAX_ROTATED_RECORDS = 1000;
7
+ export function createToolValidationFailureCorpusRecord(args) {
8
+ return {
9
+ kind: "tool_validation",
10
+ ts: args.ts,
11
+ provider: args.provider,
12
+ modelId: args.modelId,
13
+ tool: args.tool,
14
+ failureModes: [...new Set(args.failureModes)].sort(),
15
+ shape: sanitizeToolValidationShape(args.shape),
16
+ errorKeywords: [...new Set(args.errorKeywords ?? [])].sort(),
17
+ };
18
+ }
19
+ export function writeFailureCorpusRecord(filePath, record, fs = DEFAULT_FS) {
20
+ fs.mkdirSync(dirname(filePath), { recursive: true });
21
+ fs.appendFileSync(filePath, `${JSON.stringify(record)}\n`, "utf-8");
22
+ rotateFailureCorpusIfNeeded(filePath, fs);
23
+ }
24
+ function rotateFailureCorpusIfNeeded(filePath, fs) {
25
+ if (!fs.existsSync(filePath) || fs.statSync(filePath).size <= MAX_FILE_BYTES)
26
+ return;
27
+ const lines = fs
28
+ .readFileSync(filePath, "utf-8")
29
+ .split("\n")
30
+ .filter((line) => line.trim().length > 0)
31
+ .slice(-MAX_ROTATED_RECORDS);
32
+ fs.writeFileSync(filePath, `${lines.join("\n")}\n`, "utf-8");
33
+ }
7
34
  export class FailureCorpusRecorder {
8
35
  total = 0;
9
36
  unknown = 0;
@@ -31,40 +58,19 @@ export class FailureCorpusRecorder {
31
58
  });
32
59
  }
33
60
  recordToolValidation(args) {
34
- this.appendRecord({
35
- kind: "tool_validation",
36
- ts: this.now().toISOString(),
37
- provider: args.provider,
38
- modelId: args.modelId,
39
- tool: args.tool,
40
- failureModes: [...new Set(args.failureModes)].sort(),
41
- shape: sanitizeToolValidationShape(args.shape),
42
- errorKeywords: [...new Set(args.errorKeywords ?? [])].sort(),
43
- });
61
+ this.appendRecord(createToolValidationFailureCorpusRecord({ ...args, ts: this.now().toISOString() }));
44
62
  }
45
63
  stats() {
46
64
  return { total: this.total, unknown: this.unknown };
47
65
  }
48
66
  appendRecord(record) {
49
67
  try {
50
- this.fs.mkdirSync(dirname(this.filePath), { recursive: true });
51
- this.fs.appendFileSync(this.filePath, `${JSON.stringify(record)}\n`, "utf-8");
52
- this.rotateIfNeeded();
68
+ writeFailureCorpusRecord(this.filePath, record, this.fs);
53
69
  }
54
70
  catch (error) {
55
71
  this.debug(`failure corpus write skipped: ${error instanceof Error ? error.message : String(error)}`);
56
72
  }
57
73
  }
58
- rotateIfNeeded() {
59
- if (!this.fs.existsSync(this.filePath) || this.fs.statSync(this.filePath).size <= MAX_FILE_BYTES)
60
- return;
61
- const lines = this.fs
62
- .readFileSync(this.filePath, "utf-8")
63
- .split("\n")
64
- .filter((line) => line.trim().length > 0)
65
- .slice(-MAX_ROTATED_RECORDS);
66
- this.fs.writeFileSync(this.filePath, `${lines.join("\n")}\n`, "utf-8");
67
- }
68
74
  }
69
75
  function sanitizeToolValidationShape(shape) {
70
76
  return shape.map((entry) => ({
@@ -76,8 +82,8 @@ function sanitizeToolValidationShape(shape) {
76
82
  }
77
83
  export function redactSecrets(message) {
78
84
  return message
79
- .replace(/sk-(?:proj-|ant-)?[A-Za-z0-9._-]{8,}/g, "[redacted]")
80
- .replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [redacted]")
81
- .replace(/[A-Za-z0-9+/]{40,}={0,2}/g, "[redacted]");
85
+ .replace(/sk-(?:proj-|ant-)?[A-Za-z0-9._-]{8,}/g, "[REDACTED]")
86
+ .replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [REDACTED]")
87
+ .replace(/[A-Za-z0-9+/]{40,}={0,2}/g, "[REDACTED]");
82
88
  }
83
89
  //# sourceMappingURL=failure-corpus.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"failure-corpus.js","sourceRoot":"","sources":["../../src/core/failure-corpus.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACvG,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA8CpC,MAAM,UAAU,GAAoB,EAAE,UAAU,EAAE,SAAS,EAAE,cAAc,EAAE,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC;AACrH,MAAM,iBAAiB,GAAG,GAAG,CAAC;AAC9B,MAAM,cAAc,GAAG,GAAG,GAAG,IAAI,CAAC;AAClC,MAAM,mBAAmB,GAAG,IAAI,CAAC;AAEjC,MAAM,OAAO,qBAAqB;IACzB,KAAK,GAAG,CAAC,CAAC;IACV,OAAO,GAAG,CAAC,CAAC;IACH,QAAQ,CAAS;IACjB,EAAE,CAAkB;IACpB,GAAG,CAAa;IAChB,KAAK,CAA4B;IAElD,YAAY,IAAqG,EAAE;QAClH,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC9B,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC,EAAE,IAAI,UAAU,CAAC;QAChC,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;QAC1C,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,CAAC,GAAG,EAAE,CAAC,EAAC,CAAC,CAAC,CAAC;IAAA,CACtC;IAED,MAAM,CAAC,IAA2F,EAAQ;QACzG,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC;QAChB,IAAI,IAAI,CAAC,UAAU,CAAC,MAAM,KAAK,SAAS;YAAE,IAAI,CAAC,OAAO,IAAI,CAAC,CAAC;QAC5D,IAAI,CAAC,YAAY,CAAC;YACjB,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;YAC5B,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,MAAM,EAAE,IAAI,CAAC,UAAU,CAAC,MAAM;YAC9B,SAAS,EAAE,IAAI,CAAC,UAAU,CAAC,SAAS;YACpC,OAAO,EAAE,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,iBAAiB,CAAC;SAChE,CAAC,CAAC;IAAA,CACH;IAED,oBAAoB,CAAC,IAOpB,EAAQ;QACR,IAAI,CAAC,YAAY,CAAC;YACjB,IAAI,EAAE,iBAAiB;YACvB,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;YAC5B,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,YAAY,EAAE,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,EAAE;YACpD,KAAK,EAAE,2BAA2B,CAAC,IAAI,CAAC,KAAK,CAAC;YAC9C,aAAa,EAAE,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE;SAC5D,CAAC,CAAC;IAAA,CACH;IAED,KAAK,GAAuB;QAC3B,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC;IAAA,CACpD;IAEO,YAAY,CAAC,MAA2B,EAAQ;QACvD,IAAI,CAAC;YACJ,IAAI,CAAC,EAAE,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAC/D,IAAI,CAAC,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;YAC9E,IAAI,CAAC,cAAc,EAAE,CAAC;QACvB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,IAAI,CAAC,KAAK,CAAC,iCAAiC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACvG,CAAC;IAAA,CACD;IAEO,cAAc,GAAS;QAC9B,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,IAAI,cAAc;YAAE,OAAO;QACzG,MAAM,KAAK,GAAG,IAAI,CAAC,EAAE;aACnB,YAAY,CAAC,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC;aACpC,KAAK,CAAC,IAAI,CAAC;aACX,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;aACxC,KAAK,CAAC,CAAC,mBAAmB,CAAC,CAAC;QAC9B,IAAI,CAAC,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAAA,CACvE;CACD;AAED,SAAS,2BAA2B,CACnC,KAAiD,EACb;IACpC,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QAC5B,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,YAAY,EAAE,KAAK,CAAC,YAAY;QAChC,YAAY,EAAE,KAAK,CAAC,YAAY;QAChC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACpD,CAAC,CAAC,CAAC;AAAA,CACJ;AAED,MAAM,UAAU,aAAa,CAAC,OAAe,EAAU;IACtD,OAAO,OAAO;SACZ,OAAO,CAAC,uCAAuC,EAAE,YAAY,CAAC;SAC9D,OAAO,CAAC,gCAAgC,EAAE,mBAAmB,CAAC;SAC9D,OAAO,CAAC,2BAA2B,EAAE,YAAY,CAAC,CAAC;AAAA,CACrD","sourcesContent":["import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport type { ClassifiedError } from \"@caupulican/pi-agent-core\";\n\nexport interface ProviderFailureCorpusRecord {\n\tts: string;\n\tprovider?: string;\n\tmodelId?: string;\n\treason: ClassifiedError[\"reason\"];\n\tretryable: boolean;\n\tmessage: string;\n}\n\nexport interface ToolValidationFailureShapeEntry {\n\tpath: string;\n\texpectedType: string;\n\treceivedType: string;\n\tkeyword?: string;\n}\n\nexport interface ToolValidationFailureCorpusRecord {\n\tkind: \"tool_validation\";\n\tts: string;\n\tprovider?: string;\n\tmodelId?: string;\n\ttool: string;\n\tfailureModes: string[];\n\tshape: ToolValidationFailureShapeEntry[];\n\terrorKeywords: string[];\n}\n\nexport type FailureCorpusRecord = ProviderFailureCorpusRecord | ToolValidationFailureCorpusRecord;\n\nexport interface FailureCorpusStats {\n\ttotal: number;\n\tunknown: number;\n}\n\nexport interface FailureCorpusFs {\n\texistsSync(path: string): boolean;\n\tmkdirSync(path: string, options: { recursive: true }): void;\n\tappendFileSync(path: string, data: string, encoding: \"utf-8\"): void;\n\treadFileSync(path: string, encoding: \"utf-8\"): string;\n\twriteFileSync(path: string, data: string, encoding: \"utf-8\"): void;\n\tstatSync(path: string): { size: number };\n}\n\nconst DEFAULT_FS: FailureCorpusFs = { existsSync, mkdirSync, appendFileSync, readFileSync, writeFileSync, statSync };\nconst MAX_MESSAGE_CHARS = 500;\nconst MAX_FILE_BYTES = 512 * 1024;\nconst MAX_ROTATED_RECORDS = 1000;\n\nexport class FailureCorpusRecorder {\n\tprivate total = 0;\n\tprivate unknown = 0;\n\tprivate readonly filePath: string;\n\tprivate readonly fs: FailureCorpusFs;\n\tprivate readonly now: () => Date;\n\tprivate readonly debug: (message: string) => void;\n\n\tconstructor(args: { filePath: string; fs?: FailureCorpusFs; now?: () => Date; debug?: (message: string) => void }) {\n\t\tthis.filePath = args.filePath;\n\t\tthis.fs = args.fs ?? DEFAULT_FS;\n\t\tthis.now = args.now ?? (() => new Date());\n\t\tthis.debug = args.debug ?? (() => {});\n\t}\n\n\trecord(args: { provider?: string; modelId?: string; message: string; classified: ClassifiedError }): void {\n\t\tthis.total += 1;\n\t\tif (args.classified.reason === \"unknown\") this.unknown += 1;\n\t\tthis.appendRecord({\n\t\t\tts: this.now().toISOString(),\n\t\t\tprovider: args.provider,\n\t\t\tmodelId: args.modelId,\n\t\t\treason: args.classified.reason,\n\t\t\tretryable: args.classified.retryable,\n\t\t\tmessage: redactSecrets(args.message).slice(0, MAX_MESSAGE_CHARS),\n\t\t});\n\t}\n\n\trecordToolValidation(args: {\n\t\tprovider?: string;\n\t\tmodelId?: string;\n\t\ttool: string;\n\t\tfailureModes: readonly string[];\n\t\tshape: readonly ToolValidationFailureShapeEntry[];\n\t\terrorKeywords?: readonly string[];\n\t}): void {\n\t\tthis.appendRecord({\n\t\t\tkind: \"tool_validation\",\n\t\t\tts: this.now().toISOString(),\n\t\t\tprovider: args.provider,\n\t\t\tmodelId: args.modelId,\n\t\t\ttool: args.tool,\n\t\t\tfailureModes: [...new Set(args.failureModes)].sort(),\n\t\t\tshape: sanitizeToolValidationShape(args.shape),\n\t\t\terrorKeywords: [...new Set(args.errorKeywords ?? [])].sort(),\n\t\t});\n\t}\n\n\tstats(): FailureCorpusStats {\n\t\treturn { total: this.total, unknown: this.unknown };\n\t}\n\n\tprivate appendRecord(record: FailureCorpusRecord): void {\n\t\ttry {\n\t\t\tthis.fs.mkdirSync(dirname(this.filePath), { recursive: true });\n\t\t\tthis.fs.appendFileSync(this.filePath, `${JSON.stringify(record)}\\n`, \"utf-8\");\n\t\t\tthis.rotateIfNeeded();\n\t\t} catch (error) {\n\t\t\tthis.debug(`failure corpus write skipped: ${error instanceof Error ? error.message : String(error)}`);\n\t\t}\n\t}\n\n\tprivate rotateIfNeeded(): void {\n\t\tif (!this.fs.existsSync(this.filePath) || this.fs.statSync(this.filePath).size <= MAX_FILE_BYTES) return;\n\t\tconst lines = this.fs\n\t\t\t.readFileSync(this.filePath, \"utf-8\")\n\t\t\t.split(\"\\n\")\n\t\t\t.filter((line) => line.trim().length > 0)\n\t\t\t.slice(-MAX_ROTATED_RECORDS);\n\t\tthis.fs.writeFileSync(this.filePath, `${lines.join(\"\\n\")}\\n`, \"utf-8\");\n\t}\n}\n\nfunction sanitizeToolValidationShape(\n\tshape: readonly ToolValidationFailureShapeEntry[],\n): ToolValidationFailureShapeEntry[] {\n\treturn shape.map((entry) => ({\n\t\tpath: entry.path,\n\t\texpectedType: entry.expectedType,\n\t\treceivedType: entry.receivedType,\n\t\t...(entry.keyword ? { keyword: entry.keyword } : {}),\n\t}));\n}\n\nexport function redactSecrets(message: string): string {\n\treturn message\n\t\t.replace(/sk-(?:proj-|ant-)?[A-Za-z0-9._-]{8,}/g, \"[redacted]\")\n\t\t.replace(/Bearer\\s+[A-Za-z0-9._~+/=-]+/gi, \"Bearer [redacted]\")\n\t\t.replace(/[A-Za-z0-9+/]{40,}={0,2}/g, \"[redacted]\");\n}\n"]}
1
+ {"version":3,"file":"failure-corpus.js","sourceRoot":"","sources":["../../src/core/failure-corpus.ts"],"names":[],"mappings":"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","sourcesContent":["import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport type { ClassifiedError } from \"@caupulican/pi-agent-core\";\n\nexport interface ProviderFailureCorpusRecord {\n\tts: string;\n\tprovider?: string;\n\tmodelId?: string;\n\treason: ClassifiedError[\"reason\"];\n\tretryable: boolean;\n\tmessage: string;\n}\n\nexport interface ToolValidationFailureShapeEntry {\n\tpath: string;\n\texpectedType: string;\n\treceivedType: string;\n\tkeyword?: string;\n}\n\nexport interface ToolValidationFailureCorpusRecord {\n\tkind: \"tool_validation\";\n\tts: string;\n\tprovider?: string;\n\tmodelId?: string;\n\ttool: string;\n\tfailureModes: string[];\n\tshape: ToolValidationFailureShapeEntry[];\n\terrorKeywords: string[];\n}\n\nexport type FailureCorpusRecord = ProviderFailureCorpusRecord | ToolValidationFailureCorpusRecord;\n\nexport interface FailureCorpusStats {\n\ttotal: number;\n\tunknown: number;\n}\n\nexport interface FailureCorpusFs {\n\texistsSync(path: string): boolean;\n\tmkdirSync(path: string, options: { recursive: true }): void;\n\tappendFileSync(path: string, data: string, encoding: \"utf-8\"): void;\n\treadFileSync(path: string, encoding: \"utf-8\"): string;\n\twriteFileSync(path: string, data: string, encoding: \"utf-8\"): void;\n\tstatSync(path: string): { size: number };\n}\n\nconst DEFAULT_FS: FailureCorpusFs = { existsSync, mkdirSync, appendFileSync, readFileSync, writeFileSync, statSync };\nconst MAX_MESSAGE_CHARS = 500;\nconst MAX_FILE_BYTES = 512 * 1024;\nconst MAX_ROTATED_RECORDS = 1000;\n\nexport function createToolValidationFailureCorpusRecord(args: {\n\tprovider?: string;\n\tmodelId?: string;\n\ttool: string;\n\tfailureModes: readonly string[];\n\tshape: readonly ToolValidationFailureShapeEntry[];\n\terrorKeywords?: readonly string[];\n\tts: string;\n}): ToolValidationFailureCorpusRecord {\n\treturn {\n\t\tkind: \"tool_validation\",\n\t\tts: args.ts,\n\t\tprovider: args.provider,\n\t\tmodelId: args.modelId,\n\t\ttool: args.tool,\n\t\tfailureModes: [...new Set(args.failureModes)].sort(),\n\t\tshape: sanitizeToolValidationShape(args.shape),\n\t\terrorKeywords: [...new Set(args.errorKeywords ?? [])].sort(),\n\t};\n}\n\nexport function writeFailureCorpusRecord(\n\tfilePath: string,\n\trecord: FailureCorpusRecord,\n\tfs: FailureCorpusFs = DEFAULT_FS,\n): void {\n\tfs.mkdirSync(dirname(filePath), { recursive: true });\n\tfs.appendFileSync(filePath, `${JSON.stringify(record)}\\n`, \"utf-8\");\n\trotateFailureCorpusIfNeeded(filePath, fs);\n}\n\nfunction rotateFailureCorpusIfNeeded(filePath: string, fs: FailureCorpusFs): void {\n\tif (!fs.existsSync(filePath) || fs.statSync(filePath).size <= MAX_FILE_BYTES) return;\n\tconst lines = fs\n\t\t.readFileSync(filePath, \"utf-8\")\n\t\t.split(\"\\n\")\n\t\t.filter((line) => line.trim().length > 0)\n\t\t.slice(-MAX_ROTATED_RECORDS);\n\tfs.writeFileSync(filePath, `${lines.join(\"\\n\")}\\n`, \"utf-8\");\n}\n\nexport class FailureCorpusRecorder {\n\tprivate total = 0;\n\tprivate unknown = 0;\n\tprivate readonly filePath: string;\n\tprivate readonly fs: FailureCorpusFs;\n\tprivate readonly now: () => Date;\n\tprivate readonly debug: (message: string) => void;\n\n\tconstructor(args: { filePath: string; fs?: FailureCorpusFs; now?: () => Date; debug?: (message: string) => void }) {\n\t\tthis.filePath = args.filePath;\n\t\tthis.fs = args.fs ?? DEFAULT_FS;\n\t\tthis.now = args.now ?? (() => new Date());\n\t\tthis.debug = args.debug ?? (() => {});\n\t}\n\n\trecord(args: { provider?: string; modelId?: string; message: string; classified: ClassifiedError }): void {\n\t\tthis.total += 1;\n\t\tif (args.classified.reason === \"unknown\") this.unknown += 1;\n\t\tthis.appendRecord({\n\t\t\tts: this.now().toISOString(),\n\t\t\tprovider: args.provider,\n\t\t\tmodelId: args.modelId,\n\t\t\treason: args.classified.reason,\n\t\t\tretryable: args.classified.retryable,\n\t\t\tmessage: redactSecrets(args.message).slice(0, MAX_MESSAGE_CHARS),\n\t\t});\n\t}\n\n\trecordToolValidation(args: {\n\t\tprovider?: string;\n\t\tmodelId?: string;\n\t\ttool: string;\n\t\tfailureModes: readonly string[];\n\t\tshape: readonly ToolValidationFailureShapeEntry[];\n\t\terrorKeywords?: readonly string[];\n\t}): void {\n\t\tthis.appendRecord(createToolValidationFailureCorpusRecord({ ...args, ts: this.now().toISOString() }));\n\t}\n\n\tstats(): FailureCorpusStats {\n\t\treturn { total: this.total, unknown: this.unknown };\n\t}\n\n\tprivate appendRecord(record: FailureCorpusRecord): void {\n\t\ttry {\n\t\t\twriteFailureCorpusRecord(this.filePath, record, this.fs);\n\t\t} catch (error) {\n\t\t\tthis.debug(`failure corpus write skipped: ${error instanceof Error ? error.message : String(error)}`);\n\t\t}\n\t}\n}\n\nfunction sanitizeToolValidationShape(\n\tshape: readonly ToolValidationFailureShapeEntry[],\n): ToolValidationFailureShapeEntry[] {\n\treturn shape.map((entry) => ({\n\t\tpath: entry.path,\n\t\texpectedType: entry.expectedType,\n\t\treceivedType: entry.receivedType,\n\t\t...(entry.keyword ? { keyword: entry.keyword } : {}),\n\t}));\n}\n\nexport function redactSecrets(message: string): string {\n\treturn message\n\t\t.replace(/sk-(?:proj-|ant-)?[A-Za-z0-9._-]{8,}/g, \"[REDACTED]\")\n\t\t.replace(/Bearer\\s+[A-Za-z0-9._~+/=-]+/gi, \"Bearer [REDACTED]\")\n\t\t.replace(/[A-Za-z0-9+/]{40,}={0,2}/g, \"[REDACTED]\");\n}\n"]}
@@ -11,9 +11,11 @@ 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";
18
+ import { type ToolArgumentValidationLogRecord } from "./tool-recovery-log-records.ts";
17
19
  export declare const TOOL_ARGUMENT_VALIDATION_CUSTOM_TYPE = "tool_argument_validation";
18
20
  export interface ToolArgumentValidationTeachEfficacy {
19
21
  recurrenceBefore: number;
@@ -48,16 +50,21 @@ export interface SessionAnalyticsDeps {
48
50
  getSettingsManager(): SettingsManager;
49
51
  /** Resolve a tool definition for the HTML export's custom-tool renderer. */
50
52
  getToolDefinition(name: string): ToolDefinition | undefined;
53
+ /** Sidecar recovery telemetry log; read on demand so turn handling never writes session custom entries. */
54
+ getToolRecoveryEventLogPath(): string;
51
55
  }
52
56
  export declare class SessionAnalytics {
53
57
  /** Memoized spawned-usage totals, keyed by session entry count (Bug #22: O(1) between turns). */
54
58
  private _spawnedUsageCache?;
55
- /** 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. */
56
60
  private _dailyUsageCache?;
61
+ /** Live recovery telemetry records already snapshotted for the worker; keyed to dedupe sidecar reads. */
62
+ private readonly _toolArgumentValidationRecords;
57
63
  private readonly deps;
58
64
  constructor(deps: SessionAnalyticsDeps);
59
65
  getSessionStats(): SessionStats;
60
- recordToolArgumentValidation(event: ToolArgumentValidationTelemetryEvent): void;
66
+ recordToolArgumentValidation(record: ToolArgumentValidationLogRecord): void;
67
+ private readToolArgumentValidationSidecarRecords;
61
68
  getToolArgumentValidationStats(): ToolArgumentValidationStats;
62
69
  /**
63
70
  * Cumulative usage (full breakdown) for this session's entire spawn subtree: its own
@@ -93,6 +100,7 @@ export declare class SessionAnalytics {
93
100
  * instead of an O(N) scan on every keystroke (Bug #22). Recomputes only when entries change.
94
101
  */
95
102
  getSpawnedUsage(): SpawnedUsageTotals;
103
+ getCostSummary(now?: Date): SessionCostSummary;
96
104
  getDailyUsageTotals(now?: Date): DailyUsageTotals;
97
105
  getDailyUsageBreakdown(formatLabel?: (label: string) => string, now?: Date): string;
98
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, 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\";\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}\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\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(event: ToolArgumentValidationTelemetryEvent): void {\n\t\ttry {\n\t\t\tconst record: ToolArgumentValidationRecord = { version: 1, ...event };\n\t\t\tthis.deps.getSessionManager().appendCustomEntry(TOOL_ARGUMENT_VALIDATION_CUSTOM_TYPE, record);\n\t\t} catch {\n\t\t\t// Telemetry is best-effort and must never affect the observed turn.\n\t\t}\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\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\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\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"]}