@caupulican/pi-adaptative 0.81.9 → 0.81.13

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 (95) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/bundled-resources/skills/tool-call-repair/SKILL.md +152 -0
  3. package/dist/bundled-resources/skills/tool-call-repair/references/failure-grammar.md +128 -0
  4. package/dist/bundled-resources/skills/tool-call-repair/references/repair-catalogue.md +201 -0
  5. package/dist/bundled-resources/skills/tool-call-repair/references/text-protocol-grammar.md +141 -0
  6. package/dist/cli/list-models.d.ts.map +1 -1
  7. package/dist/cli/list-models.js +5 -0
  8. package/dist/cli/list-models.js.map +1 -1
  9. package/dist/core/agent-session.d.ts +46 -1
  10. package/dist/core/agent-session.d.ts.map +1 -1
  11. package/dist/core/agent-session.js +394 -1
  12. package/dist/core/agent-session.js.map +1 -1
  13. package/dist/core/delegation/worker-actions.d.ts +2 -1
  14. package/dist/core/delegation/worker-actions.d.ts.map +1 -1
  15. package/dist/core/delegation/worker-actions.js +34 -2
  16. package/dist/core/delegation/worker-actions.js.map +1 -1
  17. package/dist/core/extensions/types.d.ts +3 -1
  18. package/dist/core/extensions/types.d.ts.map +1 -1
  19. package/dist/core/extensions/types.js.map +1 -1
  20. package/dist/core/failure-corpus.d.ts +27 -1
  21. package/dist/core/failure-corpus.d.ts.map +1 -1
  22. package/dist/core/failure-corpus.js +33 -11
  23. package/dist/core/failure-corpus.js.map +1 -1
  24. package/dist/core/model-registry.d.ts +3 -2
  25. package/dist/core/model-registry.d.ts.map +1 -1
  26. package/dist/core/model-registry.js +12 -4
  27. package/dist/core/model-registry.js.map +1 -1
  28. package/dist/core/models/adaptation-store.d.ts +79 -0
  29. package/dist/core/models/adaptation-store.d.ts.map +1 -0
  30. package/dist/core/models/adaptation-store.js +196 -0
  31. package/dist/core/models/adaptation-store.js.map +1 -0
  32. package/dist/core/session-analytics.d.ts +24 -1
  33. package/dist/core/session-analytics.d.ts.map +1 -1
  34. package/dist/core/session-analytics.js +75 -0
  35. package/dist/core/session-analytics.js.map +1 -1
  36. package/dist/core/settings-manager.d.ts +6 -0
  37. package/dist/core/settings-manager.d.ts.map +1 -1
  38. package/dist/core/settings-manager.js.map +1 -1
  39. package/dist/core/slash-commands.d.ts.map +1 -1
  40. package/dist/core/slash-commands.js +10 -0
  41. package/dist/core/slash-commands.js.map +1 -1
  42. package/dist/core/system-prompt-builder.d.ts +4 -0
  43. package/dist/core/system-prompt-builder.d.ts.map +1 -1
  44. package/dist/core/system-prompt-builder.js +8 -0
  45. package/dist/core/system-prompt-builder.js.map +1 -1
  46. package/dist/core/tool-repair-health.d.ts +3 -0
  47. package/dist/core/tool-repair-health.d.ts.map +1 -0
  48. package/dist/core/tool-repair-health.js +63 -0
  49. package/dist/core/tool-repair-health.js.map +1 -0
  50. package/dist/core/tool-repair-settings.d.ts +11 -0
  51. package/dist/core/tool-repair-settings.d.ts.map +1 -0
  52. package/dist/core/tool-repair-settings.js +18 -0
  53. package/dist/core/tool-repair-settings.js.map +1 -0
  54. package/dist/core/tools/edit.d.ts.map +1 -1
  55. package/dist/core/tools/edit.js +0 -9
  56. package/dist/core/tools/edit.js.map +1 -1
  57. package/dist/core/tools/find.d.ts.map +1 -1
  58. package/dist/core/tools/find.js +10 -7
  59. package/dist/core/tools/find.js.map +1 -1
  60. package/dist/core/tools/grep.d.ts.map +1 -1
  61. package/dist/core/tools/grep.js +15 -9
  62. package/dist/core/tools/grep.js.map +1 -1
  63. package/dist/modes/interactive/components/tool-execution.d.ts +7 -3
  64. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  65. package/dist/modes/interactive/components/tool-execution.js +21 -4
  66. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  67. package/dist/modes/interactive/external-editor.d.ts.map +1 -1
  68. package/dist/modes/interactive/external-editor.js +5 -3
  69. package/dist/modes/interactive/external-editor.js.map +1 -1
  70. package/dist/modes/interactive/interactive-mode.d.ts +1 -0
  71. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  72. package/dist/modes/interactive/interactive-mode.js +61 -7
  73. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  74. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  75. package/dist/modes/rpc/rpc-mode.js +16 -0
  76. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  77. package/dist/modes/rpc/rpc-types.d.ts +47 -1
  78. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  79. package/dist/modes/rpc/rpc-types.js.map +1 -1
  80. package/docs/index.md +1 -0
  81. package/docs/models.md +5 -1
  82. package/docs/rpc.md +75 -0
  83. package/docs/settings.md +22 -0
  84. package/docs/tool-repair.md +83 -0
  85. package/docs/usage.md +3 -0
  86. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  87. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  88. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  89. package/examples/extensions/sandbox/package-lock.json +2 -2
  90. package/examples/extensions/sandbox/package.json +1 -1
  91. package/examples/extensions/with-deps/package-lock.json +2 -2
  92. package/examples/extensions/with-deps/package.json +1 -1
  93. package/npm-shrinkwrap.json +12 -12
  94. package/package.json +6 -4
  95. package/docs/bug-ledger.md +0 -134
