agentel 0.2.5 → 0.2.8

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/src/importers.js CHANGED
@@ -7,7 +7,7 @@ const path = require("path");
7
7
  const zlib = require("zlib");
8
8
  const { execFileSync, spawnSync } = require("child_process");
9
9
  const { fileURLToPath } = require("url");
10
- const { archiveRoot, deleteSessionArchive, listSessions, readTranscript, writeSession, stableSessionId, toIso } = require("./archive");
10
+ const { archiveRoot, computeSessionUsage, deleteSessionArchive, listSessions, readTranscript, writeSession, stableSessionId, toIso } = require("./archive");
11
11
  const { fingerprintPrefix, parserVersionForSource } = require("./parser-versions");
12
12
  const { canonicalRepo } = require("./repo");
13
13
  const { ensureDir, paths, readJson, writeJson } = require("./paths");
@@ -19,9 +19,11 @@ const { parseGeminiCliJsonSessions, parseGeminiCliJsonlSessions } = require("./i
19
19
  const { importSourceWithAdapter, providerAdapterForSource, providerAdapterSources } = require("./importers/providers");
20
20
  const { parseSince } = require("./importers/shared");
21
21
  const { canonicalWebProvider, derivedAccountId, getWebAccount, upsertWebAccount } = require("./web-accounts");
22
+ const { manualImportInstructionResult } = require("./web-export-instructions");
22
23
 
23
24
  const WEB_TOKEN_ESTIMATE_CHARS = 4;
24
25
  const WEB_CHAT_TOKEN_ESTIMATION_METHOD = "web-message-parts-chars-v1";
26
+ const EXPORT_ZIP_ENTRY_MAX_BUFFER = 1024 * 1024 * 512;
25
27
  const OPENCODE_SOURCE_KINDS = new Set(["cli", "desktop", "web"]);
26
28
  const OPENCODE_SESSION_ID_RE = /\bses_[A-Za-z0-9]+\b/g;
27
29
 
@@ -46,6 +48,7 @@ function importProviderHelpers() {
46
48
  importCursorProvider,
47
49
  importJsonlProvider,
48
50
  importStructuredProvider,
51
+ manualImportInstructionResult,
49
52
  readAntigravitySessions,
50
53
  readAiderSessions,
51
54
  readClineSessions,
@@ -79,10 +82,22 @@ function importJsonlProvider(provider, roots, since, options = {}, env = process
79
82
  const files =
80
83
  provider === "claude_code" ? claudeFiles(env) : provider === "claude_sdk" ? claudeSdkFiles(env) : jsonlFiles(roots);
81
84
  const claudeCodeMetadata = provider === "claude_code" ? claudeCodeSessionMetadataByCliSessionId(env) : new Map();
85
+ const claudeSubagentCache = provider === "claude_code" ? new Map() : null;
82
86
 
83
87
  const candidates = files
84
- .map((file) => ({ file, stat: safeStat(file) }))
85
- .filter((item) => item.stat && (!since || item.stat.mtime >= since))
88
+ .map((file) => {
89
+ const stat = safeStat(file);
90
+ const claudeSubagentRunSourceFiles = provider === "claude_code"
91
+ ? claudeSubagentRunSourceFilesForSession(claudeSessionIdFromFilename(file), file, env)
92
+ : [];
93
+ return {
94
+ file,
95
+ stat,
96
+ claudeSubagentRunSourceFiles,
97
+ importMtimeMs: latestFileMtimeMs([file, ...claudeSubagentRunSourceFiles], stat)
98
+ };
99
+ })
100
+ .filter((item) => item.stat && (!since || item.importMtimeMs >= since.getTime()))
86
101
  .sort((a, b) => a.file.localeCompare(b.file));
87
102
 
88
103
  const summary = {
@@ -98,7 +113,11 @@ function importJsonlProvider(provider, roots, since, options = {}, env = process
98
113
  for (let index = 0; index < candidates.length; index++) {
99
114
  const item = candidates[index];
100
115
  const sourceType = jsonlProviderSourceType(provider);
101
- const baseFingerprint = `${fingerprintPrefix(sourceType)}:${fileFingerprint(item.file, item.stat)}`;
116
+ const subagentRunFingerprint = provider === "claude_code" ? filesFingerprint(item.claudeSubagentRunSourceFiles) : "";
117
+ const baseFingerprint = [
118
+ `${fingerprintPrefix(sourceType)}:${fileFingerprint(item.file, item.stat)}`,
119
+ subagentRunFingerprint ? `claude-subagent-runs:${subagentRunFingerprint}` : ""
120
+ ].filter(Boolean).join(":");
102
121
  const preliminaryMetadata = provider === "claude_code"
103
122
  ? claudeCodeMetadata.get(claudeSessionIdFromFilename(item.file)) || null
104
123
  : null;
@@ -128,6 +147,12 @@ function importJsonlProvider(provider, roots, since, options = {}, env = process
128
147
  const cwd = parsed.cwd || sessionMetadata?.cwd || "";
129
148
  const scopeCanonical = cwd ? "" : uncategorizedScope(provider);
130
149
  const repo = repoInfoForImport(provider, cwd, sessionMetadata);
150
+ const claudeSubagents = provider === "claude_code"
151
+ ? claudeSubagentImportContext(repoCwdForImport(provider, cwd, sessionMetadata), env, claudeSubagentCache)
152
+ : null;
153
+ const claudeSubagentRuns = provider === "claude_code"
154
+ ? claudeSubagentRunImportContext(sessionId, item.file, env, item.claudeSubagentRunSourceFiles)
155
+ : null;
131
156
  if (options.repos && options.repos.length && (!repo || !options.repos.includes(repo.key))) {
132
157
  summary.skipped++;
133
158
  reportProgress(options, summary, index + 1, item.file);
@@ -154,18 +179,48 @@ function importJsonlProvider(provider, roots, since, options = {}, env = process
154
179
  startedAt: parsed.startedAt,
155
180
  endedAt: parsed.endedAt,
156
181
  sourcePath: item.file,
157
- sourceFiles: [item.file, sessionMetadata?.sourcePath || "", ...auxiliaryFiles].filter(Boolean),
182
+ sourceFiles: [
183
+ item.file,
184
+ sessionMetadata?.sourcePath || "",
185
+ ...auxiliaryFiles,
186
+ ...(claudeSubagents?.sourceFiles || []),
187
+ ...(claudeSubagentRuns?.sourceFiles || [])
188
+ ].filter(Boolean),
158
189
  sourceType,
159
190
  title: jsonlSessionTitleForImport(parsed, sessionMetadata),
160
- sessionSummary: claudeCodeSidecarSessionSummary(sessionMetadata)
191
+ sessionSummary: mergeSessionSummaries(
192
+ claudeCodeSidecarSessionSummary(sessionMetadata),
193
+ parsed.sessionSummary,
194
+ claudeSubagents?.sessionSummary,
195
+ claudeSubagentRuns?.sessionSummary
196
+ )
161
197
  },
162
198
  env
163
199
  );
164
200
  state.files[fingerprint] = { sessionId, at: new Date().toISOString() };
165
201
  state.sessions[sessionId] = { provider, sourcePath: item.file, fingerprint, auxiliary: auxiliaryFiles.length || undefined, at: new Date().toISOString() };
166
202
  archived.add(archiveSessionKey(provider, sessionId));
203
+ for (const subagentSession of claudeSubagentRuns?.sessions || []) {
204
+ writeSession(
205
+ {
206
+ ...subagentSession,
207
+ repoInfo: repo || undefined,
208
+ scopeCanonical,
209
+ sourceType
210
+ },
211
+ env
212
+ );
213
+ state.sessions[subagentSession.sessionId] = {
214
+ provider,
215
+ sourcePath: subagentSession.sourcePath,
216
+ fingerprint: `${fingerprintPrefix(sourceType)}:${filesFingerprint(subagentSession.sourceFiles || [subagentSession.sourcePath])}`,
217
+ parentSessionId: sessionId,
218
+ at: new Date().toISOString()
219
+ };
220
+ archived.add(archiveSessionKey(provider, subagentSession.sessionId));
221
+ }
167
222
  }
168
- summary.imported++;
223
+ summary.imported += 1 + (claudeSubagentRuns?.sessions?.length || 0);
169
224
  reportProgress(options, summary, index + 1, item.file);
170
225
  }
171
226
 
@@ -179,6 +234,52 @@ function jsonlProviderSourceType(provider) {
179
234
  return "cli-history";
180
235
  }
181
236
 
237
+ function codexThreadSourceType(thread) {
238
+ if (thread.source === "vscode") return "codex-desktop-history";
239
+ if (thread.source === "exec") return "codex-sdk-history";
240
+ return "codex-cli-history";
241
+ }
242
+
243
+ function finalizeCodexParsedThread(thread, parsed) {
244
+ const result = { ...(parsed || {}) };
245
+ result.messages = Array.isArray(parsed?.messages) ? [...parsed.messages] : [];
246
+ result.messages.push(...codexSupplementaryMessages(thread, result.endedAt || thread?.updatedAt));
247
+ result.messages = dedupeAdjacentMessages(result.messages)
248
+ .sort((a, b) => String(a.timestamp || "").localeCompare(String(b.timestamp || "")));
249
+ result.startedAt = result.messages[0]?.timestamp || result.startedAt;
250
+ result.endedAt = result.messages[result.messages.length - 1]?.timestamp || result.endedAt;
251
+ result.sessionSummary = mergeSessionSummaries(result.sessionSummary, codexThreadStateSessionSummary(thread));
252
+ return result;
253
+ }
254
+
255
+ function codexThreadStateSessionSummary(thread) {
256
+ const rawTotalTokens = Number(thread?.tokensUsed);
257
+ const totalTokens = Number.isFinite(rawTotalTokens) && rawTotalTokens > 0 ? rawTotalTokens : 0;
258
+ const model = firstString(thread?.model);
259
+ if (!totalTokens && !model) return null;
260
+ return {
261
+ usage: totalTokens ? { totalTokens, authoritativeTotalTokens: true, source: "codex-state-tokens-used" } : undefined,
262
+ modelUsage: model ? [{ model, source: "codex-state" }] : undefined
263
+ };
264
+ }
265
+
266
+ function codexSubagentChildrenByParent(threads) {
267
+ const byParent = new Map();
268
+ for (const thread of threads || []) {
269
+ if (!thread?.parentThreadId) continue;
270
+ const children = byParent.get(thread.parentThreadId) || [];
271
+ children.push(thread);
272
+ byParent.set(thread.parentThreadId, children);
273
+ }
274
+ for (const children of byParent.values()) {
275
+ children.sort((a, b) => {
276
+ const time = String(a.createdAt || a.updatedAt || "").localeCompare(String(b.createdAt || b.updatedAt || ""));
277
+ return time || String(a.id || "").localeCompare(String(b.id || ""));
278
+ });
279
+ }
280
+ return byParent;
281
+ }
282
+
182
283
  function importClaudeDesktopProvider(provider, since, options = {}, env = process.env) {
183
284
  const state = loadImportState(env);
184
285
  const archived = archivedSessionKeys(env);
@@ -257,17 +358,20 @@ function matchesImportedSessionRepo(session, repo, wantedRepos) {
257
358
 
258
359
  function importCodexProvider(provider, since, options = {}, env = process.env) {
259
360
  const threads = readCodexSessionEntries(env).filter((thread) => {
260
- if (options.codexSource && thread.source !== options.codexSource) return false;
261
- if (!["cli", "vscode"].includes(thread.source)) return false;
262
- return true;
361
+ if (options.codexSource) return thread.source === options.codexSource;
362
+ return ["cli", "vscode"].includes(thread.source);
263
363
  });
364
+ const codexSubagentChildren = codexSubagentChildrenByParent(threads);
365
+ if (!threads.length && options.codexSource && options.codexSource !== "cli") {
366
+ return { provider, discovered: 0, candidates: 0, imported: 0, skipped: 0, errors: [], details: {} };
367
+ }
264
368
  if (!threads.length) return importJsonlProvider(provider, codexRoots(env), since, options, env);
265
369
 
266
370
  const state = loadImportState(env);
267
371
  const archived = archivedSessionKeys(env);
268
372
  const candidates = threads
269
373
  .filter((thread) => thread.rolloutPath)
270
- .filter((thread) => !since || new Date(thread.updatedAt || thread.createdAt) >= since)
374
+ .filter((thread) => !since || new Date(codexThreadImportUpdatedAt(thread, codexSubagentChildren.get(thread.id) || [])) >= since)
271
375
  .sort((a, b) => a.rolloutPath.localeCompare(b.rolloutPath));
272
376
  const summary = {
273
377
  provider,
@@ -288,8 +392,14 @@ function importCodexProvider(provider, since, options = {}, env = process.env) {
288
392
  reportProgress(options, summary, index + 1, thread.rolloutPath);
289
393
  continue;
290
394
  }
291
- const sourceType = thread.source === "vscode" ? "codex-desktop-history" : "codex-cli-history";
292
- const fingerprint = `${fingerprintPrefix(sourceType)}:${fileFingerprint(thread.rolloutPath, stat)}:${codexSupplementFingerprint(thread)}`;
395
+ const sourceType = codexThreadSourceType(thread);
396
+ const children = codexSubagentChildren.get(thread.id) || [];
397
+ const fingerprint = [
398
+ `${fingerprintPrefix(sourceType)}:${fileFingerprint(thread.rolloutPath, stat)}`,
399
+ codexSupplementFingerprint(thread),
400
+ codexThreadMetadataFingerprint(thread),
401
+ codexSubagentRunsFingerprint(children)
402
+ ].filter(Boolean).join(":");
293
403
  if (alreadyImported(state, thread.id, fingerprint, archived, provider)) {
294
404
  summary.skipped++;
295
405
  reportProgress(options, summary, index + 1, thread.rolloutPath);
@@ -303,10 +413,7 @@ function importCodexProvider(provider, since, options = {}, env = process.env) {
303
413
  reportProgress(options, summary, index + 1, thread.rolloutPath);
304
414
  continue;
305
415
  }
306
- parsed.messages.push(...codexSupplementaryMessages(thread, parsed.endedAt || thread.updatedAt));
307
- parsed.messages = dedupeAdjacentMessages(parsed.messages).sort((a, b) => String(a.timestamp || "").localeCompare(String(b.timestamp || "")));
308
- parsed.startedAt = parsed.messages[0]?.timestamp || parsed.startedAt;
309
- parsed.endedAt = parsed.messages[parsed.messages.length - 1]?.timestamp || parsed.endedAt;
416
+ parsed = finalizeCodexParsedThread(thread, parsed);
310
417
  if (!parsed.messages.length) {
311
418
  state.files[fingerprint] = { skipped: true, reason: "no messages", at: new Date().toISOString() };
312
419
  summary.skipped++;
@@ -328,6 +435,8 @@ function importCodexProvider(provider, since, options = {}, env = process.env) {
328
435
  reportProgress(options, summary, index + 1, thread.rolloutPath);
329
436
  continue;
330
437
  }
438
+ const codexSubagentRuns = codexSubagentRunImportContext(thread, children, env);
439
+ const codexSubagentRun = codexSubagentSessionSummary(thread, parsed, env);
331
440
  if (!options.dryRun) {
332
441
  writeSession(
333
442
  {
@@ -340,9 +449,12 @@ function importCodexProvider(provider, since, options = {}, env = process.env) {
340
449
  startedAt: parsed.startedAt || thread.createdAt,
341
450
  endedAt: parsed.endedAt || thread.updatedAt,
342
451
  sourcePath: thread.rolloutPath,
343
- sourceFiles: codexSourceFiles(thread, env),
452
+ sourceFiles: codexSourceFiles(thread, env, children),
344
453
  sourceType,
345
- title: thread.title || parsed.title
454
+ title: codexSessionTitleForImport(thread, parsed),
455
+ sessionSummary: mergeSessionSummaries(parsed.sessionSummary, codexSubagentRuns?.sessionSummary, codexSubagentRun?.sessionSummary),
456
+ conversationKind: thread.isCodexSubagent ? "codex_subagent" : undefined,
457
+ parentComposerId: thread.parentThreadId || undefined
346
458
  },
347
459
  env
348
460
  );
@@ -579,10 +691,12 @@ function structuredSessionUsesSharedRawFiles(provider, sourceType) {
579
691
 
580
692
  function parseAgentJsonl(file, provider) {
581
693
  const text = readTextMaybeZstd(file);
694
+ const events = [];
582
695
  const messages = [];
583
696
  let cwd = "";
584
697
  let sessionId = "";
585
698
  let title = "";
699
+ let titleSource = "";
586
700
  const context = { model: "", tokenUsage: { input: 0, output: 0 } };
587
701
  for (const line of text.split(/\r?\n/)) {
588
702
  if (!line.trim()) continue;
@@ -592,6 +706,7 @@ function parseAgentJsonl(file, provider) {
592
706
  } catch {
593
707
  continue;
594
708
  }
709
+ events.push(event);
595
710
  cwd ||= firstString(event.cwd, event.working_directory, event.workingDirectory, event.context?.cwd, event.payload?.cwd);
596
711
  sessionId ||= firstString(
597
712
  event.session_id,
@@ -603,7 +718,24 @@ function parseAgentJsonl(file, provider) {
603
718
  event.payload?.session_id,
604
719
  event.sessionId
605
720
  );
606
- title ||= firstString(event.title, event.conversation_title, event.payload?.title);
721
+ const codexThreadTitle = provider === "codex" ? codexThreadNameFromEvent(event) : "";
722
+ if (codexThreadTitle) {
723
+ title = codexThreadTitle;
724
+ titleSource = "thread-name";
725
+ } else if (!title) {
726
+ const sourceTitle = firstString(event.title, event.conversation_title, event.payload?.title);
727
+ const aiTitle = firstString(event.aiTitle, event.ai_title);
728
+ if (sourceTitle) {
729
+ title = sourceTitle;
730
+ titleSource = "source";
731
+ } else if (aiTitle) {
732
+ title = aiTitle;
733
+ titleSource = "ai-title";
734
+ }
735
+ }
736
+ collectClaudeJsonlSessionMetadata(event, provider, context);
737
+ }
738
+ for (const event of events) {
607
739
  updateCodexParseContext(event, provider, context);
608
740
  updateClaudeParseContext(event, provider, context);
609
741
  if (applyCodexTokenCount(event, provider, messages, context)) continue;
@@ -617,13 +749,292 @@ function parseAgentJsonl(file, provider) {
617
749
  cwd,
618
750
  sessionId,
619
751
  title: title || inferredTitle,
620
- titleSource: title ? "source" : inferredTitle ? "first-user-prompt" : "",
752
+ titleSource: titleSource || (inferredTitle ? "first-user-prompt" : ""),
753
+ sessionSummary: claudeJsonlSessionSummary(provider, context),
621
754
  messages: deduped,
622
755
  startedAt: deduped[0]?.timestamp || "",
623
756
  endedAt: deduped[deduped.length - 1]?.timestamp || ""
624
757
  };
625
758
  }
626
759
 
760
+ function collectClaudeJsonlSessionMetadata(event, provider, context = {}) {
761
+ if (!isClaudeJsonlProvider(provider) || !event || typeof event !== "object") return;
762
+ const metadata = context.claudeJsonl || (context.claudeJsonl = {
763
+ eventTypeCounts: {},
764
+ entrypoints: new Set(),
765
+ userTypes: new Set(),
766
+ versions: new Set(),
767
+ gitBranches: new Set(),
768
+ permissionModes: new Set(),
769
+ promptIds: new Set(),
770
+ requestIds: new Set(),
771
+ sourceToolAssistantUUIDs: new Set(),
772
+ attachmentTypes: new Set(),
773
+ remoteToolNames: new Set(),
774
+ mcpServerNames: new Set(),
775
+ toolNames: new Set(),
776
+ agentIds: new Set(),
777
+ slugs: new Set(),
778
+ sourceToolUseIDs: new Set(),
779
+ parentToolUseIDs: new Set(),
780
+ toolUseIDs: new Set(),
781
+ attributionSkills: new Set(),
782
+ queue: {},
783
+ attachmentDetails: {},
784
+ apiErrors: {},
785
+ mcpStructuredContentCount: 0,
786
+ aiTitles: new Set(),
787
+ lastPrompt: "",
788
+ sidechainMessageCount: 0,
789
+ messageEventCount: 0,
790
+ remoteControl: false
791
+ });
792
+
793
+ const type = firstString(event.type, event.kind);
794
+ if (type) metadata.eventTypeCounts[type] = (metadata.eventTypeCounts[type] || 0) + 1;
795
+ addSetValue(metadata.entrypoints, event.entrypoint);
796
+ addSetValue(metadata.userTypes, event.userType, event.user_type);
797
+ addSetValue(metadata.versions, event.version);
798
+ addSetValue(metadata.gitBranches, event.gitBranch, event.git_branch);
799
+ addSetValue(metadata.permissionModes, event.permissionMode, event.permission_mode);
800
+ addSetValue(metadata.promptIds, event.promptId, event.prompt_id);
801
+ addSetValue(metadata.requestIds, event.requestId, event.request_id);
802
+ addSetValue(metadata.sourceToolAssistantUUIDs, event.sourceToolAssistantUUID, event.source_tool_assistant_uuid);
803
+ addSetValue(metadata.agentIds, event.agentId, event.agent_id);
804
+ addSetValue(metadata.slugs, event.slug);
805
+ addSetValue(metadata.sourceToolUseIDs, event.sourceToolUseID, event.source_tool_use_id);
806
+ addSetValue(metadata.parentToolUseIDs, event.parentToolUseID, event.parent_tool_use_id);
807
+ addSetValue(metadata.toolUseIDs, event.toolUseID, event.tool_use_id);
808
+ addSetValue(metadata.attributionSkills, event.attributionSkill, event.attribution_skill);
809
+ if (event.mcpMeta?.structuredContent && typeof event.mcpMeta.structuredContent === "object") metadata.mcpStructuredContentCount++;
810
+ collectClaudeApiError(event, metadata);
811
+ if (event.isSidechain === true) metadata.sidechainMessageCount++;
812
+ if ((event.type === "user" || event.type === "assistant") && event.message) metadata.messageEventCount++;
813
+
814
+ const aiTitle = firstString(event.aiTitle, event.ai_title);
815
+ if (aiTitle) metadata.aiTitles.add(aiTitle);
816
+ const lastPrompt = firstString(event.lastPrompt, event.last_prompt);
817
+ if (lastPrompt) metadata.lastPrompt = lastPrompt;
818
+
819
+ if (event.type === "queue-operation") collectClaudeQueueOperation(event, metadata);
820
+ if (event.type === "attachment") collectClaudeAttachmentMetadata(event, metadata);
821
+ collectClaudeToolNames(event, metadata);
822
+ }
823
+
824
+ function collectClaudeQueueOperation(event, metadata) {
825
+ const op = firstString(event.operation).toLowerCase();
826
+ if (!op) return;
827
+ const key = op.replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "unknown";
828
+ metadata.queue[key] = (metadata.queue[key] || 0) + 1;
829
+ const timestamp = toIso(event.timestamp || event.created_at || event.createdAt || event.time);
830
+ if (timestamp) {
831
+ if (!metadata.queue.firstAt || timestamp < metadata.queue.firstAt) metadata.queue.firstAt = timestamp;
832
+ if (!metadata.queue.lastAt || timestamp > metadata.queue.lastAt) metadata.queue.lastAt = timestamp;
833
+ const upper = key.charAt(0).toUpperCase() + key.slice(1);
834
+ if (!metadata.queue[`first${upper}At`] || timestamp < metadata.queue[`first${upper}At`]) metadata.queue[`first${upper}At`] = timestamp;
835
+ if (!metadata.queue[`last${upper}At`] || timestamp > metadata.queue[`last${upper}At`]) metadata.queue[`last${upper}At`] = timestamp;
836
+ }
837
+ if (typeof event.content === "string" && event.content.trim()) {
838
+ metadata.queue.lastContent = event.content.trim();
839
+ }
840
+ }
841
+
842
+ function collectClaudeApiError(event, metadata) {
843
+ if (!event || typeof event !== "object") return;
844
+ if (event.isApiErrorMessage !== true && !event.error && event.apiErrorStatus == null) return;
845
+ const status = Number(event.apiErrorStatus);
846
+ if (Number.isFinite(status)) {
847
+ const key = String(status);
848
+ metadata.apiErrors.statusCounts = metadata.apiErrors.statusCounts || {};
849
+ metadata.apiErrors.statusCounts[key] = (metadata.apiErrors.statusCounts[key] || 0) + 1;
850
+ }
851
+ const type = firstString(event.error);
852
+ if (type) {
853
+ metadata.apiErrors.typeCounts = metadata.apiErrors.typeCounts || {};
854
+ metadata.apiErrors.typeCounts[type] = (metadata.apiErrors.typeCounts[type] || 0) + 1;
855
+ }
856
+ metadata.apiErrors.count = (metadata.apiErrors.count || 0) + 1;
857
+ }
858
+
859
+ function collectClaudeAttachmentMetadata(event, metadata) {
860
+ const attachment = event.attachment && typeof event.attachment === "object" ? event.attachment : {};
861
+ addSetValue(metadata.attachmentTypes, attachment.type);
862
+ collectClaudeAttachmentDetails(attachment, metadata);
863
+ for (const name of [...asStringArray(attachment.addedNames), ...asStringArray(attachment.removedNames)]) {
864
+ if (typeof name !== "string" || !name.trim()) continue;
865
+ const value = name.trim();
866
+ metadata.remoteToolNames.add(value);
867
+ if (isClaudeRemoteControlToolName(value)) metadata.remoteControl = true;
868
+ const server = claudeMcpServerNameFromTool(value);
869
+ if (server) metadata.mcpServerNames.add(server);
870
+ }
871
+ }
872
+
873
+ function collectClaudeAttachmentDetails(attachment, metadata) {
874
+ const type = firstString(attachment.type);
875
+ if (!type) return;
876
+ const details = metadata.attachmentDetails;
877
+ details.countByType = details.countByType || {};
878
+ details.countByType[type] = (details.countByType[type] || 0) + 1;
879
+ if (type === "nested_memory") {
880
+ details.nestedMemoryPaths = details.nestedMemoryPaths || new Set();
881
+ addSetValue(details.nestedMemoryPaths, firstString(attachment.path, attachment.content?.path, attachment.displayPath));
882
+ } else if (type === "edited_text_file") {
883
+ details.editedTextFiles = details.editedTextFiles || new Set();
884
+ addSetValue(details.editedTextFiles, firstString(attachment.path, attachment.content?.path, attachment.displayPath));
885
+ } else if (type === "queued_command") {
886
+ details.queuedCommandCount = (details.queuedCommandCount || 0) + 1;
887
+ details.lastQueuedCommand = compactMetadata({
888
+ prompt: firstString(attachment.prompt),
889
+ commandMode: firstString(attachment.commandMode, attachment.command_mode)
890
+ });
891
+ } else if (type === "command_permissions") {
892
+ details.commandPermissionCount = (details.commandPermissionCount || 0) + 1;
893
+ details.lastAllowedTools = asStringArray(attachment.allowedTools);
894
+ } else if (type === "date_change") {
895
+ details.dateChanges = details.dateChanges || new Set();
896
+ addSetValue(details.dateChanges, attachment.newDate, attachment.new_date);
897
+ } else if (type === "max_turns_reached") {
898
+ details.maxTurnsReachedCount = (details.maxTurnsReachedCount || 0) + 1;
899
+ details.lastMaxTurnsReached = compactMetadata({
900
+ maxTurns: numericValue(attachment.maxTurns, attachment.max_turns),
901
+ turnCount: numericValue(attachment.turnCount, attachment.turn_count)
902
+ });
903
+ } else if (type === "compact_file_reference") {
904
+ details.compactFileReferences = details.compactFileReferences || new Set();
905
+ addSetValue(details.compactFileReferences, attachment.path, attachment.displayPath);
906
+ }
907
+ }
908
+
909
+ function collectClaudeToolNames(event, metadata) {
910
+ const parts = Array.isArray(event.message?.content) ? event.message.content : [];
911
+ for (const part of parts) {
912
+ if (!part || typeof part !== "object") continue;
913
+ const type = String(part.type || part.kind || "").toLowerCase();
914
+ if (type !== "tool_use" && type !== "server_tool_use" && type !== "mcp_tool_use" && type !== "function_call") continue;
915
+ addSetValue(metadata.toolNames, part.name, part.tool_name, part.toolName, part.function?.name);
916
+ }
917
+ }
918
+
919
+ function claudeMcpServerNameFromTool(name) {
920
+ const match = String(name || "").match(/^mcp__(.+?)__/);
921
+ if (!match) return "";
922
+ return match[1];
923
+ }
924
+
925
+ const CLAUDE_REMOTE_CONTROL_TOOL_NAMES = new Set([
926
+ "RemoteTrigger",
927
+ "TaskOutput",
928
+ "TaskStop",
929
+ "PushNotification",
930
+ "AskUserQuestion",
931
+ "Monitor",
932
+ "TaskCreate",
933
+ "TaskUpdate",
934
+ "TaskList",
935
+ "TaskGet",
936
+ "CronCreate",
937
+ "CronDelete",
938
+ "CronList"
939
+ ]);
940
+
941
+ function isClaudeRemoteControlToolName(name) {
942
+ const value = String(name || "").trim();
943
+ return CLAUDE_REMOTE_CONTROL_TOOL_NAMES.has(value);
944
+ }
945
+
946
+ function claudeJsonlSessionSummary(provider, context = {}) {
947
+ if (!isClaudeJsonlProvider(provider)) return null;
948
+ const metadata = context.claudeJsonl;
949
+ if (!metadata) return null;
950
+ const remoteToolNames = sortedSet(metadata.remoteToolNames);
951
+ const mcpServerNames = sortedSet(metadata.mcpServerNames);
952
+ const attachmentDetails = claudeAttachmentDetailsSummary(metadata.attachmentDetails);
953
+ const result = compactMetadata({
954
+ claudeJsonl: compactMetadata({
955
+ entrypoint: singleOrArray(metadata.entrypoints),
956
+ userType: singleOrArray(metadata.userTypes),
957
+ version: singleOrArray(metadata.versions),
958
+ gitBranch: singleOrArray(metadata.gitBranches),
959
+ permissionModes: sortedSet(metadata.permissionModes),
960
+ eventTypeCounts: compactMetadata(metadata.eventTypeCounts),
961
+ messageEventCount: metadata.messageEventCount || undefined,
962
+ sidechainMessageCount: metadata.sidechainMessageCount || undefined,
963
+ promptIdCount: metadata.promptIds.size || undefined,
964
+ requestIdCount: metadata.requestIds.size || undefined,
965
+ sourceToolAssistantUUIDCount: metadata.sourceToolAssistantUUIDs.size || undefined,
966
+ agentIds: sortedSet(metadata.agentIds),
967
+ slugs: sortedSet(metadata.slugs),
968
+ sourceToolUseIDCount: metadata.sourceToolUseIDs.size || undefined,
969
+ parentToolUseIDCount: metadata.parentToolUseIDs.size || undefined,
970
+ toolUseIDCount: metadata.toolUseIDs.size || undefined,
971
+ attributionSkills: sortedSet(metadata.attributionSkills),
972
+ mcpStructuredContentCount: metadata.mcpStructuredContentCount || undefined,
973
+ apiErrors: compactMetadata(metadata.apiErrors),
974
+ attachmentTypes: sortedSet(metadata.attachmentTypes),
975
+ attachmentDetails,
976
+ toolNames: sortedSet(metadata.toolNames),
977
+ aiTitle: sortedSet(metadata.aiTitles)[0] || undefined,
978
+ lastPrompt: metadata.lastPrompt || undefined,
979
+ queue: compactMetadata(metadata.queue),
980
+ remoteControl: metadata.remoteControl ? compactMetadata({
981
+ detected: true,
982
+ toolCount: remoteToolNames.length || undefined,
983
+ toolNames: remoteToolNames,
984
+ mcpToolCount: remoteToolNames.filter((name) => name.startsWith("mcp__")).length || undefined,
985
+ mcpServerNames
986
+ }) : undefined
987
+ })
988
+ });
989
+ return result || null;
990
+ }
991
+
992
+ function claudeAttachmentDetailsSummary(details = {}) {
993
+ return compactMetadata({
994
+ countByType: compactMetadata(details.countByType),
995
+ nestedMemoryPaths: details.nestedMemoryPaths ? sortedSet(details.nestedMemoryPaths) : undefined,
996
+ editedTextFiles: details.editedTextFiles ? sortedSet(details.editedTextFiles) : undefined,
997
+ queuedCommandCount: details.queuedCommandCount || undefined,
998
+ lastQueuedCommand: details.lastQueuedCommand || undefined,
999
+ commandPermissionCount: details.commandPermissionCount || undefined,
1000
+ lastAllowedTools: details.lastAllowedTools?.length ? details.lastAllowedTools : undefined,
1001
+ dateChanges: details.dateChanges ? sortedSet(details.dateChanges) : undefined,
1002
+ maxTurnsReachedCount: details.maxTurnsReachedCount || undefined,
1003
+ lastMaxTurnsReached: details.lastMaxTurnsReached || undefined,
1004
+ compactFileReferences: details.compactFileReferences ? sortedSet(details.compactFileReferences) : undefined
1005
+ });
1006
+ }
1007
+
1008
+ function addSetValue(set, ...values) {
1009
+ for (const value of values) {
1010
+ if (typeof value === "string" && value.trim()) set.add(value.trim());
1011
+ }
1012
+ }
1013
+
1014
+ function asStringArray(value) {
1015
+ if (!Array.isArray(value)) return [];
1016
+ return value.filter((item) => typeof item === "string" && item.trim()).map((item) => item.trim());
1017
+ }
1018
+
1019
+ function numericValue(...values) {
1020
+ for (const value of values) {
1021
+ if (value == null || value === "") continue;
1022
+ const number = Number(value);
1023
+ if (Number.isFinite(number)) return number;
1024
+ }
1025
+ return undefined;
1026
+ }
1027
+
1028
+ function sortedSet(set) {
1029
+ return [...set].sort((a, b) => a.localeCompare(b));
1030
+ }
1031
+
1032
+ function singleOrArray(set) {
1033
+ const values = sortedSet(set);
1034
+ if (!values.length) return undefined;
1035
+ return values.length === 1 ? values[0] : values;
1036
+ }
1037
+
627
1038
  function repoInfoForImport(provider, cwd, metadata = null) {
628
1039
  const repoCwd = repoCwdForImport(provider, cwd, metadata);
629
1040
  if (!repoCwd) return null;
@@ -648,24 +1059,53 @@ function claudeWorktreeParentRepo(provider, cwd) {
648
1059
  }
649
1060
 
650
1061
  function inferredJsonlSessionTitle(provider, messages) {
651
- if (!isClaudeJsonlProvider(provider)) return "";
1062
+ if (!isClaudeJsonlProvider(provider) && provider !== "codex") return "";
652
1063
  const firstUser = (messages || []).find((message) => message.role === "user" && !message.metadata?.providerGenerated);
653
1064
  return titleFromPrompt(firstUser?.content);
654
1065
  }
655
1066
 
656
1067
  function titleFromPrompt(value) {
657
- const cleaned = firstLine(value).replace(/\s+/g, " ").trim();
1068
+ const cleaned = cleanPromptTitleLine(promptTitleLine(value));
658
1069
  if (!cleaned) return "";
659
1070
  const max = 96;
660
1071
  return cleaned.length > max ? `${cleaned.slice(0, max - 1).trimEnd()}…` : cleaned;
661
1072
  }
662
1073
 
1074
+ function promptTitleLine(value) {
1075
+ const lines = String(value || "")
1076
+ .split(/\r?\n/)
1077
+ .map((line) => line.trim())
1078
+ .filter(Boolean);
1079
+ if (!lines.length) return "";
1080
+ if (isAgentlogRecallSkillLine(lines[0]) && lines.length > 1) {
1081
+ const candidate = lines.slice(1).find((line) => cleanPromptTitleLine(line) && !lowSignalPromptTitleLine(line));
1082
+ if (candidate) return candidate;
1083
+ }
1084
+ return lines[0];
1085
+ }
1086
+
1087
+ function cleanPromptTitleLine(value) {
1088
+ return String(value || "")
1089
+ .replace(/\[\$[^\]\n]+\]\([^)]+\)\s*/g, "")
1090
+ .replace(/\[([^\]\n]+)\]\([^)]+\)/g, "$1")
1091
+ .replace(/\s+/g, " ")
1092
+ .trim();
1093
+ }
1094
+
1095
+ function isAgentlogRecallSkillLine(value) {
1096
+ return /^\[\$agentlog-recall\]\([^)]+\)/i.test(String(value || "").trim());
1097
+ }
1098
+
1099
+ function lowSignalPromptTitleLine(value) {
1100
+ const cleaned = cleanPromptTitleLine(value);
1101
+ return /^#/.test(cleaned) || /^<[^>]+>/.test(cleaned) || /^['"]?\/[^'"]+['"]?$/.test(cleaned);
1102
+ }
1103
+
663
1104
  function isClaudeJsonlProvider(provider) {
664
1105
  return provider === "claude_code" || provider === "claude_sdk";
665
1106
  }
666
1107
 
667
1108
  function jsonlSessionTitleForImport(parsed, metadata = null) {
668
- if (parsed?.titleSource === "source") return parsed.title || "";
669
1109
  return firstString(metadata?.title, parsed?.title);
670
1110
  }
671
1111
 
@@ -701,6 +1141,23 @@ function claudeCodeSidecarSessionSummary(metadata = null) {
701
1141
  });
702
1142
  }
703
1143
 
1144
+ function mergeSessionSummaries(...summaries) {
1145
+ const result = {};
1146
+ for (const summary of summaries) {
1147
+ if (!summary || typeof summary !== "object" || Array.isArray(summary)) continue;
1148
+ for (const [key, value] of Object.entries(summary)) {
1149
+ if (key === "modelUsage" && Array.isArray(value)) {
1150
+ result.modelUsage = [...(Array.isArray(result.modelUsage) ? result.modelUsage : []), ...value];
1151
+ } else if (value && typeof value === "object" && !Array.isArray(value) && result[key] && typeof result[key] === "object" && !Array.isArray(result[key])) {
1152
+ result[key] = compactMetadata({ ...result[key], ...value });
1153
+ } else {
1154
+ result[key] = value;
1155
+ }
1156
+ }
1157
+ }
1158
+ return compactMetadata(result);
1159
+ }
1160
+
704
1161
  function firstExistingDirectory(...values) {
705
1162
  for (const value of values) {
706
1163
  if (typeof value !== "string" || !value.trim()) continue;
@@ -757,9 +1214,54 @@ function dedupeAdjacentMessages(messages) {
757
1214
  }
758
1215
  result.push(message);
759
1216
  }
1217
+ return dedupeDuplicateToolResults(result);
1218
+ }
1219
+
1220
+ function dedupeDuplicateToolResults(messages) {
1221
+ const result = [];
1222
+ const toolResultsByKey = new Map();
1223
+ for (const message of messages) {
1224
+ const key = duplicateToolResultKey(message);
1225
+ if (key && toolResultsByKey.has(key)) {
1226
+ const existingIndex = toolResultsByKey.get(key);
1227
+ const existing = result[existingIndex];
1228
+ if (timestampsNear(existing?.timestamp, message?.timestamp)) {
1229
+ result[existingIndex] = preferredToolResultMessage(existing, message);
1230
+ continue;
1231
+ }
1232
+ }
1233
+ if (key) toolResultsByKey.set(key, result.length);
1234
+ result.push(message);
1235
+ }
760
1236
  return result;
761
1237
  }
762
1238
 
1239
+ function duplicateToolResultKey(message) {
1240
+ const result = message?.metadata?.toolResult;
1241
+ const id = firstString(result?.id, result?.callId, result?.call_id, result?.toolCallId, result?.tool_call_id, result?.toolUseId, result?.tool_use_id);
1242
+ if (!id || message?.role !== "tool") return "";
1243
+ return `${message?.metadata?.provider || result?.provider || ""}:${id}`;
1244
+ }
1245
+
1246
+ function preferredToolResultMessage(left, right) {
1247
+ return toolResultMessageDisplayScore(right) > toolResultMessageDisplayScore(left) ? right : left;
1248
+ }
1249
+
1250
+ function toolResultMessageDisplayScore(message) {
1251
+ const result = message?.metadata?.toolResult || {};
1252
+ const rawCategory = normalizeToolToken(result.rawCategory);
1253
+ const kind = normalizeToolToken(result.kind);
1254
+ const category = normalizeToolToken(result.category);
1255
+ let score = 0;
1256
+ if (["exec_command_end", "patch_apply_end", "mcp_tool_call_end", "web_search_end", "tool_result", "tool_output"].includes(rawCategory)) score += 2000;
1257
+ if (rawCategory === "function_call_output" || rawCategory === "custom_tool_call_output") score -= 1000;
1258
+ if (category && category !== "function") score += 250;
1259
+ if (kind && !kind.startsWith("call_")) score += 150;
1260
+ if (/^\$\s/.test(String(result.summary || result.output || message?.content || ""))) score += 250;
1261
+ score += Math.min(200, String(result.output || message?.content || "").length / 200);
1262
+ return score;
1263
+ }
1264
+
763
1265
  function hasStructuredTranscriptMetadata(message) {
764
1266
  const metadata = message?.metadata || {};
765
1267
  return Boolean(
@@ -864,21 +1366,37 @@ function applyCodexTokenCount(event, provider, messages, context) {
864
1366
  if (String(payload.type || event.type || "").toLowerCase() !== "token_count") return false;
865
1367
  const usage = codexTokenUsage(payload);
866
1368
  if (!usage) return true;
867
- const previous = context.tokenUsage || { input: 0, output: 0 };
868
- const inputDelta = previous.input || previous.output ? Math.max(0, usage.input - previous.input) : usage.input;
869
- const outputDelta = previous.input || previous.output ? Math.max(0, usage.output - previous.output) : usage.output;
1369
+ const previous = context.tokenUsage || { input: 0, output: 0, cachedInput: 0, reasoningOutput: 0, total: 0 };
1370
+ const hasPrevious = codexTokenUsageHasProgress(previous);
1371
+ const inputDelta = hasPrevious ? Math.max(0, usage.input - previous.input) : usage.input;
1372
+ const outputDelta = hasPrevious ? Math.max(0, usage.output - previous.output) : usage.output;
1373
+ const cacheInputDelta = hasPrevious ? Math.max(0, usage.cachedInput - previous.cachedInput) : usage.cachedInput;
1374
+ const reasoningOutputDelta = hasPrevious ? Math.max(0, usage.reasoningOutput - previous.reasoningOutput) : usage.reasoningOutput;
1375
+ const totalDelta = usage.total
1376
+ ? (hasPrevious ? Math.max(0, usage.total - previous.total) : usage.total)
1377
+ : inputDelta + outputDelta;
870
1378
  context.tokenUsage = usage;
871
1379
  const target = [...messages].reverse().find((message) => message.role === "assistant");
872
1380
  if (!target) return true;
1381
+ const existingUsage = target.metadata?.usage && typeof target.metadata.usage === "object" ? target.metadata.usage : {};
1382
+ const freshInputDelta = Math.max(0, inputDelta - cacheInputDelta);
1383
+ const nextUsage = compactMetadata({
1384
+ ...existingUsage,
1385
+ inputTokens: addTokenNumbers(existingUsage.inputTokens, freshInputDelta),
1386
+ outputTokens: addTokenNumbers(existingUsage.outputTokens, outputDelta),
1387
+ cacheInputTokens: addTokenNumbers(existingUsage.cacheInputTokens, cacheInputDelta),
1388
+ reasoningOutputTokens: addTokenNumbers(existingUsage.reasoningOutputTokens, reasoningOutputDelta),
1389
+ reasoningOutputTokensIncludedInOutput: reasoningOutputDelta || existingUsage.reasoningOutputTokensIncludedInOutput ? true : undefined,
1390
+ totalTokens: addTokenNumbers(existingUsage.totalTokens, totalDelta || freshInputDelta + cacheInputDelta + outputDelta),
1391
+ totalInputTokens: usage.input,
1392
+ totalOutputTokens: usage.output,
1393
+ totalCacheInputTokens: usage.cachedInput || undefined,
1394
+ totalReasoningOutputTokens: usage.reasoningOutput || undefined
1395
+ });
873
1396
  target.metadata = {
874
1397
  ...(target.metadata || {}),
875
1398
  provider,
876
- usage: {
877
- inputTokens: inputDelta,
878
- outputTokens: outputDelta,
879
- totalInputTokens: usage.input,
880
- totalOutputTokens: usage.output
881
- }
1399
+ usage: nextUsage
882
1400
  };
883
1401
  return true;
884
1402
  }
@@ -894,11 +1412,50 @@ function codexTokenUsage(payload) {
894
1412
  for (const item of candidates) {
895
1413
  const input = Number(item?.input_tokens ?? item?.inputTokens ?? item?.prompt_tokens ?? item?.promptTokens);
896
1414
  const output = Number(item?.output_tokens ?? item?.outputTokens ?? item?.completion_tokens ?? item?.completionTokens);
897
- if (Number.isFinite(input) && Number.isFinite(output)) return { input, output };
1415
+ if (Number.isFinite(input) && Number.isFinite(output)) {
1416
+ const cachedInput = Number(
1417
+ item?.cached_input_tokens ??
1418
+ item?.cachedInputTokens ??
1419
+ item?.cache_read_input_tokens ??
1420
+ item?.cacheReadInputTokens ??
1421
+ item?.cacheInputTokens ??
1422
+ item?.prompt_tokens_details?.cached_tokens ??
1423
+ item?.promptTokensDetails?.cachedTokens
1424
+ );
1425
+ const reasoningOutput = Number(
1426
+ item?.reasoning_output_tokens ??
1427
+ item?.reasoningOutputTokens ??
1428
+ item?.reasoning_tokens ??
1429
+ item?.reasoningTokens ??
1430
+ item?.completion_tokens_details?.reasoning_tokens ??
1431
+ item?.completionTokensDetails?.reasoningTokens ??
1432
+ item?.output_tokens_details?.reasoning_tokens ??
1433
+ item?.outputTokensDetails?.reasoningTokens
1434
+ );
1435
+ const total = Number(item?.total_tokens ?? item?.totalTokens ?? item?.totalTokenCount ?? item?.total_token_count ?? item?.total);
1436
+ return {
1437
+ input,
1438
+ output,
1439
+ cachedInput: Number.isFinite(cachedInput) && cachedInput > 0 ? cachedInput : 0,
1440
+ reasoningOutput: Number.isFinite(reasoningOutput) && reasoningOutput > 0 ? reasoningOutput : 0,
1441
+ total: Number.isFinite(total) && total > 0 ? total : input + output
1442
+ };
1443
+ }
898
1444
  }
899
1445
  return null;
900
1446
  }
901
1447
 
1448
+ function codexTokenUsageHasProgress(usage) {
1449
+ return Boolean((usage?.input || 0) || (usage?.output || 0) || (usage?.cachedInput || 0) || (usage?.reasoningOutput || 0) || (usage?.total || 0));
1450
+ }
1451
+
1452
+ function addTokenNumbers(left, right) {
1453
+ const a = Number(left || 0);
1454
+ const b = Number(right || 0);
1455
+ const sum = (Number.isFinite(a) && a > 0 ? a : 0) + (Number.isFinite(b) && b > 0 ? b : 0);
1456
+ return sum || undefined;
1457
+ }
1458
+
902
1459
  function extractCodexSpecialMessage(event, provider, context = {}) {
903
1460
  if (provider !== "codex") return null;
904
1461
  const item = event?.payload || event?.item || event?.data || event;
@@ -1398,7 +1955,17 @@ function extractText(value, depth = 0) {
1398
1955
 
1399
1956
  function importWebChat(providerInput, file, options = {}, env = process.env) {
1400
1957
  const provider = canonicalWebProvider(providerInput);
1401
- const source = readExportBundle(file);
1958
+ reportWebImportProgress(options, provider, {
1959
+ current: 0,
1960
+ total: 0,
1961
+ message: "reading export"
1962
+ });
1963
+ const source = readExportBundle(file, provider);
1964
+ reportWebImportProgress(options, provider, {
1965
+ current: 0,
1966
+ total: source.entries.length || 0,
1967
+ message: source.entries.length ? `parsed ${source.entries.length} export files` : "parsed export"
1968
+ });
1402
1969
  const sourceAccountId = options.accountId || inferWebSourceAccountId(provider, source);
1403
1970
  let accountId = options.accountId || sourceAccountId;
1404
1971
  const existing = accountId ? getWebAccount(provider, accountId, env) : null;
@@ -1416,6 +1983,11 @@ function importWebChat(providerInput, file, options = {}, env = process.env) {
1416
1983
  displayName: options.displayName || existing?.displayName || inferredUsername,
1417
1984
  sourceAccountId
1418
1985
  }, env);
1986
+ reportWebImportProgress(options, provider, {
1987
+ current: 0,
1988
+ total: source.entries.length || 0,
1989
+ message: "normalizing conversations"
1990
+ });
1419
1991
  const normalized = normalizeWebConversations(provider, source, account);
1420
1992
  const conversations = normalized.conversations;
1421
1993
  const summary = {
@@ -1431,19 +2003,39 @@ function importWebChat(providerInput, file, options = {}, env = process.env) {
1431
2003
  };
1432
2004
  const state = loadImportState(env);
1433
2005
  const archived = archivedSessionKeys(env);
1434
- const sharedRaw = options.dryRun ? null : ensureSharedWebExportRaw(provider, source, account, env);
2006
+ reportWebImportProgress(options, provider, {
2007
+ current: 0,
2008
+ total: conversations.length,
2009
+ message: `found ${conversations.length} conversations`
2010
+ });
2011
+ const sharedRaw = options.dryRun ? null : ensureSharedWebExportRaw(provider, source, account, env, options);
1435
2012
 
1436
- for (const conversation of conversations) {
2013
+ conversations.forEach((conversation, index) => {
2014
+ const current = index + 1;
1437
2015
  if (!conversation.messages.length) {
1438
2016
  summary.skipped++;
1439
- continue;
2017
+ reportWebImportProgress(options, provider, {
2018
+ current,
2019
+ total: conversations.length,
2020
+ imported: summary.imported,
2021
+ skipped: summary.skipped,
2022
+ errors: summary.errors.length
2023
+ });
2024
+ return;
1440
2025
  }
1441
2026
  const sourceType = conversation.sourceType || webConversationSourceType(provider, conversation);
1442
2027
  const sessionId = webConversationSessionId(provider, account.accountId, conversation.id);
1443
2028
  const fingerprint = webConversationFingerprint(sourceType, account.accountId, conversation);
1444
2029
  if (alreadyImported(state, sessionId, fingerprint, archived, provider)) {
1445
2030
  summary.skipped++;
1446
- continue;
2031
+ reportWebImportProgress(options, provider, {
2032
+ current,
2033
+ total: conversations.length,
2034
+ imported: summary.imported,
2035
+ skipped: summary.skipped,
2036
+ errors: summary.errors.length
2037
+ });
2038
+ return;
1447
2039
  }
1448
2040
  const scopeCanonical = webConversationScope(provider, account.accountId, conversation.projectPath);
1449
2041
  const displayPath = webConversationDisplayPath(account.displayName, conversation.projectPath);
@@ -1489,12 +2081,36 @@ function importWebChat(providerInput, file, options = {}, env = process.env) {
1489
2081
  archived.add(archiveSessionKey(provider, sessionId));
1490
2082
  }
1491
2083
  summary.imported++;
1492
- }
2084
+ reportWebImportProgress(options, provider, {
2085
+ current,
2086
+ total: conversations.length,
2087
+ imported: summary.imported,
2088
+ skipped: summary.skipped,
2089
+ errors: summary.errors.length
2090
+ });
2091
+ });
1493
2092
 
1494
2093
  if (!options.dryRun) saveImportState(state, env);
2094
+ reportWebImportProgress(options, provider, {
2095
+ current: conversations.length,
2096
+ total: conversations.length,
2097
+ imported: summary.imported,
2098
+ skipped: summary.skipped,
2099
+ errors: summary.errors.length,
2100
+ message: `${options.dryRun ? "dry run complete" : "import complete"}: imported=${summary.imported} skipped=${summary.skipped}`
2101
+ });
1495
2102
  return summary;
1496
2103
  }
1497
2104
 
2105
+ function reportWebImportProgress(options, provider, event) {
2106
+ if (typeof options.onProgress !== "function") return;
2107
+ options.onProgress({
2108
+ kind: "import",
2109
+ provider: providerLabelForWeb(provider),
2110
+ ...event
2111
+ });
2112
+ }
2113
+
1498
2114
  function importWindsurfTrajectoryExport(target, options = {}, env = process.env) {
1499
2115
  const since = parseSince(options.since || "all");
1500
2116
  return importStructuredProvider(
@@ -1514,7 +2130,8 @@ function readExportJson(file) {
1514
2130
  return bundle.entries.map((entry) => entry.data);
1515
2131
  }
1516
2132
 
1517
- function readExportBundle(file) {
2133
+ function readExportBundle(file, provider = "") {
2134
+ if (Array.isArray(file)) return readExportBundleList(file, provider);
1518
2135
  const resolved = path.resolve(file);
1519
2136
  let stat;
1520
2137
  try {
@@ -1522,31 +2139,96 @@ function readExportBundle(file) {
1522
2139
  } catch (error) {
1523
2140
  throw exportAccessError(resolved, error);
1524
2141
  }
1525
- const entries = stat.isDirectory() ? readExportFolder(resolved) : readExportFile(resolved);
2142
+ const bundle = stat.isDirectory()
2143
+ ? readExportFolder(resolved, provider)
2144
+ : { entries: readExportFile(resolved, provider), rawFiles: [resolved] };
2145
+ const entries = bundle.entries;
1526
2146
  if (!entries.length) {
1527
- throw new Error(`${file} did not contain readable JSON, JSONL, or NDJSON export files. If this is an external path such as Downloads, Desktop, Documents, iCloud Drive, or an attached volume, make sure the terminal app running agentlog has permission to read it, or import the original .zip file.`);
2147
+ throw new Error(`${compactExportPath(file)} did not contain readable JSON, JSONL, NDJSON, or recognized nested export ZIP files. If this is an external path such as Downloads, Desktop, Documents, iCloud Drive, or an attached volume, make sure the terminal app running agentlog has permission to read it, unzip the relevant conversation part ZIPs, or copy the export into a readable folder and rerun.`);
1528
2148
  }
1529
2149
  return {
1530
2150
  root: resolved,
1531
2151
  kind: stat.isDirectory() ? "folder" : path.extname(resolved).toLowerCase() === ".zip" ? "zip" : "json",
1532
2152
  entries,
2153
+ rawFiles: bundle.rawFiles || [],
2154
+ fingerprint: hashId(entries.map((entry) => `${entry.name}:${entry.sha256}`).sort().join("\n"))
2155
+ };
2156
+ }
2157
+
2158
+ function readExportBundleList(files, provider = "") {
2159
+ const resolved = files.map((file) => path.resolve(file)).filter(Boolean);
2160
+ if (!resolved.length) throw new Error("no export paths provided");
2161
+ const bundles = resolved.map((file) => readExportBundle(file, provider));
2162
+ const entries = [];
2163
+ const rawFiles = [];
2164
+ for (const bundle of bundles) {
2165
+ const prefix = exportBundleMergePrefix(bundle.root, resolved);
2166
+ for (const entry of bundle.entries || []) entries.push(prefixExportEntry(entry, prefix));
2167
+ for (const rawFile of bundle.rawFiles || []) rawFiles.push(prefixExportRawFile(rawFile, prefix));
2168
+ }
2169
+ return {
2170
+ root: resolved.join(path.delimiter),
2171
+ kind: "multi",
2172
+ entries: entries.sort((a, b) => a.name.localeCompare(b.name)),
2173
+ rawFiles: rawFiles.sort((a, b) => a.name.localeCompare(b.name)),
1533
2174
  fingerprint: hashId(entries.map((entry) => `${entry.name}:${entry.sha256}`).sort().join("\n"))
1534
2175
  };
1535
2176
  }
1536
2177
 
1537
- function readExportFolder(root) {
2178
+ function exportBundleMergePrefix(root, roots) {
2179
+ if (roots.length <= 1) return "";
2180
+ const base = safeArchiveRelativePath(path.basename(root)) || hashId(root);
2181
+ return `${base}/`;
2182
+ }
2183
+
2184
+ function prefixExportEntry(entry, prefix) {
2185
+ if (!prefix) return entry;
2186
+ return {
2187
+ ...entry,
2188
+ name: `${prefix}${entry.name}`
2189
+ };
2190
+ }
2191
+
2192
+ function prefixExportRawFile(rawFile, prefix) {
2193
+ if (!prefix) return rawFile;
2194
+ return {
2195
+ ...rawFile,
2196
+ name: `${prefix}${rawFile.name}`
2197
+ };
2198
+ }
2199
+
2200
+ function readExportFolder(root, provider = "") {
1538
2201
  const entries = [];
2202
+ const rawFiles = [];
1539
2203
  collectExportFiles(root, (file) => {
1540
- if (!isExportDataFile(file)) return;
2204
+ const relativeName = path.relative(root, file).split(path.sep).join("/");
2205
+ if (!ignoredExportRawFile(relativeName)) rawFiles.push(exportRawFile(root, file, relativeName));
2206
+ if (nestedExportZipEntryName(relativeName, provider)) {
2207
+ const zipPrefix = `${relativeName.replace(/\.zip$/i, "")}/`;
2208
+ entries.push(...readZipExportEntries(file, provider, {
2209
+ entryPrefix: zipPrefix,
2210
+ sourcePrefix: `${file}#`
2211
+ }));
2212
+ return;
2213
+ }
2214
+ const namedJson = isExportEntryName(file);
2215
+ if (!namedJson && !looksLikeJsonPayload(file)) return;
1541
2216
  let text;
1542
2217
  try {
1543
2218
  text = readExportText(file);
1544
2219
  } catch (error) {
1545
2220
  throw exportAccessError(root, error, file);
1546
2221
  }
1547
- entries.push(exportEntry(path.relative(root, file).split(path.sep).join("/"), text, file));
2222
+ try {
2223
+ entries.push(exportEntry(relativeName, text, file));
2224
+ } catch (error) {
2225
+ if (namedJson) throw error;
2226
+ }
1548
2227
  });
1549
- return entries.sort((a, b) => a.name.localeCompare(b.name));
2228
+ return {
2229
+ entries: entries.sort((a, b) => a.name.localeCompare(b.name)),
2230
+ rawFiles: rawFiles.sort((a, b) => a.name.localeCompare(b.name))
2231
+ };
1550
2232
  }
1551
2233
 
1552
2234
  function collectExportFiles(root, visit) {
@@ -1568,26 +2250,88 @@ function collectExportFiles(root, visit) {
1568
2250
 
1569
2251
  function exportAccessError(root, error, target = root) {
1570
2252
  const code = error?.code ? ` (${error.code})` : "";
1571
- const message = error?.message || String(error || "unknown error");
2253
+ const message = compactExportPath(error?.message || String(error || "unknown error"));
1572
2254
  const hint = "Grant Full Disk Access to Terminal/iTerm or the Node process running agentlog, import the original .zip file, or copy the export into a readable folder and rerun.";
1573
- const wrapped = new Error(`Cannot read web export path ${target}${code}: ${message}. ${hint}`);
2255
+ const wrapped = new Error(`Cannot read web export path ${compactExportPath(target)}${code}: ${message}. ${hint}`);
1574
2256
  wrapped.code = "AGENTLOG_WEB_EXPORT_ACCESS";
1575
2257
  wrapped.causeCode = error?.code || "";
1576
2258
  return wrapped;
1577
2259
  }
1578
2260
 
1579
- function readExportFile(file) {
2261
+ function compactExportPath(value) {
2262
+ let text = String(value || "");
2263
+ const home = os.homedir();
2264
+ if (home) {
2265
+ const escaped = home.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2266
+ text = text.replace(new RegExp(escaped, "g"), "~");
2267
+ }
2268
+ return text.replace(/\/Users\/[^/\s'"]+/g, "~");
2269
+ }
2270
+
2271
+ function readExportFile(file, provider = "") {
1580
2272
  if (path.extname(file).toLowerCase() !== ".zip") {
1581
2273
  return [exportEntry(path.basename(file), readExportText(file), file)];
1582
2274
  }
2275
+ return readZipExportEntries(file, provider);
2276
+ }
2277
+
2278
+ function readZipExportEntries(file, provider = "", context = {}) {
1583
2279
  const list = spawnSync("unzip", ["-Z1", file], { encoding: "utf8" });
1584
2280
  if (list.status !== 0) throw new Error("reading zip exports requires the `unzip` command");
1585
- const names = list.stdout.split(/\r?\n/).filter(isExportEntryName);
1586
- return names.map((name) => {
1587
- const content = spawnSync("unzip", ["-p", file, name], { encoding: "utf8", maxBuffer: 1024 * 1024 * 200 });
1588
- if (content.status !== 0) throw new Error(`failed to read ${name} from zip`);
1589
- return exportEntry(name, content.stdout, `${file}#${name}`);
1590
- }).sort((a, b) => a.name.localeCompare(b.name));
2281
+ const names = list.stdout.split(/\r?\n/).filter(Boolean);
2282
+ const entryPrefix = context.entryPrefix || "";
2283
+ const sourcePrefix = context.sourcePrefix || `${file}#`;
2284
+ const depth = context.depth || 0;
2285
+ const entries = [];
2286
+ for (const name of names.filter(isExportEntryName)) {
2287
+ const content = readZipEntryText(file, name);
2288
+ entries.push(exportEntry(`${entryPrefix}${name}`, content, `${sourcePrefix}${name}`));
2289
+ }
2290
+ if (depth < 2) {
2291
+ for (const name of names.filter((entry) => nestedExportZipEntryName(entry, provider))) {
2292
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "agentlog-nested-export-"));
2293
+ const tempZip = path.join(tempDir, safeArchiveRelativePath(path.basename(name)) || "nested.zip");
2294
+ try {
2295
+ extractZipEntryToFile(file, name, tempZip);
2296
+ const nestedPrefix = `${entryPrefix}${name.replace(/\.zip$/i, "")}/`;
2297
+ entries.push(...readZipExportEntries(tempZip, provider, {
2298
+ entryPrefix: nestedPrefix,
2299
+ sourcePrefix: `${sourcePrefix}${name}#`,
2300
+ depth: depth + 1
2301
+ }));
2302
+ } finally {
2303
+ fs.rmSync(tempDir, { recursive: true, force: true });
2304
+ }
2305
+ }
2306
+ }
2307
+ return entries.sort((a, b) => a.name.localeCompare(b.name));
2308
+ }
2309
+
2310
+ function readZipEntryText(file, name) {
2311
+ const content = spawnSync("unzip", ["-p", file, name], { encoding: "utf8", maxBuffer: EXPORT_ZIP_ENTRY_MAX_BUFFER });
2312
+ if (content.status !== 0) throw new Error(`failed to read ${name} from zip`);
2313
+ return content.stdout;
2314
+ }
2315
+
2316
+ function extractZipEntryToFile(file, name, target) {
2317
+ ensureDir(path.dirname(target));
2318
+ const fd = fs.openSync(target, "w", 0o600);
2319
+ try {
2320
+ const result = spawnSync("unzip", ["-p", file, name], { stdio: ["ignore", fd, "pipe"] });
2321
+ if (result.status !== 0) {
2322
+ const message = result.stderr ? result.stderr.toString("utf8").trim() : "";
2323
+ throw new Error(`failed to extract ${name} from zip${message ? `: ${message}` : ""}`);
2324
+ }
2325
+ } finally {
2326
+ fs.closeSync(fd);
2327
+ }
2328
+ }
2329
+
2330
+ function nestedExportZipEntryName(name, provider = "") {
2331
+ const text = String(name || "");
2332
+ if (!/\.zip$/i.test(text) || ignoredExportRawFile(text)) return false;
2333
+ if (provider === "chatgpt") return openAiConversationExportPath(text);
2334
+ return /(^|\/)Conversations__[^/]*\.zip$/i.test(text);
1591
2335
  }
1592
2336
 
1593
2337
  function exportEntry(name, text, sourcePath) {
@@ -1603,6 +2347,16 @@ function exportEntry(name, text, sourcePath) {
1603
2347
  };
1604
2348
  }
1605
2349
 
2350
+ function exportRawFile(root, file, name = path.relative(root, file).split(path.sep).join("/")) {
2351
+ const stat = safeStat(file);
2352
+ return {
2353
+ name,
2354
+ sourcePath: file,
2355
+ size: stat?.size || 0,
2356
+ mtime: stat?.mtimeMs ? new Date(stat.mtimeMs).toISOString() : ""
2357
+ };
2358
+ }
2359
+
1606
2360
  function readExportText(file) {
1607
2361
  const buffer = fs.readFileSync(file);
1608
2362
  const bytes = file.toLowerCase().endsWith(".gz") ? zlib.gunzipSync(buffer) : buffer;
@@ -1633,11 +2387,6 @@ function parseJsonLines(text, name = "", options = {}) {
1633
2387
  return rows;
1634
2388
  }
1635
2389
 
1636
- function isExportDataFile(file) {
1637
- if (isExportEntryName(file)) return true;
1638
- return looksLikeJsonPayload(file);
1639
- }
1640
-
1641
2390
  function isExportEntryName(name) {
1642
2391
  const lower = String(name || "").toLowerCase();
1643
2392
  return /\.(json|jsonl|ndjson)(\.gz)?$/.test(lower);
@@ -1665,6 +2414,12 @@ function looksLikeJsonPayload(file) {
1665
2414
  }
1666
2415
  }
1667
2416
 
2417
+ function ignoredExportRawFile(name) {
2418
+ return String(name || "")
2419
+ .split(/[\\/]+/)
2420
+ .some((part) => part === ".DS_Store" || part === "__MACOSX" || part.startsWith("._"));
2421
+ }
2422
+
1668
2423
  function normalizeWebConversations(provider, source, account) {
1669
2424
  if (provider === "chatgpt") return { conversations: normalizeChatGptExport(source, account) };
1670
2425
  return { conversations: normalizeClaudeWebExport(source, account) };
@@ -1675,7 +2430,7 @@ function normalizeChatGptExport(source) {
1675
2430
  return entries.flatMap((entry) => chatgptRawConversations(entry.data).map((conversation, index) => {
1676
2431
  const id = firstString(conversation.id, conversation.uuid, conversation.conversation_id) || `chatgpt-${hashId(`${entry.name}:${index}`)}`;
1677
2432
  const title = firstString(conversation.title, conversation.name, conversation.summary) || "ChatGPT conversation";
1678
- const messages = chatgptMessages(conversation).filter((message) => message.content.trim());
2433
+ const messages = chatgptMessages(conversation).filter(chatgptMessageHasDisplayContent);
1679
2434
  const sorted = sortConversationMessages(messages);
1680
2435
  return {
1681
2436
  id,
@@ -1687,34 +2442,41 @@ function normalizeChatGptExport(source) {
1687
2442
  projectPath: "",
1688
2443
  entryPath: entry.name,
1689
2444
  sourceType: "chatgpt-export",
1690
- kind: "conversation"
2445
+ kind: "conversation",
2446
+ sessionSummary: chatgptSessionSummary(conversation)
1691
2447
  };
1692
2448
  }));
1693
2449
  }
1694
2450
 
1695
2451
  function chatConversationEntries(source) {
1696
- const preferred = source.entries.filter((entry) => /(^|\/)conversations\.json$/i.test(entry.name));
1697
- return preferred.length ? preferred : source.entries.filter((entry) => Array.isArray(entry.data) || Array.isArray(entry.data?.conversations));
2452
+ const preferred = source.entries.filter((entry) => /(^|\/)conversations(?:-\d+)?\.json$/i.test(entry.name));
2453
+ return preferred.length ? preferred : source.entries.filter((entry) => chatgptRawConversations(entry.data).length);
1698
2454
  }
1699
2455
 
1700
2456
  function chatgptRawConversations(data) {
1701
- if (Array.isArray(data)) return data;
1702
- if (Array.isArray(data?.conversations)) return data.conversations;
2457
+ if (Array.isArray(data)) return data.filter(chatgptLooksLikeConversation);
2458
+ if (Array.isArray(data?.conversations)) return data.conversations.filter(chatgptLooksLikeConversation);
1703
2459
  if (data?.mapping || Array.isArray(data?.messages)) return [data];
1704
2460
  return [];
1705
2461
  }
1706
2462
 
2463
+ function chatgptLooksLikeConversation(value) {
2464
+ return Boolean(value && typeof value === "object" && (value.mapping || Array.isArray(value.messages) || value.conversation_id || value.current_node));
2465
+ }
2466
+
1707
2467
  function chatgptMessages(conversation) {
1708
2468
  if (conversation.mapping && typeof conversation.mapping === "object") {
1709
2469
  const nodes = chatgptMainPathNodes(conversation);
1710
2470
  return nodes.map((node) => node && node.message).filter(Boolean).map((message) => {
1711
2471
  const role = normalizeEventRole(message.author?.role) || "unknown";
1712
- const content = extractChatGptContent(message.content);
2472
+ const content = extractChatGptContent(message);
2473
+ const toolCall = chatgptToolCallFromMessage(message, content);
2474
+ const toolResult = role === "tool" ? chatgptToolResultFromMessage(message, content) : null;
1713
2475
  return {
1714
2476
  role,
1715
- content,
2477
+ content: toolCall ? "" : content,
1716
2478
  timestamp: toIso(message.create_time || message.update_time),
1717
- metadata: chatgptMessageMetadata(message, role, content)
2479
+ metadata: chatgptMessageMetadata(message, role, content, { toolCall, toolResult })
1718
2480
  };
1719
2481
  });
1720
2482
  }
@@ -1738,22 +2500,266 @@ function chatgptMainPathNodes(conversation) {
1738
2500
  );
1739
2501
  }
1740
2502
 
1741
- function extractChatGptContent(content) {
1742
- if (!content) return "";
1743
- if (Array.isArray(content.parts)) return extractText(content.parts);
1744
- if (Array.isArray(content.text)) return extractText(content.text);
1745
- if (typeof content.text === "string") return content.text;
1746
- return extractText(content);
2503
+ function extractChatGptContent(message) {
2504
+ const content = message?.content || {};
2505
+ const parts = [];
2506
+ if (Array.isArray(content.parts)) {
2507
+ for (const part of content.parts) {
2508
+ const text = chatgptContentPartText(part);
2509
+ if (text) parts.push(text);
2510
+ }
2511
+ } else if (Array.isArray(content.text)) {
2512
+ parts.push(extractText(content.text));
2513
+ } else if (typeof content.text === "string") {
2514
+ parts.push(content.text);
2515
+ } else {
2516
+ parts.push(extractText(content));
2517
+ }
2518
+ return parts.filter((part) => String(part || "").trim()).join("\n").trim();
2519
+ }
2520
+
2521
+ function chatgptContentPartText(part) {
2522
+ if (typeof part === "string") return part;
2523
+ if (!part || typeof part !== "object") return extractText(part);
2524
+ const text = extractText(part);
2525
+ if (text) return text;
2526
+ const contentType = firstString(part.content_type, part.type, "asset");
2527
+ const pointer = firstString(part.asset_pointer, part.assetPointer, part.file_id, part.fileId);
2528
+ if (pointer || /image|file|asset/i.test(contentType)) {
2529
+ return "";
2530
+ }
2531
+ return "";
1747
2532
  }
1748
2533
 
1749
- function chatgptMessageMetadata(message, role, content) {
2534
+ function chatgptMessageMetadata(message, role, content, options = {}) {
2535
+ const assetPointers = chatgptAssetPointers(message);
2536
+ const attachments = chatgptNormalizedAttachments(message);
1750
2537
  return webWithUsage({
1751
2538
  source: "chatgpt-export",
1752
2539
  messageId: message.id || undefined,
1753
- model: firstString(message.metadata?.model_slug, message.metadata?.model, message.metadata?.default_model_slug) || undefined
2540
+ model: firstString(message.metadata?.model_slug, message.metadata?.model, message.metadata?.default_model_slug) || undefined,
2541
+ contentType: firstString(message.content?.content_type) || undefined,
2542
+ channel: firstString(message.channel) || undefined,
2543
+ recipient: firstString(message.recipient) || undefined,
2544
+ status: firstString(message.status) || undefined,
2545
+ attachments: chatgptAttachAssetPointers(attachments, assetPointers),
2546
+ assetPointers,
2547
+ toolCalls: options.toolCall ? [options.toolCall] : undefined,
2548
+ toolResult: options.toolResult || undefined
1754
2549
  }, webMessageUsage(message, role, { inputText: content, outputText: content }));
1755
2550
  }
1756
2551
 
2552
+ function chatgptSessionSummary(conversation) {
2553
+ const summary = firstString(conversation.summary);
2554
+ if (!summary) return undefined;
2555
+ return {
2556
+ summary,
2557
+ source: "chatgpt-export",
2558
+ summaryKind: "conversation_summary"
2559
+ };
2560
+ }
2561
+
2562
+ function chatgptMessageHasDisplayContent(message) {
2563
+ if (!message || typeof message !== "object") return false;
2564
+ if (String(message.content || "").trim()) return true;
2565
+ const metadata = message.metadata || {};
2566
+ return Boolean(
2567
+ (Array.isArray(metadata.attachments) && metadata.attachments.length) ||
2568
+ (Array.isArray(metadata.assetPointers) && metadata.assetPointers.length) ||
2569
+ (Array.isArray(metadata.toolCalls) && metadata.toolCalls.length) ||
2570
+ metadata.toolResult
2571
+ );
2572
+ }
2573
+
2574
+ function chatgptToolCallFromMessage(message, content) {
2575
+ const recipient = firstString(message?.recipient);
2576
+ if (!recipient || recipient === "all" || recipient === "assistant") return null;
2577
+ const rawInput = String(content || "").trim();
2578
+ if (!rawInput) return null;
2579
+ const args = parseToolArgsValue(rawInput);
2580
+ const name = chatgptToolName(recipient, args);
2581
+ const summary = summarizeToolArguments(args) || rawInput.slice(0, 240);
2582
+ return compactObject({
2583
+ provider: "chatgpt",
2584
+ name,
2585
+ displayName: toolDisplayName(name),
2586
+ category: chatgptToolCategory(recipient, name),
2587
+ rawCategory: "chatgpt_tool_call",
2588
+ title: toolDisplayName(name),
2589
+ status: "tool_call",
2590
+ argument: summary,
2591
+ rawInputSummary: summary,
2592
+ inputPreview: chatgptToolInputPreview(args, rawInput),
2593
+ arguments: args && typeof args === "object" && !Array.isArray(args) ? args : undefined
2594
+ });
2595
+ }
2596
+
2597
+ function chatgptToolName(recipient, args) {
2598
+ const base = String(recipient || "tool").trim();
2599
+ if (base === "web.run" && args && typeof args === "object" && !Array.isArray(args)) {
2600
+ if (args.search_query || args.searchQuery) return "web.search";
2601
+ if (args.open) return "web.open";
2602
+ if (args.image_query || args.imageQuery) return "web.image_search";
2603
+ if (args.finance) return "web.finance";
2604
+ if (args.weather) return "web.weather";
2605
+ if (args.sports) return "web.sports";
2606
+ if (args.time) return "web.time";
2607
+ }
2608
+ return base || "tool";
2609
+ }
2610
+
2611
+ function chatgptToolCategory(recipient, name) {
2612
+ const text = `${recipient || ""} ${name || ""}`.toLowerCase();
2613
+ if (text.includes("web.")) return "web";
2614
+ return "";
2615
+ }
2616
+
2617
+ function chatgptToolInputPreview(args, rawInput) {
2618
+ if (!args || typeof args !== "object") return rawInput.slice(0, 2000);
2619
+ try {
2620
+ return JSON.stringify(args, null, 2).slice(0, 2000);
2621
+ } catch {
2622
+ return rawInput.slice(0, 2000);
2623
+ }
2624
+ }
2625
+
2626
+ function chatgptToolResultFromMessage(message, content) {
2627
+ const text = String(content || "").trim();
2628
+ if (!text) return null;
2629
+ const parsedFile = chatgptParsedFileToolResult(text);
2630
+ if (parsedFile) return parsedFile;
2631
+ if (/^All the files uploaded by the user have been fully loaded\./i.test(text)) {
2632
+ return compactObject({
2633
+ provider: "chatgpt",
2634
+ kind: "Uploaded files loaded",
2635
+ title: "Uploaded files loaded",
2636
+ category: "read",
2637
+ categoryLabel: "Files",
2638
+ rawCategory: "chatgpt_file_tool",
2639
+ summary: firstLine(text),
2640
+ output: text,
2641
+ lineCount: text.split("\n").length,
2642
+ collapsed: false,
2643
+ status: "completed"
2644
+ });
2645
+ }
2646
+ return null;
2647
+ }
2648
+
2649
+ function chatgptParsedFileToolResult(text) {
2650
+ if (!/<PARSED TEXT FOR PAGE:\s*\d+\s*\/\s*\d+>/i.test(text)) return null;
2651
+ const pageMatches = [...text.matchAll(/<PARSED TEXT FOR PAGE:\s*(\d+)\s*\/\s*(\d+)>/gi)];
2652
+ const pageCount = pageMatches.reduce((max, match) => Math.max(max, Number(match[2]) || 0), 0);
2653
+ const citationMatch = text.match(/\uE200filecite\uE202([^\uE201]+)\uE201/i);
2654
+ const citation = citationMatch ? chatgptCitationText(citationMatch[1]) : "";
2655
+ const detail = [citation, pageCount ? `${pageCount} page${pageCount === 1 ? "" : "s"}` : ""].filter(Boolean).join(" · ");
2656
+ return compactObject({
2657
+ provider: "chatgpt",
2658
+ kind: "Parsed uploaded file",
2659
+ title: "Parsed uploaded file",
2660
+ category: "read",
2661
+ categoryLabel: "Files",
2662
+ rawCategory: "chatgpt_file_tool",
2663
+ summary: detail ? `Parsed uploaded file text · ${detail}` : "Parsed uploaded file text",
2664
+ output: text,
2665
+ lineCount: text.split("\n").length,
2666
+ collapsed: true,
2667
+ status: "completed"
2668
+ });
2669
+ }
2670
+
2671
+ function chatgptCitationText(value) {
2672
+ return String(value || "").split("\uE202").map((part) => part.trim()).filter(Boolean).join(" ");
2673
+ }
2674
+
2675
+ function chatgptNormalizedAttachments(message) {
2676
+ const attachments = Array.isArray(message?.metadata?.attachments) ? message.metadata.attachments : [];
2677
+ const normalized = attachments.map((attachment) => {
2678
+ if (!attachment || typeof attachment !== "object") return null;
2679
+ return compactObject({
2680
+ id: firstString(attachment.id, attachment.file_id, attachment.fileId),
2681
+ name: firstString(attachment.name, attachment.filename, attachment.file_name),
2682
+ mimeType: firstString(attachment.mime_type, attachment.mimeType),
2683
+ size: finiteNumber(attachment.size, attachment.size_bytes, attachment.sizeBytes),
2684
+ width: finiteNumber(attachment.width),
2685
+ height: finiteNumber(attachment.height),
2686
+ source: firstString(attachment.source),
2687
+ libraryFileId: firstString(attachment.library_file_id, attachment.libraryFileId)
2688
+ });
2689
+ }).filter((attachment) => attachment && Object.keys(attachment).length);
2690
+ return normalized.length ? normalized : undefined;
2691
+ }
2692
+
2693
+ function chatgptAssetPointers(message) {
2694
+ const parts = Array.isArray(message?.content?.parts) ? message.content.parts : [];
2695
+ const pointers = parts
2696
+ .filter((part) => part && typeof part === "object" && (part.asset_pointer || part.assetPointer))
2697
+ .map((part) => compactObject({
2698
+ assetPointer: firstString(part.asset_pointer, part.assetPointer),
2699
+ contentType: firstString(part.content_type, part.type),
2700
+ mimeType: firstString(part.mime_type, part.mimeType),
2701
+ width: finiteNumber(part.width),
2702
+ height: finiteNumber(part.height),
2703
+ size: finiteNumber(part.size_bytes, part.sizeBytes)
2704
+ }))
2705
+ .filter((pointer) => Object.keys(pointer).length);
2706
+ return pointers.length ? pointers : undefined;
2707
+ }
2708
+
2709
+ function chatgptAttachAssetPointers(attachments, assetPointers) {
2710
+ if (!Array.isArray(attachments) || !attachments.length) return undefined;
2711
+ if (!Array.isArray(assetPointers) || !assetPointers.length) return attachments;
2712
+ return attachments.map((attachment, index) => {
2713
+ const pointer = chatgptAssetPointerForAttachment(attachment, assetPointers, index, attachments.length);
2714
+ if (!pointer) return attachment;
2715
+ return compactObject({
2716
+ ...attachment,
2717
+ assetPointer: pointer.assetPointer,
2718
+ pointerContentType: pointer.contentType,
2719
+ pointerMimeType: pointer.mimeType
2720
+ });
2721
+ });
2722
+ }
2723
+
2724
+ function chatgptAssetPointerForAttachment(attachment, assetPointers, index, attachmentCount) {
2725
+ const id = normalizeAttachmentToken(attachment?.id || "");
2726
+ if (id) {
2727
+ const match = assetPointers.find((pointer) => normalizeAttachmentToken(pointer.assetPointer || "").endsWith(id));
2728
+ if (match) return match;
2729
+ }
2730
+ if (attachmentCount === assetPointers.length) return assetPointers[index] || null;
2731
+ if (attachmentCount === 1 && assetPointers.length === 1) return assetPointers[0];
2732
+ return null;
2733
+ }
2734
+
2735
+ function normalizeAttachmentToken(value) {
2736
+ return String(value || "")
2737
+ .trim()
2738
+ .toLowerCase()
2739
+ .replace(/^file-service:\/\//, "")
2740
+ .replace(/^sandbox:\/\//, "")
2741
+ .replace(/^attachment:\/\//, "")
2742
+ .replace(/[?#].*$/, "")
2743
+ .split(/[\\/]+/)
2744
+ .filter(Boolean)
2745
+ .pop() || "";
2746
+ }
2747
+
2748
+ function compactObject(input) {
2749
+ const output = {};
2750
+ for (const [key, value] of Object.entries(input || {})) {
2751
+ if (value === undefined || value === null || value === "") continue;
2752
+ if (Array.isArray(value) && !value.length) continue;
2753
+ output[key] = value;
2754
+ }
2755
+ return output;
2756
+ }
2757
+
2758
+ function finiteNumber(...values) {
2759
+ const value = numericValue(...values);
2760
+ return Number.isFinite(value) ? value : undefined;
2761
+ }
2762
+
1757
2763
  function webWithUsage(metadata, usage) {
1758
2764
  if (!usage) return metadata;
1759
2765
  return { ...metadata, usage };
@@ -2345,12 +3351,19 @@ function webConversationSourcePath(source, conversation) {
2345
3351
  }
2346
3352
 
2347
3353
  function inferWebSourceAccountId(provider, source) {
3354
+ const accountMetadataName = provider === "chatgpt"
3355
+ ? /(user|account|profile)/
3356
+ : /(user|account|profile|organization|export|memories)/;
2348
3357
  for (const entry of source.entries) {
2349
3358
  const name = entry.name.toLowerCase();
2350
- if (!/(user|account|profile|organization|export|memories)/.test(name)) continue;
3359
+ if (!accountMetadataName.test(name) || /(^|\/)conversations(?:-\d+)?\.json$/i.test(name)) continue;
2351
3360
  const id = webAccountIdFromData(entry.data);
2352
3361
  if (id) return id;
2353
3362
  }
3363
+ if (provider === "chatgpt") {
3364
+ const id = openAiConversationExportAccountId(source);
3365
+ if (id) return id;
3366
+ }
2354
3367
  if (provider === "claude_web") {
2355
3368
  const match = path.basename(source.root).match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i);
2356
3369
  if (match) return match[0].toLowerCase();
@@ -2358,6 +3371,19 @@ function inferWebSourceAccountId(provider, source) {
2358
3371
  return "";
2359
3372
  }
2360
3373
 
3374
+ function openAiConversationExportAccountId(source) {
3375
+ const values = [
3376
+ source.root,
3377
+ ...(Array.isArray(source.entries) ? source.entries.map((entry) => entry.name) : []),
3378
+ ...(Array.isArray(source.rawFiles) ? source.rawFiles.map((file) => file.name) : [])
3379
+ ];
3380
+ for (const value of values) {
3381
+ const match = String(value || "").match(/Conversations__([A-Za-z0-9]+)-chatgpt-\d+/i);
3382
+ if (match) return match[1];
3383
+ }
3384
+ return "";
3385
+ }
3386
+
2361
3387
  function inferWebUsername(provider, source) {
2362
3388
  for (const entry of source.entries) {
2363
3389
  const name = entry.name.toLowerCase();
@@ -2402,10 +3428,15 @@ function providerLabelForWeb(provider) {
2402
3428
  return provider === "chatgpt" ? "ChatGPT" : "Claude.ai";
2403
3429
  }
2404
3430
 
2405
- function ensureSharedWebExportRaw(provider, source, account, env = process.env) {
3431
+ function ensureSharedWebExportRaw(provider, source, account, env = process.env, options = {}) {
2406
3432
  const root = path.join(archiveRoot(env), "raw", "web-exports", provider, account.accountId, source.fingerprint);
2407
3433
  const manifestPath = path.join(root, "manifest.json");
2408
3434
  if (fs.existsSync(manifestPath)) {
3435
+ reportWebImportProgress(options, provider, {
3436
+ current: 1,
3437
+ total: 1,
3438
+ message: "raw export already preserved"
3439
+ });
2409
3440
  return { root, manifestPath, sha256: source.fingerprint };
2410
3441
  }
2411
3442
  ensureDir(root);
@@ -2413,6 +3444,11 @@ function ensureSharedWebExportRaw(provider, source, account, env = process.env)
2413
3444
  const records = [];
2414
3445
  let containerPath = "";
2415
3446
  if (source.kind === "zip" || source.kind === "json") {
3447
+ reportWebImportProgress(options, provider, {
3448
+ current: 0,
3449
+ total: 1,
3450
+ message: "preserving raw export"
3451
+ });
2416
3452
  const extension = source.kind === "zip" ? ".zip" : path.extname(source.root) || ".json";
2417
3453
  containerPath = path.join(root, `source${extension}`);
2418
3454
  fs.copyFileSync(source.root, containerPath);
@@ -2423,22 +3459,55 @@ function ensureSharedWebExportRaw(provider, source, account, env = process.env)
2423
3459
  size: stat?.size || 0,
2424
3460
  sha256: fileSha256(containerPath)
2425
3461
  });
3462
+ reportWebImportProgress(options, provider, {
3463
+ current: 1,
3464
+ total: 1,
3465
+ message: "preserved raw export"
3466
+ });
2426
3467
  }
2427
- for (const entry of source.entries) {
2428
- let archivedPath = containerPath;
2429
- if (source.kind === "folder" && fs.existsSync(entry.sourcePath)) {
2430
- archivedPath = path.join(root, "entries", safeArchiveRelativePath(entry.name));
3468
+ const rawFiles = webExportRawFilesForProvider(source, provider);
3469
+ const entryBySourcePath = new Map(source.entries.map((entry) => [path.resolve(entry.sourcePath), entry]));
3470
+ const copiedRaw = new Set();
3471
+ if (source.kind === "folder" || source.kind === "multi") {
3472
+ let rawIndex = 0;
3473
+ for (const rawFile of rawFiles) {
3474
+ rawIndex++;
3475
+ if (!rawFile?.sourcePath || !fs.existsSync(rawFile.sourcePath)) continue;
3476
+ const resolved = path.resolve(rawFile.sourcePath);
3477
+ if (copiedRaw.has(resolved)) continue;
3478
+ copiedRaw.add(resolved);
3479
+ const archivedPath = path.join(root, "entries", safeArchiveRelativePath(rawFile.name));
2431
3480
  ensureDir(path.dirname(archivedPath));
2432
- fs.copyFileSync(entry.sourcePath, archivedPath);
3481
+ fs.copyFileSync(rawFile.sourcePath, archivedPath);
3482
+ const stat = safeStat(archivedPath);
3483
+ const parsedEntry = entryBySourcePath.get(resolved);
3484
+ records.push({
3485
+ entryPath: rawFile.name,
3486
+ originalPath: rawFile.sourcePath,
3487
+ archivedPath,
3488
+ size: stat?.size || rawFile.size || 0,
3489
+ mtime: rawFile.mtime || (stat?.mtimeMs ? new Date(stat.mtimeMs).toISOString() : undefined),
3490
+ sha256: parsedEntry?.sha256 || fileSha256(archivedPath),
3491
+ parsed: Boolean(parsedEntry) || undefined
3492
+ });
3493
+ reportWebImportProgress(options, provider, {
3494
+ current: rawIndex,
3495
+ total: rawFiles.length,
3496
+ message: `preserving raw export files: ${rawIndex}/${rawFiles.length}`
3497
+ });
3498
+ }
3499
+ } else {
3500
+ for (const entry of source.entries) {
3501
+ records.push({
3502
+ entryPath: entry.name,
3503
+ originalPath: entry.sourcePath,
3504
+ archivedPath: containerPath,
3505
+ containerPath: containerPath || undefined,
3506
+ size: entry.size,
3507
+ sha256: entry.sha256,
3508
+ parsed: true
3509
+ });
2433
3510
  }
2434
- records.push({
2435
- entryPath: entry.name,
2436
- originalPath: entry.sourcePath,
2437
- archivedPath,
2438
- containerPath: containerPath || undefined,
2439
- size: entry.size,
2440
- sha256: entry.sha256
2441
- });
2442
3511
  }
2443
3512
  writeJson(manifestPath, {
2444
3513
  version: 1,
@@ -2454,6 +3523,20 @@ function ensureSharedWebExportRaw(provider, source, account, env = process.env)
2454
3523
  return { root, manifestPath, sha256: source.fingerprint };
2455
3524
  }
2456
3525
 
3526
+ function webExportRawFilesForProvider(source, provider) {
3527
+ const rawFiles = Array.isArray(source.rawFiles) ? source.rawFiles : [];
3528
+ if (!rawFiles.length) return [];
3529
+ if (provider !== "chatgpt") return rawFiles;
3530
+ const hasOpenAiConversationParts = rawFiles.some((file) => openAiConversationExportPath(file.name));
3531
+ if (!hasOpenAiConversationParts) return rawFiles;
3532
+ return rawFiles.filter((file) => openAiConversationExportPath(file.name));
3533
+ }
3534
+
3535
+ function openAiConversationExportPath(name) {
3536
+ const text = String(name || "");
3537
+ return /(^|\/)Conversations__[^/]*chatgpt[^/]*(?:\/|$)/i.test(text) || /(^|\/)Conversations__[^/]*chatgpt[^/]*\.zip$/i.test(text);
3538
+ }
3539
+
2457
3540
  function webRawReference(sharedRaw, conversation) {
2458
3541
  return {
2459
3542
  filename: "web-export-reference",
@@ -2487,6 +3570,7 @@ function discoverCliHistory(env = process.env, options = {}) {
2487
3570
  const codexThreads = readCodexSessionEntries(env);
2488
3571
  publish("codexCli", "Codex CLI", summarizeCodexThreads(codexThreads, "cli"));
2489
3572
  publish("codexDesktop", "Codex Desktop", summarizeCodexThreads(codexThreads, "vscode"));
3573
+ publish("codexSdk", "Codex SDK jobs", summarizeCodexThreads(codexThreads, "exec"));
2490
3574
  result.codex = summarizeCodexThreads(codexThreads);
2491
3575
 
2492
3576
  const claudeScan = scanClaudeProjectFiles({
@@ -2613,8 +3697,8 @@ function summarizeCodex(env = process.env, source) {
2613
3697
 
2614
3698
  function summarizeCodexThreads(allThreads, source) {
2615
3699
  const threads = allThreads.filter((thread) => {
2616
- if (!["cli", "vscode"].includes(thread.source)) return false;
2617
- return source ? thread.source === source : true;
3700
+ if (source) return thread.source === source;
3701
+ return ["cli", "vscode"].includes(thread.source);
2618
3702
  });
2619
3703
  if (threads.length) {
2620
3704
  const oldest = threads.length
@@ -2629,10 +3713,24 @@ function summarizeCodexThreads(allThreads, source) {
2629
3713
  ? "terminal `codex` sessions"
2630
3714
  : source === "vscode"
2631
3715
  ? "Codex desktop app sessions"
3716
+ : source === "exec"
3717
+ ? "Codex exec/SDK batch jobs; opt-in"
2632
3718
  : "includes Codex CLI and Codex Desktop top-level sessions"
2633
3719
  };
2634
3720
  }
2635
- return { sessions: 0, oldest: "", details: {}, note: source === "cli" ? "terminal `codex` sessions" : source === "vscode" ? "Codex desktop app sessions" : "" };
3721
+ return {
3722
+ sessions: 0,
3723
+ oldest: "",
3724
+ details: {},
3725
+ note:
3726
+ source === "cli"
3727
+ ? "terminal `codex` sessions"
3728
+ : source === "vscode"
3729
+ ? "Codex desktop app sessions"
3730
+ : source === "exec"
3731
+ ? "Codex exec/SDK batch jobs; opt-in"
3732
+ : ""
3733
+ };
2636
3734
  }
2637
3735
 
2638
3736
  function summarizeClaude() {
@@ -2724,90 +3822,534 @@ function summarizeCursor(env = process.env, options = {}) {
2724
3822
  errors++;
2725
3823
  }
2726
3824
  }
2727
- const transcriptSessions = readCursorProjectTranscriptSessions({ env });
2728
- const rawCompanionMerge = mergeCursorRawCompanionSessions(sessions.concat(transcriptSessions), { withStats: true });
2729
- sessions = dedupeCursorSessions(rawCompanionMerge.sessions);
2730
- const oldest = sessions.length ? sessions.map((session) => session.startedAt).sort()[0]?.slice(0, 10) || "" : "";
2731
- return {
2732
- sessions: sessions.length,
2733
- oldest,
2734
- details: {
2735
- workspaceDbs: dbs.length,
2736
- globalDbs: globalDbs.length,
2737
- projectTranscripts: transcriptSessions.length,
2738
- rawCompanionFragmentsMerged: rawCompanionMerge.merged,
2739
- rawCompanionFragmentsDropped: rawCompanionMerge.dropped,
2740
- unreadable: errors
2741
- },
2742
- note: "Cursor workspace/global SQLite, raw SQLite salvage, plus ~/.cursor/projects agent transcripts"
2743
- };
3825
+ const transcriptSessions = readCursorProjectTranscriptSessions({ env });
3826
+ const rawCompanionMerge = mergeCursorRawCompanionSessions(sessions.concat(transcriptSessions), { withStats: true });
3827
+ sessions = dedupeCursorSessions(rawCompanionMerge.sessions);
3828
+ const oldest = sessions.length ? sessions.map((session) => session.startedAt).sort()[0]?.slice(0, 10) || "" : "";
3829
+ return {
3830
+ sessions: sessions.length,
3831
+ oldest,
3832
+ details: {
3833
+ workspaceDbs: dbs.length,
3834
+ globalDbs: globalDbs.length,
3835
+ projectTranscripts: transcriptSessions.length,
3836
+ rawCompanionFragmentsMerged: rawCompanionMerge.merged,
3837
+ rawCompanionFragmentsDropped: rawCompanionMerge.dropped,
3838
+ unreadable: errors
3839
+ },
3840
+ note: "Cursor workspace/global SQLite, raw SQLite salvage, plus ~/.cursor/projects agent transcripts"
3841
+ };
3842
+ }
3843
+
3844
+ function summarizeDevin(env = process.env, options = {}) {
3845
+ const sessions = readDevinSessions(env, options);
3846
+ const oldest = sessions.length ? sessions.map((session) => session.startedAt).sort()[0]?.slice(0, 10) || "" : "";
3847
+ const db = devinSessionsDb(env);
3848
+ return {
3849
+ sessions: sessions.length,
3850
+ oldest,
3851
+ details: { database: fs.existsSync(db) ? 1 : 0, messages: sessions.reduce((sum, session) => sum + session.messages.length, 0) },
3852
+ note: "Devin for Terminal SQLite sessions.db"
3853
+ };
3854
+ }
3855
+
3856
+ function summarizeStructuredSessions(sessions, note = "") {
3857
+ const importable = sessions.filter((session) => session.messages.length);
3858
+ const oldest = importable.length ? importable.map((session) => session.startedAt).sort()[0]?.slice(0, 10) || "" : "";
3859
+ return {
3860
+ sessions: importable.length,
3861
+ oldest,
3862
+ details: summarizeStructuredSessionDetails(sessions),
3863
+ note
3864
+ };
3865
+ }
3866
+
3867
+ function summarizeStructuredSessionDetails(sessions) {
3868
+ return sessions.reduce((acc, session) => {
3869
+ if (session.detailKey) acc[session.detailKey] = (acc[session.detailKey] || 0) + 1;
3870
+ if (session.artifactCount) acc.artifacts = (acc.artifacts || 0) + session.artifactCount;
3871
+ if (session.binaryCount) acc.binaryOnly = (acc.binaryOnly || 0) + session.binaryCount;
3872
+ if (session.partialSummary) acc.partialSummaries = (acc.partialSummaries || 0) + 1;
3873
+ if (session.stateDbCount) acc.stateDbs = (acc.stateDbs || 0) + session.stateDbCount;
3874
+ return acc;
3875
+ }, {});
3876
+ }
3877
+
3878
+ function codexRoots(env = process.env) {
3879
+ const home = codexHome(env);
3880
+ return [path.join(home, "sessions"), path.join(home, "archived_sessions")];
3881
+ }
3882
+
3883
+ function claudeRoots(env = process.env) {
3884
+ const home = env && env.HOME ? env.HOME : os.homedir();
3885
+ return [path.join(home, ".claude", "projects")];
3886
+ }
3887
+
3888
+ function claudeFileHistoryRoot(env = process.env) {
3889
+ const home = env && env.HOME ? env.HOME : os.homedir();
3890
+ return path.join(home, ".claude", "file-history");
3891
+ }
3892
+
3893
+ function claudeFileHistoryFiles(sessionId, env = process.env) {
3894
+ if (!sessionId) return [];
3895
+ const dir = path.join(claudeFileHistoryRoot(env), String(sessionId));
3896
+ let entries;
3897
+ try {
3898
+ entries = fs.readdirSync(dir, { withFileTypes: true });
3899
+ } catch (error) {
3900
+ if (error.code === "ENOENT" || error.code === "ENOTDIR" || error.code === "EACCES") return [];
3901
+ throw error;
3902
+ }
3903
+ const files = [];
3904
+ for (const entry of entries) {
3905
+ if (entry.isFile()) files.push(path.join(dir, entry.name));
3906
+ }
3907
+ files.sort((a, b) => a.localeCompare(b));
3908
+ return files;
3909
+ }
3910
+
3911
+ function claudeSubagentImportContext(cwd, env = process.env, cache = null) {
3912
+ const key = `${claudeSubagentUserRoot(env)}\0${path.resolve(String(cwd || ""))}`;
3913
+ if (cache?.has(key)) return cache.get(key);
3914
+ const definitions = readClaudeSubagentDefinitions(cwd, env);
3915
+ const effective = effectiveClaudeSubagentDefinitions(definitions);
3916
+ const sourceFiles = definitions.map((definition) => definition.sourcePath).filter(Boolean);
3917
+ const projectCount = effective.filter((definition) => definition.scope === "project").length;
3918
+ const userCount = effective.filter((definition) => definition.scope === "user").length;
3919
+ const names = effective.map((definition) => definition.name).sort((a, b) => a.localeCompare(b));
3920
+ const shadowedNames = shadowedClaudeSubagentNames(definitions);
3921
+ const sessionSummary = effective.length
3922
+ ? {
3923
+ claudeSubagents: compactMetadata({
3924
+ count: effective.length,
3925
+ parsedCount: definitions.length !== effective.length ? definitions.length : undefined,
3926
+ projectCount: projectCount || undefined,
3927
+ userCount: userCount || undefined,
3928
+ shadowedNames: shadowedNames.length ? shadowedNames : undefined,
3929
+ names,
3930
+ definitions: effective.map(claudeSubagentDefinitionSummary)
3931
+ })
3932
+ }
3933
+ : null;
3934
+ const result = { sessionSummary, sourceFiles };
3935
+ if (cache) cache.set(key, result);
3936
+ return result;
3937
+ }
3938
+
3939
+ function readClaudeSubagentDefinitions(cwd, env = process.env) {
3940
+ return claudeSubagentDefinitionFiles(cwd, env)
3941
+ .map((entry) => parseClaudeSubagentDefinitionFile(entry.file, entry))
3942
+ .filter(Boolean)
3943
+ .sort((a, b) => {
3944
+ const scope = subagentScopePriority(a.scope) - subagentScopePriority(b.scope);
3945
+ return scope || a.name.localeCompare(b.name) || a.sourcePath.localeCompare(b.sourcePath);
3946
+ });
3947
+ }
3948
+
3949
+ function claudeSubagentDefinitionFiles(cwd, env = process.env) {
3950
+ const roots = [];
3951
+ const userRoot = claudeSubagentUserRoot(env);
3952
+ if (safeStat(userRoot)?.isDirectory()) roots.push({ root: userRoot, scope: "user" });
3953
+ const projectRoot = findClaudeProjectSubagentRoot(cwd);
3954
+ if (projectRoot && path.resolve(projectRoot) !== path.resolve(userRoot)) {
3955
+ roots.push({ root: projectRoot, scope: "project" });
3956
+ }
3957
+ const seen = new Set();
3958
+ const files = [];
3959
+ for (const root of roots) {
3960
+ collectFiles(root.root, (file) => {
3961
+ if (!/\.md$/i.test(file)) return;
3962
+ const resolved = path.resolve(file);
3963
+ if (seen.has(resolved)) return;
3964
+ seen.add(resolved);
3965
+ files.push({ ...root, file: resolved });
3966
+ });
3967
+ }
3968
+ return files.sort((a, b) => {
3969
+ const scope = subagentScopePriority(a.scope) - subagentScopePriority(b.scope);
3970
+ return scope || a.file.localeCompare(b.file);
3971
+ });
3972
+ }
3973
+
3974
+ function parseClaudeSubagentDefinitionFile(file, context = {}) {
3975
+ let text;
3976
+ try {
3977
+ text = fs.readFileSync(file, "utf8");
3978
+ } catch {
3979
+ return null;
3980
+ }
3981
+ const document = splitMarkdownFrontmatter(text);
3982
+ if (!document.frontmatter) return null;
3983
+ const metadata = parseSimpleYamlFrontmatter(document.frontmatter);
3984
+ const name = firstString(metadata.name, path.basename(file, path.extname(file)));
3985
+ if (!name) return null;
3986
+ const sourceRoot = context.root || path.dirname(file);
3987
+ const body = String(document.body || "").trim();
3988
+ const stat = safeStat(file);
3989
+ return compactMetadata({
3990
+ name,
3991
+ description: firstString(metadata.description),
3992
+ tools: normalizeClaudeSubagentTools(metadata.tools),
3993
+ model: firstString(metadata.model),
3994
+ scope: context.scope || "unknown",
3995
+ sourcePath: path.resolve(file),
3996
+ relativePath: relativePathWithin(sourceRoot, file),
3997
+ frontmatterKeys: Object.keys(metadata).sort((a, b) => a.localeCompare(b)),
3998
+ instructionPreview: previewString(body, 600),
3999
+ instructionLineCount: body ? body.split(/\r?\n/).length : undefined,
4000
+ mtime: stat?.mtimeMs ? new Date(stat.mtimeMs).toISOString() : undefined
4001
+ });
4002
+ }
4003
+
4004
+ function effectiveClaudeSubagentDefinitions(definitions) {
4005
+ const byName = new Map();
4006
+ for (const definition of definitions) {
4007
+ const key = definition.name.toLowerCase();
4008
+ const existing = byName.get(key);
4009
+ if (!existing || subagentScopePriority(definition.scope) >= subagentScopePriority(existing.scope)) {
4010
+ byName.set(key, definition);
4011
+ }
4012
+ }
4013
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
4014
+ }
4015
+
4016
+ function shadowedClaudeSubagentNames(definitions) {
4017
+ const seen = new Map();
4018
+ const shadowed = new Set();
4019
+ for (const definition of definitions) {
4020
+ const key = definition.name.toLowerCase();
4021
+ if (seen.has(key)) shadowed.add(definition.name);
4022
+ seen.set(key, definition);
4023
+ }
4024
+ return [...shadowed].sort((a, b) => a.localeCompare(b));
4025
+ }
4026
+
4027
+ function claudeSubagentDefinitionSummary(definition) {
4028
+ return compactMetadata({
4029
+ name: definition.name,
4030
+ description: definition.description,
4031
+ tools: definition.tools,
4032
+ model: definition.model,
4033
+ scope: definition.scope,
4034
+ sourcePath: definition.sourcePath,
4035
+ relativePath: definition.relativePath,
4036
+ instructionPreview: definition.instructionPreview,
4037
+ instructionLineCount: definition.instructionLineCount
4038
+ });
4039
+ }
4040
+
4041
+ function claudeSubagentRunImportContext(parentSessionId, conversationFile, env = process.env, sourceFiles = null) {
4042
+ const files = Array.isArray(sourceFiles)
4043
+ ? sourceFiles
4044
+ : claudeSubagentRunSourceFilesForSession(parentSessionId, conversationFile, env);
4045
+ const transcriptFiles = files.filter((file) => /\.jsonl$/i.test(file));
4046
+ const sourceRoot = conversationFile ? path.dirname(conversationFile) : "";
4047
+ const sessions = [];
4048
+ const runs = [];
4049
+ for (const file of transcriptFiles) {
4050
+ const parsed = parseClaudeSubagentRunFile(file, parentSessionId, sourceRoot);
4051
+ if (!parsed) continue;
4052
+ sessions.push(parsed.session);
4053
+ runs.push(parsed.summary);
4054
+ }
4055
+ const sourceFileList = [...new Set(files)].sort((a, b) => a.localeCompare(b));
4056
+ return {
4057
+ sourceFiles: sourceFileList,
4058
+ sessions,
4059
+ sessionSummary: runs.length
4060
+ ? {
4061
+ claudeSubagentRuns: compactMetadata({
4062
+ count: runs.length,
4063
+ agentIds: [...new Set(runs.map((run) => run.agentId).filter(Boolean))].sort((a, b) => a.localeCompare(b)),
4064
+ agentTypes: [...new Set(runs.map((run) => run.agentType).filter(Boolean))].sort((a, b) => a.localeCompare(b)),
4065
+ messageCount: runs.reduce((sum, run) => sum + (run.messageCount || 0), 0),
4066
+ userMessageCount: runs.reduce((sum, run) => sum + (run.userMessageCount || 0), 0),
4067
+ assistantMessageCount: runs.reduce((sum, run) => sum + (run.assistantMessageCount || 0), 0),
4068
+ toolCallCount: runs.reduce((sum, run) => sum + (run.toolCallCount || 0), 0),
4069
+ toolResultCount: runs.reduce((sum, run) => sum + (run.toolResultCount || 0), 0),
4070
+ runs
4071
+ })
4072
+ }
4073
+ : null
4074
+ };
4075
+ }
4076
+
4077
+ function parseClaudeSubagentRunFile(file, parentSessionId, sourceRoot = "") {
4078
+ let parsed;
4079
+ try {
4080
+ parsed = parseAgentJsonl(file, "claude_code");
4081
+ } catch {
4082
+ return null;
4083
+ }
4084
+ if (!parsed.messages.length) return null;
4085
+ const meta = readClaudeSubagentRunMeta(file);
4086
+ const agentId = claudeSubagentRunAgentId(parsed, file);
4087
+ const agentType = firstString(meta?.agentType, meta?.type, meta?.name);
4088
+ const models = sessionMessageModels(parsed.messages);
4089
+ const usage = computeSessionUsage(parsed.messages);
4090
+ const promptPreview = previewString(firstClaudeSubagentUserPrompt(parsed.messages), 360);
4091
+ const resultPreview = previewString(lastClaudeSubagentAssistantText(parsed.messages), 360);
4092
+ const toolCallCount = parsed.messages.reduce((sum, message) => sum + (Array.isArray(message.metadata?.toolCalls) ? message.metadata.toolCalls.length : 0), 0);
4093
+ const toolResultCount = parsed.messages.reduce((sum, message) => sum + (message.metadata?.toolResult ? 1 : 0), 0);
4094
+ const sourceFiles = claudeSubagentRunSourceFilesForTranscript(file);
4095
+ const sessionId = `claude-subagent-${hashId(`${parentSessionId}:${agentId}:${file}`)}`;
4096
+ const title = claudeSubagentRunTitle(agentType, parsed.title, promptPreview, agentId);
4097
+ const relativePath = relativePathWithin(sourceRoot || path.dirname(file), file);
4098
+ const summary = compactMetadata({
4099
+ sessionId,
4100
+ parentSessionId,
4101
+ agentId,
4102
+ agentType,
4103
+ title,
4104
+ startedAt: parsed.startedAt,
4105
+ endedAt: parsed.endedAt,
4106
+ messageCount: parsed.messages.length,
4107
+ userMessageCount: parsed.messages.filter((message) => message.role === "user").length,
4108
+ assistantMessageCount: parsed.messages.filter((message) => message.role === "assistant").length,
4109
+ toolCallCount,
4110
+ toolResultCount,
4111
+ models,
4112
+ usage,
4113
+ promptPreview,
4114
+ resultPreview,
4115
+ sourcePath: file,
4116
+ relativePath
4117
+ });
4118
+ return {
4119
+ summary,
4120
+ session: {
4121
+ provider: "claude_code",
4122
+ sessionId,
4123
+ cwd: parsed.cwd,
4124
+ messages: parsed.messages,
4125
+ startedAt: parsed.startedAt,
4126
+ endedAt: parsed.endedAt,
4127
+ sourcePath: file,
4128
+ sourceFiles,
4129
+ title,
4130
+ conversationKind: "claude_subagent",
4131
+ parentComposerId: parentSessionId,
4132
+ sessionSummary: mergeSessionSummaries(parsed.sessionSummary, {
4133
+ claudeSubagentRun: compactMetadata({
4134
+ parentSessionId,
4135
+ agentId,
4136
+ agentType,
4137
+ title,
4138
+ promptPreview,
4139
+ resultPreview,
4140
+ sourcePath: file,
4141
+ relativePath
4142
+ })
4143
+ })
4144
+ }
4145
+ };
4146
+ }
4147
+
4148
+ function claudeSubagentRunSourceFilesForSession(parentSessionId, conversationFile, env = process.env) {
4149
+ const files = [];
4150
+ for (const dir of claudeSubagentRunDirs(parentSessionId, conversationFile, env)) {
4151
+ let entries;
4152
+ try {
4153
+ entries = fs.readdirSync(dir, { withFileTypes: true });
4154
+ } catch (error) {
4155
+ if (error.code === "ENOENT" || error.code === "ENOTDIR" || error.code === "EACCES") continue;
4156
+ throw error;
4157
+ }
4158
+ for (const entry of entries) {
4159
+ if (!entry.isFile()) continue;
4160
+ if (!/\.jsonl$/i.test(entry.name) && !/\.meta\.json$/i.test(entry.name)) continue;
4161
+ files.push(path.join(dir, entry.name));
4162
+ }
4163
+ }
4164
+ return [...new Set(files)].sort((a, b) => a.localeCompare(b));
2744
4165
  }
2745
4166
 
2746
- function summarizeDevin(env = process.env, options = {}) {
2747
- const sessions = readDevinSessions(env, options);
2748
- const oldest = sessions.length ? sessions.map((session) => session.startedAt).sort()[0]?.slice(0, 10) || "" : "";
2749
- const db = devinSessionsDb(env);
2750
- return {
2751
- sessions: sessions.length,
2752
- oldest,
2753
- details: { database: fs.existsSync(db) ? 1 : 0, messages: sessions.reduce((sum, session) => sum + session.messages.length, 0) },
2754
- note: "Devin for Terminal SQLite sessions.db"
2755
- };
4167
+ function claudeSubagentRunDirs(parentSessionId, conversationFile, env = process.env) {
4168
+ if (!parentSessionId || !conversationFile) return [];
4169
+ const dirs = [
4170
+ path.join(path.dirname(conversationFile), String(parentSessionId), "subagents"),
4171
+ path.join(path.dirname(conversationFile), "subagents")
4172
+ ];
4173
+ const root = claudeRoots(env)[0];
4174
+ const directParent = path.join(root, path.basename(path.dirname(conversationFile)), String(parentSessionId), "subagents");
4175
+ dirs.push(directParent);
4176
+ const seen = new Set();
4177
+ return dirs.filter((dir) => {
4178
+ const resolved = path.resolve(dir);
4179
+ if (seen.has(resolved)) return false;
4180
+ seen.add(resolved);
4181
+ return safeStat(resolved)?.isDirectory();
4182
+ });
2756
4183
  }
2757
4184
 
2758
- function summarizeStructuredSessions(sessions, note = "") {
2759
- const importable = sessions.filter((session) => session.messages.length);
2760
- const oldest = importable.length ? importable.map((session) => session.startedAt).sort()[0]?.slice(0, 10) || "" : "";
2761
- return {
2762
- sessions: importable.length,
2763
- oldest,
2764
- details: summarizeStructuredSessionDetails(sessions),
2765
- note
2766
- };
4185
+ function claudeSubagentRunSourceFilesForTranscript(file) {
4186
+ const files = [file];
4187
+ const meta = claudeSubagentRunMetaFile(file);
4188
+ if (safeStat(meta)?.isFile()) files.push(meta);
4189
+ return files;
2767
4190
  }
2768
4191
 
2769
- function summarizeStructuredSessionDetails(sessions) {
2770
- return sessions.reduce((acc, session) => {
2771
- if (session.detailKey) acc[session.detailKey] = (acc[session.detailKey] || 0) + 1;
2772
- if (session.artifactCount) acc.artifacts = (acc.artifacts || 0) + session.artifactCount;
2773
- if (session.binaryCount) acc.binaryOnly = (acc.binaryOnly || 0) + session.binaryCount;
2774
- if (session.partialSummary) acc.partialSummaries = (acc.partialSummaries || 0) + 1;
2775
- if (session.stateDbCount) acc.stateDbs = (acc.stateDbs || 0) + session.stateDbCount;
2776
- return acc;
2777
- }, {});
4192
+ function claudeSubagentRunMetaFile(file) {
4193
+ return String(file || "").replace(/\.jsonl$/i, ".meta.json");
2778
4194
  }
2779
4195
 
2780
- function codexRoots(env = process.env) {
2781
- const home = codexHome(env);
2782
- return [path.join(home, "sessions"), path.join(home, "archived_sessions")];
4196
+ function readClaudeSubagentRunMeta(file) {
4197
+ const metaFile = claudeSubagentRunMetaFile(file);
4198
+ try {
4199
+ return readJson(metaFile, null);
4200
+ } catch {
4201
+ return null;
4202
+ }
2783
4203
  }
2784
4204
 
2785
- function claudeRoots(env = process.env) {
2786
- const home = env && env.HOME ? env.HOME : os.homedir();
2787
- return [path.join(home, ".claude", "projects")];
4205
+ function claudeSubagentRunAgentId(parsed, file) {
4206
+ const agentIds = parsed?.sessionSummary?.claudeJsonl?.agentIds;
4207
+ if (Array.isArray(agentIds) && agentIds.length) return agentIds[0];
4208
+ if (typeof agentIds === "string" && agentIds.trim()) return agentIds.trim();
4209
+ return path.basename(String(file || ""), path.extname(String(file || ""))).replace(/^agent-/, "");
2788
4210
  }
2789
4211
 
2790
- function claudeFileHistoryRoot(env = process.env) {
4212
+ function claudeSubagentRunTitle(agentType, parsedTitle, promptPreview, agentId) {
4213
+ const promptTitle = titleFromPrompt(promptPreview) || titleFromPrompt(parsedTitle) || firstString(parsedTitle);
4214
+ const prefix = firstString(agentType);
4215
+ const title = prefix && promptTitle ? `${prefix}: ${promptTitle}` : firstString(promptTitle, prefix, agentId, "Claude subagent");
4216
+ return titleFromPrompt(title) || title;
4217
+ }
4218
+
4219
+ function firstClaudeSubagentUserPrompt(messages) {
4220
+ return (messages || []).find((message) => message.role === "user" && String(message.content || "").trim())?.content || "";
4221
+ }
4222
+
4223
+ function lastClaudeSubagentAssistantText(messages) {
4224
+ return [...(messages || [])].reverse().find((message) => message.role === "assistant" && String(message.content || "").trim())?.content || "";
4225
+ }
4226
+
4227
+ function sessionMessageModels(messages) {
4228
+ return [...new Set((messages || []).map((message) => message.metadata?.model).filter((model) => typeof model === "string" && model.trim()).map((model) => model.trim()))]
4229
+ .sort((a, b) => a.localeCompare(b));
4230
+ }
4231
+
4232
+ function claudeSubagentUserRoot(env = process.env) {
2791
4233
  const home = env && env.HOME ? env.HOME : os.homedir();
2792
- return path.join(home, ".claude", "file-history");
4234
+ return path.join(home, ".claude", "agents");
2793
4235
  }
2794
4236
 
2795
- function claudeFileHistoryFiles(sessionId, env = process.env) {
2796
- if (!sessionId) return [];
2797
- const dir = path.join(claudeFileHistoryRoot(env), String(sessionId));
2798
- let entries;
2799
- try {
2800
- entries = fs.readdirSync(dir, { withFileTypes: true });
2801
- } catch (error) {
2802
- if (error.code === "ENOENT" || error.code === "ENOTDIR" || error.code === "EACCES") return [];
2803
- throw error;
4237
+ function findClaudeProjectSubagentRoot(cwd) {
4238
+ if (!cwd) return "";
4239
+ let dir = path.resolve(cwd);
4240
+ if (!safeStat(dir)?.isDirectory()) return "";
4241
+ for (;;) {
4242
+ const candidate = path.join(dir, ".claude", "agents");
4243
+ if (safeStat(candidate)?.isDirectory()) return candidate;
4244
+ const parent = path.dirname(dir);
4245
+ if (parent === dir || fs.existsSync(path.join(dir, ".git"))) return "";
4246
+ dir = parent;
2804
4247
  }
2805
- const files = [];
2806
- for (const entry of entries) {
2807
- if (entry.isFile()) files.push(path.join(dir, entry.name));
4248
+ }
4249
+
4250
+ function subagentScopePriority(scope) {
4251
+ if (scope === "project") return 2;
4252
+ if (scope === "user") return 1;
4253
+ return 0;
4254
+ }
4255
+
4256
+ function splitMarkdownFrontmatter(text) {
4257
+ const normalized = String(text || "").replace(/^\uFEFF/, "");
4258
+ const match = normalized.match(/^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)([\s\S]*)$/);
4259
+ if (!match) return { frontmatter: "", body: normalized };
4260
+ return { frontmatter: match[1], body: match[2] || "" };
4261
+ }
4262
+
4263
+ function parseSimpleYamlFrontmatter(text) {
4264
+ const lines = String(text || "").split(/\r?\n/);
4265
+ const result = {};
4266
+ for (let index = 0; index < lines.length; index++) {
4267
+ const line = lines[index];
4268
+ if (!line.trim() || /^\s*#/.test(line)) continue;
4269
+ const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
4270
+ if (!match) continue;
4271
+ const key = match[1];
4272
+ const raw = match[2] || "";
4273
+ if (/^[>|]/.test(raw.trim())) {
4274
+ const block = [];
4275
+ while (index + 1 < lines.length && (/^(?:\s+|$)/.test(lines[index + 1]))) {
4276
+ index++;
4277
+ block.push(lines[index].replace(/^\s{2}/, ""));
4278
+ }
4279
+ result[key] = raw.trim().startsWith(">") ? block.join(" ").replace(/\s+/g, " ").trim() : block.join("\n").trim();
4280
+ } else if (!raw.trim()) {
4281
+ const values = [];
4282
+ while (index + 1 < lines.length) {
4283
+ const item = lines[index + 1].match(/^\s*-\s*(.*)$/);
4284
+ if (!item) break;
4285
+ index++;
4286
+ values.push(parseYamlScalar(item[1]));
4287
+ }
4288
+ result[key] = values.length ? values : "";
4289
+ } else {
4290
+ result[key] = parseYamlScalar(raw);
4291
+ }
2808
4292
  }
2809
- files.sort((a, b) => a.localeCompare(b));
2810
- return files;
4293
+ return result;
4294
+ }
4295
+
4296
+ function parseYamlScalar(value) {
4297
+ const text = stripYamlComment(String(value || "").trim());
4298
+ if (!text) return "";
4299
+ if (text.startsWith("[") && text.endsWith("]")) {
4300
+ return text
4301
+ .slice(1, -1)
4302
+ .split(",")
4303
+ .map((item) => stripYamlString(item.trim()))
4304
+ .filter(Boolean);
4305
+ }
4306
+ return stripYamlString(text);
4307
+ }
4308
+
4309
+ function stripYamlComment(value) {
4310
+ let quote = "";
4311
+ for (let index = 0; index < value.length; index++) {
4312
+ const char = value[index];
4313
+ if ((char === '"' || char === "'") && value[index - 1] !== "\\") {
4314
+ quote = quote === char ? "" : quote || char;
4315
+ }
4316
+ if (char === "#" && !quote && (index === 0 || /\s/.test(value[index - 1]))) {
4317
+ return value.slice(0, index).trimEnd();
4318
+ }
4319
+ }
4320
+ return value;
4321
+ }
4322
+
4323
+ function stripYamlString(value) {
4324
+ const text = String(value || "").trim();
4325
+ if ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'"))) {
4326
+ return text.slice(1, -1);
4327
+ }
4328
+ return text;
4329
+ }
4330
+
4331
+ function normalizeClaudeSubagentTools(value) {
4332
+ const values = Array.isArray(value)
4333
+ ? value
4334
+ : typeof value === "string"
4335
+ ? value.split(",")
4336
+ : [];
4337
+ return values
4338
+ .map((item) => String(item || "").trim())
4339
+ .filter(Boolean)
4340
+ .filter((item) => !/^none$/i.test(item))
4341
+ .sort((a, b) => a.localeCompare(b));
4342
+ }
4343
+
4344
+ function relativePathWithin(root, file) {
4345
+ const relative = path.relative(root || path.dirname(file), file);
4346
+ return relative && !relative.startsWith("..") && !path.isAbsolute(relative) ? relative : path.basename(file);
4347
+ }
4348
+
4349
+ function previewString(value, max = 600) {
4350
+ const text = String(value || "").replace(/\s+/g, " ").trim();
4351
+ if (!text) return "";
4352
+ return text.length > max ? `${text.slice(0, max - 3).trimEnd()}...` : text;
2811
4353
  }
2812
4354
 
2813
4355
  function jsonlFiles(roots) {
@@ -2888,11 +4430,13 @@ function classifyClaudeFile(file) {
2888
4430
  }
2889
4431
  let inspected = 0;
2890
4432
  let sawSdkMessage = false;
4433
+ let sawRemoteControlTools = false;
2891
4434
  for (const line of lines) {
2892
4435
  if (!line.trim()) continue;
2893
4436
  inspected++;
2894
4437
  try {
2895
4438
  const event = JSON.parse(line);
4439
+ if (isClaudeRemoteControlToolDelta(event)) sawRemoteControlTools = true;
2896
4440
  if ((event.type === "user" || event.type === "assistant") && event.message) {
2897
4441
  if (event.entrypoint === "sdk-cli") sawSdkMessage = true;
2898
4442
  else return "conversation";
@@ -2900,9 +4444,14 @@ function classifyClaudeFile(file) {
2900
4444
  } catch {
2901
4445
  return "other";
2902
4446
  }
2903
- if (inspected >= 80) return sawSdkMessage ? "sdk-job" : "other";
4447
+ if (inspected >= 80) return sawSdkMessage ? (sawRemoteControlTools ? "conversation" : "sdk-job") : "other";
2904
4448
  }
2905
- return sawSdkMessage ? "sdk-job" : "other";
4449
+ return sawSdkMessage ? (sawRemoteControlTools ? "conversation" : "sdk-job") : "other";
4450
+ }
4451
+
4452
+ function isClaudeRemoteControlToolDelta(event) {
4453
+ if (!event || event.type !== "attachment" || !Array.isArray(event.attachment?.addedNames)) return false;
4454
+ return event.attachment.addedNames.some((name) => isClaudeRemoteControlToolName(name));
2906
4455
  }
2907
4456
 
2908
4457
  function readInitialLines(file, maxLines, maxBytes = 1024 * 1024) {
@@ -2936,59 +4485,196 @@ function readInitialLines(file, maxLines, maxBytes = 1024 * 1024) {
2936
4485
  function readCodexThreads(env = process.env) {
2937
4486
  const db = codexStateDb(env);
2938
4487
  if (!fs.existsSync(db)) return [];
4488
+ const sessionIndex = readCodexSessionIndex(env);
4489
+ const threadColumns = sqliteTableColumns(db, "threads");
2939
4490
  const hasStage1Outputs = sqliteTableExists(db, "stage1_outputs");
2940
- const query = hasStage1Outputs
2941
- ? [
2942
- "select t.id, t.rollout_path, t.created_at, t.updated_at, t.source, t.cwd, t.title,",
2943
- "s.raw_memory, s.rollout_summary, s.generated_at as summary_generated_at,",
2944
- "s.source_updated_at as summary_source_updated_at, s.rollout_slug",
2945
- "from threads t",
2946
- "left join stage1_outputs s on s.thread_id = t.id",
2947
- "where t.rollout_path != ''",
2948
- "order by t.updated_at desc"
2949
- ].join(" ")
2950
- : [
2951
- "select id, rollout_path, created_at, updated_at, source, cwd, title",
2952
- "from threads",
2953
- "where rollout_path != ''",
2954
- "order by updated_at desc"
2955
- ].join(" ");
4491
+ const hasSpawnEdges = sqliteTableExists(db, "thread_spawn_edges");
4492
+ const select = [
4493
+ "t.id",
4494
+ "t.rollout_path",
4495
+ "t.created_at",
4496
+ "t.updated_at",
4497
+ "t.source",
4498
+ "t.cwd",
4499
+ "t.title",
4500
+ sqliteSelectMaybe(threadColumns, "t", "created_at_ms"),
4501
+ sqliteSelectMaybe(threadColumns, "t", "updated_at_ms"),
4502
+ sqliteSelectMaybe(threadColumns, "t", "agent_nickname"),
4503
+ sqliteSelectMaybe(threadColumns, "t", "agent_role"),
4504
+ sqliteSelectMaybe(threadColumns, "t", "agent_path"),
4505
+ sqliteSelectMaybe(threadColumns, "t", "thread_source"),
4506
+ sqliteSelectMaybe(threadColumns, "t", "tokens_used"),
4507
+ sqliteSelectMaybe(threadColumns, "t", "model"),
4508
+ sqliteSelectMaybe(threadColumns, "t", "model_provider", "model_provider"),
4509
+ hasStage1Outputs ? "s.raw_memory" : "null as raw_memory",
4510
+ hasStage1Outputs ? "s.rollout_summary" : "null as rollout_summary",
4511
+ hasStage1Outputs ? "s.generated_at as summary_generated_at" : "null as summary_generated_at",
4512
+ hasStage1Outputs ? "s.source_updated_at as summary_source_updated_at" : "null as summary_source_updated_at",
4513
+ hasStage1Outputs ? "s.rollout_slug" : "null as rollout_slug",
4514
+ hasSpawnEdges ? "e.parent_thread_id as spawn_parent_thread_id" : "null as spawn_parent_thread_id",
4515
+ hasSpawnEdges ? "e.status as spawn_status" : "null as spawn_status"
4516
+ ];
4517
+ const joins = [
4518
+ hasStage1Outputs ? "left join stage1_outputs s on s.thread_id = t.id" : "",
4519
+ hasSpawnEdges ? "left join thread_spawn_edges e on e.child_thread_id = t.id" : ""
4520
+ ].filter(Boolean).join(" ");
4521
+ const query = [
4522
+ `select ${select.join(", ")}`,
4523
+ "from threads t",
4524
+ joins,
4525
+ "where t.rollout_path != ''",
4526
+ "order by t.updated_at desc"
4527
+ ].filter(Boolean).join(" ");
2956
4528
  const result = spawnSync("sqlite3", [db, "-json", query], { argv0: "agentlog-sqlite", encoding: "utf8", maxBuffer: 1024 * 1024 * 50 });
2957
4529
  if (result.status !== 0 || !result.stdout.trim()) return [];
2958
4530
  try {
2959
- return JSON.parse(result.stdout).map((row) => ({
2960
- id: row.id,
2961
- rolloutPath: row.rollout_path,
2962
- createdAt: toIso(row.created_at),
2963
- updatedAt: toIso(row.updated_at),
2964
- source: row.source,
2965
- cwd: row.cwd,
2966
- title: row.title,
2967
- rawMemory: row.raw_memory || "",
2968
- rolloutSummary: row.rollout_summary || "",
2969
- summaryGeneratedAt: toIso(row.summary_generated_at),
2970
- summarySourceUpdatedAt: toIso(row.summary_source_updated_at),
2971
- rolloutSlug: row.rollout_slug || ""
2972
- }));
4531
+ const rows = JSON.parse(result.stdout).map((row) => {
4532
+ const sourceInfo = codexThreadSourceInfo(row.source);
4533
+ return {
4534
+ id: row.id,
4535
+ rolloutPath: row.rollout_path,
4536
+ createdAt: toIso(row.created_at_ms || row.created_at),
4537
+ updatedAt: toIso(row.updated_at_ms || row.updated_at),
4538
+ source: row.source,
4539
+ rawSource: row.source,
4540
+ threadSource: row.thread_source || "",
4541
+ tokensUsed: numberValue(row.tokens_used),
4542
+ model: firstString(row.model),
4543
+ modelProvider: firstString(row.model_provider),
4544
+ cwd: row.cwd,
4545
+ stateTitle: row.title,
4546
+ rawMemory: row.raw_memory || "",
4547
+ rolloutSummary: row.rollout_summary || "",
4548
+ summaryGeneratedAt: toIso(row.summary_generated_at),
4549
+ summarySourceUpdatedAt: toIso(row.summary_source_updated_at),
4550
+ rolloutSlug: row.rollout_slug || "",
4551
+ parentThreadId: firstString(row.spawn_parent_thread_id, sourceInfo.parentThreadId),
4552
+ spawnStatus: firstString(row.spawn_status, sourceInfo.status),
4553
+ agentNickname: firstString(row.agent_nickname, sourceInfo.agentNickname),
4554
+ agentRole: firstString(row.agent_role, sourceInfo.agentRole),
4555
+ agentPath: firstString(row.agent_path, sourceInfo.agentPath),
4556
+ subagentDepth: numberValue(sourceInfo.depth),
4557
+ isCodexSubagent: Boolean(row.spawn_parent_thread_id || sourceInfo.isSubagent || row.agent_nickname || row.agent_role || row.agent_path)
4558
+ };
4559
+ });
4560
+ return resolveCodexThreadSources(rows).map((thread) => applyCodexSessionIndexTitle({ ...thread, title: thread.stateTitle || "" }, sessionIndex));
2973
4561
  } catch {
2974
4562
  return [];
2975
4563
  }
2976
4564
  }
2977
4565
 
4566
+ function resolveCodexThreadSources(threads) {
4567
+ const byId = new Map((threads || []).map((thread) => [thread.id, thread]));
4568
+ const resolved = new Map();
4569
+ const sourceForThread = (thread, seen = new Set()) => {
4570
+ if (!thread) return "cli";
4571
+ const cached = resolved.get(thread.id);
4572
+ if (cached) return cached;
4573
+ const direct = codexSourceKind(thread.rawSource || thread.source) || codexSourceKind(thread.threadSource);
4574
+ if (direct) {
4575
+ resolved.set(thread.id, direct);
4576
+ return direct;
4577
+ }
4578
+ if (thread.parentThreadId && !seen.has(thread.id)) {
4579
+ seen.add(thread.id);
4580
+ const parentSource = sourceForThread(byId.get(thread.parentThreadId), seen);
4581
+ resolved.set(thread.id, parentSource);
4582
+ return parentSource;
4583
+ }
4584
+ resolved.set(thread.id, "cli");
4585
+ return "cli";
4586
+ };
4587
+ return (threads || []).map((thread) => ({
4588
+ ...thread,
4589
+ source: sourceForThread(thread),
4590
+ isCodexSubagent: Boolean(thread.isCodexSubagent || thread.parentThreadId)
4591
+ }));
4592
+ }
4593
+
4594
+ function codexSourceKind(value) {
4595
+ const text = String(value || "").trim();
4596
+ return ["cli", "vscode", "exec"].includes(text) ? text : "";
4597
+ }
4598
+
4599
+ function codexThreadSourceInfo(value) {
4600
+ let parsed = null;
4601
+ try {
4602
+ parsed = typeof value === "string" && value.trim().startsWith("{") ? JSON.parse(value) : null;
4603
+ } catch {
4604
+ parsed = null;
4605
+ }
4606
+ const spawn = parsed?.subagent?.thread_spawn || parsed?.thread_spawn || null;
4607
+ if (!spawn || typeof spawn !== "object") return {};
4608
+ return {
4609
+ isSubagent: true,
4610
+ parentThreadId: firstString(spawn.parent_thread_id, spawn.parentThreadId, spawn.parent_id, spawn.parentId),
4611
+ agentNickname: firstString(spawn.agent_nickname, spawn.agentNickname, spawn.nickname),
4612
+ agentRole: firstString(spawn.agent_role, spawn.agentRole, spawn.role),
4613
+ agentPath: firstString(spawn.agent_path, spawn.agentPath, spawn.path),
4614
+ status: firstString(spawn.status),
4615
+ depth: spawn.depth
4616
+ };
4617
+ }
4618
+
4619
+ function applyCodexSessionIndexTitle(thread, sessionIndex) {
4620
+ if (!thread) return thread;
4621
+ const indexedTitle = sessionIndex.get(thread.id);
4622
+ if (!indexedTitle?.title) return thread;
4623
+ return {
4624
+ ...thread,
4625
+ title: indexedTitle.title,
4626
+ titleSource: "session-index",
4627
+ sessionIndexPath: indexedTitle.path,
4628
+ sessionIndexUpdatedAt: indexedTitle.updatedAt,
4629
+ sessionIndexTitle: indexedTitle.title
4630
+ };
4631
+ }
4632
+
4633
+ function readCodexSessionIndex(env = process.env) {
4634
+ const file = codexSessionIndexPath(env);
4635
+ if (!fs.existsSync(file)) return new Map();
4636
+ const byId = new Map();
4637
+ let lines = [];
4638
+ try {
4639
+ lines = fs.readFileSync(file, "utf8").split(/\r?\n/);
4640
+ } catch {
4641
+ return byId;
4642
+ }
4643
+ for (const line of lines) {
4644
+ if (!line.trim()) continue;
4645
+ let entry;
4646
+ try {
4647
+ entry = JSON.parse(line);
4648
+ } catch {
4649
+ continue;
4650
+ }
4651
+ const id = firstString(entry.id, entry.thread_id, entry.threadId, entry.session_id, entry.sessionId);
4652
+ const title = titleFromPrompt(firstString(entry.thread_name, entry.threadName, entry.title, entry.name));
4653
+ if (!id || !title) continue;
4654
+ const updatedAt = toIso(entry.updated_at || entry.updatedAt || entry.time || entry.timestamp);
4655
+ const previous = byId.get(id);
4656
+ if (!previous || String(updatedAt || "").localeCompare(String(previous.updatedAt || "")) >= 0) {
4657
+ byId.set(id, { title, updatedAt, path: file });
4658
+ }
4659
+ }
4660
+ return byId;
4661
+ }
4662
+
2978
4663
  function readCodexSessionEntries(env = process.env) {
4664
+ const sessionIndex = readCodexSessionIndex(env);
2979
4665
  const indexed = readCodexThreads(env).map((thread) => ({ ...thread, indexed: true }));
2980
4666
  const seen = new Set(indexed.map((thread) => normalizeSourcePath(thread.rolloutPath)));
2981
4667
  const discovered = [];
2982
4668
  for (const file of codexRolloutFiles(env)) {
2983
4669
  const key = normalizeSourcePath(file);
2984
4670
  if (seen.has(key)) continue;
2985
- const thread = codexRolloutFileThread(file);
4671
+ const thread = applyCodexSessionIndexTitle(codexRolloutFileThread(file), sessionIndex);
2986
4672
  if (thread) {
2987
4673
  discovered.push(thread);
2988
4674
  seen.add(key);
2989
4675
  }
2990
4676
  }
2991
- return indexed.concat(discovered).sort((a, b) => String(b.updatedAt || "").localeCompare(String(a.updatedAt || "")));
4677
+ return indexed.concat(discovered).sort((a, b) => String(codexThreadImportUpdatedAt(b)).localeCompare(String(codexThreadImportUpdatedAt(a))));
2992
4678
  }
2993
4679
 
2994
4680
  function codexRolloutFiles(env = process.env) {
@@ -3060,18 +4746,174 @@ function readCodexRolloutInfo(file) {
3060
4746
  info.cwd ||= firstString(payload.cwd, event.cwd);
3061
4747
  }
3062
4748
  info.cwd ||= firstString(event.cwd, payload.cwd);
3063
- info.title ||= firstUserTextFromCodexEvent(event).replace(/\s+/g, " ").slice(0, 100);
3064
- if (info.id && info.cwd && info.title && lastTimestamp) break;
4749
+ const threadTitle = codexThreadNameFromEvent(event);
4750
+ if (threadTitle) {
4751
+ info.title = threadTitle;
4752
+ info.titleSource = "thread-name";
4753
+ } else if (!info.title) {
4754
+ const promptTitle = titleFromPrompt(firstUserTextFromCodexEvent(event));
4755
+ if (promptTitle) {
4756
+ info.title = promptTitle;
4757
+ info.titleSource = "prompt";
4758
+ }
4759
+ }
4760
+ if (info.id && info.cwd && info.title && lastTimestamp && info.titleSource === "thread-name") break;
3065
4761
  }
3066
4762
  if (lastTimestamp) info.updatedAt = lastTimestamp;
3067
4763
  return info;
3068
4764
  }
3069
4765
 
4766
+ function codexSessionTitleForImport(thread, parsed) {
4767
+ const indexedTitle = firstString(thread?.sessionIndexTitle);
4768
+ if (indexedTitle) return indexedTitle;
4769
+ const parsedTitle = firstString(parsed?.title);
4770
+ if (parsedTitle && (parsed?.titleSource === "thread-name" || codexPromptLikeThreadTitle(thread?.title))) return parsedTitle;
4771
+ return firstString(thread?.title, parsedTitle);
4772
+ }
4773
+
4774
+ function codexSubagentRunImportContext(parentThread, childThreads = [], env = process.env) {
4775
+ if (!parentThread?.id || !Array.isArray(childThreads) || !childThreads.length) return null;
4776
+ const runs = [];
4777
+ for (const childThread of childThreads) {
4778
+ const parsed = parseCodexSubagentThread(childThread);
4779
+ const summary = codexSubagentRunSummary(parentThread, childThread, parsed, env);
4780
+ if (summary) runs.push(summary);
4781
+ }
4782
+ runs.sort((a, b) => {
4783
+ const time = String(a.startedAt || a.endedAt || "").localeCompare(String(b.startedAt || b.endedAt || ""));
4784
+ return time || String(a.sessionId || "").localeCompare(String(b.sessionId || ""));
4785
+ });
4786
+ if (!runs.length) return null;
4787
+ return {
4788
+ sessionSummary: {
4789
+ codexSubagentRuns: compactMetadata({
4790
+ count: runs.length,
4791
+ agentIds: [...new Set(runs.map((run) => run.agentId).filter(Boolean))].sort((a, b) => a.localeCompare(b)),
4792
+ agentNicknames: [...new Set(runs.map((run) => run.agentNickname).filter(Boolean))].sort((a, b) => a.localeCompare(b)),
4793
+ agentRoles: [...new Set(runs.map((run) => run.agentRole).filter(Boolean))].sort((a, b) => a.localeCompare(b)),
4794
+ statuses: [...new Set(runs.map((run) => run.status).filter(Boolean))].sort((a, b) => a.localeCompare(b)),
4795
+ messageCount: runs.reduce((sum, run) => sum + (run.messageCount || 0), 0),
4796
+ userMessageCount: runs.reduce((sum, run) => sum + (run.userMessageCount || 0), 0),
4797
+ assistantMessageCount: runs.reduce((sum, run) => sum + (run.assistantMessageCount || 0), 0),
4798
+ toolCallCount: runs.reduce((sum, run) => sum + (run.toolCallCount || 0), 0),
4799
+ toolResultCount: runs.reduce((sum, run) => sum + (run.toolResultCount || 0), 0),
4800
+ runs
4801
+ })
4802
+ }
4803
+ };
4804
+ }
4805
+
4806
+ function codexSubagentSessionSummary(thread, parsed, env = process.env) {
4807
+ if (!thread?.isCodexSubagent && !thread?.parentThreadId) return null;
4808
+ const summary = codexSubagentRunSummary(null, thread, parsed, env);
4809
+ return summary ? { sessionSummary: { codexSubagentRun: summary } } : null;
4810
+ }
4811
+
4812
+ function parseCodexSubagentThread(thread) {
4813
+ if (!thread?.rolloutPath) return null;
4814
+ try {
4815
+ return finalizeCodexParsedThread(thread, parseAgentJsonl(thread.rolloutPath, "codex"));
4816
+ } catch {
4817
+ return null;
4818
+ }
4819
+ }
4820
+
4821
+ function codexSubagentRunSummary(parentThread, childThread, parsed, env = process.env) {
4822
+ if (!childThread?.id && !parsed?.sessionId) return null;
4823
+ const messages = Array.isArray(parsed?.messages) ? parsed.messages : [];
4824
+ const sessionId = parsed?.sessionId || childThread.id;
4825
+ const parentSessionId = firstString(parentThread?.id, childThread.parentThreadId);
4826
+ const promptPreview = previewString(firstCodexSubagentUserPrompt(messages) || childThread.title, 360);
4827
+ const resultPreview = previewString(lastCodexSubagentAssistantText(messages), 360);
4828
+ const toolCallCount = messages.reduce((sum, message) => sum + (Array.isArray(message.metadata?.toolCalls) ? message.metadata.toolCalls.length : 0), 0);
4829
+ const toolResultCount = messages.reduce((sum, message) => sum + (message.metadata?.toolResult ? 1 : 0), 0);
4830
+ const usage = messages.length ? computeSessionUsage(messages) : null;
4831
+ const sourceRoot = codexHome(env);
4832
+ return compactMetadata({
4833
+ sessionId,
4834
+ parentSessionId,
4835
+ agentId: childThread.id || sessionId,
4836
+ agentNickname: childThread.agentNickname,
4837
+ agentRole: childThread.agentRole,
4838
+ agentType: childThread.agentRole,
4839
+ agentPath: childThread.agentPath,
4840
+ status: childThread.spawnStatus,
4841
+ depth: childThread.subagentDepth,
4842
+ provider: "codex",
4843
+ providerLabel: "Codex",
4844
+ title: codexSubagentRunTitle(childThread, parsed?.title, promptPreview),
4845
+ startedAt: parsed?.startedAt || childThread.createdAt,
4846
+ endedAt: parsed?.endedAt || childThread.updatedAt,
4847
+ messageCount: messages.length || undefined,
4848
+ userMessageCount: messages.filter((message) => message.role === "user").length || undefined,
4849
+ assistantMessageCount: messages.filter((message) => message.role === "assistant").length || undefined,
4850
+ toolCallCount: toolCallCount || undefined,
4851
+ toolResultCount: toolResultCount || undefined,
4852
+ models: sessionMessageModels(messages),
4853
+ usage,
4854
+ promptPreview,
4855
+ resultPreview,
4856
+ sourcePath: childThread.rolloutPath,
4857
+ relativePath: childThread.rolloutPath ? relativePathWithin(sourceRoot, childThread.rolloutPath) : ""
4858
+ });
4859
+ }
4860
+
4861
+ function codexSubagentRunTitle(thread, parsedTitle, promptPreview) {
4862
+ const prefix = firstString(thread?.agentNickname, thread?.agentRole);
4863
+ const promptTitle = titleFromPrompt(firstString(parsedTitle, thread?.title, promptPreview));
4864
+ if (prefix && promptTitle) return `${prefix}: ${promptTitle}`;
4865
+ return firstString(promptTitle, prefix, thread?.id, "Codex subagent");
4866
+ }
4867
+
4868
+ function firstCodexSubagentUserPrompt(messages) {
4869
+ return (messages || []).find((message) => message.role === "user" && String(message.content || "").trim())?.content || "";
4870
+ }
4871
+
4872
+ function lastCodexSubagentAssistantText(messages) {
4873
+ return [...(messages || [])].reverse().find((message) => message.role === "assistant" && String(message.content || "").trim())?.content || "";
4874
+ }
4875
+
4876
+ function codexThreadImportUpdatedAt(thread, childThreads = []) {
4877
+ return latestIso(thread?.updatedAt, thread?.sessionIndexUpdatedAt, thread?.createdAt, ...(childThreads || []).map((child) => codexThreadImportUpdatedAt(child)));
4878
+ }
4879
+
4880
+ function latestIso(...values) {
4881
+ let latest = 0;
4882
+ for (const value of values) {
4883
+ const iso = toIso(value);
4884
+ if (!iso) continue;
4885
+ const time = new Date(iso).getTime();
4886
+ if (Number.isFinite(time) && time > latest) latest = time;
4887
+ }
4888
+ return latest ? new Date(latest).toISOString() : "";
4889
+ }
4890
+
4891
+ function codexPromptLikeThreadTitle(value) {
4892
+ const text = String(value || "").trim();
4893
+ return text.length > 160 || /^# Files mentioned by the user:/m.test(text) || /\[\$[^\]\n]+\]\([^)]+SKILL\.md\)/i.test(text);
4894
+ }
4895
+
4896
+ function codexThreadNameFromEvent(event) {
4897
+ const item = event?.payload || event?.item || event?.data || event;
4898
+ const type = String(item?.type || event?.type || item?.kind || "").toLowerCase();
4899
+ if (!["thread_name_updated", "thread_title_updated", "conversation_title_updated"].includes(type)) return "";
4900
+ return titleFromPrompt(firstString(item.thread_name, item.threadName, item.title, item.name));
4901
+ }
4902
+
3070
4903
  function firstUserTextFromCodexEvent(event) {
3071
4904
  const payload = event.payload || event;
3072
- if (payload.type === "message" && payload.role === "user") return extractText(payload.content);
3073
- if (payload.type === "user_message") return firstString(payload.message, payload.text, extractText(payload.content));
3074
- return "";
4905
+ let content = "";
4906
+ if (payload.type === "message" && payload.role === "user") content = extractText(payload.content);
4907
+ else if (payload.type === "user_message") content = firstString(payload.message, payload.text, extractText(payload.content));
4908
+ if (!content) return "";
4909
+ const classification = providerGeneratedContext(content, "codex", event);
4910
+ if (!classification) return content;
4911
+ return classification.kind === "attachment_context" ? codexAttachmentRequestText(content) : "";
4912
+ }
4913
+
4914
+ function codexAttachmentRequestText(value) {
4915
+ const match = String(value || "").match(/(?:^|\n)## My request for Codex:\s*\n([\s\S]*)$/i);
4916
+ return match ? match[1].trim() : "";
3075
4917
  }
3076
4918
 
3077
4919
  function codexTimestampFromFilename(file) {
@@ -3125,12 +4967,20 @@ function codexStateDb(env = process.env) {
3125
4967
  return env.CODEX_STATE_DB || path.join(codexHome(env), "state_5.sqlite");
3126
4968
  }
3127
4969
 
4970
+ function codexSessionIndexPath(env = process.env) {
4971
+ return env.CODEX_SESSION_INDEX || path.join(codexHome(env), "session_index.jsonl");
4972
+ }
4973
+
3128
4974
  function codexHome(env = process.env) {
3129
4975
  return env.CODEX_HOME || path.join(os.homedir(), ".codex");
3130
4976
  }
3131
4977
 
3132
- function codexSourceFiles(thread, env = process.env) {
3133
- return [thread.rolloutPath, thread.indexed ? codexStateDb(env) : ""].filter(Boolean);
4978
+ function codexSourceFiles(thread, env = process.env, childThreads = []) {
4979
+ const files = [thread.rolloutPath, thread.indexed ? codexStateDb(env) : "", thread.sessionIndexPath || ""].filter(Boolean);
4980
+ for (const child of childThreads || []) {
4981
+ files.push(...codexSourceFiles(child, env));
4982
+ }
4983
+ return [...new Set(files)].sort((a, b) => a.localeCompare(b));
3134
4984
  }
3135
4985
 
3136
4986
  function codexSupplementaryMessages(thread, fallbackTimestamp = "") {
@@ -3163,19 +5013,59 @@ function codexSupplementFingerprint(thread) {
3163
5013
  thread.rolloutSummary || "",
3164
5014
  thread.summaryGeneratedAt || "",
3165
5015
  thread.summarySourceUpdatedAt || "",
3166
- thread.rolloutSlug || ""
5016
+ thread.rolloutSlug || "",
5017
+ thread.sessionIndexTitle || "",
5018
+ thread.sessionIndexUpdatedAt || ""
3167
5019
  ].join("\0");
3168
5020
  if (!payload.replace(/\0/g, "")) return "summaries:none";
3169
5021
  const hash = crypto.createHash("sha256").update(payload).digest("hex").slice(0, 24);
3170
5022
  return `summaries:${hash}`;
3171
5023
  }
3172
5024
 
5025
+ function codexThreadMetadataFingerprint(thread) {
5026
+ const payload = [
5027
+ thread?.rawSource || "",
5028
+ thread?.threadSource || "",
5029
+ thread?.parentThreadId || "",
5030
+ thread?.spawnStatus || "",
5031
+ thread?.agentNickname || "",
5032
+ thread?.agentRole || "",
5033
+ thread?.agentPath || "",
5034
+ thread?.subagentDepth || "",
5035
+ thread?.tokensUsed || ""
5036
+ ].join("\0");
5037
+ if (!payload.replace(/\0/g, "")) return "";
5038
+ const hash = crypto.createHash("sha256").update(payload).digest("hex").slice(0, 24);
5039
+ return `thread-meta:${hash}`;
5040
+ }
5041
+
5042
+ function codexSubagentRunsFingerprint(childThreads = []) {
5043
+ const payload = (childThreads || [])
5044
+ .map((thread) => [
5045
+ thread.id || "",
5046
+ thread.rolloutPath || "",
5047
+ thread.parentThreadId || "",
5048
+ thread.spawnStatus || "",
5049
+ thread.agentNickname || "",
5050
+ thread.agentRole || "",
5051
+ thread.agentPath || "",
5052
+ thread.tokensUsed || "",
5053
+ thread.rolloutPath ? fileFingerprint(thread.rolloutPath, safeStat(thread.rolloutPath)) : ""
5054
+ ].join("\t"))
5055
+ .join("\n");
5056
+ if (!payload) return "";
5057
+ const hash = crypto.createHash("sha256").update(payload).digest("hex").slice(0, 24);
5058
+ return `codex-subagent-runs:${hash}`;
5059
+ }
5060
+
3173
5061
  function summarizeCodexSources(threads) {
3174
5062
  const cli = threads.filter((thread) => thread.source === "cli").length;
3175
5063
  const desktop = threads.filter((thread) => thread.source === "vscode").length;
5064
+ const exec = threads.filter((thread) => thread.source === "exec").length;
3176
5065
  const summaries = threads.filter((thread) => thread.rawMemory || thread.rolloutSummary).length;
3177
5066
  const archived = threads.filter((thread) => thread.sourceDetail === "archived_sessions").length;
3178
- return { cli, desktop, ...(archived ? { archived } : {}), ...(summaries ? { summaries } : {}) };
5067
+ const subagents = threads.filter((thread) => thread.isCodexSubagent).length;
5068
+ return { cli, desktop, ...(exec ? { exec } : {}), ...(subagents ? { subagents } : {}), ...(archived ? { archived } : {}), ...(summaries ? { summaries } : {}) };
3179
5069
  }
3180
5070
 
3181
5071
  function readClaudeDesktopSessions(options = {}, env = process.env) {
@@ -6967,10 +8857,10 @@ function readOpenCodeSqliteSessionsFromDb(dbPath, options = {}, env = process.en
6967
8857
  if (!sqliteTableExists(dbPath, "session") || !sqliteTableExists(dbPath, "message") || !sqliteTableExists(dbPath, "part")) return [];
6968
8858
  const sessionRows = readOpenCodeSqliteSessionRows(dbPath, options);
6969
8859
  if (!sessionRows.length) return [];
6970
- const classifications = openCodeSqliteSessionClassifications(sessionRows, dbPath, env, options);
6971
8860
  const sessionIds = sessionRows.map((row) => row.id).filter(Boolean);
6972
8861
  const messageRows = sortOpenCodeSqliteRows(readOpenCodeSqliteMessageRows(dbPath, sessionIds), ["session_id", "time_created", "id"]);
6973
8862
  const partRows = sortOpenCodeSqliteRows(readOpenCodeSqlitePartRows(dbPath, sessionIds), ["session_id", "message_id", "time_created", "id"]);
8863
+ const classifications = openCodeSqliteSessionClassifications(sessionRows, dbPath, env, options, messageRows);
6974
8864
  const messagesBySession = groupRowsBy(messageRows, "session_id");
6975
8865
  const partsByMessage = groupRowsBy(partRows, "message_id");
6976
8866
  const storageRoot = path.join(path.dirname(dbPath), "storage");
@@ -7061,9 +8951,10 @@ function readOpenCodeSqliteSessionRows(dbPath, options = {}) {
7061
8951
  return readSqliteJson(dbPath, queryParts.join(" "), "OpenCode SQLite sessions");
7062
8952
  }
7063
8953
 
7064
- function openCodeSqliteSessionClassifications(sessionRows, dbPath, env = process.env, options = {}) {
8954
+ function openCodeSqliteSessionClassifications(sessionRows, dbPath, env = process.env, options = {}, messageRows = []) {
7065
8955
  const baseSourceType = openCodeSqliteSourceType(dbPath, env, options);
7066
8956
  const rowsById = new Map((sessionRows || []).map((row) => [String(row.id || ""), row]).filter(([id]) => id));
8957
+ const messagesBySession = groupRowsBy(messageRows, "session_id");
7067
8958
  const desktopHints = openCodeDesktopSessionHints(env);
7068
8959
  const sourceTypes = new Map();
7069
8960
  const hintFiles = new Map();
@@ -7088,7 +8979,7 @@ function openCodeSqliteSessionClassifications(sessionRows, dbPath, env = process
7088
8979
  return parentSourceType;
7089
8980
  }
7090
8981
  }
7091
- const sourceType = openCodeSqliteRowSourceType(row, dbPath, env, options, baseSourceType);
8982
+ const sourceType = openCodeSqliteRowSourceType(row, dbPath, env, options, baseSourceType, messagesBySession.get(id) || []);
7092
8983
  sourceTypes.set(id, sourceType);
7093
8984
  return sourceType;
7094
8985
  };
@@ -7096,10 +8987,10 @@ function openCodeSqliteSessionClassifications(sessionRows, dbPath, env = process
7096
8987
  return { sourceTypes, hintFiles };
7097
8988
  }
7098
8989
 
7099
- function openCodeSqliteRowSourceType(row, dbPath, env = process.env, options = {}, baseSourceType = openCodeSqliteSourceType(dbPath, env, options)) {
8990
+ function openCodeSqliteRowSourceType(row, dbPath, env = process.env, options = {}, baseSourceType = openCodeSqliteSourceType(dbPath, env, options), messageRows = []) {
7100
8991
  if (baseSourceType === "opencode-desktop-sqlite-history") return baseSourceType;
7101
8992
  if (!pathInsideAny(dbPath, openCodeCliDataRoots(env)) && !OPENCODE_SOURCE_KINDS.has(options.openCodeKind)) return baseSourceType;
7102
- if (openCodeSqliteRowHasCliMetadata(row)) return "opencode-cli-sqlite-history";
8993
+ if (openCodeSqliteRowHasCliMetadata(row) || openCodeSqliteMessagesHaveCliMetadata(messageRows)) return "opencode-cli-sqlite-history";
7103
8994
  if (openCodeSqliteRowLooksWeb(row, dbPath, env)) return "opencode-web-sqlite-history";
7104
8995
  return baseSourceType;
7105
8996
  }
@@ -7108,6 +8999,13 @@ function openCodeSqliteRowHasCliMetadata(row) {
7108
8999
  return Boolean(firstString(row?.agent, row?.model));
7109
9000
  }
7110
9001
 
9002
+ function openCodeSqliteMessagesHaveCliMetadata(rows = []) {
9003
+ return rows.some((row) => {
9004
+ const data = parseJsonObject(row?.data);
9005
+ return Boolean(firstString(data.agent, data.mode, data.path?.cwd, data.path?.root));
9006
+ });
9007
+ }
9008
+
7111
9009
  function openCodeSqliteRowLooksWeb(row, dbPath, env = process.env) {
7112
9010
  const version = firstString(row?.version);
7113
9011
  if (!version || version === "local") return false;
@@ -7171,7 +9069,7 @@ function readOpenCodeSqliteMessageRows(dbPath, sessionIds = []) {
7171
9069
  "session_id",
7172
9070
  sqliteSelectMaybe(columns, "message", "time_created"),
7173
9071
  sqliteSelectMaybe(columns, "message", "time_updated"),
7174
- sqliteSelectMaybe(columns, "message", "data")
9072
+ openCodeSqliteMessageDataSelect(dbPath, columns)
7175
9073
  ];
7176
9074
  return readOpenCodeSqliteRowsForSessionIds(
7177
9075
  dbPath,
@@ -7182,6 +9080,20 @@ function readOpenCodeSqliteMessageRows(dbPath, sessionIds = []) {
7182
9080
  );
7183
9081
  }
7184
9082
 
9083
+ function openCodeSqliteMessageDataSelect(dbPath, columns) {
9084
+ if (!columns.has("data")) return "null as data";
9085
+ if (!sqliteJsonFunctionsAvailable(dbPath)) return "message.data";
9086
+ return "case when json_valid(message.data) then json_remove(message.data, '$.summary') else message.data end as data";
9087
+ }
9088
+
9089
+ function sqliteJsonFunctionsAvailable(dbPath) {
9090
+ try {
9091
+ return readSqliteJson(dbPath, "select json_valid('{\"ok\":true}') as ok", "SQLite JSON function check")[0]?.ok === 1;
9092
+ } catch {
9093
+ return false;
9094
+ }
9095
+ }
9096
+
7185
9097
  function readOpenCodeSqlitePartRows(dbPath, sessionIds = []) {
7186
9098
  const columns = sqliteTableColumns(dbPath, "part");
7187
9099
  if (!columns.has("id") || !columns.has("message_id") || !columns.has("session_id")) return [];
@@ -9136,6 +11048,22 @@ function fileFingerprint(file, stat = safeStat(file)) {
9136
11048
  return `${file}:${stat?.size || 0}:${Math.floor(stat?.mtimeMs || 0)}`;
9137
11049
  }
9138
11050
 
11051
+ function filesFingerprint(files) {
11052
+ return (files || [])
11053
+ .map((file) => fileFingerprint(file, safeStat(file)))
11054
+ .sort((a, b) => a.localeCompare(b))
11055
+ .join("|");
11056
+ }
11057
+
11058
+ function latestFileMtimeMs(files, primaryStat = null) {
11059
+ let latest = primaryStat?.mtimeMs || 0;
11060
+ for (const file of files || []) {
11061
+ const stat = safeStat(file);
11062
+ if (stat?.mtimeMs && stat.mtimeMs > latest) latest = stat.mtimeMs;
11063
+ }
11064
+ return latest;
11065
+ }
11066
+
9139
11067
  function fileSha256(file) {
9140
11068
  return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
9141
11069
  }
@@ -9259,6 +11187,7 @@ module.exports = {
9259
11187
  importWebChat,
9260
11188
  normalizeEventRole,
9261
11189
  parseClaudeDesktopSessionFile,
11190
+ parseClaudeSubagentDefinitionFile,
9262
11191
  parseAgentJsonl,
9263
11192
  parseJsonlHistoryFile,
9264
11193
  parseSince,
@@ -9273,6 +11202,7 @@ module.exports = {
9273
11202
  readAiderSessions,
9274
11203
  readAntigravitySessions,
9275
11204
  readClineSessions,
11205
+ readClaudeSubagentDefinitions,
9276
11206
  readDevinSessionsFromDb,
9277
11207
  readGeminiCliSessions,
9278
11208
  readOpenCodeSessions,