@yugabytedb/meko-mcp 2.1.0 → 2.3.0
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 +233 -40
- package/assets/hooks-handlers/codex-session-end.sh +4 -69
- package/assets/hooks-handlers/cursor-session-start.sh +19 -1
- package/assets/hooks-handlers/lib/capture-cleanup-cli.js +1473 -0
- package/assets/hooks-handlers/lib/capture-notices.js +438 -0
- package/assets/hooks-handlers/lib/capture-state.js +2173 -0
- package/assets/hooks-handlers/lib/capture-status-cli.js +86 -0
- package/assets/hooks-handlers/lib/capture.js +2782 -403
- package/assets/hooks-handlers/lib/checkpoint-timer.js +485 -81
- package/assets/hooks-handlers/lib/journal-bridge.js +218 -0
- package/assets/hooks-handlers/lib/kiro-handler.js +760 -0
- package/assets/hooks-handlers/lib/opencode-handler.js +620 -0
- package/assets/hooks-handlers/lib/opencode-normalizer.js +145 -0
- package/assets/hooks-handlers/lib/terminal-handoff.js +427 -0
- package/assets/hooks-handlers/session-end.sh +3 -34
- package/assets/opencode-plugin/meko.js +213 -0
- package/assets/scripts/meko-statusline.sh +123 -16
- package/assets/skills/meko-mcp-tools/SKILL.md +116 -126
- package/assets/skills/meko-mcp-tools/references/_sections.md +3 -3
- package/assets/skills/meko-mcp-tools/references/tools-agent-id-conventions.md +25 -18
- package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +99 -24
- package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +1 -1
- package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +53 -6
- package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +25 -79
- package/assets/skills/meko-mcp-tools/references/tools-overview.md +37 -26
- package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +14 -8
- package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +66 -1
- package/assets/skills/meko-mcp-tools/references/tools-user-id-conventions.md +7 -40
- package/assets/skills/meko-mcp-tools-desktop/SKILL.md +56 -20
- package/assets/skills/meko-mcp-tools-desktop/desktop/README.md +1 -1
- package/assets/skills/meko-mcp-tools-desktop/references/_sections.md +1 -1
- package/assets/skills/meko-mcp-tools-desktop/references/tools-agent-id-conventions.md +15 -8
- package/assets/skills/meko-mcp-tools-desktop/references/tools-cookbook.md +99 -36
- package/assets/skills/meko-mcp-tools-desktop/references/tools-known-limitations.md +22 -5
- package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +18 -14
- package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +39 -28
- package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +14 -8
- package/assets/skills/meko-mcp-tools-desktop/references/tools-troubleshooting.md +67 -1
- package/assets/skills/meko-select-datapack-desktop/SKILL.md +4 -4
- package/assets/skills/meko-select-datapack-desktop/references/cookbook.md +8 -8
- package/assets/skills/meko-select-datapack-desktop/references/role-display-future.md +7 -5
- package/assets/skills/meko-select-datapack-desktop/references/troubleshooting.md +4 -4
- package/bin/create-meko-setup.mjs +113 -12
- package/lib/capture-cleanup.mjs +63 -0
- package/lib/capture-status.mjs +67 -0
- package/lib/clients/aliases.mjs +5 -0
- package/lib/clients/claude-code.mjs +1 -0
- package/lib/clients/claude-desktop.mjs +120 -5
- package/lib/clients/codex.mjs +85 -37
- package/lib/clients/common.mjs +7 -0
- package/lib/clients/index.mjs +4 -0
- package/lib/clients/kiro.mjs +705 -0
- package/lib/clients/opencode.mjs +532 -0
- package/lib/config.mjs +158 -1
- package/lib/installer.mjs +18 -1
- package/lib/migrate/adapters/claude-code.mjs +11 -2
- package/lib/migrate/adapters/codex.mjs +497 -0
- package/lib/migrate/adapters/memory-md.mjs +27 -8
- package/lib/migrate/adapters/opencode.mjs +148 -0
- package/lib/migrate/claude-parser.mjs +153 -9
- package/lib/migrate/id.mjs +217 -10
- package/lib/migrate/mcp-client.mjs +25 -1
- package/lib/migrate/opencode-normalizer.mjs +132 -0
- package/lib/migrate/orchestrator.mjs +299 -56
- package/lib/migrate/progress.mjs +239 -5
- package/lib/migrate/registry.mjs +72 -0
- package/lib/migrate.mjs +100 -10
- package/lib/prompt.mjs +16 -1
- package/package.json +4 -4
- package/scripts/bundle-assets.mjs +4 -0
|
@@ -93,7 +93,31 @@ export class McpClient {
|
|
|
93
93
|
lastErr = err;
|
|
94
94
|
const code = err.statusCode;
|
|
95
95
|
if (code === 401 || code === 403) {
|
|
96
|
-
// Auth
|
|
96
|
+
// Auth errors are never worth retrying: the same credential will
|
|
97
|
+
// be rejected again. Whether they're fatal depends on the caller.
|
|
98
|
+
//
|
|
99
|
+
// A fire-and-forget call is one whose loss the run can absorb
|
|
100
|
+
// (conversation_add_message, memory_add). A token that expires
|
|
101
|
+
// partway through a multi-hour migration used to kill the whole
|
|
102
|
+
// run from inside one such call; now the call is dropped, the skip
|
|
103
|
+
// is recorded, and the run finishes with the gap visible.
|
|
104
|
+
//
|
|
105
|
+
// The initial auth probe is `connect()`, which does not come
|
|
106
|
+
// through here, so a bad key still aborts before any work starts.
|
|
107
|
+
if (opts.fireAndForget) {
|
|
108
|
+
this.log.warn?.("mcp_skipped_auth", {
|
|
109
|
+
tool: name,
|
|
110
|
+
statusCode: code,
|
|
111
|
+
error: err.message,
|
|
112
|
+
});
|
|
113
|
+
return {
|
|
114
|
+
ok: false,
|
|
115
|
+
skipped: true,
|
|
116
|
+
authSkipped: true,
|
|
117
|
+
statusCode: code,
|
|
118
|
+
error: { message: err.message },
|
|
119
|
+
};
|
|
120
|
+
}
|
|
97
121
|
throw err;
|
|
98
122
|
}
|
|
99
123
|
if (attempt < maxRetries) {
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/** OpenCode session → canonical exchanges. Keep in sync with the CJS handler mirror. */
|
|
2
|
+
|
|
3
|
+
const SYNTHETIC_PART_TYPES = new Set(["compaction", "subtask", "continuation"]);
|
|
4
|
+
const EXCLUDED_PART_TYPES = new Set([
|
|
5
|
+
"tool", "tool-invocation", "tool-result", "tool_result", "reasoning",
|
|
6
|
+
"file", "attachment", "step", "snapshot", "patch",
|
|
7
|
+
]);
|
|
8
|
+
const INTERRUPTED_MARKER =
|
|
9
|
+
"[Meko capture: assistant response was interrupted before completion.]";
|
|
10
|
+
export function normalizeOpenCodeSession(exportShape, opts = {}) {
|
|
11
|
+
const root = exportShape && typeof exportShape === "object" ? exportShape : {};
|
|
12
|
+
const info = root.info && typeof root.info === "object" ? root.info : {};
|
|
13
|
+
const rawMessages = Array.isArray(root.messages) ? root.messages : [];
|
|
14
|
+
const sessionId = firstString(info.id, info.sessionID, info.sessionId, root.sessionId) || "unknown";
|
|
15
|
+
const cwd = firstString(info.cwd, info.directory, info.projectPath, root.cwd) || "";
|
|
16
|
+
const parentSessionId = firstString(info.parentID, info.parentId, info.parentSessionId) || null;
|
|
17
|
+
const title = firstString(info.title, info.name) || sessionId;
|
|
18
|
+
const startedAt = firstTimestamp(info.time?.created, info.created, info.startedAt, info.time);
|
|
19
|
+
const visibleMessages = rawMessages.map(normalizeMessage).filter(Boolean).sort(compareMessageOrder);
|
|
20
|
+
const exchanges = [];
|
|
21
|
+
let pendingUser = null;
|
|
22
|
+
let firstUserText = "";
|
|
23
|
+
for (const msg of visibleMessages) {
|
|
24
|
+
if (msg.role === "user") {
|
|
25
|
+
if (pendingUser) exchanges.push(makeExchange(pendingUser, null, { forceInterrupted: true }));
|
|
26
|
+
pendingUser = msg;
|
|
27
|
+
if (!firstUserText && msg.text) firstUserText = msg.text.slice(0, 240);
|
|
28
|
+
} else if (msg.role === "assistant" && pendingUser) {
|
|
29
|
+
exchanges.push(makeExchange(pendingUser, msg, {}));
|
|
30
|
+
pendingUser = null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
let partial = Boolean(pendingUser);
|
|
34
|
+
if (pendingUser && (opts.terminalError || opts.interrupted)) {
|
|
35
|
+
exchanges.push(makeExchange(pendingUser, null, { ...opts, interrupted: true }));
|
|
36
|
+
pendingUser = null;
|
|
37
|
+
partial = false;
|
|
38
|
+
} else if (!pendingUser && (opts.terminalError || opts.interrupted) && exchanges.length) {
|
|
39
|
+
markInterrupted(exchanges[exchanges.length - 1]);
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
sessionId, cwd, parentSessionId, title,
|
|
43
|
+
startedAt: startedAt || visibleMessages[0]?.timestamp || null,
|
|
44
|
+
firstUserText,
|
|
45
|
+
exchanges: exchanges.filter((ex) => ex.input || ex.output),
|
|
46
|
+
partial,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function normalizeMessage(raw) {
|
|
50
|
+
if (!raw || typeof raw !== "object") return null;
|
|
51
|
+
const info = raw.info && typeof raw.info === "object" ? raw.info : raw;
|
|
52
|
+
const role = firstString(info.role, raw.role)?.toLowerCase();
|
|
53
|
+
if (role !== "user" && role !== "assistant") return null;
|
|
54
|
+
const synthetic = Boolean(
|
|
55
|
+
info.synthetic || info.compaction || info.subtask || info.continuation ||
|
|
56
|
+
SYNTHETIC_PART_TYPES.has(String(info.type || "").toLowerCase()),
|
|
57
|
+
);
|
|
58
|
+
if (synthetic) return null;
|
|
59
|
+
const parts = Array.isArray(raw.parts) ? raw.parts : [];
|
|
60
|
+
const text = joinVisibleText(parts);
|
|
61
|
+
if (!text && role === "assistant") return null;
|
|
62
|
+
return {
|
|
63
|
+
role,
|
|
64
|
+
id: firstString(info.id, raw.id, raw.uuid) || "",
|
|
65
|
+
timestamp: firstTimestamp(info.time?.created, info.createdAt, info.timestamp, raw.timestamp) || "",
|
|
66
|
+
text,
|
|
67
|
+
order: Number.isFinite(Number(info.order)) ? Number(info.order) : null,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function joinVisibleText(parts) {
|
|
71
|
+
const chunks = [];
|
|
72
|
+
for (const part of parts) {
|
|
73
|
+
if (!part || typeof part !== "object") continue;
|
|
74
|
+
if (part.synthetic || part.ignored) continue;
|
|
75
|
+
const type = String(part.type || "text").toLowerCase();
|
|
76
|
+
if (EXCLUDED_PART_TYPES.has(type) || SYNTHETIC_PART_TYPES.has(type)) continue;
|
|
77
|
+
if (type !== "text") continue;
|
|
78
|
+
const text = typeof part.text === "string" ? part.text : "";
|
|
79
|
+
if (!text.trim()) continue;
|
|
80
|
+
chunks.push(text);
|
|
81
|
+
}
|
|
82
|
+
return chunks.join("\n");
|
|
83
|
+
}
|
|
84
|
+
function compareMessageOrder(a, b) {
|
|
85
|
+
const ao = a.order; const bo = b.order;
|
|
86
|
+
if (ao != null && bo != null && ao !== bo) return ao - bo;
|
|
87
|
+
if (a.timestamp && b.timestamp && a.timestamp !== b.timestamp) {
|
|
88
|
+
return a.timestamp < b.timestamp ? -1 : 1;
|
|
89
|
+
}
|
|
90
|
+
return String(a.id).localeCompare(String(b.id));
|
|
91
|
+
}
|
|
92
|
+
function makeExchange(userMsg, assistantMsg, opts) {
|
|
93
|
+
const interrupted = Boolean(opts.interrupted || (!assistantMsg && opts.forceInterrupted));
|
|
94
|
+
return {
|
|
95
|
+
userId: userMsg.id || `opencode-user-${hashish(userMsg.text)}`,
|
|
96
|
+
assistantId: assistantMsg?.id || null,
|
|
97
|
+
input: userMsg.text,
|
|
98
|
+
output: interrupted ? INTERRUPTED_MARKER : (assistantMsg?.text || ""),
|
|
99
|
+
timestamp: assistantMsg?.timestamp || userMsg.timestamp || "",
|
|
100
|
+
interrupted,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function markInterrupted(exchange) {
|
|
104
|
+
exchange.interrupted = true;
|
|
105
|
+
exchange.output = exchange.output
|
|
106
|
+
? `${exchange.output}\n\n${INTERRUPTED_MARKER}`
|
|
107
|
+
: INTERRUPTED_MARKER;
|
|
108
|
+
}
|
|
109
|
+
function firstString(...values) {
|
|
110
|
+
for (const value of values) {
|
|
111
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
112
|
+
}
|
|
113
|
+
return "";
|
|
114
|
+
}
|
|
115
|
+
function firstTimestamp(...values) {
|
|
116
|
+
for (const value of values) {
|
|
117
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
118
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
119
|
+
const date = new Date(value);
|
|
120
|
+
if (!Number.isNaN(date.getTime())) return date.toISOString();
|
|
121
|
+
}
|
|
122
|
+
if (value instanceof Date && !Number.isNaN(value.getTime())) return value.toISOString();
|
|
123
|
+
}
|
|
124
|
+
return "";
|
|
125
|
+
}
|
|
126
|
+
function hashish(text) {
|
|
127
|
+
let h = 0;
|
|
128
|
+
const s = String(text || "");
|
|
129
|
+
for (let i = 0; i < s.length; i++) h = ((h << 5) - h + s.charCodeAt(i)) | 0;
|
|
130
|
+
return Math.abs(h).toString(36);
|
|
131
|
+
}
|
|
132
|
+
export { INTERRUPTED_MARKER };
|
|
@@ -15,9 +15,55 @@ import { McpClient } from "./mcp-client.mjs";
|
|
|
15
15
|
import { createLogger, nullLogger } from "./logger.mjs";
|
|
16
16
|
import { seedFor } from "./id.mjs";
|
|
17
17
|
import { resolveDestinationDatapack } from "./datapack.mjs";
|
|
18
|
-
import * as claudeCodeAdapter from "./adapters/claude-code.mjs";
|
|
19
|
-
import * as cursorAdapter from "./adapters/cursor.mjs";
|
|
20
18
|
import * as memoryMdAdapter from "./adapters/memory-md.mjs";
|
|
19
|
+
import {
|
|
20
|
+
enumerateSessionRefs,
|
|
21
|
+
parseTaggedSession,
|
|
22
|
+
sessionsByAdapter,
|
|
23
|
+
} from "./registry.mjs";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Preserve the owner used by a previous migrator run. Conversation ownership
|
|
27
|
+
* is immutable server-side, and resume state is keyed by agent_id, so moving
|
|
28
|
+
* an existing session to its newly derived repo-root id would both miss the
|
|
29
|
+
* done/partial record and retry against the wrong owner.
|
|
30
|
+
*/
|
|
31
|
+
function compatibleSessionAgentId(st, ref) {
|
|
32
|
+
if (state.getSession(st, ref.agentId, ref.sessionUuid)) return ref.agentId;
|
|
33
|
+
if (
|
|
34
|
+
ref.legacyAgentId
|
|
35
|
+
&& ref.legacyAgentId !== ref.agentId
|
|
36
|
+
&& state.getSession(st, ref.legacyAgentId, ref.sessionUuid)
|
|
37
|
+
) {
|
|
38
|
+
return ref.legacyAgentId;
|
|
39
|
+
}
|
|
40
|
+
return ref.agentId;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Keep project memories with an existing pre-MEKO-590 bucket when prior
|
|
45
|
+
* migration state proves that bucket is already in use. New migrations use
|
|
46
|
+
* the repo-root id.
|
|
47
|
+
*/
|
|
48
|
+
function compatibleMemoryAgentId(st, persistedAgentIds, item) {
|
|
49
|
+
const current = st.agents?.[item.agentId];
|
|
50
|
+
if (current?.memories?.[item.hash] || current?.memoryConversationId) {
|
|
51
|
+
return item.agentId;
|
|
52
|
+
}
|
|
53
|
+
if (!item.legacyAgentId || item.legacyAgentId === item.agentId) {
|
|
54
|
+
return item.agentId;
|
|
55
|
+
}
|
|
56
|
+
const legacy = st.agents?.[item.legacyAgentId];
|
|
57
|
+
if (legacy?.memories?.[item.hash] || legacy?.memoryConversationId) {
|
|
58
|
+
return item.legacyAgentId;
|
|
59
|
+
}
|
|
60
|
+
// Base the bucket choice on state that existed before this run. Sessions
|
|
61
|
+
// created earlier in this run must not make project memories abandon the
|
|
62
|
+
// legacy bucket that proves prior ownership.
|
|
63
|
+
if (persistedAgentIds.has(item.agentId)) return item.agentId;
|
|
64
|
+
if (persistedAgentIds.has(item.legacyAgentId)) return item.legacyAgentId;
|
|
65
|
+
return item.agentId;
|
|
66
|
+
}
|
|
21
67
|
|
|
22
68
|
/**
|
|
23
69
|
* @param {object} opts
|
|
@@ -43,10 +89,13 @@ import * as memoryMdAdapter from "./adapters/memory-md.mjs";
|
|
|
43
89
|
* destination is explicit on the
|
|
44
90
|
* wire rather than relying on the
|
|
45
91
|
* server's tenant-context fallback.
|
|
92
|
+
* @param {Array<object>} [opts.sessionAdapters] Test-only adapter override.
|
|
46
93
|
* @returns {Promise<{ok: boolean, summary: object, counts: object}>}
|
|
47
94
|
*/
|
|
48
95
|
export async function runMigration(opts) {
|
|
49
|
-
|
|
96
|
+
// A planning pass is intentionally read-only. In particular, don't create
|
|
97
|
+
// a run directory that `--migrate-status` can mistake for an active run.
|
|
98
|
+
if (!opts.dryRun) fs.mkdirSync(opts.runDir, { recursive: true });
|
|
50
99
|
|
|
51
100
|
const log = opts.dryRun
|
|
52
101
|
? nullLogger()
|
|
@@ -55,10 +104,33 @@ export async function runMigration(opts) {
|
|
|
55
104
|
const prog = progress.initial(opts.runId, opts.scope);
|
|
56
105
|
const progressPath = path.join(opts.runDir, "progress.json");
|
|
57
106
|
const writeProgress = () => {
|
|
107
|
+
if (opts.dryRun) return;
|
|
58
108
|
try { progress.write(progressPath, prog); } catch { /* best effort */ }
|
|
59
109
|
};
|
|
60
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Move the run into a terminal state: stop the heartbeat, stamp
|
|
113
|
+
* `finishedAt`, clear the `current*` fields, and mirror the transition
|
|
114
|
+
* into the event log so `log.ndjson` carries the same story as
|
|
115
|
+
* `progress.json`.
|
|
116
|
+
*
|
|
117
|
+
* @param {"succeeded"|"failed"} status
|
|
118
|
+
* @param {string|null} [error]
|
|
119
|
+
*/
|
|
120
|
+
const finish = (status, error = null) => {
|
|
121
|
+
progress.stopHeartbeat();
|
|
122
|
+
prog.status = status;
|
|
123
|
+
prog.error = error;
|
|
124
|
+
prog.finishedAt = new Date().toISOString();
|
|
125
|
+
prog.currentAgent = null;
|
|
126
|
+
prog.currentSession = null;
|
|
127
|
+
prog.currentMessage = null;
|
|
128
|
+
writeProgress();
|
|
129
|
+
log.info("status", { status, error, runId: opts.runId });
|
|
130
|
+
};
|
|
131
|
+
|
|
61
132
|
let st = state.load(opts.statePath);
|
|
133
|
+
const persistedAgentIds = new Set(Object.keys(st.agents || {}));
|
|
62
134
|
let opsSinceFlush = 0;
|
|
63
135
|
const FLUSH_EVERY = 25;
|
|
64
136
|
const flushState = () => {
|
|
@@ -80,49 +152,91 @@ export async function runMigration(opts) {
|
|
|
80
152
|
lockAcquiredHere = true;
|
|
81
153
|
}
|
|
82
154
|
|
|
155
|
+
// A silently killed daemon (kill -9, OOM, laptop sleep) runs none of the
|
|
156
|
+
// handlers below, so the reader needs an independent liveness signal.
|
|
157
|
+
// Ordinary progress writes refresh `heartbeatAt` too; this timer only
|
|
158
|
+
// covers the long gaps between them (a slow `memory_add` extraction).
|
|
159
|
+
// It starts after the lock, so the `lock_busy` return above leaves no
|
|
160
|
+
// `running` record and no timer behind.
|
|
161
|
+
if (!opts.dryRun) {
|
|
162
|
+
writeProgress();
|
|
163
|
+
progress.startHeartbeat(progressPath, prog);
|
|
164
|
+
}
|
|
165
|
+
|
|
83
166
|
// Plan the work.
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
projectCwd: opts.projectCwd,
|
|
87
|
-
home: opts.home,
|
|
88
|
-
});
|
|
89
|
-
const cursorSessions = cursorAdapter.listSessions({
|
|
90
|
-
scope: opts.scope,
|
|
91
|
-
projectCwd: opts.projectCwd,
|
|
92
|
-
home: opts.home,
|
|
93
|
-
});
|
|
94
|
-
const memoryItems = await memoryMdAdapter.enumerateMemoryItems({
|
|
167
|
+
const sourceErrors = [];
|
|
168
|
+
const listOpts = {
|
|
95
169
|
scope: opts.scope,
|
|
96
170
|
projectCwd: opts.projectCwd,
|
|
97
171
|
home: opts.home,
|
|
98
|
-
|
|
172
|
+
sessionAdapters: opts.sessionAdapters,
|
|
173
|
+
opencodeBin: opts.opencodeBin,
|
|
174
|
+
runExport: opts.runExport,
|
|
175
|
+
onSourceError({ source, displayName, error }) {
|
|
176
|
+
const detail = error?.message || String(error);
|
|
177
|
+
sourceErrors.push({ source, error: detail });
|
|
178
|
+
prog.counts.warnings++;
|
|
179
|
+
log.warn("source_enumeration_failed", { source, displayName, error: detail });
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
let taggedSessions;
|
|
183
|
+
try {
|
|
184
|
+
taggedSessions = await enumerateSessionRefs(listOpts);
|
|
185
|
+
} catch (err) {
|
|
186
|
+
log.error("source_enumeration_failed", { error: err.message });
|
|
187
|
+
prog.status = "failed";
|
|
188
|
+
prog.finishedAt = new Date().toISOString();
|
|
189
|
+
writeProgress();
|
|
190
|
+
log.close();
|
|
191
|
+
releaseLock();
|
|
192
|
+
return {
|
|
193
|
+
ok: false,
|
|
194
|
+
summary: { error: "source_enumeration_failed", detail: err.message },
|
|
195
|
+
counts: prog.counts,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
const byAdapter = sessionsByAdapter(taggedSessions);
|
|
199
|
+
const memoryItems = await memoryMdAdapter.enumerateMemoryItems(listOpts);
|
|
99
200
|
|
|
100
|
-
prog.counts.sessionsPlanned =
|
|
201
|
+
prog.counts.sessionsPlanned = taggedSessions.length;
|
|
101
202
|
writeProgress();
|
|
102
203
|
|
|
103
204
|
log.info("plan", {
|
|
104
205
|
scope: opts.scope,
|
|
105
|
-
claudeSessions:
|
|
106
|
-
cursorSessions:
|
|
206
|
+
claudeSessions: byAdapter["claude-code"] || 0,
|
|
207
|
+
cursorSessions: byAdapter.cursor || 0,
|
|
208
|
+
codexSessions: byAdapter.codex || 0,
|
|
209
|
+
openCodeSessions: byAdapter.opencode || 0,
|
|
210
|
+
sessionsByAdapter: byAdapter,
|
|
107
211
|
memoryItems: memoryItems.length,
|
|
108
212
|
dryRun: !!opts.dryRun,
|
|
109
213
|
noExtract: !!opts.noExtract,
|
|
110
214
|
});
|
|
111
215
|
|
|
112
216
|
if (opts.dryRun) {
|
|
113
|
-
// Peek-parse every session so we can report planned turn counts and
|
|
114
|
-
// the total expected MCP call volume — helps users estimate
|
|
115
|
-
// LLM-extraction cost before firing a real run.
|
|
116
217
|
let pairedTurns = 0;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
for (const
|
|
122
|
-
|
|
218
|
+
let parsedSessions = 0;
|
|
219
|
+
const sessionErrors = [];
|
|
220
|
+
/** @type {Record<string, number>} */
|
|
221
|
+
const turnsByAdapter = {};
|
|
222
|
+
for (const tagged of taggedSessions) {
|
|
223
|
+
let parsed;
|
|
224
|
+
try {
|
|
225
|
+
parsed = await Promise.resolve(parseTaggedSession(tagged, listOpts));
|
|
226
|
+
} catch (err) {
|
|
227
|
+
sessionErrors.push({
|
|
228
|
+
source: tagged.source,
|
|
229
|
+
sessionUuid: tagged.ref.sessionUuid,
|
|
230
|
+
error: err.message,
|
|
231
|
+
});
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
parsedSessions++;
|
|
123
235
|
pairedTurns += parsed.exchanges.length;
|
|
236
|
+
turnsByAdapter[tagged.source] = (turnsByAdapter[tagged.source] || 0) + parsed.exchanges.length;
|
|
124
237
|
}
|
|
125
|
-
const plannedConversationCreates =
|
|
238
|
+
const plannedConversationCreates =
|
|
239
|
+
parsedSessions
|
|
126
240
|
+ (memoryItems.length > 0 ? countDistinctMemoryAgents(memoryItems) : 0);
|
|
127
241
|
const plannedConversationAddMessage = pairedTurns;
|
|
128
242
|
const plannedMemoryAddExtract = opts.noExtract ? 0 : pairedTurns;
|
|
@@ -130,10 +244,16 @@ export async function runMigration(opts) {
|
|
|
130
244
|
|
|
131
245
|
const summary = {
|
|
132
246
|
scope: opts.scope,
|
|
133
|
-
claudeSessions:
|
|
134
|
-
cursorSessions:
|
|
247
|
+
claudeSessions: byAdapter["claude-code"] || 0,
|
|
248
|
+
cursorSessions: byAdapter.cursor || 0,
|
|
249
|
+
codexSessions: byAdapter.codex || 0,
|
|
250
|
+
openCodeSessions: byAdapter.opencode || 0,
|
|
251
|
+
sessionsByAdapter: byAdapter,
|
|
252
|
+
turnsByAdapter,
|
|
135
253
|
memoryItems: memoryItems.length,
|
|
136
254
|
pairedTurns,
|
|
255
|
+
sourceErrors,
|
|
256
|
+
sessionErrors,
|
|
137
257
|
plannedCalls: {
|
|
138
258
|
conversation_create: plannedConversationCreates,
|
|
139
259
|
conversation_add_message: plannedConversationAddMessage,
|
|
@@ -163,9 +283,7 @@ export async function runMigration(opts) {
|
|
|
163
283
|
} catch (err) {
|
|
164
284
|
log.error("mcp_connect_failed", { error: err.message, code: err.statusCode });
|
|
165
285
|
releaseLock();
|
|
166
|
-
|
|
167
|
-
prog.finishedAt = new Date().toISOString();
|
|
168
|
-
writeProgress();
|
|
286
|
+
finish("failed", `mcp_connect_failed: ${err.message}`);
|
|
169
287
|
return { ok: false, summary: { error: "mcp_connect_failed", detail: err.message }, counts: prog.counts };
|
|
170
288
|
}
|
|
171
289
|
|
|
@@ -182,9 +300,7 @@ export async function runMigration(opts) {
|
|
|
182
300
|
if (!resolved.ok) {
|
|
183
301
|
log.error("resolve_datapack_failed", { message: resolved.message });
|
|
184
302
|
releaseLock();
|
|
185
|
-
|
|
186
|
-
prog.finishedAt = new Date().toISOString();
|
|
187
|
-
writeProgress();
|
|
303
|
+
finish("failed", `resolve_datapack_failed: ${resolved.message}`);
|
|
188
304
|
return {
|
|
189
305
|
ok: false,
|
|
190
306
|
summary: { error: "resolve_datapack_failed", detail: resolved.message },
|
|
@@ -201,9 +317,7 @@ export async function runMigration(opts) {
|
|
|
201
317
|
} catch (err) {
|
|
202
318
|
log.error("resolve_datapack_threw", { error: err.message });
|
|
203
319
|
releaseLock();
|
|
204
|
-
|
|
205
|
-
prog.finishedAt = new Date().toISOString();
|
|
206
|
-
writeProgress();
|
|
320
|
+
finish("failed", `resolve_datapack_threw: ${err.message}`);
|
|
207
321
|
return {
|
|
208
322
|
ok: false,
|
|
209
323
|
summary: { error: "resolve_datapack_threw", detail: err.message },
|
|
@@ -218,7 +332,19 @@ export async function runMigration(opts) {
|
|
|
218
332
|
const withDp = (args) => (datapackId ? { ...args, datapack_id: datapackId } : args);
|
|
219
333
|
|
|
220
334
|
// -- Conversations --
|
|
221
|
-
for (const
|
|
335
|
+
for (const tagged of taggedSessions) {
|
|
336
|
+
const { ref: discoveredRef, source } = tagged;
|
|
337
|
+
const compatibleAgentId = compatibleSessionAgentId(st, discoveredRef);
|
|
338
|
+
const ref = compatibleAgentId === discoveredRef.agentId
|
|
339
|
+
? discoveredRef
|
|
340
|
+
: { ...discoveredRef, agentId: compatibleAgentId };
|
|
341
|
+
if (ref.agentId !== discoveredRef.agentId) {
|
|
342
|
+
log.info("session_legacy_agent_id", {
|
|
343
|
+
agentId: ref.agentId,
|
|
344
|
+
derivedAgentId: discoveredRef.agentId,
|
|
345
|
+
sessionUuid: ref.sessionUuid,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
222
348
|
let existing = state.getSession(st, ref.agentId, ref.sessionUuid);
|
|
223
349
|
if (existing?.status === "done" && !opts.force) {
|
|
224
350
|
log.info("session_skip_done", { agentId: ref.agentId, sessionUuid: ref.sessionUuid });
|
|
@@ -241,11 +367,28 @@ export async function runMigration(opts) {
|
|
|
241
367
|
|
|
242
368
|
prog.currentAgent = ref.agentId;
|
|
243
369
|
prog.currentSession = ref.sessionUuid;
|
|
370
|
+
prog.currentMessage = null;
|
|
244
371
|
writeProgress();
|
|
245
372
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
373
|
+
let parsed;
|
|
374
|
+
try {
|
|
375
|
+
parsed = await Promise.resolve(parseTaggedSession(tagged, listOpts));
|
|
376
|
+
} catch (err) {
|
|
377
|
+
log.error("session_parse_failed", {
|
|
378
|
+
source,
|
|
379
|
+
agentId: ref.agentId,
|
|
380
|
+
sessionUuid: ref.sessionUuid,
|
|
381
|
+
error: err.message,
|
|
382
|
+
});
|
|
383
|
+
state.markSession(st, ref.agentId, ref.sessionUuid, {
|
|
384
|
+
status: "failed",
|
|
385
|
+
lastError: err.message,
|
|
386
|
+
});
|
|
387
|
+
prog.counts.warnings++;
|
|
388
|
+
if (++opsSinceFlush >= FLUSH_EVERY) flushState();
|
|
389
|
+
writeProgress();
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
249
392
|
|
|
250
393
|
if (parsed.partial) {
|
|
251
394
|
log.warn("session_partial", { path: ref.path });
|
|
@@ -260,8 +403,9 @@ export async function runMigration(opts) {
|
|
|
260
403
|
session_id: ref.sessionUuid,
|
|
261
404
|
title: (parsed.firstUserText || ref.sessionUuid).slice(0, 120),
|
|
262
405
|
metadata: JSON.stringify({
|
|
263
|
-
source
|
|
264
|
-
cwd: ref.cwd || null,
|
|
406
|
+
source,
|
|
407
|
+
cwd: ref.cwd || parsed.cwd || null,
|
|
408
|
+
parentSessionId: parsed.parentSessionId || ref.parentSessionId || null,
|
|
265
409
|
gitBranch: ref.slug ? (parsed.gitBranch || null) : null,
|
|
266
410
|
startedAt: parsed.startedAt || null,
|
|
267
411
|
}),
|
|
@@ -279,6 +423,26 @@ export async function runMigration(opts) {
|
|
|
279
423
|
continue;
|
|
280
424
|
}
|
|
281
425
|
|
|
426
|
+
// MEKO-592: mcp-client sets ok:true whenever the HTTP call itself
|
|
427
|
+
// succeeded, so a tool body of `{"error":"datapack_id_required"}` still
|
|
428
|
+
// arrives as ok. Without this guard the extractId below returns "" and
|
|
429
|
+
// every subsequent add_message fires against an empty conversation_id,
|
|
430
|
+
// the session is marked done, and a rerun skips it - exactly the
|
|
431
|
+
// silent-drop shape this PR exists to close. Mirror the isErrorText
|
|
432
|
+
// check applied to memory_add below.
|
|
433
|
+
if (isErrorText(createRes.text)) {
|
|
434
|
+
const detail = errorDetail(createRes) || "conversation_create returned error";
|
|
435
|
+
log.error("conversation_create_error_body", { agentId: ref.agentId, sessionUuid: ref.sessionUuid, detail });
|
|
436
|
+
state.markSession(st, ref.agentId, ref.sessionUuid, {
|
|
437
|
+
status: "failed",
|
|
438
|
+
lastError: detail,
|
|
439
|
+
});
|
|
440
|
+
prog.counts.warnings++;
|
|
441
|
+
if (++opsSinceFlush >= FLUSH_EVERY) flushState();
|
|
442
|
+
writeProgress();
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
|
|
282
446
|
conversationId = extractId(createRes.text) || "";
|
|
283
447
|
state.markSession(st, ref.agentId, ref.sessionUuid, {
|
|
284
448
|
conversationId,
|
|
@@ -292,14 +456,24 @@ export async function runMigration(opts) {
|
|
|
292
456
|
let memoryPairsSent = existing?.memoryPairsSent ?? 0;
|
|
293
457
|
let lastUuid = existing?.lastUuid ?? null;
|
|
294
458
|
|
|
295
|
-
//
|
|
459
|
+
// Set when a turn loses a call to a mid-run 401/403. See below.
|
|
460
|
+
let authFailure = null;
|
|
461
|
+
|
|
462
|
+
// Resume from lastUuid if present. A cursor saved by an older parser
|
|
463
|
+
// can name an entry that is now folded into a turn (MEKO-710); resume
|
|
464
|
+
// after that turn instead of skipping the rest of the session.
|
|
296
465
|
let skipping = !!lastUuid;
|
|
297
466
|
for (const ex of parsed.exchanges) {
|
|
298
467
|
if (skipping) {
|
|
299
|
-
if (ex.user_uuid === lastUuid
|
|
468
|
+
if (ex.user_uuid === lastUuid || ex.folded_uuids?.includes(lastUuid)) {
|
|
469
|
+
skipping = false;
|
|
470
|
+
}
|
|
300
471
|
continue;
|
|
301
472
|
}
|
|
302
473
|
const seed = seedFor(ref.agentId, ref.sessionUuid, ex.user_uuid);
|
|
474
|
+
// `--migrate-status` reports this as the message id, so it has to be
|
|
475
|
+
// set before the calls that could hang, not after they return.
|
|
476
|
+
prog.currentMessage = ex.user_uuid;
|
|
303
477
|
|
|
304
478
|
// (a) conversation_add_message — fire-and-forget; server batches.
|
|
305
479
|
const addMsgPromise = client.callTool("conversation_add_message", withDp({
|
|
@@ -338,7 +512,33 @@ export async function runMigration(opts) {
|
|
|
338
512
|
}), { fireAndForget: true });
|
|
339
513
|
}
|
|
340
514
|
|
|
341
|
-
const [, memRes] = await Promise.all([addMsgPromise, memPromise]);
|
|
515
|
+
const [addMsgRes, memRes] = await Promise.all([addMsgPromise, memPromise]);
|
|
516
|
+
|
|
517
|
+
// A mid-run 401/403 on a fire-and-forget call is dropped rather than
|
|
518
|
+
// fatal (see mcp-client). Record each dropped call, then stop this
|
|
519
|
+
// session without checkpointing the turn: `lastUuid` stays on the last
|
|
520
|
+
// fully delivered turn, so a plain re-run resumes here instead of
|
|
521
|
+
// skipping the session as done. A resent conversation_add_message
|
|
522
|
+
// carries the same seed, so it resolves to the same message id.
|
|
523
|
+
const authSkips = [];
|
|
524
|
+
if (addMsgRes?.authSkipped) authSkips.push(["conversation_add_message", addMsgRes]);
|
|
525
|
+
if (!opts.noExtract && memRes?.authSkipped) authSkips.push(["memory_add", memRes]);
|
|
526
|
+
if (authSkips.length > 0) {
|
|
527
|
+
for (const [tool, res] of authSkips) {
|
|
528
|
+
prog.counts.authSkipped++;
|
|
529
|
+
log.warn("message_skipped_auth", {
|
|
530
|
+
agentId: ref.agentId,
|
|
531
|
+
sessionUuid: ref.sessionUuid,
|
|
532
|
+
userUuid: ex.user_uuid,
|
|
533
|
+
tool,
|
|
534
|
+
statusCode: res.statusCode,
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
const [tool, res] = authSkips[0];
|
|
538
|
+
authFailure = `auth rejected (HTTP ${res.statusCode}) on ${tool}`;
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
|
|
342
542
|
messagesSent++;
|
|
343
543
|
if (!opts.noExtract) {
|
|
344
544
|
if (memRes && memRes.ok && !isErrorText(memRes.text)) {
|
|
@@ -368,6 +568,30 @@ export async function runMigration(opts) {
|
|
|
368
568
|
writeProgress();
|
|
369
569
|
}
|
|
370
570
|
|
|
571
|
+
if (authFailure) {
|
|
572
|
+
// Not `done`: the resume check only skips `done` sessions, so the next
|
|
573
|
+
// run picks this one up from `lastUuid`.
|
|
574
|
+
state.markSession(st, ref.agentId, ref.sessionUuid, {
|
|
575
|
+
conversationId,
|
|
576
|
+
status: "failed",
|
|
577
|
+
messagesSent,
|
|
578
|
+
memoryPairsSent,
|
|
579
|
+
lastUuid,
|
|
580
|
+
lastError: authFailure,
|
|
581
|
+
});
|
|
582
|
+
flushState();
|
|
583
|
+
prog.currentMessage = null;
|
|
584
|
+
writeProgress();
|
|
585
|
+
log.warn("session_auth_failed", {
|
|
586
|
+
agentId: ref.agentId,
|
|
587
|
+
sessionUuid: ref.sessionUuid,
|
|
588
|
+
messagesSent,
|
|
589
|
+
lastUuid,
|
|
590
|
+
error: authFailure,
|
|
591
|
+
});
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
|
|
371
595
|
state.markSession(st, ref.agentId, ref.sessionUuid, {
|
|
372
596
|
conversationId,
|
|
373
597
|
status: parsed.partial ? "partial" : "done",
|
|
@@ -391,7 +615,15 @@ export async function runMigration(opts) {
|
|
|
391
615
|
|
|
392
616
|
// -- Memory MD files --
|
|
393
617
|
const umbrellaPerAgent = new Map();
|
|
394
|
-
for (const
|
|
618
|
+
for (const discoveredItem of memoryItems) {
|
|
619
|
+
const compatibleAgentId = compatibleMemoryAgentId(
|
|
620
|
+
st,
|
|
621
|
+
persistedAgentIds,
|
|
622
|
+
discoveredItem,
|
|
623
|
+
);
|
|
624
|
+
const item = compatibleAgentId === discoveredItem.agentId
|
|
625
|
+
? discoveredItem
|
|
626
|
+
: { ...discoveredItem, agentId: compatibleAgentId };
|
|
395
627
|
if (state.isMemoryDone(st, item.agentId, item.hash) && !opts.force) continue;
|
|
396
628
|
|
|
397
629
|
let convId = umbrellaPerAgent.get(item.agentId);
|
|
@@ -411,6 +643,13 @@ export async function runMigration(opts) {
|
|
|
411
643
|
prog.counts.warnings++;
|
|
412
644
|
continue;
|
|
413
645
|
}
|
|
646
|
+
// MEKO-592: guard against a structured error body (e.g.
|
|
647
|
+
// datapack_id_required) that the transport reports as ok:true.
|
|
648
|
+
if (isErrorText(res.text)) {
|
|
649
|
+
log.error("memory_umbrella_error_body", { agentId: item.agentId, detail: errorDetail(res) });
|
|
650
|
+
prog.counts.warnings++;
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
414
653
|
convId = extractId(res.text) || "";
|
|
415
654
|
state.ensureAgent(st, item.agentId).memoryConversationId = convId;
|
|
416
655
|
}
|
|
@@ -425,7 +664,15 @@ export async function runMigration(opts) {
|
|
|
425
664
|
metadata: JSON.stringify(item.metadata),
|
|
426
665
|
}), { fireAndForget: true });
|
|
427
666
|
|
|
428
|
-
if (res
|
|
667
|
+
if (res?.authSkipped) {
|
|
668
|
+
prog.counts.authSkipped++;
|
|
669
|
+
log.warn("message_skipped_auth", {
|
|
670
|
+
agentId: item.agentId,
|
|
671
|
+
name: item.name,
|
|
672
|
+
tool: "memory_add",
|
|
673
|
+
statusCode: res.statusCode,
|
|
674
|
+
});
|
|
675
|
+
} else if (res.ok && !isErrorText(res.text)) {
|
|
429
676
|
state.markMemory(st, item.agentId, item.hash, {
|
|
430
677
|
memoryId: extractId(res.text) || "",
|
|
431
678
|
name: item.name,
|
|
@@ -446,18 +693,14 @@ export async function runMigration(opts) {
|
|
|
446
693
|
}
|
|
447
694
|
|
|
448
695
|
flushState();
|
|
449
|
-
|
|
450
|
-
prog.finishedAt = new Date().toISOString();
|
|
451
|
-
prog.currentAgent = null;
|
|
452
|
-
prog.currentSession = null;
|
|
453
|
-
writeProgress();
|
|
696
|
+
finish("succeeded");
|
|
454
697
|
|
|
455
698
|
st.runs.push({
|
|
456
699
|
runId: opts.runId,
|
|
457
700
|
startedAt: prog.startedAt,
|
|
458
701
|
finishedAt: prog.finishedAt,
|
|
459
702
|
scope: opts.scope,
|
|
460
|
-
status: "
|
|
703
|
+
status: "succeeded",
|
|
461
704
|
counts: prog.counts,
|
|
462
705
|
});
|
|
463
706
|
flushState();
|