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.
- package/package.json +1 -1
- package/scripts/check-voice-runtime-inputs.mjs +25 -0
- package/scripts/gate-local.mjs +12 -3
- package/scripts/latest-guard.mjs +21 -9
- package/scripts/lib/isolated-test-env.mjs +10 -5
- package/scripts/lib/run-node-tests.mjs +37 -37
- package/scripts/lib/scoped-temp.d.mts +16 -0
- package/scripts/lib/scoped-temp.mjs +123 -0
- package/scripts/lib/stale-temp.mjs +44 -0
- package/scripts/post-release-check.mjs +1 -6
- package/scripts/pptx-kit-index.mjs +17 -4
- package/scripts/suite-health.mjs +10 -1
- package/scripts/test-data-dir.mjs +10 -7
- package/src/app.mjs +4 -0
- package/src/defaults/shipped-history.json +5 -0
- package/src/defaults/skills/browser-use/SKILL.md +6 -4
- package/src/defaults/skills/computer-use/SKILL.md +4 -2
- package/src/lib/rules-builder.cjs +29 -12
- package/src/output-styles/common.md +2 -2
- package/src/output-styles/simple.md +1 -1
- package/src/rules/agent/AGENT.md +5 -4
- package/src/rules/lead/LEAD.md +10 -5
- package/src/rules/shared/10-tool-workflow.md +3 -1
- package/src/rules/shared/45-destructive.md +0 -2
- package/src/rules/shared/50-execution.md +2 -0
- package/src/rules/shared/70-delivery.md +7 -9
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +29 -6
- package/src/runtime/agent/orchestrator/cache-break-trace.mjs +2 -0
- package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +1 -1
- package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +3 -1
- package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +5 -2
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-api-recovery.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +7 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-api-transport.mjs +8 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens-recovery.mjs +6 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +14 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +6 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-turn-result.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/command-code-oauth-login.mjs +1 -10
- package/src/runtime/agent/orchestrator/providers/cursor.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/devin.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +210 -20
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-prefix-trace.mjs +76 -11
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +45 -6
- package/src/runtime/agent/orchestrator/providers/lib/first-output-window.mjs +58 -0
- package/src/runtime/agent/orchestrator/providers/lib/listen-on.mjs +14 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-login.mjs +1 -10
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/chunk-events.mjs +9 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +14 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +4 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream-common.mjs +5 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +16 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events/output-items.mjs +5 -2
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events.mjs +13 -3
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +13 -2
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-watchdogs.mjs +81 -42
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +8 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +136 -5
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +20 -12
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-watchdogs.mjs +48 -22
- package/src/runtime/agent/orchestrator/providers/registry.mjs +12 -0
- package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +41 -13
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +7 -6
- package/src/runtime/agent/orchestrator/runtime-core/session-text.mjs +7 -3
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +3 -1
- package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +5 -2
- package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +1 -3
- package/src/runtime/agent/orchestrator/session/cache-warm.mjs +8 -2
- package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +1 -8
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +23 -0
- package/src/runtime/agent/orchestrator/session/compact/observation-supersession.mjs +103 -23
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +160 -36
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +13 -1
- package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +163 -22
- package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +132 -17
- package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +38 -7
- package/src/runtime/agent/orchestrator/session/conversation-import/imported-session.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +90 -22
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +33 -3
- package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +7 -2
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/provider-tool-exclusions.mjs +13 -5
- package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +13 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +39 -21
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/manager/owner-route-fallbacks.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/manager/route-fallback-chain.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +27 -9
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +61 -6
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +19 -8
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +12 -5
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-context.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +9 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/prompt-cache-touch-store.mjs +30 -7
- package/src/runtime/agent/orchestrator/session/restart-prefix-parity.fixture.mjs +154 -0
- package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +15 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +37 -8
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +9 -1
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +61 -2
- package/src/runtime/agent/orchestrator/stall-policy.mjs +23 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +4 -2
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +46 -20
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +7 -1
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +31 -5
- package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +22 -8
- package/src/runtime/agent/orchestrator/tools/builtin/lib/scoped-search-flight.mjs +9 -5
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-stream.mjs +7 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +6 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-dispatch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-publication.mjs +23 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-snapshot.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +1 -8
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/federation.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
- package/src/runtime/agent/orchestrator/tools/lib/isolated-test-env.mjs +4 -3
- package/src/runtime/agent/orchestrator/tools/lib/shell-warm-standby.mjs +26 -4
- package/src/runtime/channels/lib/webhook/signature.mjs +4 -1
- package/src/runtime/channels/lib/whisper-port.mjs +10 -1
- package/src/runtime/computer-bridge/action-schema.mjs +2 -2
- package/src/runtime/computer-bridge/core-actions.mjs +2 -2
- package/src/runtime/computer-bridge/error-recovery.mjs +5 -4
- package/src/runtime/computer-bridge/linux-action-capabilities.mjs +24 -5
- package/src/runtime/computer-bridge/linux-readiness.d.mts +7 -2
- package/src/runtime/local-provider/server-process.mjs +2 -1
- package/src/runtime/media/adapters/gemini-video.mjs +4 -1
- package/src/runtime/media/adapters/xai-media.mjs +2 -1
- package/src/runtime/memory/lib/embedding-worker.mjs +6 -3
- package/src/runtime/memory/lib/memory-action-handlers/cycle-backfill-action.mjs +1 -7
- package/src/runtime/memory/lib/memory-chunk-quality.mjs +1 -6
- package/src/runtime/office/authoring/pdf-author-action.mjs +1 -6
- package/src/runtime/office/design/design-art-direction.mjs +29 -27
- package/src/runtime/office/pdf/pdf-writer.mjs +6 -1
- package/src/runtime/office/portable/portable-snapshot-docx.mjs +7 -1
- package/src/runtime/office/portable/portable-snapshot-shared.mjs +6 -1
- package/src/runtime/office/quality/assurance-rendered.mjs +9 -2
- package/src/runtime/office/quality/assurance-structure-docx.mjs +3 -1
- package/src/runtime/office/quality/design-review.mjs +1 -5
- package/src/runtime/office/quality/document-brief.mjs +1 -5
- package/src/runtime/office/quality/forced-break-pages.mjs +9 -6
- package/src/runtime/office/quality/quality-pipeline.mjs +1 -1
- package/src/runtime/shared/bot-owned-work.mjs +56 -0
- package/src/runtime/shared/defaults-separation.mjs +1 -4
- package/src/runtime/shared/fetch-safe-listen.d.mts +16 -0
- package/src/runtime/shared/fetch-safe-listen.mjs +50 -0
- package/src/runtime/shared/handoff-artifacts.d.mts +2 -1
- package/src/runtime/shared/handoff-artifacts.mjs +18 -8
- package/src/runtime/shared/idle-auto-clear-gate.mjs +29 -7
- package/src/runtime/shared/llm/cost.mjs +5 -1
- package/src/runtime/shared/loopback-listener.mjs +25 -2
- package/src/runtime/shared/model-route-chain.d.mts +7 -2
- package/src/runtime/shared/model-route-chain.mjs +22 -4
- package/src/runtime/shared/open-url.mjs +10 -0
- package/src/runtime/shared/session-activity-signal.mjs +14 -0
- package/src/runtime/shared/session-scratch.mjs +37 -0
- package/src/runtime/shared/tool-card-model/agent-surface.mjs +1 -3
- package/src/runtime/shared/tool-card-model/card-status.mjs +15 -2
- package/src/runtime/shared/tool-result-summary.mjs +1 -1
- package/src/runtime/shared/tool-surface.mjs +3 -0
- package/src/runtime/shared/tool-work-units.mjs +2 -0
- package/src/runtime/shared/turn-snapshot-store.mjs +13 -6
- package/src/runtime/shared/worktrees/contract.mjs +4 -1
- package/src/runtime/shared/worktrees/git.mjs +15 -0
- package/src/runtime/shared/worktrees/manager.mjs +87 -38
- package/src/runtime/shared/worktrees/test-support.mjs +6 -10
- package/src/runtime/visualize-bridge/client.mjs +2 -0
- package/src/session-runtime/agent-home-delivery.mjs +41 -0
- package/src/session-runtime/agent-task-sessions.mjs +152 -63
- package/src/session-runtime/agent-tool-routing.mjs +8 -41
- package/src/session-runtime/boot/tools.mjs +5 -1
- package/src/session-runtime/bot-status.mjs +12 -6
- package/src/session-runtime/bot-target.mjs +38 -0
- package/src/session-runtime/channel-config-api.mjs +6 -1
- package/src/session-runtime/conversation-import/codex-titles.mjs +88 -0
- package/src/session-runtime/conversation-import/discovery.mjs +271 -25
- package/src/session-runtime/conversation-import/service.mjs +26 -5
- package/src/session-runtime/handoff-attachments.mjs +82 -31
- package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +12 -13
- package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +1 -0
- package/src/session-runtime/internal-tool-executor/reaction-tool.mjs +4 -1
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +8 -2
- package/src/session-runtime/lifecycle/cold-heir.mjs +11 -6
- package/src/session-runtime/lifecycle/session-catalog.mjs +9 -1
- package/src/session-runtime/model-route/set-route.mjs +11 -2
- package/src/session-runtime/provider-catalog-cache.mjs +82 -36
- package/src/session-runtime/provider-models/catalog-load.mjs +23 -6
- package/src/session-runtime/provider-models.mjs +4 -2
- package/src/session-runtime/resource-recall-api.mjs +6 -1
- package/src/session-runtime/runtime-feature-gates.mjs +5 -1
- package/src/session-runtime/services/agent-tool/execute.mjs +14 -9
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +20 -4
- package/src/session-runtime/services/agent-tool/model-select.mjs +48 -7
- package/src/session-runtime/services/agent-tool/render.mjs +71 -17
- package/src/session-runtime/services/agent-tool/spawn-preset.mjs +6 -2
- package/src/session-runtime/services/agent-tool/tool-def.mjs +9 -9
- package/src/session-runtime/services/provider-admin.mjs +2 -1
- package/src/session-runtime/tool-policy/policy-stage.mjs +1 -0
- package/src/session-runtime/workflow-agents-api/agent-editor.mjs +5 -1
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +15 -4
- package/src/session-runtime/workflow-agents-api/onboarding.mjs +3 -3
- package/src/session-runtime/workspace/transitions.mjs +25 -0
- package/src/session-runtime/workspace/worktree-tool.mjs +26 -9
- package/src/standalone/daemon-session-control.mjs +18 -2
- package/src/standalone/session-service/agent-tree/agent-turns.mjs +16 -12
- package/src/standalone/session-service/session-calls/session-action.mjs +10 -3
- package/src/standalone/session-transport/client-registry/client-grace.mjs +12 -9
- package/src/tui/app/app-view/floating-panel.jsx +7 -4
- package/src/tui/app/model-picker/route-selection/fast-parameters.mjs +1 -3
- package/src/tui/app/model-picker/route-selection.mjs +5 -6
- package/src/tui/app/provider-setup/oauth-actions.mjs +38 -20
- package/src/tui/app/provider-setup/provider-items.mjs +8 -2
- package/src/tui/app/provider-setup-picker.mjs +9 -5
- package/src/tui/components/QuestionPanel.jsx +13 -4
- package/src/tui/dist/index.mjs +75 -28
- package/src/tui/session/session-flow/queue/take-entries.mjs +11 -0
- 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
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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 {
|
|
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
|
|
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
|
-
*
|
|
55
|
-
*
|
|
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,
|
|
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
|
-
|
|
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 =
|
|
74
|
-
|
|
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),
|
|
159
|
+
return { chain: chain.reverse().filter((entry) => entry.main), replaced };
|
|
78
160
|
}
|
|
79
161
|
|
|
80
|
-
|
|
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'
|
|
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,
|
|
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
|
-
|
|
172
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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 {
|
|
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` | `
|
|
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
|
-
|
|
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 (
|
|
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
|
}));
|