mixdog 1.4.0 → 1.4.1

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.
Files changed (236) hide show
  1. package/package.json +1 -1
  2. package/scripts/check-voice-runtime-inputs.mjs +25 -0
  3. package/scripts/gate-local.mjs +12 -3
  4. package/scripts/latest-guard.mjs +21 -9
  5. package/scripts/lib/isolated-test-env.mjs +10 -5
  6. package/scripts/lib/run-node-tests.mjs +37 -37
  7. package/scripts/lib/scoped-temp.d.mts +16 -0
  8. package/scripts/lib/scoped-temp.mjs +123 -0
  9. package/scripts/lib/stale-temp.mjs +44 -0
  10. package/scripts/post-release-check.mjs +1 -6
  11. package/scripts/pptx-kit-index.mjs +17 -4
  12. package/scripts/suite-health.mjs +10 -1
  13. package/scripts/test-data-dir.mjs +10 -7
  14. package/src/app.mjs +4 -0
  15. package/src/defaults/shipped-history.json +5 -0
  16. package/src/defaults/skills/browser-use/SKILL.md +6 -4
  17. package/src/defaults/skills/computer-use/SKILL.md +4 -2
  18. package/src/lib/rules-builder.cjs +29 -12
  19. package/src/output-styles/common.md +2 -2
  20. package/src/output-styles/simple.md +1 -1
  21. package/src/rules/agent/AGENT.md +5 -4
  22. package/src/rules/lead/LEAD.md +10 -5
  23. package/src/rules/shared/10-tool-workflow.md +3 -1
  24. package/src/rules/shared/45-destructive.md +0 -2
  25. package/src/rules/shared/50-execution.md +2 -0
  26. package/src/rules/shared/70-delivery.md +7 -9
  27. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +29 -6
  28. package/src/runtime/agent/orchestrator/cache-break-trace.mjs +2 -0
  29. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +1 -1
  30. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +3 -1
  31. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +5 -2
  32. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +11 -0
  33. package/src/runtime/agent/orchestrator/providers/anthropic-api-recovery.mjs +1 -1
  34. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +7 -3
  35. package/src/runtime/agent/orchestrator/providers/anthropic-api-transport.mjs +8 -2
  36. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens-recovery.mjs +6 -2
  37. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +2 -2
  38. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +3 -1
  39. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +14 -3
  40. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +6 -0
  41. package/src/runtime/agent/orchestrator/providers/anthropic-turn-result.mjs +2 -2
  42. package/src/runtime/agent/orchestrator/providers/command-code-oauth-login.mjs +1 -10
  43. package/src/runtime/agent/orchestrator/providers/cursor.mjs +3 -0
  44. package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +1 -1
  45. package/src/runtime/agent/orchestrator/providers/devin.mjs +3 -0
  46. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +210 -20
  47. package/src/runtime/agent/orchestrator/providers/lib/anthropic-prefix-trace.mjs +76 -11
  48. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +45 -6
  49. package/src/runtime/agent/orchestrator/providers/lib/first-output-window.mjs +58 -0
  50. package/src/runtime/agent/orchestrator/providers/lib/listen-on.mjs +14 -0
  51. package/src/runtime/agent/orchestrator/providers/mimo-oauth-login.mjs +1 -10
  52. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -0
  53. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/chunk-events.mjs +9 -2
  54. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +7 -0
  55. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +14 -3
  56. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +4 -2
  57. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +1 -0
  58. package/src/runtime/agent/orchestrator/providers/openai-compat-stream-common.mjs +5 -3
  59. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +16 -3
  60. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +1 -0
  61. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events/output-items.mjs +5 -2
  62. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events.mjs +13 -3
  63. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +13 -2
  64. package/src/runtime/agent/orchestrator/providers/openai-http-sse-watchdogs.mjs +81 -42
  65. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +8 -0
  66. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +136 -5
  67. package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +20 -12
  68. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +7 -1
  69. package/src/runtime/agent/orchestrator/providers/openai-ws-watchdogs.mjs +48 -22
  70. package/src/runtime/agent/orchestrator/providers/registry.mjs +12 -0
  71. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +41 -13
  72. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +7 -6
  73. package/src/runtime/agent/orchestrator/runtime-core/session-text.mjs +7 -3
  74. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +3 -1
  75. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +5 -2
  76. package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +1 -3
  77. package/src/runtime/agent/orchestrator/session/cache-warm.mjs +8 -2
  78. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +4 -1
  79. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +1 -8
  80. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +23 -0
  81. package/src/runtime/agent/orchestrator/session/compact/observation-supersession.mjs +103 -23
  82. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +160 -36
  83. package/src/runtime/agent/orchestrator/session/context-utils.mjs +13 -1
  84. package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +163 -22
  85. package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +132 -17
  86. package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +38 -7
  87. package/src/runtime/agent/orchestrator/session/conversation-import/imported-session.mjs +3 -0
  88. package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +90 -22
  89. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +33 -3
  90. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +7 -2
  91. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -1
  92. package/src/runtime/agent/orchestrator/session/loop/provider-tool-exclusions.mjs +13 -5
  93. package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +14 -0
  94. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +13 -1
  95. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +2 -2
  96. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +39 -21
  97. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -3
  98. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +3 -1
  99. package/src/runtime/agent/orchestrator/session/manager/owner-route-fallbacks.mjs +2 -2
  100. package/src/runtime/agent/orchestrator/session/manager/route-fallback-chain.mjs +2 -1
  101. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +27 -9
  102. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +61 -6
  103. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +19 -8
  104. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +12 -5
  105. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +3 -3
  106. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-context.mjs +4 -0
  107. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +3 -0
  108. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +9 -0
  109. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +5 -1
  110. package/src/runtime/agent/orchestrator/session/prompt-cache-touch-store.mjs +30 -7
  111. package/src/runtime/agent/orchestrator/session/restart-prefix-parity.fixture.mjs +154 -0
  112. package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +7 -0
  113. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +4 -0
  114. package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +15 -0
  115. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +37 -8
  116. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +9 -1
  117. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +61 -2
  118. package/src/runtime/agent/orchestrator/stall-policy.mjs +23 -0
  119. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +2 -2
  120. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +4 -2
  121. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +6 -1
  122. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +46 -20
  123. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +7 -1
  124. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +31 -5
  125. package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +22 -8
  126. package/src/runtime/agent/orchestrator/tools/builtin/lib/scoped-search-flight.mjs +9 -5
  127. package/src/runtime/agent/orchestrator/tools/builtin/read-single-stream.mjs +7 -1
  128. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +6 -3
  129. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-dispatch.mjs +1 -1
  130. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-publication.mjs +23 -12
  131. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-snapshot.mjs +6 -1
  132. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +1 -8
  133. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/federation.mjs +3 -1
  134. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
  135. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
  136. package/src/runtime/agent/orchestrator/tools/lib/isolated-test-env.mjs +4 -3
  137. package/src/runtime/agent/orchestrator/tools/lib/shell-warm-standby.mjs +26 -4
  138. package/src/runtime/channels/lib/webhook/signature.mjs +4 -1
  139. package/src/runtime/channels/lib/whisper-port.mjs +10 -1
  140. package/src/runtime/computer-bridge/action-schema.mjs +2 -2
  141. package/src/runtime/computer-bridge/core-actions.mjs +2 -2
  142. package/src/runtime/computer-bridge/error-recovery.mjs +5 -4
  143. package/src/runtime/computer-bridge/linux-action-capabilities.mjs +24 -5
  144. package/src/runtime/computer-bridge/linux-readiness.d.mts +7 -2
  145. package/src/runtime/local-provider/server-process.mjs +2 -1
  146. package/src/runtime/media/adapters/gemini-video.mjs +4 -1
  147. package/src/runtime/media/adapters/xai-media.mjs +2 -1
  148. package/src/runtime/memory/lib/embedding-worker.mjs +6 -3
  149. package/src/runtime/memory/lib/memory-action-handlers/cycle-backfill-action.mjs +1 -7
  150. package/src/runtime/memory/lib/memory-chunk-quality.mjs +1 -6
  151. package/src/runtime/office/authoring/pdf-author-action.mjs +1 -6
  152. package/src/runtime/office/design/design-art-direction.mjs +29 -27
  153. package/src/runtime/office/pdf/pdf-writer.mjs +6 -1
  154. package/src/runtime/office/portable/portable-snapshot-docx.mjs +7 -1
  155. package/src/runtime/office/portable/portable-snapshot-shared.mjs +6 -1
  156. package/src/runtime/office/quality/assurance-rendered.mjs +9 -2
  157. package/src/runtime/office/quality/assurance-structure-docx.mjs +3 -1
  158. package/src/runtime/office/quality/design-review.mjs +1 -5
  159. package/src/runtime/office/quality/document-brief.mjs +1 -5
  160. package/src/runtime/office/quality/forced-break-pages.mjs +9 -6
  161. package/src/runtime/office/quality/quality-pipeline.mjs +1 -1
  162. package/src/runtime/shared/bot-owned-work.mjs +56 -0
  163. package/src/runtime/shared/defaults-separation.mjs +1 -4
  164. package/src/runtime/shared/fetch-safe-listen.d.mts +16 -0
  165. package/src/runtime/shared/fetch-safe-listen.mjs +50 -0
  166. package/src/runtime/shared/handoff-artifacts.d.mts +2 -1
  167. package/src/runtime/shared/handoff-artifacts.mjs +18 -8
  168. package/src/runtime/shared/idle-auto-clear-gate.mjs +29 -7
  169. package/src/runtime/shared/llm/cost.mjs +5 -1
  170. package/src/runtime/shared/loopback-listener.mjs +25 -2
  171. package/src/runtime/shared/model-route-chain.d.mts +7 -2
  172. package/src/runtime/shared/model-route-chain.mjs +22 -4
  173. package/src/runtime/shared/open-url.mjs +10 -0
  174. package/src/runtime/shared/session-activity-signal.mjs +14 -0
  175. package/src/runtime/shared/session-scratch.mjs +37 -0
  176. package/src/runtime/shared/tool-card-model/agent-surface.mjs +1 -3
  177. package/src/runtime/shared/tool-card-model/card-status.mjs +15 -2
  178. package/src/runtime/shared/tool-result-summary.mjs +1 -1
  179. package/src/runtime/shared/tool-surface.mjs +3 -0
  180. package/src/runtime/shared/tool-work-units.mjs +2 -0
  181. package/src/runtime/shared/turn-snapshot-store.mjs +13 -6
  182. package/src/runtime/shared/worktrees/contract.mjs +4 -1
  183. package/src/runtime/shared/worktrees/git.mjs +15 -0
  184. package/src/runtime/shared/worktrees/manager.mjs +87 -38
  185. package/src/runtime/shared/worktrees/test-support.mjs +6 -10
  186. package/src/runtime/visualize-bridge/client.mjs +2 -0
  187. package/src/session-runtime/agent-home-delivery.mjs +41 -0
  188. package/src/session-runtime/agent-task-sessions.mjs +152 -63
  189. package/src/session-runtime/agent-tool-routing.mjs +8 -41
  190. package/src/session-runtime/boot/tools.mjs +5 -1
  191. package/src/session-runtime/bot-status.mjs +12 -6
  192. package/src/session-runtime/bot-target.mjs +38 -0
  193. package/src/session-runtime/channel-config-api.mjs +6 -1
  194. package/src/session-runtime/conversation-import/codex-titles.mjs +88 -0
  195. package/src/session-runtime/conversation-import/discovery.mjs +271 -25
  196. package/src/session-runtime/conversation-import/service.mjs +26 -5
  197. package/src/session-runtime/handoff-attachments.mjs +82 -31
  198. package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +12 -13
  199. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +1 -0
  200. package/src/session-runtime/internal-tool-executor/reaction-tool.mjs +4 -1
  201. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +8 -2
  202. package/src/session-runtime/lifecycle/cold-heir.mjs +11 -6
  203. package/src/session-runtime/lifecycle/session-catalog.mjs +9 -1
  204. package/src/session-runtime/model-route/set-route.mjs +11 -2
  205. package/src/session-runtime/provider-catalog-cache.mjs +82 -36
  206. package/src/session-runtime/provider-models/catalog-load.mjs +23 -6
  207. package/src/session-runtime/provider-models.mjs +4 -2
  208. package/src/session-runtime/resource-recall-api.mjs +6 -1
  209. package/src/session-runtime/runtime-feature-gates.mjs +5 -1
  210. package/src/session-runtime/services/agent-tool/execute.mjs +14 -9
  211. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +20 -4
  212. package/src/session-runtime/services/agent-tool/model-select.mjs +48 -7
  213. package/src/session-runtime/services/agent-tool/render.mjs +71 -17
  214. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +6 -2
  215. package/src/session-runtime/services/agent-tool/tool-def.mjs +9 -9
  216. package/src/session-runtime/services/provider-admin.mjs +2 -1
  217. package/src/session-runtime/tool-policy/policy-stage.mjs +1 -0
  218. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +5 -1
  219. package/src/session-runtime/workflow-agents-api/agent-route.mjs +15 -4
  220. package/src/session-runtime/workflow-agents-api/onboarding.mjs +3 -3
  221. package/src/session-runtime/workspace/transitions.mjs +25 -0
  222. package/src/session-runtime/workspace/worktree-tool.mjs +26 -9
  223. package/src/standalone/daemon-session-control.mjs +18 -2
  224. package/src/standalone/session-service/agent-tree/agent-turns.mjs +16 -12
  225. package/src/standalone/session-service/session-calls/session-action.mjs +10 -3
  226. package/src/standalone/session-transport/client-registry/client-grace.mjs +12 -9
  227. package/src/tui/app/app-view/floating-panel.jsx +7 -4
  228. package/src/tui/app/model-picker/route-selection/fast-parameters.mjs +1 -3
  229. package/src/tui/app/model-picker/route-selection.mjs +5 -6
  230. package/src/tui/app/provider-setup/oauth-actions.mjs +38 -20
  231. package/src/tui/app/provider-setup/provider-items.mjs +8 -2
  232. package/src/tui/app/provider-setup-picker.mjs +9 -5
  233. package/src/tui/components/QuestionPanel.jsx +13 -4
  234. package/src/tui/dist/index.mjs +75 -28
  235. package/src/tui/session/session-flow/queue/take-entries.mjs +11 -0
  236. package/src/ui/statusline-agents.mjs +1 -3
