spiracha 1.4.0 → 1.5.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.
- package/AGENTS.md +48 -5
- package/README.md +40 -5
- package/apps/ui/AGENTS.md +23 -4
- package/apps/ui/README.md +49 -2
- package/apps/ui/dist/client/assets/analytics-DOy1b6Sh.js +1 -0
- package/apps/ui/dist/client/assets/{antigravity-conversations._conversationId-DmD0sN8X.js → antigravity-conversations._conversationId-DqySVO2K.js} +2 -2
- package/apps/ui/dist/client/assets/antigravity-keychain-panel-B77qJz6f.js +1 -0
- package/apps/ui/dist/client/assets/antigravity._workspaceKey-B3NM6gRH.js +1 -0
- package/apps/ui/dist/client/assets/{antigravity.index-DEEkjlZa.js → antigravity.index-DP9JC2P9.js} +1 -1
- package/apps/ui/dist/client/assets/{badge-DfWvPfKF.js → badge-C3i4OKs8.js} +1 -1
- package/apps/ui/dist/client/assets/claude-code-sessions._sessionId-8pQ55ci2.js +1 -0
- package/apps/ui/dist/client/assets/claude-code-sessions._sessionId-DVz5UmtU.js +1 -0
- package/apps/ui/dist/client/assets/claude-code._workspaceKey-BBKskSum.js +1 -0
- package/apps/ui/dist/client/assets/claude-code._workspaceKey-OkikMUFs.js +1 -0
- package/apps/ui/dist/client/assets/claude-code.index-Cwu76dbb.js +1 -0
- package/apps/ui/dist/client/assets/claude-code.index-D6eMd08t.js +1 -0
- package/apps/ui/dist/client/assets/createServerFn-B-KkwmHU.js +3 -0
- package/apps/ui/dist/client/assets/cursor-threads._composerId-hBWlP1D-.js +1 -0
- package/apps/ui/dist/client/assets/cursor._workspaceKey-BFPkIRi-.js +1 -0
- package/apps/ui/dist/client/assets/cursor.index-8y4o20kb.js +2 -0
- package/apps/ui/dist/client/assets/delete-confirm-dialog-Bxs07u-j.js +7 -0
- package/apps/ui/dist/client/assets/dist-BrJhN2A0.js +5 -0
- package/apps/ui/dist/client/assets/{dist-BhZOxAPP.js → dist-Cmd3AIfD.js} +1 -1
- package/apps/ui/dist/client/assets/dropdown-menu-D5cvPaZt.js +1 -0
- package/apps/ui/dist/client/assets/{es2015-BkCWttyM.js → es2015-ad6040dw.js} +1 -1
- package/apps/ui/dist/client/assets/export-dialog-Dr8d2MqD.js +1 -0
- package/apps/ui/dist/client/assets/{formatters-BM9kB7ed.js → formatters-BtqyrX_c.js} +1 -1
- package/apps/ui/dist/client/assets/index-C-STgIH4.js +166 -0
- package/apps/ui/dist/client/assets/kiro-sessions._sessionId-CKgfJ7GR.js +1 -0
- package/apps/ui/dist/client/assets/kiro-sessions._sessionId-zvIJHtQF.js +1 -0
- package/apps/ui/dist/client/assets/kiro._workspaceKey-DhVuJuZB.js +1 -0
- package/apps/ui/dist/client/assets/kiro._workspaceKey-QArsjkTY.js +1 -0
- package/apps/ui/dist/client/assets/kiro.index-C-fr4m7i.js +1 -0
- package/apps/ui/dist/client/assets/kiro.index-OYgAdtZd.js +1 -0
- package/apps/ui/dist/client/assets/opencode-sessions._sessionId-DLjiTst-.js +1 -0
- package/apps/ui/dist/client/assets/opencode-sessions._sessionId-biBwrRbt.js +8 -0
- package/apps/ui/dist/client/assets/opencode._workspaceKey-C7OIzMOJ.js +1 -0
- package/apps/ui/dist/client/assets/opencode._workspaceKey-sBakX0oI.js +1 -0
- package/apps/ui/dist/client/assets/opencode.index-Bl_kNs9N.js +1 -0
- package/apps/ui/dist/client/assets/opencode.index-CN53bVmb.js +1 -0
- package/apps/ui/dist/client/assets/preload-helper-B8fFrdxC.js +1 -0
- package/apps/ui/dist/client/assets/projects._project-D8WloxQN.js +1 -0
- package/apps/ui/dist/client/assets/projects.index-5xRtuJdX.js +3 -0
- package/apps/ui/dist/client/assets/routes-DnzF_9CX.js +1 -0
- package/apps/ui/dist/client/assets/select-CYF6pF37.js +1 -0
- package/apps/ui/dist/client/assets/settings-Dx70jgP_.js +1 -0
- package/apps/ui/dist/client/assets/styles-B7Zi9EFA.css +1 -0
- package/apps/ui/dist/client/assets/tabs-CmuhkT-c.js +7 -0
- package/apps/ui/dist/client/assets/thread-transcript-stats-BUNz6u7F.js +1 -0
- package/apps/ui/dist/client/assets/threads._threadId-BMRSFow3.js +1 -0
- package/apps/ui/dist/client/assets/useMutation-BSFult2i.js +1 -0
- package/apps/ui/dist/client/assets/useQuery-CjINKZf1.js +1 -0
- package/apps/ui/dist/client/icon.svg +2 -1
- package/apps/ui/dist/server/assets/_tanstack-start-manifest_v-CQ8Jjs3B.js +387 -0
- package/apps/ui/dist/server/assets/{antigravity-conversations._conversationId-BtexWY-K.js → antigravity-conversations._conversationId-BKOyL2T6.js} +2 -2
- package/apps/ui/dist/server/assets/{antigravity-conversations._conversationId-zMrR6v6R.js → antigravity-conversations._conversationId-eS87Dx7g.js} +1 -1
- package/apps/ui/dist/server/assets/claude-code-db-Ik3VStKZ.js +361 -0
- package/apps/ui/dist/server/assets/claude-code-queries-Cgzi4K-e.js +37 -0
- package/apps/ui/dist/server/assets/claude-code-server-PfraClXS.js +72 -0
- package/apps/ui/dist/server/assets/claude-code-sessions._sessionId-C55oe6zj.js +18 -0
- package/apps/ui/dist/server/assets/claude-code-sessions._sessionId-D1RYYi3j.js +473 -0
- package/apps/ui/dist/server/assets/claude-code-sessions._sessionId-DSvPLEUL.js +11 -0
- package/apps/ui/dist/server/assets/claude-code-transcript-C-bxh3cS.js +139 -0
- package/apps/ui/dist/server/assets/claude-code-transcript-phase-DmUMc8VU.js +10 -0
- package/apps/ui/dist/server/assets/claude-code._workspaceKey-B5Xg_-tE.js +28 -0
- package/apps/ui/dist/server/assets/claude-code._workspaceKey-BPlzyXPa.js +11 -0
- package/apps/ui/dist/server/assets/claude-code._workspaceKey-DqDve2wl.js +191 -0
- package/apps/ui/dist/server/assets/claude-code.index-B7yYR7dD.js +92 -0
- package/apps/ui/dist/server/assets/claude-code.index-BUHThG1W.js +11 -0
- package/apps/ui/dist/server/assets/{codex-server-CNXSJuc2.js → codex-server-CGXU4H8-.js} +319 -29
- package/apps/ui/dist/server/assets/{cursor-db-cYZEU3WQ.js → cursor-db-CZnYy7aT.js} +3 -3
- package/apps/ui/dist/server/assets/{cursor-recovery-nq-kR62j.js → cursor-recovery-CXpO8KcD.js} +2 -2
- package/apps/ui/dist/server/assets/{cursor-server-C3q7hrp-.js → cursor-server-DNu52A3K.js} +13 -13
- package/apps/ui/dist/server/assets/{cursor-threads._composerId-BkMMTQ3v.js → cursor-threads._composerId-Ckn6ItHI.js} +3 -3
- package/apps/ui/dist/server/assets/{cursor-threads._composerId-BPJFWfJj.js → cursor-threads._composerId-p9-ARAzc.js} +1 -1
- package/apps/ui/dist/server/assets/{cursor-transcript-2iL3KFSK.js → cursor-transcript-B7tGnqtn.js} +1 -1
- package/apps/ui/dist/server/assets/{cursor._workspaceKey-C5PdTaKB.js → cursor._workspaceKey-CZuohIUL.js} +1 -1
- package/apps/ui/dist/server/assets/{cursor._workspaceKey-Ckj7apKI.js → cursor._workspaceKey-DPp5JSjy.js} +2 -2
- package/apps/ui/dist/server/assets/{export-dialog-BItjWgkZ.js → export-dialog-D88ze9Gy.js} +2 -2
- package/apps/ui/dist/server/assets/kiro-db--fXrED_-.js +552 -0
- package/apps/ui/dist/server/assets/kiro-queries-CzNlM2ok.js +37 -0
- package/apps/ui/dist/server/assets/kiro-server-9WRxg2Au.js +72 -0
- package/apps/ui/dist/server/assets/kiro-sessions._sessionId-CRY2c7YO.js +11 -0
- package/apps/ui/dist/server/assets/kiro-sessions._sessionId-F1UlwxmR.js +449 -0
- package/apps/ui/dist/server/assets/kiro-sessions._sessionId-bvTulT7C.js +18 -0
- package/apps/ui/dist/server/assets/kiro-transcript-DoekQBaH.js +112 -0
- package/apps/ui/dist/server/assets/kiro-transcript-phase-CJG40nQg.js +27 -0
- package/apps/ui/dist/server/assets/kiro._workspaceKey-BDe9sQwo.js +11 -0
- package/apps/ui/dist/server/assets/kiro._workspaceKey-CJsjgNwO.js +28 -0
- package/apps/ui/dist/server/assets/kiro._workspaceKey-D_-4Qb7y.js +193 -0
- package/apps/ui/dist/server/assets/kiro.index-C__-UROk.js +11 -0
- package/apps/ui/dist/server/assets/kiro.index-CmTbqeYj.js +93 -0
- package/apps/ui/dist/server/assets/opencode-db-BM7KjOzc.js +397 -0
- package/apps/ui/dist/server/assets/opencode-queries-BeNYXnQv.js +50 -0
- package/apps/ui/dist/server/assets/opencode-server-BpQ_kRze.js +72 -0
- package/apps/ui/dist/server/assets/opencode-sessions._sessionId-CGQ-t0vD.js +518 -0
- package/apps/ui/dist/server/assets/opencode-sessions._sessionId-D8Q7in_Z.js +18 -0
- package/apps/ui/dist/server/assets/opencode-sessions._sessionId-DKOvi0lV.js +11 -0
- package/apps/ui/dist/server/assets/opencode-think-tags-CAoD-EcZ.js +87 -0
- package/apps/ui/dist/server/assets/opencode-transcript-BNTcGNym.js +141 -0
- package/apps/ui/dist/server/assets/opencode-transcript-phase-OqLkiQD0.js +32 -0
- package/apps/ui/dist/server/assets/opencode._workspaceKey-BdqcWHw1.js +11 -0
- package/apps/ui/dist/server/assets/opencode._workspaceKey-CfH96HTT.js +28 -0
- package/apps/ui/dist/server/assets/opencode._workspaceKey-DdbHPDrO.js +207 -0
- package/apps/ui/dist/server/assets/opencode.index-BbVzkJXP.js +11 -0
- package/apps/ui/dist/server/assets/opencode.index-Vh_Gq_1L.js +93 -0
- package/apps/ui/dist/server/assets/{projects._project-Bpbc3C-L.js → projects._project-4io5LO0E.js} +1 -1
- package/apps/ui/dist/server/assets/{projects._project-CvZho6EQ.js → projects._project-D6ZoGhJ8.js} +2 -2
- package/apps/ui/dist/server/assets/{router-6m-ihwqA.js → router-Sac2DGk6.js} +157 -49
- package/apps/ui/dist/server/assets/{shared-CPRNYIql.js → shared-DyhChtHf.js} +4 -1
- package/apps/ui/dist/server/assets/source-session-export-server-DNutCawb.js +38 -0
- package/apps/ui/dist/server/assets/{start-CI_0bSiY.js → start-B_HFwkkk.js} +1 -1
- package/apps/ui/dist/server/assets/{tabs-CGA13IZM.js → tabs-DkaChNVg.js} +0 -1
- package/apps/ui/dist/server/assets/thread-transcript-stats-Dh34mt2u.js +45 -0
- package/apps/ui/dist/server/assets/{threads._threadId-D2GYkVn6.js → threads._threadId-BeXEc4Lo.js} +15 -6
- package/apps/ui/dist/server/assets/{threads._threadId-Bs0yuqO9.js → threads._threadId-Ci0S4mKU.js} +1 -1
- package/apps/ui/dist/server/server.js +72 -24
- package/package.json +21 -4
- package/src/lib/claude-code-db.ts +655 -0
- package/src/lib/claude-code-exporter-types.ts +110 -0
- package/src/lib/claude-code-transcript-phase.ts +25 -0
- package/src/lib/claude-code-transcript.ts +180 -0
- package/src/lib/codex-browser-db.ts +490 -31
- package/src/lib/codex-browser-export.ts +18 -2
- package/src/lib/codex-thread-cache.ts +17 -1
- package/src/lib/concurrency.ts +25 -0
- package/src/lib/kiro-db.ts +949 -0
- package/src/lib/kiro-exporter-types.ts +100 -0
- package/src/lib/kiro-transcript-phase.ts +41 -0
- package/src/lib/kiro-transcript.ts +115 -0
- package/src/lib/opencode-db.ts +610 -0
- package/src/lib/opencode-exporter-types.ts +129 -0
- package/src/lib/opencode-think-tags.ts +126 -0
- package/src/lib/opencode-transcript-phase.ts +50 -0
- package/src/lib/opencode-transcript.ts +194 -0
- package/src/lib/shared.ts +25 -0
- package/src/lib/ui-export-archive.ts +61 -0
- package/apps/ui/dist/client/assets/analytics-DK2jdvmI.js +0 -1
- package/apps/ui/dist/client/assets/antigravity-keychain-panel-BRHVjFAM.js +0 -1
- package/apps/ui/dist/client/assets/antigravity._workspaceKey-CGV7amas.js +0 -1
- package/apps/ui/dist/client/assets/createServerFn-CgRRVpBH.js +0 -3
- package/apps/ui/dist/client/assets/cursor-threads._composerId-Ti2gTm63.js +0 -1
- package/apps/ui/dist/client/assets/cursor._workspaceKey-DzD_c6j9.js +0 -1
- package/apps/ui/dist/client/assets/cursor.index-PkCFVcl4.js +0 -2
- package/apps/ui/dist/client/assets/delete-confirm-dialog-BHRmyLHr.js +0 -11
- package/apps/ui/dist/client/assets/dropdown-menu-Dy5cLzTy.js +0 -1
- package/apps/ui/dist/client/assets/export-dialog-C86SYHJn.js +0 -1
- package/apps/ui/dist/client/assets/index-CPvAP-jk.js +0 -149
- package/apps/ui/dist/client/assets/projects._project-Ckf6muWZ.js +0 -1
- package/apps/ui/dist/client/assets/projects.index-V5ibNZII.js +0 -3
- package/apps/ui/dist/client/assets/routes-Df2tgpyd.js +0 -1
- package/apps/ui/dist/client/assets/select-Bf2GwfPR.js +0 -1
- package/apps/ui/dist/client/assets/settings-5VQIzME0.js +0 -1
- package/apps/ui/dist/client/assets/styles-BhRkXgwB.css +0 -1
- package/apps/ui/dist/client/assets/tabs-OO4VU5KR.js +0 -7
- package/apps/ui/dist/client/assets/threads._threadId-BgDhFj2I.js +0 -1
- package/apps/ui/dist/client/assets/useMutation-CC_B7uy5.js +0 -1
- package/apps/ui/dist/client/assets/useQuery-C5By6WKU.js +0 -1
- package/apps/ui/dist/server/assets/_tanstack-start-manifest_v-CRW2kqj0.js +0 -232
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { Database } from 'bun:sqlite';
|
|
1
|
+
import { constants, Database } from 'bun:sqlite';
|
|
2
|
+
import { closeSync, openSync, readdirSync, readSync, statSync } from 'node:fs';
|
|
2
3
|
import { rm } from 'node:fs/promises';
|
|
3
4
|
import os from 'node:os';
|
|
4
5
|
import path from 'node:path';
|
|
6
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
7
|
+
import { pathToFileURL } from 'node:url';
|
|
5
8
|
import type {
|
|
6
9
|
DashboardSummary,
|
|
7
10
|
DeleteProjectResult,
|
|
@@ -30,6 +33,44 @@ type DeleteProjectOptions = {
|
|
|
30
33
|
const SQLITE_DELETE_BATCH_SIZE = 400;
|
|
31
34
|
const SESSION_FILE_DELETE_CONCURRENCY = 16;
|
|
32
35
|
const THREAD_LIST_IO_CONCURRENCY = 8;
|
|
36
|
+
const CODEX_READONLY_DB_OPEN_FLAGS = constants.SQLITE_OPEN_READONLY | constants.SQLITE_OPEN_URI;
|
|
37
|
+
const JSONL_READ_CHUNK_BYTES = 64 * 1024;
|
|
38
|
+
const SESSION_META_READ_LIMIT_BYTES = 4 * 1024 * 1024;
|
|
39
|
+
const THREAD_ID_PATTERN = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/iu;
|
|
40
|
+
|
|
41
|
+
type SessionIndexEntry = {
|
|
42
|
+
id: string;
|
|
43
|
+
thread_name?: string;
|
|
44
|
+
updated_at?: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
type FallbackSessionMeta = {
|
|
48
|
+
agent_nickname?: string;
|
|
49
|
+
agent_path?: string;
|
|
50
|
+
agent_role?: string;
|
|
51
|
+
cli_version?: string;
|
|
52
|
+
cwd?: string;
|
|
53
|
+
forked_from_id?: string;
|
|
54
|
+
id?: string;
|
|
55
|
+
model_provider?: string;
|
|
56
|
+
parent_thread_id?: string;
|
|
57
|
+
source?: unknown;
|
|
58
|
+
thread_source?: string;
|
|
59
|
+
timestamp?: string;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
type ReadFallbackThreadRowsOptions = {
|
|
63
|
+
includeSubagents?: boolean;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
type FallbackRolloutStats = {
|
|
67
|
+
model: string | null;
|
|
68
|
+
tokensUsed: number;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const isSqliteCantOpenError = (error: unknown) => {
|
|
72
|
+
return (error as { code?: unknown }).code === 'SQLITE_CANTOPEN';
|
|
73
|
+
};
|
|
33
74
|
|
|
34
75
|
const chunkValues = <T>(values: T[], chunkSize: number) => {
|
|
35
76
|
const chunks: T[][] = [];
|
|
@@ -49,15 +90,20 @@ const isPromiseLike = (value: unknown): value is PromiseLike<unknown> => {
|
|
|
49
90
|
return 'then' in value && typeof value.then === 'function';
|
|
50
91
|
};
|
|
51
92
|
|
|
52
|
-
const openReadonlyDb = (dbPath: string
|
|
93
|
+
const openReadonlyDb = (dbPath: string) => {
|
|
53
94
|
const db = new Database(dbPath, { readonly: true });
|
|
54
95
|
try {
|
|
55
|
-
db.
|
|
96
|
+
db.query('SELECT name FROM sqlite_master LIMIT 1').get();
|
|
56
97
|
return db;
|
|
57
98
|
} catch (error) {
|
|
58
99
|
db.close();
|
|
59
|
-
|
|
100
|
+
if (!isSqliteCantOpenError(error)) {
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
60
103
|
}
|
|
104
|
+
|
|
105
|
+
// Codex uses WAL mode; immutable URI reads keep Bun usable after clean shutdown removes sidecar files.
|
|
106
|
+
return new Database(`${pathToFileURL(dbPath).href}?immutable=1`, CODEX_READONLY_DB_OPEN_FLAGS);
|
|
61
107
|
};
|
|
62
108
|
|
|
63
109
|
const openWritableDb = (dbPath: string, busyTimeoutMs: number) => {
|
|
@@ -102,7 +148,7 @@ const parseJsonSafely = (value: string | null) => {
|
|
|
102
148
|
export const withReadonlyDb = <T>(dbPath: string, callback: (db: Database) => T): T => {
|
|
103
149
|
return runWithSqliteRetry({
|
|
104
150
|
action: () => {
|
|
105
|
-
const db = openReadonlyDb(dbPath
|
|
151
|
+
const db = openReadonlyDb(dbPath);
|
|
106
152
|
try {
|
|
107
153
|
const result = callback(db);
|
|
108
154
|
if (isPromiseLike(result)) {
|
|
@@ -149,12 +195,10 @@ export const resolveCodexThreadDbPath = () => {
|
|
|
149
195
|
|
|
150
196
|
for (const candidate of candidates) {
|
|
151
197
|
try {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
});
|
|
157
|
-
db.close();
|
|
198
|
+
// Avoid opening candidates as a probe: Bun can make later read-only opens fail on Codex WAL databases.
|
|
199
|
+
if (!statSync(candidate).isFile()) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
158
202
|
return candidate;
|
|
159
203
|
} catch {}
|
|
160
204
|
}
|
|
@@ -170,6 +214,399 @@ const readAllThreads = (dbPath: string): ThreadRow[] => {
|
|
|
170
214
|
});
|
|
171
215
|
};
|
|
172
216
|
|
|
217
|
+
const resolveCodexDirFromDbPath = (dbPath: string) => {
|
|
218
|
+
const dbDir = path.dirname(dbPath);
|
|
219
|
+
return path.basename(dbDir) === 'sqlite' ? path.dirname(dbDir) : dbDir;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const parseJsonlObject = <T>(line: string): T | null => {
|
|
223
|
+
try {
|
|
224
|
+
return JSON.parse(line) as T;
|
|
225
|
+
} catch {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const emitJsonlLine = <T>(line: string, onRecord: (record: T) => void) => {
|
|
231
|
+
const trimmed = line.trim();
|
|
232
|
+
const parsed = trimmed ? parseJsonlObject<T>(trimmed) : null;
|
|
233
|
+
if (parsed) {
|
|
234
|
+
onRecord(parsed);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const emitCompleteJsonlLines = <T>(text: string, onRecord: (record: T) => void): string => {
|
|
239
|
+
const lines = text.split(/\r?\n/u);
|
|
240
|
+
const pending = lines.pop() ?? '';
|
|
241
|
+
for (const line of lines) {
|
|
242
|
+
emitJsonlLine(line, onRecord);
|
|
243
|
+
}
|
|
244
|
+
return pending;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const readJsonlObjects = <T>(filePath: string, onRecord: (record: T) => void) => {
|
|
248
|
+
let descriptor: number | null = null;
|
|
249
|
+
try {
|
|
250
|
+
const stats = statSync(filePath);
|
|
251
|
+
if (!stats.isFile()) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
descriptor = openSync(filePath, 'r');
|
|
256
|
+
const buffer = Buffer.alloc(JSONL_READ_CHUNK_BYTES);
|
|
257
|
+
const decoder = new StringDecoder('utf8');
|
|
258
|
+
let position = 0;
|
|
259
|
+
let pending = '';
|
|
260
|
+
|
|
261
|
+
while (true) {
|
|
262
|
+
const bytesRead = readSync(descriptor, buffer, 0, buffer.length, position);
|
|
263
|
+
if (bytesRead === 0) {
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
position += bytesRead;
|
|
268
|
+
pending += decoder.write(buffer.subarray(0, bytesRead));
|
|
269
|
+
pending = emitCompleteJsonlLines(pending, onRecord);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
emitJsonlLine(pending + decoder.end(), onRecord);
|
|
273
|
+
} catch {
|
|
274
|
+
return;
|
|
275
|
+
} finally {
|
|
276
|
+
if (descriptor !== null) {
|
|
277
|
+
closeSync(descriptor);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const collectJsonlObjects = <T>(filePath: string): T[] => {
|
|
283
|
+
const records: T[] = [];
|
|
284
|
+
readJsonlObjects<T>(filePath, (record) => {
|
|
285
|
+
records.push(record);
|
|
286
|
+
});
|
|
287
|
+
return records;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const readSessionIndexEntries = (codexDir: string): SessionIndexEntry[] => {
|
|
291
|
+
return collectJsonlObjects<SessionIndexEntry>(path.join(codexDir, 'session_index.jsonl')).filter(
|
|
292
|
+
(entry) => typeof entry.id === 'string' && entry.id.length > 0,
|
|
293
|
+
);
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const collectSessionFilesByThreadId = (sessionsDir: string): Map<string, string> => {
|
|
297
|
+
const sessionFiles = new Map<string, string>();
|
|
298
|
+
const visit = (directory: string) => {
|
|
299
|
+
const entries = (() => {
|
|
300
|
+
try {
|
|
301
|
+
return readdirSync(directory, { withFileTypes: true });
|
|
302
|
+
} catch {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
})();
|
|
306
|
+
if (!entries) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
for (const entry of entries) {
|
|
311
|
+
const entryPath = path.join(directory, entry.name);
|
|
312
|
+
if (entry.isDirectory()) {
|
|
313
|
+
visit(entryPath);
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (!entry.isFile()) {
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const threadId = THREAD_ID_PATTERN.exec(entry.name)?.[1];
|
|
322
|
+
if (threadId && !sessionFiles.has(threadId)) {
|
|
323
|
+
sessionFiles.set(threadId, entryPath);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
visit(sessionsDir);
|
|
329
|
+
return sessionFiles;
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const readSessionMetaLine = (sessionFile: string): string | null => {
|
|
333
|
+
let descriptor: number | null = null;
|
|
334
|
+
try {
|
|
335
|
+
descriptor = openSync(sessionFile, 'r');
|
|
336
|
+
const chunks: Buffer[] = [];
|
|
337
|
+
let totalBytes = 0;
|
|
338
|
+
|
|
339
|
+
while (totalBytes < SESSION_META_READ_LIMIT_BYTES) {
|
|
340
|
+
const buffer = Buffer.alloc(Math.min(64 * 1024, SESSION_META_READ_LIMIT_BYTES - totalBytes));
|
|
341
|
+
const bytesRead = readSync(descriptor, buffer, 0, buffer.length, totalBytes);
|
|
342
|
+
if (bytesRead === 0) {
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
chunks.push(buffer.subarray(0, bytesRead));
|
|
347
|
+
totalBytes += bytesRead;
|
|
348
|
+
if (buffer.subarray(0, bytesRead).includes(10)) {
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const firstLine = Buffer.concat(chunks).toString('utf8').split(/\r?\n/u)[0]?.trim();
|
|
354
|
+
return firstLine || null;
|
|
355
|
+
} catch {
|
|
356
|
+
return null;
|
|
357
|
+
} finally {
|
|
358
|
+
if (descriptor !== null) {
|
|
359
|
+
closeSync(descriptor);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const readFallbackSessionMeta = (sessionFile: string): FallbackSessionMeta | null => {
|
|
365
|
+
const line = readSessionMetaLine(sessionFile);
|
|
366
|
+
if (!line) {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
try {
|
|
371
|
+
const record = JSON.parse(line) as { payload?: FallbackSessionMeta; type?: string };
|
|
372
|
+
return record.type === 'session_meta' && record.payload ? record.payload : null;
|
|
373
|
+
} catch {
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
const parseIsoMs = (value: string | undefined, fallback: number) => {
|
|
379
|
+
if (!value) {
|
|
380
|
+
return fallback;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const parsed = Date.parse(value);
|
|
384
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
const stringOrNull = (value: unknown) => (typeof value === 'string' && value.length > 0 ? value : null);
|
|
388
|
+
|
|
389
|
+
const numberOrNull = (value: unknown) => (typeof value === 'number' && Number.isFinite(value) ? value : null);
|
|
390
|
+
|
|
391
|
+
const objectOrNull = (value: unknown) => {
|
|
392
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
393
|
+
? (value as Record<string, unknown>)
|
|
394
|
+
: null;
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
const isFallbackSubagent = (sessionMeta: FallbackSessionMeta) => {
|
|
398
|
+
return Boolean(
|
|
399
|
+
sessionMeta.thread_source === 'subagent' ||
|
|
400
|
+
stringOrNull(sessionMeta.parent_thread_id) ||
|
|
401
|
+
stringOrNull(sessionMeta.forked_from_id),
|
|
402
|
+
);
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const readFallbackRolloutStats = (sessionFile: string): FallbackRolloutStats => {
|
|
406
|
+
let model: string | null = null;
|
|
407
|
+
let tokensUsed = 0;
|
|
408
|
+
|
|
409
|
+
readJsonlObjects<Record<string, unknown>>(sessionFile, (record) => {
|
|
410
|
+
const payload = objectOrNull(record.payload);
|
|
411
|
+
if (!payload) {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (record.type === 'turn_context') {
|
|
416
|
+
model = stringOrNull(payload.model) ?? model;
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const payloadType = stringOrNull(payload.type);
|
|
421
|
+
if (payloadType === 'message' || payloadType === 'agent_message') {
|
|
422
|
+
model = stringOrNull(payload.model) ?? model;
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (payloadType !== 'token_count') {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const info = objectOrNull(payload.info);
|
|
431
|
+
const totalTokenUsage = objectOrNull(info?.total_token_usage);
|
|
432
|
+
tokensUsed = numberOrNull(totalTokenUsage?.total_tokens) ?? tokensUsed;
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
return {
|
|
436
|
+
model,
|
|
437
|
+
tokensUsed,
|
|
438
|
+
};
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
const buildFallbackThreadRow = (
|
|
442
|
+
entry: SessionIndexEntry,
|
|
443
|
+
sessionFile: string,
|
|
444
|
+
sessionMeta: FallbackSessionMeta,
|
|
445
|
+
rolloutStats: FallbackRolloutStats,
|
|
446
|
+
): ThreadRow | null => {
|
|
447
|
+
const cwd = stringOrNull(sessionMeta.cwd);
|
|
448
|
+
if (!cwd) {
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
let mtimeMs = Date.now();
|
|
453
|
+
try {
|
|
454
|
+
mtimeMs = statSync(sessionFile).mtimeMs;
|
|
455
|
+
} catch {}
|
|
456
|
+
|
|
457
|
+
const updatedAtMs = parseIsoMs(entry.updated_at, mtimeMs);
|
|
458
|
+
const createdAtMs = parseIsoMs(sessionMeta.timestamp, updatedAtMs);
|
|
459
|
+
const title = entry.thread_name?.trim() || path.basename(sessionFile, '.jsonl');
|
|
460
|
+
const source = stringOrNull(sessionMeta.source) ?? 'session_file';
|
|
461
|
+
|
|
462
|
+
return {
|
|
463
|
+
agent_nickname: sessionMeta.agent_nickname ?? null,
|
|
464
|
+
agent_path: sessionMeta.agent_path ?? null,
|
|
465
|
+
agent_role: sessionMeta.agent_role ?? null,
|
|
466
|
+
approval_mode: 'unknown',
|
|
467
|
+
archived: 0,
|
|
468
|
+
archived_at: null,
|
|
469
|
+
cli_version: sessionMeta.cli_version ?? '',
|
|
470
|
+
created_at: Math.floor(createdAtMs / 1000),
|
|
471
|
+
created_at_ms: Math.floor(createdAtMs),
|
|
472
|
+
cwd,
|
|
473
|
+
first_user_message: title,
|
|
474
|
+
git_branch: null,
|
|
475
|
+
git_origin_url: null,
|
|
476
|
+
git_sha: null,
|
|
477
|
+
has_user_event: sessionMeta.thread_source === 'user' ? 1 : 0,
|
|
478
|
+
id: entry.id,
|
|
479
|
+
memory_mode: 'enabled',
|
|
480
|
+
model: rolloutStats.model,
|
|
481
|
+
model_provider: sessionMeta.model_provider ?? 'unknown',
|
|
482
|
+
preview: title,
|
|
483
|
+
reasoning_effort: null,
|
|
484
|
+
rollout_path: sessionFile,
|
|
485
|
+
sandbox_policy: '{}',
|
|
486
|
+
source,
|
|
487
|
+
thread_source: sessionMeta.thread_source ?? null,
|
|
488
|
+
title,
|
|
489
|
+
tokens_used: rolloutStats.tokensUsed,
|
|
490
|
+
updated_at: Math.floor(updatedAtMs / 1000),
|
|
491
|
+
updated_at_ms: Math.floor(updatedAtMs),
|
|
492
|
+
};
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
const readFallbackThreadRows = (
|
|
496
|
+
dbPath: string,
|
|
497
|
+
existingThreadIds: Set<string>,
|
|
498
|
+
projectName: string | null = null,
|
|
499
|
+
options: ReadFallbackThreadRowsOptions = {},
|
|
500
|
+
): ThreadRow[] => {
|
|
501
|
+
const codexDir = resolveCodexDirFromDbPath(dbPath);
|
|
502
|
+
const sessionFilesByThreadId = collectSessionFilesByThreadId(path.join(codexDir, 'sessions'));
|
|
503
|
+
const fallbackThreads: ThreadRow[] = [];
|
|
504
|
+
|
|
505
|
+
for (const entry of readSessionIndexEntries(codexDir)) {
|
|
506
|
+
if (existingThreadIds.has(entry.id)) {
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const sessionFile = sessionFilesByThreadId.get(entry.id);
|
|
511
|
+
if (!sessionFile) {
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const sessionMeta = readFallbackSessionMeta(sessionFile);
|
|
516
|
+
if (!sessionMeta) {
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (!options.includeSubagents && isFallbackSubagent(sessionMeta)) {
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const fallbackThread = buildFallbackThreadRow(
|
|
525
|
+
entry,
|
|
526
|
+
sessionFile,
|
|
527
|
+
sessionMeta,
|
|
528
|
+
readFallbackRolloutStats(sessionFile),
|
|
529
|
+
);
|
|
530
|
+
if (!fallbackThread || (projectName && getPortablePathBasename(fallbackThread.cwd) !== projectName)) {
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
fallbackThreads.push(fallbackThread);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return fallbackThreads;
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
const mergeFallbackThreadRows = (dbPath: string, threads: ThreadRow[], projectName: string | null = null) => {
|
|
541
|
+
const threadIds = new Set(threads.map((thread) => thread.id));
|
|
542
|
+
return [...threads, ...readFallbackThreadRows(dbPath, threadIds, projectName)].sort((left, right) => {
|
|
543
|
+
const updatedDifference = toTimestampMs(right) - toTimestampMs(left);
|
|
544
|
+
if (updatedDifference !== 0) {
|
|
545
|
+
return updatedDifference;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return right.id.localeCompare(left.id);
|
|
549
|
+
});
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
const applyRolloutActivityTimestamps = (threads: ThreadRow[]) => {
|
|
553
|
+
return threads
|
|
554
|
+
.map((thread) => {
|
|
555
|
+
let rolloutUpdatedAtMs = toTimestampMs(thread);
|
|
556
|
+
try {
|
|
557
|
+
rolloutUpdatedAtMs = Math.max(rolloutUpdatedAtMs, statSync(thread.rollout_path).mtimeMs);
|
|
558
|
+
} catch {}
|
|
559
|
+
|
|
560
|
+
if (rolloutUpdatedAtMs <= toTimestampMs(thread)) {
|
|
561
|
+
return thread;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return {
|
|
565
|
+
...thread,
|
|
566
|
+
updated_at: Math.floor(rolloutUpdatedAtMs / 1000),
|
|
567
|
+
updated_at_ms: Math.floor(rolloutUpdatedAtMs),
|
|
568
|
+
};
|
|
569
|
+
})
|
|
570
|
+
.sort((left, right) => {
|
|
571
|
+
const updatedDifference = toTimestampMs(right) - toTimestampMs(left);
|
|
572
|
+
if (updatedDifference !== 0) {
|
|
573
|
+
return updatedDifference;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return right.id.localeCompare(left.id);
|
|
577
|
+
});
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
const buildDashboardRecentThreads = (threads: ThreadRow[]) => {
|
|
581
|
+
const bestThreadByProject = new Map<string, ThreadRow>();
|
|
582
|
+
for (const thread of threads) {
|
|
583
|
+
const project = getPortablePathBasename(thread.cwd);
|
|
584
|
+
if (!project) {
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const current = bestThreadByProject.get(project);
|
|
589
|
+
if (!current || toTimestampMs(thread) > toTimestampMs(current)) {
|
|
590
|
+
bestThreadByProject.set(project, thread);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return [...bestThreadByProject.values()]
|
|
595
|
+
.sort((left, right) => {
|
|
596
|
+
const updatedDifference = toTimestampMs(right) - toTimestampMs(left);
|
|
597
|
+
if (updatedDifference !== 0) {
|
|
598
|
+
return updatedDifference;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return right.id.localeCompare(left.id);
|
|
602
|
+
})
|
|
603
|
+
.slice(0, 5)
|
|
604
|
+
.map((thread) => ({
|
|
605
|
+
project: getPortablePathBasename(thread.cwd),
|
|
606
|
+
thread: compactThreadListRow(thread),
|
|
607
|
+
}));
|
|
608
|
+
};
|
|
609
|
+
|
|
173
610
|
const filterThreadsByProject = (threads: ThreadRow[], projectName: string | null) => {
|
|
174
611
|
if (!projectName) {
|
|
175
612
|
return threads;
|
|
@@ -367,7 +804,9 @@ const deleteThreadSessionFiles = async (sessionFiles: string[]) => {
|
|
|
367
804
|
};
|
|
368
805
|
|
|
369
806
|
export const listCodexProjects = (dbPath: string): ProjectSummary[] => {
|
|
370
|
-
return mapProjectSummaries(
|
|
807
|
+
return mapProjectSummaries(
|
|
808
|
+
buildProjectSummaryMap(applyRolloutActivityTimestamps(mergeFallbackThreadRows(dbPath, readAllThreads(dbPath)))),
|
|
809
|
+
);
|
|
371
810
|
};
|
|
372
811
|
|
|
373
812
|
type ListProjectThreadsOptions = {
|
|
@@ -387,10 +826,29 @@ export const listProjectThreads = async (
|
|
|
387
826
|
projectName: string,
|
|
388
827
|
options: ListProjectThreadsOptions = {},
|
|
389
828
|
): Promise<ThreadListEntry[]> => {
|
|
390
|
-
const threads =
|
|
391
|
-
|
|
829
|
+
const threads = mergeFallbackThreadRows(
|
|
830
|
+
dbPath,
|
|
831
|
+
filterThreadsByProject(readAllThreads(dbPath), projectName),
|
|
832
|
+
projectName,
|
|
833
|
+
);
|
|
834
|
+
const activeThreads = applyRolloutActivityTimestamps(threads);
|
|
835
|
+
const entries = await mapWithConcurrency(activeThreads, THREAD_LIST_IO_CONCURRENCY, async (thread) => {
|
|
392
836
|
const rollout = await getThreadRolloutLoadState(thread.rollout_path, options.largeTranscriptThresholdBytes);
|
|
393
837
|
|
|
838
|
+
if (rollout.fileSizeBytes === null) {
|
|
839
|
+
return {
|
|
840
|
+
project: projectName,
|
|
841
|
+
rolloutSizeBytes: null,
|
|
842
|
+
stats: {
|
|
843
|
+
deferred: false,
|
|
844
|
+
execCommandCount: 0,
|
|
845
|
+
toolCallCount: 0,
|
|
846
|
+
webSearchEventCount: 0,
|
|
847
|
+
},
|
|
848
|
+
thread: compactThreadListRow(thread),
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
|
|
394
852
|
if (rollout.shouldDeferTranscriptLoad) {
|
|
395
853
|
return {
|
|
396
854
|
project: projectName,
|
|
@@ -426,18 +884,25 @@ export const listProjectThreads = async (
|
|
|
426
884
|
export const getThreadBrowseData = (dbPath: string, threadId: string): ThreadBrowseData => {
|
|
427
885
|
return withReadonlyDb(dbPath, (db) => {
|
|
428
886
|
const existingTableNames = getExistingTableNames(db);
|
|
429
|
-
const
|
|
887
|
+
const dbThread = db.query('SELECT * FROM threads WHERE id = ? LIMIT 1').get(threadId) as ThreadRow | null;
|
|
888
|
+
const thread =
|
|
889
|
+
dbThread ??
|
|
890
|
+
readFallbackThreadRows(dbPath, new Set(), null, { includeSubagents: true }).find(
|
|
891
|
+
(fallbackThread) => fallbackThread.id === threadId,
|
|
892
|
+
) ??
|
|
893
|
+
null;
|
|
430
894
|
if (!thread) {
|
|
431
895
|
throw new Error(`Thread not found: ${threadId}`);
|
|
432
896
|
}
|
|
433
897
|
|
|
434
|
-
const dynamicTools =
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
898
|
+
const dynamicTools =
|
|
899
|
+
dbThread && existingTableNames.has('thread_dynamic_tools')
|
|
900
|
+
? (db
|
|
901
|
+
.query(
|
|
902
|
+
'SELECT thread_id, position, name, description, input_schema, defer_loading, namespace FROM thread_dynamic_tools WHERE thread_id = ? ORDER BY position ASC',
|
|
903
|
+
)
|
|
904
|
+
.all(threadId) as Array<Record<string, number | string | null>>)
|
|
905
|
+
: [];
|
|
441
906
|
|
|
442
907
|
return {
|
|
443
908
|
dynamicTools: dynamicTools.map((row) => parseDynamicToolRow(row)),
|
|
@@ -449,7 +914,7 @@ export const getThreadBrowseData = (dbPath: string, threadId: string): ThreadBro
|
|
|
449
914
|
};
|
|
450
915
|
|
|
451
916
|
export const getCodexDashboardSummary = (dbPath: string): DashboardSummary => {
|
|
452
|
-
const threads = readAllThreads(dbPath);
|
|
917
|
+
const threads = applyRolloutActivityTimestamps(mergeFallbackThreadRows(dbPath, readAllThreads(dbPath)));
|
|
453
918
|
const projects = mapProjectSummaries(buildProjectSummaryMap(threads));
|
|
454
919
|
const threadsWithRelations = withReadonlyDb(dbPath, (db) => {
|
|
455
920
|
if (!getExistingTableNames(db).has('thread_spawn_edges')) {
|
|
@@ -467,13 +932,7 @@ export const getCodexDashboardSummary = (dbPath: string): DashboardSummary => {
|
|
|
467
932
|
return {
|
|
468
933
|
activeThreads: threads.filter((thread) => !thread.archived).length,
|
|
469
934
|
archivedThreads: threads.filter((thread) => Boolean(thread.archived)).length,
|
|
470
|
-
recentThreads: threads
|
|
471
|
-
.slice(0, 5)
|
|
472
|
-
.filter((thread) => Boolean(getPortablePathBasename(thread.cwd)))
|
|
473
|
-
.map((thread) => ({
|
|
474
|
-
project: getPortablePathBasename(thread.cwd),
|
|
475
|
-
thread: compactThreadListRow(thread),
|
|
476
|
-
})),
|
|
935
|
+
recentThreads: buildDashboardRecentThreads(threads),
|
|
477
936
|
threadsWithRelations,
|
|
478
937
|
topProjectsByThreadCount: [...projects]
|
|
479
938
|
.sort((left, right) => {
|
|
@@ -579,7 +1038,7 @@ export const deleteCodexProject = async (
|
|
|
579
1038
|
};
|
|
580
1039
|
|
|
581
1040
|
export const listScopedThreads = (dbPath: string, projectName: string | null): ThreadRow[] => {
|
|
582
|
-
return filterThreadsByProject(readAllThreads(dbPath), projectName);
|
|
1041
|
+
return mergeFallbackThreadRows(dbPath, filterThreadsByProject(readAllThreads(dbPath), projectName), projectName);
|
|
583
1042
|
};
|
|
584
1043
|
|
|
585
1044
|
export const invalidateCodexUiCaches = async () => {
|
|
@@ -130,6 +130,22 @@ const getRolloutSnapshot = async (rolloutPath: string): Promise<RolloutSnapshot>
|
|
|
130
130
|
};
|
|
131
131
|
};
|
|
132
132
|
|
|
133
|
+
const isMissingFileError = (error: unknown) => {
|
|
134
|
+
return error instanceof Error && 'code' in error && error.code === 'ENOENT';
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const getExistingRolloutSnapshot = async (threadId: string, rolloutPath: string): Promise<RolloutSnapshot> => {
|
|
138
|
+
try {
|
|
139
|
+
return await getRolloutSnapshot(rolloutPath);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if (isMissingFileError(error)) {
|
|
142
|
+
throw new Error(`Thread ${threadId} rollout file is missing: ${rolloutPath}`, { cause: error });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
133
149
|
const logExportEvent = (level: 'error' | 'info' | 'warn', event: string, details: Record<string, unknown>) => {
|
|
134
150
|
console[level](`[spiracha:export] ${event}`, details);
|
|
135
151
|
};
|
|
@@ -179,7 +195,7 @@ export const renderCodexThreadDownload = async (
|
|
|
179
195
|
projectPath: browseData.thread.cwd,
|
|
180
196
|
})
|
|
181
197
|
: text;
|
|
182
|
-
const rolloutSnapshotBefore = await
|
|
198
|
+
const rolloutSnapshotBefore = await getExistingRolloutSnapshot(input.threadId, browseData.thread.rollout_path);
|
|
183
199
|
|
|
184
200
|
logExportEvent('info', 'single_start', {
|
|
185
201
|
fileName,
|
|
@@ -307,7 +323,7 @@ export const renderCodexThreadsDownload = async (
|
|
|
307
323
|
|
|
308
324
|
try {
|
|
309
325
|
for (const entry of browseEntries) {
|
|
310
|
-
const rolloutSnapshotBefore = await
|
|
326
|
+
const rolloutSnapshotBefore = await getExistingRolloutSnapshot(entry.thread.id, entry.thread.rollout_path);
|
|
311
327
|
const singleBaseName = buildExportBaseName(entry.thread);
|
|
312
328
|
const uniqueBaseName = buildUniqueBatchEntryBaseName(
|
|
313
329
|
singleBaseName,
|
|
@@ -7,6 +7,10 @@ import { getFileFingerprint, hashCacheKeyParts, withCachedJson } from './ui-cach
|
|
|
7
7
|
export const LARGE_THREAD_SIZE_BYTES = 100 * 1024 * 1024;
|
|
8
8
|
export const LARGE_THREAD_PREVIEW_EVENT_LIMIT = 200;
|
|
9
9
|
|
|
10
|
+
const isMissingFileError = (error: unknown) => {
|
|
11
|
+
return error instanceof Error && 'code' in error && error.code === 'ENOENT';
|
|
12
|
+
};
|
|
13
|
+
|
|
10
14
|
export const getCachedParsedCodexTranscript = async (sessionFile: string): Promise<ParsedCodexTranscript> => {
|
|
11
15
|
const fingerprint = await getFileFingerprint(sessionFile);
|
|
12
16
|
const key = `thread-${hashCacheKeyParts(path.basename(sessionFile), fingerprint)}`;
|
|
@@ -23,7 +27,19 @@ export const getThreadRolloutLoadState = async (
|
|
|
23
27
|
sessionFile: string,
|
|
24
28
|
largeTranscriptThresholdBytes = LARGE_THREAD_SIZE_BYTES,
|
|
25
29
|
) => {
|
|
26
|
-
|
|
30
|
+
let metadata: Awaited<ReturnType<typeof stat>>;
|
|
31
|
+
try {
|
|
32
|
+
metadata = await stat(sessionFile);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (isMissingFileError(error)) {
|
|
35
|
+
return {
|
|
36
|
+
fileSizeBytes: null,
|
|
37
|
+
shouldDeferTranscriptLoad: false,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
27
43
|
|
|
28
44
|
return {
|
|
29
45
|
fileSizeBytes: metadata.size,
|