spiracha 2.1.0 → 2.2.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 +27 -12
- package/README.md +37 -19
- package/apps/ui/AGENTS.md +18 -10
- package/apps/ui/README.md +17 -10
- package/apps/ui/src/components/analytics-breakdowns.tsx +106 -0
- package/apps/ui/src/components/antigravity-conversations-table.tsx +1 -0
- package/apps/ui/src/components/app-shell.tsx +45 -3
- package/apps/ui/src/components/data-table.tsx +17 -1
- package/apps/ui/src/components/delete-confirm-dialog.tsx +4 -3
- package/apps/ui/src/components/export-dialog.tsx +77 -62
- package/apps/ui/src/components/projects-table.tsx +4 -1
- package/apps/ui/src/components/route-error-panel.tsx +13 -0
- package/apps/ui/src/components/thread-goals-panel.tsx +50 -0
- package/apps/ui/src/components/thread-tools-panel.tsx +142 -0
- package/apps/ui/src/components/threads-table.tsx +4 -1
- package/apps/ui/src/lib/antigravity-server.ts +70 -17
- package/apps/ui/src/lib/antigravity-transcript-events.ts +46 -70
- package/apps/ui/src/lib/claude-code-server.ts +14 -4
- package/apps/ui/src/lib/codex-queries.ts +3 -3
- package/apps/ui/src/lib/codex-server.ts +3 -3
- package/apps/ui/src/lib/cursor-server.ts +41 -69
- package/apps/ui/src/lib/cursor-transcript-events.ts +11 -86
- package/apps/ui/src/lib/delete-batch.ts +52 -0
- package/apps/ui/src/lib/error-presentation.ts +43 -0
- package/apps/ui/src/lib/export-mutation.ts +20 -0
- package/apps/ui/src/lib/export-options.ts +15 -0
- package/apps/ui/src/lib/grok-server.ts +2 -3
- package/apps/ui/src/lib/kiro-server.ts +14 -2
- package/apps/ui/src/lib/mutation-error.ts +15 -0
- package/apps/ui/src/lib/opencode-server.ts +13 -2
- package/apps/ui/src/lib/path-utils.ts +1 -1
- package/apps/ui/src/lib/qoder-queries.ts +15 -0
- package/apps/ui/src/lib/qoder-transcript-events.ts +1 -1
- package/apps/ui/src/lib/route-state-reset.tsx +10 -0
- package/apps/ui/src/lib/settings-server.ts +33 -0
- package/apps/ui/src/lib/settings-store.tsx +82 -38
- package/apps/ui/src/lib/settings.ts +65 -0
- package/apps/ui/src/lib/source-session-export-server.ts +1 -1
- package/apps/ui/src/lib/thread-metadata.ts +22 -0
- package/apps/ui/src/lib/thread-transcript-stats.ts +10 -1
- package/apps/ui/src/lib/workspace-delete-navigation.ts +8 -0
- package/apps/ui/src/routeTree.gen.ts +282 -282
- package/apps/ui/src/routes/__root.tsx +12 -15
- package/apps/ui/src/routes/analytics.tsx +10 -61
- package/apps/ui/src/routes/antigravity-conversations.$conversationId.tsx +73 -19
- package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +42 -60
- package/apps/ui/src/routes/antigravity.index.tsx +2 -2
- package/apps/ui/src/routes/claude-code-sessions.$sessionId.tsx +25 -11
- package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +27 -32
- package/apps/ui/src/routes/claude-code.index.tsx +2 -2
- package/apps/ui/src/routes/codex.$project.tsx +33 -31
- package/apps/ui/src/routes/codex.index.tsx +5 -10
- package/apps/ui/src/routes/cursor-threads.$composerId.tsx +29 -16
- package/apps/ui/src/routes/cursor.$workspaceKey.tsx +33 -27
- package/apps/ui/src/routes/cursor.index.tsx +5 -2
- package/apps/ui/src/routes/grok-sessions.$sessionId.tsx +25 -12
- package/apps/ui/src/routes/grok.$workspaceKey.tsx +20 -26
- package/apps/ui/src/routes/grok.index.tsx +2 -2
- package/apps/ui/src/routes/index.tsx +2 -18
- package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +25 -12
- package/apps/ui/src/routes/kiro.$workspaceKey.tsx +27 -32
- package/apps/ui/src/routes/kiro.index.tsx +2 -2
- package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +29 -36
- package/apps/ui/src/routes/opencode.$workspaceKey.tsx +45 -61
- package/apps/ui/src/routes/opencode.index.tsx +8 -17
- package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +12 -11
- package/apps/ui/src/routes/qoder.$workspaceKey.tsx +19 -25
- package/apps/ui/src/routes/qoder.index.tsx +2 -2
- package/apps/ui/src/routes/threads.$threadId.tsx +87 -56
- package/apps/ui/vite.config.ts +22 -12
- package/bin/spiracha.ts +28 -1
- package/package.json +26 -17
- package/src/client.ts +61 -16
- package/src/lib/antigravity-db.ts +236 -82
- package/src/lib/antigravity-keychain.ts +7 -8
- package/src/lib/antigravity-transcript-phase.ts +47 -0
- package/src/lib/claude-code-db.ts +45 -19
- package/src/lib/codex-analytics.ts +32 -5
- package/src/lib/codex-browser-db.ts +540 -137
- package/src/lib/codex-browser-export.ts +2 -7
- package/src/lib/codex-browser-types.ts +21 -1
- package/src/lib/codex-thread-cache.ts +88 -33
- package/src/lib/codex-thread-parser.ts +78 -35
- package/src/lib/codex-thread-recovery.ts +38 -11
- package/src/lib/codex-transcript-renderer.ts +49 -61
- package/src/lib/concurrency.ts +15 -3
- package/src/lib/conversation-api.ts +34 -31
- package/src/lib/conversation-data/adapter-helpers.ts +25 -3
- package/src/lib/conversation-data/antigravity-adapter.ts +54 -29
- package/src/lib/conversation-data/claude-code-adapter.ts +32 -6
- package/src/lib/conversation-data/codex-adapter.ts +91 -80
- package/src/lib/conversation-data/cursor-adapter.ts +48 -34
- package/src/lib/conversation-data/grok-adapter.ts +19 -6
- package/src/lib/conversation-data/index.ts +29 -10
- package/src/lib/conversation-data/kiro-adapter.ts +18 -5
- package/src/lib/conversation-data/message-selector.ts +2 -5
- package/src/lib/conversation-data/opencode-adapter.ts +43 -15
- package/src/lib/conversation-data/path-match.ts +1 -13
- package/src/lib/conversation-data/qoder-adapter.ts +23 -18
- package/src/lib/conversation-zip-export.ts +20 -2
- package/src/lib/cursor-db.ts +34 -23
- package/src/lib/cursor-id.ts +17 -0
- package/src/lib/cursor-recovery.ts +146 -35
- package/src/lib/cursor-transcript-phase.ts +40 -0
- package/src/lib/cursor-transcript.ts +21 -5
- package/src/lib/grok-db.ts +76 -34
- package/src/lib/grok-transcript-phase.ts +11 -2
- package/src/lib/grok-transcript.ts +15 -2
- package/src/lib/kiro-db.ts +161 -68
- package/src/lib/kiro-transcript.ts +0 -4
- package/src/lib/opencode-db.ts +4 -8
- package/src/lib/opencode-think-tags.ts +17 -3
- package/src/lib/portable-path.ts +9 -0
- package/src/lib/qoder-acp-client.ts +11 -2
- package/src/lib/qoder-db.ts +51 -17
- package/src/lib/qoder-transcript-phase.ts +64 -0
- package/src/lib/qoder-transcript.ts +30 -2
- package/src/lib/shared.ts +51 -15
- package/src/lib/sqlite-error.ts +31 -5
- package/src/lib/transcript-load-limiter.ts +8 -3
- package/src/lib/ui-cache.ts +152 -17
- package/src/lib/ui-export-archive.ts +14 -55
- package/src/lib/ui-export-files.ts +75 -15
- package/src/lib/ui-export-zip.ts +40 -0
- package/apps/ui/package.json +0 -65
|
@@ -6,14 +6,9 @@ import type { CodexTranscriptRenderOptions } from './codex-thread-types';
|
|
|
6
6
|
import { renderCodexSessionFile, writeCodexSessionFileExport } from './codex-transcript-renderer';
|
|
7
7
|
import { applyPathTransforms, type PathDisplaySettings } from './path-transforms';
|
|
8
8
|
import type { ExportFormat } from './shared';
|
|
9
|
-
import {
|
|
10
|
-
buildBatchExportBaseName,
|
|
11
|
-
buildConversationExportBaseName,
|
|
12
|
-
getExportMimeType,
|
|
13
|
-
zipExportDirectory,
|
|
14
|
-
zipExportFile,
|
|
15
|
-
} from './ui-export-archive';
|
|
9
|
+
import { buildBatchExportBaseName, buildConversationExportBaseName, getExportMimeType } from './ui-export-archive';
|
|
16
10
|
import { buildUiExportDownloadUrl, ensureUiExportDir } from './ui-export-files';
|
|
11
|
+
import { zipExportDirectory, zipExportFile } from './ui-export-zip';
|
|
17
12
|
|
|
18
13
|
type RenderCodexThreadDownloadInput = {
|
|
19
14
|
dbPath: string;
|
|
@@ -14,6 +14,17 @@ export type DynamicToolRow = DynamicToolDefinition & {
|
|
|
14
14
|
threadId: string;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
+
export type ThreadGoal = {
|
|
18
|
+
createdAtMs: number;
|
|
19
|
+
goalId: string;
|
|
20
|
+
objective: string;
|
|
21
|
+
status: string;
|
|
22
|
+
timeUsedSeconds: number;
|
|
23
|
+
tokenBudget: number | null;
|
|
24
|
+
tokensUsed: number;
|
|
25
|
+
updatedAtMs: number;
|
|
26
|
+
};
|
|
27
|
+
|
|
17
28
|
export type SessionMetaExtended = SessionMeta & {
|
|
18
29
|
baseInstructions: JsonValue | null;
|
|
19
30
|
dynamicTools: DynamicToolDefinition[];
|
|
@@ -165,14 +176,20 @@ export type ThreadListEntry = {
|
|
|
165
176
|
|
|
166
177
|
export type ThreadBrowseData = {
|
|
167
178
|
dynamicTools: DynamicToolRow[];
|
|
179
|
+
goals: ThreadGoal[];
|
|
168
180
|
project: string;
|
|
169
181
|
relations: ThreadRelations;
|
|
170
182
|
thread: ThreadRow;
|
|
171
183
|
};
|
|
172
184
|
|
|
185
|
+
export type DashboardThreadSummary = Pick<
|
|
186
|
+
ThreadRow,
|
|
187
|
+
'cwd' | 'id' | 'model' | 'preview' | 'title' | 'tokens_used' | 'updated_at' | 'updated_at_ms'
|
|
188
|
+
>;
|
|
189
|
+
|
|
173
190
|
export type DashboardRecentThread = {
|
|
174
191
|
project: string;
|
|
175
|
-
thread:
|
|
192
|
+
thread: DashboardThreadSummary;
|
|
176
193
|
};
|
|
177
194
|
|
|
178
195
|
export type DashboardSummary = {
|
|
@@ -232,6 +249,7 @@ export type CodexAnalyticsSummary = {
|
|
|
232
249
|
archivedThreads: number;
|
|
233
250
|
averageTokensPerThread: number;
|
|
234
251
|
distinctToolNames: number;
|
|
252
|
+
medianTokensPerThread: number;
|
|
235
253
|
threadsWithWebSearch: number;
|
|
236
254
|
totalProjects: number;
|
|
237
255
|
totalThreads: number;
|
|
@@ -240,6 +258,8 @@ export type CodexAnalyticsSummary = {
|
|
|
240
258
|
|
|
241
259
|
export type CodexAnalytics = {
|
|
242
260
|
modelsByTokens: ModelTokenSummary[];
|
|
261
|
+
reasoningEfforts: DistributionItem[];
|
|
262
|
+
sources: DistributionItem[];
|
|
243
263
|
summary: CodexAnalyticsSummary;
|
|
244
264
|
toolUsage: ToolUsageSummary[];
|
|
245
265
|
};
|
|
@@ -1,30 +1,80 @@
|
|
|
1
1
|
import { stat } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import type { ParsedCodexTranscript } from './codex-browser-types';
|
|
3
|
+
import type { ParsedCodexTranscript, ThreadTranscriptStats } from './codex-browser-types';
|
|
4
4
|
import { parseCodexTranscriptFile } from './codex-thread-parser';
|
|
5
5
|
import type { CodexTranscriptEventFilters } from './codex-transcript-filter';
|
|
6
6
|
import { shouldShowCodexTranscriptEvent } from './codex-transcript-filter';
|
|
7
7
|
import { runWithTranscriptLoadLimit } from './transcript-load-limiter';
|
|
8
|
-
import { getFileFingerprint,
|
|
8
|
+
import { getFileFingerprint, hashCacheKeyPartsIterable, withCachedJson } from './ui-cache';
|
|
9
9
|
|
|
10
10
|
// Keep initial thread payloads below sizes that make TanStack Start SSR responses unreliable.
|
|
11
11
|
export const LARGE_THREAD_SIZE_BYTES = 8 * 1024 * 1024;
|
|
12
12
|
export const LARGE_THREAD_PREVIEW_EVENT_LIMIT = 200;
|
|
13
|
-
const CODEX_TRANSCRIPT_CACHE_VERSION = '
|
|
13
|
+
const CODEX_TRANSCRIPT_CACHE_VERSION = 'v3';
|
|
14
|
+
const CODEX_TRANSCRIPT_STATS_CACHE_VERSION = 'v1';
|
|
15
|
+
const FILE_STABILITY_ATTEMPTS = 3;
|
|
16
|
+
|
|
17
|
+
type CodexTranscriptStatsLoader = (sessionFile: string) => Promise<ThreadTranscriptStats>;
|
|
14
18
|
|
|
15
19
|
const isMissingFileError = (error: unknown) => {
|
|
16
20
|
return error instanceof Error && 'code' in error && error.code === 'ENOENT';
|
|
17
21
|
};
|
|
18
22
|
|
|
23
|
+
const withStableFileCache = async <T>(
|
|
24
|
+
sessionFile: string,
|
|
25
|
+
keyForFingerprint: (fingerprint: string) => string,
|
|
26
|
+
loader: () => Promise<T>,
|
|
27
|
+
): Promise<T> => {
|
|
28
|
+
for (let attempt = 0; attempt < FILE_STABILITY_ATTEMPTS; attempt += 1) {
|
|
29
|
+
const fingerprint = await getFileFingerprint(sessionFile);
|
|
30
|
+
const value = await withCachedJson(keyForFingerprint(fingerprint), loader);
|
|
31
|
+
if ((await getFileFingerprint(sessionFile)) === fingerprint) {
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
throw new Error(`Codex rollout changed repeatedly while loading: ${sessionFile}`);
|
|
37
|
+
};
|
|
38
|
+
|
|
19
39
|
export const getCachedParsedCodexTranscript = async (sessionFile: string): Promise<ParsedCodexTranscript> => {
|
|
20
|
-
|
|
21
|
-
|
|
40
|
+
return withStableFileCache(
|
|
41
|
+
sessionFile,
|
|
42
|
+
(fingerprint) =>
|
|
43
|
+
`thread-${hashCacheKeyPartsIterable([CODEX_TRANSCRIPT_CACHE_VERSION, path.basename(sessionFile), fingerprint])}`,
|
|
44
|
+
async () =>
|
|
45
|
+
runWithTranscriptLoadLimit(() => parseCodexTranscriptFile(sessionFile), {
|
|
46
|
+
path: sessionFile,
|
|
47
|
+
source: 'codex-full',
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const loadCodexTranscriptStats: CodexTranscriptStatsLoader = async (sessionFile) => {
|
|
53
|
+
const transcript = await parseCodexTranscriptFile(sessionFile, {
|
|
54
|
+
includeRaw: false,
|
|
55
|
+
maxTurnContexts: 0,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return transcript.stats;
|
|
59
|
+
};
|
|
22
60
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
61
|
+
export const getCachedCodexTranscriptStats = async (
|
|
62
|
+
sessionFile: string,
|
|
63
|
+
loadStats: CodexTranscriptStatsLoader = loadCodexTranscriptStats,
|
|
64
|
+
): Promise<ThreadTranscriptStats> => {
|
|
65
|
+
return withStableFileCache(
|
|
66
|
+
sessionFile,
|
|
67
|
+
(fingerprint) =>
|
|
68
|
+
`thread-list-stats-${hashCacheKeyPartsIterable([
|
|
69
|
+
CODEX_TRANSCRIPT_STATS_CACHE_VERSION,
|
|
70
|
+
path.basename(sessionFile),
|
|
71
|
+
fingerprint,
|
|
72
|
+
])}`,
|
|
73
|
+
() =>
|
|
74
|
+
runWithTranscriptLoadLimit(() => loadStats(sessionFile), {
|
|
75
|
+
path: sessionFile,
|
|
76
|
+
source: 'codex-list-stats',
|
|
77
|
+
}),
|
|
28
78
|
);
|
|
29
79
|
};
|
|
30
80
|
|
|
@@ -65,38 +115,43 @@ export const getCachedThreadTranscriptPreview = async (
|
|
|
65
115
|
const threshold = options.largeTranscriptThresholdBytes ?? LARGE_THREAD_SIZE_BYTES;
|
|
66
116
|
const previewEventLimit = options.previewEventLimit ?? LARGE_THREAD_PREVIEW_EVENT_LIMIT;
|
|
67
117
|
const filters = options.filters;
|
|
68
|
-
const fingerprint = await getFileFingerprint(sessionFile);
|
|
69
|
-
const { fileSizeBytes, shouldDeferTranscriptLoad } = await getThreadRolloutLoadState(sessionFile, threshold);
|
|
70
118
|
const filterKey = filters ? JSON.stringify(filters) : 'all';
|
|
71
|
-
|
|
119
|
+
return withStableFileCache(
|
|
120
|
+
sessionFile,
|
|
121
|
+
(fingerprint) =>
|
|
122
|
+
`thread-preview-${hashCacheKeyPartsIterable([CODEX_TRANSCRIPT_CACHE_VERSION, path.basename(sessionFile), fingerprint, String(threshold), String(previewEventLimit), filterKey])}`,
|
|
123
|
+
async () => {
|
|
124
|
+
const { fileSizeBytes, shouldDeferTranscriptLoad } = await getThreadRolloutLoadState(
|
|
125
|
+
sessionFile,
|
|
126
|
+
threshold,
|
|
127
|
+
);
|
|
128
|
+
if (!shouldDeferTranscriptLoad) {
|
|
129
|
+
return runWithTranscriptLoadLimit(
|
|
130
|
+
() =>
|
|
131
|
+
parseCodexTranscriptFile(sessionFile, {
|
|
132
|
+
sourceFileSizeBytes: fileSizeBytes,
|
|
133
|
+
}),
|
|
134
|
+
{
|
|
135
|
+
path: sessionFile,
|
|
136
|
+
source: 'codex-preview-full',
|
|
137
|
+
},
|
|
138
|
+
);
|
|
139
|
+
}
|
|
72
140
|
|
|
73
|
-
return withCachedJson(key, async () => {
|
|
74
|
-
if (!shouldDeferTranscriptLoad) {
|
|
75
141
|
return runWithTranscriptLoadLimit(
|
|
76
142
|
() =>
|
|
77
143
|
parseCodexTranscriptFile(sessionFile, {
|
|
144
|
+
eventFilter: filters ? (event) => shouldShowCodexTranscriptEvent(event, filters) : undefined,
|
|
145
|
+
includeRaw: false,
|
|
146
|
+
maxTurnContexts: 0,
|
|
78
147
|
sourceFileSizeBytes: fileSizeBytes,
|
|
148
|
+
tailEventLimit: previewEventLimit,
|
|
79
149
|
}),
|
|
80
150
|
{
|
|
81
151
|
path: sessionFile,
|
|
82
|
-
source: 'codex-preview
|
|
152
|
+
source: 'codex-preview',
|
|
83
153
|
},
|
|
84
154
|
);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return runWithTranscriptLoadLimit(
|
|
88
|
-
() =>
|
|
89
|
-
parseCodexTranscriptFile(sessionFile, {
|
|
90
|
-
eventFilter: filters ? (event) => shouldShowCodexTranscriptEvent(event, filters) : undefined,
|
|
91
|
-
includeRaw: false,
|
|
92
|
-
maxTurnContexts: 0,
|
|
93
|
-
sourceFileSizeBytes: fileSizeBytes,
|
|
94
|
-
tailEventLimit: previewEventLimit,
|
|
95
|
-
}),
|
|
96
|
-
{
|
|
97
|
-
path: sessionFile,
|
|
98
|
-
source: 'codex-preview',
|
|
99
|
-
},
|
|
100
|
-
);
|
|
101
|
-
});
|
|
155
|
+
},
|
|
156
|
+
);
|
|
102
157
|
};
|
|
@@ -33,7 +33,9 @@ type ParseCodexTranscriptState = {
|
|
|
33
33
|
maxTurnContexts: number;
|
|
34
34
|
sequence: number;
|
|
35
35
|
shouldStop: boolean;
|
|
36
|
+
tailEventCursor: number;
|
|
36
37
|
tailEventLimit: number;
|
|
38
|
+
tailEventsWrapped: boolean;
|
|
37
39
|
turnContexts: TurnContextRecord[];
|
|
38
40
|
};
|
|
39
41
|
|
|
@@ -88,7 +90,9 @@ export const parseCodexTranscriptFile = async (
|
|
|
88
90
|
maxTurnContexts,
|
|
89
91
|
sequence: 0,
|
|
90
92
|
shouldStop: false,
|
|
93
|
+
tailEventCursor: 0,
|
|
91
94
|
tailEventLimit,
|
|
95
|
+
tailEventsWrapped: false,
|
|
92
96
|
turnContexts,
|
|
93
97
|
};
|
|
94
98
|
|
|
@@ -100,6 +104,15 @@ export const parseCodexTranscriptFile = async (
|
|
|
100
104
|
}
|
|
101
105
|
}
|
|
102
106
|
|
|
107
|
+
if (state.tailEventsWrapped && events.length > 0) {
|
|
108
|
+
events.splice(
|
|
109
|
+
0,
|
|
110
|
+
events.length,
|
|
111
|
+
...events.slice(state.tailEventCursor),
|
|
112
|
+
...events.slice(0, state.tailEventCursor),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
103
116
|
for (const event of events) {
|
|
104
117
|
updateTranscriptStats(stats, event);
|
|
105
118
|
}
|
|
@@ -129,7 +142,7 @@ const captureTranscriptRecord = (parsed: Record<string, JsonValue>, state: Parse
|
|
|
129
142
|
return;
|
|
130
143
|
}
|
|
131
144
|
|
|
132
|
-
const event =
|
|
145
|
+
const event = parseCodexTranscriptRecord(parsed, state.sequence, state.includeRaw);
|
|
133
146
|
if (!event) {
|
|
134
147
|
return;
|
|
135
148
|
}
|
|
@@ -139,9 +152,12 @@ const captureTranscriptRecord = (parsed: Record<string, JsonValue>, state: Parse
|
|
|
139
152
|
return;
|
|
140
153
|
}
|
|
141
154
|
|
|
142
|
-
state.events.
|
|
143
|
-
|
|
144
|
-
|
|
155
|
+
if (!Number.isFinite(state.tailEventLimit) || state.events.length < state.tailEventLimit) {
|
|
156
|
+
state.events.push(event);
|
|
157
|
+
} else if (state.tailEventLimit > 0) {
|
|
158
|
+
state.events[state.tailEventCursor] = event;
|
|
159
|
+
state.tailEventCursor = (state.tailEventCursor + 1) % state.tailEventLimit;
|
|
160
|
+
state.tailEventsWrapped = true;
|
|
145
161
|
}
|
|
146
162
|
state.shouldStop = state.events.length >= state.maxEvents;
|
|
147
163
|
};
|
|
@@ -204,10 +220,10 @@ const captureTurnContext = (parsed: Record<string, JsonValue>, turnContexts: Tur
|
|
|
204
220
|
});
|
|
205
221
|
};
|
|
206
222
|
|
|
207
|
-
const
|
|
223
|
+
export const parseCodexTranscriptRecord = (
|
|
208
224
|
parsed: Record<string, JsonValue>,
|
|
209
|
-
sequence
|
|
210
|
-
includeRaw
|
|
225
|
+
sequence = 0,
|
|
226
|
+
includeRaw = false,
|
|
211
227
|
): ThreadEvent | null => {
|
|
212
228
|
const payload = asObject(parsed.payload);
|
|
213
229
|
if (!payload) {
|
|
@@ -235,15 +251,7 @@ const buildEventMessage = (
|
|
|
235
251
|
sequence: number,
|
|
236
252
|
timestamp: string | null,
|
|
237
253
|
) => {
|
|
238
|
-
|
|
239
|
-
return createTaskStartedEvent(payload, raw, sequence, timestamp);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
if (payloadType === 'task_complete') {
|
|
243
|
-
return createTaskCompleteEvent(payload, raw, sequence, timestamp);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return null;
|
|
254
|
+
return buildSupplementalEvent(payload, payloadType, raw, sequence, timestamp);
|
|
247
255
|
};
|
|
248
256
|
|
|
249
257
|
const buildResponseItemEvent = (
|
|
@@ -265,11 +273,11 @@ const buildResponseItemEvent = (
|
|
|
265
273
|
return createAgentMessageEvent(payload, raw, sequence, timestamp);
|
|
266
274
|
}
|
|
267
275
|
|
|
268
|
-
if (payloadType === 'function_call') {
|
|
269
|
-
return createToolCallEvent(payload, raw, sequence, timestamp);
|
|
276
|
+
if (payloadType === 'function_call' || payloadType === 'custom_tool_call') {
|
|
277
|
+
return createToolCallEvent(payload, raw, sequence, timestamp, payloadType === 'custom_tool_call');
|
|
270
278
|
}
|
|
271
279
|
|
|
272
|
-
if (payloadType === 'function_call_output') {
|
|
280
|
+
if (payloadType === 'function_call_output' || payloadType === 'custom_tool_call_output') {
|
|
273
281
|
return createToolOutputEvent(payload, raw, sequence, timestamp);
|
|
274
282
|
}
|
|
275
283
|
|
|
@@ -277,6 +285,16 @@ const buildResponseItemEvent = (
|
|
|
277
285
|
return createReasoningEvent(payload, raw, sequence, timestamp);
|
|
278
286
|
}
|
|
279
287
|
|
|
288
|
+
return buildSupplementalEvent(payload, payloadType, raw, sequence, timestamp);
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const buildSupplementalEvent = (
|
|
292
|
+
payload: Record<string, JsonValue>,
|
|
293
|
+
payloadType: string | null,
|
|
294
|
+
raw: Record<string, JsonValue>,
|
|
295
|
+
sequence: number,
|
|
296
|
+
timestamp: string | null,
|
|
297
|
+
) => {
|
|
280
298
|
if (payloadType === 'token_count') {
|
|
281
299
|
return createTokenCountEvent(payload, raw, sequence, timestamp);
|
|
282
300
|
}
|
|
@@ -305,12 +323,12 @@ const createMessageEvent = (
|
|
|
305
323
|
const role = asString(payload.role);
|
|
306
324
|
const content = payload.content;
|
|
307
325
|
const text = extractText(content);
|
|
308
|
-
if (!role || content === undefined) {
|
|
326
|
+
if (!role || content === undefined || !text.trim()) {
|
|
309
327
|
return null;
|
|
310
328
|
}
|
|
311
329
|
|
|
312
330
|
return {
|
|
313
|
-
isHiddenByDefault:
|
|
331
|
+
isHiddenByDefault: shouldHideCodexTranscriptText(role, text),
|
|
314
332
|
kind: 'message',
|
|
315
333
|
memoryCitation: null,
|
|
316
334
|
model: asString(payload.model),
|
|
@@ -330,9 +348,9 @@ const createUserMessageEvent = (
|
|
|
330
348
|
sequence: number,
|
|
331
349
|
timestamp: string | null,
|
|
332
350
|
): MessageEvent => {
|
|
333
|
-
const text =
|
|
351
|
+
const text = stripCodexMemoryCitationBlocks(asString(payload.message) ?? '');
|
|
334
352
|
return {
|
|
335
|
-
isHiddenByDefault:
|
|
353
|
+
isHiddenByDefault: shouldHideCodexTranscriptText('user', text),
|
|
336
354
|
kind: 'message',
|
|
337
355
|
memoryCitation: null,
|
|
338
356
|
model: null,
|
|
@@ -352,9 +370,9 @@ const createAgentMessageEvent = (
|
|
|
352
370
|
sequence: number,
|
|
353
371
|
timestamp: string | null,
|
|
354
372
|
): MessageEvent => {
|
|
355
|
-
const text =
|
|
373
|
+
const text = stripCodexMemoryCitationBlocks(asString(payload.message) ?? '');
|
|
356
374
|
return {
|
|
357
|
-
isHiddenByDefault:
|
|
375
|
+
isHiddenByDefault: shouldHideCodexTranscriptText('assistant', text),
|
|
358
376
|
kind: 'message',
|
|
359
377
|
memoryCitation: payload.memory_citation ?? null,
|
|
360
378
|
model: asString(payload.model),
|
|
@@ -373,10 +391,13 @@ const createToolCallEvent = (
|
|
|
373
391
|
raw: Record<string, JsonValue>,
|
|
374
392
|
sequence: number,
|
|
375
393
|
timestamp: string | null,
|
|
394
|
+
isCustomToolCall: boolean,
|
|
376
395
|
): ToolCallEvent => {
|
|
377
396
|
const name = asString(payload.name) ?? 'unknown';
|
|
378
|
-
const argumentsText = asString(payload.arguments);
|
|
379
|
-
const parsedArguments =
|
|
397
|
+
const argumentsText = asString(isCustomToolCall ? payload.input : payload.arguments);
|
|
398
|
+
const parsedArguments = isCustomToolCall
|
|
399
|
+
? { argumentsParseFailed: false, cmd: argumentsText, workdir: null }
|
|
400
|
+
: parseExecCommandArguments(argumentsText);
|
|
380
401
|
|
|
381
402
|
return {
|
|
382
403
|
argumentsParseFailed: parsedArguments.argumentsParseFailed,
|
|
@@ -398,7 +419,7 @@ const createToolOutputEvent = (
|
|
|
398
419
|
sequence: number,
|
|
399
420
|
timestamp: string | null,
|
|
400
421
|
): ToolOutputEvent => {
|
|
401
|
-
const outputText =
|
|
422
|
+
const outputText = extractToolOutputText(payload.output);
|
|
402
423
|
|
|
403
424
|
return {
|
|
404
425
|
callId: asString(payload.call_id),
|
|
@@ -524,7 +545,7 @@ const updateTranscriptStats = (stats: ThreadTranscriptStats, event: ThreadEvent)
|
|
|
524
545
|
|
|
525
546
|
if (event.kind === 'tool_call') {
|
|
526
547
|
stats.toolCallCount += 1;
|
|
527
|
-
if (event.name === 'exec_command') {
|
|
548
|
+
if (event.name === 'exec' || event.name === 'exec_command') {
|
|
528
549
|
stats.execCommandCount += 1;
|
|
529
550
|
}
|
|
530
551
|
return;
|
|
@@ -592,13 +613,31 @@ const parseExecCommandArguments = (argumentsText: string | null) => {
|
|
|
592
613
|
}
|
|
593
614
|
};
|
|
594
615
|
|
|
616
|
+
const extractToolOutputText = (output: JsonValue | undefined): string => {
|
|
617
|
+
if (typeof output === 'string') {
|
|
618
|
+
return output;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (!Array.isArray(output)) {
|
|
622
|
+
return '';
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return output
|
|
626
|
+
.map((entry) => {
|
|
627
|
+
const contentBlock = asObject(entry);
|
|
628
|
+
return contentBlock ? asString(contentBlock.text) : null;
|
|
629
|
+
})
|
|
630
|
+
.filter((entry): entry is string => entry !== null)
|
|
631
|
+
.join('\n');
|
|
632
|
+
};
|
|
633
|
+
|
|
595
634
|
const extractText = (content: JsonValue): string => {
|
|
596
635
|
if (typeof content === 'string') {
|
|
597
|
-
return
|
|
636
|
+
return stripCodexMemoryCitationBlocks(content);
|
|
598
637
|
}
|
|
599
638
|
|
|
600
639
|
if (Array.isArray(content)) {
|
|
601
|
-
return
|
|
640
|
+
return stripCodexMemoryCitationBlocks(
|
|
602
641
|
content
|
|
603
642
|
.map((entry) => extractTextPart(entry))
|
|
604
643
|
.filter(Boolean)
|
|
@@ -607,14 +646,17 @@ const extractText = (content: JsonValue): string => {
|
|
|
607
646
|
}
|
|
608
647
|
|
|
609
648
|
if (content && typeof content === 'object') {
|
|
610
|
-
return
|
|
649
|
+
return stripCodexMemoryCitationBlocks(asString((content as Record<string, JsonValue>).text) ?? '');
|
|
611
650
|
}
|
|
612
651
|
|
|
613
652
|
return '';
|
|
614
653
|
};
|
|
615
654
|
|
|
616
|
-
const
|
|
617
|
-
|
|
655
|
+
export const stripCodexMemoryCitationBlocks = (text: string): string => {
|
|
656
|
+
const withoutMemoryCitations = text.includes('<oai-mem-citation>')
|
|
657
|
+
? text.replace(/<oai-mem-citation>[\s\S]*?<\/oai-mem-citation>/gu, '')
|
|
658
|
+
: text;
|
|
659
|
+
return stripCodexAppDirectiveLines(withoutMemoryCitations);
|
|
618
660
|
};
|
|
619
661
|
|
|
620
662
|
const extractTextPart = (entry: JsonValue): string => {
|
|
@@ -633,7 +675,7 @@ const extractTextPart = (entry: JsonValue): string => {
|
|
|
633
675
|
return text ?? '';
|
|
634
676
|
};
|
|
635
677
|
|
|
636
|
-
const
|
|
678
|
+
export const shouldHideCodexTranscriptText = (role: string, text: string) => {
|
|
637
679
|
if (!text) {
|
|
638
680
|
return true;
|
|
639
681
|
}
|
|
@@ -644,6 +686,7 @@ const shouldHideTranscriptText = (role: string, text: string) => {
|
|
|
644
686
|
|
|
645
687
|
return (
|
|
646
688
|
text.startsWith('# AGENTS.md instructions for ') ||
|
|
689
|
+
text.startsWith('AGENTS.md instructions for ') ||
|
|
647
690
|
text.startsWith('<permissions instructions>') ||
|
|
648
691
|
text.startsWith('<app-context>') ||
|
|
649
692
|
text.startsWith('<environment_context>') ||
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Database } from 'bun:sqlite';
|
|
2
|
-
import { copyFile, utimes } from 'node:fs/promises';
|
|
2
|
+
import { copyFile, readdir, rm, utimes } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import type { RecoverProjectThreadsResult } from './codex-browser-types';
|
|
5
|
-
import { getPortablePathBasename } from './
|
|
5
|
+
import { getPortablePathBasename } from './portable-path';
|
|
6
6
|
import { runWithSqliteRetry } from './sqlite-retry';
|
|
7
7
|
|
|
8
8
|
type RecoveryThreadRow = {
|
|
@@ -18,6 +18,8 @@ type GlobalState = {
|
|
|
18
18
|
'project-order'?: string[];
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
const RECOVERY_BACKUP_RETENTION_COUNT = 5;
|
|
22
|
+
|
|
21
23
|
const backupFile = async (filePath: string, label: string) => {
|
|
22
24
|
const stamp = new Date()
|
|
23
25
|
.toISOString()
|
|
@@ -26,6 +28,14 @@ const backupFile = async (filePath: string, label: string) => {
|
|
|
26
28
|
.replace('T', '-');
|
|
27
29
|
const backupPath = `${filePath}.bak-${label}-${stamp}`;
|
|
28
30
|
await copyFile(filePath, backupPath);
|
|
31
|
+
const directory = path.dirname(filePath);
|
|
32
|
+
const prefix = `${path.basename(filePath)}.bak-${label}-`;
|
|
33
|
+
const backups = (await readdir(directory))
|
|
34
|
+
.filter((entry) => entry.startsWith(prefix))
|
|
35
|
+
.sort((left, right) => right.localeCompare(left));
|
|
36
|
+
await Promise.all(
|
|
37
|
+
backups.slice(RECOVERY_BACKUP_RETENTION_COUNT).map((entry) => rm(path.join(directory, entry), { force: true })),
|
|
38
|
+
);
|
|
29
39
|
return backupPath;
|
|
30
40
|
};
|
|
31
41
|
|
|
@@ -100,9 +110,9 @@ const refreshThreadRows = (db: Database, threadIds: string[]) => {
|
|
|
100
110
|
return Number(result.changes);
|
|
101
111
|
};
|
|
102
112
|
|
|
103
|
-
const
|
|
113
|
+
const prepareSessionIndexRefresh = async (sessionIndexPath: string, threadIds: string[]) => {
|
|
104
114
|
if (threadIds.length === 0) {
|
|
105
|
-
return 0;
|
|
115
|
+
return { content: null, updated: 0 };
|
|
106
116
|
}
|
|
107
117
|
|
|
108
118
|
const now = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
@@ -116,7 +126,19 @@ const refreshSessionIndex = async (sessionIndexPath: string, threadIds: string[]
|
|
|
116
126
|
continue;
|
|
117
127
|
}
|
|
118
128
|
|
|
119
|
-
|
|
129
|
+
let parsed: { id?: string; updated_at?: string };
|
|
130
|
+
try {
|
|
131
|
+
const value: unknown = JSON.parse(line);
|
|
132
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
133
|
+
rewrittenLines.push(line);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
parsed = value as { id?: string; updated_at?: string };
|
|
137
|
+
} catch {
|
|
138
|
+
rewrittenLines.push(line);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
120
142
|
if (parsed.id && threadIdSet.has(parsed.id)) {
|
|
121
143
|
parsed.updated_at = now;
|
|
122
144
|
updated += 1;
|
|
@@ -125,8 +147,10 @@ const refreshSessionIndex = async (sessionIndexPath: string, threadIds: string[]
|
|
|
125
147
|
rewrittenLines.push(JSON.stringify(parsed));
|
|
126
148
|
}
|
|
127
149
|
|
|
128
|
-
|
|
129
|
-
|
|
150
|
+
return {
|
|
151
|
+
content: `${rewrittenLines.join('\n')}\n`,
|
|
152
|
+
updated,
|
|
153
|
+
};
|
|
130
154
|
};
|
|
131
155
|
|
|
132
156
|
const touchRolloutFiles = async (codexDir: string, rolloutPaths: string[]) => {
|
|
@@ -177,12 +201,15 @@ export const recoverCodexProjectThreads = async (
|
|
|
177
201
|
const topLevelThreads = getProjectTopLevelThreads(db, projectName);
|
|
178
202
|
const projectCwds = [...new Set(topLevelThreads.map((thread) => thread.cwd))];
|
|
179
203
|
const rootUpdateResult = updateGlobalRoots(globalState, projectCwds);
|
|
180
|
-
await writeGlobalState(globalStatePath, rootUpdateResult.state);
|
|
181
|
-
|
|
182
204
|
const threadIds = topLevelThreads.map((thread) => thread.id);
|
|
183
205
|
const rolloutPaths = topLevelThreads.map((thread) => thread.rollout_path);
|
|
206
|
+
const sessionIndexRefresh = await prepareSessionIndexRefresh(sessionIndexPath, threadIds);
|
|
207
|
+
|
|
208
|
+
await writeGlobalState(globalStatePath, rootUpdateResult.state);
|
|
184
209
|
const threadDbRowsUpdated = refreshThreadRows(db, threadIds);
|
|
185
|
-
|
|
210
|
+
if (sessionIndexRefresh.content !== null) {
|
|
211
|
+
await Bun.write(sessionIndexPath, sessionIndexRefresh.content);
|
|
212
|
+
}
|
|
186
213
|
const rolloutFilesTouched = await touchRolloutFiles(codexDir, rolloutPaths);
|
|
187
214
|
|
|
188
215
|
return {
|
|
@@ -192,7 +219,7 @@ export const recoverCodexProjectThreads = async (
|
|
|
192
219
|
resolvedCwds: projectCwds,
|
|
193
220
|
rolloutFilesTouched,
|
|
194
221
|
savedRootsAdded: rootUpdateResult.savedRootsAdded,
|
|
195
|
-
sessionIndexRowsUpdated,
|
|
222
|
+
sessionIndexRowsUpdated: sessionIndexRefresh.updated,
|
|
196
223
|
threadDbRowsUpdated,
|
|
197
224
|
topLevelThreadsFound: threadIds.length,
|
|
198
225
|
};
|