@@ -3,12 +3,21 @@
3
3
  * import turns. The active branch is followed from the last main-chain entry
4
4
  * back through `parentUuid` (and `logicalParentUuid` across a compaction), so
5
5
  * replayed or abandoned branches never duplicate. Kept: text blocks and
6
- * `tool_use` as `[ran tool: <name>]` notes; images become an omission marker;
7
- * a compaction summary only when the history it replaced is no longer linked.
8
- * Dropped: sidechains, meta entries, tool results, thinking, signatures and
9
- * Claude Code's own command/notification envelopes.
6
+ * `tool_use` as tool notes; images become an omission marker; a compaction
7
+ * summary (as an assistant recap) only when the history it replaced is gone;
8
+ * a slash command as `/name args` unless the app answered it locally; pasted
9
+ * text without the app's paste envelope. Dropped: sidechains, meta entries,
10
+ * tool results, thinking, signatures and Claude Code's own
11
+ * command-output/notification envelopes.
10
12
  */
11
- import { IMAGE_OMITTED_TEXT, IMPORT_WARNING_IMAGE_OMITTED, isWholeElement, textPart, toolNote } from './turns.mjs';
13
+ import {
14
+ IMAGE_OMITTED_TEXT,
15
+ IMPORT_WARNING_IMAGE_OMITTED,
16
+ isWholeElement,
17
+ textPart,
18
+ titleFromText,
19
+ toolNote,
20
+ } from './turns.mjs';
12
21
 
