@yugabytedb/meko-mcp 1.2.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.
Files changed (44) hide show
  1. package/README.md +28 -19
  2. package/assets/hooks-handlers/before-submit-prompt.sh +2 -2
  3. package/assets/hooks-handlers/lib/capture.js +209 -144
  4. package/assets/hooks-handlers/lib/checkpoint-timer.js +23 -11
  5. package/assets/hooks-handlers/session-start.sh +6 -2
  6. package/assets/scripts/meko-statusline.sh +119 -0
  7. package/assets/scripts/package-desktop-skill.sh +27 -24
  8. package/assets/skills/meko-mcp-tools/SKILL.md +42 -50
  9. package/assets/skills/meko-mcp-tools/references/_sections.md +1 -1
  10. package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +75 -150
  11. package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +30 -36
  12. package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +6 -22
  13. package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +9 -8
  14. package/assets/skills/meko-mcp-tools/references/tools-overview.md +35 -42
  15. package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +28 -92
  16. package/assets/skills/meko-mcp-tools/references/tools-scope-permissions.md +5 -9
  17. package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +1 -81
  18. package/assets/skills/meko-mcp-tools-desktop/SKILL.md +100 -107
  19. package/assets/skills/meko-mcp-tools-desktop/desktop/README.md +41 -11
  20. package/assets/skills/meko-mcp-tools-desktop/references/_sections.md +1 -1
  21. package/assets/skills/meko-mcp-tools-desktop/references/tools-cookbook.md +71 -150
  22. package/assets/skills/meko-mcp-tools-desktop/references/tools-known-limitations.md +12 -18
  23. package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +12 -15
  24. package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +35 -42
  25. package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +28 -92
  26. package/assets/skills/meko-mcp-tools-desktop/references/tools-scope-permissions.md +5 -9
  27. package/assets/skills/meko-mcp-tools-desktop/references/tools-troubleshooting.md +1 -81
  28. package/assets/skills/meko-select-datapack-desktop/SKILL.md +198 -0
  29. package/assets/skills/meko-select-datapack-desktop/references/cookbook.md +121 -0
  30. package/assets/skills/meko-select-datapack-desktop/references/troubleshooting.md +66 -0
  31. package/bin/create-meko-setup.mjs +29 -0
  32. package/lib/clients/claude-code.mjs +133 -6
  33. package/lib/clients/claude-desktop.mjs +37 -12
  34. package/lib/clients/codex.mjs +306 -5
  35. package/lib/clients/common.mjs +41 -1
  36. package/lib/clients/cursor.mjs +75 -19
  37. package/lib/config.mjs +26 -0
  38. package/lib/installer.mjs +24 -0
  39. package/lib/mcp-transport.mjs +5 -0
  40. package/lib/validate.mjs +0 -1
  41. package/package.json +1 -1
  42. package/scripts/bundle-assets.mjs +8 -0
  43. package/assets/skills/meko-mcp-tools/references/tools-db-query-patterns.md +0 -38
  44. package/assets/skills/meko-mcp-tools/references/tools-write-guardrails.md +0 -30
@@ -211,10 +211,18 @@ function extractToolResultContent(msg) {
211
211
  }
212
212
  // Fallback to toolUseResult summary if no content blocks found
213
213
  if (parts.length === 0 && msg.toolUseResult != null) {
214
- const summary =
215
- typeof msg.toolUseResult === "string"
216
- ? msg.toolUseResult
217
- : JSON.stringify(msg.toolUseResult);
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
+ }
218
226
  parts.push(truncate(summary, MAX_TOOL_RESULT_LEN));
219
227
  }
220
228
  return parts.length > 0 ? "TOOL RESULT:\n" + parts.join("\n") : "";
@@ -323,20 +331,87 @@ function readWatermark(wmPath) {
323
331
  }
324
332
  }
325
333
 
