opencode-rewind 0.1.0

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.
@@ -0,0 +1,687 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+ import { createRequire } from "node:module";
4
+ import { homedir } from "node:os";
5
+ import { join } from "node:path";
6
+ const require = createRequire(import.meta.url);
7
+ // ---------------------------------------------------------------------------
8
+ // Helpers
9
+ // ---------------------------------------------------------------------------
10
+ function defaultDataDir() {
11
+ const dataHome = process.env["XDG_DATA_HOME"] || join(homedir(), ".local", "share");
12
+ return join(dataHome, "opencode");
13
+ }
14
+ function msToIso(ms) {
15
+ return new Date(ms).toISOString();
16
+ }
17
+ /** Safely read and parse a JSON file. Returns null on any error. */
18
+ async function readJson(path) {
19
+ try {
20
+ const raw = await readFile(/* turbopackIgnore: true */ path, "utf-8");
21
+ return JSON.parse(raw);
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
27
+ // ---------------------------------------------------------------------------
28
+ // Content conversion
29
+ // ---------------------------------------------------------------------------
30
+ /** Compute duration from a start/end pair, returning undefined if not available. */
31
+ function partDurationMs(time) {
32
+ if (!time || time.end <= time.start)
33
+ return undefined;
34
+ return time.end - time.start;
35
+ }
36
+ function partsToContent(parts) {
37
+ const content = [];
38
+ for (const part of parts) {
39
+ switch (part.type) {
40
+ case "text":
41
+ if (part.text) {
42
+ content.push({ type: "text", text: part.text });
43
+ }
44
+ break;
45
+ case "reasoning": {
46
+ const dur = partDurationMs(part.time);
47
+ content.push({
48
+ type: "thinking",
49
+ text: part.text,
50
+ ...(dur != null ? { durationMs: dur } : {}),
51
+ });
52
+ break;
53
+ }
54
+ case "tool": {
55
+ // Tool parts represent both the call and result in one.
56
+ // We emit a tool_use and (if output exists) a tool_result.
57
+ // Task tools carry the subagent session ID in metadata.
58
+ const subagentSessionId = part.tool === "task"
59
+ ? part.state.metadata?.sessionId
60
+ : undefined;
61
+ const toolDur = partDurationMs(part.state.time);
62
+ content.push({
63
+ type: "tool_use",
64
+ toolName: part.tool,
65
+ toolCallId: part.callID,
66
+ input: part.state.input,
67
+ ...(subagentSessionId ? { subagentSessionId } : {}),
68
+ ...(toolDur != null ? { durationMs: toolDur } : {}),
69
+ ...(part.state.title ? { title: part.state.title } : {}),
70
+ });
71
+ if (part.state.output !== undefined) {
72
+ content.push({
73
+ type: "tool_result",
74
+ toolCallId: part.callID,
75
+ toolName: part.tool,
76
+ output: part.state.output ?? "",
77
+ isError: part.state.status === "error",
78
+ status: part.state.status,
79
+ ...(toolDur != null ? { durationMs: toolDur } : {}),
80
+ });
81
+ }
82
+ break;
83
+ }
84
+ case "compaction":
85
+ content.push({
86
+ type: "compaction",
87
+ auto: part.auto,
88
+ });
89
+ break;
90
+ case "patch":
91
+ content.push({
92
+ type: "patch",
93
+ hash: part.hash,
94
+ files: part.files,
95
+ });
96
+ break;
97
+ // step-start, step-finish – internal bookkeeping, skip
98
+ }
99
+ }
100
+ if (content.length === 0) {
101
+ content.push({ type: "text", text: "" });
102
+ }
103
+ return content;
104
+ }
105
+ function messageTokenUsage(raw) {
106
+ if (!raw.tokens)
107
+ return undefined;
108
+ const t = raw.tokens;
109
+ const cacheRead = t.cache?.read ?? 0;
110
+ const cacheWrite = t.cache?.write ?? 0;
111
+ // inputTokens = total prompt tokens (non-cached + cache read + cache write)
112
+ const inputTokens = t.input + cacheRead + cacheWrite;
113
+ const outputTokens = t.output;
114
+ const reasoningTokens = t.reasoning || 0;
115
+ return {
116
+ inputTokens,
117
+ outputTokens,
118
+ reasoningTokens: reasoningTokens || undefined,
119
+ cacheReadTokens: cacheRead || undefined,
120
+ cacheCreationTokens: cacheWrite || undefined,
121
+ totalTokens: inputTokens + outputTokens + reasoningTokens,
122
+ };
123
+ }
124
+ // ---------------------------------------------------------------------------
125
+ // Shared message conversion
126
+ // ---------------------------------------------------------------------------
127
+ function resolveModel(raw) {
128
+ if (raw.modelID)
129
+ return raw.modelID;
130
+ if (raw.model?.modelID)
131
+ return raw.model.modelID;
132
+ return undefined;
133
+ }
134
+ function rawErrorToMessageError(raw) {
135
+ if (!raw.error)
136
+ return undefined;
137
+ return {
138
+ name: raw.error.name,
139
+ message: raw.error.data?.message,
140
+ statusCode: raw.error.data?.statusCode,
141
+ isRetryable: raw.error.data?.isRetryable,
142
+ };
143
+ }
144
+ function getPartTime(part) {
145
+ if (part.type === "text" || part.type === "reasoning") {
146
+ return part.time?.start ?? null;
147
+ }
148
+ if (part.type === "tool") {
149
+ return part.state.time?.start ?? null;
150
+ }
151
+ // compaction, step-start, step-finish have no sortable time
152
+ return null;
153
+ }
154
+ function sortParts(parts) {
155
+ parts.sort((a, b) => {
156
+ const aTime = getPartTime(a);
157
+ const bTime = getPartTime(b);
158
+ if (aTime && bTime)
159
+ return aTime - bTime;
160
+ return a.id.localeCompare(b.id);
161
+ });
162
+ }
163
+ async function rawToMessage(raw, loadPartsFn, isCompactSummary) {
164
+ const parts = raw._parts ?? await loadPartsFn(raw.id);
165
+ // Compute timing from time.completed (available on assistant messages)
166
+ const completedAt = raw.time.completed ? msToIso(raw.time.completed) : undefined;
167
+ const durationMs = raw.time.completed && raw.time.created
168
+ ? raw.time.completed - raw.time.created
169
+ : undefined;
170
+ const msg = {
171
+ id: raw.id,
172
+ role: raw.role,
173
+ content: partsToContent(parts),
174
+ timestamp: msToIso(raw.time.created),
175
+ ...(completedAt ? { completedAt } : {}),
176
+ ...(durationMs != null && durationMs > 0 ? { durationMs } : {}),
177
+ model: resolveModel(raw),
178
+ usage: messageTokenUsage(raw),
179
+ parentId: raw.parentID,
180
+ ...(isCompactSummary ? { isCompactSummary: true } : {}),
181
+ error: rawErrorToMessageError(raw),
182
+ raw: { ...raw, _parts: parts },
183
+ };
184
+ if (raw.finish)
185
+ msg.stopReason = raw.finish;
186
+ if (raw.mode)
187
+ msg.mode = raw.mode;
188
+ if (raw.cost != null)
189
+ msg.cost = raw.cost;
190
+ if (raw.providerID)
191
+ msg.apiProvider = raw.providerID;
192
+ if (raw.path?.cwd)
193
+ msg.cwd = raw.path.cwd;
194
+ if (raw.summary)
195
+ msg.summary = raw.summary;
196
+ return msg;
197
+ }
198
+ /**
199
+ * Convert a sorted array of raw messages, detecting compaction boundaries.
200
+ * When a user message contains a compaction part, the next assistant message
201
+ * is marked as a compact summary.
202
+ */
203
+ async function convertMessages(rawMessages, loadPartsFn) {
204
+ const messages = [];
205
+ let nextIsSummary = false;
206
+ for (const raw of rawMessages) {
207
+ const parts = raw._parts ?? [];
208
+ const hasCompaction = parts.some((p) => p.type === "compaction");
209
+ if (hasCompaction) {
210
+ messages.push(await rawToMessage(raw, loadPartsFn));
211
+ nextIsSummary = true;
212
+ }
213
+ else if (nextIsSummary && raw.role === "assistant") {
214
+ messages.push(await rawToMessage(raw, loadPartsFn, true));
215
+ nextIsSummary = false;
216
+ }
217
+ else {
218
+ messages.push(await rawToMessage(raw, loadPartsFn));
219
+ nextIsSummary = false;
220
+ }
221
+ }
222
+ return messages;
223
+ }
224
+ function rawSessionToSummary(raw, projectWorktrees, messageCount, aggregatedUsage, aggregatedCost, extras) {
225
+ const worktree = projectWorktrees.get(raw.projectID);
226
+ return {
227
+ id: raw.id,
228
+ provider: "opencode",
229
+ title: raw.title ?? raw.slug,
230
+ slug: raw.slug,
231
+ createdAt: msToIso(raw.time.created),
232
+ updatedAt: msToIso(raw.time.updated),
233
+ messageCount,
234
+ projectPath: raw.directory ?? worktree,
235
+ usage: aggregatedUsage,
236
+ cost: aggregatedCost,
237
+ parentSessionId: raw.parentID,
238
+ cliVersion: raw.version,
239
+ primaryModel: extras?.primaryModel,
240
+ models: extras?.models,
241
+ toolCallCount: extras?.toolCallCount,
242
+ subagentCount: extras?.subagentCount,
243
+ compactionCount: extras?.compactionCount,
244
+ topTools: extras?.topTools,
245
+ codeChanges: raw.summary
246
+ ? { additions: raw.summary.additions, deletions: raw.summary.deletions, files: raw.summary.files }
247
+ : undefined,
248
+ permissions: raw.permission?.length
249
+ ? raw.permission.map((p) => ({ type: p.type, glob: p.glob, description: p.description }))
250
+ : undefined,
251
+ };
252
+ }
253
+ /** Aggregate token usage and cost from raw messages. */
254
+ function aggregateUsage(rawMessages) {
255
+ let tInput = 0;
256
+ let tOutput = 0;
257
+ let tReasoning = 0;
258
+ let tCacheRead = 0;
259
+ let tCacheWrite = 0;
260
+ let tCost = 0;
261
+ let pInput = 0;
262
+ let hasTokens = false;
263
+ let tDuration = 0;
264
+ let hasDuration = false;
265
+ for (const msg of rawMessages) {
266
+ if (msg.tokens) {
267
+ hasTokens = true;
268
+ const cr = msg.tokens.cache?.read || 0;
269
+ const cw = msg.tokens.cache?.write || 0;
270
+ const turnIn = (msg.tokens.input || 0) + cr + cw;
271
+ tInput += turnIn;
272
+ tOutput += msg.tokens.output || 0;
273
+ tReasoning += msg.tokens.reasoning || 0;
274
+ tCacheRead += cr;
275
+ tCacheWrite += cw;
276
+ if (turnIn > pInput)
277
+ pInput = turnIn;
278
+ }
279
+ if (msg.cost)
280
+ tCost += msg.cost;
281
+ if (msg.time.completed && msg.time.created) {
282
+ const dur = msg.time.completed - msg.time.created;
283
+ if (dur > 0) {
284
+ tDuration += dur;
285
+ hasDuration = true;
286
+ }
287
+ }
288
+ }
289
+ return {
290
+ usage: hasTokens
291
+ ? {
292
+ inputTokens: tInput,
293
+ outputTokens: tOutput,
294
+ reasoningTokens: tReasoning || undefined,
295
+ cacheReadTokens: tCacheRead || undefined,
296
+ cacheCreationTokens: tCacheWrite || undefined,
297
+ totalTokens: tInput + tOutput + tReasoning,
298
+ peakInputTokens: pInput || undefined,
299
+ }
300
+ : undefined,
301
+ cost: tCost > 0 ? tCost : undefined,
302
+ totalDurationMs: hasDuration ? tDuration : undefined,
303
+ };
304
+ }
305
+ function aggregateSummaryExtras(rawMessages) {
306
+ const modelCounts = new Map();
307
+ const toolCounts = new Map();
308
+ let toolCallCount = 0;
309
+ let subagentCount = 0;
310
+ let compactionCount = 0;
311
+ for (const raw of rawMessages) {
312
+ const model = resolveModel(raw);
313
+ if (model) {
314
+ modelCounts.set(model, (modelCounts.get(model) ?? 0) + 1);
315
+ }
316
+ for (const part of raw._parts ?? []) {
317
+ if (part.type === "tool") {
318
+ toolCounts.set(part.tool, (toolCounts.get(part.tool) ?? 0) + 1);
319
+ const isSubagent = part.tool === "task" && Boolean(part.state.metadata?.sessionId);
320
+ if (isSubagent)
321
+ subagentCount++;
322
+ else
323
+ toolCallCount++;
324
+ }
325
+ if (part.type === "compaction")
326
+ compactionCount++;
327
+ }
328
+ }
329
+ const models = Array.from(modelCounts.entries())
330
+ .sort((a, b) => b[1] - a[1])
331
+ .map(([model]) => model);
332
+ const topTools = Array.from(toolCounts.entries())
333
+ .sort((a, b) => b[1] - a[1])
334
+ .slice(0, 8)
335
+ .map(([name, count]) => ({ name, count }));
336
+ return {
337
+ primaryModel: models[0],
338
+ ...(models.length > 0 ? { models } : {}),
339
+ ...(toolCallCount > 0 ? { toolCallCount } : {}),
340
+ ...(subagentCount > 0 ? { subagentCount } : {}),
341
+ ...(compactionCount > 0 ? { compactionCount } : {}),
342
+ ...(topTools.length > 0 ? { topTools } : {}),
343
+ };
344
+ }
345
+ function createSqliteBackend(dbPath) {
346
+ const Database = require("better-sqlite3");
347
+ const db = new Database(dbPath, { readonly: true });
348
+ // Prepare statements for reuse
349
+ const stmtProjects = db.prepare("SELECT id, worktree FROM project");
350
+ const stmtSessions = db.prepare(`
351
+ SELECT id, project_id, parent_id, slug, directory, title, version,
352
+ summary_additions, summary_deletions, summary_files,
353
+ permission, time_created, time_updated
354
+ FROM session
355
+ WHERE time_archived IS NULL
356
+ ORDER BY time_updated DESC
357
+ `);
358
+ const stmtMessages = db.prepare(`
359
+ SELECT id, session_id, time_created, time_updated, data
360
+ FROM message
361
+ WHERE session_id = ?
362
+ ORDER BY time_created ASC
363
+ `);
364
+ const stmtParts = db.prepare(`
365
+ SELECT id, message_id, session_id, time_created, time_updated, data
366
+ FROM part
367
+ WHERE message_id = ?
368
+ ORDER BY time_created ASC
369
+ `);
370
+ const stmtPartsBySession = db.prepare(`
371
+ SELECT id, message_id, session_id, time_created, time_updated, data
372
+ FROM part
373
+ WHERE session_id = ?
374
+ ORDER BY time_created ASC
375
+ `);
376
+ const stmtTodos = db.prepare(`
377
+ SELECT session_id, content, status, priority, position, time_created, time_updated
378
+ FROM todo
379
+ WHERE session_id = ?
380
+ ORDER BY position ASC
381
+ `);
382
+ function rowToSession(row) {
383
+ const session = {
384
+ id: row.id,
385
+ projectID: row.project_id,
386
+ slug: row.slug || undefined,
387
+ directory: row.directory || undefined,
388
+ parentID: row.parent_id || undefined,
389
+ title: row.title || undefined,
390
+ version: row.version || undefined,
391
+ time: {
392
+ created: row.time_created,
393
+ updated: row.time_updated,
394
+ },
395
+ };
396
+ // Reconstruct summary if data exists
397
+ if (row.summary_additions != null || row.summary_deletions != null || row.summary_files != null) {
398
+ session.summary = {
399
+ additions: row.summary_additions ?? 0,
400
+ deletions: row.summary_deletions ?? 0,
401
+ files: row.summary_files ?? 0,
402
+ };
403
+ }
404
+ // Parse permission JSON if present
405
+ if (row.permission) {
406
+ try {
407
+ session.permission = JSON.parse(row.permission);
408
+ }
409
+ catch {
410
+ // ignore malformed permission
411
+ }
412
+ }
413
+ return session;
414
+ }
415
+ function rowToRawMessage(row) {
416
+ const id = row.id;
417
+ const sessionID = row.session_id;
418
+ const data = JSON.parse(row.data);
419
+ // Reconstruct OpenCodeRawMessage from the row + embedded data.
420
+ const msg = {
421
+ id,
422
+ sessionID,
423
+ role: data.role,
424
+ time: data.time,
425
+ parentID: data.parentID,
426
+ modelID: data.modelID,
427
+ providerID: data.providerID,
428
+ mode: data.mode,
429
+ agent: data.agent,
430
+ cost: data.cost,
431
+ tokens: data.tokens,
432
+ finish: data.finish,
433
+ model: data.model,
434
+ summary: data.summary,
435
+ path: data.path,
436
+ tools: data.tools,
437
+ error: data.error,
438
+ };
439
+ return msg;
440
+ }
441
+ function rowToPart(row) {
442
+ const id = row.id;
443
+ const messageID = row.message_id;
444
+ const sessionID = row.session_id;
445
+ const data = JSON.parse(row.data);
446
+ // Re-attach row metadata stripped from the JSON payload.
447
+ return { ...data, id, messageID, sessionID };
448
+ }
449
+ return {
450
+ loadProjectWorktrees() {
451
+ const map = new Map();
452
+ const rows = stmtProjects.all();
453
+ for (const row of rows) {
454
+ map.set(row.id, row.worktree);
455
+ }
456
+ return map;
457
+ },
458
+ loadAllSessions() {
459
+ const rows = stmtSessions.all();
460
+ return rows.map(rowToSession);
461
+ },
462
+ loadRawMessages(sessionId) {
463
+ const rows = stmtMessages.all(sessionId);
464
+ return rows.map(rowToRawMessage);
465
+ },
466
+ loadParts(messageId) {
467
+ const rows = stmtParts.all(messageId);
468
+ const parts = rows.map(rowToPart);
469
+ sortParts(parts);
470
+ return parts;
471
+ },
472
+ loadPartsBySession(sessionId) {
473
+ const rows = stmtPartsBySession.all(sessionId);
474
+ const map = new Map();
475
+ for (const row of rows) {
476
+ const part = rowToPart(row);
477
+ const msgId = row.message_id;
478
+ let arr = map.get(msgId);
479
+ if (!arr) {
480
+ arr = [];
481
+ map.set(msgId, arr);
482
+ }
483
+ arr.push(part);
484
+ }
485
+ // Sort parts within each message
486
+ for (const parts of map.values()) {
487
+ sortParts(parts);
488
+ }
489
+ return map;
490
+ },
491
+ loadTodos(sessionId) {
492
+ const rows = stmtTodos.all(sessionId);
493
+ return rows.map((row) => ({
494
+ id: `todo-${row.session_id}-${row.position}`,
495
+ sessionID: row.session_id,
496
+ content: row.content,
497
+ status: row.status,
498
+ priority: row.priority,
499
+ time: {
500
+ created: row.time_created,
501
+ updated: row.time_updated,
502
+ },
503
+ }));
504
+ },
505
+ close() {
506
+ db.close();
507
+ },
508
+ };
509
+ }
510
+ export function createOpenCodeProvider(config = {}) {
511
+ // Resolve the base data directory and SQLite database path.
512
+ //
513
+ // Path resolution strategy:
514
+ // 1. No config → use default data dir (~/.local/share/opencode)
515
+ // 2. Config basePath provided:
516
+ // a. If <basePath>/opencode.db exists → basePath is the data dir
517
+ // b. If <basePath>/storage/ exists → basePath is the data dir
518
+ // c. Otherwise → treat basePath as the storage dir and use ../opencode.db
519
+ let dataDir;
520
+ let storageDir;
521
+ let dbPath;
522
+ if (config?.basePath) {
523
+ const bp = config.basePath;
524
+ if (existsSync(join(/* turbopackIgnore: true */ bp, "opencode.db"))) {
525
+ dataDir = bp;
526
+ storageDir = join(/* turbopackIgnore: true */ bp, "storage");
527
+ dbPath = join(/* turbopackIgnore: true */ bp, "opencode.db");
528
+ }
529
+ else if (existsSync(join(/* turbopackIgnore: true */ bp, "storage"))) {
530
+ dataDir = bp;
531
+ storageDir = join(/* turbopackIgnore: true */ bp, "storage");
532
+ dbPath = join(/* turbopackIgnore: true */ bp, "opencode.db");
533
+ }
534
+ else {
535
+ dataDir = join(/* turbopackIgnore: true */ bp, "..");
536
+ storageDir = bp;
537
+ dbPath = join(/* turbopackIgnore: true */ dataDir, "opencode.db");
538
+ }
539
+ }
540
+ else {
541
+ dataDir = defaultDataDir();
542
+ storageDir = join(/* turbopackIgnore: true */ dataDir, "storage");
543
+ dbPath = join(/* turbopackIgnore: true */ dataDir, "opencode.db");
544
+ }
545
+ const hasSqlite = existsSync(/* turbopackIgnore: true */ dbPath);
546
+ const sessionDiffDir = join(/* turbopackIgnore: true */ storageDir, "session_diff");
547
+ // Lazily initialized SQLite backend
548
+ let _sqlite = null;
549
+ function getSqlite() {
550
+ if (!_sqlite) {
551
+ _sqlite = createSqliteBackend(dbPath);
552
+ }
553
+ return _sqlite;
554
+ }
555
+ // ---- Session diff helper ----
556
+ async function loadSessionDiffsFromStorage(sessionId) {
557
+ const filePath = join(sessionDiffDir, `${sessionId}.json`);
558
+ const data = await readJson(filePath);
559
+ if (!data || !Array.isArray(data))
560
+ return [];
561
+ return data
562
+ .map(({ path: _path, ...diff }) => ({ ...diff, file: diff.file ?? _path }))
563
+ .filter((diff) => Boolean(diff.file));
564
+ }
565
+ // ---- SQLite-backed data access ----
566
+ async function loadProjectWorktrees() {
567
+ return hasSqlite ? getSqlite().loadProjectWorktrees() : new Map();
568
+ }
569
+ async function loadAllSessions() {
570
+ return hasSqlite ? getSqlite().loadAllSessions() : [];
571
+ }
572
+ async function loadRawMessages(sessionId) {
573
+ return hasSqlite ? getSqlite().loadRawMessages(sessionId) : [];
574
+ }
575
+ async function loadParts(messageId) {
576
+ return hasSqlite ? getSqlite().loadParts(messageId) : [];
577
+ }
578
+ async function loadAndAttachParts(rawMessages, sessionId) {
579
+ if (!hasSqlite)
580
+ return;
581
+ const partsByMessage = getSqlite().loadPartsBySession(sessionId);
582
+ for (const msg of rawMessages) {
583
+ msg._parts = partsByMessage.get(msg.id) ?? [];
584
+ }
585
+ }
586
+ async function loadSessionDiffs(sessionId) {
587
+ return loadSessionDiffsFromStorage(sessionId);
588
+ }
589
+ async function loadSessionTodos(sessionId) {
590
+ return hasSqlite ? getSqlite().loadTodos(sessionId) : [];
591
+ }
592
+ // ---- Provider API ----
593
+ const reportedBasePath = dataDir;
594
+ return {
595
+ id: "opencode",
596
+ name: "OpenCode",
597
+ basePath: reportedBasePath,
598
+ async listSessions(options) {
599
+ const projectWorktrees = await loadProjectWorktrees();
600
+ let sessions = await loadAllSessions();
601
+ // Filter subagent sessions (excluded by default)
602
+ if (!options?.includeSubagents) {
603
+ sessions = sessions.filter((s) => !s.parentID);
604
+ }
605
+ // Apply filters
606
+ if (options?.projectPath) {
607
+ const pp = options.projectPath;
608
+ sessions = sessions.filter((s) => {
609
+ if (s.directory === pp)
610
+ return true;
611
+ // Also check project worktree
612
+ const worktree = projectWorktrees.get(s.projectID);
613
+ return worktree === pp;
614
+ });
615
+ }
616
+ if (options?.after) {
617
+ const afterMs = new Date(options.after).getTime();
618
+ sessions = sessions.filter((s) => s.time.updated >= afterMs);
619
+ }
620
+ if (options?.before) {
621
+ const beforeMs = new Date(options.before).getTime();
622
+ sessions = sessions.filter((s) => s.time.updated <= beforeMs);
623
+ }
624
+ // Sort by updated DESC
625
+ sessions.sort((a, b) => b.time.updated - a.time.updated);
626
+ // Apply limit
627
+ if (options?.limit && options.limit > 0) {
628
+ sessions = sessions.slice(0, options.limit);
629
+ }
630
+ const results = [];
631
+ for (const raw of sessions) {
632
+ const rawMessages = await loadRawMessages(raw.id);
633
+ await loadAndAttachParts(rawMessages, raw.id);
634
+ const { usage, cost, totalDurationMs } = aggregateUsage(rawMessages);
635
+ const extras = aggregateSummaryExtras(rawMessages);
636
+ const summary = rawSessionToSummary(raw, projectWorktrees, rawMessages.length, usage, cost, extras);
637
+ if (totalDurationMs != null)
638
+ summary.totalDurationMs = totalDurationMs;
639
+ results.push(summary);
640
+ }
641
+ return results;
642
+ },
643
+ async getSession(sessionId) {
644
+ const projectWorktrees = await loadProjectWorktrees();
645
+ const allSessions = await loadAllSessions();
646
+ const rawSession = allSessions.find((s) => s.id === sessionId);
647
+ if (!rawSession)
648
+ return null;
649
+ const rawMessages = await loadRawMessages(sessionId);
650
+ await loadAndAttachParts(rawMessages, sessionId);
651
+ const messages = await convertMessages(rawMessages, loadParts);
652
+ const { usage, cost } = aggregateUsage(rawMessages);
653
+ const summary = rawSessionToSummary(rawSession, projectWorktrees, messages.length, usage, cost);
654
+ // Compute totalDurationMs from per-message timing
655
+ let totalDuration = 0;
656
+ let hasTiming = false;
657
+ for (const m of messages) {
658
+ if (m.durationMs != null) {
659
+ totalDuration += m.durationMs;
660
+ hasTiming = true;
661
+ }
662
+ }
663
+ // Load session diffs and todos
664
+ const diffs = await loadSessionDiffs(sessionId);
665
+ const todos = await loadSessionTodos(sessionId);
666
+ return {
667
+ ...summary,
668
+ ...(hasTiming ? { totalDurationMs: totalDuration } : {}),
669
+ messages,
670
+ ...(diffs.length > 0 ? { diffs } : {}),
671
+ ...(todos.length > 0 ? { todos } : {}),
672
+ };
673
+ },
674
+ async getMessages(sessionId) {
675
+ const rawMessages = await loadRawMessages(sessionId);
676
+ await loadAndAttachParts(rawMessages, sessionId);
677
+ return await convertMessages(rawMessages, loadParts);
678
+ },
679
+ close() {
680
+ if (_sqlite) {
681
+ _sqlite.close();
682
+ _sqlite = null;
683
+ }
684
+ },
685
+ };
686
+ }
687
+ //# sourceMappingURL=opencode.js.map