13
22
  const CLAUDE_ENVELOPE_TAGS = [
14
23
  'local-command-stdout',
@@ -20,11 +29,54 @@ const CLAUDE_ENVELOPE_TAGS = [
20
29
  'bash-input',
21
30
  'bash-stdout',
22
31
  'bash-stderr',
32
+ // The app's own instructions behind its "create PR" action.
33
+ 'create-pr-command',
23
34
  ];
24
- const SLASH_COMMAND_RE = /^(?:\s*<(command-name|command-message|command-args)>[\s\S]*?<\/\1>)+\s*$/;
35
+ const LOCAL_OUTPUT_TAGS = ['local-command-stdout', 'local-command-stderr'];
36
+ const SLASH_COMMAND_RE = /^(?:\s*<(command-name|command-message|command-args|skill-format)>[\s\S]*?<\/\1>)+\s*$/;
25
37
  const INTERRUPTED_RE = /^\[Request interrupted by user[^\]\n]*\]$/;
26
38
  // Claude Code's text annotation that accompanies a pasted image block.
27
39
  const IMAGE_ANNOTATION_RE = /^\[Image\b[^\n]*\]$/;
40
+ // The desktop app's paste envelope: `<pasted_content id="hhhh">\n` body
41
+ // `\n</pasted_content id="hhhh">`, one four-hex id on both tags.
42
+ const PASTE_OPEN = '<pasted_content id="';
43
+ const PASTE_ID_RE = /^[0-9a-f]{4}$/;
44
+
45
+ /**
46
+ * Text with the app's paste envelopes unwrapped the way the app displays it:
47
+ * each paste body in place, the text around it trimmed toward it, pieces on
48
+ * their own lines. Only a complete envelope with a matching id is unwrapped; any
49
+ * other `<pasted_content…>` is the user's own text and stays as typed.
50
+ */
51
+ export function unwrapClaudePastes(text) {
52
+ const pieces = [];
53
+ let cursor = 0;
54
+ let search = 0;
55
+ for (;;) {
56
+ const open = text.indexOf(PASTE_OPEN, search);
57
+ if (open < 0) break;
58
+ const idStart = open + PASTE_OPEN.length;
59
+ const id = text.slice(idStart, idStart + 4);
60
+ const bodyStart = idStart + 7; // past `hhhh">\n`
61
+ if (!PASTE_ID_RE.test(id) || text.slice(idStart + 4, bodyStart) !== '">\n') {
62
+ search = idStart;
63
+ continue;
64
+ }
65
+ const closeTag = `\n</pasted_content id="${id}">`;
66
+ // The line feed ending the open tag may also start the close tag (an empty paste).
67
+ const close = text.indexOf(closeTag, bodyStart - 1);
68
+ if (close < 0) break;
69
+ // Trimmed only toward a paste: the first fragment keeps its leading text.
70
+ const before = text.slice(cursor, open);
71
+ pieces.push(cursor ? before.trim() : before.trimEnd(), text.slice(bodyStart, Math.max(close, bodyStart)));
72
+ cursor = close + closeTag.length;
73
+ search = cursor;
74
+ }
75
+ if (!pieces.length) return text;
76
+ pieces.push(text.slice(cursor).trimStart());
77
+ const unwrapped = pieces.filter((piece, index) => index % 2 === 1 || piece).join('\n');
78
+ return unwrapped.trim() ? unwrapped : text;
79
+ }
28
80
 
