@ashx-j/lunr 0.2.27 → 0.2.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -1
- package/dist/builtin-extensions/narumiruna-pi-goal/src/accounting.d.ts.map +1 -1
- package/dist/builtin-extensions/narumiruna-pi-goal/src/accounting.js +5 -2
- package/dist/builtin-extensions/narumiruna-pi-goal/src/accounting.js.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/runs/background/chain-root-attachment.d.ts +4 -1
- package/dist/builtin-extensions/pi-subagents/src/runs/background/chain-root-attachment.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/runs/background/chain-root-attachment.js +2 -0
- package/dist/builtin-extensions/pi-subagents/src/runs/background/chain-root-attachment.js.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/runs/background/notify.d.ts +5 -0
- package/dist/builtin-extensions/pi-subagents/src/runs/background/notify.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/runs/background/notify.js +2 -0
- package/dist/builtin-extensions/pi-subagents/src/runs/background/notify.js.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/runs/background/subagent-runner.d.ts +5 -1
- package/dist/builtin-extensions/pi-subagents/src/runs/background/subagent-runner.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/runs/background/subagent-runner.js +68 -40
- package/dist/builtin-extensions/pi-subagents/src/runs/background/subagent-runner.js.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/runs/foreground/execution.d.ts +0 -3
- package/dist/builtin-extensions/pi-subagents/src/runs/foreground/execution.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/runs/foreground/execution.js +28 -4
- package/dist/builtin-extensions/pi-subagents/src/runs/foreground/execution.js.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/runs/foreground/subagent-executor.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/runs/foreground/subagent-executor.js +1 -0
- package/dist/builtin-extensions/pi-subagents/src/runs/foreground/subagent-executor.js.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/shared/session-tokens.d.ts +7 -1
- package/dist/builtin-extensions/pi-subagents/src/shared/session-tokens.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/shared/session-tokens.js +70 -38
- package/dist/builtin-extensions/pi-subagents/src/shared/session-tokens.js.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/shared/types.d.ts +9 -0
- package/dist/builtin-extensions/pi-subagents/src/shared/types.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/shared/types.js.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/slash/slash-commands.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/slash/slash-commands.js +13 -62
- package/dist/builtin-extensions/pi-subagents/src/slash/slash-commands.js.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/tui/render.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/tui/render.js +1 -1
- package/dist/builtin-extensions/pi-subagents/src/tui/render.js.map +1 -1
- package/dist/builtin-extensions/pi-tps.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-tps.js +10 -2
- package/dist/builtin-extensions/pi-tps.js.map +1 -1
- package/dist/catalog/models.json +1 -1
- package/dist/catalog/providers/google-vertex.json +1 -1
- package/dist/catalog/providers/google.json +1 -1
- package/dist/catalog/publication.json +3 -3
- package/dist/core/agent-session.d.ts +6 -15
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +35 -50
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/cache-stats.d.ts +1 -1
- package/dist/core/cache-stats.d.ts.map +1 -1
- package/dist/core/cache-stats.js +1 -1
- package/dist/core/cache-stats.js.map +1 -1
- package/dist/core/compaction/branch-summarization.d.ts +2 -1
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js +1 -0
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +5 -11
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +38 -92
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/context-breakdown.d.ts +2 -2
- package/dist/core/context-breakdown.d.ts.map +1 -1
- package/dist/core/context-breakdown.js +22 -20
- package/dist/core/context-breakdown.js.map +1 -1
- package/dist/core/extensions/types.d.ts +3 -1
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/session-manager.d.ts +5 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +16 -1
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/usage-accounting.d.ts +47 -0
- package/dist/core/usage-accounting.d.ts.map +1 -0
- package/dist/core/usage-accounting.js +205 -0
- package/dist/core/usage-accounting.js.map +1 -0
- package/dist/core/usage-history.d.ts +1 -18
- package/dist/core/usage-history.d.ts.map +1 -1
- package/dist/core/usage-history.js +46 -114
- package/dist/core/usage-history.js.map +1 -1
- package/dist/gateway/mobile-commands.d.ts.map +1 -1
- package/dist/gateway/mobile-commands.js +13 -1
- package/dist/gateway/mobile-commands.js.map +1 -1
- package/dist/modes/interactive/components/context-view.d.ts.map +1 -1
- package/dist/modes/interactive/components/context-view.js +2 -2
- package/dist/modes/interactive/components/context-view.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +8 -12
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/usage-view.d.ts +5 -0
- package/dist/modes/interactive/components/usage-view.d.ts.map +1 -1
- package/dist/modes/interactive/components/usage-view.js +21 -6
- package/dist/modes/interactive/components/usage-view.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +1 -5
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +55 -22
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/node-runtime/{agent-session-services-KDMSPQDY.js → agent-session-services-RTUGP3XR.js} +5 -4
- package/dist/node-runtime/{builtin-extensions-OX6KELNX.js → builtin-extensions-N4EUD6SB.js} +1 -1
- package/dist/node-runtime/{chunk-A2WO6WIN.js → chunk-4QRUZ64T.js} +11 -5
- package/dist/node-runtime/{chunk-ZDJUR45D.js → chunk-BMRXLQWQ.js} +41 -38
- package/dist/node-runtime/{chunk-MQPKUHRN.js → chunk-DFMPRAQQ.js} +8 -8
- package/dist/node-runtime/{chunk-C3P6Q3B2.js → chunk-E5DQGNJI.js} +10 -10
- package/dist/node-runtime/{chunk-FIRA54AC.js → chunk-ICKGAKWX.js} +22 -55
- package/dist/node-runtime/{chunk-JWEVT363.js → chunk-JEK35V2M.js} +14 -8
- package/dist/node-runtime/{chunk-EPR2F3GU.js → chunk-PWHTSQC4.js} +15 -1
- package/dist/node-runtime/{chunk-AYHH7YSD.js → chunk-QT3F6UEV.js} +61 -30
- package/dist/node-runtime/{chunk-JQYNHAKX.js → chunk-RY3FJ6WI.js} +29 -27
- package/dist/node-runtime/{chunk-I2H5QMWW.js → chunk-STMX7W5I.js} +1 -1
- package/dist/node-runtime/chunk-SWWHZ2RI.js +158 -0
- package/dist/node-runtime/{chunk-QNVMKNNN.js → chunk-U3YVZ6UC.js} +4 -4
- package/dist/node-runtime/{command-3HMSY6W2.js → command-YRT7I2BW.js} +1 -1
- package/dist/node-runtime/{commands-OCSSEEWJ.js → commands-2ETOWWRA.js} +5 -5
- package/dist/node-runtime/{export-html-BRWF5AHI.js → export-html-DBQRRMAY.js} +1 -1
- package/dist/node-runtime/{gateway-6623C2U3.js → gateway-V6K2CDWN.js} +9 -9
- package/dist/node-runtime/{goal-AIHEDUYA.js → goal-J7TYMOBK.js} +9 -3
- package/dist/node-runtime/index.js +9 -8
- package/dist/node-runtime/{interactive-mode-FM5DNUSW.js → interactive-mode-ASZ7APYP.js} +7 -6
- package/dist/node-runtime/{lunr-cron-LKVA5CZZ.js → lunr-cron-WD3YH4CR.js} +4 -4
- package/dist/node-runtime/main.js +7 -6
- package/dist/node-runtime/{mobile-commands-XT3NQXLD.js → mobile-commands-ICUXFW5V.js} +2 -2
- package/dist/node-runtime/{pi-subagents-LT3CTO4E.js → pi-subagents-QPU3OXE4.js} +17 -62
- package/dist/node-runtime/rpc-entry.js +7 -6
- package/dist/node-runtime/{session-handoff-LNSPDTKY.js → session-handoff-LYH53ESV.js} +2 -2
- package/dist/node-runtime/{session-manager-YIFIAVGC.js → session-manager-ZJ3N5Q74.js} +1 -1
- package/dist/node-runtime/{subagent-executor-Q5F7DBVM.js → subagent-executor-UI6SHG3X.js} +294 -231
- package/docs/features.md +10 -0
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/install-lock/package-lock.json +24 -24
- package/install-lock/package.json +2 -2
- package/npm-shrinkwrap.json +21 -21
- package/package.json +7 -7
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usage-accounting.d.ts","sourceRoot":"","sources":["../../src/core/usage-accounting.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AACrE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEzD,eAAO,MAAM,kBAAkB,kBAAkB,CAAC;AAElD,MAAM,WAAW,gBAAgB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,KAAK,CAAC;IACb,UAAU,EAAE,gBAAgB,CAAC,YAAY,CAAC,CAAC;CAC3C;AAED,MAAM,WAAW,WAAW;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;CACrB;AAED,wBAAgB,gBAAgB,IAAI,WAAW,CAa9C;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,GAAG,WAAW,GAAG,YAAY,CAAC,GAAG,MAAM,CAEvG;AAED,4EAA4E;AAC5E,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,GAAG,gBAAgB,EAAE,CAwBzF;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,SAAS,gBAAgB,EAAE,GAAG,WAAW,CAepF;AAED,MAAM,WAAW,kBAAkB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;CACrB;AAgCD,mIAAmI;AACnI,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,GAAG,kBAAkB,EAAE,CA4FxF;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,SAAS,kBAAkB,EAAE,GAAG,WAAW,CAepF","sourcesContent":["import type { AssistantMessage, Usage } from \"@ashx-j/lunr-ai\";\nimport type { SessionEntry } from \"./session-manager.ts\";\n\nexport const REQUEST_USAGE_TYPE = \"request-usage\";\n\nexport interface AccountedRequest {\n\tid: string;\n\tpurpose: string;\n\tprovider: string;\n\tmodel: string;\n\ttimestamp: number;\n\tusage: Usage;\n\tstopReason: AssistantMessage[\"stopReason\"];\n}\n\nexport interface UsageTotals {\n\trequests: number;\n\tinput: number;\n\toutput: number;\n\tcacheRead: number;\n\tcacheWrite: number;\n\ttotal: number;\n\tcost: number;\n\tunknownRequests: number;\n\tpartialRequests: number;\n\tunknownCosts: number;\n}\n\nexport function emptyUsageTotals(): UsageTotals {\n\treturn {\n\t\trequests: 0,\n\t\tinput: 0,\n\t\toutput: 0,\n\t\tcacheRead: 0,\n\t\tcacheWrite: 0,\n\t\ttotal: 0,\n\t\tcost: 0,\n\t\tunknownRequests: 0,\n\t\tpartialRequests: 0,\n\t\tunknownCosts: 0,\n\t};\n}\n\nexport function usageTokens(usage: Pick<Usage, \"input\" | \"output\" | \"cacheRead\" | \"cacheWrite\">): number {\n\treturn usage.input + usage.output + usage.cacheRead + usage.cacheWrite;\n}\n\n/** All incurred requests, independent of the active conversation branch. */\nexport function collectUsageRequests(entries: readonly SessionEntry[]): AccountedRequest[] {\n\tconst requests = new Map<string, AccountedRequest>();\n\tfor (const entry of entries) {\n\t\tif (entry.inherited) continue;\n\t\tlet request: AccountedRequest | undefined;\n\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\tconst message = entry.message;\n\t\t\tif (!validUsage(message.usage)) continue;\n\t\t\trequest = {\n\t\t\t\tid: message.usage.requestId ?? `${entry.id}:${entry.timestamp}:${message.provider}/${message.model}`,\n\t\t\t\tpurpose: \"conversation\",\n\t\t\t\tprovider: message.provider,\n\t\t\t\tmodel: message.responseModel ?? message.model,\n\t\t\t\ttimestamp: Number.isFinite(Date.parse(entry.timestamp)) ? Date.parse(entry.timestamp) : message.timestamp,\n\t\t\t\tusage: message.usage,\n\t\t\t\tstopReason: message.stopReason,\n\t\t\t};\n\t\t} else if (entry.type === \"custom\" && entry.customType === REQUEST_USAGE_TYPE) {\n\t\t\tconst data = entry.data as AccountedRequest | undefined;\n\t\t\tif (validRequest(data)) request = data;\n\t\t}\n\t\tif (request && validRequest(request)) requests.set(request.id, request);\n\t}\n\treturn [...requests.values()];\n}\n\nexport function totalRequestUsage(requests: readonly AccountedRequest[]): UsageTotals {\n\tconst total = emptyUsageTotals();\n\ttotal.requests = requests.length;\n\tfor (const { usage } of requests) {\n\t\ttotal.input += usage.input;\n\t\ttotal.output += usage.output;\n\t\ttotal.cacheRead += usage.cacheRead;\n\t\ttotal.cacheWrite += usage.cacheWrite;\n\t\ttotal.total += usageTokens(usage);\n\t\ttotal.cost += usage.cost.total;\n\t\ttotal.unknownRequests += Number(usage.measurement === \"unknown\");\n\t\ttotal.partialRequests += Number(usage.measurement === \"partial\");\n\t\ttotal.unknownCosts += Number(usage.costSource === \"unknown\");\n\t}\n\treturn total;\n}\n\nexport interface ChildUsageSnapshot {\n\tid: string;\n\tlabel: string;\n\tsessionFile?: string;\n\tinput: number;\n\toutput: number;\n\tcacheRead: number;\n\tcacheWrite: number;\n\tcost: number;\n\tturns: number;\n\tunknownRequests: number;\n\tpartialRequests: number;\n\tunknownCosts: number;\n}\n\nfunction record(value: unknown): Record<string, unknown> | undefined {\n\treturn value !== null && typeof value === \"object\" ? (value as Record<string, unknown>) : undefined;\n}\nfunction amount(value: unknown): number {\n\treturn typeof value === \"number\" && Number.isFinite(value) && value >= 0 ? value : 0;\n}\n\nfunction validUsage(value: unknown): value is Usage {\n\tconst usage = record(value);\n\tconst cost = record(usage?.cost);\n\treturn Boolean(\n\t\tusage &&\n\t\t\tcost &&\n\t\t\t[usage.input, usage.output, usage.cacheRead, usage.cacheWrite, cost.total].every(\n\t\t\t\t(amount) => typeof amount === \"number\" && Number.isFinite(amount) && amount >= 0,\n\t\t\t),\n\t);\n}\n\nfunction validRequest(value: unknown): value is AccountedRequest {\n\tconst request = record(value);\n\treturn Boolean(\n\t\trequest &&\n\t\t\ttypeof request.id === \"string\" &&\n\t\t\trequest.id &&\n\t\t\tvalidUsage(request.usage) &&\n\t\t\ttypeof request.timestamp === \"number\" &&\n\t\t\tNumber.isFinite(request.timestamp),\n\t);\n}\n/** Request IDs deduplicate completion, wait, fallback, resume, and inherited receipts. Legacy receipts use cumulative counters. */\nexport function collectChildUsage(entries: readonly SessionEntry[]): ChildUsageSnapshot[] {\n\tconst children = new Map<string, ChildUsageSnapshot>();\n\tconst inherited = new Map<string, ChildUsageSnapshot>();\n\tconst requestGroups = new Map<string, Map<string, AccountedRequest>>();\n\tconst inheritedRequestIds = new Set<string>();\n\tconst requestOwners = new Map<string, string>();\n\tfor (const entry of entries) {\n\t\tconst details =\n\t\t\tentry.type === \"custom_message\" && [\"subagent-notify\", \"subagent-slash-result\"].includes(entry.customType)\n\t\t\t\t? record(entry.details)\n\t\t\t\t: entry.type === \"message\" &&\n\t\t\t\t\t\tentry.message.role === \"toolResult\" &&\n\t\t\t\t\t\tentry.message.toolName?.startsWith(\"subagent\")\n\t\t\t\t\t? record(entry.message.details)\n\t\t\t\t\t: undefined;\n\t\tif (!details) continue;\n\t\tconst slash = record(record(details.result)?.details);\n\t\tconst candidates = details.children ?? slash?.results ?? details.results;\n\t\tif (!Array.isArray(candidates)) continue;\n\t\tfor (const candidate of candidates) {\n\t\t\tconst child = record(candidate);\n\t\t\tconst usage = record(child?.usage);\n\t\t\tif (!child || !usage) continue;\n\t\t\tconst identity = child.sessionFile ?? child.childId ?? child.id;\n\t\t\tif (typeof identity !== \"string\" || !identity) continue;\n\t\t\tconst snapshot: ChildUsageSnapshot = {\n\t\t\t\tid: identity,\n\t\t\t\tlabel: String(child.description ?? child.agent ?? \"child\"),\n\t\t\t\tsessionFile: typeof child.sessionFile === \"string\" ? child.sessionFile : undefined,\n\t\t\t\tinput: amount(usage.input),\n\t\t\t\toutput: amount(usage.output),\n\t\t\t\tcacheRead: amount(usage.cacheRead),\n\t\t\t\tcacheWrite: amount(usage.cacheWrite),\n\t\t\t\tcost: amount(usage.cost),\n\t\t\t\tturns: amount(usage.turns),\n\t\t\t\tunknownRequests: amount(usage.unknownRequests),\n\t\t\t\tpartialRequests: amount(usage.partialRequests),\n\t\t\t\tunknownCosts: amount(usage.unknownCosts),\n\t\t\t};\n\t\t\tif (Array.isArray(child.usageRequests)) {\n\t\t\t\tconst requests = requestGroups.get(identity) ?? new Map<string, AccountedRequest>();\n\t\t\t\trequestGroups.set(identity, requests);\n\t\t\t\tfor (const request of child.usageRequests) {\n\t\t\t\t\tif (!validRequest(request)) continue;\n\t\t\t\t\tif (entry.inherited) inheritedRequestIds.add(request.id);\n\t\t\t\t\telse if (!requestOwners.has(request.id) || requestOwners.get(request.id) === identity) {\n\t\t\t\t\t\trequestOwners.set(request.id, identity);\n\t\t\t\t\t\trequests.set(request.id, request);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst target = entry.inherited ? inherited : children;\n\t\t\tconst previous = target.get(identity);\n\t\t\t// A stale receipt must not undo a newer cumulative receipt.\n\t\t\tif (!previous || usageTokens(snapshot) >= usageTokens(previous)) target.set(identity, snapshot);\n\t\t}\n\t}\n\treturn [...children.values()].map((child) => {\n\t\tconst requests = requestGroups.get(child.id);\n\t\tif (requests) {\n\t\t\tconst ownRequests = [...requests.values()].filter((request) => !inheritedRequestIds.has(request.id));\n\t\t\tconst total = totalRequestUsage(ownRequests);\n\t\t\treturn {\n\t\t\t\t...child,\n\t\t\t\tinput: total.input,\n\t\t\t\toutput: total.output,\n\t\t\t\tcacheRead: total.cacheRead,\n\t\t\t\tcacheWrite: total.cacheWrite,\n\t\t\t\tcost: total.cost,\n\t\t\t\tturns: ownRequests.length,\n\t\t\t\tunknownRequests: total.unknownRequests,\n\t\t\t\tpartialRequests: total.partialRequests,\n\t\t\t\tunknownCosts: total.unknownCosts,\n\t\t\t};\n\t\t}\n\t\tconst baseline = inherited.get(child.id);\n\t\tif (!baseline) return child;\n\t\tconst own = { ...child };\n\t\tfor (const key of [\n\t\t\t\"input\",\n\t\t\t\"output\",\n\t\t\t\"cacheRead\",\n\t\t\t\"cacheWrite\",\n\t\t\t\"cost\",\n\t\t\t\"turns\",\n\t\t\t\"unknownRequests\",\n\t\t\t\"partialRequests\",\n\t\t\t\"unknownCosts\",\n\t\t] as const)\n\t\t\town[key] = Math.max(0, child[key] - baseline[key]);\n\t\treturn own;\n\t});\n}\n\nexport function totalChildUsage(children: readonly ChildUsageSnapshot[]): UsageTotals {\n\tconst totals = emptyUsageTotals();\n\tfor (const child of children) {\n\t\ttotals.requests += child.turns;\n\t\ttotals.input += child.input;\n\t\ttotals.output += child.output;\n\t\ttotals.cacheRead += child.cacheRead;\n\t\ttotals.cacheWrite += child.cacheWrite;\n\t\ttotals.total += usageTokens(child);\n\t\ttotals.cost += child.cost;\n\t\ttotals.unknownRequests += child.unknownRequests;\n\t\ttotals.partialRequests += child.partialRequests;\n\t\ttotals.unknownCosts += child.unknownCosts;\n\t}\n\treturn totals;\n}\n"]}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
export const REQUEST_USAGE_TYPE = "request-usage";
|
|
2
|
+
export function emptyUsageTotals() {
|
|
3
|
+
return {
|
|
4
|
+
requests: 0,
|
|
5
|
+
input: 0,
|
|
6
|
+
output: 0,
|
|
7
|
+
cacheRead: 0,
|
|
8
|
+
cacheWrite: 0,
|
|
9
|
+
total: 0,
|
|
10
|
+
cost: 0,
|
|
11
|
+
unknownRequests: 0,
|
|
12
|
+
partialRequests: 0,
|
|
13
|
+
unknownCosts: 0,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function usageTokens(usage) {
|
|
17
|
+
return usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
|
|
18
|
+
}
|
|
19
|
+
/** All incurred requests, independent of the active conversation branch. */
|
|
20
|
+
export function collectUsageRequests(entries) {
|
|
21
|
+
const requests = new Map();
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
if (entry.inherited)
|
|
24
|
+
continue;
|
|
25
|
+
let request;
|
|
26
|
+
if (entry.type === "message" && entry.message.role === "assistant") {
|
|
27
|
+
const message = entry.message;
|
|
28
|
+
if (!validUsage(message.usage))
|
|
29
|
+
continue;
|
|
30
|
+
request = {
|
|
31
|
+
id: message.usage.requestId ?? `${entry.id}:${entry.timestamp}:${message.provider}/${message.model}`,
|
|
32
|
+
purpose: "conversation",
|
|
33
|
+
provider: message.provider,
|
|
34
|
+
model: message.responseModel ?? message.model,
|
|
35
|
+
timestamp: Number.isFinite(Date.parse(entry.timestamp)) ? Date.parse(entry.timestamp) : message.timestamp,
|
|
36
|
+
usage: message.usage,
|
|
37
|
+
stopReason: message.stopReason,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
else if (entry.type === "custom" && entry.customType === REQUEST_USAGE_TYPE) {
|
|
41
|
+
const data = entry.data;
|
|
42
|
+
if (validRequest(data))
|
|
43
|
+
request = data;
|
|
44
|
+
}
|
|
45
|
+
if (request && validRequest(request))
|
|
46
|
+
requests.set(request.id, request);
|
|
47
|
+
}
|
|
48
|
+
return [...requests.values()];
|
|
49
|
+
}
|
|
50
|
+
export function totalRequestUsage(requests) {
|
|
51
|
+
const total = emptyUsageTotals();
|
|
52
|
+
total.requests = requests.length;
|
|
53
|
+
for (const { usage } of requests) {
|
|
54
|
+
total.input += usage.input;
|
|
55
|
+
total.output += usage.output;
|
|
56
|
+
total.cacheRead += usage.cacheRead;
|
|
57
|
+
total.cacheWrite += usage.cacheWrite;
|
|
58
|
+
total.total += usageTokens(usage);
|
|
59
|
+
total.cost += usage.cost.total;
|
|
60
|
+
total.unknownRequests += Number(usage.measurement === "unknown");
|
|
61
|
+
total.partialRequests += Number(usage.measurement === "partial");
|
|
62
|
+
total.unknownCosts += Number(usage.costSource === "unknown");
|
|
63
|
+
}
|
|
64
|
+
return total;
|
|
65
|
+
}
|
|
66
|
+
function record(value) {
|
|
67
|
+
return value !== null && typeof value === "object" ? value : undefined;
|
|
68
|
+
}
|
|
69
|
+
function amount(value) {
|
|
70
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
|
|
71
|
+
}
|
|
72
|
+
function validUsage(value) {
|
|
73
|
+
const usage = record(value);
|
|
74
|
+
const cost = record(usage?.cost);
|
|
75
|
+
return Boolean(usage &&
|
|
76
|
+
cost &&
|
|
77
|
+
[usage.input, usage.output, usage.cacheRead, usage.cacheWrite, cost.total].every((amount) => typeof amount === "number" && Number.isFinite(amount) && amount >= 0));
|
|
78
|
+
}
|
|
79
|
+
function validRequest(value) {
|
|
80
|
+
const request = record(value);
|
|
81
|
+
return Boolean(request &&
|
|
82
|
+
typeof request.id === "string" &&
|
|
83
|
+
request.id &&
|
|
84
|
+
validUsage(request.usage) &&
|
|
85
|
+
typeof request.timestamp === "number" &&
|
|
86
|
+
Number.isFinite(request.timestamp));
|
|
87
|
+
}
|
|
88
|
+
/** Request IDs deduplicate completion, wait, fallback, resume, and inherited receipts. Legacy receipts use cumulative counters. */
|
|
89
|
+
export function collectChildUsage(entries) {
|
|
90
|
+
const children = new Map();
|
|
91
|
+
const inherited = new Map();
|
|
92
|
+
const requestGroups = new Map();
|
|
93
|
+
const inheritedRequestIds = new Set();
|
|
94
|
+
const requestOwners = new Map();
|
|
95
|
+
for (const entry of entries) {
|
|
96
|
+
const details = entry.type === "custom_message" && ["subagent-notify", "subagent-slash-result"].includes(entry.customType)
|
|
97
|
+
? record(entry.details)
|
|
98
|
+
: entry.type === "message" &&
|
|
99
|
+
entry.message.role === "toolResult" &&
|
|
100
|
+
entry.message.toolName?.startsWith("subagent")
|
|
101
|
+
? record(entry.message.details)
|
|
102
|
+
: undefined;
|
|
103
|
+
if (!details)
|
|
104
|
+
continue;
|
|
105
|
+
const slash = record(record(details.result)?.details);
|
|
106
|
+
const candidates = details.children ?? slash?.results ?? details.results;
|
|
107
|
+
if (!Array.isArray(candidates))
|
|
108
|
+
continue;
|
|
109
|
+
for (const candidate of candidates) {
|
|
110
|
+
const child = record(candidate);
|
|
111
|
+
const usage = record(child?.usage);
|
|
112
|
+
if (!child || !usage)
|
|
113
|
+
continue;
|
|
114
|
+
const identity = child.sessionFile ?? child.childId ?? child.id;
|
|
115
|
+
if (typeof identity !== "string" || !identity)
|
|
116
|
+
continue;
|
|
117
|
+
const snapshot = {
|
|
118
|
+
id: identity,
|
|
119
|
+
label: String(child.description ?? child.agent ?? "child"),
|
|
120
|
+
sessionFile: typeof child.sessionFile === "string" ? child.sessionFile : undefined,
|
|
121
|
+
input: amount(usage.input),
|
|
122
|
+
output: amount(usage.output),
|
|
123
|
+
cacheRead: amount(usage.cacheRead),
|
|
124
|
+
cacheWrite: amount(usage.cacheWrite),
|
|
125
|
+
cost: amount(usage.cost),
|
|
126
|
+
turns: amount(usage.turns),
|
|
127
|
+
unknownRequests: amount(usage.unknownRequests),
|
|
128
|
+
partialRequests: amount(usage.partialRequests),
|
|
129
|
+
unknownCosts: amount(usage.unknownCosts),
|
|
130
|
+
};
|
|
131
|
+
if (Array.isArray(child.usageRequests)) {
|
|
132
|
+
const requests = requestGroups.get(identity) ?? new Map();
|
|
133
|
+
requestGroups.set(identity, requests);
|
|
134
|
+
for (const request of child.usageRequests) {
|
|
135
|
+
if (!validRequest(request))
|
|
136
|
+
continue;
|
|
137
|
+
if (entry.inherited)
|
|
138
|
+
inheritedRequestIds.add(request.id);
|
|
139
|
+
else if (!requestOwners.has(request.id) || requestOwners.get(request.id) === identity) {
|
|
140
|
+
requestOwners.set(request.id, identity);
|
|
141
|
+
requests.set(request.id, request);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const target = entry.inherited ? inherited : children;
|
|
146
|
+
const previous = target.get(identity);
|
|
147
|
+
// A stale receipt must not undo a newer cumulative receipt.
|
|
148
|
+
if (!previous || usageTokens(snapshot) >= usageTokens(previous))
|
|
149
|
+
target.set(identity, snapshot);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return [...children.values()].map((child) => {
|
|
153
|
+
const requests = requestGroups.get(child.id);
|
|
154
|
+
if (requests) {
|
|
155
|
+
const ownRequests = [...requests.values()].filter((request) => !inheritedRequestIds.has(request.id));
|
|
156
|
+
const total = totalRequestUsage(ownRequests);
|
|
157
|
+
return {
|
|
158
|
+
...child,
|
|
159
|
+
input: total.input,
|
|
160
|
+
output: total.output,
|
|
161
|
+
cacheRead: total.cacheRead,
|
|
162
|
+
cacheWrite: total.cacheWrite,
|
|
163
|
+
cost: total.cost,
|
|
164
|
+
turns: ownRequests.length,
|
|
165
|
+
unknownRequests: total.unknownRequests,
|
|
166
|
+
partialRequests: total.partialRequests,
|
|
167
|
+
unknownCosts: total.unknownCosts,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const baseline = inherited.get(child.id);
|
|
171
|
+
if (!baseline)
|
|
172
|
+
return child;
|
|
173
|
+
const own = { ...child };
|
|
174
|
+
for (const key of [
|
|
175
|
+
"input",
|
|
176
|
+
"output",
|
|
177
|
+
"cacheRead",
|
|
178
|
+
"cacheWrite",
|
|
179
|
+
"cost",
|
|
180
|
+
"turns",
|
|
181
|
+
"unknownRequests",
|
|
182
|
+
"partialRequests",
|
|
183
|
+
"unknownCosts",
|
|
184
|
+
])
|
|
185
|
+
own[key] = Math.max(0, child[key] - baseline[key]);
|
|
186
|
+
return own;
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
export function totalChildUsage(children) {
|
|
190
|
+
const totals = emptyUsageTotals();
|
|
191
|
+
for (const child of children) {
|
|
192
|
+
totals.requests += child.turns;
|
|
193
|
+
totals.input += child.input;
|
|
194
|
+
totals.output += child.output;
|
|
195
|
+
totals.cacheRead += child.cacheRead;
|
|
196
|
+
totals.cacheWrite += child.cacheWrite;
|
|
197
|
+
totals.total += usageTokens(child);
|
|
198
|
+
totals.cost += child.cost;
|
|
199
|
+
totals.unknownRequests += child.unknownRequests;
|
|
200
|
+
totals.partialRequests += child.partialRequests;
|
|
201
|
+
totals.unknownCosts += child.unknownCosts;
|
|
202
|
+
}
|
|
203
|
+
return totals;
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=usage-accounting.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usage-accounting.js","sourceRoot":"","sources":["../../src/core/usage-accounting.ts"],"names":[],"mappings":"AAGA,MAAM,CAAC,MAAM,kBAAkB,GAAG,eAAe,CAAC;AAyBlD,MAAM,UAAU,gBAAgB,GAAgB;IAC/C,OAAO;QACN,QAAQ,EAAE,CAAC;QACX,KAAK,EAAE,CAAC;QACR,MAAM,EAAE,CAAC;QACT,SAAS,EAAE,CAAC;QACZ,UAAU,EAAE,CAAC;QACb,KAAK,EAAE,CAAC;QACR,IAAI,EAAE,CAAC;QACP,eAAe,EAAE,CAAC;QAClB,eAAe,EAAE,CAAC;QAClB,YAAY,EAAE,CAAC;KACf,CAAC;AAAA,CACF;AAED,MAAM,UAAU,WAAW,CAAC,KAAmE,EAAU;IACxG,OAAO,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,MAAM,GAAG,KAAK,CAAC,SAAS,GAAG,KAAK,CAAC,UAAU,CAAC;AAAA,CACvE;AAED,4EAA4E;AAC5E,MAAM,UAAU,oBAAoB,CAAC,OAAgC,EAAsB;IAC1F,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA4B,CAAC;IACrD,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,KAAK,CAAC,SAAS;YAAE,SAAS;QAC9B,IAAI,OAAqC,CAAC;QAC1C,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;YACpE,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;YAC9B,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC;gBAAE,SAAS;YACzC,OAAO,GAAG;gBACT,EAAE,EAAE,OAAO,CAAC,KAAK,CAAC,SAAS,IAAI,GAAG,KAAK,CAAC,EAAE,IAAI,KAAK,CAAC,SAAS,IAAI,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,KAAK,EAAE;gBACpG,OAAO,EAAE,cAAc;gBACvB,QAAQ,EAAE,OAAO,CAAC,QAAQ;gBAC1B,KAAK,EAAE,OAAO,CAAC,aAAa,IAAI,OAAO,CAAC,KAAK;gBAC7C,SAAS,EAAE,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS;gBACzG,KAAK,EAAE,OAAO,CAAC,KAAK;gBACpB,UAAU,EAAE,OAAO,CAAC,UAAU;aAC9B,CAAC;QACH,CAAC;aAAM,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,UAAU,KAAK,kBAAkB,EAAE,CAAC;YAC/E,MAAM,IAAI,GAAG,KAAK,CAAC,IAAoC,CAAC;YACxD,IAAI,YAAY,CAAC,IAAI,CAAC;gBAAE,OAAO,GAAG,IAAI,CAAC;QACxC,CAAC;QACD,IAAI,OAAO,IAAI,YAAY,CAAC,OAAO,CAAC;YAAE,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;IACzE,CAAC;IACD,OAAO,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;AAAA,CAC9B;AAED,MAAM,UAAU,iBAAiB,CAAC,QAAqC,EAAe;IACrF,MAAM,KAAK,GAAG,gBAAgB,EAAE,CAAC;IACjC,KAAK,CAAC,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC;IACjC,KAAK,MAAM,EAAE,KAAK,EAAE,IAAI,QAAQ,EAAE,CAAC;QAClC,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,KAAK,CAAC;QAC3B,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC;QAC7B,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,SAAS,CAAC;QACnC,KAAK,CAAC,UAAU,IAAI,KAAK,CAAC,UAAU,CAAC;QACrC,KAAK,CAAC,KAAK,IAAI,WAAW,CAAC,KAAK,CAAC,CAAC;QAClC,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC;QAC/B,KAAK,CAAC,eAAe,IAAI,MAAM,CAAC,KAAK,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC;QACjE,KAAK,CAAC,eAAe,IAAI,MAAM,CAAC,KAAK,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC;QACjE,KAAK,CAAC,YAAY,IAAI,MAAM,CAAC,KAAK,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC;IAC9D,CAAC;IACD,OAAO,KAAK,CAAC;AAAA,CACb;AAiBD,SAAS,MAAM,CAAC,KAAc,EAAuC;IACpE,OAAO,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAE,KAAiC,CAAC,CAAC,CAAC,SAAS,CAAC;AAAA,CACpG;AACD,SAAS,MAAM,CAAC,KAAc,EAAU;IACvC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AAAA,CACrF;AAED,SAAS,UAAU,CAAC,KAAc,EAAkB;IACnD,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAC5B,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IACjC,OAAO,OAAO,CACb,KAAK;QACJ,IAAI;QACJ,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,UAAU,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAC/E,CAAC,MAAM,EAAE,EAAE,CAAC,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,MAAM,IAAI,CAAC,CAChF,CACF,CAAC;AAAA,CACF;AAED,SAAS,YAAY,CAAC,KAAc,EAA6B;IAChE,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAC9B,OAAO,OAAO,CACb,OAAO;QACN,OAAO,OAAO,CAAC,EAAE,KAAK,QAAQ;QAC9B,OAAO,CAAC,EAAE;QACV,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC;QACzB,OAAO,OAAO,CAAC,SAAS,KAAK,QAAQ;QACrC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,SAAS,CAAC,CACnC,CAAC;AAAA,CACF;AACD,mIAAmI;AACnI,MAAM,UAAU,iBAAiB,CAAC,OAAgC,EAAwB;IACzF,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA8B,CAAC;IACvD,MAAM,SAAS,GAAG,IAAI,GAAG,EAA8B,CAAC;IACxD,MAAM,aAAa,GAAG,IAAI,GAAG,EAAyC,CAAC;IACvE,MAAM,mBAAmB,GAAG,IAAI,GAAG,EAAU,CAAC;IAC9C,MAAM,aAAa,GAAG,IAAI,GAAG,EAAkB,CAAC;IAChD,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,OAAO,GACZ,KAAK,CAAC,IAAI,KAAK,gBAAgB,IAAI,CAAC,iBAAiB,EAAE,uBAAuB,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,UAAU,CAAC;YACzG,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC;YACvB,CAAC,CAAC,KAAK,CAAC,IAAI,KAAK,SAAS;gBACxB,KAAK,CAAC,OAAO,CAAC,IAAI,KAAK,YAAY;gBACnC,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,UAAU,CAAC,UAAU,CAAC;gBAC/C,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;gBAC/B,CAAC,CAAC,SAAS,CAAC;QACf,IAAI,CAAC,OAAO;YAAE,SAAS;QACvB,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC;QACtD,MAAM,UAAU,GAAG,OAAO,CAAC,QAAQ,IAAI,KAAK,EAAE,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC;QACzE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC;YAAE,SAAS;QACzC,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;YACpC,MAAM,KAAK,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC;YAChC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;YACnC,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK;gBAAE,SAAS;YAC/B,MAAM,QAAQ,GAAG,KAAK,CAAC,WAAW,IAAI,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,EAAE,CAAC;YAChE,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,CAAC,QAAQ;gBAAE,SAAS;YACxD,MAAM,QAAQ,GAAuB;gBACpC,EAAE,EAAE,QAAQ;gBACZ,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,WAAW,IAAI,KAAK,CAAC,KAAK,IAAI,OAAO,CAAC;gBAC1D,WAAW,EAAE,OAAO,KAAK,CAAC,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS;gBAClF,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC;gBAC1B,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC;gBAC5B,SAAS,EAAE,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC;gBAClC,UAAU,EAAE,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC;gBACpC,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC;gBACxB,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC;gBAC1B,eAAe,EAAE,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC;gBAC9C,eAAe,EAAE,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC;gBAC9C,YAAY,EAAE,MAAM,CAAC,KAAK,CAAC,YAAY,CAAC;aACxC,CAAC;YACF,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,EAAE,CAAC;gBACxC,MAAM,QAAQ,GAAG,aAAa,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,IAAI,GAAG,EAA4B,CAAC;gBACpF,aAAa,CAAC,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;gBACtC,KAAK,MAAM,OAAO,IAAI,KAAK,CAAC,aAAa,EAAE,CAAC;oBAC3C,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC;wBAAE,SAAS;oBACrC,IAAI,KAAK,CAAC,SAAS;wBAAE,mBAAmB,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;yBACpD,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,IAAI,aAAa,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,QAAQ,EAAE,CAAC;wBACvF,aAAa,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC;wBACxC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;oBACnC,CAAC;gBACF,CAAC;YACF,CAAC;YACD,MAAM,MAAM,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC;YACtD,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YACtC,4DAA4D;YAC5D,IAAI,CAAC,QAAQ,IAAI,WAAW,CAAC,QAAQ,CAAC,IAAI,WAAW,CAAC,QAAQ,CAAC;gBAAE,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QACjG,CAAC;IACF,CAAC;IACD,OAAO,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC;QAC5C,MAAM,QAAQ,GAAG,aAAa,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAC7C,IAAI,QAAQ,EAAE,CAAC;YACd,MAAM,WAAW,GAAG,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,mBAAmB,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC;YACrG,MAAM,KAAK,GAAG,iBAAiB,CAAC,WAAW,CAAC,CAAC;YAC7C,OAAO;gBACN,GAAG,KAAK;gBACR,KAAK,EAAE,KAAK,CAAC,KAAK;gBAClB,MAAM,EAAE,KAAK,CAAC,MAAM;gBACpB,SAAS,EAAE,KAAK,CAAC,SAAS;gBAC1B,UAAU,EAAE,KAAK,CAAC,UAAU;gBAC5B,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,KAAK,EAAE,WAAW,CAAC,MAAM;gBACzB,eAAe,EAAE,KAAK,CAAC,eAAe;gBACtC,eAAe,EAAE,KAAK,CAAC,eAAe;gBACtC,YAAY,EAAE,KAAK,CAAC,YAAY;aAChC,CAAC;QACH,CAAC;QACD,MAAM,QAAQ,GAAG,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACzC,IAAI,CAAC,QAAQ;YAAE,OAAO,KAAK,CAAC;QAC5B,MAAM,GAAG,GAAG,EAAE,GAAG,KAAK,EAAE,CAAC;QACzB,KAAK,MAAM,GAAG,IAAI;YACjB,OAAO;YACP,QAAQ;YACR,WAAW;YACX,YAAY;YACZ,MAAM;YACN,OAAO;YACP,iBAAiB;YACjB,iBAAiB;YACjB,cAAc;SACL;YACT,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;QACpD,OAAO,GAAG,CAAC;IAAA,CACX,CAAC,CAAC;AAAA,CACH;AAED,MAAM,UAAU,eAAe,CAAC,QAAuC,EAAe;IACrF,MAAM,MAAM,GAAG,gBAAgB,EAAE,CAAC;IAClC,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;QAC9B,MAAM,CAAC,QAAQ,IAAI,KAAK,CAAC,KAAK,CAAC;QAC/B,MAAM,CAAC,KAAK,IAAI,KAAK,CAAC,KAAK,CAAC;QAC5B,MAAM,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC;QAC9B,MAAM,CAAC,SAAS,IAAI,KAAK,CAAC,SAAS,CAAC;QACpC,MAAM,CAAC,UAAU,IAAI,KAAK,CAAC,UAAU,CAAC;QACtC,MAAM,CAAC,KAAK,IAAI,WAAW,CAAC,KAAK,CAAC,CAAC;QACnC,MAAM,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC;QAC1B,MAAM,CAAC,eAAe,IAAI,KAAK,CAAC,eAAe,CAAC;QAChD,MAAM,CAAC,eAAe,IAAI,KAAK,CAAC,eAAe,CAAC;QAChD,MAAM,CAAC,YAAY,IAAI,KAAK,CAAC,YAAY,CAAC;IAC3C,CAAC;IACD,OAAO,MAAM,CAAC;AAAA,CACd","sourcesContent":["import type { AssistantMessage, Usage } from \"@ashx-j/lunr-ai\";\nimport type { SessionEntry } from \"./session-manager.ts\";\n\nexport const REQUEST_USAGE_TYPE = \"request-usage\";\n\nexport interface AccountedRequest {\n\tid: string;\n\tpurpose: string;\n\tprovider: string;\n\tmodel: string;\n\ttimestamp: number;\n\tusage: Usage;\n\tstopReason: AssistantMessage[\"stopReason\"];\n}\n\nexport interface UsageTotals {\n\trequests: number;\n\tinput: number;\n\toutput: number;\n\tcacheRead: number;\n\tcacheWrite: number;\n\ttotal: number;\n\tcost: number;\n\tunknownRequests: number;\n\tpartialRequests: number;\n\tunknownCosts: number;\n}\n\nexport function emptyUsageTotals(): UsageTotals {\n\treturn {\n\t\trequests: 0,\n\t\tinput: 0,\n\t\toutput: 0,\n\t\tcacheRead: 0,\n\t\tcacheWrite: 0,\n\t\ttotal: 0,\n\t\tcost: 0,\n\t\tunknownRequests: 0,\n\t\tpartialRequests: 0,\n\t\tunknownCosts: 0,\n\t};\n}\n\nexport function usageTokens(usage: Pick<Usage, \"input\" | \"output\" | \"cacheRead\" | \"cacheWrite\">): number {\n\treturn usage.input + usage.output + usage.cacheRead + usage.cacheWrite;\n}\n\n/** All incurred requests, independent of the active conversation branch. */\nexport function collectUsageRequests(entries: readonly SessionEntry[]): AccountedRequest[] {\n\tconst requests = new Map<string, AccountedRequest>();\n\tfor (const entry of entries) {\n\t\tif (entry.inherited) continue;\n\t\tlet request: AccountedRequest | undefined;\n\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\tconst message = entry.message;\n\t\t\tif (!validUsage(message.usage)) continue;\n\t\t\trequest = {\n\t\t\t\tid: message.usage.requestId ?? `${entry.id}:${entry.timestamp}:${message.provider}/${message.model}`,\n\t\t\t\tpurpose: \"conversation\",\n\t\t\t\tprovider: message.provider,\n\t\t\t\tmodel: message.responseModel ?? message.model,\n\t\t\t\ttimestamp: Number.isFinite(Date.parse(entry.timestamp)) ? Date.parse(entry.timestamp) : message.timestamp,\n\t\t\t\tusage: message.usage,\n\t\t\t\tstopReason: message.stopReason,\n\t\t\t};\n\t\t} else if (entry.type === \"custom\" && entry.customType === REQUEST_USAGE_TYPE) {\n\t\t\tconst data = entry.data as AccountedRequest | undefined;\n\t\t\tif (validRequest(data)) request = data;\n\t\t}\n\t\tif (request && validRequest(request)) requests.set(request.id, request);\n\t}\n\treturn [...requests.values()];\n}\n\nexport function totalRequestUsage(requests: readonly AccountedRequest[]): UsageTotals {\n\tconst total = emptyUsageTotals();\n\ttotal.requests = requests.length;\n\tfor (const { usage } of requests) {\n\t\ttotal.input += usage.input;\n\t\ttotal.output += usage.output;\n\t\ttotal.cacheRead += usage.cacheRead;\n\t\ttotal.cacheWrite += usage.cacheWrite;\n\t\ttotal.total += usageTokens(usage);\n\t\ttotal.cost += usage.cost.total;\n\t\ttotal.unknownRequests += Number(usage.measurement === \"unknown\");\n\t\ttotal.partialRequests += Number(usage.measurement === \"partial\");\n\t\ttotal.unknownCosts += Number(usage.costSource === \"unknown\");\n\t}\n\treturn total;\n}\n\nexport interface ChildUsageSnapshot {\n\tid: string;\n\tlabel: string;\n\tsessionFile?: string;\n\tinput: number;\n\toutput: number;\n\tcacheRead: number;\n\tcacheWrite: number;\n\tcost: number;\n\tturns: number;\n\tunknownRequests: number;\n\tpartialRequests: number;\n\tunknownCosts: number;\n}\n\nfunction record(value: unknown): Record<string, unknown> | undefined {\n\treturn value !== null && typeof value === \"object\" ? (value as Record<string, unknown>) : undefined;\n}\nfunction amount(value: unknown): number {\n\treturn typeof value === \"number\" && Number.isFinite(value) && value >= 0 ? value : 0;\n}\n\nfunction validUsage(value: unknown): value is Usage {\n\tconst usage = record(value);\n\tconst cost = record(usage?.cost);\n\treturn Boolean(\n\t\tusage &&\n\t\t\tcost &&\n\t\t\t[usage.input, usage.output, usage.cacheRead, usage.cacheWrite, cost.total].every(\n\t\t\t\t(amount) => typeof amount === \"number\" && Number.isFinite(amount) && amount >= 0,\n\t\t\t),\n\t);\n}\n\nfunction validRequest(value: unknown): value is AccountedRequest {\n\tconst request = record(value);\n\treturn Boolean(\n\t\trequest &&\n\t\t\ttypeof request.id === \"string\" &&\n\t\t\trequest.id &&\n\t\t\tvalidUsage(request.usage) &&\n\t\t\ttypeof request.timestamp === \"number\" &&\n\t\t\tNumber.isFinite(request.timestamp),\n\t);\n}\n/** Request IDs deduplicate completion, wait, fallback, resume, and inherited receipts. Legacy receipts use cumulative counters. */\nexport function collectChildUsage(entries: readonly SessionEntry[]): ChildUsageSnapshot[] {\n\tconst children = new Map<string, ChildUsageSnapshot>();\n\tconst inherited = new Map<string, ChildUsageSnapshot>();\n\tconst requestGroups = new Map<string, Map<string, AccountedRequest>>();\n\tconst inheritedRequestIds = new Set<string>();\n\tconst requestOwners = new Map<string, string>();\n\tfor (const entry of entries) {\n\t\tconst details =\n\t\t\tentry.type === \"custom_message\" && [\"subagent-notify\", \"subagent-slash-result\"].includes(entry.customType)\n\t\t\t\t? record(entry.details)\n\t\t\t\t: entry.type === \"message\" &&\n\t\t\t\t\t\tentry.message.role === \"toolResult\" &&\n\t\t\t\t\t\tentry.message.toolName?.startsWith(\"subagent\")\n\t\t\t\t\t? record(entry.message.details)\n\t\t\t\t\t: undefined;\n\t\tif (!details) continue;\n\t\tconst slash = record(record(details.result)?.details);\n\t\tconst candidates = details.children ?? slash?.results ?? details.results;\n\t\tif (!Array.isArray(candidates)) continue;\n\t\tfor (const candidate of candidates) {\n\t\t\tconst child = record(candidate);\n\t\t\tconst usage = record(child?.usage);\n\t\t\tif (!child || !usage) continue;\n\t\t\tconst identity = child.sessionFile ?? child.childId ?? child.id;\n\t\t\tif (typeof identity !== \"string\" || !identity) continue;\n\t\t\tconst snapshot: ChildUsageSnapshot = {\n\t\t\t\tid: identity,\n\t\t\t\tlabel: String(child.description ?? child.agent ?? \"child\"),\n\t\t\t\tsessionFile: typeof child.sessionFile === \"string\" ? child.sessionFile : undefined,\n\t\t\t\tinput: amount(usage.input),\n\t\t\t\toutput: amount(usage.output),\n\t\t\t\tcacheRead: amount(usage.cacheRead),\n\t\t\t\tcacheWrite: amount(usage.cacheWrite),\n\t\t\t\tcost: amount(usage.cost),\n\t\t\t\tturns: amount(usage.turns),\n\t\t\t\tunknownRequests: amount(usage.unknownRequests),\n\t\t\t\tpartialRequests: amount(usage.partialRequests),\n\t\t\t\tunknownCosts: amount(usage.unknownCosts),\n\t\t\t};\n\t\t\tif (Array.isArray(child.usageRequests)) {\n\t\t\t\tconst requests = requestGroups.get(identity) ?? new Map<string, AccountedRequest>();\n\t\t\t\trequestGroups.set(identity, requests);\n\t\t\t\tfor (const request of child.usageRequests) {\n\t\t\t\t\tif (!validRequest(request)) continue;\n\t\t\t\t\tif (entry.inherited) inheritedRequestIds.add(request.id);\n\t\t\t\t\telse if (!requestOwners.has(request.id) || requestOwners.get(request.id) === identity) {\n\t\t\t\t\t\trequestOwners.set(request.id, identity);\n\t\t\t\t\t\trequests.set(request.id, request);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst target = entry.inherited ? inherited : children;\n\t\t\tconst previous = target.get(identity);\n\t\t\t// A stale receipt must not undo a newer cumulative receipt.\n\t\t\tif (!previous || usageTokens(snapshot) >= usageTokens(previous)) target.set(identity, snapshot);\n\t\t}\n\t}\n\treturn [...children.values()].map((child) => {\n\t\tconst requests = requestGroups.get(child.id);\n\t\tif (requests) {\n\t\t\tconst ownRequests = [...requests.values()].filter((request) => !inheritedRequestIds.has(request.id));\n\t\t\tconst total = totalRequestUsage(ownRequests);\n\t\t\treturn {\n\t\t\t\t...child,\n\t\t\t\tinput: total.input,\n\t\t\t\toutput: total.output,\n\t\t\t\tcacheRead: total.cacheRead,\n\t\t\t\tcacheWrite: total.cacheWrite,\n\t\t\t\tcost: total.cost,\n\t\t\t\tturns: ownRequests.length,\n\t\t\t\tunknownRequests: total.unknownRequests,\n\t\t\t\tpartialRequests: total.partialRequests,\n\t\t\t\tunknownCosts: total.unknownCosts,\n\t\t\t};\n\t\t}\n\t\tconst baseline = inherited.get(child.id);\n\t\tif (!baseline) return child;\n\t\tconst own = { ...child };\n\t\tfor (const key of [\n\t\t\t\"input\",\n\t\t\t\"output\",\n\t\t\t\"cacheRead\",\n\t\t\t\"cacheWrite\",\n\t\t\t\"cost\",\n\t\t\t\"turns\",\n\t\t\t\"unknownRequests\",\n\t\t\t\"partialRequests\",\n\t\t\t\"unknownCosts\",\n\t\t] as const)\n\t\t\town[key] = Math.max(0, child[key] - baseline[key]);\n\t\treturn own;\n\t});\n}\n\nexport function totalChildUsage(children: readonly ChildUsageSnapshot[]): UsageTotals {\n\tconst totals = emptyUsageTotals();\n\tfor (const child of children) {\n\t\ttotals.requests += child.turns;\n\t\ttotals.input += child.input;\n\t\ttotals.output += child.output;\n\t\ttotals.cacheRead += child.cacheRead;\n\t\ttotals.cacheWrite += child.cacheWrite;\n\t\ttotals.total += usageTokens(child);\n\t\ttotals.cost += child.cost;\n\t\ttotals.unknownRequests += child.unknownRequests;\n\t\ttotals.partialRequests += child.partialRequests;\n\t\ttotals.unknownCosts += child.unknownCosts;\n\t}\n\treturn totals;\n}\n"]}
|
|
@@ -1,20 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* lunr: 30-day token usage history for the /usage command.
|
|
3
|
-
*
|
|
4
|
-
* Scans the on-disk session files (`.jsonl` under `getSessionsDir()`, both the
|
|
5
|
-
* flat and the per-project-subdirectory layouts) and aggregates:
|
|
6
|
-
* - REAL token totals per provider/model from assistant-message `usage`
|
|
7
|
-
* metadata (same fields the live session rows use),
|
|
8
|
-
* - per-day totals (YYYY-MM-DD buckets from the entry timestamp),
|
|
9
|
-
* - an ESTIMATED category breakdown (chars/4) via the same accounting as
|
|
10
|
-
* `computeContextBreakdown` — session files don't store the system prompt or
|
|
11
|
-
* tool definitions, so `includesSystemPrompt` is always false and the
|
|
12
|
-
* message categories carry the whole breakdown.
|
|
13
|
-
*
|
|
14
|
-
* Results are cached per file keyed on mtime+size; unchanged files are not
|
|
15
|
-
* re-parsed. Never throws — corrupt files are skipped, total failure returns
|
|
16
|
-
* an empty aggregate.
|
|
17
|
-
*/
|
|
18
1
|
export interface UsageHistoryModelRow {
|
|
19
2
|
model: string;
|
|
20
3
|
input: number;
|
|
@@ -53,7 +36,7 @@ export interface UsageHistory {
|
|
|
53
36
|
/** Test hook: drop all cached per-file aggregates. */
|
|
54
37
|
export declare function resetUsageHistoryCache(): void;
|
|
55
38
|
/**
|
|
56
|
-
* Aggregate
|
|
39
|
+
* Aggregate requests since `sinceMs`; file modification time only narrows the scan.
|
|
57
40
|
* Never throws; on total failure returns an empty aggregate.
|
|
58
41
|
*/
|
|
59
42
|
export declare function collectUsageHistory(options: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usage-history.d.ts","sourceRoot":"","sources":["../../src/core/usage-history.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAcH,MAAM,WAAW,oBAAoB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,kBAAkB;IAClC,wBAAwB;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,sBAAsB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,YAAY;IAC5B,QAAQ,EAAE,oBAAoB,EAAE,CAAC;IACjC,MAAM,EAAE,kBAAkB,EAAE,CAAC;IAC7B,UAAU,EAAE,sBAAsB,CAAC;IACnC,6EAA6E;IAC7E,oBAAoB,EAAE,OAAO,CAAC;IAC9B,gEAA8D;IAC9D,YAAY,EAAE,MAAM,CAAC;IACrB,kFAAkF;IAClF,WAAW,EAAE,MAAM,CAAC;IACpB,0EAA0E;IAC1E,iBAAiB,EAAE,MAAM,CAAC;CAC1B;AAaD,sDAAsD;AACtD,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C;AAoID;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,YAAY,CA2EpG","sourcesContent":["/**\n * lunr: 30-day token usage history for the /usage command.\n *\n * Scans the on-disk session files (`.jsonl` under `getSessionsDir()`, both the\n * flat and the per-project-subdirectory layouts) and aggregates:\n * - REAL token totals per provider/model from assistant-message `usage`\n * metadata (same fields the live session rows use),\n * - per-day totals (YYYY-MM-DD buckets from the entry timestamp),\n * - an ESTIMATED category breakdown (chars/4) via the same accounting as\n * `computeContextBreakdown` — session files don't store the system prompt or\n * tool definitions, so `includesSystemPrompt` is always false and the\n * message categories carry the whole breakdown.\n *\n * Results are cached per file keyed on mtime+size; unchanged files are not\n * re-parsed. Never throws — corrupt files are skipped, total failure returns\n * an empty aggregate.\n */\n\nimport { type Dirent, readdirSync, type Stats, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { AssistantMessage } from \"@ashx-j/lunr-ai/compat\";\nimport { getSessionsDir } from \"../config.ts\";\nimport { computeContextBreakdown } from \"./context-breakdown.ts\";\nimport {\n\ttype FileEntry,\n\tloadEntriesFromFile,\n\ttype SessionEntry,\n\tsessionEntryToContextMessages,\n} from \"./session-manager.ts\";\n\nexport interface UsageHistoryModelRow {\n\tmodel: string;\n\tinput: number;\n\toutput: number;\n\tcacheRead: number;\n\tcacheWrite: number;\n\ttotal: number;\n}\n\nexport interface UsageHistoryDayRow {\n\t/** YYYY-MM-DD (UTC). */\n\tday: string;\n\ttotal: number;\n}\n\nexport interface UsageHistoryCategories {\n\tuser: number;\n\tassistantText: number;\n\tthinking: number;\n\ttoolCalls: number;\n\ttoolResults: number;\n\tsummaries: number;\n\ttotal: number;\n}\n\nexport interface UsageHistory {\n\tperModel: UsageHistoryModelRow[];\n\tperDay: UsageHistoryDayRow[];\n\tcategories: UsageHistoryCategories;\n\t/** False: session files don't store the system prompt / tool definitions. */\n\tincludesSystemPrompt: boolean;\n\t/** Session files inside the scan window (mtime ≥ sinceMs). */\n\tfilesScanned: number;\n\t/** Files actually parsed this call (cache misses). Also serves as a test hook. */\n\tfilesParsed: number;\n\t/** Scanned files containing at least one assistant message with usage. */\n\tsessionsWithUsage: number;\n}\n\ninterface FileAggregate {\n\tmtimeMs: number;\n\tsize: number;\n\tperModel: Map<string, UsageHistoryModelRow>;\n\tperDay: Map<string, number>;\n\tcategories: UsageHistoryCategories;\n\thasUsage: boolean;\n}\n\nconst fileCache = new Map<string, FileAggregate>();\n\n/** Test hook: drop all cached per-file aggregates. */\nexport function resetUsageHistoryCache(): void {\n\tfileCache.clear();\n}\n\nfunction emptyCategories(): UsageHistoryCategories {\n\treturn { user: 0, assistantText: 0, thinking: 0, toolCalls: 0, toolResults: 0, summaries: 0, total: 0 };\n}\n\nfunction emptyHistory(): UsageHistory {\n\treturn {\n\t\tperModel: [],\n\t\tperDay: [],\n\t\tcategories: emptyCategories(),\n\t\tincludesSystemPrompt: false,\n\t\tfilesScanned: 0,\n\t\tfilesParsed: 0,\n\t\tsessionsWithUsage: 0,\n\t};\n}\n\n/** Enumerate `.jsonl` session files in both layouts: flat and per-project subdirectories. */\nfunction enumerateSessionFiles(sessionsDir: string): string[] {\n\tconst files: string[] = [];\n\tlet dirEntries: Dirent[];\n\ttry {\n\t\tdirEntries = readdirSync(sessionsDir, { withFileTypes: true });\n\t} catch {\n\t\treturn files;\n\t}\n\tfor (const entry of dirEntries) {\n\t\tif (entry.isFile() && entry.name.endsWith(\".jsonl\")) {\n\t\t\tfiles.push(join(sessionsDir, entry.name));\n\t\t} else if (entry.isDirectory()) {\n\t\t\ttry {\n\t\t\t\tconst subDir = join(sessionsDir, entry.name);\n\t\t\t\tfor (const name of readdirSync(subDir)) {\n\t\t\t\t\tif (name.endsWith(\".jsonl\")) files.push(join(subDir, name));\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Unreadable subdirectory — skip.\n\t\t\t}\n\t\t}\n\t}\n\treturn files;\n}\n\n/** Milliseconds for day bucketing: entry timestamp, then message timestamp, then file mtime. */\nfunction entryTimestampMs(entry: SessionEntry, message: AssistantMessage, fallbackMs: number): number {\n\tconst entryMs = Date.parse(entry.timestamp ?? \"\");\n\tif (!Number.isNaN(entryMs)) return entryMs;\n\tif (typeof message.timestamp === \"number\" && message.timestamp > 0) return message.timestamp;\n\treturn fallbackMs;\n}\n\n/** Parse one session file into its aggregate. Returns null when the file is unusable. */\nfunction parseSessionFile(filePath: string, mtimeMs: number, size: number): FileAggregate | null {\n\tlet entries: FileEntry[];\n\ttry {\n\t\tentries = loadEntriesFromFile(filePath);\n\t} catch {\n\t\treturn null;\n\t}\n\tif (entries.length === 0) return null;\n\n\tconst aggregate: FileAggregate = {\n\t\tmtimeMs,\n\t\tsize,\n\t\tperModel: new Map(),\n\t\tperDay: new Map(),\n\t\tcategories: emptyCategories(),\n\t\thasUsage: false,\n\t};\n\n\tconst sessionEntries: SessionEntry[] = [];\n\tfor (const entry of entries) {\n\t\tif (entry.type === \"session\") continue; // header\n\t\tsessionEntries.push(entry);\n\n\t\tif (entry.type !== \"message\" || entry.message.role !== \"assistant\") continue;\n\t\tconst message = entry.message as AssistantMessage;\n\t\tconst usage = message.usage;\n\t\tif (!usage || typeof usage.input !== \"number\" || typeof usage.output !== \"number\") continue;\n\n\t\tconst provider = typeof message.provider === \"string\" && message.provider ? message.provider : \"unknown\";\n\t\tconst modelId = message.responseModel ?? message.model;\n\t\tconst key = `${provider}/${typeof modelId === \"string\" && modelId ? modelId : \"unknown\"}`;\n\t\tconst input = usage.input;\n\t\tconst output = usage.output;\n\t\tconst cacheRead = typeof usage.cacheRead === \"number\" ? usage.cacheRead : 0;\n\t\tconst cacheWrite = typeof usage.cacheWrite === \"number\" ? usage.cacheWrite : 0;\n\n\t\tlet row = aggregate.perModel.get(key);\n\t\tif (!row) {\n\t\t\trow = { model: key, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 };\n\t\t\taggregate.perModel.set(key, row);\n\t\t}\n\t\trow.input += input;\n\t\trow.output += output;\n\t\trow.cacheRead += cacheRead;\n\t\trow.cacheWrite += cacheWrite;\n\t\trow.total += input + output + cacheRead + cacheWrite;\n\t\taggregate.hasUsage = true;\n\n\t\tconst day = new Date(entryTimestampMs(entry, message, mtimeMs)).toISOString().slice(0, 10);\n\t\taggregate.perDay.set(day, (aggregate.perDay.get(day) ?? 0) + input + output + cacheRead + cacheWrite);\n\t}\n\n\t// Category estimate over the file's messages (system prompt/tools are not\n\t// stored in session files, so message categories carry the breakdown).\n\ttry {\n\t\tconst messages = sessionEntries.flatMap(sessionEntryToContextMessages);\n\t\tconst breakdown = computeContextBreakdown({ systemPrompt: \"\", tools: [], messages, contextWindow: 0 });\n\t\taggregate.categories = {\n\t\t\tuser: breakdown.user,\n\t\t\tassistantText: breakdown.assistantText,\n\t\t\tthinking: breakdown.thinking,\n\t\t\ttoolCalls: breakdown.toolCalls,\n\t\t\ttoolResults: breakdown.toolResults,\n\t\t\tsummaries: breakdown.summaries,\n\t\t\ttotal:\n\t\t\t\tbreakdown.user +\n\t\t\t\tbreakdown.assistantText +\n\t\t\t\tbreakdown.thinking +\n\t\t\t\tbreakdown.toolCalls +\n\t\t\t\tbreakdown.toolResults +\n\t\t\t\tbreakdown.summaries,\n\t\t};\n\t} catch {\n\t\t// Category estimation is best-effort; per-model/day totals above still stand.\n\t}\n\n\treturn aggregate;\n}\n\n/**\n * Aggregate token usage across all session files modified since `sinceMs`.\n * Never throws; on total failure returns an empty aggregate.\n */\nexport function collectUsageHistory(options: { sinceMs: number; sessionsDir?: string }): UsageHistory {\n\tconst result = emptyHistory();\n\ttry {\n\t\tconst sessionsDir = options.sessionsDir ?? getSessionsDir();\n\t\tconst files = enumerateSessionFiles(sessionsDir);\n\t\tconst seen = new Set<string>();\n\n\t\tfor (const filePath of files) {\n\t\t\tseen.add(filePath);\n\t\t\tlet stats: Stats;\n\t\t\ttry {\n\t\t\t\tstats = statSync(filePath);\n\t\t\t} catch {\n\t\t\t\tcontinue; // vanished between readdir and stat\n\t\t\t}\n\t\t\tif (stats.mtimeMs < options.sinceMs) continue;\n\t\t\tresult.filesScanned++;\n\n\t\t\tlet aggregate = fileCache.get(filePath);\n\t\t\tif (!aggregate || aggregate.mtimeMs !== stats.mtimeMs || aggregate.size !== stats.size) {\n\t\t\t\taggregate = parseSessionFile(filePath, stats.mtimeMs, stats.size) ?? undefined;\n\t\t\t\tresult.filesParsed++;\n\t\t\t\tif (aggregate) {\n\t\t\t\t\tfileCache.set(filePath, aggregate);\n\t\t\t\t} else {\n\t\t\t\t\tfileCache.delete(filePath);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!aggregate) continue;\n\n\t\t\tif (aggregate.hasUsage) result.sessionsWithUsage++;\n\t\t\tfor (const row of aggregate.perModel.values()) {\n\t\t\t\tlet target = result.perModel.find((r) => r.model === row.model);\n\t\t\t\tif (!target) {\n\t\t\t\t\ttarget = { model: row.model, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 };\n\t\t\t\t\tresult.perModel.push(target);\n\t\t\t\t}\n\t\t\t\ttarget.input += row.input;\n\t\t\t\ttarget.output += row.output;\n\t\t\t\ttarget.cacheRead += row.cacheRead;\n\t\t\t\ttarget.cacheWrite += row.cacheWrite;\n\t\t\t\ttarget.total += row.total;\n\t\t\t}\n\t\t\tfor (const [day, total] of aggregate.perDay) {\n\t\t\t\tconst existing = result.perDay.find((r) => r.day === day);\n\t\t\t\tif (existing) {\n\t\t\t\t\texisting.total += total;\n\t\t\t\t} else {\n\t\t\t\t\tresult.perDay.push({ day, total });\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (const key of [\n\t\t\t\t\"user\",\n\t\t\t\t\"assistantText\",\n\t\t\t\t\"thinking\",\n\t\t\t\t\"toolCalls\",\n\t\t\t\t\"toolResults\",\n\t\t\t\t\"summaries\",\n\t\t\t\t\"total\",\n\t\t\t] as const) {\n\t\t\t\tresult.categories[key] += aggregate.categories[key];\n\t\t\t}\n\t\t}\n\n\t\t// Drop cache entries for deleted files.\n\t\tfor (const cachedPath of fileCache.keys()) {\n\t\t\tif (!seen.has(cachedPath)) fileCache.delete(cachedPath);\n\t\t}\n\n\t\tresult.perModel.sort((a, b) => b.total - a.total);\n\t\tresult.perDay.sort((a, b) => a.day.localeCompare(b.day));\n\t} catch {\n\t\treturn emptyHistory();\n\t}\n\treturn result;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"usage-history.d.ts","sourceRoot":"","sources":["../../src/core/usage-history.ts"],"names":[],"mappings":"AAyBA,MAAM,WAAW,oBAAoB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,kBAAkB;IAClC,wBAAwB;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,sBAAsB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,YAAY;IAC5B,QAAQ,EAAE,oBAAoB,EAAE,CAAC;IACjC,MAAM,EAAE,kBAAkB,EAAE,CAAC;IAC7B,UAAU,EAAE,sBAAsB,CAAC;IACnC,6EAA6E;IAC7E,oBAAoB,EAAE,OAAO,CAAC;IAC9B,gEAA8D;IAC9D,YAAY,EAAE,MAAM,CAAC;IACrB,kFAAkF;IAClF,WAAW,EAAE,MAAM,CAAC;IACpB,0EAA0E;IAC1E,iBAAiB,EAAE,MAAM,CAAC;CAC1B;AAWD,sDAAsD;AACtD,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C;AAsDD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,YAAY,CA+EpG","sourcesContent":["import { type AccountedRequest, collectUsageRequests, usageTokens } from \"./usage-accounting.ts\";\n/**\n * Request usage history for integrations.\n *\n * Scans the on-disk session files (`.jsonl` under `getSessionsDir()`, both the\n * flat and the per-project-subdirectory layouts) and aggregates:\n * - Token totals per provider/model from conversation and auxiliary-request `usage`\n * metadata (same fields the live session rows use),\n * - per-day totals (YYYY-MM-DD buckets from the entry timestamp),\n * - an ESTIMATED category breakdown (chars/4) via the same accounting as\n * `computeContextBreakdown` — session files don't store the system prompt or\n * tool definitions, so `includesSystemPrompt` is always false and the\n * message categories carry the whole breakdown.\n *\n * Results are cached per file keyed on mtime+size; unchanged files are not\n * re-parsed. Never throws — corrupt files are skipped, total failure returns\n * an empty aggregate.\n */\n\nimport { type Dirent, readdirSync, type Stats, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { getSessionsDir } from \"../config.ts\";\nimport { computeContextBreakdown } from \"./context-breakdown.ts\";\nimport { loadEntriesFromFile, type SessionEntry, sessionEntryToContextMessages } from \"./session-manager.ts\";\n\nexport interface UsageHistoryModelRow {\n\tmodel: string;\n\tinput: number;\n\toutput: number;\n\tcacheRead: number;\n\tcacheWrite: number;\n\ttotal: number;\n}\n\nexport interface UsageHistoryDayRow {\n\t/** YYYY-MM-DD (UTC). */\n\tday: string;\n\ttotal: number;\n}\n\nexport interface UsageHistoryCategories {\n\tuser: number;\n\tassistantText: number;\n\tthinking: number;\n\ttoolCalls: number;\n\ttoolResults: number;\n\tsummaries: number;\n\ttotal: number;\n}\n\nexport interface UsageHistory {\n\tperModel: UsageHistoryModelRow[];\n\tperDay: UsageHistoryDayRow[];\n\tcategories: UsageHistoryCategories;\n\t/** False: session files don't store the system prompt / tool definitions. */\n\tincludesSystemPrompt: boolean;\n\t/** Session files inside the scan window (mtime ≥ sinceMs). */\n\tfilesScanned: number;\n\t/** Files actually parsed this call (cache misses). Also serves as a test hook. */\n\tfilesParsed: number;\n\t/** Scanned files containing at least one assistant message with usage. */\n\tsessionsWithUsage: number;\n}\n\ninterface FileAggregate {\n\tmtimeMs: number;\n\tsize: number;\n\trequests: AccountedRequest[];\n\tentries: SessionEntry[];\n}\n\nconst fileCache = new Map<string, FileAggregate>();\n\n/** Test hook: drop all cached per-file aggregates. */\nexport function resetUsageHistoryCache(): void {\n\tfileCache.clear();\n}\n\nfunction emptyCategories(): UsageHistoryCategories {\n\treturn { user: 0, assistantText: 0, thinking: 0, toolCalls: 0, toolResults: 0, summaries: 0, total: 0 };\n}\n\nfunction emptyHistory(): UsageHistory {\n\treturn {\n\t\tperModel: [],\n\t\tperDay: [],\n\t\tcategories: emptyCategories(),\n\t\tincludesSystemPrompt: false,\n\t\tfilesScanned: 0,\n\t\tfilesParsed: 0,\n\t\tsessionsWithUsage: 0,\n\t};\n}\n\n/** Enumerate `.jsonl` session files in both layouts: flat and per-project subdirectories. */\nfunction enumerateSessionFiles(sessionsDir: string): string[] {\n\tconst files: string[] = [];\n\tlet dirEntries: Dirent[];\n\ttry {\n\t\tdirEntries = readdirSync(sessionsDir, { withFileTypes: true });\n\t} catch {\n\t\treturn files;\n\t}\n\tfor (const entry of dirEntries) {\n\t\tif (entry.isFile() && entry.name.endsWith(\".jsonl\")) {\n\t\t\tfiles.push(join(sessionsDir, entry.name));\n\t\t} else if (entry.isDirectory()) {\n\t\t\ttry {\n\t\t\t\tconst subDir = join(sessionsDir, entry.name);\n\t\t\t\tfor (const name of readdirSync(subDir)) {\n\t\t\t\t\tif (name.endsWith(\".jsonl\")) files.push(join(subDir, name));\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Unreadable subdirectory — skip.\n\t\t\t}\n\t\t}\n\t}\n\treturn files;\n}\n\n/** Cache parsed records, not date-window aggregates, so changing the window stays correct. */\nfunction parseSessionFile(filePath: string, mtimeMs: number, size: number): FileAggregate | null {\n\ttry {\n\t\tconst entries = loadEntriesFromFile(filePath).filter((entry): entry is SessionEntry => entry.type !== \"session\");\n\t\treturn { mtimeMs, size, entries, requests: collectUsageRequests(entries) };\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Aggregate requests since `sinceMs`; file modification time only narrows the scan.\n * Never throws; on total failure returns an empty aggregate.\n */\nexport function collectUsageHistory(options: { sinceMs: number; sessionsDir?: string }): UsageHistory {\n\tconst result = emptyHistory();\n\ttry {\n\t\tconst sessionsDir = options.sessionsDir ?? getSessionsDir();\n\t\tconst files = enumerateSessionFiles(sessionsDir);\n\t\tconst seen = new Set<string>();\n\t\tconst seenRequests = new Set<string>();\n\t\tconst seenEntries = new Set<string>();\n\n\t\tfor (const filePath of files) {\n\t\t\tseen.add(filePath);\n\t\t\tlet stats: Stats;\n\t\t\ttry {\n\t\t\t\tstats = statSync(filePath);\n\t\t\t} catch {\n\t\t\t\tcontinue; // vanished between readdir and stat\n\t\t\t}\n\t\t\tif (stats.mtimeMs < options.sinceMs) continue;\n\t\t\tresult.filesScanned++;\n\n\t\t\tlet aggregate = fileCache.get(filePath);\n\t\t\tif (!aggregate || aggregate.mtimeMs !== stats.mtimeMs || aggregate.size !== stats.size) {\n\t\t\t\taggregate = parseSessionFile(filePath, stats.mtimeMs, stats.size) ?? undefined;\n\t\t\t\tresult.filesParsed++;\n\t\t\t\tif (aggregate) {\n\t\t\t\t\tfileCache.set(filePath, aggregate);\n\t\t\t\t} else {\n\t\t\t\t\tfileCache.delete(filePath);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!aggregate) continue;\n\n\t\t\tlet hasUsage = false;\n\t\t\tfor (const request of aggregate.requests) {\n\t\t\t\tif (request.timestamp < options.sinceMs || seenRequests.has(request.id)) continue;\n\t\t\t\tseenRequests.add(request.id);\n\t\t\t\thasUsage = true;\n\t\t\t\tconst model = `${request.provider}/${request.model}`;\n\t\t\t\tlet row = result.perModel.find((candidate) => candidate.model === model);\n\t\t\t\tif (!row) {\n\t\t\t\t\trow = { model, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 };\n\t\t\t\t\tresult.perModel.push(row);\n\t\t\t\t}\n\t\t\t\trow.input += request.usage.input;\n\t\t\t\trow.output += request.usage.output;\n\t\t\t\trow.cacheRead += request.usage.cacheRead;\n\t\t\t\trow.cacheWrite += request.usage.cacheWrite;\n\t\t\t\trow.total += usageTokens(request.usage);\n\t\t\t\tconst day = new Date(request.timestamp).toISOString().slice(0, 10);\n\t\t\t\tconst dayRow = result.perDay.find((candidate) => candidate.day === day);\n\t\t\t\tif (dayRow) dayRow.total += usageTokens(request.usage);\n\t\t\t\telse result.perDay.push({ day, total: usageTokens(request.usage) });\n\t\t\t}\n\t\t\tif (hasUsage) result.sessionsWithUsage++;\n\t\t\tconst messages = aggregate.entries\n\t\t\t\t.filter((entry) => {\n\t\t\t\t\tconst identity = `${entry.id}:${entry.timestamp}`;\n\t\t\t\t\tif (entry.inherited || Date.parse(entry.timestamp) < options.sinceMs || seenEntries.has(identity))\n\t\t\t\t\t\treturn false;\n\t\t\t\t\tseenEntries.add(identity);\n\t\t\t\t\treturn true;\n\t\t\t\t})\n\t\t\t\t.flatMap(sessionEntryToContextMessages);\n\t\t\tconst breakdown = computeContextBreakdown({ systemPrompt: \"\", tools: [], messages, contextWindow: 0 });\n\t\t\tfor (const key of Object.keys(result.categories) as Array<keyof UsageHistoryCategories>)\n\t\t\t\tresult.categories[key] += breakdown[key];\n\t\t}\n\n\t\t// Drop cache entries for deleted files.\n\t\tfor (const cachedPath of fileCache.keys()) {\n\t\t\tif (!seen.has(cachedPath)) fileCache.delete(cachedPath);\n\t\t}\n\n\t\tresult.perModel.sort((a, b) => b.total - a.total);\n\t\tresult.perDay.sort((a, b) => a.day.localeCompare(b.day));\n\t} catch {\n\t\treturn emptyHistory();\n\t}\n\treturn result;\n}\n"]}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { collectUsageRequests, usageTokens } from "./usage-accounting.js";
|
|
1
2
|
/**
|
|
2
|
-
*
|
|
3
|
+
* Request usage history for integrations.
|
|
3
4
|
*
|
|
4
5
|
* Scans the on-disk session files (`.jsonl` under `getSessionsDir()`, both the
|
|
5
6
|
* flat and the per-project-subdirectory layouts) and aggregates:
|
|
6
|
-
* -
|
|
7
|
+
* - Token totals per provider/model from conversation and auxiliary-request `usage`
|
|
7
8
|
* metadata (same fields the live session rows use),
|
|
8
9
|
* - per-day totals (YYYY-MM-DD buckets from the entry timestamp),
|
|
9
10
|
* - an ESTIMATED category breakdown (chars/4) via the same accounting as
|
|
@@ -19,7 +20,7 @@ import { readdirSync, statSync } from "node:fs";
|
|
|
19
20
|
import { join } from "node:path";
|
|
20
21
|
import { getSessionsDir } from "../config.js";
|
|
21
22
|
import { computeContextBreakdown } from "./context-breakdown.js";
|
|
22
|
-
import { loadEntriesFromFile, sessionEntryToContextMessages
|
|
23
|
+
import { loadEntriesFromFile, sessionEntryToContextMessages } from "./session-manager.js";
|
|
23
24
|
const fileCache = new Map();
|
|
24
25
|
/** Test hook: drop all cached per-file aggregates. */
|
|
25
26
|
export function resetUsageHistoryCache() {
|
|
@@ -68,93 +69,18 @@ function enumerateSessionFiles(sessionsDir) {
|
|
|
68
69
|
}
|
|
69
70
|
return files;
|
|
70
71
|
}
|
|
71
|
-
/**
|
|
72
|
-
function entryTimestampMs(entry, message, fallbackMs) {
|
|
73
|
-
const entryMs = Date.parse(entry.timestamp ?? "");
|
|
74
|
-
if (!Number.isNaN(entryMs))
|
|
75
|
-
return entryMs;
|
|
76
|
-
if (typeof message.timestamp === "number" && message.timestamp > 0)
|
|
77
|
-
return message.timestamp;
|
|
78
|
-
return fallbackMs;
|
|
79
|
-
}
|
|
80
|
-
/** Parse one session file into its aggregate. Returns null when the file is unusable. */
|
|
72
|
+
/** Cache parsed records, not date-window aggregates, so changing the window stays correct. */
|
|
81
73
|
function parseSessionFile(filePath, mtimeMs, size) {
|
|
82
|
-
let entries;
|
|
83
74
|
try {
|
|
84
|
-
entries = loadEntriesFromFile(filePath);
|
|
75
|
+
const entries = loadEntriesFromFile(filePath).filter((entry) => entry.type !== "session");
|
|
76
|
+
return { mtimeMs, size, entries, requests: collectUsageRequests(entries) };
|
|
85
77
|
}
|
|
86
78
|
catch {
|
|
87
79
|
return null;
|
|
88
80
|
}
|
|
89
|
-
if (entries.length === 0)
|
|
90
|
-
return null;
|
|
91
|
-
const aggregate = {
|
|
92
|
-
mtimeMs,
|
|
93
|
-
size,
|
|
94
|
-
perModel: new Map(),
|
|
95
|
-
perDay: new Map(),
|
|
96
|
-
categories: emptyCategories(),
|
|
97
|
-
hasUsage: false,
|
|
98
|
-
};
|
|
99
|
-
const sessionEntries = [];
|
|
100
|
-
for (const entry of entries) {
|
|
101
|
-
if (entry.type === "session")
|
|
102
|
-
continue; // header
|
|
103
|
-
sessionEntries.push(entry);
|
|
104
|
-
if (entry.type !== "message" || entry.message.role !== "assistant")
|
|
105
|
-
continue;
|
|
106
|
-
const message = entry.message;
|
|
107
|
-
const usage = message.usage;
|
|
108
|
-
if (!usage || typeof usage.input !== "number" || typeof usage.output !== "number")
|
|
109
|
-
continue;
|
|
110
|
-
const provider = typeof message.provider === "string" && message.provider ? message.provider : "unknown";
|
|
111
|
-
const modelId = message.responseModel ?? message.model;
|
|
112
|
-
const key = `${provider}/${typeof modelId === "string" && modelId ? modelId : "unknown"}`;
|
|
113
|
-
const input = usage.input;
|
|
114
|
-
const output = usage.output;
|
|
115
|
-
const cacheRead = typeof usage.cacheRead === "number" ? usage.cacheRead : 0;
|
|
116
|
-
const cacheWrite = typeof usage.cacheWrite === "number" ? usage.cacheWrite : 0;
|
|
117
|
-
let row = aggregate.perModel.get(key);
|
|
118
|
-
if (!row) {
|
|
119
|
-
row = { model: key, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
|
|
120
|
-
aggregate.perModel.set(key, row);
|
|
121
|
-
}
|
|
122
|
-
row.input += input;
|
|
123
|
-
row.output += output;
|
|
124
|
-
row.cacheRead += cacheRead;
|
|
125
|
-
row.cacheWrite += cacheWrite;
|
|
126
|
-
row.total += input + output + cacheRead + cacheWrite;
|
|
127
|
-
aggregate.hasUsage = true;
|
|
128
|
-
const day = new Date(entryTimestampMs(entry, message, mtimeMs)).toISOString().slice(0, 10);
|
|
129
|
-
aggregate.perDay.set(day, (aggregate.perDay.get(day) ?? 0) + input + output + cacheRead + cacheWrite);
|
|
130
|
-
}
|
|
131
|
-
// Category estimate over the file's messages (system prompt/tools are not
|
|
132
|
-
// stored in session files, so message categories carry the breakdown).
|
|
133
|
-
try {
|
|
134
|
-
const messages = sessionEntries.flatMap(sessionEntryToContextMessages);
|
|
135
|
-
const breakdown = computeContextBreakdown({ systemPrompt: "", tools: [], messages, contextWindow: 0 });
|
|
136
|
-
aggregate.categories = {
|
|
137
|
-
user: breakdown.user,
|
|
138
|
-
assistantText: breakdown.assistantText,
|
|
139
|
-
thinking: breakdown.thinking,
|
|
140
|
-
toolCalls: breakdown.toolCalls,
|
|
141
|
-
toolResults: breakdown.toolResults,
|
|
142
|
-
summaries: breakdown.summaries,
|
|
143
|
-
total: breakdown.user +
|
|
144
|
-
breakdown.assistantText +
|
|
145
|
-
breakdown.thinking +
|
|
146
|
-
breakdown.toolCalls +
|
|
147
|
-
breakdown.toolResults +
|
|
148
|
-
breakdown.summaries,
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
catch {
|
|
152
|
-
// Category estimation is best-effort; per-model/day totals above still stand.
|
|
153
|
-
}
|
|
154
|
-
return aggregate;
|
|
155
81
|
}
|
|
156
82
|
/**
|
|
157
|
-
* Aggregate
|
|
83
|
+
* Aggregate requests since `sinceMs`; file modification time only narrows the scan.
|
|
158
84
|
* Never throws; on total failure returns an empty aggregate.
|
|
159
85
|
*/
|
|
160
86
|
export function collectUsageHistory(options) {
|
|
@@ -163,6 +89,8 @@ export function collectUsageHistory(options) {
|
|
|
163
89
|
const sessionsDir = options.sessionsDir ?? getSessionsDir();
|
|
164
90
|
const files = enumerateSessionFiles(sessionsDir);
|
|
165
91
|
const seen = new Set();
|
|
92
|
+
const seenRequests = new Set();
|
|
93
|
+
const seenEntries = new Set();
|
|
166
94
|
for (const filePath of files) {
|
|
167
95
|
seen.add(filePath);
|
|
168
96
|
let stats;
|
|
@@ -188,40 +116,44 @@ export function collectUsageHistory(options) {
|
|
|
188
116
|
}
|
|
189
117
|
if (!aggregate)
|
|
190
118
|
continue;
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
target.cacheWrite += row.cacheWrite;
|
|
203
|
-
target.total += row.total;
|
|
204
|
-
}
|
|
205
|
-
for (const [day, total] of aggregate.perDay) {
|
|
206
|
-
const existing = result.perDay.find((r) => r.day === day);
|
|
207
|
-
if (existing) {
|
|
208
|
-
existing.total += total;
|
|
209
|
-
}
|
|
210
|
-
else {
|
|
211
|
-
result.perDay.push({ day, total });
|
|
119
|
+
let hasUsage = false;
|
|
120
|
+
for (const request of aggregate.requests) {
|
|
121
|
+
if (request.timestamp < options.sinceMs || seenRequests.has(request.id))
|
|
122
|
+
continue;
|
|
123
|
+
seenRequests.add(request.id);
|
|
124
|
+
hasUsage = true;
|
|
125
|
+
const model = `${request.provider}/${request.model}`;
|
|
126
|
+
let row = result.perModel.find((candidate) => candidate.model === model);
|
|
127
|
+
if (!row) {
|
|
128
|
+
row = { model, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
|
|
129
|
+
result.perModel.push(row);
|
|
212
130
|
}
|
|
131
|
+
row.input += request.usage.input;
|
|
132
|
+
row.output += request.usage.output;
|
|
133
|
+
row.cacheRead += request.usage.cacheRead;
|
|
134
|
+
row.cacheWrite += request.usage.cacheWrite;
|
|
135
|
+
row.total += usageTokens(request.usage);
|
|
136
|
+
const day = new Date(request.timestamp).toISOString().slice(0, 10);
|
|
137
|
+
const dayRow = result.perDay.find((candidate) => candidate.day === day);
|
|
138
|
+
if (dayRow)
|
|
139
|
+
dayRow.total += usageTokens(request.usage);
|
|
140
|
+
else
|
|
141
|
+
result.perDay.push({ day, total: usageTokens(request.usage) });
|
|
213
142
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
143
|
+
if (hasUsage)
|
|
144
|
+
result.sessionsWithUsage++;
|
|
145
|
+
const messages = aggregate.entries
|
|
146
|
+
.filter((entry) => {
|
|
147
|
+
const identity = `${entry.id}:${entry.timestamp}`;
|
|
148
|
+
if (entry.inherited || Date.parse(entry.timestamp) < options.sinceMs || seenEntries.has(identity))
|
|
149
|
+
return false;
|
|
150
|
+
seenEntries.add(identity);
|
|
151
|
+
return true;
|
|
152
|
+
})
|
|
153
|
+
.flatMap(sessionEntryToContextMessages);
|
|
154
|
+
const breakdown = computeContextBreakdown({ systemPrompt: "", tools: [], messages, contextWindow: 0 });
|
|
155
|
+
for (const key of Object.keys(result.categories))
|
|
156
|
+
result.categories[key] += breakdown[key];
|
|
225
157
|
}
|
|
226
158
|
// Drop cache entries for deleted files.
|
|
227
159
|
for (const cachedPath of fileCache.keys()) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usage-history.js","sourceRoot":"","sources":["../../src/core/usage-history.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * lunr: 30-day token usage history for the /usage command.\n *\n * Scans the on-disk session files (`.jsonl` under `getSessionsDir()`, both the\n * flat and the per-project-subdirectory layouts) and aggregates:\n * - REAL token totals per provider/model from assistant-message `usage`\n * metadata (same fields the live session rows use),\n * - per-day totals (YYYY-MM-DD buckets from the entry timestamp),\n * - an ESTIMATED category breakdown (chars/4) via the same accounting as\n * `computeContextBreakdown` — session files don't store the system prompt or\n * tool definitions, so `includesSystemPrompt` is always false and the\n * message categories carry the whole breakdown.\n *\n * Results are cached per file keyed on mtime+size; unchanged files are not\n * re-parsed. Never throws — corrupt files are skipped, total failure returns\n * an empty aggregate.\n */\n\nimport { type Dirent, readdirSync, type Stats, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { AssistantMessage } from \"@ashx-j/lunr-ai/compat\";\nimport { getSessionsDir } from \"../config.ts\";\nimport { computeContextBreakdown } from \"./context-breakdown.ts\";\nimport {\n\ttype FileEntry,\n\tloadEntriesFromFile,\n\ttype SessionEntry,\n\tsessionEntryToContextMessages,\n} from \"./session-manager.ts\";\n\nexport interface UsageHistoryModelRow {\n\tmodel: string;\n\tinput: number;\n\toutput: number;\n\tcacheRead: number;\n\tcacheWrite: number;\n\ttotal: number;\n}\n\nexport interface UsageHistoryDayRow {\n\t/** YYYY-MM-DD (UTC). */\n\tday: string;\n\ttotal: number;\n}\n\nexport interface UsageHistoryCategories {\n\tuser: number;\n\tassistantText: number;\n\tthinking: number;\n\ttoolCalls: number;\n\ttoolResults: number;\n\tsummaries: number;\n\ttotal: number;\n}\n\nexport interface UsageHistory {\n\tperModel: UsageHistoryModelRow[];\n\tperDay: UsageHistoryDayRow[];\n\tcategories: UsageHistoryCategories;\n\t/** False: session files don't store the system prompt / tool definitions. */\n\tincludesSystemPrompt: boolean;\n\t/** Session files inside the scan window (mtime ≥ sinceMs). */\n\tfilesScanned: number;\n\t/** Files actually parsed this call (cache misses). Also serves as a test hook. */\n\tfilesParsed: number;\n\t/** Scanned files containing at least one assistant message with usage. */\n\tsessionsWithUsage: number;\n}\n\ninterface FileAggregate {\n\tmtimeMs: number;\n\tsize: number;\n\tperModel: Map<string, UsageHistoryModelRow>;\n\tperDay: Map<string, number>;\n\tcategories: UsageHistoryCategories;\n\thasUsage: boolean;\n}\n\nconst fileCache = new Map<string, FileAggregate>();\n\n/** Test hook: drop all cached per-file aggregates. */\nexport function resetUsageHistoryCache(): void {\n\tfileCache.clear();\n}\n\nfunction emptyCategories(): UsageHistoryCategories {\n\treturn { user: 0, assistantText: 0, thinking: 0, toolCalls: 0, toolResults: 0, summaries: 0, total: 0 };\n}\n\nfunction emptyHistory(): UsageHistory {\n\treturn {\n\t\tperModel: [],\n\t\tperDay: [],\n\t\tcategories: emptyCategories(),\n\t\tincludesSystemPrompt: false,\n\t\tfilesScanned: 0,\n\t\tfilesParsed: 0,\n\t\tsessionsWithUsage: 0,\n\t};\n}\n\n/** Enumerate `.jsonl` session files in both layouts: flat and per-project subdirectories. */\nfunction enumerateSessionFiles(sessionsDir: string): string[] {\n\tconst files: string[] = [];\n\tlet dirEntries: Dirent[];\n\ttry {\n\t\tdirEntries = readdirSync(sessionsDir, { withFileTypes: true });\n\t} catch {\n\t\treturn files;\n\t}\n\tfor (const entry of dirEntries) {\n\t\tif (entry.isFile() && entry.name.endsWith(\".jsonl\")) {\n\t\t\tfiles.push(join(sessionsDir, entry.name));\n\t\t} else if (entry.isDirectory()) {\n\t\t\ttry {\n\t\t\t\tconst subDir = join(sessionsDir, entry.name);\n\t\t\t\tfor (const name of readdirSync(subDir)) {\n\t\t\t\t\tif (name.endsWith(\".jsonl\")) files.push(join(subDir, name));\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Unreadable subdirectory — skip.\n\t\t\t}\n\t\t}\n\t}\n\treturn files;\n}\n\n/** Milliseconds for day bucketing: entry timestamp, then message timestamp, then file mtime. */\nfunction entryTimestampMs(entry: SessionEntry, message: AssistantMessage, fallbackMs: number): number {\n\tconst entryMs = Date.parse(entry.timestamp ?? \"\");\n\tif (!Number.isNaN(entryMs)) return entryMs;\n\tif (typeof message.timestamp === \"number\" && message.timestamp > 0) return message.timestamp;\n\treturn fallbackMs;\n}\n\n/** Parse one session file into its aggregate. Returns null when the file is unusable. */\nfunction parseSessionFile(filePath: string, mtimeMs: number, size: number): FileAggregate | null {\n\tlet entries: FileEntry[];\n\ttry {\n\t\tentries = loadEntriesFromFile(filePath);\n\t} catch {\n\t\treturn null;\n\t}\n\tif (entries.length === 0) return null;\n\n\tconst aggregate: FileAggregate = {\n\t\tmtimeMs,\n\t\tsize,\n\t\tperModel: new Map(),\n\t\tperDay: new Map(),\n\t\tcategories: emptyCategories(),\n\t\thasUsage: false,\n\t};\n\n\tconst sessionEntries: SessionEntry[] = [];\n\tfor (const entry of entries) {\n\t\tif (entry.type === \"session\") continue; // header\n\t\tsessionEntries.push(entry);\n\n\t\tif (entry.type !== \"message\" || entry.message.role !== \"assistant\") continue;\n\t\tconst message = entry.message as AssistantMessage;\n\t\tconst usage = message.usage;\n\t\tif (!usage || typeof usage.input !== \"number\" || typeof usage.output !== \"number\") continue;\n\n\t\tconst provider = typeof message.provider === \"string\" && message.provider ? message.provider : \"unknown\";\n\t\tconst modelId = message.responseModel ?? message.model;\n\t\tconst key = `${provider}/${typeof modelId === \"string\" && modelId ? modelId : \"unknown\"}`;\n\t\tconst input = usage.input;\n\t\tconst output = usage.output;\n\t\tconst cacheRead = typeof usage.cacheRead === \"number\" ? usage.cacheRead : 0;\n\t\tconst cacheWrite = typeof usage.cacheWrite === \"number\" ? usage.cacheWrite : 0;\n\n\t\tlet row = aggregate.perModel.get(key);\n\t\tif (!row) {\n\t\t\trow = { model: key, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 };\n\t\t\taggregate.perModel.set(key, row);\n\t\t}\n\t\trow.input += input;\n\t\trow.output += output;\n\t\trow.cacheRead += cacheRead;\n\t\trow.cacheWrite += cacheWrite;\n\t\trow.total += input + output + cacheRead + cacheWrite;\n\t\taggregate.hasUsage = true;\n\n\t\tconst day = new Date(entryTimestampMs(entry, message, mtimeMs)).toISOString().slice(0, 10);\n\t\taggregate.perDay.set(day, (aggregate.perDay.get(day) ?? 0) + input + output + cacheRead + cacheWrite);\n\t}\n\n\t// Category estimate over the file's messages (system prompt/tools are not\n\t// stored in session files, so message categories carry the breakdown).\n\ttry {\n\t\tconst messages = sessionEntries.flatMap(sessionEntryToContextMessages);\n\t\tconst breakdown = computeContextBreakdown({ systemPrompt: \"\", tools: [], messages, contextWindow: 0 });\n\t\taggregate.categories = {\n\t\t\tuser: breakdown.user,\n\t\t\tassistantText: breakdown.assistantText,\n\t\t\tthinking: breakdown.thinking,\n\t\t\ttoolCalls: breakdown.toolCalls,\n\t\t\ttoolResults: breakdown.toolResults,\n\t\t\tsummaries: breakdown.summaries,\n\t\t\ttotal:\n\t\t\t\tbreakdown.user +\n\t\t\t\tbreakdown.assistantText +\n\t\t\t\tbreakdown.thinking +\n\t\t\t\tbreakdown.toolCalls +\n\t\t\t\tbreakdown.toolResults +\n\t\t\t\tbreakdown.summaries,\n\t\t};\n\t} catch {\n\t\t// Category estimation is best-effort; per-model/day totals above still stand.\n\t}\n\n\treturn aggregate;\n}\n\n/**\n * Aggregate token usage across all session files modified since `sinceMs`.\n * Never throws; on total failure returns an empty aggregate.\n */\nexport function collectUsageHistory(options: { sinceMs: number; sessionsDir?: string }): UsageHistory {\n\tconst result = emptyHistory();\n\ttry {\n\t\tconst sessionsDir = options.sessionsDir ?? getSessionsDir();\n\t\tconst files = enumerateSessionFiles(sessionsDir);\n\t\tconst seen = new Set<string>();\n\n\t\tfor (const filePath of files) {\n\t\t\tseen.add(filePath);\n\t\t\tlet stats: Stats;\n\t\t\ttry {\n\t\t\t\tstats = statSync(filePath);\n\t\t\t} catch {\n\t\t\t\tcontinue; // vanished between readdir and stat\n\t\t\t}\n\t\t\tif (stats.mtimeMs < options.sinceMs) continue;\n\t\t\tresult.filesScanned++;\n\n\t\t\tlet aggregate = fileCache.get(filePath);\n\t\t\tif (!aggregate || aggregate.mtimeMs !== stats.mtimeMs || aggregate.size !== stats.size) {\n\t\t\t\taggregate = parseSessionFile(filePath, stats.mtimeMs, stats.size) ?? undefined;\n\t\t\t\tresult.filesParsed++;\n\t\t\t\tif (aggregate) {\n\t\t\t\t\tfileCache.set(filePath, aggregate);\n\t\t\t\t} else {\n\t\t\t\t\tfileCache.delete(filePath);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!aggregate) continue;\n\n\t\t\tif (aggregate.hasUsage) result.sessionsWithUsage++;\n\t\t\tfor (const row of aggregate.perModel.values()) {\n\t\t\t\tlet target = result.perModel.find((r) => r.model === row.model);\n\t\t\t\tif (!target) {\n\t\t\t\t\ttarget = { model: row.model, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 };\n\t\t\t\t\tresult.perModel.push(target);\n\t\t\t\t}\n\t\t\t\ttarget.input += row.input;\n\t\t\t\ttarget.output += row.output;\n\t\t\t\ttarget.cacheRead += row.cacheRead;\n\t\t\t\ttarget.cacheWrite += row.cacheWrite;\n\t\t\t\ttarget.total += row.total;\n\t\t\t}\n\t\t\tfor (const [day, total] of aggregate.perDay) {\n\t\t\t\tconst existing = result.perDay.find((r) => r.day === day);\n\t\t\t\tif (existing) {\n\t\t\t\t\texisting.total += total;\n\t\t\t\t} else {\n\t\t\t\t\tresult.perDay.push({ day, total });\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (const key of [\n\t\t\t\t\"user\",\n\t\t\t\t\"assistantText\",\n\t\t\t\t\"thinking\",\n\t\t\t\t\"toolCalls\",\n\t\t\t\t\"toolResults\",\n\t\t\t\t\"summaries\",\n\t\t\t\t\"total\",\n\t\t\t] as const) {\n\t\t\t\tresult.categories[key] += aggregate.categories[key];\n\t\t\t}\n\t\t}\n\n\t\t// Drop cache entries for deleted files.\n\t\tfor (const cachedPath of fileCache.keys()) {\n\t\t\tif (!seen.has(cachedPath)) fileCache.delete(cachedPath);\n\t\t}\n\n\t\tresult.perModel.sort((a, b) => b.total - a.total);\n\t\tresult.perDay.sort((a, b) => a.day.localeCompare(b.day));\n\t} catch {\n\t\treturn emptyHistory();\n\t}\n\treturn result;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"usage-history.js","sourceRoot":"","sources":["../../src/core/usage-history.ts"],"names":[],"mappings":"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","sourcesContent":["import { type AccountedRequest, collectUsageRequests, usageTokens } from \"./usage-accounting.ts\";\n/**\n * Request usage history for integrations.\n *\n * Scans the on-disk session files (`.jsonl` under `getSessionsDir()`, both the\n * flat and the per-project-subdirectory layouts) and aggregates:\n * - Token totals per provider/model from conversation and auxiliary-request `usage`\n * metadata (same fields the live session rows use),\n * - per-day totals (YYYY-MM-DD buckets from the entry timestamp),\n * - an ESTIMATED category breakdown (chars/4) via the same accounting as\n * `computeContextBreakdown` — session files don't store the system prompt or\n * tool definitions, so `includesSystemPrompt` is always false and the\n * message categories carry the whole breakdown.\n *\n * Results are cached per file keyed on mtime+size; unchanged files are not\n * re-parsed. Never throws — corrupt files are skipped, total failure returns\n * an empty aggregate.\n */\n\nimport { type Dirent, readdirSync, type Stats, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { getSessionsDir } from \"../config.ts\";\nimport { computeContextBreakdown } from \"./context-breakdown.ts\";\nimport { loadEntriesFromFile, type SessionEntry, sessionEntryToContextMessages } from \"./session-manager.ts\";\n\nexport interface UsageHistoryModelRow {\n\tmodel: string;\n\tinput: number;\n\toutput: number;\n\tcacheRead: number;\n\tcacheWrite: number;\n\ttotal: number;\n}\n\nexport interface UsageHistoryDayRow {\n\t/** YYYY-MM-DD (UTC). */\n\tday: string;\n\ttotal: number;\n}\n\nexport interface UsageHistoryCategories {\n\tuser: number;\n\tassistantText: number;\n\tthinking: number;\n\ttoolCalls: number;\n\ttoolResults: number;\n\tsummaries: number;\n\ttotal: number;\n}\n\nexport interface UsageHistory {\n\tperModel: UsageHistoryModelRow[];\n\tperDay: UsageHistoryDayRow[];\n\tcategories: UsageHistoryCategories;\n\t/** False: session files don't store the system prompt / tool definitions. */\n\tincludesSystemPrompt: boolean;\n\t/** Session files inside the scan window (mtime ≥ sinceMs). */\n\tfilesScanned: number;\n\t/** Files actually parsed this call (cache misses). Also serves as a test hook. */\n\tfilesParsed: number;\n\t/** Scanned files containing at least one assistant message with usage. */\n\tsessionsWithUsage: number;\n}\n\ninterface FileAggregate {\n\tmtimeMs: number;\n\tsize: number;\n\trequests: AccountedRequest[];\n\tentries: SessionEntry[];\n}\n\nconst fileCache = new Map<string, FileAggregate>();\n\n/** Test hook: drop all cached per-file aggregates. */\nexport function resetUsageHistoryCache(): void {\n\tfileCache.clear();\n}\n\nfunction emptyCategories(): UsageHistoryCategories {\n\treturn { user: 0, assistantText: 0, thinking: 0, toolCalls: 0, toolResults: 0, summaries: 0, total: 0 };\n}\n\nfunction emptyHistory(): UsageHistory {\n\treturn {\n\t\tperModel: [],\n\t\tperDay: [],\n\t\tcategories: emptyCategories(),\n\t\tincludesSystemPrompt: false,\n\t\tfilesScanned: 0,\n\t\tfilesParsed: 0,\n\t\tsessionsWithUsage: 0,\n\t};\n}\n\n/** Enumerate `.jsonl` session files in both layouts: flat and per-project subdirectories. */\nfunction enumerateSessionFiles(sessionsDir: string): string[] {\n\tconst files: string[] = [];\n\tlet dirEntries: Dirent[];\n\ttry {\n\t\tdirEntries = readdirSync(sessionsDir, { withFileTypes: true });\n\t} catch {\n\t\treturn files;\n\t}\n\tfor (const entry of dirEntries) {\n\t\tif (entry.isFile() && entry.name.endsWith(\".jsonl\")) {\n\t\t\tfiles.push(join(sessionsDir, entry.name));\n\t\t} else if (entry.isDirectory()) {\n\t\t\ttry {\n\t\t\t\tconst subDir = join(sessionsDir, entry.name);\n\t\t\t\tfor (const name of readdirSync(subDir)) {\n\t\t\t\t\tif (name.endsWith(\".jsonl\")) files.push(join(subDir, name));\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Unreadable subdirectory — skip.\n\t\t\t}\n\t\t}\n\t}\n\treturn files;\n}\n\n/** Cache parsed records, not date-window aggregates, so changing the window stays correct. */\nfunction parseSessionFile(filePath: string, mtimeMs: number, size: number): FileAggregate | null {\n\ttry {\n\t\tconst entries = loadEntriesFromFile(filePath).filter((entry): entry is SessionEntry => entry.type !== \"session\");\n\t\treturn { mtimeMs, size, entries, requests: collectUsageRequests(entries) };\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Aggregate requests since `sinceMs`; file modification time only narrows the scan.\n * Never throws; on total failure returns an empty aggregate.\n */\nexport function collectUsageHistory(options: { sinceMs: number; sessionsDir?: string }): UsageHistory {\n\tconst result = emptyHistory();\n\ttry {\n\t\tconst sessionsDir = options.sessionsDir ?? getSessionsDir();\n\t\tconst files = enumerateSessionFiles(sessionsDir);\n\t\tconst seen = new Set<string>();\n\t\tconst seenRequests = new Set<string>();\n\t\tconst seenEntries = new Set<string>();\n\n\t\tfor (const filePath of files) {\n\t\t\tseen.add(filePath);\n\t\t\tlet stats: Stats;\n\t\t\ttry {\n\t\t\t\tstats = statSync(filePath);\n\t\t\t} catch {\n\t\t\t\tcontinue; // vanished between readdir and stat\n\t\t\t}\n\t\t\tif (stats.mtimeMs < options.sinceMs) continue;\n\t\t\tresult.filesScanned++;\n\n\t\t\tlet aggregate = fileCache.get(filePath);\n\t\t\tif (!aggregate || aggregate.mtimeMs !== stats.mtimeMs || aggregate.size !== stats.size) {\n\t\t\t\taggregate = parseSessionFile(filePath, stats.mtimeMs, stats.size) ?? undefined;\n\t\t\t\tresult.filesParsed++;\n\t\t\t\tif (aggregate) {\n\t\t\t\t\tfileCache.set(filePath, aggregate);\n\t\t\t\t} else {\n\t\t\t\t\tfileCache.delete(filePath);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!aggregate) continue;\n\n\t\t\tlet hasUsage = false;\n\t\t\tfor (const request of aggregate.requests) {\n\t\t\t\tif (request.timestamp < options.sinceMs || seenRequests.has(request.id)) continue;\n\t\t\t\tseenRequests.add(request.id);\n\t\t\t\thasUsage = true;\n\t\t\t\tconst model = `${request.provider}/${request.model}`;\n\t\t\t\tlet row = result.perModel.find((candidate) => candidate.model === model);\n\t\t\t\tif (!row) {\n\t\t\t\t\trow = { model, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 };\n\t\t\t\t\tresult.perModel.push(row);\n\t\t\t\t}\n\t\t\t\trow.input += request.usage.input;\n\t\t\t\trow.output += request.usage.output;\n\t\t\t\trow.cacheRead += request.usage.cacheRead;\n\t\t\t\trow.cacheWrite += request.usage.cacheWrite;\n\t\t\t\trow.total += usageTokens(request.usage);\n\t\t\t\tconst day = new Date(request.timestamp).toISOString().slice(0, 10);\n\t\t\t\tconst dayRow = result.perDay.find((candidate) => candidate.day === day);\n\t\t\t\tif (dayRow) dayRow.total += usageTokens(request.usage);\n\t\t\t\telse result.perDay.push({ day, total: usageTokens(request.usage) });\n\t\t\t}\n\t\t\tif (hasUsage) result.sessionsWithUsage++;\n\t\t\tconst messages = aggregate.entries\n\t\t\t\t.filter((entry) => {\n\t\t\t\t\tconst identity = `${entry.id}:${entry.timestamp}`;\n\t\t\t\t\tif (entry.inherited || Date.parse(entry.timestamp) < options.sinceMs || seenEntries.has(identity))\n\t\t\t\t\t\treturn false;\n\t\t\t\t\tseenEntries.add(identity);\n\t\t\t\t\treturn true;\n\t\t\t\t})\n\t\t\t\t.flatMap(sessionEntryToContextMessages);\n\t\t\tconst breakdown = computeContextBreakdown({ systemPrompt: \"\", tools: [], messages, contextWindow: 0 });\n\t\t\tfor (const key of Object.keys(result.categories) as Array<keyof UsageHistoryCategories>)\n\t\t\t\tresult.categories[key] += breakdown[key];\n\t\t}\n\n\t\t// Drop cache entries for deleted files.\n\t\tfor (const cachedPath of fileCache.keys()) {\n\t\t\tif (!seen.has(cachedPath)) fileCache.delete(cachedPath);\n\t\t}\n\n\t\tresult.perModel.sort((a, b) => b.total - a.total);\n\t\tresult.perDay.sort((a, b) => a.day.localeCompare(b.day));\n\t} catch {\n\t\treturn emptyHistory();\n\t}\n\treturn result;\n}\n"]}
|