@yugabytedb/meko-mcp 1.1.0 → 2.0.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 +81 -23
- package/assets/hooks-handlers/before-submit-prompt.sh +44 -0
- package/assets/hooks-handlers/cursor-session-start.sh +42 -0
- package/assets/hooks-handlers/lib/capture.js +404 -88
- package/assets/hooks-handlers/lib/checkpoint-timer.js +23 -11
- package/assets/hooks-handlers/session-start.sh +6 -2
- package/assets/scripts/meko-statusline.sh +119 -0
- package/assets/scripts/package-desktop-skill.sh +39 -0
- package/assets/skills/meko-mcp-tools/SKILL.md +42 -50
- package/assets/skills/meko-mcp-tools/references/_sections.md +1 -1
- package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +75 -150
- package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +30 -36
- package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +6 -22
- package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +9 -8
- package/assets/skills/meko-mcp-tools/references/tools-overview.md +35 -42
- package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +28 -92
- package/assets/skills/meko-mcp-tools/references/tools-scope-permissions.md +5 -9
- package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +1 -81
- package/assets/skills/meko-mcp-tools-desktop/SKILL.md +244 -0
- package/assets/skills/meko-mcp-tools-desktop/desktop/README.md +180 -0
- package/assets/skills/meko-mcp-tools-desktop/references/_sections.md +24 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-agent-id-conventions.md +99 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-cookbook.md +327 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-known-limitations.md +63 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +214 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +105 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +70 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-scope-permissions.md +71 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-troubleshooting.md +75 -0
- package/assets/skills/meko-select-datapack-desktop/SKILL.md +198 -0
- package/assets/skills/meko-select-datapack-desktop/references/cookbook.md +121 -0
- package/assets/skills/meko-select-datapack-desktop/references/troubleshooting.md +66 -0
- package/bin/create-meko-setup.mjs +33 -4
- package/lib/clients/claude-code.mjs +133 -6
- package/lib/clients/claude-desktop.mjs +48 -20
- package/lib/clients/codex.mjs +538 -5
- package/lib/clients/common.mjs +122 -1
- package/lib/clients/cursor.mjs +120 -27
- package/lib/config.mjs +52 -0
- package/lib/installer.mjs +26 -2
- package/lib/mcp-transport.mjs +5 -0
- package/lib/validate.mjs +0 -1
- package/package.json +1 -1
- package/scripts/bundle-assets.mjs +16 -0
- package/assets/skills/meko-mcp-tools/references/tools-db-query-patterns.md +0 -38
- package/assets/skills/meko-mcp-tools/references/tools-write-guardrails.md +0 -30
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
const fs = require("fs");
|
|
24
|
+
const os = require("os");
|
|
24
25
|
const path = require("path");
|
|
25
26
|
const http = require("http");
|
|
26
27
|
const https = require("https");
|
|
@@ -71,20 +72,46 @@ function deriveAgentId(opts) {
|
|
|
71
72
|
* Resolution order:
|
|
72
73
|
* 1. MEKO_AGENT_ID env override (verbatim).
|
|
73
74
|
* 2. cwd from the transcript header (peekTranscriptMetadata).
|
|
74
|
-
* 3.
|
|
75
|
+
* 3. explicit workspace/project roots from the hook input.
|
|
76
|
+
* 4. cwd from the hook input.
|
|
77
|
+
* 5. process.cwd() — last resort if no transcript metadata yet.
|
|
75
78
|
*/
|
|
76
|
-
function resolveSessionAgentId(transcriptPath) {
|
|
79
|
+
function resolveSessionAgentId(transcriptPath, hookInput = null) {
|
|
77
80
|
const envOverride = process.env.MEKO_AGENT_ID;
|
|
78
81
|
if (envOverride && envOverride.trim()) return envOverride.trim();
|
|
79
82
|
const meta = transcriptPath ? peekTranscriptMetadata(transcriptPath) : null;
|
|
80
|
-
const
|
|
81
|
-
|
|
83
|
+
const nested = hookInput && hookInput.hookSpecificInput && typeof hookInput.hookSpecificInput === "object"
|
|
84
|
+
? hookInput.hookSpecificInput
|
|
85
|
+
: {};
|
|
86
|
+
const roots = hookInput && Array.isArray(hookInput.workspace_roots)
|
|
87
|
+
? hookInput.workspace_roots
|
|
88
|
+
: [];
|
|
89
|
+
const nestedRoots = Array.isArray(nested.workspace_roots) ? nested.workspace_roots : [];
|
|
90
|
+
const workspaceRoot = [...roots, ...nestedRoots]
|
|
91
|
+
.find((item) => typeof item === "string" && item.trim());
|
|
92
|
+
const cwd =
|
|
93
|
+
(meta && meta.cwd) ||
|
|
94
|
+
hookInput?.workspace_path ||
|
|
95
|
+
hookInput?.workspacePath ||
|
|
96
|
+
hookInput?.project_root ||
|
|
97
|
+
nested.workspace_path ||
|
|
98
|
+
nested.workspacePath ||
|
|
99
|
+
nested.project_root ||
|
|
100
|
+
workspaceRoot ||
|
|
101
|
+
hookInput?.cwd ||
|
|
102
|
+
nested.cwd ||
|
|
103
|
+
process.cwd();
|
|
104
|
+
const client = process.env.MEKO_HOOK_CLIENT === "cursor" ? "cursor" : "claude_code";
|
|
105
|
+
return deriveAgentId({ client, cwd, envOverride });
|
|
82
106
|
}
|
|
83
107
|
const MEKO_API_KEY = process.env.MEKO_API_KEY || "";
|
|
84
108
|
const MEKO_API_TIMEOUT = parseInt(process.env.MEKO_API_TIMEOUT || "10", 10) * 1000;
|
|
85
109
|
const WATERMARK_DIR =
|
|
86
110
|
process.env.MEKO_WATERMARK_DIR ||
|
|
87
|
-
path.join(
|
|
111
|
+
path.join(os.homedir(), ".claude", "meko-capture");
|
|
112
|
+
const SESSION_CACHE_DIR =
|
|
113
|
+
process.env.MEKO_SESSION_CACHE_DIR ||
|
|
114
|
+
path.join(os.homedir(), ".cursor", "meko-session-cache");
|
|
88
115
|
|
|
89
116
|
let mcpRequestId = 0;
|
|
90
117
|
let mcpSessionId = null; // MCP Streamable HTTP session ID
|
|
@@ -184,10 +211,18 @@ function extractToolResultContent(msg) {
|
|
|
184
211
|
}
|
|
185
212
|
// Fallback to toolUseResult summary if no content blocks found
|
|
186
213
|
if (parts.length === 0 && msg.toolUseResult != null) {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
214
|
+
let summary;
|
|
215
|
+
if (typeof msg.toolUseResult === "string") {
|
|
216
|
+
summary = msg.toolUseResult;
|
|
217
|
+
} else {
|
|
218
|
+
// toolUseResult is arbitrary tool output; JSON.stringify can throw on
|
|
219
|
+
// circular refs or BigInt. A capture summary must never crash the hook.
|
|
220
|
+
try {
|
|
221
|
+
summary = JSON.stringify(msg.toolUseResult);
|
|
222
|
+
} catch {
|
|
223
|
+
summary = "(unserializable tool result)";
|
|
224
|
+
}
|
|
225
|
+
}
|
|
191
226
|
parts.push(truncate(summary, MAX_TOOL_RESULT_LEN));
|
|
192
227
|
}
|
|
193
228
|
return parts.length > 0 ? "TOOL RESULT:\n" + parts.join("\n") : "";
|
|
@@ -296,20 +331,87 @@ function readWatermark(wmPath) {
|
|
|
296
331
|
}
|
|
297
332
|
}
|
|
298
333
|
|
|
299
|
-
function writeWatermark(wmPath, convId, lineNum, agentId) {
|
|
334
|
+
function writeWatermark(wmPath, convId, lineNum, agentId, datapackPin = null) {
|
|
300
335
|
fs.mkdirSync(path.dirname(wmPath), { recursive: true });
|
|
301
336
|
const existing = readWatermark(wmPath);
|
|
337
|
+
const existingPin =
|
|
338
|
+
existing.datapack_id && existing.datapack_name
|
|
339
|
+
? {
|
|
340
|
+
datapack_id: existing.datapack_id,
|
|
341
|
+
datapack_name: existing.datapack_name,
|
|
342
|
+
}
|
|
343
|
+
: null;
|
|
344
|
+
const pin = datapackPin || existingPin;
|
|
302
345
|
fs.writeFileSync(
|
|
303
346
|
wmPath,
|
|
304
347
|
JSON.stringify({
|
|
305
348
|
conversation_id: convId,
|
|
306
349
|
last_line_number: lineNum,
|
|
307
350
|
agent_id: agentId != null ? agentId : existing.agent_id || "",
|
|
351
|
+
datapack_id: pin ? pin.datapack_id : null,
|
|
352
|
+
datapack_name: pin ? pin.datapack_name : null,
|
|
308
353
|
updated_at: new Date().toISOString(),
|
|
309
354
|
})
|
|
310
355
|
);
|
|
311
356
|
}
|
|
312
357
|
|
|
358
|
+
/**
|
|
359
|
+
* Convert an `agent_id` (e.g. `claude_code:meko-mcp-server`) into a
|
|
360
|
+
* filesystem-safe slug (`claude_code_meko-mcp-server`). The skill computes
|
|
361
|
+
* the same slug from the SessionStart-injected `agent_id`, so writer and
|
|
362
|
+
* reader agree on the path without sharing any session UUID.
|
|
363
|
+
*/
|
|
364
|
+
function datapackPinSlug(agentId) {
|
|
365
|
+
const trimmed = (typeof agentId === "string" ? agentId : "").trim();
|
|
366
|
+
if (!trimmed) return COMMON_BUCKET_AGENT_ID;
|
|
367
|
+
return trimmed.replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^_+|_+$/g, "") || COMMON_BUCKET_AGENT_ID;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Read the active-datapack pin written by the meko-select-datapack skill.
|
|
372
|
+
*
|
|
373
|
+
* The pin is a sidecar file at `<WATERMARK_DIR>/pin-<slug(agent_id)>.json`
|
|
374
|
+
* with shape `{ datapack_id, datapack_name, selected_at }`. Project-scoped:
|
|
375
|
+
* keyed by the same `agent_id` the hook derives for memory writes
|
|
376
|
+
* (`claude_code:<repo-basename>`), so a pin set in one Claude Code window
|
|
377
|
+
* for a repo applies to every other window in that repo, and survives
|
|
378
|
+
* `/clear`, `/compact`, and Claude Code restart.
|
|
379
|
+
*
|
|
380
|
+
* Hooks are read-only — only the skill writes this file. Returns null if
|
|
381
|
+
* the file is absent, malformed, or missing required fields, so SessionStart
|
|
382
|
+
* can proceed unchanged when no pin is set.
|
|
383
|
+
*/
|
|
384
|
+
function readDatapackPin(agentId) {
|
|
385
|
+
const slug = datapackPinSlug(agentId);
|
|
386
|
+
if (!slug) return null;
|
|
387
|
+
const pinPath = path.join(WATERMARK_DIR, `pin-${slug}.json`);
|
|
388
|
+
try {
|
|
389
|
+
const obj = JSON.parse(fs.readFileSync(pinPath, "utf-8"));
|
|
390
|
+
const id = typeof obj.datapack_id === "string" ? obj.datapack_id.trim() : "";
|
|
391
|
+
const name = typeof obj.datapack_name === "string" ? obj.datapack_name.trim() : "";
|
|
392
|
+
if (!id || !name) return null;
|
|
393
|
+
return { datapack_id: id, datapack_name: name, selected_at: obj.selected_at || "" };
|
|
394
|
+
} catch {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function datapackPinFromWatermark(watermark) {
|
|
400
|
+
if (!watermark) return null;
|
|
401
|
+
const id = typeof watermark.datapack_id === "string" ? watermark.datapack_id.trim() : "";
|
|
402
|
+
const name = typeof watermark.datapack_name === "string" ? watermark.datapack_name.trim() : "";
|
|
403
|
+
return id && name ? { datapack_id: id, datapack_name: name } : null;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function buildActiveDatapackBlock(pin) {
|
|
407
|
+
if (!pin) return "";
|
|
408
|
+
return `
|
|
409
|
+
|
|
410
|
+
### Active datapack
|
|
411
|
+
|
|
412
|
+
The user pinned datapack **${pin.datapack_name}** (\`${pin.datapack_id}\`) for this project via the \`meko-select-datapack\` skill. Pass \`datapack_id="${pin.datapack_id}"\` to every Meko MCP tool call (memory_*, db_*, knowledgebase_*, conversation_*) unless the user explicitly overrides for a single call. The pin is project-scoped (keyed by \`agent_id\`) and survives \`/clear\`, \`/compact\`, and Claude Code restart. Automatic capture keeps the datapack selected when this conversation was created; switching or clearing takes effect for automatic capture on the next new session.`;
|
|
413
|
+
}
|
|
414
|
+
|
|
313
415
|
// note: Subagents cannot auto-discover the parent's watermark today.
|
|
314
416
|
// The watermark dir is flat and keyed by session UUID, and a subagent
|
|
315
417
|
// process has no way to learn its parent's session UUID:
|
|
@@ -339,6 +441,13 @@ function mcpPost(jsonBody) {
|
|
|
339
441
|
"Content-Type": "application/json",
|
|
340
442
|
Accept: "application/json, text/event-stream",
|
|
341
443
|
"Content-Length": Buffer.byteLength(body),
|
|
444
|
+
// Cloud Meko sits behind a WAF that 403s requests with no User-Agent
|
|
445
|
+
// (Node's http/https send none by default). Without this the SessionStart
|
|
446
|
+
// / PreCompact / SessionEnd hooks all fail against mcp.mekodata.ai and
|
|
447
|
+
// automatic capture silently never runs — the hook falls back to the
|
|
448
|
+
// agent-driven path every session. Send an explicit UA so the WAF admits
|
|
449
|
+
// the request. (Verified: prod 403s without UA, 200s with one.)
|
|
450
|
+
"User-Agent": "meko-capture/1.0 (+https://github.com/yugabyte/meko-mcp-server)",
|
|
342
451
|
};
|
|
343
452
|
if (MEKO_API_KEY) {
|
|
344
453
|
headers["Authorization"] = `Bearer ${MEKO_API_KEY}`;
|
|
@@ -367,7 +476,7 @@ function mcpPost(jsonBody) {
|
|
|
367
476
|
return;
|
|
368
477
|
}
|
|
369
478
|
try {
|
|
370
|
-
const parsed =
|
|
479
|
+
const parsed = parseMcpResponseBody(data);
|
|
371
480
|
if (parsed.error) {
|
|
372
481
|
reject(new Error(JSON.stringify(parsed.error)));
|
|
373
482
|
} else {
|
|
@@ -395,6 +504,37 @@ function mcpPost(jsonBody) {
|
|
|
395
504
|
});
|
|
396
505
|
}
|
|
397
506
|
|
|
507
|
+
function parseMcpResponseBody(data) {
|
|
508
|
+
const trimmed = data.trim();
|
|
509
|
+
if (!trimmed) return null;
|
|
510
|
+
|
|
511
|
+
if (!trimmed.startsWith("event:") && !trimmed.startsWith("data:")) {
|
|
512
|
+
return JSON.parse(trimmed);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const payloads = [];
|
|
516
|
+
let current = [];
|
|
517
|
+
for (const line of data.split(/\r?\n/)) {
|
|
518
|
+
if (line === "") {
|
|
519
|
+
if (current.length) {
|
|
520
|
+
payloads.push(current.join("\n"));
|
|
521
|
+
current = [];
|
|
522
|
+
}
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
if (line.startsWith("data:")) {
|
|
526
|
+
current.push(line.startsWith("data: ") ? line.slice(6) : line.slice(5));
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (current.length) payloads.push(current.join("\n"));
|
|
530
|
+
|
|
531
|
+
const payload = payloads.find((item) => item.trim()) || "";
|
|
532
|
+
if (!payload) {
|
|
533
|
+
throw new Error("SSE response did not include a data payload");
|
|
534
|
+
}
|
|
535
|
+
return JSON.parse(payload);
|
|
536
|
+
}
|
|
537
|
+
|
|
398
538
|
/**
|
|
399
539
|
* Abort all pending MCP requests. Used by the preload timeout path so the
|
|
400
540
|
* SessionStart hook can return promptly instead of waiting for a slow server
|
|
@@ -512,13 +652,15 @@ function peekTranscriptMetadata(transcriptPath) {
|
|
|
512
652
|
* On any error the caller falls back to an empty list — memory preload is a
|
|
513
653
|
* best-effort enrichment, not a correctness requirement.
|
|
514
654
|
*/
|
|
515
|
-
async function fetchRecentMemories(agentId, { limit = 10, budget = 2000 } = {}) {
|
|
516
|
-
const
|
|
655
|
+
async function fetchRecentMemories(agentId, datapackId = null, { limit = 10, budget = 2000 } = {}) {
|
|
656
|
+
const payload = {
|
|
517
657
|
scope: "read",
|
|
518
658
|
agent_id: agentId,
|
|
519
659
|
conversation_id: "00000000-0000-0000-0000-000000000000",
|
|
520
660
|
limit,
|
|
521
|
-
}
|
|
661
|
+
};
|
|
662
|
+
if (datapackId) payload.datapack_id = datapackId;
|
|
663
|
+
const response = await mcpCall("memory_get_all", payload);
|
|
522
664
|
const result = extractToolResult(response);
|
|
523
665
|
if (!result) return [];
|
|
524
666
|
|
|
@@ -536,7 +678,7 @@ async function fetchRecentMemories(agentId, { limit = 10, budget = 2000 } = {})
|
|
|
536
678
|
return summaries;
|
|
537
679
|
}
|
|
538
680
|
|
|
539
|
-
async function createConversation(sessionId, agentId, metadata = null) {
|
|
681
|
+
async function createConversation(sessionId, agentId, metadata = null, datapackId = null) {
|
|
540
682
|
const payload = {
|
|
541
683
|
scope: "write",
|
|
542
684
|
agent_id: agentId,
|
|
@@ -551,14 +693,15 @@ async function createConversation(sessionId, agentId, metadata = null) {
|
|
|
551
693
|
startedAt: metadata.startedAt ?? null,
|
|
552
694
|
});
|
|
553
695
|
}
|
|
696
|
+
if (datapackId) payload.datapack_id = datapackId;
|
|
554
697
|
const response = await mcpCall("conversation_create", payload);
|
|
555
698
|
const result = extractToolResult(response);
|
|
556
699
|
return result && result.id ? result.id : null;
|
|
557
700
|
}
|
|
558
701
|
|
|
559
|
-
async function addMessage(convId, agentId, exchange) {
|
|
702
|
+
async function addMessage(convId, agentId, exchange, datapackId = null) {
|
|
560
703
|
const seed = `${convId}:${exchange.user_uuid}`;
|
|
561
|
-
|
|
704
|
+
const payload = {
|
|
562
705
|
scope: "write",
|
|
563
706
|
conversation_id: convId,
|
|
564
707
|
agent_id: agentId,
|
|
@@ -566,26 +709,114 @@ async function addMessage(convId, agentId, exchange) {
|
|
|
566
709
|
output: exchange.output,
|
|
567
710
|
reasoning: exchange.reasoning,
|
|
568
711
|
seed: seed,
|
|
569
|
-
}
|
|
712
|
+
};
|
|
713
|
+
if (datapackId) payload.datapack_id = datapackId;
|
|
714
|
+
const response = await mcpCall("conversation_add_message", payload);
|
|
715
|
+
// FAIL CLOSED. The MCP transport resolves successfully even when the tool
|
|
716
|
+
// failed, and failures take several shapes: an `isError` result envelope, a
|
|
717
|
+
// JSON body with an `error` field, or a non-JSON / missing-content / otherwise
|
|
718
|
+
// malformed body (which extractToolResult reports as null). If we treat any of
|
|
719
|
+
// these as success, the caller counts the turn as captured and advances the
|
|
720
|
+
// watermark — permanently dropping it. So only a *valid success body* counts;
|
|
721
|
+
// anything else throws into the caller's failed++/watermark-hold/retry path
|
|
722
|
+
// (dedup-by-seed makes the retry safe).
|
|
723
|
+
if (response && response.result && response.result.isError) {
|
|
724
|
+
throw new Error(
|
|
725
|
+
`conversation_add_message returned isError: ${JSON.stringify(response.result)}`,
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
const result = extractToolResult(response);
|
|
729
|
+
if (result === null) {
|
|
730
|
+
throw new Error(
|
|
731
|
+
"conversation_add_message: no parseable result body (non-JSON, empty, or malformed response)",
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
if (result.error) {
|
|
735
|
+
throw new Error(`conversation_add_message failed: ${JSON.stringify(result.error)}`);
|
|
736
|
+
}
|
|
737
|
+
return result;
|
|
570
738
|
}
|
|
571
739
|
|
|
572
740
|
// --- Hook output ---
|
|
573
741
|
|
|
574
|
-
function hookEventName(hookType) {
|
|
575
|
-
switch (hookType) {
|
|
576
|
-
case "session-start": return "SessionStart";
|
|
577
|
-
case "pre-compact": return "PreCompact";
|
|
578
|
-
default: return "SessionEnd";
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
|
|
582
742
|
function hookOutput(additionalContext) {
|
|
583
|
-
|
|
743
|
+
// Codex PreCompact accepts only the common hook output fields.
|
|
744
|
+
// `additionalContext` is valid hook-specific output for SessionStart, but
|
|
745
|
+
// emitting it for lifecycle capture hooks makes Codex reject the JSON.
|
|
746
|
+
if (process.argv[2] !== "session-start") return "{}";
|
|
747
|
+
|
|
748
|
+
// Emit the native coding-agent harness field while preserving the nested
|
|
749
|
+
// Claude-compatible shape for clients that still consume it.
|
|
750
|
+
const output = {
|
|
584
751
|
hookSpecificOutput: {
|
|
585
|
-
hookEventName:
|
|
752
|
+
hookEventName: "SessionStart",
|
|
586
753
|
additionalContext: additionalContext || "",
|
|
587
754
|
},
|
|
588
|
-
}
|
|
755
|
+
};
|
|
756
|
+
output.additional_context = additionalContext || "";
|
|
757
|
+
return JSON.stringify(output);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function nativeOutput(additionalContext) {
|
|
761
|
+
return JSON.stringify({ additional_context: additionalContext || "" });
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function beforeSubmitOutput(permission = "allow", userMessage = "") {
|
|
765
|
+
const output = { permission };
|
|
766
|
+
if (userMessage) output.userMessage = userMessage;
|
|
767
|
+
return JSON.stringify(output);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function extractSessionId(hookInput, transcriptPath) {
|
|
771
|
+
if (transcriptPath) return path.basename(transcriptPath, ".jsonl");
|
|
772
|
+
const nested = hookInput && hookInput.hookSpecificInput && typeof hookInput.hookSpecificInput === "object"
|
|
773
|
+
? hookInput.hookSpecificInput
|
|
774
|
+
: {};
|
|
775
|
+
for (const value of [
|
|
776
|
+
hookInput && hookInput.session_id,
|
|
777
|
+
hookInput && hookInput.sessionId,
|
|
778
|
+
nested.session_id,
|
|
779
|
+
nested.sessionId,
|
|
780
|
+
]) {
|
|
781
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
782
|
+
}
|
|
783
|
+
return null;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function sessionCachePath(sessionId) {
|
|
787
|
+
if (!sessionId) return null;
|
|
788
|
+
const safe = sessionId.replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
789
|
+
return path.join(SESSION_CACHE_DIR, `${safe}.json`);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function writeSessionCache(sessionId, payload) {
|
|
793
|
+
const filePath = sessionCachePath(sessionId);
|
|
794
|
+
if (!filePath) return;
|
|
795
|
+
const workspaceKey = payload.workspace_key || "";
|
|
796
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
797
|
+
const record = {
|
|
798
|
+
session_id: sessionId,
|
|
799
|
+
updated_at: new Date().toISOString(),
|
|
800
|
+
...payload,
|
|
801
|
+
};
|
|
802
|
+
fs.writeFileSync(filePath, JSON.stringify(record));
|
|
803
|
+
if (workspaceKey) {
|
|
804
|
+
const safe = workspaceKey.replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
805
|
+
fs.writeFileSync(path.join(SESSION_CACHE_DIR, `workspace-${safe}.json`), JSON.stringify(record));
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function workspaceKeyFromHookInput(hookInput, transcriptPath) {
|
|
810
|
+
const roots = hookInput && Array.isArray(hookInput.workspace_roots)
|
|
811
|
+
? hookInput.workspace_roots
|
|
812
|
+
: [];
|
|
813
|
+
const firstRoot = roots.find((item) => typeof item === "string" && item.trim());
|
|
814
|
+
if (firstRoot) return firstRoot.trim();
|
|
815
|
+
if (transcriptPath) return path.dirname(transcriptPath);
|
|
816
|
+
const cwd = hookInput && typeof hookInput.cwd === "string" && hookInput.cwd.trim()
|
|
817
|
+
? hookInput.cwd.trim()
|
|
818
|
+
: "";
|
|
819
|
+
return cwd || process.cwd();
|
|
589
820
|
}
|
|
590
821
|
|
|
591
822
|
// --- Main ---
|
|
@@ -610,6 +841,8 @@ function findMostRecentProjectWatermark(transcriptPath, currentSessionId) {
|
|
|
610
841
|
sessionId: sid,
|
|
611
842
|
conversation_id: wm.conversation_id,
|
|
612
843
|
agent_id: wm.agent_id || "",
|
|
844
|
+
datapack_id: wm.datapack_id || null,
|
|
845
|
+
datapack_name: wm.datapack_name || null,
|
|
613
846
|
updated_at: ts,
|
|
614
847
|
};
|
|
615
848
|
}
|
|
@@ -624,6 +857,7 @@ function findMostRecentProjectWatermark(transcriptPath, currentSessionId) {
|
|
|
624
857
|
function buildSessionStartContext(convId, sessionId, agentId, memories, opts) {
|
|
625
858
|
const resolvedAgentId = agentId || COMMON_BUCKET_AGENT_ID;
|
|
626
859
|
const resumed = Boolean(opts && opts.resumed);
|
|
860
|
+
const pin = opts && opts.datapackPin ? opts.datapackPin : null;
|
|
627
861
|
const opening = resumed
|
|
628
862
|
? `Resuming Meko conversation **${convId}** from a prior Claude Code session. The watermark file is written — PreCompact and SessionEnd hooks will append this session's new exchanges to the same conversation.`
|
|
629
863
|
: `Meko conversation **${convId}** was created automatically by the SessionStart hook. The watermark file is written — PreCompact and SessionEnd hooks will capture this session's transcript verbatim.`;
|
|
@@ -637,6 +871,7 @@ These facts were preloaded for you — inspect them before acting so you don't r
|
|
|
637
871
|
|
|
638
872
|
${memories.join("\n")}`
|
|
639
873
|
: "";
|
|
874
|
+
const datapackBlock = buildActiveDatapackBlock(pin);
|
|
640
875
|
|
|
641
876
|
return `## Meko Session Active
|
|
642
877
|
|
|
@@ -648,44 +883,48 @@ ${opening}
|
|
|
648
883
|
- **conversation_id**: Use "${convId}" for all MCP tool calls that accept it
|
|
649
884
|
- **scope**: Only "read", "write", or "admin"
|
|
650
885
|
|
|
651
|
-
###
|
|
886
|
+
### Memory capture is automatic — do NOT proactively call memory_add
|
|
652
887
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
-
|
|
888
|
+
This session's turns are captured to Meko by the hooks above, and the server extracts durable memories from them on its own. **Any fact the user states in conversation — their name, role, preferences, org conventions, corrections — is captured without you doing anything.** Do not call memory_add for facts the user says out loud; it duplicates what extraction already stores.
|
|
889
|
+
|
|
890
|
+
Call memory_add (with the session's agent_id + conversation_id) ONLY for the narrow cases extraction cannot reach:
|
|
891
|
+
- The user explicitly says "remember this" / "save this to memory".
|
|
892
|
+
- A durable fact lives only in YOUR output or a tool result, not in the user's words — extraction reads the user turn only, so it never sees these.
|
|
893
|
+
- The user negates or corrects a prior fact and the stale one must be overwritten — extraction is additive, so also fix the old memory (memory_search to find it, then memory_update / memory_delete_by_id).
|
|
657
894
|
|
|
658
|
-
###
|
|
895
|
+
### Recall and before destructive actions
|
|
659
896
|
|
|
660
|
-
|
|
897
|
+
- User asks "what do you know about X?" → call memory_search first.
|
|
898
|
+
- Before any action that deletes, overwrites, or restructures user content (Write-overwrite, large Edit deletions, rm, git reset --hard, destructive SQL), call memory_search for relevant feedback and confirm with the user if any prior guidance applies.
|
|
661
899
|
|
|
662
900
|
### Use Meko, not local storage
|
|
663
901
|
|
|
664
|
-
|
|
902
|
+
Never write memories to local files (e.g., ~/.claude/projects/.../memory/). Meko — via automatic capture plus the narrow memory_add cases above — is the only memory store.${memoryBlock}${datapackBlock}`;
|
|
665
903
|
}
|
|
666
904
|
|
|
667
|
-
function buildSessionStartFallbackContext(agentId) {
|
|
905
|
+
function buildSessionStartFallbackContext(agentId, opts) {
|
|
668
906
|
const resolvedAgentId = agentId || COMMON_BUCKET_AGENT_ID;
|
|
907
|
+
const datapackBlock = buildActiveDatapackBlock(opts && opts.datapackPin ? opts.datapackPin : null);
|
|
669
908
|
return `## Meko Memory Instructions
|
|
670
909
|
|
|
671
|
-
The SessionStart hook could not reach the Meko MCP server to create a conversation. Automatic capture
|
|
910
|
+
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.
|
|
672
911
|
|
|
673
|
-
### What you should
|
|
912
|
+
### What you should do while capture is down
|
|
674
913
|
|
|
675
|
-
- **Use Meko MCP tools** for memory_add, memory_search, etc. when available
|
|
676
914
|
- **agent_id**: use "${resolvedAgentId}" for project-scoped writes; use "${COMMON_BUCKET_AGENT_ID}" for cross-project common facts
|
|
677
915
|
- **conversation_id**: Use nil UUID "00000000-0000-0000-0000-000000000000" as placeholder
|
|
678
916
|
- **scope**: Only "read", "write", or "admin"
|
|
679
917
|
|
|
680
|
-
### Proactive storage (do NOT wait to be asked)
|
|
918
|
+
### Proactive storage (capture is down — do NOT wait to be asked)
|
|
681
919
|
|
|
920
|
+
Because nothing is capturing this session, explicit saves are the only path — this is the exception, not the normal mode:
|
|
682
921
|
- User shares personal info, preferences, or role → call memory_add immediately
|
|
683
922
|
- User shares org conventions or team standards → call memory_add immediately
|
|
684
923
|
- User asks "what do you know about X?" → call memory_search first
|
|
685
924
|
|
|
686
925
|
### Use Meko, not local storage
|
|
687
926
|
|
|
688
|
-
|
|
927
|
+
Prefer Meko MCP tools over local file-based memory. If a Meko tool call also fails, fall back to local storage and tell the user.${datapackBlock}`;
|
|
689
928
|
}
|
|
690
929
|
|
|
691
930
|
/**
|
|
@@ -727,17 +966,20 @@ function withTimeout(promise, ms, fallback, { onTimeout } = {}) {
|
|
|
727
966
|
});
|
|
728
967
|
}
|
|
729
968
|
|
|
969
|
+
function handleBeforeSubmitPrompt(hookInput) {
|
|
970
|
+
process.stdout.write(beforeSubmitOutput("allow"));
|
|
971
|
+
}
|
|
972
|
+
|
|
730
973
|
async function handleSessionStart(hookInput) {
|
|
731
974
|
const transcriptPath =
|
|
732
975
|
hookInput.transcript_path ||
|
|
733
976
|
(hookInput.hookSpecificInput || {}).transcript_path;
|
|
734
977
|
|
|
735
|
-
const sessionId = transcriptPath
|
|
736
|
-
? path.basename(transcriptPath, ".jsonl")
|
|
737
|
-
: null;
|
|
978
|
+
const sessionId = extractSessionId(hookInput, transcriptPath);
|
|
738
979
|
|
|
739
980
|
const source = hookInput.source || "";
|
|
740
|
-
const derivedAgentId = resolveSessionAgentId(transcriptPath);
|
|
981
|
+
const derivedAgentId = resolveSessionAgentId(transcriptPath, hookInput);
|
|
982
|
+
const workspaceKey = workspaceKeyFromHookInput(hookInput, transcriptPath);
|
|
741
983
|
|
|
742
984
|
// Check if watermark already exists (same session ID re-fires SessionStart, e.g. /clear, /compact)
|
|
743
985
|
if (sessionId) {
|
|
@@ -752,17 +994,23 @@ async function handleSessionStart(hookInput) {
|
|
|
752
994
|
// upgraded users stop writing into the ghost namespace.
|
|
753
995
|
const cached = (existing.agent_id || "").trim();
|
|
754
996
|
const resumedAgentId = cached && cached !== "agent" ? cached : derivedAgentId;
|
|
997
|
+
const existingDatapackPin = datapackPinFromWatermark(existing);
|
|
755
998
|
// Skip preload on resume (we don't know what's already in context).
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
null,
|
|
763
|
-
),
|
|
764
|
-
),
|
|
999
|
+
const context = buildSessionStartContext(
|
|
1000
|
+
existing.conversation_id,
|
|
1001
|
+
sessionId,
|
|
1002
|
+
resumedAgentId,
|
|
1003
|
+
null,
|
|
1004
|
+
{ datapackPin: existingDatapackPin },
|
|
765
1005
|
);
|
|
1006
|
+
writeSessionCache(sessionId, {
|
|
1007
|
+
status: "ready",
|
|
1008
|
+
conversation_id: existing.conversation_id,
|
|
1009
|
+
agent_id: resumedAgentId,
|
|
1010
|
+
workspace_key: workspaceKey,
|
|
1011
|
+
additional_context: context,
|
|
1012
|
+
});
|
|
1013
|
+
process.stdout.write(hookOutput(context));
|
|
766
1014
|
return;
|
|
767
1015
|
}
|
|
768
1016
|
}
|
|
@@ -786,19 +1034,30 @@ async function handleSessionStart(hookInput) {
|
|
|
786
1034
|
const priorAgentId = (prior.agent_id || "").trim();
|
|
787
1035
|
const resumedAgentId =
|
|
788
1036
|
priorAgentId && priorAgentId !== "agent" ? priorAgentId : agentId;
|
|
789
|
-
|
|
1037
|
+
const priorDatapackPin = datapackPinFromWatermark(prior);
|
|
1038
|
+
writeWatermark(
|
|
1039
|
+
wmPath,
|
|
1040
|
+
prior.conversation_id,
|
|
1041
|
+
currentLineCount,
|
|
1042
|
+
resumedAgentId,
|
|
1043
|
+
priorDatapackPin,
|
|
1044
|
+
);
|
|
790
1045
|
process.stderr.write(`[meko-capture] Session ${sessionId}: resuming conversation ${prior.conversation_id} from ${prior.sessionId}, skipping ${currentLineCount} copied lines\n`);
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
null,
|
|
798
|
-
{ resumed: true },
|
|
799
|
-
),
|
|
800
|
-
),
|
|
1046
|
+
const context = buildSessionStartContext(
|
|
1047
|
+
prior.conversation_id,
|
|
1048
|
+
sessionId,
|
|
1049
|
+
resumedAgentId,
|
|
1050
|
+
null,
|
|
1051
|
+
{ resumed: true, datapackPin: priorDatapackPin },
|
|
801
1052
|
);
|
|
1053
|
+
writeSessionCache(sessionId, {
|
|
1054
|
+
status: "ready",
|
|
1055
|
+
conversation_id: prior.conversation_id,
|
|
1056
|
+
agent_id: resumedAgentId,
|
|
1057
|
+
workspace_key: workspaceKey,
|
|
1058
|
+
additional_context: context,
|
|
1059
|
+
});
|
|
1060
|
+
process.stdout.write(hookOutput(context));
|
|
802
1061
|
return;
|
|
803
1062
|
}
|
|
804
1063
|
process.stderr.write(`[meko-capture] Session ${sessionId}: source=resume but no prior project watermark found; creating new conversation\n`);
|
|
@@ -807,31 +1066,54 @@ async function handleSessionStart(hookInput) {
|
|
|
807
1066
|
// Create conversation via MCP. Attach session metadata (cwd / gitBranch /
|
|
808
1067
|
// startedAt) from the transcript header so hook traces match the migrator.
|
|
809
1068
|
const transcriptMetadata = peekTranscriptMetadata(transcriptPath);
|
|
1069
|
+
const datapackPin = readDatapackPin(agentId);
|
|
810
1070
|
let convId = null;
|
|
811
1071
|
try {
|
|
812
1072
|
await mcpInitialize();
|
|
813
|
-
convId = await createConversation(
|
|
1073
|
+
convId = await createConversation(
|
|
1074
|
+
sessionId || "unknown",
|
|
1075
|
+
agentId,
|
|
1076
|
+
transcriptMetadata,
|
|
1077
|
+
datapackPin && datapackPin.datapack_id,
|
|
1078
|
+
);
|
|
814
1079
|
} catch (err) {
|
|
815
1080
|
process.stderr.write(`[meko-capture] SessionStart: MCP unavailable (${err.message}). Falling back to agent-driven setup.\n`);
|
|
816
|
-
|
|
1081
|
+
const context = buildSessionStartFallbackContext(agentId, { datapackPin: readDatapackPin(agentId) });
|
|
1082
|
+
if (sessionId) {
|
|
1083
|
+
writeSessionCache(sessionId, {
|
|
1084
|
+
status: "error",
|
|
1085
|
+
agent_id: agentId,
|
|
1086
|
+
workspace_key: workspaceKey,
|
|
1087
|
+
error: err && err.message ? String(err.message).slice(0, 240) : "MCP unavailable",
|
|
1088
|
+
additional_context: context,
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
process.stdout.write(hookOutput(context));
|
|
817
1092
|
return;
|
|
818
1093
|
}
|
|
819
1094
|
|
|
820
1095
|
if (!convId) {
|
|
821
1096
|
process.stderr.write("[meko-capture] SessionStart: conversation_create returned no ID.\n");
|
|
822
|
-
|
|
1097
|
+
const context = buildSessionStartFallbackContext(agentId, { datapackPin: readDatapackPin(agentId) });
|
|
1098
|
+
if (sessionId) {
|
|
1099
|
+
writeSessionCache(sessionId, {
|
|
1100
|
+
status: "error",
|
|
1101
|
+
agent_id: agentId,
|
|
1102
|
+
workspace_key: workspaceKey,
|
|
1103
|
+
error: "conversation_create returned no ID",
|
|
1104
|
+
additional_context: context,
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
process.stdout.write(hookOutput(context));
|
|
823
1108
|
return;
|
|
824
1109
|
}
|
|
825
1110
|
|
|
826
1111
|
// Preload recent memories with a 4s budget (SessionStart timeout is 15s).
|
|
827
1112
|
// Best-effort enrichment — on timeout/failure we still deliver the core
|
|
828
|
-
// context. agent_id is derived per-session from the cwd
|
|
829
|
-
// db_summarize_database discovery helper was removed (it depended on a
|
|
830
|
-
// paid-tier tool and only worked "correctly" via its fallback branch
|
|
831
|
-
// anyway — see scratch/2026-04-30-multi-tenant-mem0-audit.md Probe 6).
|
|
1113
|
+
// context. agent_id is derived per-session from the cwd.
|
|
832
1114
|
const PRELOAD_BUDGET_MS = 4000;
|
|
833
1115
|
const memories = await withTimeout(
|
|
834
|
-
fetchRecentMemories(agentId),
|
|
1116
|
+
fetchRecentMemories(agentId, datapackPin && datapackPin.datapack_id),
|
|
835
1117
|
PRELOAD_BUDGET_MS,
|
|
836
1118
|
[],
|
|
837
1119
|
{ onTimeout: abortInflightRequests },
|
|
@@ -839,25 +1121,33 @@ async function handleSessionStart(hookInput) {
|
|
|
839
1121
|
|
|
840
1122
|
if (sessionId) {
|
|
841
1123
|
const wmPath = path.join(WATERMARK_DIR, `${sessionId}.watermark.json`);
|
|
842
|
-
writeWatermark(wmPath, convId, 0, agentId);
|
|
1124
|
+
writeWatermark(wmPath, convId, 0, agentId, datapackPin);
|
|
843
1125
|
process.stderr.write(
|
|
844
1126
|
`[meko-capture] SessionStart: created conversation ${convId}, agent_id=${agentId}, preloaded ${memories.length} memories, watermark at ${wmPath}\n`,
|
|
845
1127
|
);
|
|
846
1128
|
}
|
|
847
1129
|
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
1130
|
+
const context = buildSessionStartContext(convId, sessionId, agentId, memories, {
|
|
1131
|
+
datapackPin,
|
|
1132
|
+
});
|
|
1133
|
+
if (sessionId) {
|
|
1134
|
+
writeSessionCache(sessionId, {
|
|
1135
|
+
status: "ready",
|
|
1136
|
+
conversation_id: convId,
|
|
1137
|
+
agent_id: agentId,
|
|
1138
|
+
workspace_key: workspaceKey,
|
|
1139
|
+
additional_context: context,
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
process.stdout.write(hookOutput(context));
|
|
853
1143
|
}
|
|
854
1144
|
|
|
855
1145
|
// --- Main ---
|
|
856
1146
|
|
|
857
1147
|
async function main() {
|
|
858
1148
|
const hookType = process.argv[2];
|
|
859
|
-
if (!hookType || !["session-start", "pre-compact", "session-end", "checkpoint"].includes(hookType)) {
|
|
860
|
-
process.stderr.write("Usage: capture.js <session-start|pre-compact|session-end|checkpoint>\n");
|
|
1149
|
+
if (!hookType || !["session-start", "before-submit-prompt", "pre-compact", "session-end", "checkpoint"].includes(hookType)) {
|
|
1150
|
+
process.stderr.write("Usage: capture.js <session-start|before-submit-prompt|pre-compact|session-end|checkpoint>\n");
|
|
861
1151
|
process.exit(1);
|
|
862
1152
|
}
|
|
863
1153
|
|
|
@@ -878,6 +1168,11 @@ async function main() {
|
|
|
878
1168
|
return;
|
|
879
1169
|
}
|
|
880
1170
|
|
|
1171
|
+
if (hookType === "before-submit-prompt") {
|
|
1172
|
+
handleBeforeSubmitPrompt(hookInput);
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
881
1176
|
// PreCompact / SessionEnd: capture transcript exchanges
|
|
882
1177
|
const transcriptPath =
|
|
883
1178
|
hookInput.transcript_path ||
|
|
@@ -903,6 +1198,7 @@ async function main() {
|
|
|
903
1198
|
const agentId = cachedAgentId && cachedAgentId !== "agent"
|
|
904
1199
|
? cachedAgentId
|
|
905
1200
|
: resolveSessionAgentId(transcriptPath);
|
|
1201
|
+
const datapackPin = datapackPinFromWatermark(watermark);
|
|
906
1202
|
|
|
907
1203
|
// Nothing new
|
|
908
1204
|
if (currentLines <= lastLine) {
|
|
@@ -933,20 +1229,40 @@ async function main() {
|
|
|
933
1229
|
// Extract and send exchanges
|
|
934
1230
|
const exchanges = extractExchanges(transcriptPath, lastLine);
|
|
935
1231
|
let captured = 0;
|
|
1232
|
+
let failed = 0;
|
|
936
1233
|
|
|
937
1234
|
for (const exchange of exchanges) {
|
|
938
1235
|
try {
|
|
939
|
-
await addMessage(
|
|
1236
|
+
await addMessage(
|
|
1237
|
+
convId,
|
|
1238
|
+
agentId,
|
|
1239
|
+
exchange,
|
|
1240
|
+
datapackPin && datapackPin.datapack_id,
|
|
1241
|
+
);
|
|
940
1242
|
captured++;
|
|
941
1243
|
} catch (err) {
|
|
1244
|
+
failed++;
|
|
942
1245
|
process.stderr.write(`[meko-capture] Failed to add message (uuid=${exchange.user_uuid}): ${err.message}\n`);
|
|
943
1246
|
}
|
|
944
1247
|
}
|
|
945
1248
|
|
|
946
|
-
//
|
|
947
|
-
//
|
|
948
|
-
//
|
|
949
|
-
|
|
1249
|
+
// Advance the watermark to currentLines ONLY when every exchange was
|
|
1250
|
+
// captured. If any addMessage failed (typically a transient network error),
|
|
1251
|
+
// keep the watermark at lastLine so the next checkpoint / PreCompact /
|
|
1252
|
+
// SessionEnd hook re-extracts and retries this batch. Re-sending the
|
|
1253
|
+
// exchanges that already succeeded is safe: capture dedups by seed
|
|
1254
|
+
// (<conv_id>:<user_uuid>), so retries never create duplicates. Advancing on
|
|
1255
|
+
// partial failure is what would permanently drop the failed exchanges.
|
|
1256
|
+
// We still always persist the resolved agent_id (migrating any legacy literal
|
|
1257
|
+
// value) so later hooks see the derived bucket directly.
|
|
1258
|
+
const watermarkLine = failed === 0 ? currentLines : lastLine;
|
|
1259
|
+
if (failed > 0) {
|
|
1260
|
+
process.stderr.write(
|
|
1261
|
+
`[meko-capture] ${failed}/${exchanges.length} exchange(s) failed; ` +
|
|
1262
|
+
`holding watermark at line ${lastLine} for retry (dedup makes resends safe).\n`
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
writeWatermark(wmPath, convId, watermarkLine, agentId, datapackPin);
|
|
950
1266
|
|
|
951
1267
|
// Output
|
|
952
1268
|
const context =
|