29
81
  export function isClaudeEnvelopeText(text) {
30
82
  const trimmed = String(text || '').trim();
@@ -37,6 +89,17 @@ export function isClaudeEnvelopeText(text) {
37
89
  );
38
90
  }
39
91
 
92
+ const tagValue = (text, tag) => new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`).exec(text)?.[1].trim() || '';
93
+
94
+ /** A slash-command envelope as Claude Code shows it (`/name args`), else null. */
95
+ export function claudeCommandText(text) {
96
+ const trimmed = String(text || '').trim();
97
+ if (!SLASH_COMMAND_RE.test(trimmed)) return null;
98
+ const name = (tagValue(trimmed, 'command-name') || tagValue(trimmed, 'command-message')).replace(/^\//, '');
99
+ if (!name) return null;
100
+ return [`/${name}`, tagValue(trimmed, 'command-args')].filter(Boolean).join(' ');
101
+ }
102
+
40
103
  const isMessageEntry = (record) => record?.type === 'user' || record?.type === 'assistant';
41
104
  const stringOrNull = (value) => (typeof value === 'string' && value.trim() ? value.trim() : null);
42
105
  const uuidOrNull = (value) => (typeof value === 'string' ? value : null);
@@ -49,42 +112,69 @@ const metadataLength = (entry) =>
49
112
  (entry.logicalParentUuid?.length || 0) +
50
113
  (typeof entry.at === 'string' ? entry.at.length : 0);
51
114
 
115
+ // The main-chain message written last before file position `seq`: the leaf of
116
+ // the history a compaction boundary replaced.
117
+ function precedingMain(entries, seq) {
118
+ for (let index = seq - 1; index >= 0; index--) {
119
+ if (entries[index].main && entries[index].uuid !== null) return entries[index];
120
+ }
121
+ return null;
122
+ }
123
+
52
124
  /**
53
125
  * The active branch, root first: walked back from the last main-chain entry.
54
- * `crossedCompaction` tells whether a compaction boundary led back into the
55
- * history it replaced (Claude Code often no longer keeps that history).
126
+ * A compaction boundary links back to the history it replaced through its
127
+ * logical parent, or, when Claude Code never wrote that message, to the last
128
+ * message before the boundary. `replaced` holds the compaction summaries whose
129
+ * history the walk crossed into (that history is imported instead).
56
130
  */
57
131
  function activeChain(entries) {
58
132
  const main = entries.filter((entry) => entry.main);
59
133
  const leaf = main[main.length - 1];
60
- if (!leaf || leaf.uuid === null) return { chain: main, crossedCompaction: false };
134
+ if (!leaf || leaf.uuid === null) return { chain: main, replaced: new Set() };
61
135
  const byUuid = new Map();
62
136
  for (const entry of entries) {
63
137
  if (entry.uuid !== null) byUuid.set(entry.uuid, entry);
64
138
  }
65
139
  const chain = [];
66
140
  const seen = new Set();
67
- let crossedCompaction = false;
141
+ const replaced = new Set();
142
+ let summary = null;
68
143
  let current = leaf;
69
144
  while (current && !seen.has(current.uuid)) {
70
145
  seen.add(current.uuid);
71
146
  chain.push(current);
147
+ if (current.summary) summary = current;
72
148
  const parent = current.parentUuid !== null ? byUuid.get(current.parentUuid) : null;
73
- const logical = !parent && current.logicalParentUuid !== null ? byUuid.get(current.logicalParentUuid) : null;
74
- if (logical) crossedCompaction = true;
149
+ const logical =
150
+ !parent && current.logicalParentUuid !== null
151
+ ? byUuid.get(current.logicalParentUuid) || precedingMain(entries, current.seq)
152
+ : null;
153
+ if (logical && summary) {
154
+ replaced.add(summary);
155
+ summary = null;
156
+ }
75
157
  current = parent || logical;
76
158
  }
77
- return { chain: chain.reverse().filter((entry) => entry.main), crossedCompaction };
159
+ return { chain: chain.reverse().filter((entry) => entry.main), replaced };
78
160
  }
79
161
 
80
- function entryParts(role, content, warnings) {
162
+ /** Kept parts of a message; a slash command is a `command` part, a local command's output marks `localOutput`. */
163
+ function entryParts(role, content, warnings, marks) {
81
164
  const blocks = typeof content === 'string' ? [{ type: 'text', text: content }] : content;
82
165
  if (!Array.isArray(blocks)) return [];
83
166
  const parts = [];
84
167
  for (const block of blocks) {
85
168
  if (!block || typeof block !== 'object') continue;
86
169
  if (block.type === 'text' && typeof block.text === 'string') {
87
- if (role === 'user' && isClaudeEnvelopeText(block.text)) continue;
170
+ if (role === 'user') {
171
+ const command = claudeCommandText(block.text);
172
+ if (command) parts.push({ ...textPart(command), command: true });
173
+ if (isWholeElement(block.text, LOCAL_OUTPUT_TAGS)) marks.localOutput = true;
174
+ if (isClaudeEnvelopeText(block.text)) continue;
175
+ parts.push(textPart(unwrapClaudePastes(block.text)));
176
+ continue;
177
+ }
88
178
  parts.push(textPart(block.text));
89
179
  } else if (block.type === 'image') {
90
180
  warnings.add(IMPORT_WARNING_IMAGE_OMITTED);
@@ -96,6 +186,31 @@ function entryParts(role, content, warnings) {
96
186
  return parts;
97
187
  }
98
188
 
189
+ /**
190
+ * The title Claude Code's resume list shows from the records it appends to a
191
+ * transcript: the last `custom-title` (a `/rename`), else the last `ai-title`,
192
+ * then the last `summary`, then the last `last-prompt`. A blank custom title
193
+ * (a cleared rename) ends the name tier without falling back to the AI title.
194
+ * Other record types, such as `task-summary` or `agent-name`, never name it.
195
+ */
196
+ export function createClaudeTitleScan() {
197
+ let custom;
198
+ let ai;
199
+ let summary;
200
+ let lastPrompt;
201
+ return {
202
+ push(record) {
203
+ if (record.type === 'custom-title' && typeof record.customTitle === 'string') custom = record.customTitle;
204
+ else if (record.type === 'ai-title' && typeof record.aiTitle === 'string') ai = record.aiTitle;
205
+ else if (record.type === 'summary' && typeof record.summary === 'string') summary = record.summary;
206
+ else if (record.type === 'last-prompt' && typeof record.lastPrompt === 'string') lastPrompt = record.lastPrompt;
207
+ },
208
+ title() {
209
+ return titleFromText(custom ?? ai) || titleFromText(summary) || titleFromText(lastPrompt);
210
+ },
211
+ };
212
+ }
213
+
99
214
  function skippedEntry(record) {
100
215
  // Claude Code flags its compaction summary transcript-only as well.
101
216
  if (record.isCompactSummary === true) return false;
@@ -111,9 +226,12 @@ function skippedEntry(record) {
111
226
  */
112
227
  export function createClaudeReducer() {
113
228
  const entries = [];
229
+ const titleScan = createClaudeTitleScan();
114
230
  let recognized = false;
115
231
  let foreignSessionId = null;
116
232
  let cwd = null;
233
+ let sidechainSeen = false;
234
+ let mainSeen = false;
117
235
  let chars = 0;
118
236
  let metadataBytes = 0;
119
237
  return {
@@ -126,10 +244,23 @@ export function createClaudeReducer() {
126
244
  get metadataBytes() {
127
245
  return metadataBytes;
128
246
  },
247
+ /** Cheap running check: messages seen so far are all sidechain (a verdict only at the end of the file). */
248
+ get sidechainOnly() {
249
+ return sidechainSeen && !mainSeen;
250
+ },
251
+ /** Feed a record for its stored title only (a transcript's tail window). */
252
+ pushTitle(record) {
253
+ titleScan.push(record);
254
+ },
129
255
  push(record) {
256
+ titleScan.push(record);
130
257
  const message = isMessageEntry(record);
131
258
  if (message) recognized = true;
132
259
  const main = message && record.isSidechain !== true;
260
+ if (message) {
261
+ if (main) mainSeen = true;
262
+ else sidechainSeen = true;
263
+ }
133
264
  if (main) {
134
265
  foreignSessionId ||= stringOrNull(record.sessionId);
135
266
  cwd ||= stringOrNull(record.cwd);
@@ -145,13 +276,14 @@ export function createClaudeReducer() {
145
276
  parts: null,
146
277
  warnings: null,
147
278
  at: timestampOrNull(record.timestamp),
279
+ seq: entries.length,
148
280
  };
149
281
  metadataBytes += ENTRY_OVERHEAD_BYTES + metadataLength(entry);
150
282
  if (main && !skippedEntry(record)) {
151
283
  const body = record.message && typeof record.message === 'object' ? record.message : null;
152
284
  entry.role = body?.role === 'user' || body?.role === 'assistant' ? body.role : record.type;
153
285
  const warnings = new Set();
154
- entry.parts = entryParts(entry.role, body ? body.content : null, warnings);
286
+ entry.parts = entryParts(entry.role, body ? body.content : null, warnings, entry);
155
287
  if (record.isCompactSummary === true) {
156
288
  entry.summary = true;
157
289
  entry.parts = entry.parts.map((part) => ({ ...part, summary: true }));
@@ -164,15 +296,24 @@ export function createClaudeReducer() {
164
296
  result() {
165
297
  const warnings = new Set();
166
298
  const rawTurns = [];
167
- const { chain, crossedCompaction } = activeChain(entries);
168
- for (const entry of chain) {
299
+ const { chain, replaced } = activeChain(entries);
300
+ for (const [index, entry] of chain.entries()) {
169
301
  if (!entry.parts?.length) continue;
170
- // The summary stands in for history only when that history is gone.
171
- if (entry.summary && crossedCompaction) continue;
172
- rawTurns.push({ role: entry.role, parts: entry.parts, at: entry.at });
302
+ // The summary stands in for history only when that history is gone,
303
+ // and then as the model's recap, never as something the user wrote.
304
+ if (entry.summary && replaced.has(entry)) continue;
305
+ const role = entry.summary ? 'assistant' : entry.role;
306
+ // A local command (`/model`, `/rename`…) answered by the app itself is not part of the conversation.
307
+ const local =
308
+ entry.parts.some((part) => part.command) &&
309
+ chain.slice(index + 1).find((next) => next.parts?.length || next.localOutput)?.localOutput;
310
+ const parts = local ? entry.parts.filter((part) => !part.command) : entry.parts;
311
+ rawTurns.push({ role, parts, at: entry.at });
173
312
  for (const code of entry.warnings || []) warnings.add(code);
174
313
  }
175
- return { recognized, foreignSessionId, cwd, rawTurns, warnings };
314
+ // A transcript whose every message is a sidechain is a subagent's own, not a conversation.
315
+ const internalKind = this.sidechainOnly ? 'subagent' : null;
316
+ return { recognized, foreignSessionId, cwd, internalKind, rawTurns, warnings, storedTitle: titleScan.title() };
176
317
  },
177
318
  };
178
319
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Codex CLI rollout (`rollout-*.jsonl`) → raw import turns. Kept: the first
3
3
  * `session_meta` (foreign id, cwd), user/assistant `message` items as text and
4
- * tool calls as `[ran tool: <name>]` notes. Dropped: developer/system
4
+ * tool calls as tool notes. Dropped: developer/system
5
5
  * messages, Codex's own wrapper user messages, tool outputs, reasoning,
6
6
  * event_msg, token counts and turn_context.
7
7
  */
@@ -17,6 +17,7 @@ const CODEX_ENVELOPE_TAGS = [
17
17
  'user_shell_command',
18
18
  'subagent_notification',
19
19
  'skills_instructions',
20
+ 'external_codex_apps_open_page',
20
21
  ];
21
22
  const TOOL_CALL_TYPES = new Set(['function_call', 'custom_tool_call', 'local_shell_call']);
22
23
 
@@ -29,6 +30,17 @@ export function isCodexEnvelopeText(text) {
29
30
  return /^<image\b[^>]*>$/.test(trimmed) || trimmed === '</image>';
30
31
  }
31
32
 
33
+ // Codex shows a user message after the last of these headings: what precedes it
34
+ // is context the app adds for the model (IDE state, mentioned files, response
35
+ // annotations, an earlier request it already carries).
36
+ const USER_REQUEST_HEADING = '## My request for Codex:';
37
+
38
+ /** A user text part as Codex displays it. */
39
+ export function codexUserDisplayText(text) {
40
+ const at = text.lastIndexOf(USER_REQUEST_HEADING);
41
+ return at < 0 ? text : text.slice(at + USER_REQUEST_HEADING.length).trim();
42
+ }
43
+
32
44
  function messageParts(payload, warnings) {
33
45
  const content = typeof payload.content === 'string' ? [{ type: 'text', text: payload.content }] : payload.content;
34
46
  if (!Array.isArray(content)) return [];
@@ -41,8 +53,8 @@ function messageParts(payload, warnings) {
41
53
  continue;
42
54
  }
43
55
  if (typeof item.text !== 'string') continue;
44
- if (payload.role === 'user' && isCodexEnvelopeText(item.text)) continue;
45
- parts.push(textPart(item.text));
56
+ if (payload.role !== 'user') parts.push(textPart(item.text));
57
+ else if (!isCodexEnvelopeText(item.text)) parts.push(textPart(codexUserDisplayText(item.text)));
46
58
  }
47
59
  return parts;
48
60
  }
@@ -53,6 +65,31 @@ function toolName(payload) {
53
65
  }
54
66
 
55
67
  const stringOrNull = (value) => (typeof value === 'string' && value.trim() ? value.trim() : null);
68
+
69
+ // `session_meta.thread_source` values Codex gives threads it runs for itself.
70
+ const INTERNAL_THREAD_SOURCES = {
71
+ subagent: 'subagent',
72
+ guardian_review: 'guardianReview',
73
+ memory_consolidation: 'internal',
74
+ };
75
+
76
+ /**
77
+ * What kind of Codex-internal record a `session_meta` payload marks, else null
78
+ * for a conversation the user had. Markers are Codex's own: `thread_source`
79
+ * (`subagent`, `guardian_review`, `memory_consolidation`) and a `source` of the
80
+ * form `{ subagent: … }` (spawned agent, review, compact; `{ other: "guardian" }`
81
+ * is the approval reviewer) or `{ internal: … }`. Other sources (cli, vscode,
82
+ * exec, mcp, custom) and features (automation, voice chat) are the user's.
83
+ */
84
+ export function codexInternalKind(payload) {
85
+ const byThread = INTERNAL_THREAD_SOURCES[payload?.thread_source];
86
+ if (byThread) return byThread;
87
+ const source = payload?.source;
88
+ if (!source || typeof source !== 'object') return null;
89
+ if ('subagent' in source) return source.subagent?.other === 'guardian' ? 'guardianReview' : 'subagent';
90
+ if ('internal' in source) return source.internal === 'guardian' ? 'guardianReview' : 'internal';
91
+ return null;
92
+ }
56
93
  // Rough fixed cost of one retained turn object, so tiny turns still count.
57
94
  const ENTRY_OVERHEAD_BYTES = 64;
58
95
 
@@ -67,6 +104,8 @@ export function createCodexReducer() {
67
104
  let recognized = false;
68
105
  let foreignSessionId = null;
69
106
  let cwd = null;
107
+ let internalKind = null;
108
+ let historyBase = null;
70
109
  let metaSeen = false;
71
110
  let chars = 0;
72
111
  let metadataBytes = 0;
@@ -86,15 +125,19 @@ export function createCodexReducer() {
86
125
  get metadataBytes() {
87
126
  return metadataBytes;
88
127
  },
89
- push(record) {
128
+ /** `inherited` records come from a history base: its session metadata is not this thread's. */
129
+ push(record, inherited = false) {
90
130
  const payload = record?.payload && typeof record.payload === 'object' ? record.payload : null;
91
131
  if (!payload) return;
92
132
  if (record.type === 'session_meta') {
133
+ if (inherited) return;
93
134
  recognized = true;
94
135
  if (!metaSeen) {
95
136
  metaSeen = true;
96
137
  foreignSessionId = stringOrNull(payload.id) || stringOrNull(payload.session_id);
97
138
  cwd = stringOrNull(payload.cwd);
139
+ internalKind = codexInternalKind(payload);
140
+ if (payload.history_base && typeof payload.history_base === 'object') historyBase = payload.history_base;
98
141
  }
99
142
  return;
100
143
  }
@@ -108,23 +151,95 @@ export function createCodexReducer() {
108
151
  }
109
152
  },
110
153
  result() {
111
- return { recognized, foreignSessionId, cwd, rawTurns, warnings };
154
+ return { recognized, foreignSessionId, cwd, internalKind, historyBase, rawTurns, warnings };
112
155
  },
113
156
  };
114
157
  }
115
158
 
116
- /** id/cwd of a `session_meta` first line cut off by a bounded prefix read. */
117
- export function codexMetaFromTruncatedLine(line) {
118
- const text = String(line || '');
119
- if (!/"type"\s*:\s*"session_meta"/.test(text)) return null;
120
- const field = (name) => {
121
- const match = new RegExp(`"${name}"\\s*:\\s*("(?:[^"\\\\]|\\\\.)*")`).exec(text);
122
- if (!match) return null;
123
- try {
124
- return stringOrNull(JSON.parse(match[1]));
125
- } catch {
126
- return null;
159
+ // ── bounded-prefix `session_meta` scan ───────────────────────────────────────
160
+ // Index after the string starting at `i`, or -1 when the text ends inside it.
161
+ function scanString(text, i) {
162
+ for (let j = i + 1; j < text.length; j++) {
163
+ if (text[j] === '\\') j++;
164
+ else if (text[j] === '"') return j + 1;
165
+ }
166
+ return -1;
167
+ }
168
+
169
+ // Index after the JSON value starting at `i`, or -1 when the text ends inside it.
170
+ function scanValue(text, i) {
171
+ if (text[i] === '"') return scanString(text, i);
172
+ if (text[i] === '{' || text[i] === '[') {
173
+ let depth = 0;
174
+ for (let j = i; j < text.length; j++) {
175
+ if (text[j] === '"') {
176
+ j = scanString(text, j) - 1;
177
+ if (j < 0) return -1;
178
+ } else if (text[j] === '{' || text[j] === '[') depth++;
179
+ else if ((text[j] === '}' || text[j] === ']') && --depth === 0) return j + 1;
127
180
  }
181
+ return -1;
182
+ }
183
+ const literal = /[^,}\]\s]+/y;
184
+ literal.lastIndex = i;
185
+ literal.exec(text);
186
+ return literal.lastIndex < text.length ? literal.lastIndex : -1;
187
+ }
188
+
189
+ /**
190
+ * Complete top-level values (raw JSON text) of the record object and of its
191
+ * `payload` object in a possibly cut-off line. Nested values are never
192
+ * inspected, so a key inside custom metadata is not mistaken for a payload field.
193
+ */
194
+ function recordFields(text) {
195
+ const top = {};
196
+ const payload = {};
197
+ const walk = (start, target, descend) => {
198
+ let j = start + 1;
199
+ for (;;) {
200
+ while (/[\s,]/.test(text[j] ?? '')) j++;
201
+ if (text[j] !== '"') return;
202
+ const keyEnd = scanString(text, j);
203
+ if (keyEnd < 0) return;
204
+ const key = JSON.parse(text.slice(j, keyEnd));
205
+ j = keyEnd;
206
+ while (/\s/.test(text[j] ?? '')) j++;
207
+ if (text[j] !== ':') return;
208
+ j++;
209
+ while (/\s/.test(text[j] ?? '')) j++;
210
+ if (descend && key === 'payload' && text[j] === '{') return walk(j, payload, false);
211
+ const end = scanValue(text, j);
212
+ if (end < 0) return;
213
+ target[key] = text.slice(j, end);
214
+ j = end;
215
+ }
216
+ };
217
+ const open = text.indexOf('{');
218
+ if (open >= 0) walk(open, top, true);
219
+ return { top, payload };
220
+ }
221
+
222
+ const jsonOrNull = (raw) => {
223
+ try {
224
+ return raw === undefined ? null : JSON.parse(raw);
225
+ } catch {
226
+ return null;
227
+ }
228
+ };
229
+
230
+ /**
231
+ * id/cwd/internal kind of a `session_meta` first line cut off by a bounded
232
+ * prefix read, from the payload fields that arrived whole. A marker that did
233
+ * not arrive whole leaves the record unclassified (`internalKind: null`).
234
+ */
235
+ export function codexMetaFromTruncatedLine(line) {
236
+ const { top, payload } = recordFields(String(line || ''));
237
+ if (jsonOrNull(top.type) !== 'session_meta') return null;
238
+ const field = (name) => stringOrNull(jsonOrNull(payload[name]));
239
+ const meta = { thread_source: field('thread_source'), source: jsonOrNull(payload.source) };
240
+ return {
241
+ foreignSessionId: field('id') || field('session_id'),
242
+ cwd: field('cwd'),
243
+ internalKind: codexInternalKind(meta),
128
244
  };
129
- return { foreignSessionId: field('id') || field('session_id'), cwd: field('cwd') };
130
245
  }
