@yugabytedb/meko-mcp 2.2.0 → 2.5.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 (67) hide show
  1. package/README.md +168 -61
  2. package/assets/hooks-handlers/lib/capture-cleanup-cli.js +2134 -0
  3. package/assets/hooks-handlers/lib/capture-notices.js +224 -14
  4. package/assets/hooks-handlers/lib/capture-state.js +206 -1
  5. package/assets/hooks-handlers/lib/capture-status-cli.js +6 -2
  6. package/assets/hooks-handlers/lib/capture.js +1364 -139
  7. package/assets/hooks-handlers/lib/checkpoint-timer.js +445 -18
  8. package/assets/hooks-handlers/lib/journal-bridge.js +218 -0
  9. package/assets/hooks-handlers/lib/kiro-handler.js +26 -4
  10. package/assets/hooks-handlers/lib/opencode-handler.js +643 -0
  11. package/assets/hooks-handlers/lib/opencode-normalizer.js +145 -0
  12. package/assets/hooks-handlers/lib/terminal-handoff.js +10 -1
  13. package/assets/opencode-plugin/meko.js +213 -0
  14. package/assets/scripts/meko-statusline.sh +102 -19
  15. package/assets/skills/meko-mcp-tools/SKILL.md +59 -16
  16. package/assets/skills/meko-mcp-tools/references/_sections.md +2 -2
  17. package/assets/skills/meko-mcp-tools/references/tools-agent-id-conventions.md +15 -7
  18. package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +275 -19
  19. package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +7 -4
  20. package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +39 -11
  21. package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +10 -6
  22. package/assets/skills/meko-mcp-tools/references/tools-overview.md +44 -29
  23. package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +79 -14
  24. package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +47 -1
  25. package/assets/skills/meko-mcp-tools/references/tools-user-id-conventions.md +6 -39
  26. package/assets/skills/meko-mcp-tools-desktop/SKILL.md +57 -21
  27. package/assets/skills/meko-mcp-tools-desktop/desktop/README.md +4 -4
  28. package/assets/skills/meko-mcp-tools-desktop/references/_sections.md +1 -1
  29. package/assets/skills/meko-mcp-tools-desktop/references/tools-agent-id-conventions.md +8 -6
  30. package/assets/skills/meko-mcp-tools-desktop/references/tools-cookbook.md +284 -38
  31. package/assets/skills/meko-mcp-tools-desktop/references/tools-known-limitations.md +32 -10
  32. package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +16 -12
  33. package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +46 -31
  34. package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +79 -14
  35. package/assets/skills/meko-mcp-tools-desktop/references/tools-troubleshooting.md +69 -1
  36. package/assets/skills/meko-select-datapack-desktop/SKILL.md +93 -14
  37. package/assets/skills/meko-select-datapack-desktop/references/cookbook.md +44 -12
  38. package/assets/skills/meko-select-datapack-desktop/references/role-display-future.md +14 -10
  39. package/assets/skills/meko-select-datapack-desktop/references/troubleshooting.md +17 -5
  40. package/bin/create-meko-setup.mjs +186 -30
  41. package/lib/autofix.mjs +100 -0
  42. package/lib/browser-login.mjs +495 -0
  43. package/lib/capture-cleanup.mjs +63 -0
  44. package/lib/clients/claude-code.mjs +1 -0
  45. package/lib/clients/codex.mjs +80 -35
  46. package/lib/clients/common.mjs +7 -0
  47. package/lib/clients/index.mjs +2 -0
  48. package/lib/clients/kiro.mjs +5 -5
  49. package/lib/clients/opencode.mjs +537 -0
  50. package/lib/config.mjs +68 -1
  51. package/lib/installer.mjs +18 -2
  52. package/lib/migrate/adapters/claude-code.mjs +11 -2
  53. package/lib/migrate/adapters/codex.mjs +9 -2
  54. package/lib/migrate/adapters/memory-md.mjs +27 -8
  55. package/lib/migrate/adapters/opencode.mjs +148 -0
  56. package/lib/migrate/claude-parser.mjs +153 -9
  57. package/lib/migrate/datapack.mjs +254 -16
  58. package/lib/migrate/id.mjs +195 -13
  59. package/lib/migrate/mcp-client.mjs +59 -1
  60. package/lib/migrate/opencode-normalizer.mjs +132 -0
  61. package/lib/migrate/orchestrator.mjs +294 -78
  62. package/lib/migrate/progress.mjs +239 -5
  63. package/lib/migrate/registry.mjs +72 -0
  64. package/lib/migrate.mjs +97 -9
  65. package/lib/validate.mjs +111 -22
  66. package/package.json +5 -5
  67. package/scripts/bundle-assets.mjs +4 -0