326
- function writeWatermark(wmPath, convId, lineNum, agentId) {
334
+ function writeWatermark(wmPath, convId, lineNum, agentId, datapackPin = null) {
327
335
  fs.mkdirSync(path.dirname(wmPath), { recursive: true });
328
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;
329
345
  fs.writeFileSync(
330
346
  wmPath,
331
347
  JSON.stringify({
332
348
  conversation_id: convId,
333
349
  last_line_number: lineNum,
334
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,
335
353
  updated_at: new Date().toISOString(),
336
354
  })
337
355
  );
338
356
  }
339
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
+
340
415
  // note: Subagents cannot auto-discover the parent's watermark today.
341
416
  // The watermark dir is flat and keyed by session UUID, and a subagent
342
417
  // process has no way to learn its parent's session UUID:
@@ -366,6 +441,13 @@ function mcpPost(jsonBody) {
366
441
  "Content-Type": "application/json",
367
442
  Accept: "application/json, text/event-stream",
368
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)",
369
451
  };
370
452
  if (MEKO_API_KEY) {
371
453
  headers["Authorization"] = `Bearer ${MEKO_API_KEY}`;
@@ -570,13 +652,15 @@ function peekTranscriptMetadata(transcriptPath) {
570
652
  * On any error the caller falls back to an empty list — memory preload is a
571
653
  * best-effort enrichment, not a correctness requirement.
572
654
  */
573
- async function fetchRecentMemories(agentId, { limit = 10, budget = 2000 } = {}) {
574
- const response = await mcpCall("memory_get_all", {
655
+ async function fetchRecentMemories(agentId, datapackId = null, { limit = 10, budget = 2000 } = {}) {
656
+ const payload = {
575
657
  scope: "read",
576
658
  agent_id: agentId,
577
659
  conversation_id: "00000000-0000-0000-0000-000000000000",
578
660
  limit,
579
- });
661
+ };
662
+ if (datapackId) payload.datapack_id = datapackId;
663
+ const response = await mcpCall("memory_get_all", payload);
580
664
  const result = extractToolResult(response);
581
665
  if (!result) return [];
582
666
 
@@ -594,7 +678,7 @@ async function fetchRecentMemories(agentId, { limit = 10, budget = 2000 } = {})
594
678
  return summaries;
595
679
  }
596
680
 
597
- async function createConversation(sessionId, agentId, metadata = null) {
681
+ async function createConversation(sessionId, agentId, metadata = null, datapackId = null) {
598
682
  const payload = {
599
683
  scope: "write",
600
684
  agent_id: agentId,
@@ -609,14 +693,15 @@ async function createConversation(sessionId, agentId, metadata = null) {
609
693
  startedAt: metadata.startedAt ?? null,
610
694
  });
611
695
  }
696
+ if (datapackId) payload.datapack_id = datapackId;
612
697
  const response = await mcpCall("conversation_create", payload);
613
698
  const result = extractToolResult(response);
614
699
  return result && result.id ? result.id : null;
615
700
  }
616
701
 
617
- async function addMessage(convId, agentId, exchange) {
702
+ async function addMessage(convId, agentId, exchange, datapackId = null) {
618
703
  const seed = `${convId}:${exchange.user_uuid}`;
619
- await mcpCall("conversation_add_message", {
704
+ const payload = {
620
705
  scope: "write",
621
706
  conversation_id: convId,
622
707
  agent_id: agentId,
@@ -624,31 +709,51 @@ async function addMessage(convId, agentId, exchange) {
624
709
  output: exchange.output,
625
710
  reasoning: exchange.reasoning,
626
711
  seed: seed,
627
- });
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;
628
738
  }
629
739
 
630
740
  // --- Hook output ---
631
741
 
632
- function hookEventName(hookType) {
633
- switch (hookType) {
634
- case "session-start": return "SessionStart";
635
- case "pre-compact": return "PreCompact";
636
- default: return "SessionEnd";
637
- }
638
- }
639
-
640
742
  function hookOutput(additionalContext) {
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
+
641
748
  // Emit the native coding-agent harness field while preserving the nested
642
749
  // Claude-compatible shape for clients that still consume it.
643
750
  const output = {
644
751
  hookSpecificOutput: {
645
- hookEventName: hookEventName(process.argv[2]),
752
+ hookEventName: "SessionStart",
646
753
  additionalContext: additionalContext || "",
647
754
  },
648
755
  };
649
- if (process.argv[2] === "session-start") {
650
- output.additional_context = additionalContext || "";
651
- }
756
+ output.additional_context = additionalContext || "";
652
757
  return JSON.stringify(output);
653
758
  }
654
759
 
@@ -656,9 +761,9 @@ function nativeOutput(additionalContext) {
656
761
  return JSON.stringify({ additional_context: additionalContext || "" });
657
762
  }
658
763
 
659
- function beforeSubmitOutput(shouldContinue, userMessage = "") {
660
- const output = { continue: Boolean(shouldContinue) };
661
- if (userMessage) output.user_message = userMessage;
764
+ function beforeSubmitOutput(permission = "allow", userMessage = "") {
765
+ const output = { permission };
766
+ if (userMessage) output.userMessage = userMessage;
662
767
  return JSON.stringify(output);
663
768
  }
664
769
 
@@ -701,16 +806,6 @@ function writeSessionCache(sessionId, payload) {
701
806
  }
702
807
  }
703
808
 
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
809
  function workspaceKeyFromHookInput(hookInput, transcriptPath) {
715
810
  const roots = hookInput && Array.isArray(hookInput.workspace_roots)
716
811
  ? hookInput.workspace_roots
@@ -724,28 +819,6 @@ function workspaceKeyFromHookInput(hookInput, transcriptPath) {
724
819
  return cwd || process.cwd();
725
820
  }
726
821
 
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;
747
- }
748
-
749
822
  // --- Main ---
750
823
 
751
824
  // --- SessionStart: create conversation + watermark deterministically ---
@@ -768,6 +841,8 @@ function findMostRecentProjectWatermark(transcriptPath, currentSessionId) {
768
841
  sessionId: sid,
769
842
  conversation_id: wm.conversation_id,
770
843
  agent_id: wm.agent_id || "",
844
+ datapack_id: wm.datapack_id || null,
845
+ datapack_name: wm.datapack_name || null,
771
846
  updated_at: ts,
772
847
  };
773
848
  }
@@ -782,6 +857,7 @@ function findMostRecentProjectWatermark(transcriptPath, currentSessionId) {
782
857
  function buildSessionStartContext(convId, sessionId, agentId, memories, opts) {
783
858
  const resolvedAgentId = agentId || COMMON_BUCKET_AGENT_ID;
784
859
  const resumed = Boolean(opts && opts.resumed);
860
+ const pin = opts && opts.datapackPin ? opts.datapackPin : null;
785
861
  const opening = resumed
786
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.`
787
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.`;
@@ -795,6 +871,7 @@ These facts were preloaded for you — inspect them before acting so you don't r
795
871
 
796
872
  ${memories.join("\n")}`
797
873
  : "";
874
+ const datapackBlock = buildActiveDatapackBlock(pin);
798
875
 
799
876
  return `## Meko Session Active
800
877
 
@@ -806,59 +883,48 @@ ${opening}
806
883
  - **conversation_id**: Use "${convId}" for all MCP tool calls that accept it
807
884
  - **scope**: Only "read", "write", or "admin"
808
885
 
809
- ### Proactive storage (do NOT wait to be asked)
886
+ ### Memory capture is automatic — do NOT proactively call memory_add
810
887
 
811
- - User shares personal info, preferences, or role → call memory_add immediately
812
- - User shares org conventions or team standards → call memory_add immediately
813
- - User asks "what do you know about X?" → call memory_search first
814
- - User provides structured/tabular data → call db_run_write_query (CREATE TABLE + INSERT)
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).
815
894
 
816
- ### Before destructive actions
895
+ ### Recall and before destructive actions
817
896
 
818
- 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.
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.
819
899
 
820
900
  ### Use Meko, not local storage
821
901
 
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}`;
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}`;
823
903
  }
824
904
 
825
- function buildSessionStartFallbackContext(agentId) {
905
+ function buildSessionStartFallbackContext(agentId, opts) {
826
906
  const resolvedAgentId = agentId || COMMON_BUCKET_AGENT_ID;
907
+ const datapackBlock = buildActiveDatapackBlock(opts && opts.datapackPin ? opts.datapackPin : null);
827
908
  return `## Meko Memory Instructions
828
909
 
829
- The SessionStart hook could not reach the Meko MCP server to create a conversation. Automatic capture may not work for this session.
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.
830
911
 
831
- ### What you should still do
912
+ ### What you should do while capture is down
832
913
 
833
- - **Use Meko MCP tools** for memory_add, memory_search, etc. when available
834
914
  - **agent_id**: use "${resolvedAgentId}" for project-scoped writes; use "${COMMON_BUCKET_AGENT_ID}" for cross-project common facts
835
915
  - **conversation_id**: Use nil UUID "00000000-0000-0000-0000-000000000000" as placeholder
836
916
  - **scope**: Only "read", "write", or "admin"
837
917
 
838
- ### Proactive storage (do NOT wait to be asked)
918
+ ### Proactive storage (capture is down — do NOT wait to be asked)
839
919
 
920
+ Because nothing is capturing this session, explicit saves are the only path — this is the exception, not the normal mode:
840
921
  - User shares personal info, preferences, or role → call memory_add immediately
841
922
  - User shares org conventions or team standards → call memory_add immediately
842
923
  - User asks "what do you know about X?" → call memory_search first
843
924
 
844
925
  ### Use Meko, not local storage
845
926
 
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.`;
847
- }
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.`;
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}`;
862
928
  }
863
929
 
864
930
  /**
@@ -900,39 +966,8 @@ function withTimeout(promise, ms, fallback, { onTimeout } = {}) {
900
966
  });
901
967
  }
902
968
 
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
969
  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));
970
+ process.stdout.write(beforeSubmitOutput("allow"));
936
971
  }
937
972
 
938
973
  async function handleSessionStart(hookInput) {
@@ -959,12 +994,14 @@ async function handleSessionStart(hookInput) {
959
994
  // upgraded users stop writing into the ghost namespace.
960
995
  const cached = (existing.agent_id || "").trim();
961
996
  const resumedAgentId = cached && cached !== "agent" ? cached : derivedAgentId;
997
+ const existingDatapackPin = datapackPinFromWatermark(existing);
962
998
  // Skip preload on resume (we don't know what's already in context).
963
999
  const context = buildSessionStartContext(
964
1000
  existing.conversation_id,
965
1001
  sessionId,
966
1002
  resumedAgentId,
967
1003
  null,
1004
+ { datapackPin: existingDatapackPin },
968
1005
  );
969
1006
  writeSessionCache(sessionId, {
970
1007
  status: "ready",
@@ -997,14 +1034,21 @@ async function handleSessionStart(hookInput) {
997
1034
  const priorAgentId = (prior.agent_id || "").trim();
998
1035
  const resumedAgentId =
999
1036
  priorAgentId && priorAgentId !== "agent" ? priorAgentId : agentId;
1000
- writeWatermark(wmPath, prior.conversation_id, currentLineCount, resumedAgentId);
1037
+ const priorDatapackPin = datapackPinFromWatermark(prior);
1038
+ writeWatermark(
1039
+ wmPath,
1040
+ prior.conversation_id,
1041
+ currentLineCount,
1042
+ resumedAgentId,
1043
+ priorDatapackPin,
1044
+ );
1001
1045
  process.stderr.write(`[meko-capture] Session ${sessionId}: resuming conversation ${prior.conversation_id} from ${prior.sessionId}, skipping ${currentLineCount} copied lines\n`);
1002
1046
  const context = buildSessionStartContext(
1003
1047
  prior.conversation_id,
1004
1048
  sessionId,
1005
1049
  resumedAgentId,
1006
1050
  null,
1007
- { resumed: true },
1051
+ { resumed: true, datapackPin: priorDatapackPin },
1008
1052
  );
1009
1053
  writeSessionCache(sessionId, {
1010
1054
  status: "ready",
@@ -1022,13 +1066,19 @@ async function handleSessionStart(hookInput) {
1022
1066
  // Create conversation via MCP. Attach session metadata (cwd / gitBranch /
1023
1067
  // startedAt) from the transcript header so hook traces match the migrator.
1024
1068
  const transcriptMetadata = peekTranscriptMetadata(transcriptPath);
1069
+ const datapackPin = readDatapackPin(agentId);
1025
1070
  let convId = null;
1026
1071
  try {
1027
1072
  await mcpInitialize();
1028
- convId = await createConversation(sessionId || "unknown", agentId, transcriptMetadata);
1073
+ convId = await createConversation(
1074
+ sessionId || "unknown",
1075
+ agentId,
1076
+ transcriptMetadata,
1077
+ datapackPin && datapackPin.datapack_id,
1078
+ );
1029
1079
  } catch (err) {
1030
1080
  process.stderr.write(`[meko-capture] SessionStart: MCP unavailable (${err.message}). Falling back to agent-driven setup.\n`);
1031
- const context = buildSessionStartFallbackContext(agentId);
1081
+ const context = buildSessionStartFallbackContext(agentId, { datapackPin: readDatapackPin(agentId) });
1032
1082
  if (sessionId) {
1033
1083
  writeSessionCache(sessionId, {
1034
1084
  status: "error",
@@ -1044,7 +1094,7 @@ async function handleSessionStart(hookInput) {
1044
1094
 
1045
1095
  if (!convId) {
1046
1096
  process.stderr.write("[meko-capture] SessionStart: conversation_create returned no ID.\n");
1047
- const context = buildSessionStartFallbackContext(agentId);
1097
+ const context = buildSessionStartFallbackContext(agentId, { datapackPin: readDatapackPin(agentId) });
1048
1098
  if (sessionId) {
1049
1099
  writeSessionCache(sessionId, {
1050
1100
  status: "error",
@@ -1060,13 +1110,10 @@ async function handleSessionStart(hookInput) {
1060
1110
 
1061
1111
  // Preload recent memories with a 4s budget (SessionStart timeout is 15s).
1062
1112
  // 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).
1113
+ // context. agent_id is derived per-session from the cwd.
1067
1114
  const PRELOAD_BUDGET_MS = 4000;
1068
1115
  const memories = await withTimeout(
1069
- fetchRecentMemories(agentId),
1116
+ fetchRecentMemories(agentId, datapackPin && datapackPin.datapack_id),
1070
1117
  PRELOAD_BUDGET_MS,
1071
1118
  [],
1072
1119
  { onTimeout: abortInflightRequests },
@@ -1074,13 +1121,15 @@ async function handleSessionStart(hookInput) {
1074
1121
 
1075
1122
  if (sessionId) {
1076
1123
  const wmPath = path.join(WATERMARK_DIR, `${sessionId}.watermark.json`);
1077
- writeWatermark(wmPath, convId, 0, agentId);
1124
+ writeWatermark(wmPath, convId, 0, agentId, datapackPin);
1078
1125
  process.stderr.write(
1079
1126
  `[meko-capture] SessionStart: created conversation ${convId}, agent_id=${agentId}, preloaded ${memories.length} memories, watermark at ${wmPath}\n`,
1080
1127
  );
1081
1128
  }
1082
1129
 
1083
- const context = buildSessionStartContext(convId, sessionId, agentId, memories);
1130
+ const context = buildSessionStartContext(convId, sessionId, agentId, memories, {
1131
+ datapackPin,
1132
+ });
1084
1133
  if (sessionId) {
1085
1134
  writeSessionCache(sessionId, {
1086
1135
  status: "ready",
@@ -1097,8 +1146,8 @@ async function handleSessionStart(hookInput) {
1097
1146
 
1098
1147
  async function main() {
1099
1148
  const hookType = process.argv[2];
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");
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");
1102
1151
  process.exit(1);
1103
1152
  }
1104
1153
 
@@ -1119,11 +1168,6 @@ async function main() {
1119
1168
  return;
1120
1169
  }
1121
1170
 
1122
- if (hookType === "session-pending") {
1123
- handleSessionPending(hookInput);
1124
- return;
1125
- }
1126
-
1127
1171
  if (hookType === "before-submit-prompt") {
1128
1172
  handleBeforeSubmitPrompt(hookInput);
1129
1173
  return;
@@ -1154,6 +1198,7 @@ async function main() {
1154
1198
  const agentId = cachedAgentId && cachedAgentId !== "agent"
1155
1199
  ? cachedAgentId
1156
1200
  : resolveSessionAgentId(transcriptPath);
1201
+ const datapackPin = datapackPinFromWatermark(watermark);
1157
1202
 
1158
1203
  // Nothing new
1159
1204
  if (currentLines <= lastLine) {
@@ -1184,20 +1229,40 @@ async function main() {
1184
1229
  // Extract and send exchanges
1185
1230
  const exchanges = extractExchanges(transcriptPath, lastLine);
1186
1231
  let captured = 0;
1232
+ let failed = 0;
1187
1233
 
1188
1234
  for (const exchange of exchanges) {
1189
1235
  try {
1190
- await addMessage(convId, agentId, exchange);
1236
+ await addMessage(
1237
+ convId,
1238
+ agentId,
1239
+ exchange,
1240
+ datapackPin && datapackPin.datapack_id,
1241
+ );
1191
1242
  captured++;
1192
1243
  } catch (err) {
1244
+ failed++;
1193
1245
  process.stderr.write(`[meko-capture] Failed to add message (uuid=${exchange.user_uuid}): ${err.message}\n`);
1194
1246
  }
1195
1247
  }
1196
1248
 
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);
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);
1201
1266
 
1202
1267
  // Output
1203
1268
  const context =
@@ -17,7 +17,7 @@
17
17
  * MEKO_API_KEY Inherited by capture.js
18
18
  */
19
19
 
20
- const { execFileSync } = require("child_process");
20
+ const { execFile } = require("child_process");
21
21
  const fs = require("fs");
22
22
  const path = require("path");
23
23
 
@@ -57,16 +57,28 @@ function tick() {
57
57
 
58
58
  const hookInput = JSON.stringify({ transcript_path: transcriptPath });
59
59
 
60
- try {
61
- execFileSync("node", [captureScript, "checkpoint"], {
62
- input: hookInput,
63
- timeout: 30000,
64
- stdio: ["pipe", "pipe", "pipe"],
65
- });
66
- } catch (err) {
67
- // Best-effort: log and continue
68
- process.stderr.write(`[meko-timer] Checkpoint tick failed: ${err.message}\n`);
69
- }
60
+ // Run capture ASYNCHRONOUSLY. execFileSync would block this daemon's event
61
+ // loop for up to 30s, during which SIGTERM/SIGINT (clean shutdown) can't be
62
+ // handled. execFile keeps the loop responsive so cleanup() fires promptly.
63
+ const child = execFile(
64
+ "node",
65
+ [captureScript, "checkpoint"],
66
+ { timeout: 30000 },
67
+ (err) => {
68
+ if (err) {
69
+ // Best-effort: log and continue to the next tick.
70
+ process.stderr.write(`[meko-timer] Checkpoint tick failed: ${err.message}\n`);
71
+ }
72
+ },
73
+ );
74
+ // The stdin stream can emit EPIPE if the child exits or fails to spawn before
75
+ // we finish writing. Without a listener that error is unhandled and crashes
76
+ // the daemon — so swallow it here; the execFile callback above reports the
77
+ // real failure.
78
+ child.stdin.on("error", (err) => {
79
+ process.stderr.write(`[meko-timer] Checkpoint stdin error: ${err.message}\n`);
80
+ });
81
+ child.stdin.end(hookInput);
70
82
  }
71
83
 
72
84
  setInterval(tick, INTERVAL_SECS * 1000);