@@ -6,7 +6,13 @@
6
6
  */
7
7
  import { createClaudeReducer } from './claude.mjs';
8
8
  import { createCodexReducer } from './codex.mjs';
9
- import { IMPORT_WARNING_MALFORMED, conversationTitle, normalizeTurns } from './turns.mjs';
9
+ import {
10
+ IMPORT_ERROR_INTERNAL,
11
+ IMPORT_WARNING_HISTORY_PARTIAL,
12
+ IMPORT_WARNING_MALFORMED,
13
+ conversationTitle,
14
+ normalizeTurns,
15
+ } from './turns.mjs';
10
16
 
11
17
  export const IMPORT_SOURCES = Object.freeze(['codex', 'claude']);
12
18
  /** A line or converted transcript beyond the import limits fails the file. */
@@ -35,7 +41,9 @@ const transcriptChars = (turns) => turns.reduce((total, turn) => total + turn.te
35
41
  * (an over-long line or file). `result()` may be called repeatedly and also
36
42
  * holds the normalized turns to `maxTranscriptChars`.
37
43
  * `errorCode` is null for an importable result, else one of `empty` |
38
- * `malformed` | `unsupported` | `tooLarge` (whose turns are not returned).
44
+ * `malformed` | `unsupported` | `internalRecord` (the source app's own
45
+ * subagent/review record; `internalKind` names it) | `tooLarge` (whose turns
46
+ * are not returned).
39
47
  */
40
48
  export function createConversationConverter(
41
49
  source,
@@ -47,18 +55,37 @@ export function createConversationConverter(
47
55
  ) {
48
56
  if (!isImportSource(source)) throw new TypeError('source must be codex or claude.');
49
57
  const reducer = source === 'codex' ? createCodexReducer() : createClaudeReducer();
50
- let lines = 0;
51
58
  let records = 0;
52
59
  let malformed = 0;
53
60
  let tooLarge = false;
61
+ let historyPartial = false;
54
62
  return {
63
+ /** True while every message seen so far is a Claude sidechain message. */
64
+ get sidechainOnly() {
65
+ return reducer.sidechainOnly === true;
66
+ },
55
67
  markTooLarge() {
56
68
  tooLarge = true;
57
69
  },
58
- pushLine(raw) {
70
+ /** A history base could not be followed in full. */
71
+ markHistoryPartial() {
72
+ historyPartial = true;
73
+ },
74
+ /** Feed a line for the stored title only (Claude Code's tail window); other sources ignore it. */
75
+ pushTitleLine(raw) {
76
+ let value;
77
+ try {
78
+ value = JSON.parse(String(raw).trim());
79
+ } catch {
80
+ return;
81
+ }
82
+ if (value && typeof value === 'object') reducer.pushTitle?.(value);
83
+ },
84
+ /** `inherited` lines belong to a history base followed ahead of this file's own lines. */
85
+ pushLine(raw, { inherited = false } = {}) {
59
86
  if (tooLarge) return false;
60
87
  let line = String(raw);
61
- if (lines++ === 0 && line.charCodeAt(0) === 0xfeff) line = line.slice(1);
88
+ if (line.charCodeAt(0) === 0xfeff) line = line.slice(1);
62
89
  if (line.endsWith('\r')) line = line.slice(0, -1);
63
90
  if (!line.trim()) return true;
64
91
  let value;
@@ -73,7 +100,7 @@ export function createConversationConverter(
73
100
  return true;
74
101
  }
75
102
  records += 1;
76
- reducer.push(value);
103
+ reducer.push(value, inherited);
77
104
  if (
78
105
  reducer.chars > maxTranscriptChars ||
79
106
  reducer.entries > maxRetainedEntries ||
@@ -90,8 +117,10 @@ export function createConversationConverter(
90
117
  if (transcriptChars(turns) > maxTranscriptChars) tooLarge = true;
91
118
  const warnings = new Set(parsed.warnings);
92
119
  if (malformed) warnings.add(IMPORT_WARNING_MALFORMED);
120
+ if (historyPartial) warnings.add(IMPORT_WARNING_HISTORY_PARTIAL);
93
121
  let errorCode = null;
94
122
  if (tooLarge) errorCode = IMPORT_ERROR_TOO_LARGE;
123
+ else if (parsed.internalKind) errorCode = IMPORT_ERROR_INTERNAL;
95
124
  else if (!records) errorCode = malformed ? 'malformed' : 'empty';
96
125
  else if (!parsed.recognized) errorCode = 'unsupported';
97
126
  else if (!turns.length) errorCode = 'empty';
@@ -100,8 +129,10 @@ export function createConversationConverter(
100
129
  errorCode,
101
130
  foreignSessionId: parsed.foreignSessionId,
102
131
  cwd: parsed.cwd,
132
+ internalKind: parsed.internalKind,
133
+ historyBase: parsed.historyBase ?? null,
103
134
  turns: tooLarge ? [] : turns,
104
- title: conversationTitle(turns),
135
+ title: parsed.storedTitle || conversationTitle(turns),
105
136
  messageCount: tooLarge ? 0 : turns.length,
106
137
  warnings: [...warnings],
107
138
  };
@@ -70,6 +70,9 @@ export function importedTranscriptMessages(turns, importedAt) {
70
70
  content: turn.text,
71
71
  meta: {
72
72
  source: IMPORTED_MESSAGE_SOURCE,
73
+ // Where the text holds a run of tool use, and which tools: the desktop
74
+ // draws each run as a native activity row.
75
+ ...(turn.toolRuns?.length ? { importedToolRuns: turn.toolRuns } : {}),
73
76
  transcript: { messageId: newTranscriptMessageId(), at: turn.at || importedAt },
74
77
  },
75
78
  }));