mixdog 1.2.0 → 1.2.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/README.md +7 -5
- package/package.json +1 -1
- package/src/defaults/shipped-history.json +4 -0
- package/src/defaults/skills/pptx/SKILL.md +6 -6
- package/src/defaults/skills/pptx/references/review.md +6 -4
- package/src/defaults/skills/pptx/scripts/source-extract.mjs +1 -1
- package/src/defaults/skills/setup/references/actions.md +13 -3
- package/src/defaults/skills/visualize/SKILL.md +12 -9
- package/src/rules/shared/60-verification.md +3 -1
- package/src/rules/shared/70-delivery.md +4 -2
- package/src/runtime/agent/orchestrator/cache-mtime.mjs +26 -5
- package/src/runtime/agent/orchestrator/config.mjs +10 -4
- package/src/runtime/agent/orchestrator/context/collect.mjs +2 -1
- package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +11 -5
- package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +74 -21
- package/src/runtime/agent/orchestrator/mcp/client-config.mjs +86 -8
- package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +5 -1
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +65 -7
- package/src/runtime/agent/orchestrator/mcp/client.mjs +128 -44
- package/src/runtime/agent/orchestrator/mcp/lazy-output-validator.mjs +25 -0
- package/src/runtime/agent/orchestrator/mcp/plugin-placeholders.mjs +54 -0
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +108 -17
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +19 -11
- package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/command-code-oauth-login.mjs +337 -0
- package/src/runtime/agent/orchestrator/providers/command-code-oauth-tokens.mjs +159 -0
- package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +444 -0
- package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +7 -4
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +9 -2
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +41 -28
- package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/model-proxy-bridge.mjs +1080 -0
- package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +15 -0
- package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +35 -0
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +12 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +6 -5
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +6 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-options.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +9 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +4 -4
- package/src/runtime/agent/orchestrator/providers/registry.mjs +35 -9
- package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +3 -0
- package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +15 -55
- package/src/runtime/agent/orchestrator/runtime-core/goal-control.mjs +15 -2
- package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +267 -84
- package/src/runtime/agent/orchestrator/runtime-core/skill-tool-loading.mjs +4 -1
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-schema.mjs +21 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +54 -3
- package/src/runtime/agent/orchestrator/session/catalog-db/index.mjs +284 -0
- package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +268 -0
- package/src/runtime/agent/orchestrator/session/catalog-db/sqlite-gate.mjs +27 -0
- package/src/runtime/agent/orchestrator/session/catalog-db/worker.mjs +628 -0
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +6 -2
- package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +184 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +136 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +128 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/imported-session.mjs +127 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +101 -0
- package/src/runtime/agent/orchestrator/session/explicit-skills.mjs +83 -5
- package/src/runtime/agent/orchestrator/session/imported-message.mjs +11 -0
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +3 -2
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +6 -4
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +4 -3
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +12 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +20 -12
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +9 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +17 -4
- package/src/runtime/agent/orchestrator/session/manager.mjs +0 -1
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +4 -3
- package/src/runtime/agent/orchestrator/session/store/listing.mjs +6 -2
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +817 -0
- package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +19 -12
- package/src/runtime/agent/orchestrator/session/store-summary-reader-catalog-fixture.mjs +158 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +183 -374
- package/src/runtime/agent/orchestrator/session/synthetic-user-envelope.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/tool-batch/pre-dispatch.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +21 -2
- package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +38 -17
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +17 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +4 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +6 -6
- package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +173 -22
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-guards.mjs +8 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +5 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +15 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +13 -10
- package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +17 -12
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/root-resolution.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/apply-patch/codex-batch.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/patch/apply-patch/route.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/read-redirects.mjs +34 -30
- package/src/runtime/agent/orchestrator/tools/patch/sequence/units.mjs +7 -7
- package/src/runtime/agent/orchestrator/tools/patch/sequence.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -0
- package/src/runtime/attachments/store.d.mts +3 -0
- package/src/runtime/channels/lib/owned-runtime/owned-start.mjs +1 -1
- package/src/runtime/channels/lib/webhook/session-dispatch.mjs +16 -4
- package/src/runtime/channels/lib/worker-main.mjs +17 -14
- package/src/runtime/computer-bridge/linux-readiness.mjs +0 -2
- package/src/runtime/effort-judge/judge-client.mjs +56 -18
- package/src/runtime/memory/lib/memory.mjs +1 -1
- package/src/runtime/memory/lib/session-ingest.mjs +2 -2
- package/src/runtime/shared/atomic-file.mjs +25 -4
- package/src/runtime/shared/extension-scopes.mjs +5 -3
- package/src/runtime/shared/file-lock.mjs +37 -11
- package/src/runtime/shared/llm/anthropic-effort.mjs +14 -0
- package/src/runtime/shared/llm/model-catalog.mjs +0 -12
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +6 -0
- package/src/runtime/shared/plugin-manifest.mjs +18 -9
- package/src/runtime/shared/pristine-execution-contract.json +9 -2
- package/src/runtime/shared/pristine-execution.mjs +28 -4
- package/src/runtime/shared/provider-accounts.mjs +124 -20
- package/src/runtime/shared/provider-api-key.mjs +12 -0
- package/src/runtime/shared/shell-display.mjs +9 -3
- package/src/runtime/shared/skill-mentions.d.mts +10 -0
- package/src/runtime/shared/skill-mentions.mjs +133 -0
- package/src/runtime/shared/skill-script-runtime.mjs +264 -0
- package/src/runtime/shared/tool-primitives.mjs +35 -2
- package/src/runtime/shared/turn-worktree-snapshot.mjs +25 -5
- package/src/runtime/shared/worker-requests.mjs +15 -2
- package/src/session-runtime/automation-agents.mjs +8 -2
- package/src/session-runtime/boot/apis.mjs +55 -0
- package/src/session-runtime/boot/config.mjs +5 -2
- package/src/session-runtime/boot/facade.mjs +1 -0
- package/src/session-runtime/boot/infrastructure.mjs +29 -18
- package/src/session-runtime/conversation-import/discovery.mjs +698 -0
- package/src/session-runtime/conversation-import/service.mjs +369 -0
- package/src/session-runtime/cwd-plugins/cwd-apply.mjs +10 -5
- package/src/session-runtime/cwd-plugins/plugin-status.mjs +47 -42
- package/src/session-runtime/mcp-glue/connect.mjs +57 -10
- package/src/session-runtime/mcp-glue/plugin-servers.mjs +499 -0
- package/src/session-runtime/mcp-glue/server-catalog.mjs +115 -30
- package/src/session-runtime/mcp-glue.mjs +22 -2
- package/src/session-runtime/new-session-config.mjs +3 -2
- package/src/session-runtime/notification-bus.mjs +9 -2
- package/src/session-runtime/provider-auth/accounts.mjs +33 -15
- package/src/session-runtime/provider-models/web-search-catalog.mjs +5 -1
- package/src/session-runtime/provider-readiness.mjs +16 -10
- package/src/session-runtime/resource-extension-sync.mjs +20 -5
- package/src/session-runtime/resource-mcp-api.mjs +101 -77
- package/src/session-runtime/resource-plugins-api.mjs +29 -41
- package/src/session-runtime/resource-scope-decorate.mjs +6 -8
- package/src/session-runtime/resource-skills-api.mjs +23 -2
- package/src/session-runtime/schedule-session-run.mjs +1 -1
- package/src/session-runtime/services/channel-session-router.mjs +4 -1
- package/src/session-runtime/services/hook-bus/command-handler.mjs +4 -1
- package/src/session-runtime/services/hook-bus/handlers.mjs +7 -3
- package/src/session-runtime/services/provider-admin.mjs +64 -18
- package/src/session-runtime/services/usage-dashboard-model.mjs +3 -0
- package/src/session-runtime/setup-tool/executor.mjs +12 -1
- package/src/session-runtime/setup-tool/extended-actions.mjs +1 -0
- package/src/session-runtime/setup-tool/settings-contract.mjs +1 -0
- package/src/session-runtime/setup-tool/tool-defs.mjs +7 -1
- package/src/session-runtime/skills-api.mjs +7 -3
- package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +1 -0
- package/src/session-runtime/webhook-session-run.mjs +16 -4
- package/src/skill-script-entry.mjs +36 -0
- package/src/standalone/daemon-stored-session-views.mjs +11 -3
- package/src/standalone/daemon.mjs +41 -4
- package/src/standalone/session-protocol.mjs +5 -0
- package/src/standalone/session-service/session-calls/session-action.mjs +6 -1
- package/src/tui/App.jsx +9 -1
- package/src/tui/app/app-view/floating-panel.jsx +9 -1
- package/src/tui/app/app-view.jsx +6 -0
- package/src/tui/app/extension-pickers/plugins-pickers.mjs +35 -16
- package/src/tui/app/model-options.mjs +2 -0
- package/src/tui/app/skill-mention-palette.mjs +88 -0
- package/src/tui/app/slash-palette.mjs +23 -7
- package/src/tui/app/transcript-row-estimate.mjs +11 -3
- package/src/tui/app/use-prompt-draft-flow.mjs +7 -2
- package/src/tui/app/use-prompt-state.mjs +5 -0
- package/src/tui/components/Message.jsx +22 -1
- package/src/tui/components/PromptInput.jsx +21 -3
- package/src/tui/components/SlashCommandPalette.jsx +12 -3
- package/src/tui/components/TranscriptItem.jsx +9 -1
- package/src/tui/components/prompt-input/key-handler.mjs +21 -8
- package/src/tui/components/user-message-skills.mjs +38 -0
- package/src/tui/dist/index.mjs +507 -171
- package/src/tui/session/queue-helpers.mjs +7 -2
- package/src/tui/session/session-api/extensions.mjs +4 -2
- package/src/tui/session/session-api/settings.mjs +7 -0
- package/src/tui/session/session-flow/drain.mjs +14 -4
- package/src/tui/session/tui-steering-persist.mjs +25 -9
- package/src/tui/session/turn.mjs +17 -0
- package/src/runtime/effort-judge/previous-human-request.mjs +0 -162
- package/src/runtime/effort-judge/verified-context-input.mjs +0 -10
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code project transcript (`projects/<dir>/<session>.jsonl`) → raw
|
|
3
|
+
* import turns. The active branch is followed from the last main-chain entry
|
|
4
|
+
* back through `parentUuid` (and `logicalParentUuid` across a compaction), so
|
|
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.
|
|
10
|
+
*/
|
|
11
|
+
import { IMAGE_OMITTED_TEXT, IMPORT_WARNING_IMAGE_OMITTED, isWholeElement, textPart, toolNote } from './turns.mjs';
|
|
12
|
+
|
|
13
|
+
const CLAUDE_ENVELOPE_TAGS = [
|
|
14
|
+
'local-command-stdout',
|
|
15
|
+
'local-command-stderr',
|
|
16
|
+
'local-command-caveat',
|
|
17
|
+
'task-notification',
|
|
18
|
+
'system-reminder',
|
|
19
|
+
'user-prompt-submit-hook',
|
|
20
|
+
'bash-input',
|
|
21
|
+
'bash-stdout',
|
|
22
|
+
'bash-stderr',
|
|
23
|
+
];
|
|
24
|
+
const SLASH_COMMAND_RE = /^(?:\s*<(command-name|command-message|command-args)>[\s\S]*?<\/\1>)+\s*$/;
|
|
25
|
+
const INTERRUPTED_RE = /^\[Request interrupted by user[^\]\n]*\]$/;
|
|
26
|
+
// Claude Code's text annotation that accompanies a pasted image block.
|
|
27
|
+
const IMAGE_ANNOTATION_RE = /^\[Image\b[^\n]*\]$/;
|
|
28
|
+
|
|
29
|
+
export function isClaudeEnvelopeText(text) {
|
|
30
|
+
const trimmed = String(text || '').trim();
|
|
31
|
+
if (!trimmed) return true;
|
|
32
|
+
return (
|
|
33
|
+
isWholeElement(trimmed, CLAUDE_ENVELOPE_TAGS) ||
|
|
34
|
+
SLASH_COMMAND_RE.test(trimmed) ||
|
|
35
|
+
INTERRUPTED_RE.test(trimmed) ||
|
|
36
|
+
IMAGE_ANNOTATION_RE.test(trimmed)
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const isMessageEntry = (record) => record?.type === 'user' || record?.type === 'assistant';
|
|
41
|
+
const stringOrNull = (value) => (typeof value === 'string' && value.trim() ? value.trim() : null);
|
|
42
|
+
const uuidOrNull = (value) => (typeof value === 'string' ? value : null);
|
|
43
|
+
const timestampOrNull = (value) => (typeof value === 'string' || typeof value === 'number' ? value : null);
|
|
44
|
+
// Rough fixed cost of one retained entry object, so tiny entries still count.
|
|
45
|
+
const ENTRY_OVERHEAD_BYTES = 64;
|
|
46
|
+
const metadataLength = (entry) =>
|
|
47
|
+
(entry.uuid?.length || 0) +
|
|
48
|
+
(entry.parentUuid?.length || 0) +
|
|
49
|
+
(entry.logicalParentUuid?.length || 0) +
|
|
50
|
+
(typeof entry.at === 'string' ? entry.at.length : 0);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 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).
|
|
56
|
+
*/
|
|
57
|
+
function activeChain(entries) {
|
|
58
|
+
const main = entries.filter((entry) => entry.main);
|
|
59
|
+
const leaf = main[main.length - 1];
|
|
60
|
+
if (!leaf || leaf.uuid === null) return { chain: main, crossedCompaction: false };
|
|
61
|
+
const byUuid = new Map();
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
if (entry.uuid !== null) byUuid.set(entry.uuid, entry);
|
|
64
|
+
}
|
|
65
|
+
const chain = [];
|
|
66
|
+
const seen = new Set();
|
|
67
|
+
let crossedCompaction = false;
|
|
68
|
+
let current = leaf;
|
|
69
|
+
while (current && !seen.has(current.uuid)) {
|
|
70
|
+
seen.add(current.uuid);
|
|
71
|
+
chain.push(current);
|
|
72
|
+
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;
|
|
75
|
+
current = parent || logical;
|
|
76
|
+
}
|
|
77
|
+
return { chain: chain.reverse().filter((entry) => entry.main), crossedCompaction };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function entryParts(role, content, warnings) {
|
|
81
|
+
const blocks = typeof content === 'string' ? [{ type: 'text', text: content }] : content;
|
|
82
|
+
if (!Array.isArray(blocks)) return [];
|
|
83
|
+
const parts = [];
|
|
84
|
+
for (const block of blocks) {
|
|
85
|
+
if (!block || typeof block !== 'object') continue;
|
|
86
|
+
if (block.type === 'text' && typeof block.text === 'string') {
|
|
87
|
+
if (role === 'user' && isClaudeEnvelopeText(block.text)) continue;
|
|
88
|
+
parts.push(textPart(block.text));
|
|
89
|
+
} else if (block.type === 'image') {
|
|
90
|
+
warnings.add(IMPORT_WARNING_IMAGE_OMITTED);
|
|
91
|
+
parts.push(textPart(IMAGE_OMITTED_TEXT));
|
|
92
|
+
} else if (block.type === 'tool_use' && role === 'assistant') {
|
|
93
|
+
parts.push(toolNote(block.name));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return parts;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function skippedEntry(record) {
|
|
100
|
+
// Claude Code flags its compaction summary transcript-only as well.
|
|
101
|
+
if (record.isCompactSummary === true) return false;
|
|
102
|
+
return record.isMeta === true || record.isVisibleInTranscriptOnly === true || record.isApiErrorMessage === true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Incremental reducer: `push(record)` keeps a compact chain entry (uuid links
|
|
107
|
+
* plus the plain-text parts of a main-chain message) and drops the record
|
|
108
|
+
* itself, so tool results, thinking and image data are never retained.
|
|
109
|
+
* `chars` is the kept text across every branch so far, `entries` the chain
|
|
110
|
+
* entries retained and `metadataBytes` their link/timestamp bookkeeping.
|
|
111
|
+
*/
|
|
112
|
+
export function createClaudeReducer() {
|
|
113
|
+
const entries = [];
|
|
114
|
+
let recognized = false;
|
|
115
|
+
let foreignSessionId = null;
|
|
116
|
+
let cwd = null;
|
|
117
|
+
let chars = 0;
|
|
118
|
+
let metadataBytes = 0;
|
|
119
|
+
return {
|
|
120
|
+
get chars() {
|
|
121
|
+
return chars;
|
|
122
|
+
},
|
|
123
|
+
get entries() {
|
|
124
|
+
return entries.length;
|
|
125
|
+
},
|
|
126
|
+
get metadataBytes() {
|
|
127
|
+
return metadataBytes;
|
|
128
|
+
},
|
|
129
|
+
push(record) {
|
|
130
|
+
const message = isMessageEntry(record);
|
|
131
|
+
if (message) recognized = true;
|
|
132
|
+
const main = message && record.isSidechain !== true;
|
|
133
|
+
if (main) {
|
|
134
|
+
foreignSessionId ||= stringOrNull(record.sessionId);
|
|
135
|
+
cwd ||= stringOrNull(record.cwd);
|
|
136
|
+
}
|
|
137
|
+
const uuid = uuidOrNull(record?.uuid);
|
|
138
|
+
if (!main && uuid === null) return;
|
|
139
|
+
const entry = {
|
|
140
|
+
uuid,
|
|
141
|
+
parentUuid: uuidOrNull(record.parentUuid),
|
|
142
|
+
logicalParentUuid: uuidOrNull(record.logicalParentUuid),
|
|
143
|
+
main,
|
|
144
|
+
role: null,
|
|
145
|
+
parts: null,
|
|
146
|
+
warnings: null,
|
|
147
|
+
at: timestampOrNull(record.timestamp),
|
|
148
|
+
};
|
|
149
|
+
metadataBytes += ENTRY_OVERHEAD_BYTES + metadataLength(entry);
|
|
150
|
+
if (main && !skippedEntry(record)) {
|
|
151
|
+
const body = record.message && typeof record.message === 'object' ? record.message : null;
|
|
152
|
+
entry.role = body?.role === 'user' || body?.role === 'assistant' ? body.role : record.type;
|
|
153
|
+
const warnings = new Set();
|
|
154
|
+
entry.parts = entryParts(entry.role, body ? body.content : null, warnings);
|
|
155
|
+
if (record.isCompactSummary === true) {
|
|
156
|
+
entry.summary = true;
|
|
157
|
+
entry.parts = entry.parts.map((part) => ({ ...part, summary: true }));
|
|
158
|
+
}
|
|
159
|
+
if (warnings.size) entry.warnings = warnings;
|
|
160
|
+
for (const part of entry.parts) chars += part.text.length;
|
|
161
|
+
}
|
|
162
|
+
entries.push(entry);
|
|
163
|
+
},
|
|
164
|
+
result() {
|
|
165
|
+
const warnings = new Set();
|
|
166
|
+
const rawTurns = [];
|
|
167
|
+
const { chain, crossedCompaction } = activeChain(entries);
|
|
168
|
+
for (const entry of chain) {
|
|
169
|
+
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 });
|
|
173
|
+
for (const code of entry.warnings || []) warnings.add(code);
|
|
174
|
+
}
|
|
175
|
+
return { recognized, foreignSessionId, cwd, rawTurns, warnings };
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function convertClaudeRecords(records) {
|
|
181
|
+
const reducer = createClaudeReducer();
|
|
182
|
+
for (const record of records) reducer.push(record);
|
|
183
|
+
return reducer.result();
|
|
184
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex CLI rollout (`rollout-*.jsonl`) → raw import turns. Kept: the first
|
|
3
|
+
* `session_meta` (foreign id, cwd), user/assistant `message` items as text and
|
|
4
|
+
* tool calls as `[ran tool: <name>]` notes. Dropped: developer/system
|
|
5
|
+
* messages, Codex's own wrapper user messages, tool outputs, reasoning,
|
|
6
|
+
* event_msg, token counts and turn_context.
|
|
7
|
+
*/
|
|
8
|
+
import { IMAGE_OMITTED_TEXT, IMPORT_WARNING_IMAGE_OMITTED, isWholeElement, textPart, toolNote } from './turns.mjs';
|
|
9
|
+
|
|
10
|
+
// User-role envelopes Codex injects itself (environment, instructions, abort
|
|
11
|
+
// notices, plugin hints, shell-command echoes, image wrappers).
|
|
12
|
+
const CODEX_ENVELOPE_TAGS = [
|
|
13
|
+
'environment_context',
|
|
14
|
+
'user_instructions',
|
|
15
|
+
'turn_aborted',
|
|
16
|
+
'recommended_plugins',
|
|
17
|
+
'user_shell_command',
|
|
18
|
+
'subagent_notification',
|
|
19
|
+
'skills_instructions',
|
|
20
|
+
];
|
|
21
|
+
const TOOL_CALL_TYPES = new Set(['function_call', 'custom_tool_call', 'local_shell_call']);
|
|
22
|
+
|
|
23
|
+
export function isCodexEnvelopeText(text) {
|
|
24
|
+
const trimmed = String(text || '').trim();
|
|
25
|
+
if (!trimmed) return true;
|
|
26
|
+
if (/^# AGENTS\.md instructions\b/.test(trimmed) && trimmed.includes('<INSTRUCTIONS>')) return true;
|
|
27
|
+
if (isWholeElement(trimmed, CODEX_ENVELOPE_TAGS)) return true;
|
|
28
|
+
// The `<image name=…>` / `</image>` text parts that wrap an input_image.
|
|
29
|
+
return /^<image\b[^>]*>$/.test(trimmed) || trimmed === '</image>';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function messageParts(payload, warnings) {
|
|
33
|
+
const content = typeof payload.content === 'string' ? [{ type: 'text', text: payload.content }] : payload.content;
|
|
34
|
+
if (!Array.isArray(content)) return [];
|
|
35
|
+
const parts = [];
|
|
36
|
+
for (const item of content) {
|
|
37
|
+
if (!item || typeof item !== 'object') continue;
|
|
38
|
+
if (item.type === 'input_image' || item.type === 'image') {
|
|
39
|
+
warnings.add(IMPORT_WARNING_IMAGE_OMITTED);
|
|
40
|
+
parts.push(textPart(IMAGE_OMITTED_TEXT));
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (typeof item.text !== 'string') continue;
|
|
44
|
+
if (payload.role === 'user' && isCodexEnvelopeText(item.text)) continue;
|
|
45
|
+
parts.push(textPart(item.text));
|
|
46
|
+
}
|
|
47
|
+
return parts;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function toolName(payload) {
|
|
51
|
+
if (typeof payload.name === 'string' && payload.name.trim()) return payload.name;
|
|
52
|
+
return payload.type === 'local_shell_call' ? 'local_shell' : '';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const stringOrNull = (value) => (typeof value === 'string' && value.trim() ? value.trim() : null);
|
|
56
|
+
// Rough fixed cost of one retained turn object, so tiny turns still count.
|
|
57
|
+
const ENTRY_OVERHEAD_BYTES = 64;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Incremental reducer: `push(record)` keeps only the plain-text parts of one
|
|
61
|
+
* record (the record itself is not retained), `chars` is the kept text so far,
|
|
62
|
+
* `entries` the raw turns kept and `metadataBytes` their timestamp bookkeeping.
|
|
63
|
+
*/
|
|
64
|
+
export function createCodexReducer() {
|
|
65
|
+
const warnings = new Set();
|
|
66
|
+
const rawTurns = [];
|
|
67
|
+
let recognized = false;
|
|
68
|
+
let foreignSessionId = null;
|
|
69
|
+
let cwd = null;
|
|
70
|
+
let metaSeen = false;
|
|
71
|
+
let chars = 0;
|
|
72
|
+
let metadataBytes = 0;
|
|
73
|
+
const keep = (role, parts, at) => {
|
|
74
|
+
for (const part of parts) chars += part.text.length;
|
|
75
|
+
const stamp = typeof at === 'string' || typeof at === 'number' ? at : null;
|
|
76
|
+
metadataBytes += ENTRY_OVERHEAD_BYTES + (typeof stamp === 'string' ? stamp.length : 0);
|
|
77
|
+
rawTurns.push({ role, parts, at: stamp });
|
|
78
|
+
};
|
|
79
|
+
return {
|
|
80
|
+
get chars() {
|
|
81
|
+
return chars;
|
|
82
|
+
},
|
|
83
|
+
get entries() {
|
|
84
|
+
return rawTurns.length;
|
|
85
|
+
},
|
|
86
|
+
get metadataBytes() {
|
|
87
|
+
return metadataBytes;
|
|
88
|
+
},
|
|
89
|
+
push(record) {
|
|
90
|
+
const payload = record?.payload && typeof record.payload === 'object' ? record.payload : null;
|
|
91
|
+
if (!payload) return;
|
|
92
|
+
if (record.type === 'session_meta') {
|
|
93
|
+
recognized = true;
|
|
94
|
+
if (!metaSeen) {
|
|
95
|
+
metaSeen = true;
|
|
96
|
+
foreignSessionId = stringOrNull(payload.id) || stringOrNull(payload.session_id);
|
|
97
|
+
cwd = stringOrNull(payload.cwd);
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (record.type !== 'response_item') return;
|
|
102
|
+
recognized = true;
|
|
103
|
+
if (payload.type === 'message' && (payload.role === 'user' || payload.role === 'assistant')) {
|
|
104
|
+
const parts = messageParts(payload, warnings);
|
|
105
|
+
if (parts.length) keep(payload.role, parts, record.timestamp);
|
|
106
|
+
} else if (TOOL_CALL_TYPES.has(payload.type)) {
|
|
107
|
+
keep('assistant', [toolNote(toolName(payload))], record.timestamp);
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
result() {
|
|
111
|
+
return { recognized, foreignSessionId, cwd, rawTurns, warnings };
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function convertCodexRecords(records) {
|
|
117
|
+
const reducer = createCodexReducer();
|
|
118
|
+
for (const record of records) reducer.push(record);
|
|
119
|
+
return reducer.result();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** id/cwd of a `session_meta` first line cut off by a bounded prefix read. */
|
|
123
|
+
export function codexMetaFromTruncatedLine(line) {
|
|
124
|
+
const text = String(line || '');
|
|
125
|
+
if (!/"type"\s*:\s*"session_meta"/.test(text)) return null;
|
|
126
|
+
const field = (name) => {
|
|
127
|
+
const match = new RegExp(`"${name}"\\s*:\\s*("(?:[^"\\\\]|\\\\.)*")`).exec(text);
|
|
128
|
+
if (!match) return null;
|
|
129
|
+
try {
|
|
130
|
+
return stringOrNull(JSON.parse(match[1]));
|
|
131
|
+
} catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
return { foreignSessionId: field('id') || field('session_id'), cwd: field('cwd') };
|
|
136
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSONL lines of a foreign conversation → normalized plain-text turns, one
|
|
3
|
+
* line at a time: each parsed record is reduced to its kept plain text and
|
|
4
|
+
* dropped. Tolerates a BOM, CRLF line ends and individual malformed lines
|
|
5
|
+
* (reported as the `malformed` warning). Pure, no IO.
|
|
6
|
+
*/
|
|
7
|
+
import { createClaudeReducer } from './claude.mjs';
|
|
8
|
+
import { createCodexReducer } from './codex.mjs';
|
|
9
|
+
import { IMPORT_WARNING_MALFORMED, conversationTitle, normalizeTurns } from './turns.mjs';
|
|
10
|
+
|
|
11
|
+
export const IMPORT_SOURCES = Object.freeze(['codex', 'claude']);
|
|
12
|
+
/** A line or converted transcript beyond the import limits fails the file. */
|
|
13
|
+
export const IMPORT_ERROR_TOO_LARGE = 'tooLarge';
|
|
14
|
+
export const IMPORT_MAX_LINE_BYTES = 32 * 1024 * 1024;
|
|
15
|
+
/** Bytes read from one source file before it fails as `tooLarge`. */
|
|
16
|
+
export const IMPORT_MAX_FILE_BYTES = 256 * 1024 * 1024;
|
|
17
|
+
/** Normalized transcript text (the final turns, separators and stub included). */
|
|
18
|
+
export const IMPORT_MAX_TRANSCRIPT_CHARS = 4 * 1024 * 1024;
|
|
19
|
+
/** Entries a reducer may retain (Claude chain links, Codex turns). */
|
|
20
|
+
export const IMPORT_MAX_RETAINED_ENTRIES = 200_000;
|
|
21
|
+
/** Bookkeeping a reducer retains besides kept text (ids, links, timestamps). */
|
|
22
|
+
export const IMPORT_MAX_METADATA_BYTES = 16 * 1024 * 1024;
|
|
23
|
+
|
|
24
|
+
export function isImportSource(value) {
|
|
25
|
+
return IMPORT_SOURCES.includes(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const transcriptChars = (turns) => turns.reduce((total, turn) => total + turn.text.length, 0);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Incremental converter. `pushLine(line)` returns false once the conversion
|
|
32
|
+
* is over a limit (`tooLarge`): kept text over `maxTranscriptChars`, retained
|
|
33
|
+
* entries over `maxRetainedEntries` or their bookkeeping over
|
|
34
|
+
* `maxMetadataBytes`; `markTooLarge()` records a limit enforced by the reader
|
|
35
|
+
* (an over-long line or file). `result()` may be called repeatedly and also
|
|
36
|
+
* holds the normalized turns to `maxTranscriptChars`.
|
|
37
|
+
* `errorCode` is null for an importable result, else one of `empty` |
|
|
38
|
+
* `malformed` | `unsupported` | `tooLarge` (whose turns are not returned).
|
|
39
|
+
*/
|
|
40
|
+
export function createConversationConverter(
|
|
41
|
+
source,
|
|
42
|
+
{
|
|
43
|
+
maxTranscriptChars = IMPORT_MAX_TRANSCRIPT_CHARS,
|
|
44
|
+
maxRetainedEntries = IMPORT_MAX_RETAINED_ENTRIES,
|
|
45
|
+
maxMetadataBytes = IMPORT_MAX_METADATA_BYTES,
|
|
46
|
+
} = {}
|
|
47
|
+
) {
|
|
48
|
+
if (!isImportSource(source)) throw new TypeError('source must be codex or claude.');
|
|
49
|
+
const reducer = source === 'codex' ? createCodexReducer() : createClaudeReducer();
|
|
50
|
+
let lines = 0;
|
|
51
|
+
let records = 0;
|
|
52
|
+
let malformed = 0;
|
|
53
|
+
let tooLarge = false;
|
|
54
|
+
return {
|
|
55
|
+
markTooLarge() {
|
|
56
|
+
tooLarge = true;
|
|
57
|
+
},
|
|
58
|
+
pushLine(raw) {
|
|
59
|
+
if (tooLarge) return false;
|
|
60
|
+
let line = String(raw);
|
|
61
|
+
if (lines++ === 0 && line.charCodeAt(0) === 0xfeff) line = line.slice(1);
|
|
62
|
+
if (line.endsWith('\r')) line = line.slice(0, -1);
|
|
63
|
+
if (!line.trim()) return true;
|
|
64
|
+
let value;
|
|
65
|
+
try {
|
|
66
|
+
value = JSON.parse(line);
|
|
67
|
+
} catch {
|
|
68
|
+
malformed += 1;
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
72
|
+
malformed += 1;
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
records += 1;
|
|
76
|
+
reducer.push(value);
|
|
77
|
+
if (
|
|
78
|
+
reducer.chars > maxTranscriptChars ||
|
|
79
|
+
reducer.entries > maxRetainedEntries ||
|
|
80
|
+
reducer.metadataBytes > maxMetadataBytes
|
|
81
|
+
) {
|
|
82
|
+
tooLarge = true;
|
|
83
|
+
}
|
|
84
|
+
return !tooLarge;
|
|
85
|
+
},
|
|
86
|
+
result() {
|
|
87
|
+
const parsed = reducer.result();
|
|
88
|
+
const turns = normalizeTurns(parsed.rawTurns);
|
|
89
|
+
// Joins and the user stub add text the reducer never counted.
|
|
90
|
+
if (transcriptChars(turns) > maxTranscriptChars) tooLarge = true;
|
|
91
|
+
const warnings = new Set(parsed.warnings);
|
|
92
|
+
if (malformed) warnings.add(IMPORT_WARNING_MALFORMED);
|
|
93
|
+
let errorCode = null;
|
|
94
|
+
if (tooLarge) errorCode = IMPORT_ERROR_TOO_LARGE;
|
|
95
|
+
else if (!records) errorCode = malformed ? 'malformed' : 'empty';
|
|
96
|
+
else if (!parsed.recognized) errorCode = 'unsupported';
|
|
97
|
+
else if (!turns.length) errorCode = 'empty';
|
|
98
|
+
return {
|
|
99
|
+
source,
|
|
100
|
+
errorCode,
|
|
101
|
+
foreignSessionId: parsed.foreignSessionId,
|
|
102
|
+
cwd: parsed.cwd,
|
|
103
|
+
turns: tooLarge ? [] : turns,
|
|
104
|
+
title: conversationTitle(turns),
|
|
105
|
+
messageCount: tooLarge ? 0 : turns.length,
|
|
106
|
+
warnings: [...warnings],
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Convert an in-memory JSONL body; `complete:false` drops a trailing partial line. */
|
|
113
|
+
export function convertConversationText(source, text, { complete = true, ...limits } = {}) {
|
|
114
|
+
const converter = createConversationConverter(source, limits);
|
|
115
|
+
let body = String(text ?? '');
|
|
116
|
+
if (!complete) {
|
|
117
|
+
const end = body.lastIndexOf('\n');
|
|
118
|
+
body = end < 0 ? '' : body.slice(0, end);
|
|
119
|
+
}
|
|
120
|
+
let start = 0;
|
|
121
|
+
while (start < body.length) {
|
|
122
|
+
const newline = body.indexOf('\n', start);
|
|
123
|
+
const end = newline < 0 ? body.length : newline;
|
|
124
|
+
if (!converter.pushLine(body.slice(start, end))) break;
|
|
125
|
+
start = end + 1;
|
|
126
|
+
}
|
|
127
|
+
return converter.result();
|
|
128
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Materialize an imported conversation as an ordinary Lead session: the normal
|
|
3
|
+
* createSession initialization (route, tools, context limits, cache config,
|
|
4
|
+
* system messages, BP3 fields) followed by the converted plain-text turns,
|
|
5
|
+
* with `importOrigin` provenance. Persisted durably before returning, then
|
|
6
|
+
* dropped from the same-process live cache.
|
|
7
|
+
*/
|
|
8
|
+
import { createSession } from '../manager/session-lifecycle.mjs';
|
|
9
|
+
import { deleteSession, evictLiveSession, saveSessionAsync } from '../store.mjs';
|
|
10
|
+
import { newTranscriptMessageId } from '../../../../shared/transcript-metadata.mjs';
|
|
11
|
+
import { writeStderrBestEffort } from '../../../../shared/best-effort-stderr.mjs';
|
|
12
|
+
import { IMPORTED_MESSAGE_SOURCE } from '../imported-message.mjs';
|
|
13
|
+
import { ingestSessionTurn } from '../../../../session-search/session-search-ingest.mjs';
|
|
14
|
+
|
|
15
|
+
export const IMPORT_ORIGIN_VERSION = 1;
|
|
16
|
+
|
|
17
|
+
/** Dedupe identity: the foreign session id, else the canonical source path. */
|
|
18
|
+
export function importOriginKey(source, foreignSessionId, canonicalPath) {
|
|
19
|
+
return foreignSessionId ? `${source}:${foreignSessionId}` : `${source}:path:${canonicalPath}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function buildImportOrigin({ source, sourcePath, foreignSessionId = null, sourceCwd = null, importedAt }) {
|
|
23
|
+
return {
|
|
24
|
+
version: IMPORT_ORIGIN_VERSION,
|
|
25
|
+
source,
|
|
26
|
+
sourcePath,
|
|
27
|
+
foreignSessionId: foreignSessionId || null,
|
|
28
|
+
sourceCwd: sourceCwd || null,
|
|
29
|
+
importedAt,
|
|
30
|
+
key: importOriginKey(source, foreignSessionId, sourcePath),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Lead identity every imported session carries (what a normal Lead session has).
|
|
35
|
+
const LEAD_FIELDS = Object.freeze({ owner: 'cli', agent: 'lead', lane: 'cli', sourceType: 'lead', sourceName: 'main' });
|
|
36
|
+
// Ownership, live binding and placement the importing runtime must not lend.
|
|
37
|
+
const NOT_INHERITED = [
|
|
38
|
+
'id',
|
|
39
|
+
'parentSessionId',
|
|
40
|
+
'ownerSessionId',
|
|
41
|
+
'visibility',
|
|
42
|
+
'agentTag',
|
|
43
|
+
'taskType',
|
|
44
|
+
'permission',
|
|
45
|
+
'permissionMode',
|
|
46
|
+
'clientHostPid',
|
|
47
|
+
'mcpScopeId',
|
|
48
|
+
'approvalMode',
|
|
49
|
+
'scopeKey',
|
|
50
|
+
'cwd',
|
|
51
|
+
'desktopSession',
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
/** Lead createSession options → options of a fresh, unbound imported session. */
|
|
55
|
+
export function importedSessionOptions(leadOptions, { cwd = null, desktopSession }) {
|
|
56
|
+
const options = { ...leadOptions };
|
|
57
|
+
for (const key of NOT_INHERITED) delete options[key];
|
|
58
|
+
Object.assign(options, LEAD_FIELDS);
|
|
59
|
+
if (cwd) options.cwd = cwd;
|
|
60
|
+
options.desktopSession = desktopSession;
|
|
61
|
+
return options;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Normalized turns → plain user/assistant transcript messages. `meta.source`
|
|
65
|
+
* marks them imported prose: restore and the model wire never read their text
|
|
66
|
+
* as runtime control content (reminders, task envelopes, summaries). */
|
|
67
|
+
export function importedTranscriptMessages(turns, importedAt) {
|
|
68
|
+
return turns.map((turn) => ({
|
|
69
|
+
role: turn.role,
|
|
70
|
+
content: turn.text,
|
|
71
|
+
meta: {
|
|
72
|
+
source: IMPORTED_MESSAGE_SOURCE,
|
|
73
|
+
transcript: { messageId: newTranscriptMessageId(), at: turn.at || importedAt },
|
|
74
|
+
},
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
79
|
+
|
|
80
|
+
// createSession's own debounced save keeps the id pinned until it settles.
|
|
81
|
+
async function evictWhenSettled(id, timeoutMs) {
|
|
82
|
+
const deadline = Date.now() + timeoutMs;
|
|
83
|
+
while (!evictLiveSession(id)) {
|
|
84
|
+
if (Date.now() >= deadline) return false;
|
|
85
|
+
await sleep(25);
|
|
86
|
+
}
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Create, fill and persist one imported session. Save failures reject (the
|
|
92
|
+
* half-made record is removed); once saved it is handed to content-search
|
|
93
|
+
* ingestion (the same fire-and-forget ingest a finished turn gets) so it is
|
|
94
|
+
* searchable right away. The live-cache eviction afterwards is bookkeeping only.
|
|
95
|
+
*/
|
|
96
|
+
export async function createImportedSession({
|
|
97
|
+
leadOptions,
|
|
98
|
+
conversation,
|
|
99
|
+
origin,
|
|
100
|
+
cwd = null,
|
|
101
|
+
desktopSession,
|
|
102
|
+
createSessionFn = createSession,
|
|
103
|
+
ingestFn = ingestSessionTurn,
|
|
104
|
+
evictTimeoutMs = 5_000,
|
|
105
|
+
}) {
|
|
106
|
+
const session = createSessionFn(importedSessionOptions(leadOptions, { cwd, desktopSession }));
|
|
107
|
+
try {
|
|
108
|
+
// Append after the system layer createSession seeded; never replace it.
|
|
109
|
+
session.messages.push(...importedTranscriptMessages(conversation.turns, origin.importedAt));
|
|
110
|
+
session.importOrigin = origin;
|
|
111
|
+
if (conversation.title) session.title = conversation.title;
|
|
112
|
+
session.updatedAt = Date.now();
|
|
113
|
+
await saveSessionAsync(session);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
try {
|
|
116
|
+
deleteSession(session.id);
|
|
117
|
+
} catch (cleanupError) {
|
|
118
|
+
writeStderrBestEffort(
|
|
119
|
+
`[conversation-import] cleanup of ${session.id} failed: ${cleanupError?.message || cleanupError}\n`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
ingestFn(session);
|
|
125
|
+
await evictWhenSettled(session.id, evictTimeoutMs);
|
|
126
|
+
return session;
|
|
127
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared shape of an imported conversation: plain-text user/assistant turns,
|
|
3
|
+
* the markers that stand in for dropped content, and the title rule. Codex and
|
|
4
|
+
* Claude Code parsers both emit raw turns `{ role, parts: [{ text, note }], at }`
|
|
5
|
+
* and normalize them here. Pure, leaf, no IO.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Language-neutral warning codes attached to an imported conversation. */
|
|
9
|
+
export const IMPORT_WARNING_IMAGE_OMITTED = 'imageOmitted';
|
|
10
|
+
export const IMPORT_WARNING_MALFORMED = 'malformed';
|
|
11
|
+
export const IMPORT_WARNING_CWD_MISSING = 'cwdMissing';
|
|
12
|
+
|
|
13
|
+
/** Stands in for an image the importer does not carry over. */
|
|
14
|
+
export const IMAGE_OMITTED_TEXT = '[image omitted]';
|
|
15
|
+
/** Leads a conversation whose first kept turn is the assistant's. */
|
|
16
|
+
export const IMPORTED_USER_STUB_TEXT = '[Imported conversation continues from an earlier assistant reply.]';
|
|
17
|
+
|
|
18
|
+
const TITLE_MAX_CHARS = 100;
|
|
19
|
+
|
|
20
|
+
/** Plain-text note for a tool call: its name only, never its arguments. */
|
|
21
|
+
export function toolNote(name) {
|
|
22
|
+
const clean = String(name || '')
|
|
23
|
+
.replace(/\s+/g, ' ')
|
|
24
|
+
.trim();
|
|
25
|
+
return { text: `[ran tool: ${clean || 'unknown'}]`, note: true };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function textPart(text) {
|
|
29
|
+
return { text: String(text), note: false };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Whole-text match of one XML-style envelope element (`<tag>…</tag>`). */
|
|
33
|
+
export function isWholeElement(text, tags) {
|
|
34
|
+
const trimmed = String(text || '').trim();
|
|
35
|
+
return tags.some(
|
|
36
|
+
(tag) => (trimmed.startsWith(`<${tag}>`) || trimmed.startsWith(`<${tag} `)) && trimmed.endsWith(`</${tag}>`)
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function timestampMs(value) {
|
|
41
|
+
const ms = typeof value === 'number' ? value : Date.parse(String(value || ''));
|
|
42
|
+
return Number.isFinite(ms) && ms > 0 ? ms : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Consecutive tool notes stay one per line; every other boundary is a blank line.
|
|
46
|
+
function joinParts(parts) {
|
|
47
|
+
let text = '';
|
|
48
|
+
let previous = null;
|
|
49
|
+
for (const part of parts) {
|
|
50
|
+
const piece = String(part.text || '').trim();
|
|
51
|
+
if (!piece) continue;
|
|
52
|
+
if (previous) text += previous.note && part.note ? '\n' : '\n\n';
|
|
53
|
+
text += piece;
|
|
54
|
+
previous = part;
|
|
55
|
+
}
|
|
56
|
+
return { text, last: previous };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Raw turns → normalized turns `{ role, text, at, stub? }`: empty turns dropped,
|
|
61
|
+
* consecutive same-role turns merged, and a short user stub inserted when the
|
|
62
|
+
* conversation would otherwise begin with the assistant.
|
|
63
|
+
*/
|
|
64
|
+
export function normalizeTurns(rawTurns) {
|
|
65
|
+
const merged = [];
|
|
66
|
+
for (const raw of rawTurns) {
|
|
67
|
+
if (raw?.role !== 'user' && raw?.role !== 'assistant') continue;
|
|
68
|
+
const parts = (Array.isArray(raw.parts) ? raw.parts : []).filter((part) => String(part?.text || '').trim());
|
|
69
|
+
if (!parts.length) continue;
|
|
70
|
+
const previous = merged[merged.length - 1];
|
|
71
|
+
if (previous && previous.role === raw.role) previous.parts.push(...parts);
|
|
72
|
+
else merged.push({ role: raw.role, parts: [...parts], at: timestampMs(raw.at) });
|
|
73
|
+
}
|
|
74
|
+
const turns = merged.map(({ role, parts, at }) => {
|
|
75
|
+
const turn = { role, text: joinParts(parts).text, at };
|
|
76
|
+
// A carried-over compaction summary never names the conversation.
|
|
77
|
+
if (parts.some((part) => part.summary)) turn.titleText = joinParts(parts.filter((part) => !part.summary)).text;
|
|
78
|
+
return turn;
|
|
79
|
+
});
|
|
80
|
+
if (turns[0]?.role === 'assistant') {
|
|
81
|
+
turns.unshift({ role: 'user', text: IMPORTED_USER_STUB_TEXT, at: turns[0].at, stub: true });
|
|
82
|
+
}
|
|
83
|
+
return turns;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const titleLines = (turn) =>
|
|
87
|
+
(turn.titleText ?? turn.text)
|
|
88
|
+
.split('\n')
|
|
89
|
+
.map((value) => value.replace(/\s+/g, ' ').trim())
|
|
90
|
+
.filter(Boolean);
|
|
91
|
+
|
|
92
|
+
/** First line of the first genuine user turn, whitespace-normalized, ≤100 chars
|
|
93
|
+
* (an image marker only when the turn has no text line). */
|
|
94
|
+
export function conversationTitle(turns) {
|
|
95
|
+
const first = turns.find((turn) => turn.role === 'user' && !turn.stub && titleLines(turn).length);
|
|
96
|
+
if (!first) return null;
|
|
97
|
+
const lines = titleLines(first);
|
|
98
|
+
const line = lines.find((value) => value !== IMAGE_OMITTED_TEXT) || lines[0];
|
|
99
|
+
if (!line) return null;
|
|
100
|
+
return Array.from(line).slice(0, TITLE_MAX_CHARS).join('').trim() || null;
|
|
101
|
+
}
|