linkshell-cli 0.3.9 → 0.3.10

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.
@@ -18,10 +18,38 @@ export interface ClaudeStoredSession {
18
18
  export interface StoredAgentTimelineItem {
19
19
  id: string;
20
20
  conversationId: string;
21
- type: "message";
22
- role: "user" | "assistant" | "system";
23
- content: Array<{ type: "text"; text: string }>;
24
- text: string;
21
+ type: "message" | "tool_call";
22
+ kind?: "tool_activity" | "command_execution" | "file_change";
23
+ itemId?: string;
24
+ role?: "user" | "assistant" | "system";
25
+ content?: Array<{ type: "text"; text: string }>;
26
+ text?: string;
27
+ toolCall?: {
28
+ id: string;
29
+ name: string;
30
+ input?: string;
31
+ output?: string;
32
+ createdAt?: number;
33
+ status: "pending" | "running" | "completed" | "failed";
34
+ };
35
+ commandExecution?: {
36
+ command?: string;
37
+ cwd?: string;
38
+ output?: string;
39
+ exitCode?: number | null;
40
+ status?: "pending" | "running" | "completed" | "failed";
41
+ };
42
+ fileChange?: {
43
+ entries: Array<{
44
+ path: string;
45
+ kind?: string;
46
+ added?: number;
47
+ removed?: number;
48
+ }>;
49
+ diff?: string;
50
+ summary?: string;
51
+ status?: "pending" | "running" | "completed" | "failed";
52
+ };
25
53
  createdAt: number;
26
54
  updatedAt?: number;
27
55
  metadata?: Record<string, unknown>;
@@ -110,6 +138,35 @@ function extractHistoryText(value: unknown): string | undefined {
110
138
  return extractHistoryText(record.content ?? record.text ?? record.message);
111
139
  }
112
140
 
141
+ function extractVisibleClaudeText(value: unknown): string | undefined {
142
+ if (!Array.isArray(value)) return extractHistoryText(value);
143
+ const text = value
144
+ .map((part) => {
145
+ if (typeof part === "string") return part;
146
+ const record = asRecord(part);
147
+ if (!record || record.type === "tool_use" || record.type === "tool_result") return "";
148
+ return typeof record.text === "string"
149
+ ? record.text
150
+ : typeof record.content === "string"
151
+ ? record.content
152
+ : "";
153
+ })
154
+ .filter(Boolean)
155
+ .join("\n")
156
+ .trim();
157
+ return text || undefined;
158
+ }
159
+
160
+ function stringifyHistoryValue(value: unknown): string | undefined {
161
+ if (value === undefined || value === null || value === "") return undefined;
162
+ if (typeof value === "string") return value;
163
+ try {
164
+ return JSON.stringify(value, null, 2);
165
+ } catch {
166
+ return String(value);
167
+ }
168
+ }
169
+
113
170
  function guessCwdFromProjectDir(projectDirName: string, fallbackCwd: string): string {
114
171
  const trimmed = projectDirName.replace(/^-+/, "");
115
172
  if (!trimmed) return resolve(fallbackCwd);
@@ -292,6 +349,127 @@ function historyMessage(
292
349
  };
293
350
  }
294
351
 
352
+ function claudeToolName(name: string | undefined): string {
353
+ if (!name) return "工具";
354
+ if (name === "Bash") return "命令";
355
+ if (isClaudeFileTool(name)) return "文件修改";
356
+ return name;
357
+ }
358
+
359
+ function isClaudeFileTool(name: string | undefined): boolean {
360
+ return name === "Edit" || name === "MultiEdit" || name === "Write" || name === "NotebookEdit";
361
+ }
362
+
363
+ function claudeToolKind(name: string | undefined): "tool_activity" | "command_execution" | "file_change" {
364
+ if (name === "Bash") return "command_execution";
365
+ if (isClaudeFileTool(name)) return "file_change";
366
+ return "tool_activity";
367
+ }
368
+
369
+ function claudeFileEntry(name: string | undefined, input: Record<string, unknown> | undefined): StoredAgentTimelineItem["fileChange"] | undefined {
370
+ if (!isClaudeFileTool(name) || !input) return undefined;
371
+ const rawPath = input.file_path ?? input.path ?? input.notebook_path;
372
+ if (typeof rawPath !== "string" || !rawPath.trim()) return undefined;
373
+ const path = rawPath.trim();
374
+ const kind = name === "Write" ? "create" : "update";
375
+ const added = typeof input.new_string === "string"
376
+ ? input.new_string.split(/\r?\n/).filter((line) => line.length > 0).length
377
+ : typeof input.content === "string"
378
+ ? input.content.split(/\r?\n/).filter((line) => line.length > 0).length
379
+ : Array.isArray(input.edits)
380
+ ? input.edits.length
381
+ : undefined;
382
+ const removed = typeof input.old_string === "string"
383
+ ? input.old_string.split(/\r?\n/).filter((line) => line.length > 0).length
384
+ : undefined;
385
+ const entry: NonNullable<StoredAgentTimelineItem["fileChange"]>["entries"][number] = { path, kind };
386
+ if (added && added > 0) entry.added = added;
387
+ if (removed && removed > 0) entry.removed = removed;
388
+ return {
389
+ entries: [entry],
390
+ summary: [kind, path].filter(Boolean).join(" "),
391
+ status: "running",
392
+ };
393
+ }
394
+
395
+ function claudeCommandExecution(input: Record<string, unknown> | undefined): StoredAgentTimelineItem["commandExecution"] | undefined {
396
+ if (!input) return undefined;
397
+ const command = typeof input.command === "string" ? input.command : undefined;
398
+ const cwd = typeof input.cwd === "string" ? input.cwd : undefined;
399
+ if (!command && !cwd) return undefined;
400
+ return { command, cwd, status: "running" };
401
+ }
402
+
403
+ function createClaudeToolItem(
404
+ conversationId: string,
405
+ toolUseId: string,
406
+ name: string | undefined,
407
+ input: Record<string, unknown> | undefined,
408
+ createdAt: number,
409
+ ): StoredAgentTimelineItem {
410
+ const kind = claudeToolKind(name);
411
+ const inputText = stringifyHistoryValue(input);
412
+ const fileChange = claudeFileEntry(name, input);
413
+ return {
414
+ id: `history-tool:${toolUseId}`,
415
+ conversationId,
416
+ type: "tool_call",
417
+ kind,
418
+ itemId: toolUseId,
419
+ toolCall: {
420
+ id: toolUseId,
421
+ name: claudeToolName(name),
422
+ input: inputText,
423
+ createdAt,
424
+ status: "running",
425
+ },
426
+ commandExecution: name === "Bash" ? claudeCommandExecution(input) : undefined,
427
+ fileChange,
428
+ text: fileChange ? `已编辑 ${fileChange.entries.length} 个文件` : undefined,
429
+ createdAt,
430
+ updatedAt: createdAt,
431
+ metadata: { source: "device-history", provider: "claude", toolName: name },
432
+ };
433
+ }
434
+
435
+ function completeClaudeToolItem(
436
+ itemsById: Map<string, StoredAgentTimelineItem>,
437
+ conversationId: string,
438
+ toolUseId: string,
439
+ output: string | undefined,
440
+ failed: boolean,
441
+ createdAt: number,
442
+ ): void {
443
+ const id = `history-tool:${toolUseId}`;
444
+ const existing = itemsById.get(id);
445
+ const status = failed ? "failed" : "completed";
446
+ itemsById.set(id, {
447
+ id,
448
+ conversationId,
449
+ type: "tool_call",
450
+ kind: existing?.kind ?? "tool_activity",
451
+ itemId: toolUseId,
452
+ toolCall: {
453
+ id: toolUseId,
454
+ name: existing?.toolCall?.name ?? "工具",
455
+ input: existing?.toolCall?.input,
456
+ output,
457
+ createdAt: existing?.toolCall?.createdAt ?? createdAt,
458
+ status,
459
+ },
460
+ commandExecution: existing?.commandExecution
461
+ ? { ...existing.commandExecution, output, status }
462
+ : undefined,
463
+ fileChange: existing?.fileChange
464
+ ? { ...existing.fileChange, summary: output ?? existing.fileChange.summary, status }
465
+ : undefined,
466
+ text: existing?.text,
467
+ createdAt: existing?.createdAt ?? createdAt,
468
+ updatedAt: createdAt,
469
+ metadata: existing?.metadata ?? { source: "device-history", provider: "claude" },
470
+ });
471
+ }
472
+
295
473
  export function loadClaudeStoredTimeline(
296
474
  sessionId: string,
297
475
  conversationId: string,
@@ -308,7 +486,7 @@ export function loadClaudeStoredTimeline(
308
486
  return { items: [] };
309
487
  }
310
488
 
311
- const items: StoredAgentTimelineItem[] = [];
489
+ const itemsById = new Map<string, StoredAgentTimelineItem>();
312
490
  let index = 0;
313
491
  for (const line of readHistorySample(filePath, statSize).split(/\r?\n/)) {
314
492
  const trimmed = line.trim();
@@ -326,20 +504,50 @@ export function loadClaudeStoredTimeline(
326
504
  : undefined;
327
505
  const role = rawRole === "assistant" ? "assistant" : rawRole === "user" ? "user" : undefined;
328
506
  if (!role) continue;
329
- const text = extractHistoryText(message?.content ?? record.content ?? record.text);
330
- if (!text) continue;
331
507
  const createdAt =
332
508
  parseTimestamp(record.timestamp ?? record.createdAt ?? record.created_at) ??
333
509
  statMtime + index;
334
- items.push(historyMessage(conversationId, index, role, text, createdAt, sessionId));
335
- index += 1;
510
+ const content = message?.content ?? record.content ?? record.text;
511
+ const text = extractVisibleClaudeText(content);
512
+ if (text) {
513
+ itemsById.set(
514
+ `history:${sessionId}:${index}`,
515
+ historyMessage(conversationId, index, role, text, createdAt, sessionId),
516
+ );
517
+ index += 1;
518
+ }
519
+ if (Array.isArray(content)) {
520
+ for (const part of content) {
521
+ const block = asRecord(part);
522
+ if (!block) continue;
523
+ if (role === "assistant" && block.type === "tool_use") {
524
+ const toolUseId = typeof block.id === "string" ? block.id : undefined;
525
+ if (!toolUseId) continue;
526
+ const name = typeof block.name === "string" ? block.name : undefined;
527
+ const input = asRecord(block.input);
528
+ const item = createClaudeToolItem(conversationId, toolUseId, name, input, createdAt);
529
+ itemsById.set(item.id, item);
530
+ } else if (role === "user" && block.type === "tool_result") {
531
+ const toolUseId = typeof block.tool_use_id === "string" ? block.tool_use_id : undefined;
532
+ if (!toolUseId) continue;
533
+ completeClaudeToolItem(
534
+ itemsById,
535
+ conversationId,
536
+ toolUseId,
537
+ extractHistoryText(block.content),
538
+ block.is_error === true,
539
+ createdAt,
540
+ );
541
+ }
542
+ }
543
+ }
336
544
  } catch {
337
545
  // Ignore malformed or partial JSONL lines in the history window.
338
546
  }
339
547
  }
340
548
 
341
549
  return {
342
- items: items
550
+ items: [...itemsById.values()]
343
551
  .sort((a, b) => a.createdAt - b.createdAt)
344
552
  .slice(-MAX_HISTORY_ITEMS),
345
553
  };
@@ -19,10 +19,38 @@ export interface CodexStoredSession {
19
19
  export interface StoredAgentTimelineItem {
20
20
  id: string;
21
21
  conversationId: string;
22
- type: "message";
23
- role: "user" | "assistant" | "system";
24
- content: Array<{ type: "text"; text: string }>;
25
- text: string;
22
+ type: "message" | "tool_call";
23
+ kind?: "tool_activity" | "command_execution" | "file_change";
24
+ itemId?: string;
25
+ role?: "user" | "assistant" | "system";
26
+ content?: Array<{ type: "text"; text: string }>;
27
+ text?: string;
28
+ toolCall?: {
29
+ id: string;
30
+ name: string;
31
+ input?: string;
32
+ output?: string;
33
+ createdAt?: number;
34
+ status: "pending" | "running" | "completed" | "failed";
35
+ };
36
+ commandExecution?: {
37
+ command?: string;
38
+ cwd?: string;
39
+ output?: string;
40
+ exitCode?: number | null;
41
+ status?: "pending" | "running" | "completed" | "failed";
42
+ };
43
+ fileChange?: {
44
+ entries: Array<{
45
+ path: string;
46
+ kind?: string;
47
+ added?: number;
48
+ removed?: number;
49
+ }>;
50
+ diff?: string;
51
+ summary?: string;
52
+ status?: "pending" | "running" | "completed" | "failed";
53
+ };
26
54
  createdAt: number;
27
55
  updatedAt?: number;
28
56
  metadata?: Record<string, unknown>;
@@ -276,6 +304,40 @@ function normalizeHistoryText(value: unknown): string | undefined {
276
304
  return normalizeHistoryText(record.content ?? record.text ?? record.message);
277
305
  }
278
306
 
307
+ function stringifyHistoryValue(value: unknown): string | undefined {
308
+ if (value === undefined || value === null || value === "") return undefined;
309
+ if (typeof value === "string") return value;
310
+ try {
311
+ return JSON.stringify(value, null, 2);
312
+ } catch {
313
+ return String(value);
314
+ }
315
+ }
316
+
317
+ function visibleCodexRole(value: unknown): "user" | "assistant" | undefined {
318
+ return value === "user" || value === "assistant" ? value : undefined;
319
+ }
320
+
321
+ function isInjectedCodexContext(text: string): boolean {
322
+ const trimmed = text.trimStart();
323
+ return (
324
+ trimmed.startsWith("<permissions instructions>") ||
325
+ trimmed.startsWith("<app-context>") ||
326
+ trimmed.startsWith("<environment_context>") ||
327
+ trimmed.startsWith("<skill>") ||
328
+ trimmed.startsWith("<turn_aborted>") ||
329
+ trimmed.startsWith("<developer") ||
330
+ trimmed.startsWith("# AGENTS.md instructions") ||
331
+ trimmed.startsWith("AGENTS.md instructions for ") ||
332
+ trimmed.includes("\n# AGENTS.md\n") ||
333
+ trimmed.includes("\n<INSTRUCTIONS>")
334
+ );
335
+ }
336
+
337
+ function messageDedupeKey(role: "user" | "assistant" | "system", text: string): string {
338
+ return `${role}:${text.replace(/\s+/g, " ").trim().slice(0, 400)}`;
339
+ }
340
+
279
341
  function historyMessage(
280
342
  conversationId: string,
281
343
  index: number,
@@ -297,6 +359,207 @@ function historyMessage(
297
359
  };
298
360
  }
299
361
 
362
+ function historyToolName(name: string | undefined): string {
363
+ if (!name) return "工具";
364
+ if (name.endsWith("exec_command") || name.endsWith("write_stdin")) return "命令";
365
+ if (name === "apply_patch") return "文件修改";
366
+ return name;
367
+ }
368
+
369
+ function historyToolKind(name: string | undefined): "tool_activity" | "command_execution" {
370
+ return name?.endsWith("exec_command") || name?.endsWith("write_stdin") ? "command_execution" : "tool_activity";
371
+ }
372
+
373
+ function commandFromCodexTool(name: string | undefined, rawInput: unknown, output?: string): StoredAgentTimelineItem["commandExecution"] {
374
+ if (!name?.endsWith("exec_command") && !name?.endsWith("write_stdin")) return undefined;
375
+ let input = asRecord(rawInput);
376
+ if (!input && typeof rawInput === "string" && rawInput.trim().startsWith("{")) {
377
+ try {
378
+ input = asRecord(JSON.parse(rawInput));
379
+ } catch {
380
+ input = undefined;
381
+ }
382
+ }
383
+ const command = typeof input?.cmd === "string"
384
+ ? input.cmd
385
+ : typeof input?.chars === "string"
386
+ ? input.chars
387
+ : undefined;
388
+ const cwd = typeof input?.workdir === "string" ? input.workdir : undefined;
389
+ if (!command && !cwd && !output) return undefined;
390
+ return { command, cwd, output, status: output === undefined ? "running" : "completed" };
391
+ }
392
+
393
+ function upsertHistoryTool(
394
+ itemsById: Map<string, StoredAgentTimelineItem>,
395
+ conversationId: string,
396
+ callId: string,
397
+ name: string | undefined,
398
+ rawInput: unknown,
399
+ input: string | undefined,
400
+ createdAt: number,
401
+ ): void {
402
+ const id = `history-tool:${callId}`;
403
+ const existing = itemsById.get(id);
404
+ const commandExecution = commandFromCodexTool(name, rawInput, existing?.commandExecution?.output);
405
+ itemsById.set(id, {
406
+ id,
407
+ conversationId,
408
+ type: "tool_call",
409
+ kind: historyToolKind(name),
410
+ itemId: callId,
411
+ toolCall: {
412
+ id: callId,
413
+ name: historyToolName(name),
414
+ input: input ?? existing?.toolCall?.input,
415
+ output: existing?.toolCall?.output,
416
+ createdAt: existing?.toolCall?.createdAt ?? createdAt,
417
+ status: existing?.toolCall?.status ?? "running",
418
+ },
419
+ commandExecution: commandExecution ?? existing?.commandExecution,
420
+ createdAt: existing?.createdAt ?? createdAt,
421
+ updatedAt: createdAt,
422
+ metadata: { source: "device-history", provider: "codex" },
423
+ });
424
+ }
425
+
426
+ function completeHistoryTool(
427
+ itemsById: Map<string, StoredAgentTimelineItem>,
428
+ conversationId: string,
429
+ callId: string,
430
+ output: string | undefined,
431
+ createdAt: number,
432
+ ): void {
433
+ const id = `history-tool:${callId}`;
434
+ const existing = itemsById.get(id);
435
+ const commandExecution = existing?.commandExecution
436
+ ? { ...existing.commandExecution, output, status: "completed" as const }
437
+ : undefined;
438
+ itemsById.set(id, {
439
+ id,
440
+ conversationId,
441
+ type: "tool_call",
442
+ kind: existing?.kind ?? "tool_activity",
443
+ itemId: callId,
444
+ toolCall: {
445
+ id: callId,
446
+ name: existing?.toolCall?.name ?? "工具",
447
+ input: existing?.toolCall?.input,
448
+ output,
449
+ createdAt: existing?.toolCall?.createdAt ?? createdAt,
450
+ status: "completed",
451
+ },
452
+ commandExecution,
453
+ createdAt: existing?.createdAt ?? createdAt,
454
+ updatedAt: createdAt,
455
+ metadata: { source: "device-history", provider: "codex" },
456
+ });
457
+ }
458
+
459
+ function relativeHistoryPath(path: string, cwd: string): string {
460
+ const normalized = path.replace(/\\/g, "/").replace(/^["']|["']$/g, "");
461
+ const normalizedCwd = cwd.replace(/\\/g, "/").replace(/\/+$/, "");
462
+ return normalized.startsWith(`${normalizedCwd}/`)
463
+ ? normalized.slice(normalizedCwd.length + 1)
464
+ : normalized;
465
+ }
466
+
467
+ function countDiffLines(diff: string | undefined): { added: number; removed: number } {
468
+ if (!diff) return { added: 0, removed: 0 };
469
+ let added = 0;
470
+ let removed = 0;
471
+ for (const line of diff.split("\n")) {
472
+ if (line.startsWith("+") && !line.startsWith("+++")) added += 1;
473
+ else if (line.startsWith("-") && !line.startsWith("---")) removed += 1;
474
+ }
475
+ return { added, removed };
476
+ }
477
+
478
+ function countContentLines(content: unknown): number {
479
+ if (typeof content !== "string") return 0;
480
+ if (!content) return 0;
481
+ return content.split(/\r?\n/).filter((line) => line.length > 0).length;
482
+ }
483
+
484
+ function changeKind(value: string | undefined): string | undefined {
485
+ if (value === "add") return "create";
486
+ if (value === "delete") return "delete";
487
+ if (value === "update" || value === "move") return "update";
488
+ return value;
489
+ }
490
+
491
+ function patchApplyFileChange(
492
+ conversationId: string,
493
+ payload: Record<string, unknown>,
494
+ cwd: string,
495
+ createdAt: number,
496
+ ): StoredAgentTimelineItem | undefined {
497
+ const changes = asRecord(payload.changes);
498
+ if (!changes) return undefined;
499
+ const entries: NonNullable<StoredAgentTimelineItem["fileChange"]>["entries"] = [];
500
+ const diffParts: string[] = [];
501
+ for (const [absolutePath, rawChange] of Object.entries(changes)) {
502
+ const change = asRecord(rawChange);
503
+ if (!change) continue;
504
+ const path = relativeHistoryPath(absolutePath, cwd);
505
+ const kind = changeKind(typeof change.type === "string" ? change.type : undefined);
506
+ const diff = typeof change.unified_diff === "string" ? change.unified_diff : undefined;
507
+ const stats = countDiffLines(diff);
508
+ const added = stats.added || (kind === "create" ? countContentLines(change.content) : 0);
509
+ const removed = stats.removed;
510
+ const entry: NonNullable<StoredAgentTimelineItem["fileChange"]>["entries"][number] = { path };
511
+ if (kind) entry.kind = kind;
512
+ if (added > 0) entry.added = added;
513
+ if (removed > 0) entry.removed = removed;
514
+ entries.push(entry);
515
+ if (diff) {
516
+ diffParts.push([
517
+ `Path: ${path}`,
518
+ kind ? `Kind: ${kind}` : undefined,
519
+ `Totals: +${added} -${removed}`,
520
+ "",
521
+ "```diff",
522
+ diff.trimEnd(),
523
+ "```",
524
+ ].filter((line): line is string => line !== undefined).join("\n"));
525
+ }
526
+ }
527
+ if (entries.length === 0) return undefined;
528
+ const callId = typeof payload.call_id === "string" ? payload.call_id : `patch-${createdAt}`;
529
+ const status = payload.success === false ? "failed" : "completed";
530
+ const totalAdded = entries.reduce((sum, entry) => sum + (entry.added ?? 0), 0);
531
+ const totalRemoved = entries.reduce((sum, entry) => sum + (entry.removed ?? 0), 0);
532
+ const summary = entries
533
+ .map((entry) => [entry.kind, entry.path].filter(Boolean).join(" "))
534
+ .join("\n");
535
+ const diff = diffParts.length > 0 ? diffParts.join("\n\n---\n\n") : undefined;
536
+ return {
537
+ id: `history-file-change:${callId}`,
538
+ conversationId,
539
+ type: "tool_call",
540
+ kind: "file_change",
541
+ itemId: callId,
542
+ toolCall: {
543
+ id: callId,
544
+ name: "文件修改",
545
+ input: summary,
546
+ output: diff ?? (typeof payload.stdout === "string" ? payload.stdout : undefined),
547
+ createdAt,
548
+ status,
549
+ },
550
+ fileChange: {
551
+ entries,
552
+ diff,
553
+ summary,
554
+ status,
555
+ },
556
+ text: `已编辑 ${entries.length} 个文件 +${totalAdded} -${totalRemoved}`,
557
+ createdAt,
558
+ updatedAt: createdAt,
559
+ metadata: { source: "device-history", provider: "codex" },
560
+ };
561
+ }
562
+
300
563
  export function listCodexStoredSessions(inputCwd: string): { sessions: CodexStoredSession[] } {
301
564
  const root = join(homedir(), ".codex");
302
565
  if (!existsSync(root)) return { sessions: [] };
@@ -360,7 +623,8 @@ export function loadCodexStoredTimeline(
360
623
  return { items: [] };
361
624
  }
362
625
 
363
- const items: StoredAgentTimelineItem[] = [];
626
+ const itemsById = new Map<string, StoredAgentTimelineItem>();
627
+ const seenMessages = new Set<string>();
364
628
  let index = 0;
365
629
  for (const line of readHistorySample(file.path, statSize).split(/\r?\n/)) {
366
630
  const trimmed = line.trim();
@@ -368,21 +632,76 @@ export function loadCodexStoredTimeline(
368
632
  try {
369
633
  const entry = asRecord(JSON.parse(trimmed));
370
634
  const payload = asRecord(entry?.payload);
371
- if (entry?.type !== "event_msg" || !payload) continue;
372
- const eventType = typeof payload.type === "string" ? payload.type : undefined;
373
635
  const createdAt =
374
- parseTimestamp(entry.timestamp ?? payload.created_at ?? payload.started_at ?? payload.completed_at) ??
636
+ parseTimestamp(entry?.timestamp ?? payload?.created_at ?? payload?.started_at ?? payload?.completed_at) ??
375
637
  statMtime + index;
376
- if (eventType === "user_message") {
377
- const text = normalizeHistoryText(payload.message);
378
- if (!text || text.startsWith("<turn_aborted>")) continue;
379
- items.push(historyMessage(conversationId, index, "user", text, createdAt, sessionId));
380
- index += 1;
381
- } else if (eventType === "agent_message") {
638
+ if (entry?.type === "event_msg" && payload) {
639
+ const eventType = typeof payload.type === "string" ? payload.type : undefined;
640
+ if (eventType === "user_message") {
641
+ const text = normalizeHistoryText(payload.message);
642
+ if (!text || isInjectedCodexContext(text)) continue;
643
+ const dedupeKey = messageDedupeKey("user", text);
644
+ if (seenMessages.has(dedupeKey)) continue;
645
+ seenMessages.add(dedupeKey);
646
+ itemsById.set(`history:${sessionId}:${index}`, historyMessage(conversationId, index, "user", text, createdAt, sessionId));
647
+ index += 1;
648
+ } else if (eventType === "agent_message") {
649
+ const text = normalizeHistoryText(payload.message);
650
+ if (!text || isInjectedCodexContext(text)) continue;
651
+ const dedupeKey = messageDedupeKey("assistant", text);
652
+ if (seenMessages.has(dedupeKey)) continue;
653
+ seenMessages.add(dedupeKey);
654
+ itemsById.set(`history:${sessionId}:${index}`, historyMessage(conversationId, index, "assistant", text, createdAt, sessionId));
655
+ index += 1;
656
+ } else if (eventType === "patch_apply_end") {
657
+ const item = patchApplyFileChange(conversationId, payload, inputCwd, createdAt);
658
+ if (item) itemsById.set(item.id, item);
659
+ }
660
+ continue;
661
+ }
662
+
663
+ if (entry?.type !== "response_item" || !payload) continue;
664
+ const responseType = typeof payload.type === "string" ? payload.type : undefined;
665
+ if (responseType === "message") {
666
+ const role = visibleCodexRole(payload.role);
667
+ if (!role) continue;
382
668
  const text = normalizeHistoryText(payload.message);
383
- if (!text) continue;
384
- items.push(historyMessage(conversationId, index, "assistant", text, createdAt, sessionId));
669
+ const contentText = text ?? normalizeHistoryText(payload.content);
670
+ if (!contentText || isInjectedCodexContext(contentText)) continue;
671
+ const dedupeKey = messageDedupeKey(role, contentText);
672
+ if (seenMessages.has(dedupeKey)) continue;
673
+ seenMessages.add(dedupeKey);
674
+ itemsById.set(`history:${sessionId}:${index}`, historyMessage(conversationId, index, role, contentText, createdAt, sessionId));
385
675
  index += 1;
676
+ continue;
677
+ }
678
+ if (responseType === "function_call") {
679
+ const callId = typeof payload.call_id === "string"
680
+ ? payload.call_id
681
+ : typeof payload.id === "string"
682
+ ? payload.id
683
+ : undefined;
684
+ if (!callId) continue;
685
+ const name = typeof payload.name === "string" ? payload.name : undefined;
686
+ upsertHistoryTool(
687
+ itemsById,
688
+ conversationId,
689
+ callId,
690
+ name,
691
+ payload.arguments,
692
+ stringifyHistoryValue(payload.arguments),
693
+ createdAt,
694
+ );
695
+ continue;
696
+ }
697
+ if (responseType === "function_call_output") {
698
+ const callId = typeof payload.call_id === "string"
699
+ ? payload.call_id
700
+ : typeof payload.id === "string"
701
+ ? payload.id
702
+ : undefined;
703
+ if (!callId) continue;
704
+ completeHistoryTool(itemsById, conversationId, callId, stringifyHistoryValue(payload.output), createdAt);
386
705
  }
387
706
  } catch {
388
707
  // Ignore malformed or partial JSONL lines in the history window.
@@ -390,7 +709,7 @@ export function loadCodexStoredTimeline(
390
709
  }
391
710
 
392
711
  return {
393
- items: items
712
+ items: [...itemsById.values()]
394
713
  .sort((a, b) => a.createdAt - b.createdAt)
395
714
  .slice(-MAX_HISTORY_ITEMS),
396
715
  };