@yugabytedb/meko-mcp 2.3.0 → 2.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -35
- package/assets/hooks-handlers/lib/capture-cleanup-cli.js +735 -74
- package/assets/hooks-handlers/lib/capture-notices.js +94 -1
- package/assets/hooks-handlers/lib/capture-state.js +111 -0
- package/assets/hooks-handlers/lib/capture.js +130 -4
- package/assets/hooks-handlers/lib/checkpoint-timer.js +394 -8
- package/assets/hooks-handlers/lib/opencode-handler.js +29 -6
- package/assets/scripts/meko-statusline.sh +17 -1
- package/assets/skills/meko-mcp-tools/SKILL.md +6 -5
- package/assets/skills/meko-mcp-tools/references/_sections.md +1 -1
- package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +194 -10
- package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +6 -3
- package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +11 -6
- package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +4 -4
- package/assets/skills/meko-mcp-tools/references/tools-overview.md +19 -14
- package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +69 -8
- package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +3 -1
- package/assets/skills/meko-mcp-tools-desktop/SKILL.md +8 -7
- 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-cookbook.md +194 -10
- package/assets/skills/meko-mcp-tools-desktop/references/tools-known-limitations.md +10 -5
- package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +4 -4
- package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +19 -14
- package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +68 -8
- package/assets/skills/meko-mcp-tools-desktop/references/tools-troubleshooting.md +3 -1
- package/assets/skills/meko-select-datapack-desktop/SKILL.md +91 -12
- package/assets/skills/meko-select-datapack-desktop/references/cookbook.md +39 -7
- package/assets/skills/meko-select-datapack-desktop/references/role-display-future.md +8 -6
- package/assets/skills/meko-select-datapack-desktop/references/troubleshooting.md +13 -1
- package/bin/create-meko-setup.mjs +152 -35
- package/lib/autofix.mjs +100 -0
- package/lib/browser-login.mjs +495 -0
- package/lib/clients/codex.mjs +1 -24
- package/lib/clients/common.mjs +25 -0
- package/lib/clients/opencode.mjs +6 -1
- package/lib/migrate/datapack.mjs +254 -16
- package/lib/migrate/mcp-client.mjs +34 -0
- package/lib/migrate/orchestrator.mjs +1 -1
- package/lib/migrate.mjs +2 -6
- package/lib/validate.mjs +111 -22
- package/package.json +5 -5
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
"use strict";
|
|
19
19
|
|
|
20
20
|
const {
|
|
21
|
+
autofixEnabled,
|
|
22
|
+
autofixIntervalSeconds,
|
|
21
23
|
computeAggregateHealth,
|
|
22
24
|
readHealthCache,
|
|
23
25
|
scanCompleteForDropClamp,
|
|
@@ -107,6 +109,94 @@ function statuslineSuffixFromHealth(health) {
|
|
|
107
109
|
return `capture:${short}(${queued})`;
|
|
108
110
|
}
|
|
109
111
|
|
|
112
|
+
const NEEDS_YOU_LABELS = {
|
|
113
|
+
auth: "API key",
|
|
114
|
+
account_limit: "plan limit",
|
|
115
|
+
datapack: "datapack access",
|
|
116
|
+
forbidden: "access",
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Claude Code status bar text for capture, or "" when there is nothing to
|
|
121
|
+
* show. Built when the health cache is written and read by
|
|
122
|
+
* meko-statusline.sh, which the npm installer copies and wires into Claude
|
|
123
|
+
* Code's statusLine; the plugin ships no status-line reader, so a plugin-only
|
|
124
|
+
* install shows nothing. `autofix` is the last `--capture-cleanup fix` summary
|
|
125
|
+
* (autofix-last.json). Uses a clock time for the next automatic fix, so the
|
|
126
|
+
* text stays right between cache writes.
|
|
127
|
+
*
|
|
128
|
+
* @param {object} health
|
|
129
|
+
* @param {object|null} autofix
|
|
130
|
+
* @param {number} [nowMs]
|
|
131
|
+
* @returns {string}
|
|
132
|
+
*/
|
|
133
|
+
function statuslineText(health, autofix, nowMs) {
|
|
134
|
+
return statuslineFields(health, autofix, nowMs).statusline;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* statuslineText plus when it stops being true: `statusline_until` (epoch
|
|
139
|
+
* seconds) and the text to show after that, `statusline_after`. The
|
|
140
|
+
* status bar compares them with the clock, so "auto-fixed N" disappears after
|
|
141
|
+
* an hour and "auto-fix at 14:30" turns into "auto-fix soon" at 14:30, even
|
|
142
|
+
* when nothing rewrites the cache.
|
|
143
|
+
*/
|
|
144
|
+
function statuslineFields(health, autofix, nowMs) {
|
|
145
|
+
const until = (ms, after) => ({ statusline_until: Math.floor(ms / 1000), statusline_after: after });
|
|
146
|
+
const h = health && typeof health === "object" ? health : {};
|
|
147
|
+
const status = h.status || "healthy";
|
|
148
|
+
const now = nowMs != null ? nowMs : Date.now();
|
|
149
|
+
const a = autofix && typeof autofix === "object" ? autofix : null;
|
|
150
|
+
const sessions = Array.isArray(h.sessions) ? h.sessions : [];
|
|
151
|
+
const queued = Number(h.queued_exchanges || 0);
|
|
152
|
+
const stopped = sessions.filter((x) => x && x.parked_reason === "stop");
|
|
153
|
+
const parkedByFix = sessions.filter((x) => x && x.parked_reason && x.parked_reason !== "stop");
|
|
154
|
+
const stuck = sessions.filter((x) => x && x.status !== "healthy" && !x.parked_reason);
|
|
155
|
+
|
|
156
|
+
// A run that never finished (its timer was killed) leaves a placeholder;
|
|
157
|
+
// once it is older than any run can take, its needs_you list is stale.
|
|
158
|
+
const ranAtMs = a ? Date.parse(a.ran_at) : NaN;
|
|
159
|
+
const abandoned = Boolean(a && a.running && (!Number.isFinite(ranAtMs) || now - ranAtMs > 30 * 60 * 1000));
|
|
160
|
+
const needs = a && !abandoned && Array.isArray(a.needs_you) ? a.needs_you : [];
|
|
161
|
+
if (needs.length && (parkedByFix.length || stuck.length)) {
|
|
162
|
+
const labels = [...new Set(needs.map((n) => NEEDS_YOU_LABELS[n.cause] || n.cause))];
|
|
163
|
+
return { statusline: `capture: needs you (${labels.join(", ")})` };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (status === "healthy") {
|
|
167
|
+
const fixed = a && a.counts ? Number(a.counts.fixed || 0) : 0;
|
|
168
|
+
const ranAt = a ? Date.parse(a.ran_at) : NaN;
|
|
169
|
+
if (a && a.mode === "auto" && fixed > 0 && Number.isFinite(ranAt) && now - ranAt < 3600 * 1000) {
|
|
170
|
+
return { statusline: `capture: auto-fixed ${fixed}`, ...until(ranAt + 3600 * 1000, "") };
|
|
171
|
+
}
|
|
172
|
+
return { statusline: "" };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const q = `${queued} queued`;
|
|
176
|
+
if (!stuck.length && !parkedByFix.length && stopped.length) {
|
|
177
|
+
return { statusline: `capture: paused (${q})` };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let next = "";
|
|
181
|
+
let due = null;
|
|
182
|
+
if (autofixEnabled()) {
|
|
183
|
+
const ranAt = a ? Date.parse(a.ran_at) : NaN;
|
|
184
|
+
due = (Number.isFinite(ranAt) ? ranAt : now) + autofixIntervalSeconds() * 1000;
|
|
185
|
+
next =
|
|
186
|
+
due <= now
|
|
187
|
+
? ", auto-fix soon"
|
|
188
|
+
: `, auto-fix at ${new Date(due).toTimeString().slice(0, 5)}`;
|
|
189
|
+
}
|
|
190
|
+
const label =
|
|
191
|
+
status === "degraded_retrying" ? "retrying" : status === "blocked_action_required" ? "blocked" : null;
|
|
192
|
+
if (!label) return { statusline: `capture: sending ${queued}` };
|
|
193
|
+
const text = `capture: ${label} (${q})${next}`;
|
|
194
|
+
if (due != null && due > now) {
|
|
195
|
+
return { statusline: text, ...until(due, `capture: ${label} (${q}), auto-fix soon`) };
|
|
196
|
+
}
|
|
197
|
+
return { statusline: text };
|
|
198
|
+
}
|
|
199
|
+
|
|
110
200
|
/**
|
|
111
201
|
* Sentence describing exchanges that were skipped because the server will
|
|
112
202
|
* never accept them, or "" when there are none new to report.
|
|
@@ -236,7 +326,8 @@ function buildHealthNotice({ health, previousStatus, previousDropped } = {}) {
|
|
|
236
326
|
: "catching_up";
|
|
237
327
|
|
|
238
328
|
const cleanupHint = retriesRunning(h)
|
|
239
|
-
? "
|
|
329
|
+
? " To repair it, offer `npx @yugabytedb/meko-mcp@latest " +
|
|
330
|
+
"--capture-cleanup fix`. To only stop the retries, offer " +
|
|
240
331
|
"`npx @yugabytedb/meko-mcp@latest --capture-cleanup stop`; it parks " +
|
|
241
332
|
"the backlog without deleting it."
|
|
242
333
|
: "";
|
|
@@ -429,6 +520,8 @@ module.exports = {
|
|
|
429
520
|
affectedSessionCount,
|
|
430
521
|
reasonForHealth,
|
|
431
522
|
statuslineSuffixFromHealth,
|
|
523
|
+
statuslineText,
|
|
524
|
+
statuslineFields,
|
|
432
525
|
buildHealthNotice,
|
|
433
526
|
injectHealthNotice,
|
|
434
527
|
formatCaptureStatusText,
|
|
@@ -131,6 +131,53 @@ function healthCacheFilename() {
|
|
|
131
131
|
return envString("MEKO_HEALTH_CACHE", DEFAULTS.HEALTH_CACHE);
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
/**
|
|
135
|
+
* The retry date `--capture-cleanup` parks a session until. Hooks skip the
|
|
136
|
+
* network while failure_class is set and next_retry_at is in the future, so
|
|
137
|
+
* a session with this date is parked, not retrying.
|
|
138
|
+
*/
|
|
139
|
+
const CLEANUP_PARK_UNTIL = "9999-12-31T00:00:00.000Z";
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Automatic repair from the checkpoint timer. MEKO_AUTOFIX (on/off) wins when
|
|
143
|
+
* set; otherwise autofix-config.json in the capture directory, which the
|
|
144
|
+
* installer writes for --autofix / --no-autofix. On by default.
|
|
145
|
+
*/
|
|
146
|
+
function autofixEnabled() {
|
|
147
|
+
return autofixSetting().enabled;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Whether automatic repair is on, and what decided it: "env" (MEKO_AUTOFIX),
|
|
152
|
+
* "config" (autofix-config.json, written by --autofix / --no-autofix), or
|
|
153
|
+
* "default" (on).
|
|
154
|
+
*/
|
|
155
|
+
function autofixSetting() {
|
|
156
|
+
const raw = process.env.MEKO_AUTOFIX;
|
|
157
|
+
if (raw != null && raw !== "") {
|
|
158
|
+
return { enabled: !/^(0|off|false|no)$/i.test(String(raw)), source: "env", path: null };
|
|
159
|
+
}
|
|
160
|
+
const file = autofixConfigPath();
|
|
161
|
+
try {
|
|
162
|
+
const cfg = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
163
|
+
if (cfg && typeof cfg.enabled === "boolean") {
|
|
164
|
+
return { enabled: cfg.enabled, source: "config", path: file };
|
|
165
|
+
}
|
|
166
|
+
} catch {
|
|
167
|
+
/* no config: default on */
|
|
168
|
+
}
|
|
169
|
+
return { enabled: true, source: "default", path: null };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function autofixConfigPath() {
|
|
173
|
+
return path.join(watermarkDir(), "autofix-config.json");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Seconds between automatic `--capture-cleanup fix` runs on one machine. */
|
|
177
|
+
function autofixIntervalSeconds() {
|
|
178
|
+
return Math.max(60, envInt("MEKO_AUTOFIX_INTERVAL", 1800));
|
|
179
|
+
}
|
|
180
|
+
|
|
134
181
|
// ---------------------------------------------------------------------------
|
|
135
182
|
// Path helpers
|
|
136
183
|
// ---------------------------------------------------------------------------
|
|
@@ -162,6 +209,20 @@ function healthCachePath() {
|
|
|
162
209
|
return path.join(watermarkDir(), healthCacheFilename());
|
|
163
210
|
}
|
|
164
211
|
|
|
212
|
+
/** Summary of the last `--capture-cleanup fix` run, manual or automatic. */
|
|
213
|
+
function autofixLastPath() {
|
|
214
|
+
return path.join(watermarkDir(), "autofix-last.json");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function readAutofixLast() {
|
|
218
|
+
try {
|
|
219
|
+
const value = JSON.parse(fs.readFileSync(autofixLastPath(), "utf8"));
|
|
220
|
+
return value && typeof value === "object" ? value : null;
|
|
221
|
+
} catch {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
165
226
|
/**
|
|
166
227
|
* Convert an agent_id into the filesystem slug used by legacy error files and
|
|
167
228
|
* datapack pins (matches capture.js datapackPinSlug).
|
|
@@ -1295,6 +1356,8 @@ const PERSISTENT_PATTERNS = [
|
|
|
1295
1356
|
["payment_required", /payment[_\s-]?required/i],
|
|
1296
1357
|
["subscription_required", /subscription[_\s-]?(required|inactive|expired)/i],
|
|
1297
1358
|
["entitlement_denied", /entitlement.*(denied|required|missing)|(?:denied|required|missing).*entitlement/i],
|
|
1359
|
+
// A viewer of a shared datapack can't record conversations (MEKO-876).
|
|
1360
|
+
["read_only_access", /read_only_access/i],
|
|
1298
1361
|
];
|
|
1299
1362
|
|
|
1300
1363
|
/**
|
|
@@ -1422,6 +1485,26 @@ function timerLooksLive(sessionId) {
|
|
|
1422
1485
|
}
|
|
1423
1486
|
}
|
|
1424
1487
|
|
|
1488
|
+
/**
|
|
1489
|
+
* A session whose client may still be writing turns: its lifecycle is
|
|
1490
|
+
* active and its transcript changed within recoveryMinIdle() seconds, or its
|
|
1491
|
+
* checkpoint timer is alive. --capture-cleanup uses it to decide drain or
|
|
1492
|
+
* recover, and capture.js move re-checks it under the lease.
|
|
1493
|
+
*/
|
|
1494
|
+
function sessionLooksLive(state, nowMs) {
|
|
1495
|
+
if (!state || state.lifecycle !== "active") return false;
|
|
1496
|
+
const now = nowMs != null ? nowMs : Date.now();
|
|
1497
|
+
if (state.transcript_path) {
|
|
1498
|
+
try {
|
|
1499
|
+
const st = fs.statSync(state.transcript_path);
|
|
1500
|
+
if (now - st.mtimeMs < recoveryMinIdle() * 1000) return true;
|
|
1501
|
+
} catch {
|
|
1502
|
+
// A missing transcript says nothing about liveness; check the timer.
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
return timerLooksLive(state.session_id);
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1425
1508
|
function buildEligibility(state, nowMs, options) {
|
|
1426
1509
|
const opts = options && typeof options === "object" ? options : {};
|
|
1427
1510
|
const now = nowMs != null ? nowMs : Date.now();
|
|
@@ -1736,6 +1819,16 @@ function computeAggregateHealth(states, options) {
|
|
|
1736
1819
|
queued_exchanges: q,
|
|
1737
1820
|
dropped_exchanges: d,
|
|
1738
1821
|
lifecycle: state ? state.lifecycle : null,
|
|
1822
|
+
// Why --capture-cleanup parked this session ("stop", "drain failed: ...")
|
|
1823
|
+
// while the park is in effect; null otherwise.
|
|
1824
|
+
parked_reason:
|
|
1825
|
+
state &&
|
|
1826
|
+
state.parked &&
|
|
1827
|
+
typeof state.parked === "object" &&
|
|
1828
|
+
state.failure_class &&
|
|
1829
|
+
state.next_retry_at === CLEANUP_PARK_UNTIL
|
|
1830
|
+
? String(state.parked.reason || "parked")
|
|
1831
|
+
: null,
|
|
1739
1832
|
});
|
|
1740
1833
|
}
|
|
1741
1834
|
|
|
@@ -1782,6 +1875,16 @@ function writeHealthCache(health, options) {
|
|
|
1782
1875
|
? Math.min(previous, coerceNonNegInt(merged.dropped_exchanges, 0))
|
|
1783
1876
|
: previous;
|
|
1784
1877
|
}
|
|
1878
|
+
// A ready-made status bar text, so meko-statusline.sh needs no JSON parsing
|
|
1879
|
+
// beyond one field. capture-notices requires this module, so load it here.
|
|
1880
|
+
try {
|
|
1881
|
+
const fields = require("./capture-notices").statuslineFields(merged, readAutofixLast());
|
|
1882
|
+
merged.statusline = fields.statusline;
|
|
1883
|
+
merged.statusline_until = fields.statusline_until != null ? fields.statusline_until : null;
|
|
1884
|
+
merged.statusline_after = fields.statusline_after != null ? fields.statusline_after : null;
|
|
1885
|
+
} catch {
|
|
1886
|
+
/* keep the cache write even if the text can't be built */
|
|
1887
|
+
}
|
|
1785
1888
|
try {
|
|
1786
1889
|
atomicWriteJson(healthCachePath(), merged);
|
|
1787
1890
|
return { ok: true, path: healthCachePath(), health: merged };
|
|
@@ -2117,6 +2220,13 @@ module.exports = {
|
|
|
2117
2220
|
workerMaxAgeSeconds,
|
|
2118
2221
|
timerMaxAge,
|
|
2119
2222
|
healthCacheFilename,
|
|
2223
|
+
CLEANUP_PARK_UNTIL,
|
|
2224
|
+
autofixEnabled,
|
|
2225
|
+
autofixSetting,
|
|
2226
|
+
autofixConfigPath,
|
|
2227
|
+
autofixIntervalSeconds,
|
|
2228
|
+
autofixLastPath,
|
|
2229
|
+
readAutofixLast,
|
|
2120
2230
|
|
|
2121
2231
|
// paths
|
|
2122
2232
|
watermarkDir,
|
|
@@ -2124,6 +2234,7 @@ module.exports = {
|
|
|
2124
2234
|
leasePath,
|
|
2125
2235
|
mutationLockPath,
|
|
2126
2236
|
timerPidPath,
|
|
2237
|
+
sessionLooksLive,
|
|
2127
2238
|
healthCachePath,
|
|
2128
2239
|
legacyErrorPath,
|
|
2129
2240
|
agentSlug,
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
* node capture.js checkpoint # stdin hook JSON → drainSession
|
|
14
14
|
* node capture.js drain # stdin JSON → drain one session; stdout DrainResult
|
|
15
15
|
* node capture.js recover # alias of drain (single-session recovery)
|
|
16
|
+
* node capture.js move # stdin {"session_id"} → move the unsent
|
|
17
|
+
* # tail to a new conversation; stdout
|
|
18
|
+
* # {ok, from, to, last_line_number} or
|
|
19
|
+
* # {ok:false, busy?, error}
|
|
16
20
|
*
|
|
17
21
|
* drain / recover stdin contract (Workstream C):
|
|
18
22
|
* {
|
|
@@ -1563,6 +1567,9 @@ function classifyPersistentCaptureFailure(err, opts) {
|
|
|
1563
1567
|
// access is (re-)granted out of band, so the caller should hold rather
|
|
1564
1568
|
// than retry forever or silently drop the turn.
|
|
1565
1569
|
["datapack_access_denied", /datapack_access_denied/],
|
|
1570
|
+
// MEKO-876: a viewer of a shared datapack can't record conversations.
|
|
1571
|
+
// Account-scoped like the above: it stays refused until the role changes.
|
|
1572
|
+
["read_only_access", /read_only_access/],
|
|
1566
1573
|
];
|
|
1567
1574
|
for (const [code, pattern] of persistentPatterns) {
|
|
1568
1575
|
if (pattern.test(normalized)) {
|
|
@@ -1575,6 +1582,7 @@ function classifyPersistentCaptureFailure(err, opts) {
|
|
|
1575
1582
|
subscription_required: "capture requires an active subscription",
|
|
1576
1583
|
entitlement_denied: "the account is not entitled to capture conversations",
|
|
1577
1584
|
datapack_access_denied: "the account does not have access to the target datapack",
|
|
1585
|
+
read_only_access: "the account has viewer (read-only) access to the target datapack",
|
|
1578
1586
|
};
|
|
1579
1587
|
return { code, reason: reasons[code], scope: "account", detail };
|
|
1580
1588
|
}
|
|
@@ -1771,6 +1779,111 @@ async function recoverMissingConversation({
|
|
|
1771
1779
|
return { ok: true, state: written.state || next, convId: newConvId };
|
|
1772
1780
|
}
|
|
1773
1781
|
|
|
1782
|
+
/**
|
|
1783
|
+
* Move a session's unsent turns to a new conversation on request, without
|
|
1784
|
+
* waiting for capture to see `not_found` twice. Used by
|
|
1785
|
+
* `--capture-cleanup fix` when the recorded conversation can't be served:
|
|
1786
|
+
* its owner reads as `None`, its lookup keeps failing, or a replay loop
|
|
1787
|
+
* bloated it. Holds the drain lease for the whole move, keeps the cursor,
|
|
1788
|
+
* and creates the conversation under the session's recorded owner and
|
|
1789
|
+
* datapack.
|
|
1790
|
+
*
|
|
1791
|
+
* The caller can pass `expected_conversation_id`. Under the lease, the move
|
|
1792
|
+
* is refused (`changed: true`) when the session's conversation no longer
|
|
1793
|
+
* matches it, or when a previous move is still waiting for its new
|
|
1794
|
+
* conversation to accept a turn (`recovered_from_conversation_id`): a hook
|
|
1795
|
+
* drain may have recovered or delivered the session while the caller waited.
|
|
1796
|
+
*
|
|
1797
|
+
* @param {{ session_id: string, expected_conversation_id?: string }} options
|
|
1798
|
+
* @returns {Promise<{ok: boolean, busy?: boolean, changed?: boolean, from?: string, to?: string, last_line_number?: number, error?: string}>}
|
|
1799
|
+
*/
|
|
1800
|
+
async function moveSessionToNewConversation(options) {
|
|
1801
|
+
const sessionId = String((options && options.session_id) || "").trim();
|
|
1802
|
+
if (!sessionId) return { ok: false, error: "session_id required" };
|
|
1803
|
+
const lease = captureState.acquireLease(sessionId);
|
|
1804
|
+
if (!lease.ok) {
|
|
1805
|
+
return {
|
|
1806
|
+
ok: false,
|
|
1807
|
+
busy: lease.failure_class === "ownership",
|
|
1808
|
+
error: lease.error || "drain lease unavailable",
|
|
1809
|
+
};
|
|
1810
|
+
}
|
|
1811
|
+
try {
|
|
1812
|
+
const read = captureState.readState(sessionId);
|
|
1813
|
+
if (!read.ok) return { ok: false, error: read.error || "watermark unreadable" };
|
|
1814
|
+
let state = read.state;
|
|
1815
|
+
if (!state.agent_id) return { ok: false, error: "recorded owner missing" };
|
|
1816
|
+
const expected = options && options.expected_conversation_id;
|
|
1817
|
+
// An automatic cleanup run must not end a park the user set with `stop`
|
|
1818
|
+
// after that run scanned the session.
|
|
1819
|
+
if (
|
|
1820
|
+
options &&
|
|
1821
|
+
options.respect_stop &&
|
|
1822
|
+
state.parked &&
|
|
1823
|
+
state.parked.reason === "stop" &&
|
|
1824
|
+
state.failure_class &&
|
|
1825
|
+
state.next_retry_at === captureState.CLEANUP_PARK_UNTIL
|
|
1826
|
+
) {
|
|
1827
|
+
return { ok: false, paused: true, error: "paused by the user with stop" };
|
|
1828
|
+
}
|
|
1829
|
+
if (state.recovered_from_conversation_id) {
|
|
1830
|
+
return {
|
|
1831
|
+
ok: false,
|
|
1832
|
+
changed: true,
|
|
1833
|
+
error: "a previous move is waiting for its new conversation to accept a turn",
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1836
|
+
if (expected && state.conversation_id !== expected) {
|
|
1837
|
+
return {
|
|
1838
|
+
ok: false,
|
|
1839
|
+
changed: true,
|
|
1840
|
+
error: `the session moved to conversation ${state.conversation_id} while waiting`,
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
// Checked again here, under the lease: the session may have become live
|
|
1844
|
+
// after --capture-cleanup planned the move. SessionStart gave a live
|
|
1845
|
+
// session's agent its conversation_id, and a move can't tell it the new one.
|
|
1846
|
+
if (options && options.defer_if_live && captureState.sessionLooksLive(state)) {
|
|
1847
|
+
return { ok: false, deferred: true, error: "the session is still open" };
|
|
1848
|
+
}
|
|
1849
|
+
// MEKO-592: a session that started before the user pinned a datapack has
|
|
1850
|
+
// none in its watermark. Re-read the pin the way drainSession does, so
|
|
1851
|
+
// the new conversation is created in the datapack its turns will go to.
|
|
1852
|
+
if (!state.datapack_id) {
|
|
1853
|
+
const pin = readDatapackPin(state.agent_id);
|
|
1854
|
+
if (pin && pin.datapack_id) {
|
|
1855
|
+
state = { ...state, datapack_id: pin.datapack_id, datapack_name: pin.datapack_name || null };
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
await mcpInitialize();
|
|
1859
|
+
const moved = await recoverMissingConversation({
|
|
1860
|
+
// cleanup_moved_epoch lets --capture-cleanup count epochs since its own
|
|
1861
|
+
// last move, not the session's lifetime total. The move also ends any
|
|
1862
|
+
// park the cleanup tool set (the retry fields are reset here).
|
|
1863
|
+
state: {
|
|
1864
|
+
...state,
|
|
1865
|
+
parked: null,
|
|
1866
|
+
cleanup_moved_epoch: Number(state.conversation_epoch || 0) + 1,
|
|
1867
|
+
},
|
|
1868
|
+
sessionId,
|
|
1869
|
+
agentId: state.agent_id,
|
|
1870
|
+
transcriptPath: state.transcript_path,
|
|
1871
|
+
datapackId: state.datapack_id || null,
|
|
1872
|
+
});
|
|
1873
|
+
if (!moved.ok) return { ok: false, error: moved.error };
|
|
1874
|
+
return {
|
|
1875
|
+
ok: true,
|
|
1876
|
+
from: state.conversation_id || null,
|
|
1877
|
+
to: moved.convId,
|
|
1878
|
+
last_line_number: moved.state.last_line_number,
|
|
1879
|
+
};
|
|
1880
|
+
} catch (err) {
|
|
1881
|
+
return { ok: false, error: err.message };
|
|
1882
|
+
} finally {
|
|
1883
|
+
captureState.releaseLease(sessionId, lease.lease.token);
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1774
1887
|
/**
|
|
1775
1888
|
* Abort all pending MCP requests. Used by the preload timeout path so the
|
|
1776
1889
|
* SessionStart hook can return promptly instead of waiting for a slow server
|
|
@@ -1894,6 +2007,7 @@ function peekTranscriptMetadata(transcriptPath) {
|
|
|
1894
2007
|
async function fetchRecentMemories(agentId, datapackId = null, { limit = 10, budget = 2000 } = {}) {
|
|
1895
2008
|
// memory_get_all takes no `limit`, and FastMCP rejects an unknown argument
|
|
1896
2009
|
// with an isError result, so `limit` caps the page client-side instead.
|
|
2010
|
+
// Don't send `page_size` either: a server older than MEKO-453 rejects it.
|
|
1897
2011
|
const payload = {
|
|
1898
2012
|
agent_id: agentId,
|
|
1899
2013
|
conversation_id: "00000000-0000-0000-0000-000000000000",
|
|
@@ -2324,7 +2438,7 @@ function buildSessionStartFallbackContext(agentId, opts) {
|
|
|
2324
2438
|
// act on.
|
|
2325
2439
|
const reason = opts && opts.reason;
|
|
2326
2440
|
const preamble = reason === "datapack_required"
|
|
2327
|
-
? "The SessionStart hook reached the Meko MCP server, but no datapack is pinned for this project, so the server declined to create a conversation. **Automatic conversation capture is idle**. Use `datapack_list` to find a datapack id, then invoke the `meko-select-datapack` skill (or your client's equivalent) to pin it. The next drain picks up the fresh pin and starts capturing this session's queued turns; until then, save facts explicitly."
|
|
2441
|
+
? "The SessionStart hook reached the Meko MCP server, but no datapack is pinned for this project, so the server declined to create a conversation. **Automatic conversation capture is idle**. Use `datapack_search(query=...)` (or `datapack_list` on a server without it) to find a datapack id, then invoke the `meko-select-datapack` skill (or your client's equivalent) to pin it. The next drain picks up the fresh pin and starts capturing this session's queued turns; until then, save facts explicitly."
|
|
2328
2442
|
: "The SessionStart hook could not reach the Meko MCP server to create a conversation. **Automatic conversation capture is NOT running this session**, so the server-side extraction that normally saves memories for you will not fire. Fall back to saving facts explicitly.";
|
|
2329
2443
|
return `## Meko Memory Instructions
|
|
2330
2444
|
|
|
@@ -2651,7 +2765,7 @@ async function handleSessionStart(hookInput) {
|
|
|
2651
2765
|
const isDatapackRequired = err && err.code === "datapack_id_required";
|
|
2652
2766
|
if (isDatapackRequired) {
|
|
2653
2767
|
process.stderr.write(
|
|
2654
|
-
`[meko-capture] SessionStart: no datapack pinned; auto-capture is idle. Pin one with datapack_list + meko-select-datapack; the next drain picks up the fresh pin.\n`,
|
|
2768
|
+
`[meko-capture] SessionStart: no datapack pinned; auto-capture is idle. Pin one with datapack_search (or datapack_list) + meko-select-datapack; the next drain picks up the fresh pin.\n`,
|
|
2655
2769
|
);
|
|
2656
2770
|
} else if (err && err.isMcpToolError) {
|
|
2657
2771
|
// Server accepted the call and returned a structured error (e.g.
|
|
@@ -3683,10 +3797,11 @@ async function main() {
|
|
|
3683
3797
|
"checkpoint",
|
|
3684
3798
|
"drain",
|
|
3685
3799
|
"recover",
|
|
3800
|
+
"move",
|
|
3686
3801
|
];
|
|
3687
3802
|
if (!hookType || !known.includes(hookType)) {
|
|
3688
3803
|
process.stderr.write(
|
|
3689
|
-
"Usage: capture.js <session-start|before-submit-prompt|pre-compact|session-end|checkpoint|drain|recover>\n",
|
|
3804
|
+
"Usage: capture.js <session-start|before-submit-prompt|pre-compact|session-end|checkpoint|drain|recover|move>\n",
|
|
3690
3805
|
);
|
|
3691
3806
|
process.exit(1);
|
|
3692
3807
|
}
|
|
@@ -3699,7 +3814,11 @@ async function main() {
|
|
|
3699
3814
|
process.stderr.write(
|
|
3700
3815
|
`[meko-capture] Failed to parse hook input from stdin: ${err.message}\n`,
|
|
3701
3816
|
);
|
|
3702
|
-
if (hookType === "
|
|
3817
|
+
if (hookType === "move") {
|
|
3818
|
+
process.stdout.write(
|
|
3819
|
+
`${JSON.stringify({ ok: false, error: `invalid stdin JSON: ${err.message}` })}\n`,
|
|
3820
|
+
);
|
|
3821
|
+
} else if (hookType === "drain" || hookType === "recover") {
|
|
3703
3822
|
process.stdout.write(
|
|
3704
3823
|
JSON.stringify(
|
|
3705
3824
|
makeDrainResult({
|
|
@@ -3720,6 +3839,12 @@ async function main() {
|
|
|
3720
3839
|
return;
|
|
3721
3840
|
}
|
|
3722
3841
|
|
|
3842
|
+
if (hookType === "move") {
|
|
3843
|
+
const result = await moveSessionToNewConversation(hookInput);
|
|
3844
|
+
process.stdout.write(JSON.stringify(result) + "\n");
|
|
3845
|
+
return;
|
|
3846
|
+
}
|
|
3847
|
+
|
|
3723
3848
|
if (hookType === "before-submit-prompt") {
|
|
3724
3849
|
handleBeforeSubmitPrompt(hookInput);
|
|
3725
3850
|
return;
|
|
@@ -3796,6 +3921,7 @@ module.exports = {
|
|
|
3796
3921
|
addMessage,
|
|
3797
3922
|
isMissingConversationError,
|
|
3798
3923
|
recoverMissingConversation,
|
|
3924
|
+
moveSessionToNewConversation,
|
|
3799
3925
|
};
|
|
3800
3926
|
|
|
3801
3927
|
if (require.main === module) {
|