@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
|
@@ -5,22 +5,32 @@
|
|
|
5
5
|
* CommonJS, zero npm deps. Safe to require from capture.js SessionStart
|
|
6
6
|
* without touching the drain loop. See docs/plans/meko-capture-v2-contracts.md.
|
|
7
7
|
*
|
|
8
|
-
* Health cache field
|
|
8
|
+
* Health cache field extensions (documented):
|
|
9
9
|
* capture-health.json may include `last_notified_status` — the aggregate
|
|
10
10
|
* status last shown to the user via SessionStart. Used for one-shot
|
|
11
11
|
* recovery notices when status returns to `healthy`. writeHealthCache()
|
|
12
12
|
* preserves this field when callers omit it.
|
|
13
|
+
*
|
|
14
|
+
* `last_notified_dropped` records the aggregate dropped-exchange count last
|
|
15
|
+
* shown. A complete refresh may clamp it when session watermarks disappear.
|
|
13
16
|
*/
|
|
14
17
|
|
|
15
18
|
"use strict";
|
|
16
19
|
|
|
17
20
|
const {
|
|
21
|
+
autofixEnabled,
|
|
22
|
+
autofixIntervalSeconds,
|
|
18
23
|
computeAggregateHealth,
|
|
19
24
|
readHealthCache,
|
|
25
|
+
scanCompleteForDropClamp,
|
|
20
26
|
scanStates,
|
|
21
27
|
writeHealthCache,
|
|
22
28
|
} = require("./capture-state");
|
|
23
29
|
|
|
30
|
+
// blocked_reason prefix written by `--capture-cleanup stop`
|
|
31
|
+
// (capture-cleanup-cli.js RESUME_HINT).
|
|
32
|
+
const PARKED_REASON_PREFIX = "parked by capture cleanup";
|
|
33
|
+
|
|
24
34
|
const NON_HEALTHY = new Set([
|
|
25
35
|
"catching_up",
|
|
26
36
|
"degraded_retrying",
|
|
@@ -99,14 +109,160 @@ function statuslineSuffixFromHealth(health) {
|
|
|
99
109
|
return `capture:${short}(${queued})`;
|
|
100
110
|
}
|
|
101
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
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Sentence describing exchanges that were skipped because the server will
|
|
202
|
+
* never accept them, or "" when there are none new to report.
|
|
203
|
+
*
|
|
204
|
+
* Drops are permanent, so they are not a health status — capture is running
|
|
205
|
+
* fine — but the user is owed the fact that specific turns are missing.
|
|
206
|
+
*/
|
|
207
|
+
function dropSentence(dropped, previouslyNotified, health) {
|
|
208
|
+
const newDrops = dropped - previouslyNotified;
|
|
209
|
+
if (newDrops <= 0) return "";
|
|
210
|
+
const reasons = Array.isArray(health && health.drop_reasons)
|
|
211
|
+
? health.drop_reasons.filter(Boolean)
|
|
212
|
+
: [];
|
|
213
|
+
// drop_reasons is the union of every session's last_drop_reason, including
|
|
214
|
+
// drops already announced. Attribute a reason only when there is one shared
|
|
215
|
+
// cause; otherwise point at --capture-status instead of mis-blaming.
|
|
216
|
+
const because =
|
|
217
|
+
reasons.length === 1
|
|
218
|
+
? ` Reason: ${reasons[0]}.`
|
|
219
|
+
: reasons.length > 1
|
|
220
|
+
? " Run `meko-mcp --capture-status` for drop reasons."
|
|
221
|
+
: "";
|
|
222
|
+
return (
|
|
223
|
+
`[Meko capture] ${newDrops} exchange(s) could not be stored and were ` +
|
|
224
|
+
`skipped; the rest of the session is captured normally.${because} ` +
|
|
225
|
+
`Tell the user those turns are missing from Meko.`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* True when some session is still retrying. A session parked by
|
|
231
|
+
* `--capture-cleanup stop` is blocked on purpose and never retries, so it
|
|
232
|
+
* doesn't count; its blocked reason already says how to resume it.
|
|
233
|
+
*/
|
|
234
|
+
function retriesRunning(health) {
|
|
235
|
+
// A degraded session with nothing left to send (leftover failure fields on
|
|
236
|
+
// a delivered session) isn't retrying; `repair` clears it, not `stop`.
|
|
237
|
+
const sessions = Array.isArray(health && health.sessions) ? health.sessions : [];
|
|
238
|
+
const retrying = sessions.some(
|
|
239
|
+
(s) =>
|
|
240
|
+
s &&
|
|
241
|
+
s.status === "degraded_retrying" &&
|
|
242
|
+
(Number(s.queued_exchanges || 0) > 0 ||
|
|
243
|
+
["needs_conversation", "pending", "draining", "retry_wait"].includes(s.delivery)),
|
|
244
|
+
);
|
|
245
|
+
if (retrying) return true;
|
|
246
|
+
if (health.status !== "blocked_action_required") return false;
|
|
247
|
+
const reasons = Array.isArray(health.blocked_reasons)
|
|
248
|
+
? health.blocked_reasons.filter(Boolean)
|
|
249
|
+
: [];
|
|
250
|
+
return (
|
|
251
|
+
reasons.length === 0 ||
|
|
252
|
+
reasons.some((r) => !String(r).startsWith(PARKED_REASON_PREFIX))
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
102
256
|
/**
|
|
103
257
|
* Build a SessionStart-injectable health notice.
|
|
104
258
|
*
|
|
105
259
|
* - When status ≠ healthy: always emit (exact affected-session + queued counts + reason).
|
|
106
260
|
* - When status === healthy and previousStatus was non-healthy: one-shot recovery notice.
|
|
107
261
|
* - When status === healthy and previous was already healthy / null: no notice.
|
|
262
|
+
* - Any new dropped exchanges are appended in every case, and are enough on
|
|
263
|
+
* their own to produce a notice while status is healthy.
|
|
108
264
|
*
|
|
109
|
-
* @param {{ health: object, previousStatus?: string|null }} opts
|
|
265
|
+
* @param {{ health: object, previousStatus?: string|null, previousDropped?: number }} opts
|
|
110
266
|
* @returns {{
|
|
111
267
|
* notice: string|null,
|
|
112
268
|
* nextNotifiedStatus: string,
|
|
@@ -114,10 +270,11 @@ function statuslineSuffixFromHealth(health) {
|
|
|
114
270
|
* status: string,
|
|
115
271
|
* affectedSessions: number,
|
|
116
272
|
* queuedExchanges: number,
|
|
273
|
+
* droppedExchanges: number,
|
|
117
274
|
* reason: string,
|
|
118
275
|
* }}
|
|
119
276
|
*/
|
|
120
|
-
function buildHealthNotice({ health, previousStatus } = {}) {
|
|
277
|
+
function buildHealthNotice({ health, previousStatus, previousDropped } = {}) {
|
|
121
278
|
const h = health && typeof health === "object" ? health : { status: "healthy" };
|
|
122
279
|
const status = h.status || "healthy";
|
|
123
280
|
const prev =
|
|
@@ -126,29 +283,37 @@ function buildHealthNotice({ health, previousStatus } = {}) {
|
|
|
126
283
|
: String(previousStatus);
|
|
127
284
|
const affected = affectedSessionCount(h);
|
|
128
285
|
const queued = Number(h.queued_exchanges || 0);
|
|
286
|
+
const dropped = Number(h.dropped_exchanges || 0);
|
|
287
|
+
const notifiedDropped = Number(previousDropped || 0);
|
|
288
|
+
const drops = dropSentence(dropped, notifiedDropped, h);
|
|
129
289
|
const reason = reasonForHealth(h);
|
|
290
|
+
const join = (a, b) => (a && b ? `${a}\n\n${b}` : a || b || null);
|
|
130
291
|
|
|
131
292
|
if (status === "healthy") {
|
|
132
293
|
if (prev && NON_HEALTHY.has(prev)) {
|
|
133
294
|
return {
|
|
134
|
-
notice:
|
|
295
|
+
notice: join(
|
|
135
296
|
"[Meko capture] Capture recovered — all sessions healthy. " +
|
|
136
|
-
|
|
297
|
+
"Queued backlog is clear. Automatic conversation capture is running normally again.",
|
|
298
|
+
drops,
|
|
299
|
+
),
|
|
137
300
|
nextNotifiedStatus: "healthy",
|
|
138
301
|
shouldPersistNotified: true,
|
|
139
302
|
status,
|
|
140
303
|
affectedSessions: 0,
|
|
141
304
|
queuedExchanges: queued,
|
|
305
|
+
droppedExchanges: dropped,
|
|
142
306
|
reason,
|
|
143
307
|
};
|
|
144
308
|
}
|
|
145
309
|
return {
|
|
146
|
-
notice: null,
|
|
310
|
+
notice: drops || null,
|
|
147
311
|
nextNotifiedStatus: "healthy",
|
|
148
|
-
shouldPersistNotified: prev !== "healthy",
|
|
312
|
+
shouldPersistNotified: prev !== "healthy" || Boolean(drops),
|
|
149
313
|
status,
|
|
150
314
|
affectedSessions: 0,
|
|
151
315
|
queuedExchanges: queued,
|
|
316
|
+
droppedExchanges: dropped,
|
|
152
317
|
reason,
|
|
153
318
|
};
|
|
154
319
|
}
|
|
@@ -160,10 +325,18 @@ function buildHealthNotice({ health, previousStatus } = {}) {
|
|
|
160
325
|
? "degraded_retrying"
|
|
161
326
|
: "catching_up";
|
|
162
327
|
|
|
163
|
-
const
|
|
328
|
+
const cleanupHint = retriesRunning(h)
|
|
329
|
+
? " To repair it, offer `npx @yugabytedb/meko-mcp@latest " +
|
|
330
|
+
"--capture-cleanup fix`. To only stop the retries, offer " +
|
|
331
|
+
"`npx @yugabytedb/meko-mcp@latest --capture-cleanup stop`; it parks " +
|
|
332
|
+
"the backlog without deleting it."
|
|
333
|
+
: "";
|
|
334
|
+
const notice = join(
|
|
164
335
|
`[Meko capture] Status: ${label} — ${affected} session(s) affected, ` +
|
|
165
|
-
|
|
166
|
-
|
|
336
|
+
`${queued} exchange(s) queued. Reason: ${reason}. ` +
|
|
337
|
+
`Run \`meko-mcp --capture-status\` for details.${cleanupHint}`,
|
|
338
|
+
drops,
|
|
339
|
+
);
|
|
167
340
|
|
|
168
341
|
return {
|
|
169
342
|
notice,
|
|
@@ -172,6 +345,7 @@ function buildHealthNotice({ health, previousStatus } = {}) {
|
|
|
172
345
|
status,
|
|
173
346
|
affectedSessions: affected,
|
|
174
347
|
queuedExchanges: queued,
|
|
348
|
+
droppedExchanges: dropped,
|
|
175
349
|
reason,
|
|
176
350
|
};
|
|
177
351
|
}
|
|
@@ -195,14 +369,23 @@ function injectHealthNotice(context, opts) {
|
|
|
195
369
|
const base = context == null ? "" : String(context);
|
|
196
370
|
|
|
197
371
|
let health = null;
|
|
372
|
+
let scanComplete = false;
|
|
198
373
|
if (refresh) {
|
|
199
374
|
const scan = scanStates({ includeBlocked: true });
|
|
375
|
+
scanComplete = scanCompleteForDropClamp(scan);
|
|
200
376
|
health = computeAggregateHealth(scan.entries || []);
|
|
201
377
|
const existing = readHealthCache();
|
|
202
378
|
if (existing && existing.last_notified_status !== undefined) {
|
|
203
379
|
health.last_notified_status = existing.last_notified_status;
|
|
204
380
|
}
|
|
205
|
-
|
|
381
|
+
if (existing && existing.last_notified_dropped !== undefined) {
|
|
382
|
+
const previous = Number(existing.last_notified_dropped || 0);
|
|
383
|
+
health.last_notified_dropped = scanComplete
|
|
384
|
+
? Math.min(previous, Number(health.dropped_exchanges || 0))
|
|
385
|
+
: previous;
|
|
386
|
+
}
|
|
387
|
+
const written = writeHealthCache(health, { clampDropped: scanComplete });
|
|
388
|
+
if (written.ok) health = written.health;
|
|
206
389
|
} else {
|
|
207
390
|
health = readHealthCache() || {
|
|
208
391
|
schema_version: 1,
|
|
@@ -214,9 +397,11 @@ function injectHealthNotice(context, opts) {
|
|
|
214
397
|
blocked_action_required: 0,
|
|
215
398
|
},
|
|
216
399
|
queued_exchanges: 0,
|
|
400
|
+
dropped_exchanges: 0,
|
|
217
401
|
oldest_pending_age_seconds: null,
|
|
218
402
|
last_success_at: null,
|
|
219
403
|
blocked_reasons: [],
|
|
404
|
+
drop_reasons: [],
|
|
220
405
|
sessions: [],
|
|
221
406
|
};
|
|
222
407
|
}
|
|
@@ -225,13 +410,23 @@ function injectHealthNotice(context, opts) {
|
|
|
225
410
|
health.last_notified_status !== undefined
|
|
226
411
|
? health.last_notified_status
|
|
227
412
|
: null;
|
|
228
|
-
const
|
|
413
|
+
const previousDropped = Number(health.last_notified_dropped || 0);
|
|
414
|
+
const built = buildHealthNotice({ health, previousStatus, previousDropped });
|
|
229
415
|
|
|
230
416
|
if (persist && built.shouldPersistNotified) {
|
|
231
|
-
|
|
417
|
+
const nextHealth = {
|
|
232
418
|
...health,
|
|
233
419
|
last_notified_status: built.nextNotifiedStatus,
|
|
234
|
-
}
|
|
420
|
+
};
|
|
421
|
+
if (scanComplete || !refresh) {
|
|
422
|
+
nextHealth.last_notified_dropped = built.droppedExchanges;
|
|
423
|
+
} else {
|
|
424
|
+
nextHealth.last_notified_dropped = Math.max(
|
|
425
|
+
previousDropped,
|
|
426
|
+
built.droppedExchanges,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
writeHealthCache(nextHealth, { clampDropped: scanComplete });
|
|
235
430
|
}
|
|
236
431
|
|
|
237
432
|
if (!built.notice) return base;
|
|
@@ -255,6 +450,7 @@ function formatCaptureStatusText(health) {
|
|
|
255
450
|
`degraded_retrying=${Number(counts.degraded_retrying || 0)} ` +
|
|
256
451
|
`blocked_action_required=${Number(counts.blocked_action_required || 0)}`,
|
|
257
452
|
` queued exchanges: ${Number(h.queued_exchanges || 0)}`,
|
|
453
|
+
` dropped exchanges: ${Number(h.dropped_exchanges || 0)}`,
|
|
258
454
|
` oldest pending age (seconds): ${
|
|
259
455
|
h.oldest_pending_age_seconds == null ? "n/a" : h.oldest_pending_age_seconds
|
|
260
456
|
}`,
|
|
@@ -265,6 +461,15 @@ function formatCaptureStatusText(health) {
|
|
|
265
461
|
if (blocked.length) {
|
|
266
462
|
lines.push(` blocked reasons: ${blocked.join("; ")}`);
|
|
267
463
|
}
|
|
464
|
+
const dropReasons = Array.isArray(h.drop_reasons) ? h.drop_reasons : [];
|
|
465
|
+
if (dropReasons.length) {
|
|
466
|
+
lines.push(` drop reasons: ${dropReasons.join("; ")}`);
|
|
467
|
+
}
|
|
468
|
+
if (status === "degraded_retrying" || status === "blocked_action_required") {
|
|
469
|
+
lines.push(
|
|
470
|
+
" per-session detail and cleanup: meko-mcp --capture-cleanup status",
|
|
471
|
+
);
|
|
472
|
+
}
|
|
268
473
|
return lines.join("\n");
|
|
269
474
|
}
|
|
270
475
|
|
|
@@ -300,18 +505,23 @@ function captureStatusJson(health) {
|
|
|
300
505
|
),
|
|
301
506
|
},
|
|
302
507
|
queued_exchanges: Number(h.queued_exchanges || 0),
|
|
508
|
+
dropped_exchanges: Number(h.dropped_exchanges || 0),
|
|
303
509
|
oldest_pending_age_seconds:
|
|
304
510
|
h.oldest_pending_age_seconds == null ? null : h.oldest_pending_age_seconds,
|
|
305
511
|
last_success_at: h.last_success_at || null,
|
|
306
512
|
blocked_reasons: Array.isArray(h.blocked_reasons) ? h.blocked_reasons : [],
|
|
513
|
+
drop_reasons: Array.isArray(h.drop_reasons) ? h.drop_reasons : [],
|
|
307
514
|
sessions: Array.isArray(h.sessions) ? h.sessions : [],
|
|
308
515
|
};
|
|
309
516
|
}
|
|
310
517
|
|
|
311
518
|
module.exports = {
|
|
519
|
+
PARKED_REASON_PREFIX,
|
|
312
520
|
affectedSessionCount,
|
|
313
521
|
reasonForHealth,
|
|
314
522
|
statuslineSuffixFromHealth,
|
|
523
|
+
statuslineText,
|
|
524
|
+
statuslineFields,
|
|
315
525
|
buildHealthNotice,
|
|
316
526
|
injectHealthNotice,
|
|
317
527
|
formatCaptureStatusText,
|