@@ -0,0 +1,196 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { currentHostFingerprint } from "./fitness-store.js";
4
+ const STORE_VERSION = 1;
5
+ const MAX_RULES_PER_MODEL = 5;
6
+ const RETIRE_AFTER_MS = 30 * 24 * 60 * 60 * 1000;
7
+ function emptyProfile() {
8
+ return { rules: [], teachStats: {} };
9
+ }
10
+ function normalizeProfile(profile) {
11
+ return {
12
+ rules: Array.isArray(profile?.rules) ? profile.rules.filter(isRule) : [],
13
+ ...(isProtocol(profile?.protocol) && { protocol: profile.protocol }),
14
+ ...(isToolProbe(profile?.toolProbe) && { toolProbe: profile.toolProbe }),
15
+ teachStats: isRecord(profile?.teachStats) ? filterTeachStats(profile.teachStats) : {},
16
+ };
17
+ }
18
+ function isRecord(value) {
19
+ return typeof value === "object" && value !== null && !Array.isArray(value);
20
+ }
21
+ function isRule(value) {
22
+ return (isRecord(value) &&
23
+ typeof value.mode === "string" &&
24
+ typeof value.text === "string" &&
25
+ typeof value.addedAt === "string" &&
26
+ typeof value.lastFiredAt === "string");
27
+ }
28
+ function isProtocol(value) {
29
+ if (!isRecord(value) || typeof value.version !== "number")
30
+ return false;
31
+ if (value.status === "failed") {
32
+ return (typeof value.attemptedAt === "string" &&
33
+ Array.isArray(value.variantsTried) &&
34
+ value.variantsTried.every((variant) => typeof variant === "string"));
35
+ }
36
+ return ((value.status === undefined || value.status === "calibrated") &&
37
+ typeof value.variant === "string" &&
38
+ typeof value.calibratedAt === "string");
39
+ }
40
+ function isToolProbe(value) {
41
+ return (isRecord(value) &&
42
+ typeof value.version === "number" &&
43
+ (value.status === "native" || value.status === "text-protocol" || value.status === "none") &&
44
+ typeof value.probedAt === "string" &&
45
+ (value.variant === undefined || typeof value.variant === "string") &&
46
+ (value.diagnostic === undefined || typeof value.diagnostic === "string"));
47
+ }
48
+ function isTeachStats(value) {
49
+ return (isRecord(value) &&
50
+ typeof value.taught === "number" &&
51
+ typeof value.recurrenceBefore === "number" &&
52
+ typeof value.recurrenceAfter === "number");
53
+ }
54
+ function filterTeachStats(value) {
55
+ return Object.fromEntries(Object.entries(value).filter((entry) => isTeachStats(entry[1])));
56
+ }
57
+ function ruleRecency(rule) {
58
+ const lastFired = Date.parse(rule.lastFiredAt);
59
+ if (Number.isFinite(lastFired))
60
+ return lastFired;
61
+ const added = Date.parse(rule.addedAt);
62
+ return Number.isFinite(added) ? added : 0;
63
+ }
64
+ function pruneRetiredRules(rules, now) {
65
+ const cutoff = now.getTime() - RETIRE_AFTER_MS;
66
+ return rules.filter((rule) => ruleRecency(rule) >= cutoff);
67
+ }
68
+ function enforceRuleCap(rules) {
69
+ if (rules.length <= MAX_RULES_PER_MODEL)
70
+ return [...rules];
71
+ return [...rules].sort((a, b) => ruleRecency(b) - ruleRecency(a)).slice(0, MAX_RULES_PER_MODEL);
72
+ }
73
+ function mergeRule(rules, rule) {
74
+ const withoutSameMode = rules.filter((existing) => existing.mode !== rule.mode);
75
+ return enforceRuleCap([...withoutSameMode, rule]);
76
+ }
77
+ export class ModelAdaptationStore {
78
+ filePath;
79
+ fingerprint;
80
+ constructor(filePath, options) {
81
+ this.filePath = filePath;
82
+ this.fingerprint = options?.fingerprint ?? currentHostFingerprint;
83
+ }
84
+ static forAgentDir(agentDir, options) {
85
+ return new ModelAdaptationStore(join(agentDir, "state", "model-adaptation.json"), options);
86
+ }
87
+ load() {
88
+ try {
89
+ if (!existsSync(this.filePath))
90
+ return { version: STORE_VERSION, hosts: {} };
91
+ const parsed = JSON.parse(readFileSync(this.filePath, "utf-8"));
92
+ if (parsed && parsed.version === STORE_VERSION && parsed.hosts && typeof parsed.hosts === "object") {
93
+ return parsed;
94
+ }
95
+ }
96
+ catch {
97
+ // Unreadable/corrupt store: start fresh in memory; the next save rewrites the file.
98
+ }
99
+ return { version: STORE_VERSION, hosts: {} };
100
+ }
101
+ write(file) {
102
+ mkdirSync(dirname(this.filePath), { recursive: true });
103
+ writeFileSync(this.filePath, `${JSON.stringify(file, null, "\t")}\n`, "utf-8");
104
+ }
105
+ store(model, profile, at) {
106
+ const host = this.fingerprint();
107
+ const entry = { model, profile: normalizeProfile(profile), at, host };
108
+ const file = this.load();
109
+ file.hosts[host.id] = { ...(file.hosts[host.id] ?? {}), [model]: entry };
110
+ this.write(file);
111
+ return entry;
112
+ }
113
+ /** Persist the profile for a model on the CURRENT host. Best-effort, returns the entry. */
114
+ save(model, profile, at) {
115
+ return this.store(model, profile, at ?? new Date().toISOString());
116
+ }
117
+ /** Profile for a model on the current host; prunes retired rules before returning. */
118
+ get(model, now = new Date()) {
119
+ const host = this.fingerprint();
120
+ const file = this.load();
121
+ const entry = file.hosts[host.id]?.[model];
122
+ if (!entry)
123
+ return emptyProfile();
124
+ const profile = normalizeProfile(entry.profile);
125
+ const prunedRules = pruneRetiredRules(profile.rules, now);
126
+ if (prunedRules.length !== profile.rules.length) {
127
+ return this.store(model, { ...profile, rules: prunedRules }, now.toISOString()).profile;
128
+ }
129
+ return profile;
130
+ }
131
+ /** Add or replace one standing rule, enforcing the per-model cap. */
132
+ addRule(model, rule, now = new Date()) {
133
+ const profile = this.get(model, now);
134
+ const at = now.toISOString();
135
+ const nextRule = {
136
+ mode: rule.mode,
137
+ text: rule.text,
138
+ addedAt: rule.addedAt ?? at,
139
+ lastFiredAt: rule.lastFiredAt ?? at,
140
+ };
141
+ return this.store(model, { ...profile, rules: mergeRule(profile.rules, nextRule) }, at);
142
+ }
143
+ removeRule(model, mode, at = new Date()) {
144
+ const profile = this.get(model, at);
145
+ const rules = profile.rules.filter((rule) => rule.mode !== mode);
146
+ if (rules.length === profile.rules.length)
147
+ return false;
148
+ this.store(model, { ...profile, rules }, at.toISOString());
149
+ return true;
150
+ }
151
+ /** Update last-fired recency for an existing rule. No-op when absent. */
152
+ markRuleFired(model, mode, at = new Date()) {
153
+ const profile = this.get(model, at);
154
+ const rules = profile.rules.map((rule) => rule.mode === mode ? { ...rule, lastFiredAt: at.toISOString() } : rule);
155
+ if (rules.every((rule, index) => rule === profile.rules[index]))
156
+ return undefined;
157
+ return this.store(model, { ...profile, rules }, at.toISOString());
158
+ }
159
+ setProtocol(model, protocol, at) {
160
+ const now = at ?? (protocol.status === "failed" ? protocol.attemptedAt : protocol.calibratedAt);
161
+ const profile = this.get(model, new Date(now));
162
+ return this.store(model, { ...profile, protocol }, now);
163
+ }
164
+ setToolProbe(model, toolProbe, at) {
165
+ const now = at ?? toolProbe.probedAt;
166
+ const profile = this.get(model, new Date(now));
167
+ return this.store(model, { ...profile, toolProbe }, now);
168
+ }
169
+ removeProtocol(model, at = new Date()) {
170
+ const profile = this.get(model, at);
171
+ if (!profile.protocol)
172
+ return false;
173
+ const { protocol: _protocol, ...rest } = profile;
174
+ this.store(model, rest, at.toISOString());
175
+ return true;
176
+ }
177
+ setTeachStats(model, mode, stats, at) {
178
+ const now = at ?? new Date().toISOString();
179
+ const profile = this.get(model, new Date(now));
180
+ return this.store(model, { ...profile, teachStats: { ...profile.teachStats, [mode]: stats } }, now);
181
+ }
182
+ /** Profiles for the current host (default) or an explicit host id. */
183
+ getForHost(hostId) {
184
+ const file = this.load();
185
+ return Object.values(file.hosts[hostId ?? this.fingerprint().id] ?? {}).map((entry) => ({
186
+ ...entry,
187
+ profile: normalizeProfile(entry.profile),
188
+ }));
189
+ }
190
+ /** Every stored profile across all hosts (for cross-machine comparisons). */
191
+ getAll() {
192
+ const file = this.load();
193
+ return Object.values(file.hosts).flatMap((models) => Object.values(models).map((entry) => ({ ...entry, profile: normalizeProfile(entry.profile) })));
194
+ }
195
+ }
196
+ //# sourceMappingURL=adaptation-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adaptation-store.js","sourceRoot":"","sources":["../../../src/core/models/adaptation-store.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { currentHostFingerprint, type HostFingerprint } from \"./fitness-store.ts\";\n\nconst STORE_VERSION = 1;\nconst MAX_RULES_PER_MODEL = 5;\nconst RETIRE_AFTER_MS = 30 * 24 * 60 * 60 * 1000;\n\nexport interface ModelAdaptationRule {\n\tmode: string;\n\ttext: string;\n\taddedAt: string;\n\tlastFiredAt: string;\n}\n\nexport type ModelProtocolCalibration =\n\t| {\n\t\t\tversion: number;\n\t\t\tstatus?: \"calibrated\";\n\t\t\tvariant: string;\n\t\t\tcalibratedAt: string;\n\t }\n\t| {\n\t\t\tversion: number;\n\t\t\tstatus: \"failed\";\n\t\t\tattemptedAt: string;\n\t\t\tvariantsTried: string[];\n\t };\n\nexport type ModelToolProbeVerdict = \"native\" | \"text-protocol\" | \"none\";\n\nexport interface ModelToolProbe {\n\tversion: number;\n\tstatus: ModelToolProbeVerdict;\n\tprobedAt: string;\n\tvariant?: string;\n\tdiagnostic?: string;\n}\n\nexport interface ModelTeachStats {\n\ttaught: number;\n\trecurrenceBefore: number;\n\trecurrenceAfter: number;\n}\n\nexport interface ModelAdaptationProfile {\n\trules: ModelAdaptationRule[];\n\tprotocol?: ModelProtocolCalibration;\n\ttoolProbe?: ModelToolProbe;\n\tteachStats: Record<string, ModelTeachStats>;\n}\n\nexport interface StoredModelAdaptation {\n\tmodel: string;\n\tprofile: ModelAdaptationProfile;\n\tat: string;\n\thost: HostFingerprint;\n}\n\ninterface AdaptationStoreFile {\n\tversion: 1;\n\t/** hostId -> modelRef -> latest stored adaptation profile. */\n\thosts: Record<string, Record<string, StoredModelAdaptation>>;\n}\n\nfunction emptyProfile(): ModelAdaptationProfile {\n\treturn { rules: [], teachStats: {} };\n}\n\nfunction normalizeProfile(profile: Partial<ModelAdaptationProfile> | undefined): ModelAdaptationProfile {\n\treturn {\n\t\trules: Array.isArray(profile?.rules) ? profile.rules.filter(isRule) : [],\n\t\t...(isProtocol(profile?.protocol) && { protocol: profile.protocol }),\n\t\t...(isToolProbe(profile?.toolProbe) && { toolProbe: profile.toolProbe }),\n\t\tteachStats: isRecord(profile?.teachStats) ? filterTeachStats(profile.teachStats) : {},\n\t};\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isRule(value: unknown): value is ModelAdaptationRule {\n\treturn (\n\t\tisRecord(value) &&\n\t\ttypeof value.mode === \"string\" &&\n\t\ttypeof value.text === \"string\" &&\n\t\ttypeof value.addedAt === \"string\" &&\n\t\ttypeof value.lastFiredAt === \"string\"\n\t);\n}\n\nfunction isProtocol(value: unknown): value is ModelProtocolCalibration {\n\tif (!isRecord(value) || typeof value.version !== \"number\") return false;\n\tif (value.status === \"failed\") {\n\t\treturn (\n\t\t\ttypeof value.attemptedAt === \"string\" &&\n\t\t\tArray.isArray(value.variantsTried) &&\n\t\t\tvalue.variantsTried.every((variant) => typeof variant === \"string\")\n\t\t);\n\t}\n\treturn (\n\t\t(value.status === undefined || value.status === \"calibrated\") &&\n\t\ttypeof value.variant === \"string\" &&\n\t\ttypeof value.calibratedAt === \"string\"\n\t);\n}\n\nfunction isToolProbe(value: unknown): value is ModelToolProbe {\n\treturn (\n\t\tisRecord(value) &&\n\t\ttypeof value.version === \"number\" &&\n\t\t(value.status === \"native\" || value.status === \"text-protocol\" || value.status === \"none\") &&\n\t\ttypeof value.probedAt === \"string\" &&\n\t\t(value.variant === undefined || typeof value.variant === \"string\") &&\n\t\t(value.diagnostic === undefined || typeof value.diagnostic === \"string\")\n\t);\n}\n\nfunction isTeachStats(value: unknown): value is ModelTeachStats {\n\treturn (\n\t\tisRecord(value) &&\n\t\ttypeof value.taught === \"number\" &&\n\t\ttypeof value.recurrenceBefore === \"number\" &&\n\t\ttypeof value.recurrenceAfter === \"number\"\n\t);\n}\n\nfunction filterTeachStats(value: Record<string, unknown>): Record<string, ModelTeachStats> {\n\treturn Object.fromEntries(\n\t\tObject.entries(value).filter((entry): entry is [string, ModelTeachStats] => isTeachStats(entry[1])),\n\t);\n}\n\nfunction ruleRecency(rule: ModelAdaptationRule): number {\n\tconst lastFired = Date.parse(rule.lastFiredAt);\n\tif (Number.isFinite(lastFired)) return lastFired;\n\tconst added = Date.parse(rule.addedAt);\n\treturn Number.isFinite(added) ? added : 0;\n}\n\nfunction pruneRetiredRules(rules: readonly ModelAdaptationRule[], now: Date): ModelAdaptationRule[] {\n\tconst cutoff = now.getTime() - RETIRE_AFTER_MS;\n\treturn rules.filter((rule) => ruleRecency(rule) >= cutoff);\n}\n\nfunction enforceRuleCap(rules: readonly ModelAdaptationRule[]): ModelAdaptationRule[] {\n\tif (rules.length <= MAX_RULES_PER_MODEL) return [...rules];\n\treturn [...rules].sort((a, b) => ruleRecency(b) - ruleRecency(a)).slice(0, MAX_RULES_PER_MODEL);\n}\n\nfunction mergeRule(rules: readonly ModelAdaptationRule[], rule: ModelAdaptationRule): ModelAdaptationRule[] {\n\tconst withoutSameMode = rules.filter((existing) => existing.mode !== rule.mode);\n\treturn enforceRuleCap([...withoutSameMode, rule]);\n}\n\nexport class ModelAdaptationStore {\n\tprivate readonly filePath: string;\n\tprivate readonly fingerprint: () => HostFingerprint;\n\n\tconstructor(filePath: string, options?: { fingerprint?: () => HostFingerprint }) {\n\t\tthis.filePath = filePath;\n\t\tthis.fingerprint = options?.fingerprint ?? currentHostFingerprint;\n\t}\n\n\tstatic forAgentDir(agentDir: string, options?: { fingerprint?: () => HostFingerprint }): ModelAdaptationStore {\n\t\treturn new ModelAdaptationStore(join(agentDir, \"state\", \"model-adaptation.json\"), options);\n\t}\n\n\tprivate load(): AdaptationStoreFile {\n\t\ttry {\n\t\t\tif (!existsSync(this.filePath)) return { version: STORE_VERSION, hosts: {} };\n\t\t\tconst parsed = JSON.parse(readFileSync(this.filePath, \"utf-8\")) as AdaptationStoreFile;\n\t\t\tif (parsed && parsed.version === STORE_VERSION && parsed.hosts && typeof parsed.hosts === \"object\") {\n\t\t\t\treturn parsed;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Unreadable/corrupt store: start fresh in memory; the next save rewrites the file.\n\t\t}\n\t\treturn { version: STORE_VERSION, hosts: {} };\n\t}\n\n\tprivate write(file: AdaptationStoreFile): void {\n\t\tmkdirSync(dirname(this.filePath), { recursive: true });\n\t\twriteFileSync(this.filePath, `${JSON.stringify(file, null, \"\\t\")}\\n`, \"utf-8\");\n\t}\n\n\tprivate store(model: string, profile: ModelAdaptationProfile, at: string): StoredModelAdaptation {\n\t\tconst host = this.fingerprint();\n\t\tconst entry: StoredModelAdaptation = { model, profile: normalizeProfile(profile), at, host };\n\t\tconst file = this.load();\n\t\tfile.hosts[host.id] = { ...(file.hosts[host.id] ?? {}), [model]: entry };\n\t\tthis.write(file);\n\t\treturn entry;\n\t}\n\n\t/** Persist the profile for a model on the CURRENT host. Best-effort, returns the entry. */\n\tsave(model: string, profile: ModelAdaptationProfile, at?: string): StoredModelAdaptation {\n\t\treturn this.store(model, profile, at ?? new Date().toISOString());\n\t}\n\n\t/** Profile for a model on the current host; prunes retired rules before returning. */\n\tget(model: string, now: Date = new Date()): ModelAdaptationProfile {\n\t\tconst host = this.fingerprint();\n\t\tconst file = this.load();\n\t\tconst entry = file.hosts[host.id]?.[model];\n\t\tif (!entry) return emptyProfile();\n\t\tconst profile = normalizeProfile(entry.profile);\n\t\tconst prunedRules = pruneRetiredRules(profile.rules, now);\n\t\tif (prunedRules.length !== profile.rules.length) {\n\t\t\treturn this.store(model, { ...profile, rules: prunedRules }, now.toISOString()).profile;\n\t\t}\n\t\treturn profile;\n\t}\n\n\t/** Add or replace one standing rule, enforcing the per-model cap. */\n\taddRule(\n\t\tmodel: string,\n\t\trule: { mode: string; text: string; addedAt?: string; lastFiredAt?: string },\n\t\tnow = new Date(),\n\t): StoredModelAdaptation {\n\t\tconst profile = this.get(model, now);\n\t\tconst at = now.toISOString();\n\t\tconst nextRule: ModelAdaptationRule = {\n\t\t\tmode: rule.mode,\n\t\t\ttext: rule.text,\n\t\t\taddedAt: rule.addedAt ?? at,\n\t\t\tlastFiredAt: rule.lastFiredAt ?? at,\n\t\t};\n\t\treturn this.store(model, { ...profile, rules: mergeRule(profile.rules, nextRule) }, at);\n\t}\n\n\tremoveRule(model: string, mode: string, at = new Date()): boolean {\n\t\tconst profile = this.get(model, at);\n\t\tconst rules = profile.rules.filter((rule) => rule.mode !== mode);\n\t\tif (rules.length === profile.rules.length) return false;\n\t\tthis.store(model, { ...profile, rules }, at.toISOString());\n\t\treturn true;\n\t}\n\n\t/** Update last-fired recency for an existing rule. No-op when absent. */\n\tmarkRuleFired(model: string, mode: string, at = new Date()): StoredModelAdaptation | undefined {\n\t\tconst profile = this.get(model, at);\n\t\tconst rules = profile.rules.map((rule) =>\n\t\t\trule.mode === mode ? { ...rule, lastFiredAt: at.toISOString() } : rule,\n\t\t);\n\t\tif (rules.every((rule, index) => rule === profile.rules[index])) return undefined;\n\t\treturn this.store(model, { ...profile, rules }, at.toISOString());\n\t}\n\n\tsetProtocol(model: string, protocol: ModelProtocolCalibration, at?: string): StoredModelAdaptation {\n\t\tconst now = at ?? (protocol.status === \"failed\" ? protocol.attemptedAt : protocol.calibratedAt);\n\t\tconst profile = this.get(model, new Date(now));\n\t\treturn this.store(model, { ...profile, protocol }, now);\n\t}\n\n\tsetToolProbe(model: string, toolProbe: ModelToolProbe, at?: string): StoredModelAdaptation {\n\t\tconst now = at ?? toolProbe.probedAt;\n\t\tconst profile = this.get(model, new Date(now));\n\t\treturn this.store(model, { ...profile, toolProbe }, now);\n\t}\n\n\tremoveProtocol(model: string, at = new Date()): boolean {\n\t\tconst profile = this.get(model, at);\n\t\tif (!profile.protocol) return false;\n\t\tconst { protocol: _protocol, ...rest } = profile;\n\t\tthis.store(model, rest, at.toISOString());\n\t\treturn true;\n\t}\n\n\tsetTeachStats(model: string, mode: string, stats: ModelTeachStats, at?: string): StoredModelAdaptation {\n\t\tconst now = at ?? new Date().toISOString();\n\t\tconst profile = this.get(model, new Date(now));\n\t\treturn this.store(model, { ...profile, teachStats: { ...profile.teachStats, [mode]: stats } }, now);\n\t}\n\n\t/** Profiles for the current host (default) or an explicit host id. */\n\tgetForHost(hostId?: string): StoredModelAdaptation[] {\n\t\tconst file = this.load();\n\t\treturn Object.values(file.hosts[hostId ?? this.fingerprint().id] ?? {}).map((entry) => ({\n\t\t\t...entry,\n\t\t\tprofile: normalizeProfile(entry.profile),\n\t\t}));\n\t}\n\n\t/** Every stored profile across all hosts (for cross-machine comparisons). */\n\tgetAll(): StoredModelAdaptation[] {\n\t\tconst file = this.load();\n\t\treturn Object.values(file.hosts).flatMap((models) =>\n\t\t\tObject.values(models).map((entry) => ({ ...entry, profile: normalizeProfile(entry.profile) })),\n\t\t);\n\t}\n}\n"]}
@@ -9,11 +9,32 @@
9
9
  */
