spiracha 2.0.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 +42 -19
- package/apps/ui/AGENTS.md +18 -10
- package/apps/ui/README.md +36 -11
- package/apps/ui/src/components/analytics-breakdowns.tsx +106 -0
- package/apps/ui/src/components/antigravity-conversations-table.tsx +51 -11
- package/apps/ui/src/components/antigravity-workspaces-table.tsx +1 -1
- package/apps/ui/src/components/app-shell.tsx +46 -3
- package/apps/ui/src/components/breadcrumbs.tsx +21 -3
- package/apps/ui/src/components/claude-code-sessions-table.tsx +42 -6
- package/apps/ui/src/components/claude-code-workspaces-table.tsx +3 -1
- package/apps/ui/src/components/cursor-threads-table.tsx +10 -47
- package/apps/ui/src/components/data-table.tsx +23 -5
- package/apps/ui/src/components/delete-confirm-dialog.tsx +4 -3
- package/apps/ui/src/components/export-dialog.tsx +79 -74
- package/apps/ui/src/components/grok-sessions-table.tsx +146 -0
- package/apps/ui/src/components/grok-workspaces-table.tsx +53 -0
- package/apps/ui/src/components/kiro-sessions-table.tsx +40 -4
- package/apps/ui/src/components/opencode-sessions-table.tsx +40 -4
- package/apps/ui/src/components/page-header.tsx +15 -9
- package/apps/ui/src/components/projects-table.tsx +4 -1
- package/apps/ui/src/components/qoder-sessions-table.tsx +23 -2
- package/apps/ui/src/components/qoder-workspaces-table.tsx +5 -1
- package/apps/ui/src/components/route-error-panel.tsx +13 -0
- package/apps/ui/src/components/selection-actions-toolbar.tsx +80 -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 +13 -47
- package/apps/ui/src/components/transcript-view.tsx +148 -52
- package/apps/ui/src/components/ui/select.tsx +1 -1
- package/apps/ui/src/lib/antigravity-conversation-state.ts +17 -4
- package/apps/ui/src/lib/antigravity-server.ts +209 -11
- package/apps/ui/src/lib/antigravity-transcript-events.ts +57 -73
- package/apps/ui/src/lib/claude-code-queries.ts +8 -0
- package/apps/ui/src/lib/claude-code-server.ts +185 -7
- package/apps/ui/src/lib/claude-code-transcript-events.ts +8 -1
- package/apps/ui/src/lib/codex-queries.ts +16 -0
- package/apps/ui/src/lib/codex-server.ts +134 -52
- package/apps/ui/src/lib/cursor-server.ts +68 -53
- 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/formatters.ts +3 -5
- package/apps/ui/src/lib/grok-queries.ts +22 -0
- package/apps/ui/src/lib/grok-server.ts +168 -0
- package/apps/ui/src/lib/grok-transcript-events.ts +149 -0
- package/apps/ui/src/lib/kiro-server.ts +97 -7
- package/apps/ui/src/lib/mutation-error.ts +15 -0
- package/apps/ui/src/lib/opencode-server.ts +96 -7
- 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-server.ts +67 -11
- package/apps/ui/src/lib/qoder-transcript-events.ts +1 -1
- package/apps/ui/src/lib/route-search.ts +114 -0
- 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 +86 -3
- package/apps/ui/src/lib/thread-metadata.ts +22 -0
- package/apps/ui/src/lib/thread-transcript-load.ts +15 -0
- package/apps/ui/src/lib/thread-transcript-stats.ts +10 -1
- package/apps/ui/src/lib/workspace-delete-navigation.ts +20 -0
- package/apps/ui/src/routeTree.gen.ts +341 -234
- 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 +149 -29
- package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +262 -42
- package/apps/ui/src/routes/antigravity.index.tsx +2 -2
- package/apps/ui/src/routes/api.v1.conversations.$source.$id.ts +4 -0
- package/apps/ui/src/routes/api.v1.conversations.delete.ts +12 -0
- package/apps/ui/src/routes/api.v1.conversations.export.ts +12 -0
- package/apps/ui/src/routes/claude-code-sessions.$sessionId.tsx +168 -28
- package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +179 -45
- 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 +31 -18
- package/apps/ui/src/routes/cursor.$workspaceKey.tsx +44 -28
- package/apps/ui/src/routes/cursor.index.tsx +5 -2
- package/apps/ui/src/routes/grok-sessions.$sessionId.tsx +401 -0
- package/apps/ui/src/routes/grok.$workspaceKey.tsx +295 -0
- package/apps/ui/src/routes/grok.index.tsx +48 -0
- package/apps/ui/src/routes/index.tsx +2 -18
- package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +91 -27
- package/apps/ui/src/routes/kiro.$workspaceKey.tsx +213 -51
- package/apps/ui/src/routes/kiro.index.tsx +2 -2
- package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +108 -29
- package/apps/ui/src/routes/opencode.$workspaceKey.tsx +231 -55
- package/apps/ui/src/routes/opencode.index.tsx +6 -2
- package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +19 -15
- package/apps/ui/src/routes/qoder.$workspaceKey.tsx +89 -48
- package/apps/ui/src/routes/qoder.index.tsx +2 -2
- package/apps/ui/src/routes/threads.$threadId.tsx +620 -105
- package/apps/ui/vite.config.ts +22 -12
- package/bin/spiracha.ts +28 -1
- package/package.json +37 -27
- package/src/client.ts +183 -5
- package/src/lib/antigravity-db.ts +424 -94
- package/src/lib/antigravity-exporter-types.ts +3 -0
- package/src/lib/antigravity-keychain.ts +7 -8
- package/src/lib/antigravity-projects.ts +201 -0
- package/src/lib/antigravity-transcript-phase.ts +47 -0
- package/src/lib/claude-code-db.ts +534 -42
- package/src/lib/claude-code-exporter-types.ts +5 -0
- package/src/lib/claude-code-transcript-phase.ts +60 -1
- package/src/lib/claude-code-transcript.ts +8 -5
- package/src/lib/codex-analytics.ts +32 -5
- package/src/lib/codex-browser-db.ts +540 -137
- package/src/lib/codex-browser-export.ts +18 -27
- package/src/lib/codex-browser-types.ts +21 -1
- package/src/lib/codex-thread-cache.ts +106 -23
- package/src/lib/codex-thread-parser.ts +149 -55
- package/src/lib/codex-thread-recovery.ts +38 -11
- package/src/lib/codex-transcript-filter.ts +31 -0
- package/src/lib/codex-transcript-renderer.ts +87 -79
- package/src/lib/concurrency.ts +56 -3
- package/src/lib/conversation-api.ts +410 -27
- package/src/lib/conversation-data/adapter-helpers.ts +25 -3
- package/src/lib/conversation-data/antigravity-adapter.ts +70 -26
- package/src/lib/conversation-data/claude-code-adapter.ts +69 -12
- package/src/lib/conversation-data/codex-adapter.ts +108 -73
- package/src/lib/conversation-data/cursor-adapter.ts +77 -29
- package/src/lib/conversation-data/grok-adapter.ts +223 -0
- package/src/lib/conversation-data/index.ts +105 -11
- package/src/lib/conversation-data/kiro-adapter.ts +59 -12
- package/src/lib/conversation-data/message-selector.ts +2 -5
- package/src/lib/conversation-data/opencode-adapter.ts +67 -17
- package/src/lib/conversation-data/path-match.ts +1 -13
- package/src/lib/conversation-data/qoder-adapter.ts +67 -37
- package/src/lib/conversation-data/types.ts +43 -0
- package/src/lib/conversation-zip-export.ts +75 -0
- package/src/lib/cursor-db.ts +68 -28
- 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 +1068 -0
- package/src/lib/grok-exporter-types.ts +110 -0
- package/src/lib/grok-transcript-phase.ts +61 -0
- package/src/lib/grok-transcript.ts +167 -0
- package/src/lib/kiro-db.ts +187 -43
- package/src/lib/kiro-transcript.ts +0 -4
- package/src/lib/model-label.ts +11 -3
- package/src/lib/opencode-db.ts +599 -60
- package/src/lib/opencode-think-tags.ts +17 -3
- package/src/lib/opencode-transcript.ts +8 -5
- 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 +63 -15
- package/src/lib/sqlite-error.ts +31 -5
- package/src/lib/transcript-load-limiter.ts +87 -0
- package/src/lib/ui-cache.ts +185 -24
- package/src/lib/ui-export-archive.ts +58 -39
- 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
|
@@ -5,9 +5,10 @@ import { getThreadBrowseData } from './codex-browser-db';
|
|
|
5
5
|
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
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import type { ExportFormat } from './shared';
|
|
9
|
+
import { buildBatchExportBaseName, buildConversationExportBaseName, getExportMimeType } from './ui-export-archive';
|
|
10
10
|
import { buildUiExportDownloadUrl, ensureUiExportDir } from './ui-export-files';
|
|
11
|
+
import { zipExportDirectory, zipExportFile } from './ui-export-zip';
|
|
11
12
|
|
|
12
13
|
type RenderCodexThreadDownloadInput = {
|
|
13
14
|
dbPath: string;
|
|
@@ -42,31 +43,15 @@ export type CodexThreadDownload =
|
|
|
42
43
|
|
|
43
44
|
const LARGE_BROWSER_EXPORT_THRESHOLD_BYTES = 128 * 1024 * 1024;
|
|
44
45
|
|
|
45
|
-
const formatReadableExportDate = (value: number) => {
|
|
46
|
-
const date = new Date(value);
|
|
47
|
-
const year = date.getUTCFullYear();
|
|
48
|
-
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
|
|
49
|
-
const day = String(date.getUTCDate()).padStart(2, '0');
|
|
50
|
-
const hours = String(date.getUTCHours()).padStart(2, '0');
|
|
51
|
-
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
|
|
52
|
-
return `${year}-${month}-${day}-${hours}${minutes}`;
|
|
53
|
-
};
|
|
54
|
-
|
|
55
46
|
const buildExportBaseName = (thread: ReturnType<typeof getThreadBrowseData>['thread']) => {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
throw new Error('No threads selected for export');
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const projectName = sanitizeExportFileName(getPortablePathBasename(firstThread.cwd) || 'threads') || 'threads';
|
|
68
|
-
const latestTimestamp = Math.max(...threads.map((thread) => thread.updated_at_ms ?? thread.updated_at * 1000));
|
|
69
|
-
return `${projectName}-${formatReadableExportDate(latestTimestamp)}-threads-${threads.length}`;
|
|
47
|
+
return buildConversationExportBaseName(
|
|
48
|
+
{
|
|
49
|
+
cwd: thread.cwd,
|
|
50
|
+
id: thread.id,
|
|
51
|
+
updatedAtMs: thread.updated_at_ms ?? thread.updated_at * 1000,
|
|
52
|
+
},
|
|
53
|
+
'thread',
|
|
54
|
+
);
|
|
70
55
|
};
|
|
71
56
|
|
|
72
57
|
const buildUniqueArchivePath = (exportDir: string, exportBaseName: string) => {
|
|
@@ -302,7 +287,13 @@ export const renderCodexThreadsDownload = async (
|
|
|
302
287
|
const browseEntries = threadIds.map((threadId) => getThreadBrowseData(input.dbPath, threadId));
|
|
303
288
|
const threads = browseEntries.map((entry) => entry.thread);
|
|
304
289
|
const exportDir = await resolvePublicExportDir(input.publicExportDir);
|
|
305
|
-
const exportBaseName = buildBatchExportBaseName(
|
|
290
|
+
const exportBaseName = buildBatchExportBaseName(
|
|
291
|
+
threads.map((thread) => ({
|
|
292
|
+
cwd: thread.cwd,
|
|
293
|
+
updatedAtMs: thread.updated_at_ms ?? thread.updated_at * 1000,
|
|
294
|
+
})),
|
|
295
|
+
'threads',
|
|
296
|
+
);
|
|
306
297
|
const bundleDirectory = await createExportWorkspace(exportDir, exportBaseName);
|
|
307
298
|
const zipPath = buildUniqueArchivePath(exportDir, exportBaseName);
|
|
308
299
|
const usedBatchEntryBaseNames = new Set<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,24 +1,85 @@
|
|
|
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
|
-
import {
|
|
5
|
+
import type { CodexTranscriptEventFilters } from './codex-transcript-filter';
|
|
6
|
+
import { shouldShowCodexTranscriptEvent } from './codex-transcript-filter';
|
|
7
|
+
import { runWithTranscriptLoadLimit } from './transcript-load-limiter';
|
|
8
|
+
import { getFileFingerprint, hashCacheKeyPartsIterable, withCachedJson } from './ui-cache';
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
// Keep initial thread payloads below sizes that make TanStack Start SSR responses unreliable.
|
|
11
|
+
export const LARGE_THREAD_SIZE_BYTES = 8 * 1024 * 1024;
|
|
8
12
|
export const LARGE_THREAD_PREVIEW_EVENT_LIMIT = 200;
|
|
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>;
|
|
9
18
|
|
|
10
19
|
const isMissingFileError = (error: unknown) => {
|
|
11
20
|
return error instanceof Error && 'code' in error && error.code === 'ENOENT';
|
|
12
21
|
};
|
|
13
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
|
+
|
|
14
39
|
export const getCachedParsedCodexTranscript = async (sessionFile: string): Promise<ParsedCodexTranscript> => {
|
|
15
|
-
|
|
16
|
-
|
|
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
|
+
};
|
|
17
51
|
|
|
18
|
-
|
|
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
|
+
};
|
|
60
|
+
|
|
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
|
+
}),
|
|
78
|
+
);
|
|
19
79
|
};
|
|
20
80
|
|
|
21
81
|
type CachedThreadTranscriptPreviewOptions = {
|
|
82
|
+
filters?: CodexTranscriptEventFilters;
|
|
22
83
|
largeTranscriptThresholdBytes?: number;
|
|
23
84
|
previewEventLimit?: number;
|
|
24
85
|
};
|
|
@@ -53,22 +114,44 @@ export const getCachedThreadTranscriptPreview = async (
|
|
|
53
114
|
): Promise<ParsedCodexTranscript> => {
|
|
54
115
|
const threshold = options.largeTranscriptThresholdBytes ?? LARGE_THREAD_SIZE_BYTES;
|
|
55
116
|
const previewEventLimit = options.previewEventLimit ?? LARGE_THREAD_PREVIEW_EVENT_LIMIT;
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
117
|
+
const filters = options.filters;
|
|
118
|
+
const filterKey = filters ? JSON.stringify(filters) : 'all';
|
|
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
|
+
}
|
|
66
140
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
141
|
+
return runWithTranscriptLoadLimit(
|
|
142
|
+
() =>
|
|
143
|
+
parseCodexTranscriptFile(sessionFile, {
|
|
144
|
+
eventFilter: filters ? (event) => shouldShowCodexTranscriptEvent(event, filters) : undefined,
|
|
145
|
+
includeRaw: false,
|
|
146
|
+
maxTurnContexts: 0,
|
|
147
|
+
sourceFileSizeBytes: fileSizeBytes,
|
|
148
|
+
tailEventLimit: previewEventLimit,
|
|
149
|
+
}),
|
|
150
|
+
{
|
|
151
|
+
path: sessionFile,
|
|
152
|
+
source: 'codex-preview',
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
},
|
|
156
|
+
);
|
|
74
157
|
};
|
|
@@ -14,13 +14,29 @@ import type {
|
|
|
14
14
|
TurnContextRecord,
|
|
15
15
|
WebSearchEvent,
|
|
16
16
|
} from './codex-browser-types';
|
|
17
|
-
import { asNumber, asObject, asString, type JsonValue, readJsonlObjects } from './shared';
|
|
17
|
+
import { asNumber, asObject, asString, type JsonValue, readJsonlObjects, stripCodexAppDirectiveLines } from './shared';
|
|
18
18
|
|
|
19
19
|
type ParseCodexTranscriptOptions = {
|
|
20
|
+
eventFilter?: (event: ThreadEvent) => boolean;
|
|
20
21
|
includeRaw?: boolean;
|
|
21
22
|
maxEvents?: number;
|
|
22
23
|
maxTurnContexts?: number;
|
|
23
24
|
sourceFileSizeBytes?: number | null;
|
|
25
|
+
tailEventLimit?: number;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type ParseCodexTranscriptState = {
|
|
29
|
+
events: ThreadEvent[];
|
|
30
|
+
eventFilter: (event: ThreadEvent) => boolean;
|
|
31
|
+
includeRaw: boolean;
|
|
32
|
+
maxEvents: number;
|
|
33
|
+
maxTurnContexts: number;
|
|
34
|
+
sequence: number;
|
|
35
|
+
shouldStop: boolean;
|
|
36
|
+
tailEventCursor: number;
|
|
37
|
+
tailEventLimit: number;
|
|
38
|
+
tailEventsWrapped: boolean;
|
|
39
|
+
turnContexts: TurnContextRecord[];
|
|
24
40
|
};
|
|
25
41
|
|
|
26
42
|
const createEmptyStats = (): ThreadTranscriptStats => {
|
|
@@ -64,45 +80,88 @@ export const parseCodexTranscriptFile = async (
|
|
|
64
80
|
const includeRaw = options.includeRaw ?? true;
|
|
65
81
|
const maxEvents = options.maxEvents ?? Number.POSITIVE_INFINITY;
|
|
66
82
|
const maxTurnContexts = options.maxTurnContexts ?? Number.POSITIVE_INFINITY;
|
|
67
|
-
|
|
83
|
+
const tailEventLimit = options.tailEventLimit ?? Number.POSITIVE_INFINITY;
|
|
84
|
+
const eventFilter = options.eventFilter ?? (() => true);
|
|
85
|
+
const state: ParseCodexTranscriptState = {
|
|
86
|
+
eventFilter,
|
|
87
|
+
events,
|
|
88
|
+
includeRaw,
|
|
89
|
+
maxEvents,
|
|
90
|
+
maxTurnContexts,
|
|
91
|
+
sequence: 0,
|
|
92
|
+
shouldStop: false,
|
|
93
|
+
tailEventCursor: 0,
|
|
94
|
+
tailEventLimit,
|
|
95
|
+
tailEventsWrapped: false,
|
|
96
|
+
turnContexts,
|
|
97
|
+
};
|
|
68
98
|
|
|
69
99
|
for await (const parsed of readJsonlObjects(sessionFile)) {
|
|
70
100
|
captureSessionMeta(parsed, sessionMeta);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (turnContexts.length < maxTurnContexts) {
|
|
75
|
-
captureTurnContext(parsed, turnContexts);
|
|
76
|
-
}
|
|
77
|
-
continue;
|
|
101
|
+
captureTranscriptRecord(parsed, state);
|
|
102
|
+
if (state.shouldStop) {
|
|
103
|
+
break;
|
|
78
104
|
}
|
|
105
|
+
}
|
|
79
106
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
+
}
|
|
84
115
|
|
|
85
|
-
|
|
116
|
+
for (const event of events) {
|
|
86
117
|
updateTranscriptStats(stats, event);
|
|
87
|
-
sequence += 1;
|
|
88
|
-
|
|
89
|
-
if (events.length >= maxEvents) {
|
|
90
|
-
break;
|
|
91
|
-
}
|
|
92
118
|
}
|
|
93
119
|
|
|
94
120
|
return {
|
|
95
121
|
events,
|
|
96
|
-
isPartial:
|
|
122
|
+
isPartial:
|
|
123
|
+
Number.isFinite(maxEvents) ||
|
|
124
|
+
Number.isFinite(maxTurnContexts) ||
|
|
125
|
+
Number.isFinite(tailEventLimit) ||
|
|
126
|
+
options.eventFilter !== undefined,
|
|
97
127
|
rawIncluded: includeRaw,
|
|
98
128
|
sessionMeta,
|
|
99
129
|
sourceFileSizeBytes: options.sourceFileSizeBytes ?? null,
|
|
100
130
|
stats,
|
|
101
|
-
statsArePartial:
|
|
131
|
+
statsArePartial:
|
|
132
|
+
Number.isFinite(maxEvents) || Number.isFinite(tailEventLimit) || options.eventFilter !== undefined,
|
|
102
133
|
turnContexts,
|
|
103
134
|
};
|
|
104
135
|
};
|
|
105
136
|
|
|
137
|
+
const captureTranscriptRecord = (parsed: Record<string, JsonValue>, state: ParseCodexTranscriptState) => {
|
|
138
|
+
if (asString(parsed.type) === 'turn_context') {
|
|
139
|
+
if (state.turnContexts.length < state.maxTurnContexts) {
|
|
140
|
+
captureTurnContext(parsed, state.turnContexts);
|
|
141
|
+
}
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const event = parseCodexTranscriptRecord(parsed, state.sequence, state.includeRaw);
|
|
146
|
+
if (!event) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
state.sequence += 1;
|
|
151
|
+
if (!state.eventFilter(event)) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
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;
|
|
161
|
+
}
|
|
162
|
+
state.shouldStop = state.events.length >= state.maxEvents;
|
|
163
|
+
};
|
|
164
|
+
|
|
106
165
|
const captureSessionMeta = (parsed: Record<string, JsonValue>, sessionMeta: SessionMetaExtended) => {
|
|
107
166
|
if (parsed.type !== 'session_meta') {
|
|
108
167
|
return;
|
|
@@ -161,10 +220,10 @@ const captureTurnContext = (parsed: Record<string, JsonValue>, turnContexts: Tur
|
|
|
161
220
|
});
|
|
162
221
|
};
|
|
163
222
|
|
|
164
|
-
const
|
|
223
|
+
export const parseCodexTranscriptRecord = (
|
|
165
224
|
parsed: Record<string, JsonValue>,
|
|
166
|
-
sequence
|
|
167
|
-
includeRaw
|
|
225
|
+
sequence = 0,
|
|
226
|
+
includeRaw = false,
|
|
168
227
|
): ThreadEvent | null => {
|
|
169
228
|
const payload = asObject(parsed.payload);
|
|
170
229
|
if (!payload) {
|
|
@@ -192,15 +251,7 @@ const buildEventMessage = (
|
|
|
192
251
|
sequence: number,
|
|
193
252
|
timestamp: string | null,
|
|
194
253
|
) => {
|
|
195
|
-
|
|
196
|
-
return createTaskStartedEvent(payload, raw, sequence, timestamp);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (payloadType === 'task_complete') {
|
|
200
|
-
return createTaskCompleteEvent(payload, raw, sequence, timestamp);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return null;
|
|
254
|
+
return buildSupplementalEvent(payload, payloadType, raw, sequence, timestamp);
|
|
204
255
|
};
|
|
205
256
|
|
|
206
257
|
const buildResponseItemEvent = (
|
|
@@ -222,11 +273,11 @@ const buildResponseItemEvent = (
|
|
|
222
273
|
return createAgentMessageEvent(payload, raw, sequence, timestamp);
|
|
223
274
|
}
|
|
224
275
|
|
|
225
|
-
if (payloadType === 'function_call') {
|
|
226
|
-
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');
|
|
227
278
|
}
|
|
228
279
|
|
|
229
|
-
if (payloadType === 'function_call_output') {
|
|
280
|
+
if (payloadType === 'function_call_output' || payloadType === 'custom_tool_call_output') {
|
|
230
281
|
return createToolOutputEvent(payload, raw, sequence, timestamp);
|
|
231
282
|
}
|
|
232
283
|
|
|
@@ -234,6 +285,16 @@ const buildResponseItemEvent = (
|
|
|
234
285
|
return createReasoningEvent(payload, raw, sequence, timestamp);
|
|
235
286
|
}
|
|
236
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
|
+
) => {
|
|
237
298
|
if (payloadType === 'token_count') {
|
|
238
299
|
return createTokenCountEvent(payload, raw, sequence, timestamp);
|
|
239
300
|
}
|
|
@@ -261,12 +322,13 @@ const createMessageEvent = (
|
|
|
261
322
|
): MessageEvent | null => {
|
|
262
323
|
const role = asString(payload.role);
|
|
263
324
|
const content = payload.content;
|
|
264
|
-
|
|
325
|
+
const text = extractText(content);
|
|
326
|
+
if (!role || content === undefined || !text.trim()) {
|
|
265
327
|
return null;
|
|
266
328
|
}
|
|
267
329
|
|
|
268
330
|
return {
|
|
269
|
-
isHiddenByDefault:
|
|
331
|
+
isHiddenByDefault: shouldHideCodexTranscriptText(role, text),
|
|
270
332
|
kind: 'message',
|
|
271
333
|
memoryCitation: null,
|
|
272
334
|
model: asString(payload.model),
|
|
@@ -274,7 +336,7 @@ const createMessageEvent = (
|
|
|
274
336
|
raw,
|
|
275
337
|
role,
|
|
276
338
|
sequence,
|
|
277
|
-
text
|
|
339
|
+
text,
|
|
278
340
|
timestamp,
|
|
279
341
|
variant: 'message',
|
|
280
342
|
};
|
|
@@ -286,8 +348,9 @@ const createUserMessageEvent = (
|
|
|
286
348
|
sequence: number,
|
|
287
349
|
timestamp: string | null,
|
|
288
350
|
): MessageEvent => {
|
|
351
|
+
const text = stripCodexMemoryCitationBlocks(asString(payload.message) ?? '');
|
|
289
352
|
return {
|
|
290
|
-
isHiddenByDefault:
|
|
353
|
+
isHiddenByDefault: shouldHideCodexTranscriptText('user', text),
|
|
291
354
|
kind: 'message',
|
|
292
355
|
memoryCitation: null,
|
|
293
356
|
model: null,
|
|
@@ -295,7 +358,7 @@ const createUserMessageEvent = (
|
|
|
295
358
|
raw,
|
|
296
359
|
role: 'user',
|
|
297
360
|
sequence,
|
|
298
|
-
text
|
|
361
|
+
text,
|
|
299
362
|
timestamp,
|
|
300
363
|
variant: 'user_message',
|
|
301
364
|
};
|
|
@@ -307,8 +370,9 @@ const createAgentMessageEvent = (
|
|
|
307
370
|
sequence: number,
|
|
308
371
|
timestamp: string | null,
|
|
309
372
|
): MessageEvent => {
|
|
373
|
+
const text = stripCodexMemoryCitationBlocks(asString(payload.message) ?? '');
|
|
310
374
|
return {
|
|
311
|
-
isHiddenByDefault:
|
|
375
|
+
isHiddenByDefault: shouldHideCodexTranscriptText('assistant', text),
|
|
312
376
|
kind: 'message',
|
|
313
377
|
memoryCitation: payload.memory_citation ?? null,
|
|
314
378
|
model: asString(payload.model),
|
|
@@ -316,7 +380,7 @@ const createAgentMessageEvent = (
|
|
|
316
380
|
raw,
|
|
317
381
|
role: 'assistant',
|
|
318
382
|
sequence,
|
|
319
|
-
text
|
|
383
|
+
text,
|
|
320
384
|
timestamp,
|
|
321
385
|
variant: 'agent_message',
|
|
322
386
|
};
|
|
@@ -327,10 +391,13 @@ const createToolCallEvent = (
|
|
|
327
391
|
raw: Record<string, JsonValue>,
|
|
328
392
|
sequence: number,
|
|
329
393
|
timestamp: string | null,
|
|
394
|
+
isCustomToolCall: boolean,
|
|
330
395
|
): ToolCallEvent => {
|
|
331
396
|
const name = asString(payload.name) ?? 'unknown';
|
|
332
|
-
const argumentsText = asString(payload.arguments);
|
|
333
|
-
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);
|
|
334
401
|
|
|
335
402
|
return {
|
|
336
403
|
argumentsParseFailed: parsedArguments.argumentsParseFailed,
|
|
@@ -352,7 +419,7 @@ const createToolOutputEvent = (
|
|
|
352
419
|
sequence: number,
|
|
353
420
|
timestamp: string | null,
|
|
354
421
|
): ToolOutputEvent => {
|
|
355
|
-
const outputText =
|
|
422
|
+
const outputText = extractToolOutputText(payload.output);
|
|
356
423
|
|
|
357
424
|
return {
|
|
358
425
|
callId: asString(payload.call_id),
|
|
@@ -478,7 +545,7 @@ const updateTranscriptStats = (stats: ThreadTranscriptStats, event: ThreadEvent)
|
|
|
478
545
|
|
|
479
546
|
if (event.kind === 'tool_call') {
|
|
480
547
|
stats.toolCallCount += 1;
|
|
481
|
-
if (event.name === 'exec_command') {
|
|
548
|
+
if (event.name === 'exec' || event.name === 'exec_command') {
|
|
482
549
|
stats.execCommandCount += 1;
|
|
483
550
|
}
|
|
484
551
|
return;
|
|
@@ -546,26 +613,52 @@ const parseExecCommandArguments = (argumentsText: string | null) => {
|
|
|
546
613
|
}
|
|
547
614
|
};
|
|
548
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
|
+
|
|
549
634
|
const extractText = (content: JsonValue): string => {
|
|
550
635
|
if (typeof content === 'string') {
|
|
551
|
-
return content
|
|
636
|
+
return stripCodexMemoryCitationBlocks(content);
|
|
552
637
|
}
|
|
553
638
|
|
|
554
639
|
if (Array.isArray(content)) {
|
|
555
|
-
return
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
640
|
+
return stripCodexMemoryCitationBlocks(
|
|
641
|
+
content
|
|
642
|
+
.map((entry) => extractTextPart(entry))
|
|
643
|
+
.filter(Boolean)
|
|
644
|
+
.join('\n\n'),
|
|
645
|
+
);
|
|
560
646
|
}
|
|
561
647
|
|
|
562
648
|
if (content && typeof content === 'object') {
|
|
563
|
-
return asString((content as Record<string, JsonValue>).text)
|
|
649
|
+
return stripCodexMemoryCitationBlocks(asString((content as Record<string, JsonValue>).text) ?? '');
|
|
564
650
|
}
|
|
565
651
|
|
|
566
652
|
return '';
|
|
567
653
|
};
|
|
568
654
|
|
|
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);
|
|
660
|
+
};
|
|
661
|
+
|
|
569
662
|
const extractTextPart = (entry: JsonValue): string => {
|
|
570
663
|
const objectValue = asObject(entry);
|
|
571
664
|
if (!objectValue) {
|
|
@@ -582,7 +675,7 @@ const extractTextPart = (entry: JsonValue): string => {
|
|
|
582
675
|
return text ?? '';
|
|
583
676
|
};
|
|
584
677
|
|
|
585
|
-
const
|
|
678
|
+
export const shouldHideCodexTranscriptText = (role: string, text: string) => {
|
|
586
679
|
if (!text) {
|
|
587
680
|
return true;
|
|
588
681
|
}
|
|
@@ -593,6 +686,7 @@ const shouldHideTranscriptText = (role: string, text: string) => {
|
|
|
593
686
|
|
|
594
687
|
return (
|
|
595
688
|
text.startsWith('# AGENTS.md instructions for ') ||
|
|
689
|
+
text.startsWith('AGENTS.md instructions for ') ||
|
|
596
690
|
text.startsWith('<permissions instructions>') ||
|
|
597
691
|
text.startsWith('<app-context>') ||
|
|
598
692
|
text.startsWith('<environment_context>') ||
|