@yugabytedb/meko-mcp 1.2.0 → 2.1.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 +59 -25
- package/assets/hooks-handlers/before-submit-prompt.sh +2 -2
- package/assets/hooks-handlers/codex-session-end.sh +78 -0
- package/assets/hooks-handlers/lib/capture.js +366 -152
- package/assets/hooks-handlers/lib/checkpoint-timer.js +67 -13
- package/assets/hooks-handlers/session-end.sh +10 -0
- package/assets/hooks-handlers/session-start.sh +14 -3
- package/assets/hooks-handlers/test_checkpoint_timer.sh +162 -0
- package/assets/scripts/meko-statusline.sh +119 -0
- package/assets/scripts/package-desktop-skill.sh +64 -30
- package/assets/skills/meko-mcp-tools/SKILL.md +84 -86
- package/assets/skills/meko-mcp-tools/references/_sections.md +2 -4
- package/assets/skills/meko-mcp-tools/references/tools-agent-id-conventions.md +29 -25
- package/assets/skills/meko-mcp-tools/references/tools-conversation-dedup.md +5 -7
- package/assets/skills/meko-mcp-tools/references/tools-conversation-reasoning.md +50 -0
- package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +126 -184
- package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +31 -37
- package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +10 -26
- package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +26 -30
- package/assets/skills/meko-mcp-tools/references/tools-overview.md +60 -66
- package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +28 -93
- package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +5 -99
- package/assets/skills/meko-mcp-tools/references/tools-user-id-conventions.md +9 -8
- package/assets/skills/meko-mcp-tools-desktop/SKILL.md +138 -138
- package/assets/skills/meko-mcp-tools-desktop/desktop/README.md +46 -16
- package/assets/skills/meko-mcp-tools-desktop/references/_sections.md +2 -2
- package/assets/skills/meko-mcp-tools-desktop/references/tools-agent-id-conventions.md +28 -24
- package/assets/skills/meko-mcp-tools-desktop/references/tools-conversation-reasoning.md +50 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-cookbook.md +120 -174
- package/assets/skills/meko-mcp-tools-desktop/references/tools-known-limitations.md +16 -22
- package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +28 -34
- package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +60 -66
- package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +28 -93
- package/assets/skills/meko-mcp-tools-desktop/references/tools-troubleshooting.md +4 -98
- package/assets/skills/meko-select-datapack-desktop/SKILL.md +208 -0
- package/assets/skills/meko-select-datapack-desktop/references/cookbook.md +121 -0
- package/assets/skills/meko-select-datapack-desktop/references/role-display-future.md +41 -0
- package/assets/skills/meko-select-datapack-desktop/references/troubleshooting.md +66 -0
- package/bin/create-meko-setup.mjs +74 -6
- package/lib/clients/claude-code.mjs +137 -6
- package/lib/clients/claude-desktop.mjs +51 -12
- package/lib/clients/codex.mjs +329 -13
- package/lib/clients/common.mjs +43 -1
- package/lib/clients/cursor.mjs +82 -21
- package/lib/config.mjs +26 -0
- package/lib/detect.mjs +7 -2
- package/lib/installer.mjs +24 -0
- package/lib/mcp-transport.mjs +5 -0
- package/lib/prompt.mjs +1 -1
- package/lib/url-selection.mjs +55 -0
- package/lib/validate.mjs +0 -1
- package/package.json +1 -1
- package/scripts/bundle-assets.mjs +8 -0
- package/assets/skills/meko-mcp-tools/references/tools-db-query-patterns.md +0 -38
- package/assets/skills/meko-mcp-tools/references/tools-scope-permissions.md +0 -75
- package/assets/skills/meko-mcp-tools/references/tools-write-guardrails.md +0 -30
- package/assets/skills/meko-mcp-tools-desktop/references/tools-scope-permissions.md +0 -75
|
@@ -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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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,126 @@ 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
|
+
function captureErrorPath(agentId) {
|
|
371
|
+
return path.join(WATERMARK_DIR, `last-capture-error-${datapackPinSlug(agentId)}.json`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function readCaptureError(agentId) {
|
|
375
|
+
try {
|
|
376
|
+
const error = JSON.parse(fs.readFileSync(captureErrorPath(agentId), "utf-8"));
|
|
377
|
+
return error && typeof error === "object" ? error : null;
|
|
378
|
+
} catch {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function writeCaptureError(agentId, error) {
|
|
384
|
+
try {
|
|
385
|
+
fs.mkdirSync(WATERMARK_DIR, { recursive: true });
|
|
386
|
+
fs.writeFileSync(
|
|
387
|
+
captureErrorPath(agentId),
|
|
388
|
+
JSON.stringify({
|
|
389
|
+
...error,
|
|
390
|
+
agent_id: agentId,
|
|
391
|
+
occurred_at: new Date().toISOString(),
|
|
392
|
+
}),
|
|
393
|
+
);
|
|
394
|
+
} catch (err) {
|
|
395
|
+
process.stderr.write(`[meko-capture] Could not persist capture error: ${err.message}\n`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function clearCaptureError(agentId) {
|
|
400
|
+
try {
|
|
401
|
+
fs.unlinkSync(captureErrorPath(agentId));
|
|
402
|
+
} catch (err) {
|
|
403
|
+
if (err && err.code !== "ENOENT") {
|
|
404
|
+
process.stderr.write(`[meko-capture] Could not clear capture error: ${err.message}\n`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Read the active-datapack pin written by the meko-select-datapack skill.
|
|
411
|
+
*
|
|
412
|
+
* The pin is a sidecar file at `<WATERMARK_DIR>/pin-<slug(agent_id)>.json`
|
|
413
|
+
* with shape `{ datapack_id, datapack_name, selected_at }`. Project-scoped:
|
|
414
|
+
* keyed by the same `agent_id` the hook derives for memory writes
|
|
415
|
+
* (`claude_code:<repo-basename>`), so a pin set in one Claude Code window
|
|
416
|
+
* for a repo applies to every other window in that repo, and survives
|
|
417
|
+
* `/clear`, `/compact`, and Claude Code restart.
|
|
418
|
+
*
|
|
419
|
+
* Hooks are read-only — only the skill writes this file. Returns null if
|
|
420
|
+
* the file is absent, malformed, or missing required fields, so SessionStart
|
|
421
|
+
* can proceed unchanged when no pin is set.
|
|
422
|
+
*/
|
|
423
|
+
function readDatapackPin(agentId) {
|
|
424
|
+
const slug = datapackPinSlug(agentId);
|
|
425
|
+
if (!slug) return null;
|
|
426
|
+
const pinPath = path.join(WATERMARK_DIR, `pin-${slug}.json`);
|
|
427
|
+
try {
|
|
428
|
+
const obj = JSON.parse(fs.readFileSync(pinPath, "utf-8"));
|
|
429
|
+
const id = typeof obj.datapack_id === "string" ? obj.datapack_id.trim() : "";
|
|
430
|
+
const name = typeof obj.datapack_name === "string" ? obj.datapack_name.trim() : "";
|
|
431
|
+
if (!id || !name) return null;
|
|
432
|
+
return { datapack_id: id, datapack_name: name, selected_at: obj.selected_at || "" };
|
|
433
|
+
} catch {
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function datapackPinFromWatermark(watermark) {
|
|
439
|
+
if (!watermark) return null;
|
|
440
|
+
const id = typeof watermark.datapack_id === "string" ? watermark.datapack_id.trim() : "";
|
|
441
|
+
const name = typeof watermark.datapack_name === "string" ? watermark.datapack_name.trim() : "";
|
|
442
|
+
return id && name ? { datapack_id: id, datapack_name: name } : null;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function buildActiveDatapackBlock(pin) {
|
|
446
|
+
if (!pin) return "";
|
|
447
|
+
return `
|
|
448
|
+
|
|
449
|
+
### Active datapack
|
|
450
|
+
|
|
451
|
+
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 that accepts it (memory_*, knowledgebase_search, conversation_*, artifact_*) 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.`;
|
|
452
|
+
}
|
|
453
|
+
|
|
340
454
|
// note: Subagents cannot auto-discover the parent's watermark today.
|
|
341
455
|
// The watermark dir is flat and keyed by session UUID, and a subagent
|
|
342
456
|
// process has no way to learn its parent's session UUID:
|
|
@@ -366,6 +480,13 @@ function mcpPost(jsonBody) {
|
|
|
366
480
|
"Content-Type": "application/json",
|
|
367
481
|
Accept: "application/json, text/event-stream",
|
|
368
482
|
"Content-Length": Buffer.byteLength(body),
|
|
483
|
+
// Cloud Meko sits behind a WAF that 403s requests with no User-Agent
|
|
484
|
+
// (Node's http/https send none by default). Without this the SessionStart
|
|
485
|
+
// / PreCompact / SessionEnd hooks all fail against mcp.mekodata.ai and
|
|
486
|
+
// automatic capture silently never runs — the hook falls back to the
|
|
487
|
+
// agent-driven path every session. Send an explicit UA so the WAF admits
|
|
488
|
+
// the request. (Verified: prod 403s without UA, 200s with one.)
|
|
489
|
+
"User-Agent": "meko-capture/1.0 (+https://github.com/yugabyte/meko-mcp-server)",
|
|
369
490
|
};
|
|
370
491
|
if (MEKO_API_KEY) {
|
|
371
492
|
headers["Authorization"] = `Bearer ${MEKO_API_KEY}`;
|
|
@@ -388,6 +509,16 @@ function mcpPost(jsonBody) {
|
|
|
388
509
|
res.on("data", (chunk) => (data += chunk));
|
|
389
510
|
res.on("end", () => {
|
|
390
511
|
inflightRequests.delete(req);
|
|
512
|
+
if (res.statusCode >= 400) {
|
|
513
|
+
const snippet = data.trim().replace(/\s+/g, " ").slice(0, 200);
|
|
514
|
+
const err = new Error(
|
|
515
|
+
`MCP HTTP ${res.statusCode}${snippet ? `: ${snippet}` : ""}`,
|
|
516
|
+
);
|
|
517
|
+
err.statusCode = res.statusCode;
|
|
518
|
+
err.responseBody = data.slice(0, 1000);
|
|
519
|
+
reject(err);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
391
522
|
// Notifications return 202/204 with no body — that's OK
|
|
392
523
|
if (!data.trim()) {
|
|
393
524
|
resolve({ body: null, headers: res.headers });
|
|
@@ -453,6 +584,51 @@ function parseMcpResponseBody(data) {
|
|
|
453
584
|
return JSON.parse(payload);
|
|
454
585
|
}
|
|
455
586
|
|
|
587
|
+
function classifyPersistentCaptureFailure(err) {
|
|
588
|
+
const message = err && err.message ? String(err.message) : String(err || "");
|
|
589
|
+
const normalized = message.toLowerCase();
|
|
590
|
+
const statusCode = Number(err && err.statusCode);
|
|
591
|
+
const statusMatch = normalized.match(/\bmcp http (402|403)\b/);
|
|
592
|
+
let persistentStatus = null;
|
|
593
|
+
if (statusCode === 402 || statusCode === 403) {
|
|
594
|
+
persistentStatus = statusCode;
|
|
595
|
+
} else if (statusMatch) {
|
|
596
|
+
persistentStatus = Number(statusMatch[1]);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (persistentStatus) {
|
|
600
|
+
return {
|
|
601
|
+
code: `http_${persistentStatus}`,
|
|
602
|
+
reason: `Meko rejected capture with HTTP ${persistentStatus}`,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const persistentPatterns = [
|
|
607
|
+
["free_tier_limit_reached", /free[_\s-]?tier.*limit|free_tier_limit_reached/],
|
|
608
|
+
["quota_exceeded", /quota[_\s-]?(reached|exceeded)|(?:reached|exceeded).*quota/],
|
|
609
|
+
["usage_limit_reached", /usage[_\s-]?limit[_\s-]?(reached|exceeded)/],
|
|
610
|
+
["plan_limit_reached", /plan[_\s-]?limit[_\s-]?(reached|exceeded)/],
|
|
611
|
+
["payment_required", /payment[_\s-]?required/],
|
|
612
|
+
["subscription_required", /subscription[_\s-]?(required|inactive|expired)/],
|
|
613
|
+
["entitlement_denied", /entitlement.*(denied|required|missing)|(?:denied|required|missing).*entitlement/],
|
|
614
|
+
];
|
|
615
|
+
for (const [code, pattern] of persistentPatterns) {
|
|
616
|
+
if (pattern.test(normalized)) {
|
|
617
|
+
const reasons = {
|
|
618
|
+
free_tier_limit_reached: "the free-tier capture limit was reached",
|
|
619
|
+
quota_exceeded: "the capture quota was exceeded",
|
|
620
|
+
usage_limit_reached: "the capture usage limit was reached",
|
|
621
|
+
plan_limit_reached: "the current plan's capture limit was reached",
|
|
622
|
+
payment_required: "capture requires payment",
|
|
623
|
+
subscription_required: "capture requires an active subscription",
|
|
624
|
+
entitlement_denied: "the account is not entitled to capture conversations",
|
|
625
|
+
};
|
|
626
|
+
return { code, reason: reasons[code] };
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
631
|
+
|
|
456
632
|
/**
|
|
457
633
|
* Abort all pending MCP requests. Used by the preload timeout path so the
|
|
458
634
|
* SessionStart hook can return promptly instead of waiting for a slow server
|
|
@@ -570,13 +746,14 @@ function peekTranscriptMetadata(transcriptPath) {
|
|
|
570
746
|
* On any error the caller falls back to an empty list — memory preload is a
|
|
571
747
|
* best-effort enrichment, not a correctness requirement.
|
|
572
748
|
*/
|
|
573
|
-
async function fetchRecentMemories(agentId, { limit = 10, budget = 2000 } = {}) {
|
|
574
|
-
const
|
|
575
|
-
scope: "read",
|
|
749
|
+
async function fetchRecentMemories(agentId, datapackId = null, { limit = 10, budget = 2000 } = {}) {
|
|
750
|
+
const payload = {
|
|
576
751
|
agent_id: agentId,
|
|
577
752
|
conversation_id: "00000000-0000-0000-0000-000000000000",
|
|
578
753
|
limit,
|
|
579
|
-
}
|
|
754
|
+
};
|
|
755
|
+
if (datapackId) payload.datapack_id = datapackId;
|
|
756
|
+
const response = await mcpCall("memory_get_all", payload);
|
|
580
757
|
const result = extractToolResult(response);
|
|
581
758
|
if (!result) return [];
|
|
582
759
|
|
|
@@ -594,9 +771,8 @@ async function fetchRecentMemories(agentId, { limit = 10, budget = 2000 } = {})
|
|
|
594
771
|
return summaries;
|
|
595
772
|
}
|
|
596
773
|
|
|
597
|
-
async function createConversation(sessionId, agentId, metadata = null) {
|
|
774
|
+
async function createConversation(sessionId, agentId, metadata = null, datapackId = null) {
|
|
598
775
|
const payload = {
|
|
599
|
-
scope: "write",
|
|
600
776
|
agent_id: agentId,
|
|
601
777
|
title: "Claude Code session (auto-captured)",
|
|
602
778
|
session_id: sessionId,
|
|
@@ -609,46 +785,72 @@ async function createConversation(sessionId, agentId, metadata = null) {
|
|
|
609
785
|
startedAt: metadata.startedAt ?? null,
|
|
610
786
|
});
|
|
611
787
|
}
|
|
788
|
+
if (datapackId) payload.datapack_id = datapackId;
|
|
612
789
|
const response = await mcpCall("conversation_create", payload);
|
|
613
790
|
const result = extractToolResult(response);
|
|
614
791
|
return result && result.id ? result.id : null;
|
|
615
792
|
}
|
|
616
793
|
|
|
617
|
-
async function addMessage(convId, agentId, exchange) {
|
|
794
|
+
async function addMessage(convId, agentId, exchange, datapackId = null) {
|
|
618
795
|
const seed = `${convId}:${exchange.user_uuid}`;
|
|
619
|
-
|
|
620
|
-
scope: "write",
|
|
796
|
+
const payload = {
|
|
621
797
|
conversation_id: convId,
|
|
622
798
|
agent_id: agentId,
|
|
623
799
|
input: exchange.input,
|
|
624
800
|
output: exchange.output,
|
|
625
801
|
reasoning: exchange.reasoning,
|
|
626
802
|
seed: seed,
|
|
627
|
-
}
|
|
803
|
+
};
|
|
804
|
+
if (datapackId) payload.datapack_id = datapackId;
|
|
805
|
+
const response = await mcpCall("conversation_add_message", payload);
|
|
806
|
+
// FAIL CLOSED. The MCP transport resolves successfully even when the tool
|
|
807
|
+
// failed, and failures take several shapes: an `isError` result envelope, a
|
|
808
|
+
// JSON body with an `error` field, or a non-JSON / missing-content / otherwise
|
|
809
|
+
// malformed body (which extractToolResult reports as null). If we treat any of
|
|
810
|
+
// these as success, the caller counts the turn as captured and advances the
|
|
811
|
+
// watermark — permanently dropping it. So only a *valid success body* counts;
|
|
812
|
+
// anything else throws into the caller's failed++/watermark-hold/retry path
|
|
813
|
+
// (dedup-by-seed makes the retry safe).
|
|
814
|
+
if (response && response.result && response.result.isError) {
|
|
815
|
+
throw new Error(
|
|
816
|
+
`conversation_add_message returned isError: ${JSON.stringify(response.result)}`,
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
const result = extractToolResult(response);
|
|
820
|
+
if (result === null) {
|
|
821
|
+
throw new Error(
|
|
822
|
+
"conversation_add_message: no parseable result body (non-JSON, empty, or malformed response)",
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
if (result.error) {
|
|
826
|
+
throw new Error(`conversation_add_message failed: ${JSON.stringify(result.error)}`);
|
|
827
|
+
}
|
|
828
|
+
return result;
|
|
628
829
|
}
|
|
629
830
|
|
|
630
831
|
// --- Hook output ---
|
|
631
832
|
|
|
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
833
|
function hookOutput(additionalContext) {
|
|
641
|
-
//
|
|
642
|
-
//
|
|
834
|
+
// Codex PreCompact accepts only the common hook output fields.
|
|
835
|
+
// `additionalContext` is valid hook-specific output for SessionStart, but
|
|
836
|
+
// emitting it for lifecycle capture hooks makes Codex reject the JSON.
|
|
837
|
+
if (process.argv[2] !== "session-start") return "{}";
|
|
838
|
+
|
|
839
|
+
// Codex rejects unknown top-level fields, so its SessionStart response must
|
|
840
|
+
// contain only the hook-specific output defined by the Codex wire schema.
|
|
643
841
|
const output = {
|
|
644
842
|
hookSpecificOutput: {
|
|
645
|
-
hookEventName:
|
|
843
|
+
hookEventName: "SessionStart",
|
|
646
844
|
additionalContext: additionalContext || "",
|
|
647
845
|
},
|
|
648
846
|
};
|
|
649
|
-
if (process.
|
|
650
|
-
output
|
|
847
|
+
if (process.env.MEKO_HOOK_CLIENT === "codex") {
|
|
848
|
+
return JSON.stringify(output);
|
|
651
849
|
}
|
|
850
|
+
|
|
851
|
+
// Preserve the native coding-agent harness field for clients that consume
|
|
852
|
+
// it alongside the nested Claude-compatible shape.
|
|
853
|
+
output.additional_context = additionalContext || "";
|
|
652
854
|
return JSON.stringify(output);
|
|
653
855
|
}
|
|
654
856
|
|
|
@@ -656,9 +858,9 @@ function nativeOutput(additionalContext) {
|
|
|
656
858
|
return JSON.stringify({ additional_context: additionalContext || "" });
|
|
657
859
|
}
|
|
658
860
|
|
|
659
|
-
function beforeSubmitOutput(
|
|
660
|
-
const output = {
|
|
661
|
-
if (userMessage) output.
|
|
861
|
+
function beforeSubmitOutput(permission = "allow", userMessage = "") {
|
|
862
|
+
const output = { permission };
|
|
863
|
+
if (userMessage) output.userMessage = userMessage;
|
|
662
864
|
return JSON.stringify(output);
|
|
663
865
|
}
|
|
664
866
|
|
|
@@ -701,16 +903,6 @@ function writeSessionCache(sessionId, payload) {
|
|
|
701
903
|
}
|
|
702
904
|
}
|
|
703
905
|
|
|
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
906
|
function workspaceKeyFromHookInput(hookInput, transcriptPath) {
|
|
715
907
|
const roots = hookInput && Array.isArray(hookInput.workspace_roots)
|
|
716
908
|
? hookInput.workspace_roots
|
|
@@ -724,28 +916,6 @@ function workspaceKeyFromHookInput(hookInput, transcriptPath) {
|
|
|
724
916
|
return cwd || process.cwd();
|
|
725
917
|
}
|
|
726
918
|
|
|
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
919
|
// --- Main ---
|
|
750
920
|
|
|
751
921
|
// --- SessionStart: create conversation + watermark deterministically ---
|
|
@@ -768,6 +938,8 @@ function findMostRecentProjectWatermark(transcriptPath, currentSessionId) {
|
|
|
768
938
|
sessionId: sid,
|
|
769
939
|
conversation_id: wm.conversation_id,
|
|
770
940
|
agent_id: wm.agent_id || "",
|
|
941
|
+
datapack_id: wm.datapack_id || null,
|
|
942
|
+
datapack_name: wm.datapack_name || null,
|
|
771
943
|
updated_at: ts,
|
|
772
944
|
};
|
|
773
945
|
}
|
|
@@ -782,9 +954,18 @@ function findMostRecentProjectWatermark(transcriptPath, currentSessionId) {
|
|
|
782
954
|
function buildSessionStartContext(convId, sessionId, agentId, memories, opts) {
|
|
783
955
|
const resolvedAgentId = agentId || COMMON_BUCKET_AGENT_ID;
|
|
784
956
|
const resumed = Boolean(opts && opts.resumed);
|
|
957
|
+
const pin = opts && opts.datapackPin ? opts.datapackPin : null;
|
|
958
|
+
const captureError = opts && opts.captureError ? opts.captureError : null;
|
|
785
959
|
const opening = resumed
|
|
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.`
|
|
787
|
-
: `Meko conversation **${convId}** was created automatically by the SessionStart hook. The watermark file is written — PreCompact and SessionEnd hooks
|
|
960
|
+
? `Resuming Meko conversation **${convId}** from a prior Claude Code session. The watermark file is written — PreCompact and SessionEnd hooks will attempt to append this session's new exchanges to the same conversation.`
|
|
961
|
+
: `Meko conversation **${convId}** was created automatically by the SessionStart hook. The watermark file is written — PreCompact and SessionEnd hooks are configured to capture this session's transcript.`;
|
|
962
|
+
const captureWarning = captureError
|
|
963
|
+
? `
|
|
964
|
+
|
|
965
|
+
### Automatic capture warning — tell the user
|
|
966
|
+
|
|
967
|
+
The last automatic capture attempt was persistently rejected because ${captureError.reason || "Meko rejected the write"}. **Do not claim that this session's turns are being saved.** The hook kept ${Number(captureError.held_exchanges || captureError.failed_exchanges) || 1} exchange(s) queued for retry instead of dropping them. Tell the user that automatic conversation capture is currently unhealthy and include this reason. This warning remains until a later hook write succeeds.`
|
|
968
|
+
: "";
|
|
788
969
|
const memoryBlock =
|
|
789
970
|
Array.isArray(memories) && memories.length > 0
|
|
790
971
|
? `
|
|
@@ -795,70 +976,59 @@ These facts were preloaded for you — inspect them before acting so you don't r
|
|
|
795
976
|
|
|
796
977
|
${memories.join("\n")}`
|
|
797
978
|
: "";
|
|
979
|
+
const datapackBlock = buildActiveDatapackBlock(pin);
|
|
798
980
|
|
|
799
981
|
return `## Meko Session Active
|
|
800
982
|
|
|
801
983
|
${opening}
|
|
984
|
+
${captureWarning}
|
|
802
985
|
|
|
803
986
|
### What you MUST do with Meko tools
|
|
804
987
|
|
|
805
988
|
- **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.
|
|
806
989
|
- **conversation_id**: Use "${convId}" for all MCP tool calls that accept it
|
|
807
|
-
- **scope**: Only "read", "write", or "admin"
|
|
808
990
|
|
|
809
|
-
###
|
|
991
|
+
### Memory capture is automatic only when hook delivery succeeds — do NOT proactively call memory_add
|
|
810
992
|
|
|
811
|
-
|
|
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)
|
|
993
|
+
The hooks above attempt to capture this session's turns, and the server extracts durable memories from successfully delivered turns on its own. Do not call memory_add for facts the user says out loud; when hook delivery succeeds, that would duplicate what extraction already stores. If the warning above is present, tell the user instead of pretending an explicit memory write will bypass the same server rejection.
|
|
815
994
|
|
|
816
|
-
|
|
995
|
+
Call memory_add (with the session's agent_id + conversation_id) ONLY for the narrow cases extraction cannot reach:
|
|
996
|
+
- The user explicitly says "remember this" / "save this to memory".
|
|
997
|
+
- 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.
|
|
998
|
+
- 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).
|
|
817
999
|
|
|
818
|
-
|
|
1000
|
+
### Recall and before destructive actions
|
|
1001
|
+
|
|
1002
|
+
- User asks "what do you know about X?" → call memory_search first.
|
|
1003
|
+
- 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
1004
|
|
|
820
1005
|
### Use Meko, not local storage
|
|
821
1006
|
|
|
822
|
-
|
|
1007
|
+
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
1008
|
}
|
|
824
1009
|
|
|
825
|
-
function buildSessionStartFallbackContext(agentId) {
|
|
1010
|
+
function buildSessionStartFallbackContext(agentId, opts) {
|
|
826
1011
|
const resolvedAgentId = agentId || COMMON_BUCKET_AGENT_ID;
|
|
1012
|
+
const datapackBlock = buildActiveDatapackBlock(opts && opts.datapackPin ? opts.datapackPin : null);
|
|
827
1013
|
return `## Meko Memory Instructions
|
|
828
1014
|
|
|
829
|
-
The SessionStart hook could not reach the Meko MCP server to create a conversation. Automatic capture
|
|
1015
|
+
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
1016
|
|
|
831
|
-
### What you should
|
|
1017
|
+
### What you should do while capture is down
|
|
832
1018
|
|
|
833
|
-
- **Use Meko MCP tools** for memory_add, memory_search, etc. when available
|
|
834
1019
|
- **agent_id**: use "${resolvedAgentId}" for project-scoped writes; use "${COMMON_BUCKET_AGENT_ID}" for cross-project common facts
|
|
835
1020
|
- **conversation_id**: Use nil UUID "00000000-0000-0000-0000-000000000000" as placeholder
|
|
836
|
-
- **scope**: Only "read", "write", or "admin"
|
|
837
1021
|
|
|
838
|
-
### Proactive storage (do NOT wait to be asked)
|
|
1022
|
+
### Proactive storage (capture is down — do NOT wait to be asked)
|
|
839
1023
|
|
|
1024
|
+
Because nothing is capturing this session, explicit saves are the only path — this is the exception, not the normal mode:
|
|
840
1025
|
- User shares personal info, preferences, or role → call memory_add immediately
|
|
841
1026
|
- User shares org conventions or team standards → call memory_add immediately
|
|
842
1027
|
- User asks "what do you know about X?" → call memory_search first
|
|
843
1028
|
|
|
844
1029
|
### Use Meko, not local storage
|
|
845
1030
|
|
|
846
|
-
|
|
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.`;
|
|
1031
|
+
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
1032
|
}
|
|
863
1033
|
|
|
864
1034
|
/**
|
|
@@ -900,39 +1070,8 @@ function withTimeout(promise, ms, fallback, { onTimeout } = {}) {
|
|
|
900
1070
|
});
|
|
901
1071
|
}
|
|
902
1072
|
|
|
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
1073
|
function handleBeforeSubmitPrompt(hookInput) {
|
|
922
|
-
|
|
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));
|
|
1074
|
+
process.stdout.write(beforeSubmitOutput("allow"));
|
|
936
1075
|
}
|
|
937
1076
|
|
|
938
1077
|
async function handleSessionStart(hookInput) {
|
|
@@ -959,12 +1098,17 @@ async function handleSessionStart(hookInput) {
|
|
|
959
1098
|
// upgraded users stop writing into the ghost namespace.
|
|
960
1099
|
const cached = (existing.agent_id || "").trim();
|
|
961
1100
|
const resumedAgentId = cached && cached !== "agent" ? cached : derivedAgentId;
|
|
1101
|
+
const existingDatapackPin = datapackPinFromWatermark(existing);
|
|
962
1102
|
// Skip preload on resume (we don't know what's already in context).
|
|
963
1103
|
const context = buildSessionStartContext(
|
|
964
1104
|
existing.conversation_id,
|
|
965
1105
|
sessionId,
|
|
966
1106
|
resumedAgentId,
|
|
967
1107
|
null,
|
|
1108
|
+
{
|
|
1109
|
+
datapackPin: existingDatapackPin,
|
|
1110
|
+
captureError: readCaptureError(resumedAgentId),
|
|
1111
|
+
},
|
|
968
1112
|
);
|
|
969
1113
|
writeSessionCache(sessionId, {
|
|
970
1114
|
status: "ready",
|
|
@@ -997,14 +1141,25 @@ async function handleSessionStart(hookInput) {
|
|
|
997
1141
|
const priorAgentId = (prior.agent_id || "").trim();
|
|
998
1142
|
const resumedAgentId =
|
|
999
1143
|
priorAgentId && priorAgentId !== "agent" ? priorAgentId : agentId;
|
|
1000
|
-
|
|
1144
|
+
const priorDatapackPin = datapackPinFromWatermark(prior);
|
|
1145
|
+
writeWatermark(
|
|
1146
|
+
wmPath,
|
|
1147
|
+
prior.conversation_id,
|
|
1148
|
+
currentLineCount,
|
|
1149
|
+
resumedAgentId,
|
|
1150
|
+
priorDatapackPin,
|
|
1151
|
+
);
|
|
1001
1152
|
process.stderr.write(`[meko-capture] Session ${sessionId}: resuming conversation ${prior.conversation_id} from ${prior.sessionId}, skipping ${currentLineCount} copied lines\n`);
|
|
1002
1153
|
const context = buildSessionStartContext(
|
|
1003
1154
|
prior.conversation_id,
|
|
1004
1155
|
sessionId,
|
|
1005
1156
|
resumedAgentId,
|
|
1006
1157
|
null,
|
|
1007
|
-
{
|
|
1158
|
+
{
|
|
1159
|
+
resumed: true,
|
|
1160
|
+
datapackPin: priorDatapackPin,
|
|
1161
|
+
captureError: readCaptureError(resumedAgentId),
|
|
1162
|
+
},
|
|
1008
1163
|
);
|
|
1009
1164
|
writeSessionCache(sessionId, {
|
|
1010
1165
|
status: "ready",
|
|
@@ -1022,13 +1177,19 @@ async function handleSessionStart(hookInput) {
|
|
|
1022
1177
|
// Create conversation via MCP. Attach session metadata (cwd / gitBranch /
|
|
1023
1178
|
// startedAt) from the transcript header so hook traces match the migrator.
|
|
1024
1179
|
const transcriptMetadata = peekTranscriptMetadata(transcriptPath);
|
|
1180
|
+
const datapackPin = readDatapackPin(agentId);
|
|
1025
1181
|
let convId = null;
|
|
1026
1182
|
try {
|
|
1027
1183
|
await mcpInitialize();
|
|
1028
|
-
convId = await createConversation(
|
|
1184
|
+
convId = await createConversation(
|
|
1185
|
+
sessionId || "unknown",
|
|
1186
|
+
agentId,
|
|
1187
|
+
transcriptMetadata,
|
|
1188
|
+
datapackPin && datapackPin.datapack_id,
|
|
1189
|
+
);
|
|
1029
1190
|
} catch (err) {
|
|
1030
1191
|
process.stderr.write(`[meko-capture] SessionStart: MCP unavailable (${err.message}). Falling back to agent-driven setup.\n`);
|
|
1031
|
-
const context = buildSessionStartFallbackContext(agentId);
|
|
1192
|
+
const context = buildSessionStartFallbackContext(agentId, { datapackPin: readDatapackPin(agentId) });
|
|
1032
1193
|
if (sessionId) {
|
|
1033
1194
|
writeSessionCache(sessionId, {
|
|
1034
1195
|
status: "error",
|
|
@@ -1044,7 +1205,7 @@ async function handleSessionStart(hookInput) {
|
|
|
1044
1205
|
|
|
1045
1206
|
if (!convId) {
|
|
1046
1207
|
process.stderr.write("[meko-capture] SessionStart: conversation_create returned no ID.\n");
|
|
1047
|
-
const context = buildSessionStartFallbackContext(agentId);
|
|
1208
|
+
const context = buildSessionStartFallbackContext(agentId, { datapackPin: readDatapackPin(agentId) });
|
|
1048
1209
|
if (sessionId) {
|
|
1049
1210
|
writeSessionCache(sessionId, {
|
|
1050
1211
|
status: "error",
|
|
@@ -1060,13 +1221,10 @@ async function handleSessionStart(hookInput) {
|
|
|
1060
1221
|
|
|
1061
1222
|
// Preload recent memories with a 4s budget (SessionStart timeout is 15s).
|
|
1062
1223
|
// Best-effort enrichment — on timeout/failure we still deliver the core
|
|
1063
|
-
// context. agent_id is derived per-session from the cwd
|
|
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).
|
|
1224
|
+
// context. agent_id is derived per-session from the cwd.
|
|
1067
1225
|
const PRELOAD_BUDGET_MS = 4000;
|
|
1068
1226
|
const memories = await withTimeout(
|
|
1069
|
-
fetchRecentMemories(agentId),
|
|
1227
|
+
fetchRecentMemories(agentId, datapackPin && datapackPin.datapack_id),
|
|
1070
1228
|
PRELOAD_BUDGET_MS,
|
|
1071
1229
|
[],
|
|
1072
1230
|
{ onTimeout: abortInflightRequests },
|
|
@@ -1074,13 +1232,16 @@ async function handleSessionStart(hookInput) {
|
|
|
1074
1232
|
|
|
1075
1233
|
if (sessionId) {
|
|
1076
1234
|
const wmPath = path.join(WATERMARK_DIR, `${sessionId}.watermark.json`);
|
|
1077
|
-
writeWatermark(wmPath, convId, 0, agentId);
|
|
1235
|
+
writeWatermark(wmPath, convId, 0, agentId, datapackPin);
|
|
1078
1236
|
process.stderr.write(
|
|
1079
1237
|
`[meko-capture] SessionStart: created conversation ${convId}, agent_id=${agentId}, preloaded ${memories.length} memories, watermark at ${wmPath}\n`,
|
|
1080
1238
|
);
|
|
1081
1239
|
}
|
|
1082
1240
|
|
|
1083
|
-
const context = buildSessionStartContext(convId, sessionId, agentId, memories
|
|
1241
|
+
const context = buildSessionStartContext(convId, sessionId, agentId, memories, {
|
|
1242
|
+
datapackPin,
|
|
1243
|
+
captureError: readCaptureError(agentId),
|
|
1244
|
+
});
|
|
1084
1245
|
if (sessionId) {
|
|
1085
1246
|
writeSessionCache(sessionId, {
|
|
1086
1247
|
status: "ready",
|
|
@@ -1097,8 +1258,8 @@ async function handleSessionStart(hookInput) {
|
|
|
1097
1258
|
|
|
1098
1259
|
async function main() {
|
|
1099
1260
|
const hookType = process.argv[2];
|
|
1100
|
-
if (!hookType || !["session-start", "
|
|
1101
|
-
process.stderr.write("Usage: capture.js <session-start|
|
|
1261
|
+
if (!hookType || !["session-start", "before-submit-prompt", "pre-compact", "session-end", "checkpoint"].includes(hookType)) {
|
|
1262
|
+
process.stderr.write("Usage: capture.js <session-start|before-submit-prompt|pre-compact|session-end|checkpoint>\n");
|
|
1102
1263
|
process.exit(1);
|
|
1103
1264
|
}
|
|
1104
1265
|
|
|
@@ -1119,11 +1280,6 @@ async function main() {
|
|
|
1119
1280
|
return;
|
|
1120
1281
|
}
|
|
1121
1282
|
|
|
1122
|
-
if (hookType === "session-pending") {
|
|
1123
|
-
handleSessionPending(hookInput);
|
|
1124
|
-
return;
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
1283
|
if (hookType === "before-submit-prompt") {
|
|
1128
1284
|
handleBeforeSubmitPrompt(hookInput);
|
|
1129
1285
|
return;
|
|
@@ -1154,6 +1310,7 @@ async function main() {
|
|
|
1154
1310
|
const agentId = cachedAgentId && cachedAgentId !== "agent"
|
|
1155
1311
|
? cachedAgentId
|
|
1156
1312
|
: resolveSessionAgentId(transcriptPath);
|
|
1313
|
+
const datapackPin = datapackPinFromWatermark(watermark);
|
|
1157
1314
|
|
|
1158
1315
|
// Nothing new
|
|
1159
1316
|
if (currentLines <= lastLine) {
|
|
@@ -1165,6 +1322,20 @@ async function main() {
|
|
|
1165
1322
|
try {
|
|
1166
1323
|
await mcpInitialize();
|
|
1167
1324
|
} catch (err) {
|
|
1325
|
+
const persistent = classifyPersistentCaptureFailure(err);
|
|
1326
|
+
if (persistent) {
|
|
1327
|
+
const pendingExchanges = extractExchanges(transcriptPath, lastLine).length;
|
|
1328
|
+
writeCaptureError(agentId, {
|
|
1329
|
+
code: persistent.code,
|
|
1330
|
+
reason: persistent.reason,
|
|
1331
|
+
held_exchanges: pendingExchanges || 1,
|
|
1332
|
+
session_id: sessionId,
|
|
1333
|
+
conversation_id: convId || null,
|
|
1334
|
+
});
|
|
1335
|
+
process.stderr.write(
|
|
1336
|
+
`[meko-capture] Persistent capture rejection recorded for the next SessionStart: ${persistent.reason}.\n`,
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1168
1339
|
process.stderr.write(`[meko-capture] Failed to initialize MCP session: ${err.message}\n`);
|
|
1169
1340
|
process.stdout.write(hookOutput(""));
|
|
1170
1341
|
return;
|
|
@@ -1184,20 +1355,63 @@ async function main() {
|
|
|
1184
1355
|
// Extract and send exchanges
|
|
1185
1356
|
const exchanges = extractExchanges(transcriptPath, lastLine);
|
|
1186
1357
|
let captured = 0;
|
|
1358
|
+
let failed = 0;
|
|
1359
|
+
const persistentFailures = [];
|
|
1187
1360
|
|
|
1188
1361
|
for (const exchange of exchanges) {
|
|
1189
1362
|
try {
|
|
1190
|
-
await addMessage(
|
|
1363
|
+
await addMessage(
|
|
1364
|
+
convId,
|
|
1365
|
+
agentId,
|
|
1366
|
+
exchange,
|
|
1367
|
+
datapackPin && datapackPin.datapack_id,
|
|
1368
|
+
);
|
|
1191
1369
|
captured++;
|
|
1192
1370
|
} catch (err) {
|
|
1371
|
+
failed++;
|
|
1372
|
+
const persistent = classifyPersistentCaptureFailure(err);
|
|
1373
|
+
if (persistent) persistentFailures.push(persistent);
|
|
1193
1374
|
process.stderr.write(`[meko-capture] Failed to add message (uuid=${exchange.user_uuid}): ${err.message}\n`);
|
|
1194
1375
|
}
|
|
1195
1376
|
}
|
|
1196
1377
|
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1378
|
+
if (persistentFailures.length > 0) {
|
|
1379
|
+
const persistent = persistentFailures[0];
|
|
1380
|
+
writeCaptureError(agentId, {
|
|
1381
|
+
code: persistent.code,
|
|
1382
|
+
reason: persistent.reason,
|
|
1383
|
+
// Every failed exchange remains held by the watermark, including any
|
|
1384
|
+
// transient failures that happened in the same persistently rejected batch.
|
|
1385
|
+
held_exchanges: failed,
|
|
1386
|
+
session_id: sessionId,
|
|
1387
|
+
conversation_id: convId,
|
|
1388
|
+
});
|
|
1389
|
+
process.stderr.write(
|
|
1390
|
+
`[meko-capture] Persistent capture rejection recorded for the next SessionStart: ${persistent.reason}.\n`,
|
|
1391
|
+
);
|
|
1392
|
+
} else if (captured > 0) {
|
|
1393
|
+
// A successful write proves the prior persistent rejection has cleared.
|
|
1394
|
+
// Transient failures may still hold this batch's watermark independently.
|
|
1395
|
+
clearCaptureError(agentId);
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
// Advance the watermark to currentLines ONLY when every exchange was
|
|
1399
|
+
// captured. If any addMessage failed (typically a transient network error),
|
|
1400
|
+
// keep the watermark at lastLine so the next checkpoint / PreCompact /
|
|
1401
|
+
// SessionEnd hook re-extracts and retries this batch. Re-sending the
|
|
1402
|
+
// exchanges that already succeeded is safe: capture dedups by seed
|
|
1403
|
+
// (<conv_id>:<user_uuid>), so retries never create duplicates. Advancing on
|
|
1404
|
+
// partial failure is what would permanently drop the failed exchanges.
|
|
1405
|
+
// We still always persist the resolved agent_id (migrating any legacy literal
|
|
1406
|
+
// value) so later hooks see the derived bucket directly.
|
|
1407
|
+
const watermarkLine = failed === 0 ? currentLines : lastLine;
|
|
1408
|
+
if (failed > 0) {
|
|
1409
|
+
process.stderr.write(
|
|
1410
|
+
`[meko-capture] ${failed}/${exchanges.length} exchange(s) failed; ` +
|
|
1411
|
+
`holding watermark at line ${lastLine} for retry (dedup makes resends safe).\n`
|
|
1412
|
+
);
|
|
1413
|
+
}
|
|
1414
|
+
writeWatermark(wmPath, convId, watermarkLine, agentId, datapackPin);
|
|
1201
1415
|
|
|
1202
1416
|
// Output
|
|
1203
1417
|
const context =
|