10
10
  import type { AgentMessage, AgentState } from "@caupulican/pi-agent-core";
11
11
  import { type SessionManager } from "@caupulican/pi-agent-core/node";
12
- import type { Model, Usage } from "@caupulican/pi-ai";
12
+ import type { Model, ToolArgumentExecutionOutcome, ToolArgumentTeachState, ToolArgumentValidationTelemetryEvent, Usage } from "@caupulican/pi-ai";
13
13
  import { type SessionStats, type SpawnedUsageTotals } from "./agent-session.ts";
14
14
  import { type DailyUsageTotals } from "./cost/daily-usage.ts";
15
15
  import type { ContextUsage, ToolDefinition } from "./extensions/index.ts";
16
16
  import type { SettingsManager } from "./settings-manager.ts";
17
+ export declare const TOOL_ARGUMENT_VALIDATION_CUSTOM_TYPE = "tool_argument_validation";
18
+ export interface ToolArgumentValidationTeachEfficacy {
19
+ recurrenceBefore: number;
20
+ recurrenceAfter: number;
21
+ repairedThenSucceeded: number;
22
+ repairedThenFailed: number;
23
+ repairedThenNotRun: number;
24
+ }
25
+ export interface ToolArgumentValidationStats {
26
+ clean: number;
27
+ repaired: number;
28
+ bounced: number;
29
+ failureModes: Record<string, number>;
30
+ repairsApplied: Record<string, number>;
31
+ taught: Record<ToolArgumentTeachState, number>;
32
+ executionOutcome: Record<ToolArgumentExecutionOutcome, number>;
33
+ teachEfficacy: Record<string, ToolArgumentValidationTeachEfficacy>;
34
+ }
35
+ export interface ToolArgumentValidationRecord extends ToolArgumentValidationTelemetryEvent {
36
+ version: 1;
37
+ }
17
38
  export interface SessionAnalyticsDeps {
18
39
  /** Live agent state — assistant-message usage and message counts are read from here. */
19
40
  getState(): AgentState;
@@ -36,6 +57,8 @@ export declare class SessionAnalytics {
36
57
  private readonly deps;
37
58
  constructor(deps: SessionAnalyticsDeps);
38
59
  getSessionStats(): SessionStats;
60
+ recordToolArgumentValidation(event: ToolArgumentValidationTelemetryEvent): void;
61
+ getToolArgumentValidationStats(): ToolArgumentValidationStats;
39
62
  /**
40
63
  * Cumulative usage (full breakdown) for this session's entire spawn subtree: its own
41
64
  * assistant messages PLUS every `spawned_usage` report it has rolled up. Single source of
@@ -1 +1 @@
1
- {"version":3,"file":"session-analytics.d.ts","sourceRoot":"","sources":["../../src/core/session-analytics.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAC1E,OAAO,EAMN,KAAK,cAAc,EACnB,MAAM,gCAAgC,CAAC;AACxC,OAAO,KAAK,EAAoB,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAIxE,OAAO,EACN,KAAK,YAAY,EAGjB,KAAK,kBAAkB,EACvB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAGN,KAAK,gBAAgB,EAGrB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC1E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D,MAAM,WAAW,oBAAoB;IACpC,0FAAwF;IACxF,QAAQ,IAAI,UAAU,CAAC;IACvB,sGAAoG;IACpG,WAAW,IAAI,YAAY,EAAE,CAAC;IAC9B,iFAA+E;IAC/E,QAAQ,IAAI,KAAK,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;IACnC,uFAAqF;IACrF,iBAAiB,IAAI,cAAc,CAAC;IACpC,kDAAgD;IAChD,kBAAkB,IAAI,eAAe,CAAC;IACtC,4EAA4E;IAC5E,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAAC;CAC5D;AAED,qBAAa,gBAAgB;IAC5B,iGAAiG;IACjG,OAAO,CAAC,kBAAkB,CAAC,CAAqD;IAChF,qFAAqF;IACrF,OAAO,CAAC,gBAAgB,CAAC,CAAsE;IAE/F,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAuB;IAE5C,YAAY,IAAI,EAAE,oBAAoB,EAErC;IAED,eAAe,IAAI,YAAY,CA2C9B;IAED;;;;;;;;;OASG;IACH,kBAAkB,IAAI,KAAK,CAiD1B;IAED;;;;;;;;;;;OAWG;IACH,eAAe,CACd,KAAK,EAAE,KAAK,EACZ,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,eAAe,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GACpE,MAAM,GAAG,SAAS,CAoBpB;IAED;;;;OAIG;IACH,eAAe,IAAI,kBAAkB,CAgBpC;IAED,mBAAmB,CAAC,GAAG,OAAa,GAAG,gBAAgB,CActD;IAED,sBAAsB,CAAC,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,EAAE,GAAG,OAAa,GAAG,MAAM,CAExF;IAED,eAAe,IAAI,YAAY,GAAG,SAAS,CA4C1C;IAED;;;;OAIG;IACG,YAAY,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAgBvD;IAED;;;;;OAKG;IACH,aAAa,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAgCzC;IAED;;;;OAIG;IACH,oBAAoB,IAAI,MAAM,GAAG,SAAS,CAuBzC;CACD","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 { AssistantMessage, Model, Usage } 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 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\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};\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, 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"]}
@@ -17,6 +17,7 @@ import { SPAWNED_USAGE_CUSTOM_TYPE, } from "./agent-session.js";
17
17
  import { aggregateDailyUsageFromSessionFiles, aggregateDailyUsageFromSessionRoot, formatDailyUsageBreakdown, getLocalDayWindow, } from "./cost/daily-usage.js";
18
18
  import { exportSessionToHtml } from "./export-html/index.js";
19
19
  import { createToolHtmlRenderer } from "./export-html/tool-renderer.js";
20
+ export const TOOL_ARGUMENT_VALIDATION_CUSTOM_TYPE = "tool_argument_validation";
20
21
  export class SessionAnalytics {
21
22
  /** Memoized spawned-usage totals, keyed by session entry count (Bug #22: O(1) between turns). */
22
23
  _spawnedUsageCache;
@@ -48,6 +49,7 @@ export class SessionAnalytics {
48
49
  totalCost += assistantMsg.usage.cost.total;
49
50
  }
50
51
  }
52
+ const toolArgumentValidation = this.getToolArgumentValidationStats();
51
53
  return {
52
54
  sessionFile: this.deps.getSessionManager().getSessionFile(),
53
55
  sessionId: this.deps.getSessionManager().getSessionId(),
@@ -65,8 +67,81 @@ export class SessionAnalytics {
65
67
  },
66
68
  cost: totalCost,
67
69
  contextUsage: this.getContextUsage(),
70
+ toolArgumentValidation,
68
71
  };
69
72
  }