@@ -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,10 +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
- import * as codexAdapter from "./adapters/codex.mjs";
21
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
+ }
22
67
 
23
68
  /**
24
69
  * @param {object} opts
@@ -44,6 +89,7 @@ import * as memoryMdAdapter from "./adapters/memory-md.mjs";
44
89
  * destination is explicit on the
45
90
  * wire rather than relying on the
46
91
  * server's tenant-context fallback.
92
+ * @param {Array<object>} [opts.sessionAdapters] Test-only adapter override.
47
93
  * @returns {Promise<{ok: boolean, summary: object, counts: object}>}
48
94
  */
49
95
  export async function runMigration(opts) {
@@ -62,7 +108,29 @@ export async function runMigration(opts) {
62
108
  try { progress.write(progressPath, prog); } catch { /* best effort */ }
63
109
  };
64
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
+
65
132
  let st = state.load(opts.statePath);
133
+ const persistedAgentIds = new Set(Object.keys(st.agents || {}));
66
134
  let opsSinceFlush = 0;
67
135
  const FLUSH_EVERY = 25;
68
136
  const flushState = () => {
@@ -84,61 +152,91 @@ export async function runMigration(opts) {
84
152
  lockAcquiredHere = true;
85
153
  }
86
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
+
87
166
  // Plan the work.
88
- const claudeSessions = await claudeCodeAdapter.listSessions({
89
- scope: opts.scope,
90
- projectCwd: opts.projectCwd,
91
- home: opts.home,
92
- });
93
- const cursorSessions = cursorAdapter.listSessions({
94
- scope: opts.scope,
95
- projectCwd: opts.projectCwd,
96
- home: opts.home,
97
- });
98
- const codexSessions = codexAdapter.listSessions({
167
+ const sourceErrors = [];
168
+ const listOpts = {
99
169
  scope: opts.scope,
100
170
  projectCwd: opts.projectCwd,
101
171
  home: opts.home,
102
- });
103
- const memoryItems = await memoryMdAdapter.enumerateMemoryItems({
104
- scope: opts.scope,
105
- projectCwd: opts.projectCwd,
106
- home: opts.home,
107
- });
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);
108
200
 
109
- prog.counts.sessionsPlanned =
110
- claudeSessions.length + cursorSessions.length + codexSessions.length;
201
+ prog.counts.sessionsPlanned = taggedSessions.length;
111
202
  writeProgress();
112
203
 
113
204
  log.info("plan", {
114
205
  scope: opts.scope,
115
- claudeSessions: claudeSessions.length,
116
- cursorSessions: cursorSessions.length,
117
- codexSessions: codexSessions.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,
118
211
  memoryItems: memoryItems.length,
119
212
  dryRun: !!opts.dryRun,
120
213
  noExtract: !!opts.noExtract,
121
214
  });
122
215
 
123
216
  if (opts.dryRun) {
124
- // Peek-parse every session so we can report planned turn counts and
125
- // the total expected MCP call volume — helps users estimate
126
- // LLM-extraction cost before firing a real run.
127
217
  let pairedTurns = 0;
128
- for (const ref of claudeSessions) {
129
- const parsed = await claudeCodeAdapter.parseSession(ref.path);
130
- pairedTurns += parsed.exchanges.length;
131
- }
132
- for (const ref of cursorSessions) {
133
- const parsed = cursorAdapter.parseSession(ref.path);
134
- pairedTurns += parsed.exchanges.length;
135
- }
136
- for (const ref of codexSessions) {
137
- const parsed = codexAdapter.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++;
138
235
  pairedTurns += parsed.exchanges.length;
236
+ turnsByAdapter[tagged.source] = (turnsByAdapter[tagged.source] || 0) + parsed.exchanges.length;
139
237
  }
140
238
  const plannedConversationCreates =
141
- claudeSessions.length + cursorSessions.length + codexSessions.length
239
+ parsedSessions
142
240
  + (memoryItems.length > 0 ? countDistinctMemoryAgents(memoryItems) : 0);
143
241
  const plannedConversationAddMessage = pairedTurns;
144
242
  const plannedMemoryAddExtract = opts.noExtract ? 0 : pairedTurns;
@@ -146,11 +244,16 @@ export async function runMigration(opts) {
146
244
 
147
245
  const summary = {
148
246
  scope: opts.scope,
149
- claudeSessions: claudeSessions.length,
150
- cursorSessions: cursorSessions.length,
151
- codexSessions: codexSessions.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,
152
253
  memoryItems: memoryItems.length,
153
254
  pairedTurns,
255
+ sourceErrors,
256
+ sessionErrors,
154
257
  plannedCalls: {
155
258
  conversation_create: plannedConversationCreates,
156
259
  conversation_add_message: plannedConversationAddMessage,
@@ -180,13 +283,11 @@ export async function runMigration(opts) {
180
283
  } catch (err) {
181
284
  log.error("mcp_connect_failed", { error: err.message, code: err.statusCode });
182
285
  releaseLock();
183
- prog.status = "failed";
184
- prog.finishedAt = new Date().toISOString();
185
- writeProgress();
286
+ finish("failed", `mcp_connect_failed: ${err.message}`);
186
287
  return { ok: false, summary: { error: "mcp_connect_failed", detail: err.message }, counts: prog.counts };
187
288
  }
188
289
 
189
- // Resolve --datapack against the live datapack_list so every subsequent
290
+ // Resolve --datapack against the live server so every subsequent
190
291
  // tools/call carries an explicit datapack_id. Without this we'd be silently
191
292
  // relying on the server's tenant-context default — fine when it's correct,
192
293
  // invisible when it isn't.
@@ -199,9 +300,7 @@ export async function runMigration(opts) {
199
300
  if (!resolved.ok) {
200
301
  log.error("resolve_datapack_failed", { message: resolved.message });
201
302
  releaseLock();
202
- prog.status = "failed";
203
- prog.finishedAt = new Date().toISOString();
204
- writeProgress();
303
+ finish("failed", `resolve_datapack_failed: ${resolved.message}`);
205
304
  return {
206
305
  ok: false,
207
306
  summary: { error: "resolve_datapack_failed", detail: resolved.message },
@@ -218,9 +317,7 @@ export async function runMigration(opts) {
218
317
  } catch (err) {
219
318
  log.error("resolve_datapack_threw", { error: err.message });
220
319
  releaseLock();
221
- prog.status = "failed";
222
- prog.finishedAt = new Date().toISOString();
223
- writeProgress();
320
+ finish("failed", `resolve_datapack_threw: ${err.message}`);
224
321
  return {
225
322
  ok: false,
226
323
  summary: { error: "resolve_datapack_threw", detail: err.message },
@@ -235,15 +332,19 @@ export async function runMigration(opts) {
235
332
  const withDp = (args) => (datapackId ? { ...args, datapack_id: datapackId } : args);
236
333
 
237
334
  // -- Conversations --
238
- // Tag each ref with its source so the parse dispatch below is explicit.
239
- // Claude Code and Codex both store rollouts as `.jsonl`, so an
240
- // extension check alone can't tell them apart.
241
- const taggedSessions = [
242
- ...claudeSessions.map((ref) => ({ ref, source: "claude-code" })),
243
- ...cursorSessions.map((ref) => ({ ref, source: "cursor" })),
244
- ...codexSessions.map((ref) => ({ ref, source: "codex" })),
245
- ];
246
- for (const { ref, source } of taggedSessions) {
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
+ }
247
348
  let existing = state.getSession(st, ref.agentId, ref.sessionUuid);
248
349
  if (existing?.status === "done" && !opts.force) {
249
350
  log.info("session_skip_done", { agentId: ref.agentId, sessionUuid: ref.sessionUuid });
@@ -266,13 +367,28 @@ export async function runMigration(opts) {
266
367
 
267
368
  prog.currentAgent = ref.agentId;
268
369
  prog.currentSession = ref.sessionUuid;
370
+ prog.currentMessage = null;
269
371
  writeProgress();
270
372
 
271
- const parsed = source === "claude-code"
272
- ? await claudeCodeAdapter.parseSession(ref.path)
273
- : source === "codex"
274
- ? codexAdapter.parseSession(ref.path)
275
- : 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
+ }
276
392
 
277
393
  if (parsed.partial) {
278
394
  log.warn("session_partial", { path: ref.path });
@@ -288,7 +404,8 @@ export async function runMigration(opts) {
288
404
  title: (parsed.firstUserText || ref.sessionUuid).slice(0, 120),
289
405
  metadata: JSON.stringify({
290
406
  source,
291
- cwd: ref.cwd || null,
407
+ cwd: ref.cwd || parsed.cwd || null,
408
+ parentSessionId: parsed.parentSessionId || ref.parentSessionId || null,
292
409
  gitBranch: ref.slug ? (parsed.gitBranch || null) : null,
293
410
  startedAt: parsed.startedAt || null,
294
411
  }),
@@ -306,6 +423,26 @@ export async function runMigration(opts) {
306
423
  continue;
307
424
  }
308
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
+
309
446
  conversationId = extractId(createRes.text) || "";
310
447
  state.markSession(st, ref.agentId, ref.sessionUuid, {
311
448
  conversationId,
@@ -319,14 +456,24 @@ export async function runMigration(opts) {
319
456
  let memoryPairsSent = existing?.memoryPairsSent ?? 0;
320
457
  let lastUuid = existing?.lastUuid ?? null;
321
458
 
322
- // 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.
323
465
  let skipping = !!lastUuid;
324
466
  for (const ex of parsed.exchanges) {
325
467
  if (skipping) {
326
- if (ex.user_uuid === lastUuid) skipping = false;
468
+ if (ex.user_uuid === lastUuid || ex.folded_uuids?.includes(lastUuid)) {
469
+ skipping = false;
470
+ }
327
471
  continue;
328
472
  }
329
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;
330
477
 
331
478
  // (a) conversation_add_message — fire-and-forget; server batches.
332
479
  const addMsgPromise = client.callTool("conversation_add_message", withDp({
@@ -365,7 +512,33 @@ export async function runMigration(opts) {
365
512
  }), { fireAndForget: true });
366
513
  }
367
514
 
368
- 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
+
369
542
  messagesSent++;
370
543
  if (!opts.noExtract) {
371
544
  if (memRes && memRes.ok && !isErrorText(memRes.text)) {
@@ -395,6 +568,30 @@ export async function runMigration(opts) {
395
568
  writeProgress();
396
569
  }
397
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
+
398
595
  state.markSession(st, ref.agentId, ref.sessionUuid, {
399
596
  conversationId,
400
597
  status: parsed.partial ? "partial" : "done",
@@ -418,7 +615,15 @@ export async function runMigration(opts) {
418
615
 
419
616
  // -- Memory MD files --
420
617
  const umbrellaPerAgent = new Map();
421
- 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 };
422
627
  if (state.isMemoryDone(st, item.agentId, item.hash) && !opts.force) continue;
423
628
 
424
629
  let convId = umbrellaPerAgent.get(item.agentId);
@@ -438,6 +643,13 @@ export async function runMigration(opts) {
438
643
  prog.counts.warnings++;
439
644
  continue;
440
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
+ }
441
653
  convId = extractId(res.text) || "";
442
654
  state.ensureAgent(st, item.agentId).memoryConversationId = convId;
443
655
  }
@@ -452,7 +664,15 @@ export async function runMigration(opts) {
452
664
  metadata: JSON.stringify(item.metadata),
453
665
  }), { fireAndForget: true });
454
666
 
455
- 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)) {
456
676
  state.markMemory(st, item.agentId, item.hash, {
457
677
  memoryId: extractId(res.text) || "",
458
678
  name: item.name,
@@ -473,18 +693,14 @@ export async function runMigration(opts) {
473
693
  }
474
694
 
475
695
  flushState();
476
- prog.status = "done";
477
- prog.finishedAt = new Date().toISOString();
478
- prog.currentAgent = null;
479
- prog.currentSession = null;
480
- writeProgress();
696
+ finish("succeeded");
481
697
 
482
698
  st.runs.push({
483
699
  runId: opts.runId,
484
700
  startedAt: prog.startedAt,
485
701
  finishedAt: prog.finishedAt,
486
702
  scope: opts.scope,
487
- status: "done",
703
+ status: "succeeded",
488
704
  counts: prog.counts,
489
705
  });
490
706
  flushState();