@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.
Files changed (70) hide show
  1. package/README.md +233 -40
  2. package/assets/hooks-handlers/codex-session-end.sh +4 -69
  3. package/assets/hooks-handlers/cursor-session-start.sh +19 -1
  4. package/assets/hooks-handlers/lib/capture-cleanup-cli.js +1473 -0
  5. package/assets/hooks-handlers/lib/capture-notices.js +438 -0
  6. package/assets/hooks-handlers/lib/capture-state.js +2173 -0
  7. package/assets/hooks-handlers/lib/capture-status-cli.js +86 -0
  8. package/assets/hooks-handlers/lib/capture.js +2782 -403
  9. package/assets/hooks-handlers/lib/checkpoint-timer.js +485 -81
  10. package/assets/hooks-handlers/lib/journal-bridge.js +218 -0
  11. package/assets/hooks-handlers/lib/kiro-handler.js +760 -0
  12. package/assets/hooks-handlers/lib/opencode-handler.js +620 -0
  13. package/assets/hooks-handlers/lib/opencode-normalizer.js +145 -0
  14. package/assets/hooks-handlers/lib/terminal-handoff.js +427 -0
  15. package/assets/hooks-handlers/session-end.sh +3 -34
  16. package/assets/opencode-plugin/meko.js +213 -0
  17. package/assets/scripts/meko-statusline.sh +123 -16
  18. package/assets/skills/meko-mcp-tools/SKILL.md +116 -126
  19. package/assets/skills/meko-mcp-tools/references/_sections.md +3 -3
  20. package/assets/skills/meko-mcp-tools/references/tools-agent-id-conventions.md +25 -18
  21. package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +99 -24
  22. package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +1 -1
  23. package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +53 -6
  24. package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +25 -79
  25. package/assets/skills/meko-mcp-tools/references/tools-overview.md +37 -26
  26. package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +14 -8
  27. package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +66 -1
  28. package/assets/skills/meko-mcp-tools/references/tools-user-id-conventions.md +7 -40
  29. package/assets/skills/meko-mcp-tools-desktop/SKILL.md +56 -20
  30. package/assets/skills/meko-mcp-tools-desktop/desktop/README.md +1 -1
  31. package/assets/skills/meko-mcp-tools-desktop/references/_sections.md +1 -1
  32. package/assets/skills/meko-mcp-tools-desktop/references/tools-agent-id-conventions.md +15 -8
  33. package/assets/skills/meko-mcp-tools-desktop/references/tools-cookbook.md +99 -36
  34. package/assets/skills/meko-mcp-tools-desktop/references/tools-known-limitations.md +22 -5
  35. package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +18 -14
  36. package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +39 -28
  37. package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +14 -8
  38. package/assets/skills/meko-mcp-tools-desktop/references/tools-troubleshooting.md +67 -1
  39. package/assets/skills/meko-select-datapack-desktop/SKILL.md +4 -4
  40. package/assets/skills/meko-select-datapack-desktop/references/cookbook.md +8 -8
  41. package/assets/skills/meko-select-datapack-desktop/references/role-display-future.md +7 -5
  42. package/assets/skills/meko-select-datapack-desktop/references/troubleshooting.md +4 -4
  43. package/bin/create-meko-setup.mjs +113 -12
  44. package/lib/capture-cleanup.mjs +63 -0
  45. package/lib/capture-status.mjs +67 -0
  46. package/lib/clients/aliases.mjs +5 -0
  47. package/lib/clients/claude-code.mjs +1 -0
  48. package/lib/clients/claude-desktop.mjs +120 -5
  49. package/lib/clients/codex.mjs +85 -37
  50. package/lib/clients/common.mjs +7 -0
  51. package/lib/clients/index.mjs +4 -0
  52. package/lib/clients/kiro.mjs +705 -0
  53. package/lib/clients/opencode.mjs +532 -0
  54. package/lib/config.mjs +158 -1
  55. package/lib/installer.mjs +18 -1
  56. package/lib/migrate/adapters/claude-code.mjs +11 -2
  57. package/lib/migrate/adapters/codex.mjs +497 -0
  58. package/lib/migrate/adapters/memory-md.mjs +27 -8
  59. package/lib/migrate/adapters/opencode.mjs +148 -0
  60. package/lib/migrate/claude-parser.mjs +153 -9
  61. package/lib/migrate/id.mjs +217 -10
  62. package/lib/migrate/mcp-client.mjs +25 -1
  63. package/lib/migrate/opencode-normalizer.mjs +132 -0
  64. package/lib/migrate/orchestrator.mjs +299 -56
  65. package/lib/migrate/progress.mjs +239 -5
  66. package/lib/migrate/registry.mjs +72 -0
  67. package/lib/migrate.mjs +100 -10
  68. package/lib/prompt.mjs +16 -1
  69. package/package.json +4 -4
  70. 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 is fatal; surface immediately.
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
- fs.mkdirSync(opts.runDir, { recursive: true });
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 claudeSessions = await claudeCodeAdapter.listSessions({
85
- scope: opts.scope,
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 = claudeSessions.length + cursorSessions.length;
201
+ prog.counts.sessionsPlanned = taggedSessions.length;
101
202
  writeProgress();
102
203
 
103
204
  log.info("plan", {
104
205
  scope: opts.scope,
105
- claudeSessions: claudeSessions.length,
106
- cursorSessions: cursorSessions.length,
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
- for (const ref of claudeSessions) {
118
- const parsed = await claudeCodeAdapter.parseSession(ref.path);
119
- pairedTurns += parsed.exchanges.length;
120
- }
121
- for (const ref of cursorSessions) {
122
- const parsed = cursorAdapter.parseSession(ref.path);
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 = claudeSessions.length + cursorSessions.length
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: claudeSessions.length,
134
- cursorSessions: cursorSessions.length,
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
- prog.status = "failed";
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
- prog.status = "failed";
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
- prog.status = "failed";
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 ref of [...claudeSessions, ...cursorSessions]) {
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
- const parsed = (ref.path && ref.path.endsWith(".jsonl"))
247
- ? await claudeCodeAdapter.parseSession(ref.path)
248
- : cursorAdapter.parseSession(ref.path);
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: ref.path.endsWith(".jsonl") ? "claude-code" : "cursor",
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
- // Resume from lastUuid if present.
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) skipping = false;
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 item of memoryItems) {
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.ok && !isErrorText(res.text)) {
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
- prog.status = "done";
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: "done",
703
+ status: "succeeded",
461
704
  counts: prog.counts,
462
705
  });
463
706
  flushState();