73
+ recordToolArgumentValidation(event) {
74
+ try {
75
+ const record = { version: 1, ...event };
76
+ this.deps.getSessionManager().appendCustomEntry(TOOL_ARGUMENT_VALIDATION_CUSTOM_TYPE, record);
77
+ }
78
+ catch {
79
+ // Telemetry is best-effort and must never affect the observed turn.
80
+ }
81
+ }
82
+ getToolArgumentValidationStats() {
83
+ const stats = {
84
+ clean: 0,
85
+ repaired: 0,
86
+ bounced: 0,
87
+ failureModes: {},
88
+ repairsApplied: {},
89
+ taught: { none: 0, note: 0, rule: 0 },
90
+ executionOutcome: { not_run: 0, succeeded: 0, failed: 0 },
91
+ teachEfficacy: {},
92
+ };
93
+ const getEfficacy = (record, mode) => {
94
+ const key = `${record.provider ?? "unknown"}/${record.model ?? "unknown"}:${mode}`;
95
+ stats.teachEfficacy[key] ??= {
96
+ recurrenceBefore: 0,
97
+ recurrenceAfter: 0,
98
+ repairedThenSucceeded: 0,
99
+ repairedThenFailed: 0,
100
+ repairedThenNotRun: 0,
101
+ };
102
+ return stats.teachEfficacy[key];
103
+ };
104
+ for (const entry of this.deps.getSessionManager().getEntries()) {
105
+ if (entry.type !== "custom" || entry.customType !== TOOL_ARGUMENT_VALIDATION_CUSTOM_TYPE)
106
+ continue;
107
+ const record = entry.data;
108
+ if (!record || record.version !== 1)
109
+ continue;
110
+ stats[record.outcome] += 1;
111
+ const taught = record.taught ?? "none";
112
+ const executionOutcome = record.executionOutcome ?? "not_run";
113
+ stats.taught[taught] += 1;
114
+ stats.executionOutcome[executionOutcome] += 1;
115
+ const modes = new Set([...record.failureModes, ...record.repairsApplied]);
116
+ for (const mode of record.failureModes) {
117
+ stats.failureModes[mode] = (stats.failureModes[mode] ?? 0) + 1;
118
+ }
119
+ for (const repair of record.repairsApplied) {
120
+ stats.repairsApplied[repair] = (stats.repairsApplied[repair] ?? 0) + 1;
121
+ }
122
+ for (const mode of modes) {
123
+ const efficacy = getEfficacy(record, mode);
124
+ if (taught === "none") {
125
+ efficacy.recurrenceBefore++;
126
+ }
127
+ else {
128
+ efficacy.recurrenceAfter++;
129
+ }
130
+ }
131
+ if (record.outcome === "repaired") {
132
+ for (const repair of record.repairsApplied) {
133
+ const efficacy = getEfficacy(record, repair);
134
+ if (executionOutcome === "succeeded")
135
+ efficacy.repairedThenSucceeded++;
136
+ if (executionOutcome === "failed")
137
+ efficacy.repairedThenFailed++;
138
+ if (executionOutcome === "not_run")
139
+ efficacy.repairedThenNotRun++;
140
+ }
141
+ }
142
+ }
143
+ return stats;
144
+ }
70
145
  /**
71
146
  * Cumulative usage (full breakdown) for this session's entire spawn subtree: its own
72
147
  * assistant messages PLUS every `spawned_usage` report it has rolled up. Single source of
@@ -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, 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 { AssistantMessage, Model, Usage } 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 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\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};\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, 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"]}