(
sessionId: string,
messages: Message[],
usage: { input: number; output: number; costUsd: number },
title?: string,
)
| 201 | } |
| 202 | |
| 203 | /** |
| 204 | * Create a session row with a caller-chosen id if it doesn't exist yet (idempotent). |
| 205 | * Used for sub-agent sessions, whose ids are derived from the parent's |
| 206 | * (`<parent>/sub-<ts>`, `<parent>/fanout-<n>`, …): messages.session_id has a FK to |
| 207 | * sessions.id, so the row MUST exist before the sub-agent's first recordTurn — |
| 208 | * otherwise every child write dies with "FOREIGN KEY constraint failed". |
| 209 | */ |
| 210 | ensureSession(id: string, cwd: string, model: string): void { |
| 211 | this.db.prepare( |
| 212 | `INSERT OR IGNORE INTO sessions (id, cwd, model, title) VALUES (?, ?, ?, ?)`, |
| 213 | ).run(id, cwd, model, null); |
| 214 | } |
| 215 | |
| 216 | recordTurn( |
| 217 | sessionId: string, |
| 218 | messages: Message[], |
| 219 | usage: { input: number; output: number; costUsd: number }, |
| 220 | title?: string, |
| 221 | ): void { |
| 222 | const hasUserMessage = messages.some(m => m.role === 'user'); |
| 223 | // Reuse the same turn_number for all messages in this batch. |
| 224 | // Only bump to a new turn_number when this batch starts a new user turn. |
| 225 | const currentMax = this.getCurrentTurnNumber(sessionId); |
| 226 | const turnNumber = hasUserMessage ? currentMax + 1 : Math.max(currentMax, 1); |
| 227 | |
| 228 | const tx = this.db.transaction(() => { |
| 229 | for (const msg of messages) { |
| 230 | this.insertMessage.run( |
| 231 | sessionId, |
| 232 | turnNumber, |
| 233 | msg.role, |
| 234 | msg.content ?? null, |
| 235 | msg.tool_calls ? JSON.stringify(msg.tool_calls) : null, |
| 236 | msg.tool_call_id ?? null, |
| 237 | msg.name ?? null, |
no test coverage detected