@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.
- package/README.md +168 -61
- package/assets/hooks-handlers/lib/capture-cleanup-cli.js +2134 -0
- package/assets/hooks-handlers/lib/capture-notices.js +224 -14
- package/assets/hooks-handlers/lib/capture-state.js +206 -1
- package/assets/hooks-handlers/lib/capture-status-cli.js +6 -2
- package/assets/hooks-handlers/lib/capture.js +1364 -139
- package/assets/hooks-handlers/lib/checkpoint-timer.js +445 -18
- package/assets/hooks-handlers/lib/journal-bridge.js +218 -0
- package/assets/hooks-handlers/lib/kiro-handler.js +26 -4
- package/assets/hooks-handlers/lib/opencode-handler.js +643 -0
- package/assets/hooks-handlers/lib/opencode-normalizer.js +145 -0
- package/assets/hooks-handlers/lib/terminal-handoff.js +10 -1
- package/assets/opencode-plugin/meko.js +213 -0
- package/assets/scripts/meko-statusline.sh +102 -19
- package/assets/skills/meko-mcp-tools/SKILL.md +59 -16
- package/assets/skills/meko-mcp-tools/references/_sections.md +2 -2
- package/assets/skills/meko-mcp-tools/references/tools-agent-id-conventions.md +15 -7
- package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +275 -19
- package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +7 -4
- package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +39 -11
- package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +10 -6
- package/assets/skills/meko-mcp-tools/references/tools-overview.md +44 -29
- package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +79 -14
- package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +47 -1
- package/assets/skills/meko-mcp-tools/references/tools-user-id-conventions.md +6 -39
- package/assets/skills/meko-mcp-tools-desktop/SKILL.md +57 -21
- package/assets/skills/meko-mcp-tools-desktop/desktop/README.md +4 -4
- 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 +8 -6
- package/assets/skills/meko-mcp-tools-desktop/references/tools-cookbook.md +284 -38
- package/assets/skills/meko-mcp-tools-desktop/references/tools-known-limitations.md +32 -10
- package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +16 -12
- package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +46 -31
- package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +79 -14
- package/assets/skills/meko-mcp-tools-desktop/references/tools-troubleshooting.md +69 -1
- package/assets/skills/meko-select-datapack-desktop/SKILL.md +93 -14
- package/assets/skills/meko-select-datapack-desktop/references/cookbook.md +44 -12
- package/assets/skills/meko-select-datapack-desktop/references/role-display-future.md +14 -10
- package/assets/skills/meko-select-datapack-desktop/references/troubleshooting.md +17 -5
- package/bin/create-meko-setup.mjs +186 -30
- package/lib/autofix.mjs +100 -0
- package/lib/browser-login.mjs +495 -0
- package/lib/capture-cleanup.mjs +63 -0
- package/lib/clients/claude-code.mjs +1 -0
- package/lib/clients/codex.mjs +80 -35
- package/lib/clients/common.mjs +7 -0
- package/lib/clients/index.mjs +2 -0
- package/lib/clients/kiro.mjs +5 -5
- package/lib/clients/opencode.mjs +537 -0
- package/lib/config.mjs +68 -1
- package/lib/installer.mjs +18 -2
- package/lib/migrate/adapters/claude-code.mjs +11 -2
- package/lib/migrate/adapters/codex.mjs +9 -2
- 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/datapack.mjs +254 -16
- package/lib/migrate/id.mjs +195 -13
- package/lib/migrate/mcp-client.mjs +59 -1
- package/lib/migrate/opencode-normalizer.mjs +132 -0
- package/lib/migrate/orchestrator.mjs +294 -78
- package/lib/migrate/progress.mjs +239 -5
- package/lib/migrate/registry.mjs +72 -0
- package/lib/migrate.mjs +97 -9
- package/lib/validate.mjs +111 -22
- package/package.json +5 -5
- 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
|
|
89
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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:
|
|
116
|
-
cursorSessions:
|
|
117
|
-
codexSessions:
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
for (const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
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:
|
|
150
|
-
cursorSessions:
|
|
151
|
-
codexSessions:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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: "
|
|
703
|
+
status: "succeeded",
|
|
488
704
|
counts: prog.counts,
|
|
489
705
|
});
|
|
490
706
|
flushState();
|