|
2 | 2 | * @fileoverview Event hook for session lifecycle and token tracking. |
3 | 3 | */ |
4 | 4 |
|
| 5 | +import { randomUUID } from "crypto" |
| 6 | + |
5 | 7 | import type { AssistantMessage, Event, Message } from "@opencode-ai/sdk" |
6 | 8 |
|
7 | | -import type { CsvWriter } from "../services/CsvWriter" |
8 | 9 | import type { SessionManager } from "../services/SessionManager" |
9 | 10 | import type { TicketResolver } from "../services/TicketResolver" |
| 11 | +import type { CsvEntryData } from "../types/CsvEntryData" |
10 | 12 | import type { MessagePartUpdatedProperties } from "../types/MessagePartUpdatedProperties" |
11 | 13 | import type { MessageWithParts } from "../types/MessageWithParts" |
12 | 14 | import type { OpencodeClient } from "../types/OpencodeClient" |
13 | 15 | import type { TimeTrackingConfig } from "../types/TimeTrackingConfig" |
| 16 | +import type { WriteResult, WriterService } from "../types/WriterService" |
14 | 17 |
|
15 | 18 | import { AgentMatcher } from "../utils/AgentMatcher" |
16 | 19 | import { DescriptionGenerator } from "../utils/DescriptionGenerator" |
@@ -65,27 +68,33 @@ async function extractSummaryTitle( |
65 | 68 | * Creates the event hook for session lifecycle management. |
66 | 69 | * |
67 | 70 | * @param sessionManager - The session manager instance |
68 | | - * @param csvWriter - The CSV writer instance |
| 71 | + * @param writers - Array of writer services to persist entries (e.g., CsvWriter, WebhookSender) |
69 | 72 | * @param client - The OpenCode SDK client |
| 73 | + * @param ticketResolver - The ticket resolver instance |
| 74 | + * @param config - The time tracking configuration |
70 | 75 | * @returns The event hook function |
71 | 76 | * |
72 | 77 | * @remarks |
73 | 78 | * Handles three types of events: |
74 | 79 | * |
75 | 80 | * 1. **message.updated** - Tracks model from assistant messages |
76 | 81 | * 2. **message.part.updated** - Tracks token usage from step-finish parts |
77 | | - * 3. **session.idle** - Finalizes and exports the session |
| 82 | + * 3. **session.idle** - Finalizes and exports the session via all writers |
| 83 | + * |
| 84 | + * Writers are called in order. Each writer handles its own errors internally, |
| 85 | + * so a failure in one writer does not affect others. |
78 | 86 | * |
79 | 87 | * @example |
80 | 88 | * ```typescript |
| 89 | + * const writers: WriterService[] = [csvWriter, webhookSender] |
81 | 90 | * const hooks: Hooks = { |
82 | | - * event: createEventHook(sessionManager, csvWriter, client), |
| 91 | + * event: createEventHook(sessionManager, writers, client, ticketResolver, config), |
83 | 92 | * } |
84 | 93 | * ``` |
85 | 94 | */ |
86 | 95 | export function createEventHook( |
87 | 96 | sessionManager: SessionManager, |
88 | | - csvWriter: CsvWriter, |
| 97 | + writers: WriterService[], |
89 | 98 | client: OpencodeClient, |
90 | 99 | ticketResolver: TicketResolver, |
91 | 100 | config: TimeTrackingConfig |
@@ -219,37 +228,48 @@ export function createEventHook( |
219 | 228 | // Resolve ticket and account key with fallback hierarchy |
220 | 229 | const resolved = await ticketResolver.resolve(sessionID, agentString) |
221 | 230 |
|
222 | | - try { |
223 | | - await csvWriter.write({ |
224 | | - ticket: resolved.ticket, |
225 | | - accountKey: resolved.accountKey, |
226 | | - startTime: session.startTime, |
227 | | - endTime, |
228 | | - durationSeconds, |
229 | | - description, |
230 | | - notes: `Auto-tracked: ${toolSummary}`, |
231 | | - tokenUsage: session.tokenUsage, |
232 | | - cost: session.cost, |
233 | | - model: modelString, |
234 | | - agent: resolved.primaryAgent ?? agentString, |
235 | | - }) |
| 231 | + // Build entry data once, shared across all writers |
| 232 | + const entryData: CsvEntryData = { |
| 233 | + id: randomUUID(), |
| 234 | + userEmail: config.user_email, |
| 235 | + ticket: resolved.ticket, |
| 236 | + accountKey: resolved.accountKey, |
| 237 | + startTime: session.startTime, |
| 238 | + endTime, |
| 239 | + durationSeconds, |
| 240 | + description, |
| 241 | + notes: `Auto-tracked: ${toolSummary}`, |
| 242 | + tokenUsage: session.tokenUsage, |
| 243 | + cost: session.cost, |
| 244 | + model: modelString, |
| 245 | + agent: resolved.primaryAgent ?? agentString, |
| 246 | + } |
236 | 247 |
|
237 | | - const minutes = Math.round(durationSeconds / 60) |
| 248 | + // Call all writers in order (CSV first, then webhook, etc.) |
| 249 | + // Collect results for combined status reporting |
| 250 | + const results: WriteResult[] = [] |
| 251 | + for (const writer of writers) { |
| 252 | + const result = await writer.write(entryData) |
| 253 | + results.push(result) |
| 254 | + } |
238 | 255 |
|
239 | | - await client.tui.showToast({ |
240 | | - body: { |
241 | | - message: `Time tracked: ${minutes} min, ${totalTokens} tokens${resolved.ticket ? ` for ${resolved.ticket}` : ""}`, |
242 | | - variant: "success", |
243 | | - }, |
244 | | - }) |
245 | | - } catch { |
246 | | - await client.tui.showToast({ |
247 | | - body: { |
248 | | - message: "Time Tracking: Failed to save entry", |
249 | | - variant: "error", |
250 | | - }, |
251 | | - }) |
| 256 | + // Build combined toast message with writer status |
| 257 | + const minutes = Math.round(durationSeconds / 60) |
| 258 | + const failedWriters = results.filter((r) => !r.success) |
| 259 | + |
| 260 | + let message = `Time tracked: ${minutes} min, ${totalTokens} tokens${resolved.ticket ? ` for ${resolved.ticket}` : ""}` |
| 261 | + |
| 262 | + if (failedWriters.length > 0) { |
| 263 | + const failedNames = failedWriters.map((r) => r.writer).join(", ") |
| 264 | + message += ` (${failedNames}: failed)` |
252 | 265 | } |
| 266 | + |
| 267 | + await client.tui.showToast({ |
| 268 | + body: { |
| 269 | + message, |
| 270 | + variant: failedWriters.length > 0 ? "warning" : "success", |
| 271 | + }, |
| 272 | + }) |
253 | 273 | } |
254 | 274 | } |
255 | 275 | } |
0 commit comments