| 87 | } |
| 88 | |
| 89 | export class SessionUsageService { |
| 90 | private readonly SESSION_USAGE_FILE = "session-usage.json"; |
| 91 | private readonly fileLocks = workspaceFileLocks; |
| 92 | private readonly config: Config; |
| 93 | private readonly historyService: HistoryService; |
| 94 | |
| 95 | constructor(config: Config, historyService: HistoryService) { |
| 96 | this.config = config; |
| 97 | this.historyService = historyService; |
| 98 | } |
| 99 | /** |
| 100 | * Collect all messages from iterateFullHistory into an array. |
| 101 | * Usage rebuild needs every epoch for accurate totals. |
| 102 | */ |
| 103 | private async collectFullHistory(workspaceId: string): Promise<MuxMessage[]> { |
| 104 | const messages: MuxMessage[] = []; |
| 105 | const result = await this.historyService.iterateFullHistory(workspaceId, "forward", (chunk) => { |
| 106 | messages.push(...chunk); |
| 107 | }); |
| 108 | if (!result.success) { |
| 109 | log.warn(`Failed to iterate history for ${workspaceId}: ${result.error}`); |
| 110 | return []; |
| 111 | } |
| 112 | return messages; |
| 113 | } |
| 114 | |
| 115 | private getFilePath(workspaceId: string): string { |
| 116 | return path.join(this.config.getSessionDir(workspaceId), this.SESSION_USAGE_FILE); |
| 117 | } |
| 118 | |
| 119 | private createEmptyUsageFile(): SessionUsageFile { |
| 120 | return { byModel: {}, version: 1 }; |
| 121 | } |
| 122 | |
| 123 | private async readFile(workspaceId: string): Promise<SessionUsageFile> { |
| 124 | try { |
| 125 | const data = await fs.readFile(this.getFilePath(workspaceId), "utf-8"); |
| 126 | return JSON.parse(data) as SessionUsageFile; |
| 127 | } catch (error) { |
| 128 | if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") { |
| 129 | return this.createEmptyUsageFile(); |
| 130 | } |
| 131 | throw error; |
| 132 | } |
| 133 | } |
| 134 | |
| 135 | private async writeFile(workspaceId: string, data: SessionUsageFile): Promise<void> { |
| 136 | const filePath = this.getFilePath(workspaceId); |
| 137 | await fs.mkdir(path.dirname(filePath), { recursive: true }); |
| 138 | await writeFileAtomic(filePath, JSON.stringify(data, null, 2)); |
| 139 | } |
| 140 | |
| 141 | /** |
| 142 | * Record usage from a completed stream. Accumulates with existing usage |
| 143 | * AND updates lastRequest in a single atomic write. |
| 144 | * Model should already be normalized via normalizeToCanonical(). |
| 145 | */ |
| 146 | async recordUsage(workspaceId: string, model: string, usage: ChatUsageDisplay): Promise<void> { |
nothing calls this directly
no outgoing calls
no test coverage detected