@yugabytedb/meko-mcp 1.0.8 → 1.2.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 +242 -44
- 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 +457 -107
- package/assets/hooks-handlers/lib/package.json +1 -0
- package/assets/hooks-handlers/test_single_conversation.sh +1 -1
- package/assets/scripts/package-desktop-skill.sh +36 -0
- package/assets/skills/meko-mcp-tools/SKILL.md +167 -53
- package/assets/skills/meko-mcp-tools/references/_sections.md +1 -1
- package/assets/skills/meko-mcp-tools/references/tools-agent-id-conventions.md +84 -22
- package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +89 -56
- package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +15 -7
- package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +17 -9
- package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +94 -5
- package/assets/skills/meko-mcp-tools/references/tools-overview.md +52 -37
- package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +108 -37
- package/assets/skills/meko-mcp-tools/references/tools-scope-permissions.md +6 -10
- package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +30 -18
- package/assets/skills/meko-mcp-tools/references/tools-user-id-conventions.md +55 -69
- package/assets/skills/meko-mcp-tools-desktop/SKILL.md +251 -0
- package/assets/skills/meko-mcp-tools-desktop/desktop/README.md +150 -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 +406 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-known-limitations.md +69 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +217 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +112 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +134 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-scope-permissions.md +75 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-troubleshooting.md +155 -0
- package/bin/create-meko-setup.mjs +151 -22
- package/lib/clients/claude-code.mjs +378 -68
- package/lib/clients/claude-desktop.mjs +45 -8
- package/lib/clients/codex.mjs +1209 -0
- package/lib/clients/common.mjs +223 -1
- package/lib/clients/cursor.mjs +118 -13
- package/lib/clients/index.mjs +2 -0
- package/lib/config.mjs +56 -0
- package/lib/detect.mjs +52 -44
- package/lib/installer.mjs +11 -4
- package/lib/mcp-transport.mjs +61 -2
- package/lib/migrate/adapters/memory-md.mjs +13 -6
- package/lib/migrate/datapack.mjs +167 -0
- package/lib/migrate/id.mjs +127 -9
- package/lib/migrate/orchestrator.mjs +68 -10
- package/lib/migrate.mjs +346 -3
- package/lib/prompt.mjs +22 -4
- package/lib/validate.mjs +313 -9
- package/lib/version-check.mjs +178 -0
- package/package.json +2 -1
- package/scripts/bundle-assets.mjs +8 -0
|
@@ -12,13 +12,16 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Environment:
|
|
14
14
|
* MEKO_MCP_URL MCP server URL (default: http://localhost:8000/mcp)
|
|
15
|
-
* MEKO_AGENT_ID Agent identifier
|
|
15
|
+
* MEKO_AGENT_ID Agent identifier override; if unset, derived from the
|
|
16
|
+
* session's cwd as `claude_code:<repo-basename>` (or
|
|
17
|
+
* `meko_agent` if cwd is unknown). See deriveAgentId().
|
|
16
18
|
* MEKO_API_KEY API key for Cloud Meko auth (optional, omit for local)
|
|
17
19
|
* MEKO_API_TIMEOUT Request timeout in seconds (default: 10)
|
|
18
20
|
* MEKO_WATERMARK_DIR Watermark directory (default: ~/.claude/meko-capture)
|
|
19
21
|
*/
|
|
20
22
|
|
|
21
23
|
const fs = require("fs");
|
|
24
|
+
const os = require("os");
|
|
22
25
|
const path = require("path");
|
|
23
26
|
const http = require("http");
|
|
24
27
|
const https = require("https");
|
|
@@ -26,12 +29,89 @@ const https = require("https");
|
|
|
26
29
|
// --- Config ---
|
|
27
30
|
|
|
28
31
|
const MEKO_MCP_URL = process.env.MEKO_MCP_URL || "http://localhost:8000/mcp";
|
|
29
|
-
|
|
32
|
+
|
|
33
|
+
// Server-side fallback for empty agent_id; the cross-project common bucket.
|
|
34
|
+
// Mirror of COMMON_BUCKET_AGENT_ID in installer/lib/migrate/id.mjs.
|
|
35
|
+
const COMMON_BUCKET_AGENT_ID = "meko_agent";
|
|
36
|
+
const AGENT_ID_MAX_LEN = 64;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Derive an `agent_id` for a Meko write. Mirror of `deriveAgentId` in
|
|
40
|
+
* installer/lib/migrate/id.mjs — kept inline because capture.js is published
|
|
41
|
+
* in a separate package with no module dependencies on the installer.
|
|
42
|
+
*
|
|
43
|
+
* - `envOverride` (typically MEKO_AGENT_ID) wins if non-empty.
|
|
44
|
+
* - Coding clients (claude_code, cursor) → `<client>:<repo-basename>`,
|
|
45
|
+
* with the basename lowercased and non-`[a-z0-9-]` runs collapsed to `-`.
|
|
46
|
+
* Outside a repo → bare client name (keeps coding-agent traffic out of
|
|
47
|
+
* the common bucket).
|
|
48
|
+
* - Loose clients (claude-desktop) → `claude_desktop`.
|
|
49
|
+
* - Anything else → `meko_agent` (common cross-project bucket).
|
|
50
|
+
*/
|
|
51
|
+
function deriveAgentId(opts) {
|
|
52
|
+
const override = (opts && opts.envOverride ? String(opts.envOverride) : "").trim();
|
|
53
|
+
if (override) return override;
|
|
54
|
+
const client = (opts && opts.client ? String(opts.client) : "").trim();
|
|
55
|
+
if (client === "claude-desktop" || client === "claude_desktop") return "claude_desktop";
|
|
56
|
+
if (client !== "claude_code" && client !== "cursor") return COMMON_BUCKET_AGENT_ID;
|
|
57
|
+
const rawBase = opts && opts.cwd ? path.basename(opts.cwd) : "";
|
|
58
|
+
if (!rawBase || rawBase === "." || rawBase === "/") return client;
|
|
59
|
+
const project = rawBase.toLowerCase()
|
|
60
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
61
|
+
.replace(/^-+|-+$/g, "")
|
|
62
|
+
.slice(0, AGENT_ID_MAX_LEN);
|
|
63
|
+
if (!project) return client;
|
|
64
|
+
return `${client}:${project}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Resolve the agent_id for the current session. Frozen at SessionStart;
|
|
69
|
+
* subsequent hook invocations re-derive from the watermark + env, not
|
|
70
|
+
* from a stale module-level cache.
|
|
71
|
+
*
|
|
72
|
+
* Resolution order:
|
|
73
|
+
* 1. MEKO_AGENT_ID env override (verbatim).
|
|
74
|
+
* 2. cwd from the transcript header (peekTranscriptMetadata).
|
|
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.
|
|
78
|
+
*/
|
|
79
|
+
function resolveSessionAgentId(transcriptPath, hookInput = null) {
|
|
80
|
+
const envOverride = process.env.MEKO_AGENT_ID;
|
|
81
|
+
if (envOverride && envOverride.trim()) return envOverride.trim();
|
|
82
|
+
const meta = transcriptPath ? peekTranscriptMetadata(transcriptPath) : null;
|
|
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 });
|
|
106
|
+
}
|
|
30
107
|
const MEKO_API_KEY = process.env.MEKO_API_KEY || "";
|
|
31
108
|
const MEKO_API_TIMEOUT = parseInt(process.env.MEKO_API_TIMEOUT || "10", 10) * 1000;
|
|
32
109
|
const WATERMARK_DIR =
|
|
33
110
|
process.env.MEKO_WATERMARK_DIR ||
|
|
34
|
-
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");
|
|
35
115
|
|
|
36
116
|
let mcpRequestId = 0;
|
|
37
117
|
let mcpSessionId = null; // MCP Streamable HTTP session ID
|
|
@@ -257,6 +337,20 @@ function writeWatermark(wmPath, convId, lineNum, agentId) {
|
|
|
257
337
|
);
|
|
258
338
|
}
|
|
259
339
|
|
|
340
|
+
// note: Subagents cannot auto-discover the parent's watermark today.
|
|
341
|
+
// The watermark dir is flat and keyed by session UUID, and a subagent
|
|
342
|
+
// process has no way to learn its parent's session UUID:
|
|
343
|
+
// - no CLAUDE_SESSION_ID / CLAUDE_PARENT_SESSION_ID env var
|
|
344
|
+
// - ~/.claude/session-env/<uuid>/ directories exist but are empty
|
|
345
|
+
// - cwd is shared with the parent but isn't a unique session key
|
|
346
|
+
// (parent may have multiple concurrent sessions in one project;
|
|
347
|
+
// findMostRecentProjectWatermark() keys off transcript_path,
|
|
348
|
+
// which Agent / Task spawn prompts do not expose)
|
|
349
|
+
// Verified on Claude Code 2.1.126 via issue #49 spike (2026-05-01).
|
|
350
|
+
// Enforcement is shifted to the skill: parents prepend an inherited-
|
|
351
|
+
// context block to every Task / Agent spawn prompt. See
|
|
352
|
+
// skills/skills/meko-mcp-tools/SKILL.md "When spawning subagents".
|
|
353
|
+
|
|
260
354
|
// --- MCP JSON-RPC client (Streamable HTTP with session init) ---
|
|
261
355
|
|
|
262
356
|
/**
|
|
@@ -300,7 +394,7 @@ function mcpPost(jsonBody) {
|
|
|
300
394
|
return;
|
|
301
395
|
}
|
|
302
396
|
try {
|
|
303
|
-
const parsed =
|
|
397
|
+
const parsed = parseMcpResponseBody(data);
|
|
304
398
|
if (parsed.error) {
|
|
305
399
|
reject(new Error(JSON.stringify(parsed.error)));
|
|
306
400
|
} else {
|
|
@@ -328,6 +422,37 @@ function mcpPost(jsonBody) {
|
|
|
328
422
|
});
|
|
329
423
|
}
|
|
330
424
|
|
|
425
|
+
function parseMcpResponseBody(data) {
|
|
426
|
+
const trimmed = data.trim();
|
|
427
|
+
if (!trimmed) return null;
|
|
428
|
+
|
|
429
|
+
if (!trimmed.startsWith("event:") && !trimmed.startsWith("data:")) {
|
|
430
|
+
return JSON.parse(trimmed);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const payloads = [];
|
|
434
|
+
let current = [];
|
|
435
|
+
for (const line of data.split(/\r?\n/)) {
|
|
436
|
+
if (line === "") {
|
|
437
|
+
if (current.length) {
|
|
438
|
+
payloads.push(current.join("\n"));
|
|
439
|
+
current = [];
|
|
440
|
+
}
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
if (line.startsWith("data:")) {
|
|
444
|
+
current.push(line.startsWith("data: ") ? line.slice(6) : line.slice(5));
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (current.length) payloads.push(current.join("\n"));
|
|
448
|
+
|
|
449
|
+
const payload = payloads.find((item) => item.trim()) || "";
|
|
450
|
+
if (!payload) {
|
|
451
|
+
throw new Error("SSE response did not include a data payload");
|
|
452
|
+
}
|
|
453
|
+
return JSON.parse(payload);
|
|
454
|
+
}
|
|
455
|
+
|
|
331
456
|
/**
|
|
332
457
|
* Abort all pending MCP requests. Used by the preload timeout path so the
|
|
333
458
|
* SessionStart hook can return promptly instead of waiting for a slow server
|
|
@@ -398,43 +523,43 @@ function extractToolResult(response) {
|
|
|
398
523
|
}
|
|
399
524
|
|
|
400
525
|
/**
|
|
401
|
-
*
|
|
402
|
-
*
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
* pick the one with the highest `row_count`. Falls back to "agent" when the
|
|
406
|
-
* user has no prior memories. Never returns null — always returns a usable
|
|
407
|
-
* string.
|
|
526
|
+
* Peek the first JSONL line of a transcript to harvest `cwd`, `gitBranch`,
|
|
527
|
+
* and `timestamp`. These are attached as `metadata` on the hook's
|
|
528
|
+
* `conversation_create` so hook-captured traces match the metadata shape
|
|
529
|
+
* the migrator emits (see installer/lib/migrate/orchestrator.mjs).
|
|
408
530
|
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
531
|
+
* Reads a bounded 4KB prefix, not the whole file — on PreCompact / SessionEnd
|
|
532
|
+
* the transcript can be several MB, and we only need the first line's JSON
|
|
533
|
+
* header. If the header happens to exceed 4KB (pathological; a Claude Code
|
|
534
|
+
* header is usually <1KB), JSON.parse will reject the truncated prefix and
|
|
535
|
+
* we fall back to empty metadata via the catch. Same graceful degradation
|
|
536
|
+
* as any other read/parse failure — metadata is best-effort enrichment.
|
|
411
537
|
*/
|
|
412
|
-
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
.
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
if (
|
|
434
|
-
|
|
538
|
+
function peekTranscriptMetadata(transcriptPath) {
|
|
539
|
+
const empty = { cwd: null, gitBranch: null, startedAt: null };
|
|
540
|
+
if (!transcriptPath) return empty;
|
|
541
|
+
let fd;
|
|
542
|
+
try {
|
|
543
|
+
fd = fs.openSync(transcriptPath, "r");
|
|
544
|
+
const buffer = Buffer.alloc(4096);
|
|
545
|
+
const bytesRead = fs.readSync(fd, buffer, 0, 4096, 0);
|
|
546
|
+
if (bytesRead === 0) return empty;
|
|
547
|
+
const content = buffer.toString("utf-8", 0, bytesRead);
|
|
548
|
+
const firstNewline = content.indexOf("\n");
|
|
549
|
+
const firstLine = firstNewline === -1 ? content : content.slice(0, firstNewline);
|
|
550
|
+
const obj = JSON.parse(firstLine);
|
|
551
|
+
return {
|
|
552
|
+
cwd: typeof obj.cwd === "string" ? obj.cwd : null,
|
|
553
|
+
gitBranch: typeof obj.gitBranch === "string" ? obj.gitBranch : null,
|
|
554
|
+
startedAt: typeof obj.timestamp === "string" ? obj.timestamp : null,
|
|
555
|
+
};
|
|
556
|
+
} catch {
|
|
557
|
+
return empty;
|
|
558
|
+
} finally {
|
|
559
|
+
if (fd !== undefined) {
|
|
560
|
+
try { fs.closeSync(fd); } catch { /* FD already gone; nothing to do */ }
|
|
435
561
|
}
|
|
436
562
|
}
|
|
437
|
-
return best.rows > 0 ? best.agent : "agent";
|
|
438
563
|
}
|
|
439
564
|
|
|
440
565
|
/**
|
|
@@ -469,23 +594,32 @@ async function fetchRecentMemories(agentId, { limit = 10, budget = 2000 } = {})
|
|
|
469
594
|
return summaries;
|
|
470
595
|
}
|
|
471
596
|
|
|
472
|
-
async function createConversation(sessionId) {
|
|
473
|
-
const
|
|
597
|
+
async function createConversation(sessionId, agentId, metadata = null) {
|
|
598
|
+
const payload = {
|
|
474
599
|
scope: "write",
|
|
475
|
-
agent_id:
|
|
600
|
+
agent_id: agentId,
|
|
476
601
|
title: "Claude Code session (auto-captured)",
|
|
477
602
|
session_id: sessionId,
|
|
478
|
-
}
|
|
603
|
+
};
|
|
604
|
+
if (metadata) {
|
|
605
|
+
payload.metadata = JSON.stringify({
|
|
606
|
+
source: "hook",
|
|
607
|
+
cwd: metadata.cwd ?? null,
|
|
608
|
+
gitBranch: metadata.gitBranch ?? null,
|
|
609
|
+
startedAt: metadata.startedAt ?? null,
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
const response = await mcpCall("conversation_create", payload);
|
|
479
613
|
const result = extractToolResult(response);
|
|
480
614
|
return result && result.id ? result.id : null;
|
|
481
615
|
}
|
|
482
616
|
|
|
483
|
-
async function addMessage(convId, exchange) {
|
|
617
|
+
async function addMessage(convId, agentId, exchange) {
|
|
484
618
|
const seed = `${convId}:${exchange.user_uuid}`;
|
|
485
619
|
await mcpCall("conversation_add_message", {
|
|
486
620
|
scope: "write",
|
|
487
621
|
conversation_id: convId,
|
|
488
|
-
agent_id:
|
|
622
|
+
agent_id: agentId,
|
|
489
623
|
input: exchange.input,
|
|
490
624
|
output: exchange.output,
|
|
491
625
|
reasoning: exchange.reasoning,
|
|
@@ -504,12 +638,112 @@ function hookEventName(hookType) {
|
|
|
504
638
|
}
|
|
505
639
|
|
|
506
640
|
function hookOutput(additionalContext) {
|
|
507
|
-
|
|
641
|
+
// Emit the native coding-agent harness field while preserving the nested
|
|
642
|
+
// Claude-compatible shape for clients that still consume it.
|
|
643
|
+
const output = {
|
|
508
644
|
hookSpecificOutput: {
|
|
509
645
|
hookEventName: hookEventName(process.argv[2]),
|
|
510
646
|
additionalContext: additionalContext || "",
|
|
511
647
|
},
|
|
512
|
-
}
|
|
648
|
+
};
|
|
649
|
+
if (process.argv[2] === "session-start") {
|
|
650
|
+
output.additional_context = additionalContext || "";
|
|
651
|
+
}
|
|
652
|
+
return JSON.stringify(output);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function nativeOutput(additionalContext) {
|
|
656
|
+
return JSON.stringify({ additional_context: additionalContext || "" });
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function beforeSubmitOutput(shouldContinue, userMessage = "") {
|
|
660
|
+
const output = { continue: Boolean(shouldContinue) };
|
|
661
|
+
if (userMessage) output.user_message = userMessage;
|
|
662
|
+
return JSON.stringify(output);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function extractSessionId(hookInput, transcriptPath) {
|
|
666
|
+
if (transcriptPath) return path.basename(transcriptPath, ".jsonl");
|
|
667
|
+
const nested = hookInput && hookInput.hookSpecificInput && typeof hookInput.hookSpecificInput === "object"
|
|
668
|
+
? hookInput.hookSpecificInput
|
|
669
|
+
: {};
|
|
670
|
+
for (const value of [
|
|
671
|
+
hookInput && hookInput.session_id,
|
|
672
|
+
hookInput && hookInput.sessionId,
|
|
673
|
+
nested.session_id,
|
|
674
|
+
nested.sessionId,
|
|
675
|
+
]) {
|
|
676
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
677
|
+
}
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function sessionCachePath(sessionId) {
|
|
682
|
+
if (!sessionId) return null;
|
|
683
|
+
const safe = sessionId.replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
684
|
+
return path.join(SESSION_CACHE_DIR, `${safe}.json`);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function writeSessionCache(sessionId, payload) {
|
|
688
|
+
const filePath = sessionCachePath(sessionId);
|
|
689
|
+
if (!filePath) return;
|
|
690
|
+
const workspaceKey = payload.workspace_key || "";
|
|
691
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
692
|
+
const record = {
|
|
693
|
+
session_id: sessionId,
|
|
694
|
+
updated_at: new Date().toISOString(),
|
|
695
|
+
...payload,
|
|
696
|
+
};
|
|
697
|
+
fs.writeFileSync(filePath, JSON.stringify(record));
|
|
698
|
+
if (workspaceKey) {
|
|
699
|
+
const safe = workspaceKey.replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
700
|
+
fs.writeFileSync(path.join(SESSION_CACHE_DIR, `workspace-${safe}.json`), JSON.stringify(record));
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function readSessionCache(sessionId) {
|
|
705
|
+
const filePath = sessionCachePath(sessionId);
|
|
706
|
+
if (!filePath) return null;
|
|
707
|
+
try {
|
|
708
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
709
|
+
} catch {
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function workspaceKeyFromHookInput(hookInput, transcriptPath) {
|
|
715
|
+
const roots = hookInput && Array.isArray(hookInput.workspace_roots)
|
|
716
|
+
? hookInput.workspace_roots
|
|
717
|
+
: [];
|
|
718
|
+
const firstRoot = roots.find((item) => typeof item === "string" && item.trim());
|
|
719
|
+
if (firstRoot) return firstRoot.trim();
|
|
720
|
+
if (transcriptPath) return path.dirname(transcriptPath);
|
|
721
|
+
const cwd = hookInput && typeof hookInput.cwd === "string" && hookInput.cwd.trim()
|
|
722
|
+
? hookInput.cwd.trim()
|
|
723
|
+
: "";
|
|
724
|
+
return cwd || process.cwd();
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function readWorkspaceSessionCache(workspaceKey) {
|
|
728
|
+
if (!workspaceKey) return null;
|
|
729
|
+
const safe = workspaceKey.replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
730
|
+
try {
|
|
731
|
+
return JSON.parse(fs.readFileSync(path.join(SESSION_CACHE_DIR, `workspace-${safe}.json`), "utf-8"));
|
|
732
|
+
} catch {
|
|
733
|
+
return null;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function selectSessionCache(sessionId, workspaceKey) {
|
|
738
|
+
const exactCache = sessionId ? readSessionCache(sessionId) : null;
|
|
739
|
+
const workspaceCache = readWorkspaceSessionCache(workspaceKey);
|
|
740
|
+
const cache =
|
|
741
|
+
exactCache && exactCache.status === "ready"
|
|
742
|
+
? exactCache
|
|
743
|
+
: workspaceCache && workspaceCache.status === "ready"
|
|
744
|
+
? workspaceCache
|
|
745
|
+
: exactCache || workspaceCache;
|
|
746
|
+
return cache;
|
|
513
747
|
}
|
|
514
748
|
|
|
515
749
|
// --- Main ---
|
|
@@ -546,7 +780,7 @@ function findMostRecentProjectWatermark(transcriptPath, currentSessionId) {
|
|
|
546
780
|
}
|
|
547
781
|
|
|
548
782
|
function buildSessionStartContext(convId, sessionId, agentId, memories, opts) {
|
|
549
|
-
const resolvedAgentId = agentId ||
|
|
783
|
+
const resolvedAgentId = agentId || COMMON_BUCKET_AGENT_ID;
|
|
550
784
|
const resumed = Boolean(opts && opts.resumed);
|
|
551
785
|
const opening = resumed
|
|
552
786
|
? `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.`
|
|
@@ -568,7 +802,7 @@ ${opening}
|
|
|
568
802
|
|
|
569
803
|
### What you MUST do with Meko tools
|
|
570
804
|
|
|
571
|
-
- **agent_id**: use "${resolvedAgentId}"
|
|
805
|
+
- **agent_id**: use "${resolvedAgentId}" verbatim for every Meko MCP tool call in this session. This was derived from the cwd as \`<client>:<repo-basename>\` so memories stay scoped to this project. For genuinely cross-project facts (user identity, global preferences) pass agent_id="${COMMON_BUCKET_AGENT_ID}" — that's the common bucket any agent can read regardless of project.
|
|
572
806
|
- **conversation_id**: Use "${convId}" for all MCP tool calls that accept it
|
|
573
807
|
- **scope**: Only "read", "write", or "admin"
|
|
574
808
|
|
|
@@ -588,7 +822,8 @@ Before any action that deletes, overwrites, or restructures user content (Write-
|
|
|
588
822
|
When Meko MCP tools are available, ALWAYS use them instead of local file-based memory (e.g., writing to ~/.claude/projects/.../memory/). If a Meko tool call fails, fall back to local storage and tell the user.${memoryBlock}`;
|
|
589
823
|
}
|
|
590
824
|
|
|
591
|
-
function buildSessionStartFallbackContext() {
|
|
825
|
+
function buildSessionStartFallbackContext(agentId) {
|
|
826
|
+
const resolvedAgentId = agentId || COMMON_BUCKET_AGENT_ID;
|
|
592
827
|
return `## Meko Memory Instructions
|
|
593
828
|
|
|
594
829
|
The SessionStart hook could not reach the Meko MCP server to create a conversation. Automatic capture may not work for this session.
|
|
@@ -596,7 +831,7 @@ The SessionStart hook could not reach the Meko MCP server to create a conversati
|
|
|
596
831
|
### What you should still do
|
|
597
832
|
|
|
598
833
|
- **Use Meko MCP tools** for memory_add, memory_search, etc. when available
|
|
599
|
-
- **agent_id**:
|
|
834
|
+
- **agent_id**: use "${resolvedAgentId}" for project-scoped writes; use "${COMMON_BUCKET_AGENT_ID}" for cross-project common facts
|
|
600
835
|
- **conversation_id**: Use nil UUID "00000000-0000-0000-0000-000000000000" as placeholder
|
|
601
836
|
- **scope**: Only "read", "write", or "admin"
|
|
602
837
|
|
|
@@ -611,6 +846,21 @@ The SessionStart hook could not reach the Meko MCP server to create a conversati
|
|
|
611
846
|
When Meko MCP tools are available, ALWAYS use them instead of local file-based memory. If a Meko tool call fails, fall back to local storage and tell the user.`;
|
|
612
847
|
}
|
|
613
848
|
|
|
849
|
+
function buildBootstrapPendingContext(agentId) {
|
|
850
|
+
const resolvedAgentId = agentId || COMMON_BUCKET_AGENT_ID;
|
|
851
|
+
return `## Meko Bootstrap Pending
|
|
852
|
+
|
|
853
|
+
The Cursor SessionStart hook has started Meko bootstrap work in the background, but the local Meko session cache is not ready yet. Do not guess a Meko conversation_id.
|
|
854
|
+
|
|
855
|
+
### What you should do
|
|
856
|
+
|
|
857
|
+
- **agent_id**: use "${resolvedAgentId}" for project-scoped reads/writes if a Meko MCP call is necessary before bootstrap completes
|
|
858
|
+
- **conversation_id**: use nil UUID "00000000-0000-0000-0000-000000000000" only as a placeholder until the cache provides the real conversation_id
|
|
859
|
+
- **scope**: Only "read", "write", or "admin"
|
|
860
|
+
|
|
861
|
+
If the user explicitly needs Meko-backed continuity before proceeding, ask them to retry the prompt after bootstrap finishes.`;
|
|
862
|
+
}
|
|
863
|
+
|
|
614
864
|
/**
|
|
615
865
|
* Race a promise against a timeout; on timeout resolve to the fallback value.
|
|
616
866
|
* Memory preload is best-effort — if it stalls, the SessionStart hook must
|
|
@@ -650,16 +900,51 @@ function withTimeout(promise, ms, fallback, { onTimeout } = {}) {
|
|
|
650
900
|
});
|
|
651
901
|
}
|
|
652
902
|
|
|
903
|
+
function handleSessionPending(hookInput) {
|
|
904
|
+
const transcriptPath =
|
|
905
|
+
hookInput.transcript_path ||
|
|
906
|
+
(hookInput.hookSpecificInput || {}).transcript_path;
|
|
907
|
+
const sessionId = extractSessionId(hookInput, transcriptPath);
|
|
908
|
+
const agentId = resolveSessionAgentId(transcriptPath, hookInput);
|
|
909
|
+
const workspaceKey = workspaceKeyFromHookInput(hookInput, transcriptPath);
|
|
910
|
+
if (sessionId) {
|
|
911
|
+
writeSessionCache(sessionId, {
|
|
912
|
+
status: "pending",
|
|
913
|
+
agent_id: agentId,
|
|
914
|
+
workspace_key: workspaceKey,
|
|
915
|
+
additional_context: buildBootstrapPendingContext(agentId),
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
process.stdout.write(nativeOutput(""));
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function handleBeforeSubmitPrompt(hookInput) {
|
|
922
|
+
const transcriptPath =
|
|
923
|
+
hookInput.transcript_path ||
|
|
924
|
+
(hookInput.hookSpecificInput || {}).transcript_path;
|
|
925
|
+
const sessionId = extractSessionId(hookInput, transcriptPath);
|
|
926
|
+
const workspaceKey = workspaceKeyFromHookInput(hookInput, transcriptPath);
|
|
927
|
+
const cache = selectSessionCache(sessionId, workspaceKey);
|
|
928
|
+
if (cache && cache.status === "pending") {
|
|
929
|
+
process.stdout.write(beforeSubmitOutput(
|
|
930
|
+
false,
|
|
931
|
+
"Meko is still creating this session's conversation. Please retry in a moment so the agent starts with Meko context.",
|
|
932
|
+
));
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
process.stdout.write(beforeSubmitOutput(true));
|
|
936
|
+
}
|
|
937
|
+
|
|
653
938
|
async function handleSessionStart(hookInput) {
|
|
654
939
|
const transcriptPath =
|
|
655
940
|
hookInput.transcript_path ||
|
|
656
941
|
(hookInput.hookSpecificInput || {}).transcript_path;
|
|
657
942
|
|
|
658
|
-
const sessionId = transcriptPath
|
|
659
|
-
? path.basename(transcriptPath, ".jsonl")
|
|
660
|
-
: null;
|
|
943
|
+
const sessionId = extractSessionId(hookInput, transcriptPath);
|
|
661
944
|
|
|
662
945
|
const source = hookInput.source || "";
|
|
946
|
+
const derivedAgentId = resolveSessionAgentId(transcriptPath, hookInput);
|
|
947
|
+
const workspaceKey = workspaceKeyFromHookInput(hookInput, transcriptPath);
|
|
663
948
|
|
|
664
949
|
// Check if watermark already exists (same session ID re-fires SessionStart, e.g. /clear, /compact)
|
|
665
950
|
if (sessionId) {
|
|
@@ -667,22 +952,32 @@ async function handleSessionStart(hookInput) {
|
|
|
667
952
|
const existing = readWatermark(wmPath);
|
|
668
953
|
if (existing.conversation_id) {
|
|
669
954
|
process.stderr.write(`[meko-capture] Session ${sessionId}: reusing existing conversation ${existing.conversation_id}\n`);
|
|
670
|
-
//
|
|
671
|
-
//
|
|
672
|
-
//
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
955
|
+
// Prefer the watermark's cached agent_id so a user-set bucket survives
|
|
956
|
+
// re-fires of SessionStart (e.g. /clear, /compact). The exception is
|
|
957
|
+
// the legacy literal "agent" left by pre-contract installers — we
|
|
958
|
+
// override that with the freshly derived `claude_code:<repo>` so
|
|
959
|
+
// upgraded users stop writing into the ghost namespace.
|
|
960
|
+
const cached = (existing.agent_id || "").trim();
|
|
961
|
+
const resumedAgentId = cached && cached !== "agent" ? cached : derivedAgentId;
|
|
962
|
+
// Skip preload on resume (we don't know what's already in context).
|
|
963
|
+
const context = buildSessionStartContext(
|
|
964
|
+
existing.conversation_id,
|
|
965
|
+
sessionId,
|
|
966
|
+
resumedAgentId,
|
|
967
|
+
null,
|
|
682
968
|
);
|
|
969
|
+
writeSessionCache(sessionId, {
|
|
970
|
+
status: "ready",
|
|
971
|
+
conversation_id: existing.conversation_id,
|
|
972
|
+
agent_id: resumedAgentId,
|
|
973
|
+
workspace_key: workspaceKey,
|
|
974
|
+
additional_context: context,
|
|
975
|
+
});
|
|
976
|
+
process.stdout.write(hookOutput(context));
|
|
683
977
|
return;
|
|
684
978
|
}
|
|
685
979
|
}
|
|
980
|
+
const agentId = derivedAgentId;
|
|
686
981
|
|
|
687
982
|
// Resume: Claude Code created a new session ID for a resumed thread. Find the most recent
|
|
688
983
|
// prior watermark for this project and reuse its conversation_id so continuity is preserved.
|
|
@@ -693,83 +988,117 @@ async function handleSessionStart(hookInput) {
|
|
|
693
988
|
if (prior && prior.conversation_id) {
|
|
694
989
|
const currentLineCount = fs.existsSync(transcriptPath) ? countLines(transcriptPath) : 0;
|
|
695
990
|
const wmPath = path.join(WATERMARK_DIR, `${sessionId}.watermark.json`);
|
|
696
|
-
|
|
991
|
+
// Preserve a non-legacy cached agent_id from the prior watermark so a
|
|
992
|
+
// user-set bucket (e.g. MEKO_AGENT_ID=foo, or a project-scoped
|
|
993
|
+
// claude_code:<other-repo> from the original session) survives the
|
|
994
|
+
// resume. Mirrors the same-session reuse branch above. The legacy
|
|
995
|
+
// literal "agent" is overridden with the freshly derived per-project
|
|
996
|
+
// bucket so upgraded users stop writing into the ghost namespace.
|
|
997
|
+
const priorAgentId = (prior.agent_id || "").trim();
|
|
998
|
+
const resumedAgentId =
|
|
999
|
+
priorAgentId && priorAgentId !== "agent" ? priorAgentId : agentId;
|
|
1000
|
+
writeWatermark(wmPath, prior.conversation_id, currentLineCount, resumedAgentId);
|
|
697
1001
|
process.stderr.write(`[meko-capture] Session ${sessionId}: resuming conversation ${prior.conversation_id} from ${prior.sessionId}, skipping ${currentLineCount} copied lines\n`);
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
null,
|
|
705
|
-
{ resumed: true },
|
|
706
|
-
),
|
|
707
|
-
),
|
|
1002
|
+
const context = buildSessionStartContext(
|
|
1003
|
+
prior.conversation_id,
|
|
1004
|
+
sessionId,
|
|
1005
|
+
resumedAgentId,
|
|
1006
|
+
null,
|
|
1007
|
+
{ resumed: true },
|
|
708
1008
|
);
|
|
1009
|
+
writeSessionCache(sessionId, {
|
|
1010
|
+
status: "ready",
|
|
1011
|
+
conversation_id: prior.conversation_id,
|
|
1012
|
+
agent_id: resumedAgentId,
|
|
1013
|
+
workspace_key: workspaceKey,
|
|
1014
|
+
additional_context: context,
|
|
1015
|
+
});
|
|
1016
|
+
process.stdout.write(hookOutput(context));
|
|
709
1017
|
return;
|
|
710
1018
|
}
|
|
711
1019
|
process.stderr.write(`[meko-capture] Session ${sessionId}: source=resume but no prior project watermark found; creating new conversation\n`);
|
|
712
1020
|
}
|
|
713
1021
|
|
|
714
|
-
// Create conversation via MCP
|
|
1022
|
+
// Create conversation via MCP. Attach session metadata (cwd / gitBranch /
|
|
1023
|
+
// startedAt) from the transcript header so hook traces match the migrator.
|
|
1024
|
+
const transcriptMetadata = peekTranscriptMetadata(transcriptPath);
|
|
715
1025
|
let convId = null;
|
|
716
1026
|
try {
|
|
717
1027
|
await mcpInitialize();
|
|
718
|
-
convId = await createConversation(sessionId || "unknown");
|
|
1028
|
+
convId = await createConversation(sessionId || "unknown", agentId, transcriptMetadata);
|
|
719
1029
|
} catch (err) {
|
|
720
1030
|
process.stderr.write(`[meko-capture] SessionStart: MCP unavailable (${err.message}). Falling back to agent-driven setup.\n`);
|
|
721
|
-
|
|
1031
|
+
const context = buildSessionStartFallbackContext(agentId);
|
|
1032
|
+
if (sessionId) {
|
|
1033
|
+
writeSessionCache(sessionId, {
|
|
1034
|
+
status: "error",
|
|
1035
|
+
agent_id: agentId,
|
|
1036
|
+
workspace_key: workspaceKey,
|
|
1037
|
+
error: err && err.message ? String(err.message).slice(0, 240) : "MCP unavailable",
|
|
1038
|
+
additional_context: context,
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
process.stdout.write(hookOutput(context));
|
|
722
1042
|
return;
|
|
723
1043
|
}
|
|
724
1044
|
|
|
725
1045
|
if (!convId) {
|
|
726
1046
|
process.stderr.write("[meko-capture] SessionStart: conversation_create returned no ID.\n");
|
|
727
|
-
|
|
1047
|
+
const context = buildSessionStartFallbackContext(agentId);
|
|
1048
|
+
if (sessionId) {
|
|
1049
|
+
writeSessionCache(sessionId, {
|
|
1050
|
+
status: "error",
|
|
1051
|
+
agent_id: agentId,
|
|
1052
|
+
workspace_key: workspaceKey,
|
|
1053
|
+
error: "conversation_create returned no ID",
|
|
1054
|
+
additional_context: context,
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
process.stdout.write(hookOutput(context));
|
|
728
1058
|
return;
|
|
729
1059
|
}
|
|
730
1060
|
|
|
731
|
-
//
|
|
732
|
-
//
|
|
733
|
-
//
|
|
1061
|
+
// Preload recent memories with a 4s budget (SessionStart timeout is 15s).
|
|
1062
|
+
// Best-effort enrichment — on timeout/failure we still deliver the core
|
|
1063
|
+
// context. agent_id is derived per-session from the cwd; the old
|
|
1064
|
+
// db_summarize_database discovery helper was removed (it depended on a
|
|
1065
|
+
// paid-tier tool and only worked "correctly" via its fallback branch
|
|
1066
|
+
// anyway — see scratch/2026-04-30-multi-tenant-mem0-audit.md Probe 6).
|
|
734
1067
|
const PRELOAD_BUDGET_MS = 4000;
|
|
735
|
-
const
|
|
736
|
-
(
|
|
737
|
-
const agentId = await discoverAgentId();
|
|
738
|
-
const memories = await fetchRecentMemories(agentId);
|
|
739
|
-
return { agentId, memories };
|
|
740
|
-
})(),
|
|
1068
|
+
const memories = await withTimeout(
|
|
1069
|
+
fetchRecentMemories(agentId),
|
|
741
1070
|
PRELOAD_BUDGET_MS,
|
|
742
|
-
|
|
1071
|
+
[],
|
|
743
1072
|
{ onTimeout: abortInflightRequests },
|
|
744
1073
|
);
|
|
745
1074
|
|
|
746
|
-
// Write watermark (now cachinG agent_id too)
|
|
747
1075
|
if (sessionId) {
|
|
748
1076
|
const wmPath = path.join(WATERMARK_DIR, `${sessionId}.watermark.json`);
|
|
749
|
-
writeWatermark(wmPath, convId, 0,
|
|
1077
|
+
writeWatermark(wmPath, convId, 0, agentId);
|
|
750
1078
|
process.stderr.write(
|
|
751
|
-
`[meko-capture] SessionStart: created conversation ${convId}, agent_id=${
|
|
1079
|
+
`[meko-capture] SessionStart: created conversation ${convId}, agent_id=${agentId}, preloaded ${memories.length} memories, watermark at ${wmPath}\n`,
|
|
752
1080
|
);
|
|
753
1081
|
}
|
|
754
1082
|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
)
|
|
764
|
-
|
|
1083
|
+
const context = buildSessionStartContext(convId, sessionId, agentId, memories);
|
|
1084
|
+
if (sessionId) {
|
|
1085
|
+
writeSessionCache(sessionId, {
|
|
1086
|
+
status: "ready",
|
|
1087
|
+
conversation_id: convId,
|
|
1088
|
+
agent_id: agentId,
|
|
1089
|
+
workspace_key: workspaceKey,
|
|
1090
|
+
additional_context: context,
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
process.stdout.write(hookOutput(context));
|
|
765
1094
|
}
|
|
766
1095
|
|
|
767
1096
|
// --- Main ---
|
|
768
1097
|
|
|
769
1098
|
async function main() {
|
|
770
1099
|
const hookType = process.argv[2];
|
|
771
|
-
if (!hookType || !["session-start", "pre-compact", "session-end", "checkpoint"].includes(hookType)) {
|
|
772
|
-
process.stderr.write("Usage: capture.js <session-start|pre-compact|session-end|checkpoint>\n");
|
|
1100
|
+
if (!hookType || !["session-start", "session-pending", "before-submit-prompt", "pre-compact", "session-end", "checkpoint"].includes(hookType)) {
|
|
1101
|
+
process.stderr.write("Usage: capture.js <session-start|session-pending|before-submit-prompt|pre-compact|session-end|checkpoint>\n");
|
|
773
1102
|
process.exit(1);
|
|
774
1103
|
}
|
|
775
1104
|
|
|
@@ -790,6 +1119,16 @@ async function main() {
|
|
|
790
1119
|
return;
|
|
791
1120
|
}
|
|
792
1121
|
|
|
1122
|
+
if (hookType === "session-pending") {
|
|
1123
|
+
handleSessionPending(hookInput);
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
if (hookType === "before-submit-prompt") {
|
|
1128
|
+
handleBeforeSubmitPrompt(hookInput);
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
793
1132
|
// PreCompact / SessionEnd: capture transcript exchanges
|
|
794
1133
|
const transcriptPath =
|
|
795
1134
|
hookInput.transcript_path ||
|
|
@@ -807,6 +1146,15 @@ async function main() {
|
|
|
807
1146
|
const lastLine = watermark.last_line_number || 0;
|
|
808
1147
|
const currentLines = countLines(transcriptPath);
|
|
809
1148
|
|
|
1149
|
+
// Resolve agent_id for this hook invocation. Prefer the value the
|
|
1150
|
+
// SessionStart hook persisted in the watermark; if it's missing or a
|
|
1151
|
+
// legacy literal ("agent"), fall back to a fresh derivation from the
|
|
1152
|
+
// transcript's cwd so writes land in the right bucket.
|
|
1153
|
+
const cachedAgentId = (watermark.agent_id || "").trim();
|
|
1154
|
+
const agentId = cachedAgentId && cachedAgentId !== "agent"
|
|
1155
|
+
? cachedAgentId
|
|
1156
|
+
: resolveSessionAgentId(transcriptPath);
|
|
1157
|
+
|
|
810
1158
|
// Nothing new
|
|
811
1159
|
if (currentLines <= lastLine) {
|
|
812
1160
|
process.stdout.write(hookOutput(""));
|
|
@@ -839,15 +1187,17 @@ async function main() {
|
|
|
839
1187
|
|
|
840
1188
|
for (const exchange of exchanges) {
|
|
841
1189
|
try {
|
|
842
|
-
await addMessage(convId, exchange);
|
|
1190
|
+
await addMessage(convId, agentId, exchange);
|
|
843
1191
|
captured++;
|
|
844
1192
|
} catch (err) {
|
|
845
1193
|
process.stderr.write(`[meko-capture] Failed to add message (uuid=${exchange.user_uuid}): ${err.message}\n`);
|
|
846
1194
|
}
|
|
847
1195
|
}
|
|
848
1196
|
|
|
849
|
-
// Update watermark
|
|
850
|
-
|
|
1197
|
+
// Update watermark with the resolved agent_id. If the cached value was a
|
|
1198
|
+
// legacy literal we now overwrite it so subsequent hooks see the derived
|
|
1199
|
+
// bucket directly.
|
|
1200
|
+
writeWatermark(wmPath, convId, currentLines, agentId);
|
|
851
1201
|
|
|
852
1202
|
// Output
|
|
853
1203
|
const context =
|