mindgraph-mcp 0.14.9 → 0.16.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 (4) hide show
  1. package/README.md +3 -3
  2. package/dist/cli.js +1112 -205
  3. package/dist/index.js +600 -94
  4. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -915,6 +915,61 @@ var init_dist = __esm({
915
915
  }
916
916
  });
917
917
 
918
+ // src/hook-env.ts
919
+ function stableAgentId() {
920
+ let user = "user";
921
+ try {
922
+ user = import_node_os.default.userInfo().username || "user";
923
+ } catch {
924
+ }
925
+ const digest = import_node_crypto.default.createHash("sha256").update(`${user}@${import_node_os.default.hostname()}`).digest("hex").slice(0, 12);
926
+ return `u-${digest}`;
927
+ }
928
+ function hookEnvPath(dir) {
929
+ return import_node_path.default.join(dir || import_node_path.default.join(import_node_os.default.homedir(), ".mindgraph"), "hooks.json");
930
+ }
931
+ function loadHookEnv(dir) {
932
+ try {
933
+ const raw = import_node_fs.default.readFileSync(hookEnvPath(dir), "utf8");
934
+ const parsed = JSON.parse(raw);
935
+ return {
936
+ baseUrl: typeof parsed.baseUrl === "string" ? parsed.baseUrl : void 0,
937
+ apiKey: typeof parsed.apiKey === "string" ? parsed.apiKey : void 0,
938
+ agentId: typeof parsed.agentId === "string" ? parsed.agentId : void 0,
939
+ orgId: typeof parsed.orgId === "string" ? parsed.orgId : void 0
940
+ };
941
+ } catch {
942
+ return {};
943
+ }
944
+ }
945
+ function saveHookEnv(env, dir) {
946
+ const file = hookEnvPath(dir);
947
+ import_node_fs.default.mkdirSync(import_node_path.default.dirname(file), { recursive: true });
948
+ const existing = loadHookEnv(dir);
949
+ const merged = {
950
+ baseUrl: env.baseUrl ?? existing.baseUrl,
951
+ apiKey: env.apiKey ?? existing.apiKey,
952
+ agentId: env.agentId ?? existing.agentId,
953
+ orgId: env.orgId === null ? void 0 : env.orgId ?? existing.orgId
954
+ };
955
+ import_node_fs.default.writeFileSync(file, `${JSON.stringify(merged, null, 2)}
956
+ `, {
957
+ mode: 384
958
+ });
959
+ import_node_fs.default.chmodSync(file, 384);
960
+ return file;
961
+ }
962
+ var import_node_crypto, import_node_fs, import_node_os, import_node_path;
963
+ var init_hook_env = __esm({
964
+ "src/hook-env.ts"() {
965
+ "use strict";
966
+ import_node_crypto = __toESM(require("crypto"));
967
+ import_node_fs = __toESM(require("fs"));
968
+ import_node_os = __toESM(require("os"));
969
+ import_node_path = __toESM(require("path"));
970
+ }
971
+ });
972
+
918
973
  // src/codegraph.ts
919
974
  function unavailabilityCaveats(reason, message) {
920
975
  const caveats = [message || "codegraph status failed"];
@@ -1095,7 +1150,7 @@ function codeRefIdentityKey(ref) {
1095
1150
  }
1096
1151
  };
1097
1152
  }
1098
- var import_node_child_process, import_node_fs2, import_promises, import_node_os2, import_node_path2, import_node_crypto, DEFAULT_TIMEOUT_MS, DEFAULT_STATUS_TTL_MS, MAX_OUTPUT_BYTES, CodegraphAdapter;
1153
+ var import_node_child_process, import_node_fs2, import_promises, import_node_os2, import_node_path2, import_node_crypto2, DEFAULT_TIMEOUT_MS, DEFAULT_STATUS_TTL_MS, MAX_OUTPUT_BYTES, CodegraphAdapter;
1099
1154
  var init_codegraph = __esm({
1100
1155
  "src/codegraph.ts"() {
1101
1156
  "use strict";
@@ -1104,7 +1159,7 @@ var init_codegraph = __esm({
1104
1159
  import_promises = require("fs/promises");
1105
1160
  import_node_os2 = require("os");
1106
1161
  import_node_path2 = require("path");
1107
- import_node_crypto = require("crypto");
1162
+ import_node_crypto2 = require("crypto");
1108
1163
  DEFAULT_TIMEOUT_MS = 5e3;
1109
1164
  DEFAULT_STATUS_TTL_MS = 6e4;
1110
1165
  MAX_OUTPUT_BYTES = 5 * 1024 * 1024;
@@ -1204,7 +1259,7 @@ var init_codegraph = __esm({
1204
1259
  } catch {
1205
1260
  }
1206
1261
  (0, import_node_fs2.mkdirSync)(identityDir, { recursive: true, mode: 448 });
1207
- const generated = (0, import_node_crypto.randomUUID)();
1262
+ const generated = (0, import_node_crypto2.randomUUID)();
1208
1263
  try {
1209
1264
  (0, import_node_fs2.writeFileSync)(identityFile, `${generated}
1210
1265
  `, {
@@ -1617,10 +1672,28 @@ function errorDetail(e) {
1617
1672
  }
1618
1673
  return e instanceof Error ? e.message : String(e);
1619
1674
  }
1675
+ function conflictState(e) {
1676
+ if (!(e instanceof MindGraphError) || !e.body || typeof e.body !== "object") {
1677
+ return {};
1678
+ }
1679
+ const body = e.body;
1680
+ const lifted = {};
1681
+ for (const key of CONFLICT_STATE_KEYS) {
1682
+ if (body[key] !== void 0) lifted[key] = body[key];
1683
+ }
1684
+ return lifted;
1685
+ }
1686
+ var CONFLICT_STATE_KEYS;
1620
1687
  var init_error_detail = __esm({
1621
1688
  "src/error-detail.ts"() {
1622
1689
  "use strict";
1623
1690
  init_dist();
1691
+ CONFLICT_STATE_KEYS = [
1692
+ "current_version",
1693
+ "current_epoch",
1694
+ "lease_expires_at",
1695
+ "lease_owner_agent_id"
1696
+ ];
1624
1697
  }
1625
1698
  });
1626
1699
 
@@ -2144,7 +2217,7 @@ function fileDescriptor(root, absolute) {
2144
2217
  const content = import_node_fs5.default.readFileSync(absolute, "utf8");
2145
2218
  return {
2146
2219
  logical_path: import_node_path5.default.relative(root, absolute).split(import_node_path5.default.sep).join("/"),
2147
- content_hash: import_node_crypto4.default.createHash("sha256").update(content).digest("hex"),
2220
+ content_hash: import_node_crypto5.default.createHash("sha256").update(content).digest("hex"),
2148
2221
  bytes: Buffer.byteLength(content),
2149
2222
  content
2150
2223
  };
@@ -2252,11 +2325,11 @@ async function scanRepository(remote, repository, requested, provider, agentId)
2252
2325
  files: results
2253
2326
  };
2254
2327
  }
2255
- var import_node_crypto4, import_node_fs5, import_node_path5, ok2, err2, SYNC_TOOL;
2328
+ var import_node_crypto5, import_node_fs5, import_node_path5, ok2, err2, SYNC_TOOL;
2256
2329
  var init_sync_tool = __esm({
2257
2330
  "src/sync-tool.ts"() {
2258
2331
  "use strict";
2259
- import_node_crypto4 = __toESM(require("crypto"));
2332
+ import_node_crypto5 = __toESM(require("crypto"));
2260
2333
  import_node_fs5 = __toESM(require("fs"));
2261
2334
  import_node_path5 = __toESM(require("path"));
2262
2335
  init_codegraph();
@@ -2339,6 +2412,361 @@ var init_sync_tool = __esm({
2339
2412
  }
2340
2413
  });
2341
2414
 
2415
+ // src/render.ts
2416
+ function obj(value) {
2417
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
2418
+ }
2419
+ function str(value) {
2420
+ return typeof value === "string" && value ? value : void 0;
2421
+ }
2422
+ function num(value) {
2423
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
2424
+ }
2425
+ function clip(value, max, state) {
2426
+ const text = str(value);
2427
+ if (!text) return void 0;
2428
+ if (text.length <= max) return text;
2429
+ state.bounded = true;
2430
+ return `${text.slice(0, max - 1)}\u2026`;
2431
+ }
2432
+ function isNodeShaped(value) {
2433
+ const node = obj(value);
2434
+ return Boolean(node && str(node.uid) && (str(node.label) || str(node.node_type)));
2435
+ }
2436
+ function unwrapSearchResult(value) {
2437
+ if (isNodeShaped(value)) return value;
2438
+ const wrapper = obj(value);
2439
+ const inner = obj(wrapper?.node);
2440
+ if (inner && isNodeShaped(inner)) {
2441
+ const score = num(wrapper.score);
2442
+ return score !== void 0 ? { ...inner, retrieval_score: score } : inner;
2443
+ }
2444
+ return void 0;
2445
+ }
2446
+ function statusTag(node) {
2447
+ const tags = [];
2448
+ if (node.superseded === true) {
2449
+ tags.push(
2450
+ node.superseded_by ? `SUPERSEDED by ${str(node.superseded_by)}` : "SUPERSEDED"
2451
+ );
2452
+ }
2453
+ const truth = str(node.truth_status) ?? str(obj(node.props)?.truth_status);
2454
+ if (truth && truth !== "accepted") tags.push(`truth: ${truth}`);
2455
+ if (node.currently_valid === false) tags.push("OUTSIDE ITS VALIDITY WINDOW");
2456
+ if (node.valid_at_time === false) tags.push("NOT VALID AT THE REQUESTED TIME");
2457
+ return tags.length > 0 ? ` [${tags.join("; ")}]` : "";
2458
+ }
2459
+ function firstQuote(node, state) {
2460
+ const chunks = Array.isArray(node.source_chunks) ? node.source_chunks : [];
2461
+ for (const raw of chunks) {
2462
+ const chunk = obj(raw);
2463
+ const quote = str(chunk?.quote) ?? str(obj(chunk?.anchor)?.exact);
2464
+ if (quote) return clip(quote, QUOTE_MAX, state);
2465
+ }
2466
+ return void 0;
2467
+ }
2468
+ function sourceTag(node) {
2469
+ const docs = Array.isArray(node.source_documents) ? node.source_documents : [];
2470
+ const first = obj(docs[0]);
2471
+ const title = str(first?.title);
2472
+ if (!title) return void 0;
2473
+ const extra = docs.length > 1 ? ` (+${docs.length - 1} more)` : "";
2474
+ return `from: ${title}${extra}`;
2475
+ }
2476
+ function layer7Fields(node, state) {
2477
+ const fields = obj(obj(node.props)?.fields);
2478
+ if (!fields) return void 0;
2479
+ const pairs = Object.entries(fields).filter(([, value]) => value !== null && value !== void 0 && value !== "").map(([key, value]) => `${key}: ${Array.isArray(value) ? value.join(", ") : String(value)}`);
2480
+ if (pairs.length === 0) return void 0;
2481
+ return clip(pairs.join("; "), SUMMARY_MAX, state);
2482
+ }
2483
+ function nodeSummaryText(node, state) {
2484
+ const props = obj(node.props);
2485
+ return clip(node.summary, SUMMARY_MAX, state) ?? clip(props?.content, SUMMARY_MAX, state) ?? clip(props?.description, SUMMARY_MAX, state) ?? clip(props?.definition, SUMMARY_MAX, state);
2486
+ }
2487
+ function nodeLines(node, state) {
2488
+ const label = clip(node.label, LABEL_MAX, state) || "(unlabeled)";
2489
+ const type = str(node.node_type) || "Node";
2490
+ const confidence = num(node.confidence);
2491
+ const score = num(node.retrieval_score);
2492
+ const parenthetical = [
2493
+ type,
2494
+ confidence !== void 0 ? `confidence ${confidence}` : void 0,
2495
+ score !== void 0 ? `score ${Math.round(score * 100) / 100}` : void 0
2496
+ ].filter(Boolean).join(", ");
2497
+ const head = `- **${label}** [${str(node.uid) || "?"}] (${parenthetical})${statusTag(node)}`;
2498
+ const summary = nodeSummaryText(node, state);
2499
+ const lines = [summary && summary !== label ? `${head}: ${summary}` : head];
2500
+ const fields = layer7Fields(node, state);
2501
+ if (fields) lines.push(` - fields: ${fields}`);
2502
+ const quote = firstQuote(node, state);
2503
+ if (quote) lines.push(` - source quote: "${quote}"`);
2504
+ const source = sourceTag(node);
2505
+ if (source) lines.push(` - ${source}`);
2506
+ const believers = Array.isArray(node.believed_by) ? node.believed_by : [];
2507
+ if (believers.length > 0) {
2508
+ const rendered = believers.slice(0, 3).map((raw) => {
2509
+ const believer = obj(raw);
2510
+ const conf = num(believer?.confidence);
2511
+ return `${str(believer?.agent_label) || "?"}${conf !== void 0 ? ` (${conf})` : ""}`;
2512
+ }).join(", ");
2513
+ lines.push(` - believed by: ${rendered}`);
2514
+ }
2515
+ return lines.join("\n");
2516
+ }
2517
+ function groupedNodeSections(nodes, state, headingPrefix = "") {
2518
+ const groups = /* @__PURE__ */ new Map();
2519
+ for (const node of nodes) {
2520
+ const type = str(node.node_type) || "Node";
2521
+ const group = groups.get(type) ?? [];
2522
+ group.push(node);
2523
+ groups.set(type, group);
2524
+ }
2525
+ const sections = [];
2526
+ let first = true;
2527
+ for (const [type, group] of groups) {
2528
+ sections.push({
2529
+ heading: `${first ? headingPrefix : ""}### ${type}`,
2530
+ items: group.map((node) => nodeLines(node, state))
2531
+ });
2532
+ first = false;
2533
+ }
2534
+ return sections;
2535
+ }
2536
+ function relationshipItems(edges, labelByUid, state) {
2537
+ const lines = [];
2538
+ for (const raw of edges) {
2539
+ const edge = obj(raw);
2540
+ if (!edge) continue;
2541
+ const type = str(edge.edge_type) || "?";
2542
+ const from = str(edge.from_uid);
2543
+ const to = str(edge.to_uid);
2544
+ if (CURATION_EDGE_TYPES.has(type)) continue;
2545
+ if (!from || !to || from === to) continue;
2546
+ const fromLabel = labelByUid.get(from);
2547
+ const toLabel = labelByUid.get(to);
2548
+ if (!fromLabel || !toLabel) continue;
2549
+ lines.push(`- ${fromLabel} \u2014${type}\u2192 ${toLabel}`);
2550
+ }
2551
+ if (lines.length > RELATIONSHIP_MAX) {
2552
+ state.bounded = true;
2553
+ const extra = lines.length - RELATIONSHIP_MAX;
2554
+ return [...lines.slice(0, RELATIONSHIP_MAX), `- (+${extra} more relationships)`];
2555
+ }
2556
+ return lines;
2557
+ }
2558
+ function assemble(header, sections, state) {
2559
+ const lines = [header];
2560
+ let used = header.length;
2561
+ for (const section of sections) {
2562
+ if (section.items.length === 0 && !section.heading) continue;
2563
+ const headingCost = section.heading.length + 2;
2564
+ if (used + headingCost > RENDER_CHAR_BUDGET) {
2565
+ state.bounded = true;
2566
+ continue;
2567
+ }
2568
+ let sectionUsed = 0;
2569
+ const kept = [];
2570
+ let dropped = 0;
2571
+ for (const item of section.items) {
2572
+ if (used + headingCost + sectionUsed + item.length + 1 > RENDER_CHAR_BUDGET) {
2573
+ dropped += 1;
2574
+ continue;
2575
+ }
2576
+ kept.push(item);
2577
+ sectionUsed += item.length + 1;
2578
+ }
2579
+ if (kept.length === 0 && section.items.length > 0) {
2580
+ state.bounded = true;
2581
+ continue;
2582
+ }
2583
+ if (dropped > 0) {
2584
+ state.bounded = true;
2585
+ const marker = `(+${dropped} more not shown)`;
2586
+ kept.push(marker);
2587
+ sectionUsed += marker.length + 1;
2588
+ }
2589
+ lines.push(
2590
+ section.items.length === 0 ? section.heading : `${section.heading}
2591
+ ${kept.join("\n")}`
2592
+ );
2593
+ used += headingCost + sectionUsed;
2594
+ }
2595
+ if (state.bounded) {
2596
+ lines.push(
2597
+ '\u2026[rendered context bounded \u2014 fetch full node content by uid, or pass format: "json" for the raw response]'
2598
+ );
2599
+ }
2600
+ return lines.join("\n\n");
2601
+ }
2602
+ function policySections(payload, state) {
2603
+ const policies = Array.isArray(payload.applicable_policies) ? payload.applicable_policies : [];
2604
+ if (policies.length === 0) return [];
2605
+ const items = policies.map((raw) => obj(raw)).filter((policy) => Boolean(policy)).map((policy) => {
2606
+ const rules = Array.isArray(policy.prose_rules) ? policy.prose_rules.map(String).join("; ") : policy.prose_rules;
2607
+ return `- ${str(policy.name) || "(unnamed policy)"}: ${clip(rules, SUMMARY_MAX, state) || "(structured rules apply)"}`;
2608
+ });
2609
+ return [{ heading: "## Applicable policies", items }];
2610
+ }
2611
+ function hiddenSections(payload) {
2612
+ const hidden = payload.hidden_item_count;
2613
+ if (hidden === void 0 || hidden === null) return [];
2614
+ return [
2615
+ {
2616
+ heading: `Note: ${hidden} item(s) were hidden by access scoping.`,
2617
+ items: []
2618
+ }
2619
+ ];
2620
+ }
2621
+ function renderContext(response) {
2622
+ const payload = obj(response);
2623
+ if (!payload) return void 0;
2624
+ const graph = obj(payload.graph);
2625
+ const rawNodes = Array.isArray(graph?.nodes) ? graph.nodes : [];
2626
+ const shaped = rawNodes.filter(isNodeShaped);
2627
+ const nodes = shaped.filter((node) => !INFRA_NODE_TYPES.has(str(node.node_type) || ""));
2628
+ const infraOnly = shaped.length - nodes.length;
2629
+ const articles = Array.isArray(payload.articles) ? payload.articles : [];
2630
+ const chunks = Array.isArray(payload.chunks) ? payload.chunks : [];
2631
+ const state = { bounded: false };
2632
+ const sections = [];
2633
+ if (articles.length > 0) {
2634
+ sections.push({
2635
+ heading: "## Knowledge articles",
2636
+ items: articles.map((raw) => obj(raw)).filter((article) => Boolean(article)).map(
2637
+ (article) => `### ${str(article.label) || "(untitled)"} [${str(article.uid) || "?"}]
2638
+ ${clip(article.content, ARTICLE_MAX, state) || ""}`
2639
+ )
2640
+ });
2641
+ }
2642
+ if (nodes.length > 0) {
2643
+ sections.push(...groupedNodeSections(nodes, state, "## Knowledge graph\n\n"));
2644
+ const labelByUid = /* @__PURE__ */ new Map();
2645
+ for (const node of nodes) labelByUid.set(str(node.uid), str(node.label) || "?");
2646
+ const relationships = relationshipItems(
2647
+ Array.isArray(graph?.edges) ? graph.edges : [],
2648
+ labelByUid,
2649
+ state
2650
+ );
2651
+ if (relationships.length > 0) {
2652
+ sections.push({ heading: "## Relationships", items: relationships });
2653
+ }
2654
+ } else if (infraOnly > 0) {
2655
+ sections.push({
2656
+ heading: `Note: ${infraOnly} document/chunk node(s) matched \u2014 use action "document_index" or include_chunks for their content.`,
2657
+ items: []
2658
+ });
2659
+ }
2660
+ if (chunks.length > 0) {
2661
+ sections.push({
2662
+ heading: "## Source excerpts",
2663
+ items: chunks.map((raw) => obj(raw)).filter((chunk) => Boolean(chunk)).map(
2664
+ (chunk) => `- [${str(chunk.chunk_uid) || "?"}] from "${str(chunk.document_title) || "unknown document"}": "${clip(chunk.content, CHUNK_MAX, state) || ""}"`
2665
+ )
2666
+ });
2667
+ }
2668
+ sections.push(...policySections(payload, state));
2669
+ sections.push(...hiddenSections(payload));
2670
+ if (sections.length === 0) {
2671
+ return "No results found for this search.";
2672
+ }
2673
+ if (nodes.length === 0 && articles.length === 0 && chunks.length === 0) {
2674
+ return assemble("# Retrieved context\n\nNo graph results matched this search.", sections, state);
2675
+ }
2676
+ return assemble("# Retrieved context", sections, state);
2677
+ }
2678
+ function renderNodeList(response, title, limit) {
2679
+ let candidates = [];
2680
+ if (Array.isArray(response)) {
2681
+ candidates = response;
2682
+ } else {
2683
+ const payload = obj(response);
2684
+ if (!payload) return void 0;
2685
+ for (const key of ["nodes", "results", "items", "goals", "questions", "claims", "preferences", "documents"]) {
2686
+ const candidate = payload[key];
2687
+ if (Array.isArray(candidate) && candidate.some((entry) => unwrapSearchResult(entry))) {
2688
+ candidates = candidate;
2689
+ break;
2690
+ }
2691
+ }
2692
+ }
2693
+ let nodes = candidates.map(unwrapSearchResult).filter((node) => Boolean(node));
2694
+ if (nodes.length === 0) return void 0;
2695
+ const total = nodes.length;
2696
+ const state = { bounded: false };
2697
+ if (limit !== void 0 && limit > 0 && nodes.length > limit) {
2698
+ nodes = nodes.slice(0, limit);
2699
+ state.bounded = true;
2700
+ }
2701
+ const sections = groupedNodeSections(nodes, state);
2702
+ const shown = nodes.length;
2703
+ return assemble(
2704
+ `# ${title} (${shown === total ? total : `${shown} of ${total}`})`,
2705
+ sections,
2706
+ state
2707
+ );
2708
+ }
2709
+ function renderTraversal(response) {
2710
+ const payload = obj(response);
2711
+ if (!payload) return void 0;
2712
+ const container = obj(payload.graph) ?? payload;
2713
+ const rawNodes = Array.isArray(container.nodes) ? container.nodes : [];
2714
+ const nodes = rawNodes.filter(isNodeShaped);
2715
+ const paths = Array.isArray(payload.paths) ? payload.paths : [];
2716
+ if (nodes.length === 0 && paths.length === 0) return void 0;
2717
+ const state = { bounded: false };
2718
+ const sections = [];
2719
+ const labelByUid = /* @__PURE__ */ new Map();
2720
+ for (const node of nodes) labelByUid.set(str(node.uid), str(node.label) || "?");
2721
+ if (paths.length > 0) {
2722
+ const items = paths.map((raw) => obj(raw)).filter((path6) => Boolean(path6)).map((path6, index) => {
2723
+ const uids = Array.isArray(path6.node_uids) ? path6.node_uids.map(String) : [];
2724
+ const labels = Array.isArray(path6.labels) ? path6.labels.map(String) : Array.isArray(path6.nodes) ? path6.nodes.map((hop) => {
2725
+ const hopNode = obj(hop);
2726
+ return str(hopNode?.label) || labelByUid.get(str(hopNode?.uid) || "") || str(hop) || "?";
2727
+ }) : [];
2728
+ const route = labels.length > 0 ? labels.join(" \u2192 ") : uids.join(" \u2192 ") || "(path)";
2729
+ const cost = num(path6.cost) ?? num(path6.path_cost);
2730
+ const confidence = num(path6.path_confidence);
2731
+ const meta = [
2732
+ cost !== void 0 ? `cost ${cost}` : void 0,
2733
+ confidence !== void 0 ? `confidence ${confidence}` : void 0
2734
+ ].filter(Boolean);
2735
+ const head = `${index + 1}. ${route}${meta.length > 0 ? ` (${meta.join(", ")})` : ""}`;
2736
+ return labels.length > 0 && uids.length > 0 ? `${head}
2737
+ uids: ${uids.join(" \u2192 ")}` : head;
2738
+ });
2739
+ sections.push({ heading: "## Paths", items });
2740
+ }
2741
+ if (nodes.length > 0) {
2742
+ sections.push(...groupedNodeSections(nodes, state, "## Nodes\n\n"));
2743
+ const relationships = relationshipItems(
2744
+ Array.isArray(container.edges) ? container.edges : [],
2745
+ labelByUid,
2746
+ state
2747
+ );
2748
+ if (relationships.length > 0) {
2749
+ sections.push({ heading: "## Relationships", items: relationships });
2750
+ }
2751
+ }
2752
+ return assemble("# Graph traversal", sections, state);
2753
+ }
2754
+ var LABEL_MAX, SUMMARY_MAX, QUOTE_MAX, ARTICLE_MAX, CHUNK_MAX, RELATIONSHIP_MAX, RENDER_CHAR_BUDGET, CURATION_EDGE_TYPES, INFRA_NODE_TYPES;
2755
+ var init_render = __esm({
2756
+ "src/render.ts"() {
2757
+ "use strict";
2758
+ LABEL_MAX = 200;
2759
+ SUMMARY_MAX = 400;
2760
+ QUOTE_MAX = 180;
2761
+ ARTICLE_MAX = 6e3;
2762
+ CHUNK_MAX = 6e3;
2763
+ RELATIONSHIP_MAX = 40;
2764
+ RENDER_CHAR_BUDGET = 24e3;
2765
+ CURATION_EDGE_TYPES = /* @__PURE__ */ new Set(["POSSIBLE_DUPLICATE", "PossibleDuplicate"]);
2766
+ INFRA_NODE_TYPES = /* @__PURE__ */ new Set(["Chunk", "Document", "Article"]);
2767
+ }
2768
+ });
2769
+
2342
2770
  // src/tools.ts
2343
2771
  function toolsForProfile(profile = process.env.MINDGRAPH_PROFILE || "general") {
2344
2772
  if (profile === "coding") return TOOLS;
@@ -2405,6 +2833,18 @@ async function handleTool(client2, name, args) {
2405
2833
  ].includes(action);
2406
2834
  return attach ? await attachCodeRefsToToolResult(client2, result, args) : result;
2407
2835
  } catch (e) {
2836
+ const lifted = conflictState(e);
2837
+ if (Object.keys(lifted).length > 0) {
2838
+ return {
2839
+ content: [
2840
+ {
2841
+ type: "text",
2842
+ text: JSON.stringify({ error: errorDetail(e), ...lifted })
2843
+ }
2844
+ ],
2845
+ isError: true
2846
+ };
2847
+ }
2408
2848
  return err3(errorDetail(e));
2409
2849
  }
2410
2850
  }
@@ -2920,6 +3360,7 @@ async function handlePlan(client2, args) {
2920
3360
  async function handleRetrieve(client2, args) {
2921
3361
  const {
2922
3362
  action,
3363
+ format,
2923
3364
  query,
2924
3365
  start_uid,
2925
3366
  end_uid,
@@ -2940,12 +3381,24 @@ async function handleRetrieve(client2, args) {
2940
3381
  graph_max_depth,
2941
3382
  confidence_min,
2942
3383
  salience_min,
3384
+ article_limit,
3385
+ valid_at,
3386
+ created_after,
3387
+ created_before,
3388
+ explain,
3389
+ k,
3390
+ max_cost,
2943
3391
  agent_id
2944
3392
  } = args;
3393
+ const rendered = (raw, render) => {
3394
+ if (format === "json") return ok3(raw);
3395
+ const text = render(raw);
3396
+ return text !== void 0 ? { content: [{ type: "text", text }] } : ok3(raw);
3397
+ };
2945
3398
  switch (action) {
2946
3399
  case "context":
2947
3400
  if (!query) return err3("query is required for context retrieval");
2948
- return ok3(
3401
+ return rendered(
2949
3402
  await client2.retrieveContext({
2950
3403
  query,
2951
3404
  node_limit: limit,
@@ -2954,110 +3407,139 @@ async function handleRetrieve(client2, args) {
2954
3407
  chunk_limit: include_chunks ? limit ?? 5 : 0,
2955
3408
  // Scoped by default, never walled: the coding profile drops the
2956
3409
  // document/article leg (the "Bob Work" class of noise) unless the
2957
- // model opts in for a genuinely document-shaped question.
2958
- ...process.env.MINDGRAPH_PROFILE === "coding" && !include_documents ? { article_limit: 0 } : {},
3410
+ // model opts in for a genuinely document-shaped question. Any
3411
+ // profile can drop it with include_documents: false (previously a
3412
+ // silent no-op outside coding). An EXPLICIT article_limit is at
3413
+ // least as clear an opt-in as include_documents:true — it always
3414
+ // wins over profile defaults (a coding-profile model reaching for
3415
+ // the advertised knob must not get a silent no-op).
3416
+ ...article_limit !== void 0 ? { article_limit } : include_documents === false || process.env.MINDGRAPH_PROFILE === "coding" && !include_documents ? { article_limit: 0 } : {},
3417
+ valid_at,
2959
3418
  include_graph,
2960
3419
  graph_expansion_limit: graph_expansion_limit ?? 3,
2961
3420
  graph_max_depth: graph_max_depth ?? 2
2962
- })
3421
+ }),
3422
+ renderContext
2963
3423
  );
2964
3424
  case "text":
2965
3425
  if (!query) return err3("query is required for text search");
2966
- return ok3(
3426
+ return rendered(
2967
3427
  await client2.retrieve({
2968
3428
  action: "text",
2969
3429
  query,
2970
3430
  limit,
2971
3431
  node_types,
2972
3432
  layer,
3433
+ explain,
2973
3434
  agent_id
2974
- })
3435
+ }),
3436
+ (raw) => renderNodeList(raw, `Text search: ${query}`)
2975
3437
  );
2976
3438
  case "semantic":
2977
3439
  if (!query) return err3("query is required for semantic search");
2978
- return ok3(
3440
+ return rendered(
2979
3441
  await client2.retrieve({
2980
3442
  action: "semantic",
2981
3443
  query,
2982
3444
  k: limit,
2983
3445
  node_types,
2984
3446
  layer,
3447
+ explain,
2985
3448
  agent_id
2986
- })
3449
+ }),
3450
+ (raw) => renderNodeList(raw, `Semantic search: ${query}`)
2987
3451
  );
2988
3452
  case "hybrid":
2989
3453
  if (!query) return err3("query is required for hybrid search");
2990
- return ok3(
3454
+ return rendered(
2991
3455
  await client2.retrieve({
2992
3456
  action: "hybrid",
2993
3457
  query,
2994
3458
  k: limit,
2995
3459
  node_types,
2996
3460
  layer,
3461
+ explain,
2997
3462
  agent_id
2998
- })
3463
+ }),
3464
+ // With explain, per-leg scoring must survive — raw JSON only.
3465
+ explain ? () => void 0 : (raw) => renderNodeList(raw, `Hybrid search: ${query}`)
2999
3466
  );
3000
- // Structured queries
3467
+ // Structured queries. These go through the generic /retrieve action
3468
+ // dispatch rather than the SDK's no-argument convenience methods — the
3469
+ // conveniences silently dropped `limit`/`offset` (the tool advertised
3470
+ // them; five actions ignored them).
3001
3471
  case "active_goals":
3002
- return ok3(await client2.getGoals());
3003
3472
  case "open_questions":
3004
- return ok3(await client2.getOpenQuestions());
3005
3473
  case "weak_claims":
3006
- return ok3(await client2.getWeakClaims());
3007
3474
  case "pending_approvals":
3008
- return ok3(await client2.getPendingApprovals());
3009
3475
  case "unresolved_contradictions":
3010
- return ok3(await client2.getContradictions());
3476
+ return rendered(
3477
+ await client2.retrieve({
3478
+ action,
3479
+ limit,
3480
+ offset,
3481
+ agent_id
3482
+ }),
3483
+ (raw) => renderNodeList(raw, action.replace(/_/g, " "), limit)
3484
+ );
3011
3485
  case "stale_derivations":
3012
- return ok3(
3486
+ return rendered(
3013
3487
  await client2.retrieve({
3014
3488
  action: "stale_derivations",
3015
3489
  limit,
3016
3490
  offset
3017
- })
3491
+ }),
3492
+ (raw) => renderNodeList(raw, "stale derivations", limit)
3018
3493
  );
3019
3494
  case "preferences":
3020
- return ok3(
3495
+ return rendered(
3021
3496
  await client2.retrieve({
3022
3497
  action: "preferences",
3023
3498
  ...query ? { query, k: limit } : {},
3024
3499
  limit,
3025
3500
  agent_id
3026
- })
3501
+ }),
3502
+ (raw) => renderNodeList(raw, "Preferences", limit)
3027
3503
  );
3028
3504
  case "layer":
3029
3505
  if (!layer) return err3("layer is required for layer retrieval");
3030
- return ok3(
3506
+ return rendered(
3031
3507
  await client2.retrieve({
3032
3508
  action: "layer",
3033
3509
  layer,
3034
3510
  limit,
3511
+ node_types,
3035
3512
  agent_id
3036
- })
3513
+ }),
3514
+ (raw) => renderNodeList(raw, `Layer: ${layer}`, limit)
3037
3515
  );
3038
3516
  case "recent":
3039
- return ok3(
3517
+ return rendered(
3040
3518
  await client2.retrieve({
3041
3519
  action: "recent",
3042
3520
  limit,
3043
3521
  node_types,
3044
3522
  confidence_min,
3045
3523
  salience_min,
3524
+ created_after,
3525
+ created_before,
3046
3526
  agent_id
3047
- })
3527
+ }),
3528
+ (raw) => renderNodeList(raw, "Recently ingested", limit)
3048
3529
  );
3049
3530
  // Document inventory
3050
3531
  case "document_index":
3051
- return ok3(
3532
+ return rendered(
3052
3533
  await client2.getNodes({
3053
3534
  node_type: "Document",
3054
3535
  limit: limit ?? 100
3055
- })
3536
+ }),
3537
+ (raw) => renderNodeList(raw, "Ingested documents")
3056
3538
  );
3057
3539
  // Graph traversal
3058
3540
  case "chain":
3059
3541
  if (!start_uid) return err3("start_uid is required for chain traversal");
3060
- return ok3(
3542
+ return rendered(
3061
3543
  await client2.traverse({
3062
3544
  action: "chain",
3063
3545
  start_uid,
@@ -3065,11 +3547,12 @@ async function handleRetrieve(client2, args) {
3065
3547
  exclude_edge_types,
3066
3548
  include_provenance,
3067
3549
  max_nodes
3068
- })
3550
+ }),
3551
+ renderTraversal
3069
3552
  );
3070
3553
  case "neighborhood":
3071
3554
  if (!start_uid) return err3("start_uid is required for neighborhood traversal");
3072
- return ok3(
3555
+ return rendered(
3073
3556
  await client2.traverse({
3074
3557
  action: "neighborhood",
3075
3558
  start_uid,
@@ -3079,12 +3562,13 @@ async function handleRetrieve(client2, args) {
3079
3562
  exclude_edge_types,
3080
3563
  include_provenance,
3081
3564
  max_nodes
3082
- })
3565
+ }),
3566
+ renderTraversal
3083
3567
  );
3084
3568
  case "path":
3085
3569
  if (!start_uid) return err3("start_uid is required for path traversal");
3086
3570
  if (!end_uid) return err3("end_uid is required for path traversal");
3087
- return ok3(
3571
+ return rendered(
3088
3572
  await client2.traverse({
3089
3573
  action: "path",
3090
3574
  start_uid,
@@ -3095,11 +3579,12 @@ async function handleRetrieve(client2, args) {
3095
3579
  exclude_edge_types,
3096
3580
  include_provenance,
3097
3581
  max_nodes
3098
- })
3582
+ }),
3583
+ renderTraversal
3099
3584
  );
3100
3585
  case "subgraph":
3101
3586
  if (!start_uid) return err3("start_uid is required for subgraph traversal");
3102
- return ok3(
3587
+ return rendered(
3103
3588
  await client2.traverse({
3104
3589
  action: "subgraph",
3105
3590
  start_uid,
@@ -3109,7 +3594,24 @@ async function handleRetrieve(client2, args) {
3109
3594
  exclude_edge_types,
3110
3595
  include_provenance,
3111
3596
  max_nodes
3112
- })
3597
+ }),
3598
+ renderTraversal
3599
+ );
3600
+ case "top_k_paths":
3601
+ if (!start_uid) return err3("start_uid is required for top_k_paths");
3602
+ if (!end_uid) return err3("end_uid is required for top_k_paths");
3603
+ return rendered(
3604
+ await client2.traverse({
3605
+ action: "top_k_paths",
3606
+ start_uid,
3607
+ end_uid,
3608
+ k,
3609
+ max_hops: max_depth,
3610
+ max_cost,
3611
+ edge_types,
3612
+ exclude_edge_types
3613
+ }),
3614
+ renderTraversal
3113
3615
  );
3114
3616
  default:
3115
3617
  return err3(`Unknown retrieve action: ${action}`);
@@ -3355,6 +3857,7 @@ var init_tools = __esm({
3355
3857
  init_code_tool();
3356
3858
  init_codegraph();
3357
3859
  init_sync_tool();
3860
+ init_render();
3358
3861
  TOOLS = [
3359
3862
  {
3360
3863
  name: "mindgraph_capture",
@@ -3928,10 +4431,16 @@ var init_tools = __esm({
3928
4431
  "neighborhood",
3929
4432
  "path",
3930
4433
  "subgraph",
4434
+ "top_k_paths",
3931
4435
  "document_index"
3932
4436
  ],
3933
4437
  description: "Retrieval or traversal strategy"
3934
4438
  },
4439
+ format: {
4440
+ type: "string",
4441
+ enum: ["text", "json"],
4442
+ description: "Output format (default 'text'): a compact rendered context block \u2014 labels, uids, summaries, status tags, relationships. Pass 'json' for the raw server response (full props, source-chunk offsets, retrieval metadata)."
4443
+ },
3935
4444
  query: {
3936
4445
  type: "string",
3937
4446
  description: "Search query \u2014 for 'text'/'context': 1-3 BM25 keywords, drop filler words. For 'semantic'/'hybrid': natural language is OK since it uses vector similarity."
@@ -3991,7 +4500,35 @@ var init_tools = __esm({
3991
4500
  },
3992
4501
  include_documents: {
3993
4502
  type: "boolean",
3994
- description: "Include ingested documents and wiki articles in context results. The coding profile defaults these OFF so engineering questions aren't answered from unrelated ingested content; pass true when the question is about a document, spec, or article."
4503
+ description: "Include ingested documents and wiki articles in context results. The coding profile defaults these OFF so engineering questions aren't answered from unrelated ingested content; pass true when the question is about a document, spec, or article \u2014 or false in any profile to drop the article leg."
4504
+ },
4505
+ article_limit: {
4506
+ type: "number",
4507
+ description: "Max wiki articles in context results (default 3; 0 disables the article leg). An explicit value overrides profile defaults, including the coding profile's articles-off default."
4508
+ },
4509
+ valid_at: {
4510
+ type: "string",
4511
+ description: "ISO-8601 date (e.g. '2021-06') for context: nodes with validity windows are annotated as of this date instead of today"
4512
+ },
4513
+ created_after: {
4514
+ type: "number",
4515
+ description: "Unix seconds \u2014 'recent' action: only nodes ingested after this time"
4516
+ },
4517
+ created_before: {
4518
+ type: "number",
4519
+ description: "Unix seconds \u2014 'recent' action: only nodes ingested before this time"
4520
+ },
4521
+ explain: {
4522
+ type: "boolean",
4523
+ description: "hybrid action: include per-leg retrieval scoring (legs) in the response"
4524
+ },
4525
+ k: {
4526
+ type: "number",
4527
+ description: "top_k_paths action: number of cheapest paths (default 3, cap 25)"
4528
+ },
4529
+ max_cost: {
4530
+ type: "number",
4531
+ description: "top_k_paths action: optional cost ceiling for returned paths"
3995
4532
  },
3996
4533
  include_chunks: {
3997
4534
  type: "boolean",
@@ -4822,7 +5359,7 @@ async function main() {
4822
5359
  const transport = new import_stdio.StdioServerTransport();
4823
5360
  await server.connect(transport);
4824
5361
  console.error(
4825
- `MindGraph MCP server v0.7.1 running on stdio (${BASE_URL})`
5362
+ `MindGraph MCP server v${"0.16.0"} running on stdio (${BASE_URL})`
4826
5363
  );
4827
5364
  }
4828
5365
  var import_server, import_stdio, import_types, API_KEY, BASE_URL, AGENT_ID, ORG_ID, PROFILE, HARNESS, client, INSTRUCTIONS, server, GOVERNANCE_CONFIG, PROMPTS;
@@ -4833,13 +5370,14 @@ var init_src = __esm({
4833
5370
  import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
4834
5371
  import_types = require("@modelcontextprotocol/sdk/types.js");
4835
5372
  init_dist();
5373
+ init_hook_env();
4836
5374
  init_tools();
4837
5375
  init_generated_tools();
4838
5376
  init_governance();
4839
5377
  init_resources();
4840
5378
  API_KEY = process.env.MINDGRAPH_API_KEY;
4841
5379
  BASE_URL = process.env.MINDGRAPH_BASE_URL || "https://api.mindgraph.cloud";
4842
- AGENT_ID = process.env.MINDGRAPH_AGENT_ID || "mcp";
5380
+ AGENT_ID = process.env.MINDGRAPH_AGENT_ID || loadHookEnv().agentId || stableAgentId();
4843
5381
  ORG_ID = process.env.MINDGRAPH_ORG_ID;
4844
5382
  PROFILE = process.env.MINDGRAPH_PROFILE || "general";
4845
5383
  HARNESS = process.env.MINDGRAPH_HARNESS || "generic";
@@ -4961,7 +5499,7 @@ Capture knowledge when the user shares something worth remembering:
4961
5499
  server = new import_server.Server(
4962
5500
  {
4963
5501
  name: "mindgraph",
4964
- version: "0.14.9"
5502
+ version: "0.16.0"
4965
5503
  },
4966
5504
  {
4967
5505
  capabilities: {
@@ -5182,46 +5720,10 @@ ${conversation}`
5182
5720
  var fs5 = __toESM(require("fs"));
5183
5721
  var path5 = __toESM(require("path"));
5184
5722
  var os4 = __toESM(require("os"));
5185
- var crypto4 = __toESM(require("crypto"));
5723
+ var crypto5 = __toESM(require("crypto"));
5186
5724
  var import_child_process = require("child_process");
5187
5725
  init_dist();
5188
-
5189
- // src/hook-env.ts
5190
- var import_node_fs = __toESM(require("fs"));
5191
- var import_node_os = __toESM(require("os"));
5192
- var import_node_path = __toESM(require("path"));
5193
- function hookEnvPath(dir) {
5194
- return import_node_path.default.join(dir || import_node_path.default.join(import_node_os.default.homedir(), ".mindgraph"), "hooks.json");
5195
- }
5196
- function loadHookEnv(dir) {
5197
- try {
5198
- const raw = import_node_fs.default.readFileSync(hookEnvPath(dir), "utf8");
5199
- const parsed = JSON.parse(raw);
5200
- return {
5201
- baseUrl: typeof parsed.baseUrl === "string" ? parsed.baseUrl : void 0,
5202
- apiKey: typeof parsed.apiKey === "string" ? parsed.apiKey : void 0,
5203
- agentId: typeof parsed.agentId === "string" ? parsed.agentId : void 0
5204
- };
5205
- } catch {
5206
- return {};
5207
- }
5208
- }
5209
- function saveHookEnv(env, dir) {
5210
- const file = hookEnvPath(dir);
5211
- import_node_fs.default.mkdirSync(import_node_path.default.dirname(file), { recursive: true });
5212
- const existing = loadHookEnv(dir);
5213
- const merged = {
5214
- baseUrl: env.baseUrl ?? existing.baseUrl,
5215
- apiKey: env.apiKey ?? existing.apiKey,
5216
- agentId: env.agentId ?? existing.agentId
5217
- };
5218
- import_node_fs.default.writeFileSync(file, `${JSON.stringify(merged, null, 2)}
5219
- `, {
5220
- mode: 384
5221
- });
5222
- import_node_fs.default.chmodSync(file, 384);
5223
- return file;
5224
- }
5726
+ init_hook_env();
5225
5727
 
5226
5728
  // src/cli-args.ts
5227
5729
  function parseArgs(argv) {
@@ -5292,12 +5794,14 @@ function classifyMcpAddFailure(e) {
5292
5794
  }
5293
5795
 
5294
5796
  // src/hook-core.ts
5295
- var import_node_crypto2 = __toESM(require("crypto"));
5797
+ var import_node_crypto3 = __toESM(require("crypto"));
5296
5798
  var import_node_fs3 = __toESM(require("fs"));
5297
5799
  var import_node_os3 = __toESM(require("os"));
5298
5800
  var import_node_path3 = __toESM(require("path"));
5299
5801
  var import_node_child_process2 = require("child_process");
5300
5802
  init_codegraph();
5803
+ init_code_tool();
5804
+ init_hook_env();
5301
5805
  var NOOP = { kind: "noop" };
5302
5806
  function defaultLedger(sessionId, options) {
5303
5807
  return {
@@ -5319,7 +5823,7 @@ function runtimeDir(options) {
5319
5823
  }
5320
5824
  function ledgerPath(sessionId, options) {
5321
5825
  const harness = "harness" in options ? options.harness : "claude-code";
5322
- const key = import_node_crypto2.default.createHash("sha256").update(`${harness}\0${sessionId}`).digest("hex");
5826
+ const key = import_node_crypto3.default.createHash("sha256").update(`${harness}\0${sessionId}`).digest("hex");
5323
5827
  return import_node_path3.default.join(runtimeDir(options), `${key}.json`);
5324
5828
  }
5325
5829
  function waitBriefly(ms) {
@@ -5334,14 +5838,14 @@ function withLedger(sessionId, options, update) {
5334
5838
  }
5335
5839
  const lock = `${file}.lock`;
5336
5840
  let lockFd;
5337
- for (let attempt = 0; attempt < 50; attempt++) {
5841
+ for (let attempt = 0; attempt < 400; attempt++) {
5338
5842
  try {
5339
5843
  lockFd = import_node_fs3.default.openSync(lock, "wx", 384);
5340
5844
  break;
5341
5845
  } catch (error) {
5342
5846
  if (error.code !== "EEXIST") throw error;
5343
5847
  try {
5344
- if (Date.now() - import_node_fs3.default.statSync(lock).mtimeMs > 1e4) import_node_fs3.default.unlinkSync(lock);
5848
+ if (Date.now() - import_node_fs3.default.statSync(lock).mtimeMs > 3e3) import_node_fs3.default.unlinkSync(lock);
5345
5849
  } catch {
5346
5850
  }
5347
5851
  waitBriefly(10);
@@ -5361,7 +5865,7 @@ function withLedger(sessionId, options, update) {
5361
5865
  }
5362
5866
  const result = update(ledger);
5363
5867
  ledger.updatedAt = Date.now();
5364
- const temporary = `${file}.${process.pid}.${import_node_crypto2.default.randomUUID()}.tmp`;
5868
+ const temporary = `${file}.${process.pid}.${import_node_crypto3.default.randomUUID()}.tmp`;
5365
5869
  import_node_fs3.default.writeFileSync(temporary, `${JSON.stringify(ledger)}
5366
5870
  `, { mode: 384 });
5367
5871
  import_node_fs3.default.renameSync(temporary, file);
@@ -5396,17 +5900,13 @@ function worktreeState(cwd) {
5396
5900
  const status = gitValue(cwd, ["status", "--porcelain"]);
5397
5901
  return status === void 0 ? "unknown" : status ? "dirty" : "clean";
5398
5902
  }
5399
- async function invocationContext(input, options) {
5400
- const repository = await new CodegraphAdapter({
5401
- cwd: input.cwd,
5402
- workspaceFile: options.workspaceFile
5403
- }).resolveRepository();
5903
+ function invocationContext(input, options, repoId) {
5404
5904
  return {
5405
5905
  harness: options.harness,
5406
5906
  harnessSessionId: input.session_id,
5407
5907
  harnessTurnId: input.turn_id,
5408
5908
  cwd: input.cwd,
5409
- repoId: repository.repoId,
5909
+ repoId,
5410
5910
  branch: gitValue(input.cwd, ["branch", "--show-current"]),
5411
5911
  commit: gitValue(input.cwd, ["rev-parse", "HEAD"]),
5412
5912
  worktreeState: worktreeState(input.cwd),
@@ -5414,6 +5914,9 @@ async function invocationContext(input, options) {
5414
5914
  injectedBy: "hook"
5415
5915
  };
5416
5916
  }
5917
+ function resolveAgentId(options) {
5918
+ return options.agentId || process.env.MINDGRAPH_AGENT_ID || stableAgentId();
5919
+ }
5417
5920
  function object(value) {
5418
5921
  return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
5419
5922
  }
@@ -5441,56 +5944,198 @@ function number(value) {
5441
5944
  function string(value) {
5442
5945
  return typeof value === "string" && value ? value : void 0;
5443
5946
  }
5444
- function boundedBrief(brief) {
5445
- const encoded = JSON.stringify(brief, null, 2);
5446
- const capped = encoded.length <= 9e3 ? encoded : `${encoded.slice(0, 8900)}
5947
+ var BRIEF_CHAR_BUDGET = 9e3;
5948
+ function renderBrief(brief, meta) {
5949
+ let bounded = false;
5950
+ const clipTracked = (value, max) => {
5951
+ const text = string(value);
5952
+ if (!text) return void 0;
5953
+ if (text.length <= max) return text;
5954
+ bounded = true;
5955
+ return `${text.slice(0, max - 1)}\u2026`;
5956
+ };
5957
+ const nodeLine = (value, summaryMax) => {
5958
+ const node = object(value);
5959
+ if (!node) return void 0;
5960
+ const label = clipTracked(node.label, 160) || "(unlabeled)";
5961
+ const summary = clipTracked(node.summary, summaryMax);
5962
+ const uid = string(node.uid);
5963
+ return `- ${label}${uid ? ` [${uid}]` : ""}${summary && summary !== label ? ` \u2014 ${summary}` : ""}`;
5964
+ };
5965
+ const lines = [
5966
+ "MindGraph durable work brief (authoritative across sessions/harnesses):"
5967
+ ];
5968
+ if (meta.scopeNote) lines.push(meta.scopeNote);
5969
+ const task = object(brief?.task);
5970
+ const scopedOut = number(brief?.scoped_out_tasks);
5971
+ if (!task) {
5972
+ lines.push("No durable task is eligible for this session's scope.");
5973
+ if (scopedOut) {
5974
+ lines.push(
5975
+ `${scopedOut} durable task(s) exist outside this repository scope \u2014 resume one explicitly by task_uid if that is intended.`
5976
+ );
5977
+ }
5978
+ } else {
5979
+ const props = object(task.props);
5980
+ const details = [
5981
+ `status ${string(props?.status) || "unknown"}`,
5982
+ string(props?.priority) ? `priority ${string(props?.priority)}` : void 0,
5983
+ number(task.version) !== void 0 ? `v${number(task.version)}` : void 0
5984
+ ].filter(Boolean);
5985
+ lines.push(
5986
+ `Task: ${clipTracked(task.label, 200) || "(unlabeled)"} [${string(task.uid) || "?"}] \u2014 ${details.join(", ")}`
5987
+ );
5988
+ const description = clipTracked(props?.description, 400);
5989
+ if (description) lines.push(` ${description}`);
5990
+ const reason = string(brief?.selection_reason);
5991
+ if (reason) lines.push(`Selection: ${reason}`);
5992
+ const lease = object(brief?.claim) || object(brief?.lease);
5993
+ if (meta.claimed) {
5994
+ const epoch = number(lease?.lease_epoch);
5995
+ lines.push(
5996
+ `Claimed: yes${epoch !== void 0 ? ` (lease epoch ${epoch})` : ""} \u2014 this session holds the lease.`
5997
+ );
5998
+ } else if (meta.claimFailed) {
5999
+ lines.push(
6000
+ "Claimed: no \u2014 the claim attempt failed (another session may hold the lease). Treat as context; claim_task explicitly before mutating it."
6001
+ );
6002
+ } else {
6003
+ lines.push(
6004
+ "Claimed: no \u2014 backlog context, not owned. claim_task deliberately before starting this work."
6005
+ );
6006
+ }
6007
+ const goal = nodeLine(brief?.goal, 200);
6008
+ if (goal) lines.push(`Goal:
6009
+ ${goal}`);
6010
+ const steps = Array.isArray(brief?.next_steps) ? brief.next_steps : [];
6011
+ if (steps.length > 5) bounded = true;
6012
+ const stepLines = steps.slice(0, 5).map((step) => nodeLine(step, 160)).filter(Boolean);
6013
+ if (stepLines.length) lines.push(`Next steps:
6014
+ ${stepLines.join("\n")}`);
6015
+ const blockers = Array.isArray(brief?.blockers) ? brief.blockers : [];
6016
+ if (blockers.length > 5) bounded = true;
6017
+ const blockerLines = blockers.slice(0, 5).map((blocker) => nodeLine(blocker, 200)).filter(Boolean);
6018
+ if (blockerLines.length) lines.push(`Blockers:
6019
+ ${blockerLines.join("\n")}`);
6020
+ const active = object(brief?.active_execution);
6021
+ if (active) {
6022
+ const iteration = number(active.iteration) ?? number(object(active.props)?.iteration);
6023
+ lines.push(
6024
+ `Active execution: [${string(active.uid) || "?"}] iteration ${iteration ?? "?"} still running from a prior session \u2014 checkpoint or abandon it.`
6025
+ );
6026
+ }
6027
+ const poison = object(brief?.poison_task) || string(brief?.warning);
6028
+ if (poison) {
6029
+ lines.push(
6030
+ `Warning: ${typeof poison === "string" ? clipTracked(poison, 300) : "recent executions of this task repeatedly failed \u2014 investigate before retrying."}`
6031
+ );
6032
+ }
6033
+ }
6034
+ const optional = [];
6035
+ const knowledge = Array.isArray(brief?.knowledge) ? brief.knowledge : [];
6036
+ if (knowledge.length > 5) bounded = true;
6037
+ const knowledgeLines = knowledge.slice(0, 5).map((item) => nodeLine(item, 350)).filter(Boolean);
6038
+ if (knowledgeLines.length) {
6039
+ optional.push(`Relevant knowledge:
6040
+ ${knowledgeLines.join("\n")}`);
6041
+ }
6042
+ const targets = Array.isArray(brief?.code_targets) ? brief.code_targets : [];
6043
+ const targetLabels = targets.slice(0, 10).map((target) => clipTracked(object(target)?.label, 120)).filter(Boolean);
6044
+ if (targetLabels.length) {
6045
+ const extra = targets.length - targetLabels.length;
6046
+ if (extra > 0) bounded = true;
6047
+ optional.push(
6048
+ `Code targets: ${targetLabels.join(", ")}${extra > 0 ? ` (+${extra} more)` : ""}`
6049
+ );
6050
+ }
6051
+ const footer = task ? "Use the harness todo list only for local execution details; do not duplicate this durable Task there." : void 0;
6052
+ let used = lines.join("\n").length + (footer ? footer.length + 1 : 0);
6053
+ for (const section of optional) {
6054
+ if (used + section.length + 1 > BRIEF_CHAR_BUDGET) {
6055
+ bounded = true;
6056
+ break;
6057
+ }
6058
+ lines.push(section);
6059
+ used += section.length + 1;
6060
+ }
6061
+ if (bounded) {
6062
+ lines.push(
6063
+ "\u2026[bounded work brief truncated \u2014 fetch full node content by uid if needed]"
6064
+ );
6065
+ }
6066
+ if (footer) lines.push(footer);
6067
+ const rendered = lines.join("\n");
6068
+ return rendered.length <= BRIEF_CHAR_BUDGET ? rendered : `${rendered.slice(0, BRIEF_CHAR_BUDGET - 40)}
5447
6069
  \u2026[bounded work brief truncated]`;
5448
- return [
5449
- "MindGraph durable work brief (authoritative across sessions/harnesses):",
5450
- capped,
5451
- "Use the harness todo list only for local execution details; do not duplicate this durable Task there."
5452
- ].join("\n");
5453
6070
  }
5454
6071
  async function repositoryScopeUids(input, client2, options, agentId) {
5455
- if (!client2.entity) return [];
6072
+ if (!client2.entity) return { uids: [], failures: [] };
5456
6073
  const adapter = new CodegraphAdapter({
5457
6074
  cwd: input.cwd,
5458
6075
  workspaceFile: options.workspaceFile
5459
6076
  });
5460
6077
  const configured = await adapter.resolveRepositories();
5461
6078
  const repositories = configured.length > 0 ? configured : [await adapter.resolveRepository()];
6079
+ const failures = [];
5462
6080
  const resolved = await Promise.all(
5463
- repositories.map(
5464
- (repository) => client2.entity({
5465
- action: "resolve_identity",
5466
- identity: {
5467
- namespace: "external.code",
5468
- key_version: 1,
5469
- key: repositoryIdentityKey(repository)
5470
- },
5471
- agent_id: agentId
5472
- }).catch(() => void 0)
5473
- )
6081
+ repositories.map(async (repository) => {
6082
+ try {
6083
+ const payload = object(
6084
+ await client2.entity({
6085
+ action: "resolve_identity",
6086
+ identity: {
6087
+ namespace: "external.code",
6088
+ key_version: 1,
6089
+ key: repositoryIdentityKey(repository)
6090
+ },
6091
+ agent_id: agentId
6092
+ })
6093
+ );
6094
+ if (payload?.status === "existing" && string(payload.uid)) {
6095
+ return string(payload.uid);
6096
+ }
6097
+ if (payload?.status === "absent") {
6098
+ const anchored = await anchorRepository(
6099
+ client2,
6100
+ repository,
6101
+ repository.spaceUid ?? `space:agent:${agentId}`,
6102
+ agentId
6103
+ );
6104
+ if (anchored?.uid) return anchored.uid;
6105
+ failures.push(
6106
+ `${repository.repoId}: anchor ${anchored?.status ?? "failed"}`
6107
+ );
6108
+ return void 0;
6109
+ }
6110
+ failures.push(
6111
+ `${repository.repoId}: ${string(payload?.status) || "unresolved"}`
6112
+ );
6113
+ return void 0;
6114
+ } catch (error) {
6115
+ failures.push(
6116
+ `${repository.repoId}: ${error?.message || "resolve failed"}`
6117
+ );
6118
+ return void 0;
6119
+ }
6120
+ })
5474
6121
  );
5475
- return [
5476
- ...new Set(
5477
- resolved.flatMap((response) => {
5478
- const payload = object(response);
5479
- return payload?.status === "existing" && string(payload.uid) ? [string(payload.uid)] : [];
5480
- })
5481
- )
5482
- ];
6122
+ return {
6123
+ uids: [...new Set(resolved.filter((uid) => Boolean(uid)))],
6124
+ failures
6125
+ };
5483
6126
  }
5484
- function isOwnPriorWork(brief, agentId) {
6127
+ function isOwnPriorWork(brief, agentId, harness) {
5485
6128
  const reason = string(brief?.selection_reason);
5486
6129
  if (reason === "owned_live_lease" || reason === "same_agent_session_rebind") {
5487
6130
  return true;
5488
6131
  }
5489
- return string(object(brief?.lease)?.lease_owner_agent_id) === agentId;
6132
+ const owner = string(object(brief?.lease)?.lease_owner_agent_id);
6133
+ return owner === agentId || owner === harness;
5490
6134
  }
5491
6135
  async function sessionStart(input, client2, options) {
5492
- const agentId = options.agentId || process.env.MINDGRAPH_AGENT_ID || options.harness;
6136
+ const agentId = resolveAgentId(options);
5493
6137
  const harnessLabel = options.harness === "claude-code" ? "Claude Code" : "Codex";
6138
+ const nowSeconds = (options.now || (() => Date.now() / 1e3))();
5494
6139
  const opened = object(
5495
6140
  await client2.session({
5496
6141
  action: "open",
@@ -5502,7 +6147,21 @@ async function sessionStart(input, client2, options) {
5502
6147
  })
5503
6148
  );
5504
6149
  const sessionUid = string(opened?.uid);
5505
- const scopeUids = await repositoryScopeUids(input, client2, options, agentId);
6150
+ const baselineWorktree = worktreeState(input.cwd);
6151
+ if (sessionUid) {
6152
+ withLedger(input.session_id, options, (ledger) => {
6153
+ ledger.sessionUid = sessionUid;
6154
+ ledger.baselineWorktree = baselineWorktree;
6155
+ return void 0;
6156
+ });
6157
+ }
6158
+ const scope = await repositoryScopeUids(input, client2, options, agentId);
6159
+ if (scope.failures.length > 0) {
6160
+ console.error(
6161
+ `mindgraph-hook: ${scope.failures.length} repository scope(s) failed to resolve (${scope.failures.join("; ")}); resume runs ${scope.uids.length > 0 ? "narrowed" : "UNSCOPED"}`
6162
+ );
6163
+ }
6164
+ const scopeUids = scope.uids;
5506
6165
  let brief = object(
5507
6166
  await client2.plan({
5508
6167
  action: "resume_work",
@@ -5513,9 +6172,19 @@ async function sessionStart(input, client2, options) {
5513
6172
  );
5514
6173
  const task = object(brief?.task);
5515
6174
  const lease = object(brief?.lease);
5516
- if (sessionUid && string(task?.uid) && number(task?.version) && isOwnPriorWork(brief, agentId)) {
6175
+ const prior = readLedger(input.session_id, options);
6176
+ const leaseExpiry = number(lease?.lease_expires_at);
6177
+ const leaseLive = leaseExpiry !== void 0 && leaseExpiry > nowSeconds;
6178
+ const heldByThisSession = leaseLive && prior.taskUid !== void 0 && prior.taskUid === string(task?.uid) && prior.leaseEpoch === number(lease?.lease_epoch);
6179
+ let claimed;
6180
+ let claimFailed = false;
6181
+ if (sessionUid && string(task?.uid) && number(task?.version) && isOwnPriorWork(brief, agentId, options.harness) && // A LIVE lease belongs to whichever session claimed it — claiming from
6182
+ // here would fence that session out mid-work (the released server
6183
+ // rebinds same-owner live leases without allow_takeover). Re-claim live
6184
+ // leases only when this very session already holds them (re-entry).
6185
+ (!leaseLive || heldByThisSession)) {
5517
6186
  try {
5518
- const claimed = object(
6187
+ claimed = object(
5519
6188
  await client2.plan({
5520
6189
  action: "claim_task",
5521
6190
  task_uid: task.uid,
@@ -5525,35 +6194,63 @@ async function sessionStart(input, client2, options) {
5525
6194
  agent_id: agentId
5526
6195
  })
5527
6196
  );
5528
- brief = object(
5529
- await client2.plan({
5530
- action: "resume_work",
5531
- task_uid: task.uid,
5532
- session_uid: sessionUid,
5533
- ...scopeUids.length > 0 ? { scope_uids: scopeUids } : {},
5534
- agent_id: agentId
5535
- })
5536
- );
5537
- if (brief && claimed) brief.claim = claimed;
5538
6197
  } catch {
6198
+ claimFailed = true;
6199
+ }
6200
+ if (claimed) {
6201
+ try {
6202
+ const reResumed = object(
6203
+ await client2.plan({
6204
+ action: "resume_work",
6205
+ task_uid: task.uid,
6206
+ session_uid: sessionUid,
6207
+ ...scopeUids.length > 0 ? { scope_uids: scopeUids } : {},
6208
+ agent_id: agentId
6209
+ })
6210
+ );
6211
+ if (reResumed) brief = reResumed;
6212
+ } catch {
6213
+ }
6214
+ if (brief) brief.claim = claimed;
5539
6215
  }
5540
6216
  }
5541
- const briefText = boundedBrief(brief || { status: "no_eligible_work" });
5542
- const briefHash = import_node_crypto2.default.createHash("sha256").update(briefText).digest("hex");
6217
+ const stillHeld = !claimed && claimFailed && heldByThisSession;
6218
+ const scopeNote = scope.failures.length > 0 ? `Scope: ${scopeUids.length} repositories resolved, ${scope.failures.length} FAILED \u2014 durable-work selection ran ${scopeUids.length > 0 ? "narrowed" : "unscoped"}; tasks in the failed repositories may be missing.` : void 0;
6219
+ const briefText = renderBrief(brief || { status: "no_eligible_work" }, {
6220
+ claimed: Boolean(claimed) || stillHeld,
6221
+ claimFailed: claimFailed && !stillHeld,
6222
+ scopeNote
6223
+ });
6224
+ const briefHash = import_node_crypto3.default.createHash("sha256").update(briefText).digest("hex");
5543
6225
  const shouldInject = withLedger(input.session_id, options, (ledger) => {
5544
6226
  const changed = ledger.lastBriefHash !== briefHash;
5545
6227
  const reinjectCompact = options.reinjectUnchangedCompact && input.source === "compact";
5546
6228
  ledger.sessionUid = sessionUid;
5547
- ledger.baselineWorktree = worktreeState(input.cwd);
5548
- const selectedTask = object(brief?.task);
5549
- const selectedLease = object(brief?.lease) || object(brief?.claim);
5550
- const executions = Array.isArray(brief?.recent_executions) ? brief?.recent_executions : [];
5551
- const latest = object(executions[0]);
5552
- ledger.taskUid = string(selectedTask?.uid);
5553
- ledger.taskVersion = number(selectedTask?.version);
5554
- ledger.leaseEpoch = number(selectedLease?.lease_epoch);
5555
- ledger.leaseExpiresAt = number(selectedLease?.lease_expires_at);
5556
- ledger.executionUid = string(object(brief?.active_execution)?.uid) || string(latest?.uid);
6229
+ ledger.baselineWorktree = baselineWorktree;
6230
+ if (claimed) {
6231
+ const claimLease = object(brief?.claim) || object(brief?.lease);
6232
+ const selectedTask = object(brief?.task);
6233
+ ledger.taskUid = string(selectedTask?.uid);
6234
+ ledger.taskVersion = number(claimed.task_version) ?? number(selectedTask?.version) ?? void 0;
6235
+ ledger.leaseEpoch = number(claimLease?.lease_epoch);
6236
+ ledger.leaseExpiresAt = number(claimLease?.lease_expires_at);
6237
+ ledger.lastLeaseRenewalAt = nowSeconds;
6238
+ const running = [
6239
+ object(brief?.active_execution),
6240
+ ...Array.isArray(brief?.recent_executions) ? brief.recent_executions.map(object) : []
6241
+ ].find(
6242
+ (execution) => execution && (string(object(execution.props)?.status) ?? string(execution.status)) === "running"
6243
+ );
6244
+ ledger.executionUid = running ? string(running.uid) : void 0;
6245
+ } else if (claimFailed && ledger.taskUid !== void 0 && ledger.taskUid === string(object(brief?.task)?.uid) && ledger.lastLeaseRenewalAt !== void 0) {
6246
+ } else {
6247
+ ledger.taskUid = void 0;
6248
+ ledger.taskVersion = void 0;
6249
+ ledger.leaseEpoch = void 0;
6250
+ ledger.leaseExpiresAt = void 0;
6251
+ ledger.executionUid = void 0;
6252
+ ledger.lastLeaseRenewalAt = void 0;
6253
+ }
5557
6254
  ledger.lastBriefHash = briefHash;
5558
6255
  return changed || reinjectCompact;
5559
6256
  });
@@ -5562,82 +6259,173 @@ async function sessionStart(input, client2, options) {
5562
6259
  async function preTool(input, options) {
5563
6260
  const original = { ...input.tool_input || {} };
5564
6261
  const ledger = readLedger(input.session_id, options);
6262
+ let repoId = ledger.repoContextCwd === input.cwd && ledger.repoContextAt !== void 0 && Date.now() - ledger.repoContextAt < 3e5 ? ledger.repoContextRepoId : void 0;
6263
+ if (!repoId) {
6264
+ repoId = (await new CodegraphAdapter({
6265
+ cwd: input.cwd,
6266
+ workspaceFile: options.workspaceFile
6267
+ }).resolveRepository()).repoId;
6268
+ withLedger(input.session_id, options, (fresh) => {
6269
+ fresh.repoContextCwd = input.cwd;
6270
+ fresh.repoContextRepoId = repoId;
6271
+ fresh.repoContextAt = Date.now();
6272
+ return void 0;
6273
+ });
6274
+ }
5565
6275
  const updatedInput = {
5566
6276
  ...original,
5567
- invocation_context: await invocationContext(input, options)
6277
+ invocation_context: invocationContext(input, options, repoId)
5568
6278
  };
5569
6279
  if (ledger.sessionUid) updatedInput.session_uid = ledger.sessionUid;
5570
6280
  if (ledger.taskUid && updatedInput.task_uid === void 0) {
5571
6281
  updatedInput.task_uid = ledger.taskUid;
5572
6282
  }
5573
- if (ledger.taskUid && input.tool_name?.includes("mindgraph_capture") && updatedInput.work_uid === void 0) {
6283
+ const modelTask = string(original.task_uid);
6284
+ const targetsLedgerTask = ledger.taskUid !== void 0 && (modelTask === void 0 || modelTask === ledger.taskUid);
6285
+ const isCapture = Boolean(input.tool_name?.includes("mindgraph_capture"));
6286
+ const fencedAction = Boolean(input.tool_name?.includes("mindgraph_plan")) && FENCED_PLAN_ACTIONS.has(string(original.action) || "");
6287
+ if (ledger.taskUid && isCapture && updatedInput.work_uid === void 0) {
5574
6288
  updatedInput.work_uid = ledger.taskUid;
5575
6289
  }
5576
- if (ledger.executionUid && updatedInput.execution_uid === void 0) {
6290
+ if (targetsLedgerTask && (fencedAction || isCapture) && ledger.executionUid && updatedInput.execution_uid === void 0) {
5577
6291
  updatedInput.execution_uid = ledger.executionUid;
5578
6292
  }
5579
- if (ledger.leaseEpoch !== void 0 && updatedInput.lease_epoch === void 0) {
6293
+ if (targetsLedgerTask && fencedAction && ledger.leaseEpoch !== void 0 && updatedInput.lease_epoch === void 0) {
5580
6294
  updatedInput.lease_epoch = ledger.leaseEpoch;
5581
6295
  }
5582
- if (ledger.taskVersion !== void 0 && updatedInput.expected_version === void 0) {
6296
+ if (targetsLedgerTask && fencedAction && ledger.taskVersion !== void 0 && updatedInput.expected_version === void 0) {
5583
6297
  updatedInput.expected_version = ledger.taskVersion;
5584
6298
  }
5585
6299
  return { kind: "rewrite", updatedInput };
5586
6300
  }
6301
+ var FENCED_PLAN_ACTIONS = /* @__PURE__ */ new Set([
6302
+ "claim_task",
6303
+ "heartbeat",
6304
+ "start_iteration",
6305
+ "checkpoint_iteration",
6306
+ "block_task",
6307
+ "complete_task",
6308
+ "abandon_iteration"
6309
+ ]);
5587
6310
  async function postTool(input, client2, options) {
5588
6311
  const name = input.tool_name || "";
5589
6312
  const args = input.tool_input || {};
5590
6313
  const action = string(args.action);
5591
6314
  const payload = responsePayload(input.tool_response);
5592
6315
  const nowSeconds = (options.now || (() => Date.now() / 1e3))();
5593
- let heartbeat;
6316
+ let renewal;
5594
6317
  withLedger(input.session_id, options, (ledger) => {
5595
6318
  ledger.materialToolCalls += 1;
5596
- if (["Edit", "Write", "NotebookEdit", "apply_patch"].includes(name) || name === "Bash" && action !== "read") {
6319
+ if (["Edit", "Write", "NotebookEdit", "apply_patch"].includes(name)) {
5597
6320
  ledger.mutatingOperations += 1;
5598
6321
  }
5599
- if (name.includes("mindgraph_capture") && !payload?.error) {
5600
- ledger.memoryWritten = true;
6322
+ if (!payload?.error) {
6323
+ if (name.includes("mindgraph_capture") && ["lesson", "journal"].includes(action || "") || name.includes("mindgraph_commit") && action === "resolve_decision" || name.includes("mindgraph_plan") && action === "assess_risk") {
6324
+ ledger.memoryWritten = true;
6325
+ }
5601
6326
  }
5602
6327
  if (name.includes("mindgraph_plan")) {
5603
- if (action === "start_iteration" && !payload?.error) {
5604
- ledger.executionUid = string(payload?.execution_uid) || ledger.executionUid;
5605
- }
5606
- if (["checkpoint_iteration", "block_task", "complete_task", "abandon_iteration"].includes(
5607
- action || ""
5608
- ) && !payload?.error) {
5609
- ledger.iterationCheckpointed = true;
5610
- if (action !== "block_task") ledger.executionUid = void 0;
5611
- }
5612
- ledger.taskVersion = number(payload?.task_version) || number(payload?.version) || ledger.taskVersion;
5613
- ledger.leaseEpoch = number(payload?.lease_epoch) || ledger.leaseEpoch;
5614
- ledger.leaseExpiresAt = number(payload?.lease_expires_at) || ledger.leaseExpiresAt;
6328
+ const targetTask = string(args.task_uid) || ledger.taskUid;
6329
+ const aboutLedgerTask = targetTask !== void 0 && targetTask === ledger.taskUid;
6330
+ if (!payload?.error) {
6331
+ if (action === "claim_task" && targetTask) {
6332
+ const rebindToOther = targetTask !== ledger.taskUid;
6333
+ ledger.taskUid = targetTask;
6334
+ ledger.taskVersion = number(payload?.task_version) ?? number(payload?.version) ?? (rebindToOther ? void 0 : ledger.taskVersion);
6335
+ ledger.leaseEpoch = number(payload?.lease_epoch) ?? (rebindToOther ? void 0 : ledger.leaseEpoch);
6336
+ ledger.leaseExpiresAt = number(payload?.lease_expires_at) ?? (rebindToOther ? void 0 : ledger.leaseExpiresAt);
6337
+ if (rebindToOther) ledger.executionUid = void 0;
6338
+ ledger.lastLeaseRenewalAt = nowSeconds;
6339
+ }
6340
+ if (action === "start_iteration") {
6341
+ ledger.executionUid = string(payload?.execution_uid) || ledger.executionUid;
6342
+ }
6343
+ if (["checkpoint_iteration", "block_task", "complete_task", "abandon_iteration"].includes(
6344
+ action || ""
6345
+ )) {
6346
+ ledger.iterationCheckpointed = true;
6347
+ if (action !== "block_task") ledger.executionUid = void 0;
6348
+ }
6349
+ if (action === "complete_task" && aboutLedgerTask) {
6350
+ ledger.taskUid = void 0;
6351
+ ledger.taskVersion = void 0;
6352
+ ledger.leaseEpoch = void 0;
6353
+ ledger.leaseExpiresAt = void 0;
6354
+ ledger.lastLeaseRenewalAt = void 0;
6355
+ } else if (aboutLedgerTask && action !== "claim_task") {
6356
+ ledger.taskVersion = number(payload?.task_version) ?? (string(payload?.uid) === ledger.taskUid ? number(payload?.version) : void 0) ?? ledger.taskVersion;
6357
+ ledger.leaseEpoch = number(payload?.lease_epoch) ?? ledger.leaseEpoch;
6358
+ if (number(payload?.lease_expires_at) !== void 0) {
6359
+ ledger.leaseExpiresAt = number(payload?.lease_expires_at);
6360
+ ledger.lastLeaseRenewalAt = nowSeconds;
6361
+ }
6362
+ }
6363
+ } else if (aboutLedgerTask) {
6364
+ ledger.taskVersion = number(payload?.current_version) ?? ledger.taskVersion;
6365
+ ledger.leaseEpoch = number(payload?.current_epoch) ?? ledger.leaseEpoch;
6366
+ ledger.leaseExpiresAt = number(payload?.lease_expires_at) ?? ledger.leaseExpiresAt;
6367
+ }
5615
6368
  }
5616
- if (ledger.taskUid && ledger.sessionUid && ledger.taskVersion !== void 0 && ledger.leaseEpoch !== void 0 && ledger.leaseExpiresAt !== void 0 && ledger.leaseExpiresAt - nowSeconds <= 60) {
5617
- heartbeat = {
5618
- taskUid: ledger.taskUid,
5619
- sessionUid: ledger.sessionUid,
5620
- taskVersion: ledger.taskVersion,
5621
- leaseEpoch: ledger.leaseEpoch
5622
- };
6369
+ if (ledger.taskUid && ledger.sessionUid && ledger.taskVersion !== void 0 && ledger.leaseEpoch !== void 0 && ledger.leaseExpiresAt !== void 0 && // Only ledgers whose lease state was written by a claim THIS code made
6370
+ // may renew: 0.14.9 ledgers carry adopted-but-never-claimed fence
6371
+ // state, and a lapse-reclaim from one would auto-seize a task this
6372
+ // session never owned — the incident class all of this exists to kill.
6373
+ ledger.lastLeaseRenewalAt !== void 0) {
6374
+ const expiresIn = ledger.leaseExpiresAt - nowSeconds;
6375
+ const sinceRenewal = nowSeconds - ledger.lastLeaseRenewalAt;
6376
+ if (expiresIn <= 0) {
6377
+ renewal = {
6378
+ kind: "reclaim",
6379
+ taskUid: ledger.taskUid,
6380
+ sessionUid: ledger.sessionUid,
6381
+ taskVersion: ledger.taskVersion,
6382
+ leaseEpoch: ledger.leaseEpoch
6383
+ };
6384
+ } else if (expiresIn <= 60 || sinceRenewal >= 180) {
6385
+ renewal = {
6386
+ kind: "heartbeat",
6387
+ taskUid: ledger.taskUid,
6388
+ sessionUid: ledger.sessionUid,
6389
+ taskVersion: ledger.taskVersion,
6390
+ leaseEpoch: ledger.leaseEpoch
6391
+ };
6392
+ }
5623
6393
  }
5624
6394
  return void 0;
5625
6395
  });
5626
- if (heartbeat) {
6396
+ if (renewal) {
6397
+ const active = renewal;
5627
6398
  try {
5628
6399
  const response = object(
5629
- await client2.plan({
6400
+ active.kind === "reclaim" ? (
6401
+ // The lease already lapsed: a heartbeat can only 409. Re-claim
6402
+ // rebinds the same owner's expired lease (bumping the epoch) so
6403
+ // durable tracking survives a >TTL quiet stretch mid-session.
6404
+ await client2.plan({
6405
+ action: "claim_task",
6406
+ task_uid: active.taskUid,
6407
+ session_uid: active.sessionUid,
6408
+ expected_version: active.taskVersion,
6409
+ idempotency_key: `hook-reclaim:${input.session_id}:${active.taskUid}:${active.leaseEpoch}:${Math.floor(nowSeconds / 60)}`,
6410
+ agent_id: resolveAgentId(options)
6411
+ })
6412
+ ) : await client2.plan({
5630
6413
  action: "heartbeat",
5631
- task_uid: heartbeat.taskUid,
5632
- session_uid: heartbeat.sessionUid,
5633
- expected_version: heartbeat.taskVersion,
5634
- lease_epoch: heartbeat.leaseEpoch,
5635
- idempotency_key: `hook-heartbeat:${input.session_id}:${heartbeat.taskUid}:${Math.floor(nowSeconds / 60)}`,
5636
- agent_id: options.agentId || options.harness
6414
+ task_uid: active.taskUid,
6415
+ session_uid: active.sessionUid,
6416
+ expected_version: active.taskVersion,
6417
+ lease_epoch: active.leaseEpoch,
6418
+ idempotency_key: `hook-heartbeat:${input.session_id}:${active.taskUid}:${active.leaseEpoch}:${active.taskVersion}:${Math.floor(nowSeconds / 60)}`,
6419
+ agent_id: resolveAgentId(options)
5637
6420
  })
5638
6421
  );
5639
6422
  withLedger(input.session_id, options, (ledger) => {
5640
- ledger.leaseExpiresAt = number(response?.lease_expires_at) || ledger.leaseExpiresAt;
6423
+ if (payloadIsError(response)) return void 0;
6424
+ if (ledger.taskUid !== active.taskUid) return void 0;
6425
+ ledger.taskVersion = number(response?.task_version) ?? ledger.taskVersion;
6426
+ ledger.leaseEpoch = number(response?.lease_epoch) ?? ledger.leaseEpoch;
6427
+ ledger.leaseExpiresAt = number(response?.lease_expires_at) ?? ledger.leaseExpiresAt;
6428
+ ledger.lastLeaseRenewalAt = nowSeconds;
5641
6429
  return void 0;
5642
6430
  });
5643
6431
  } catch {
@@ -5645,6 +6433,9 @@ async function postTool(input, client2, options) {
5645
6433
  }
5646
6434
  return NOOP;
5647
6435
  }
6436
+ function payloadIsError(payload) {
6437
+ return Boolean(payload && payload.error);
6438
+ }
5648
6439
  function stop(input, options) {
5649
6440
  if (input.stop_hook_active) return NOOP;
5650
6441
  return withLedger(input.session_id, options, (ledger) => {
@@ -5675,17 +6466,20 @@ async function sessionEnd(input, client2, options) {
5675
6466
  release_lease: true,
5676
6467
  summary: `${harnessLabel} SessionEnd: ${input.reason || "interrupted"}`,
5677
6468
  idempotency_key: `session-end:${input.session_id}:${ledger.executionUid}`,
5678
- agent_id: options.agentId || options.harness
6469
+ agent_id: resolveAgentId(options)
5679
6470
  });
5680
6471
  } catch {
5681
6472
  }
5682
6473
  }
5683
- if (ledger.sessionUid) {
6474
+ const abandonRan = Boolean(
6475
+ ledger.executionUid && ledger.taskUid && ledger.sessionUid && ledger.taskVersion !== void 0 && ledger.leaseEpoch !== void 0
6476
+ );
6477
+ if (ledger.sessionUid && (options.harness !== "codex" || !abandonRan)) {
5684
6478
  try {
5685
6479
  await client2.session({
5686
6480
  action: "close",
5687
6481
  session_uid: ledger.sessionUid,
5688
- agent_id: options.agentId || options.harness
6482
+ agent_id: resolveAgentId(options)
5689
6483
  });
5690
6484
  } catch {
5691
6485
  }
@@ -5795,7 +6589,7 @@ async function runCodexHook(input, client2, options = {}) {
5795
6589
  var import_node_fs4 = __toESM(require("fs"));
5796
6590
  var import_node_os4 = __toESM(require("os"));
5797
6591
  var import_node_path4 = __toESM(require("path"));
5798
- var import_node_crypto3 = __toESM(require("crypto"));
6592
+ var import_node_crypto4 = __toESM(require("crypto"));
5799
6593
  var OWNER_MARKER = "--owner mindgraph";
5800
6594
  var RUNNER_RELATIVE = ".mindgraph/bin/mindgraph-hook.cjs";
5801
6595
  function command(harness) {
@@ -5839,11 +6633,14 @@ function desiredEntries(harness) {
5839
6633
  // inherited 20s edge in both harnesses: the hooks correctly failed
5840
6634
  // open, but no brief reached the model. The same installed runner
5841
6635
  // completed in 16.84s once warm. Keep a 30s cold-tenant margin.
6636
+ // The declared Codex context limit must not undercut the brief's own
6637
+ // 9,000-char budget: at 3,000 the harness applied a SECOND, tighter
6638
+ // truncation the renderer knew nothing about.
5842
6639
  hooks: [
5843
6640
  ownedHook(
5844
6641
  harness,
5845
6642
  30,
5846
- harness === "codex" ? 3e3 : void 0
6643
+ harness === "codex" ? 1e4 : void 0
5847
6644
  )
5848
6645
  ]
5849
6646
  }
@@ -5851,13 +6648,23 @@ function desiredEntries(harness) {
5851
6648
  PreToolUse: [
5852
6649
  {
5853
6650
  matcher: "mcp__mindgraph__.*",
5854
- hooks: [ownedHook(harness, 3)]
6651
+ // Repository resolution over a large multi-repo workspace plus three
6652
+ // git probes can exceed 3s on the first (uncached) call; a killed
6653
+ // PreToolUse means the call goes through untagged.
6654
+ hooks: [ownedHook(harness, 5)]
5855
6655
  }
5856
6656
  ],
5857
- PostToolUse: [{ matcher: ".*", hooks: [ownedHook(harness, 5)] }],
6657
+ // PostToolUse awaits the lease renewal round-trip, which fires exactly
6658
+ // when the tenant is cold (post-idle reclaim) — 5s killed it mid-claim.
6659
+ PostToolUse: [{ matcher: ".*", hooks: [ownedHook(harness, 10)] }],
5858
6660
  Stop: [{ hooks: [ownedHook(harness, 5)] }],
6661
+ // SessionEnd makes up to two sequential cloud calls (abandon_iteration +
6662
+ // session close) — the SAME cold-tenant latency SessionStart needs 30s
6663
+ // for. Claude honors the configured budget; Codex clamps SessionEnd to a
6664
+ // 3s platform cap regardless of this value (see codex-hooks.ts wire
6665
+ // map), so the codex adapter instead makes at most ONE call there.
5859
6666
  SessionEnd: [
5860
- { hooks: [ownedHook(harness, harness === "codex" ? 3 : 8)] }
6667
+ { hooks: [ownedHook(harness, harness === "codex" ? 3 : 30)] }
5861
6668
  ]
5862
6669
  };
5863
6670
  return harness === "claude-code" ? {
@@ -5878,7 +6685,7 @@ function entryHasOwnedHook(value) {
5878
6685
  }
5879
6686
  function writeSettings(file, settings) {
5880
6687
  import_node_fs4.default.mkdirSync(import_node_path4.default.dirname(file), { recursive: true });
5881
- const temporary = `${file}.${process.pid}.${import_node_crypto3.default.randomUUID()}.tmp`;
6688
+ const temporary = `${file}.${process.pid}.${import_node_crypto4.default.randomUUID()}.tmp`;
5882
6689
  import_node_fs4.default.writeFileSync(temporary, `${JSON.stringify(settings, null, 2)}
5883
6690
  `, {
5884
6691
  mode: 384
@@ -5956,6 +6763,52 @@ function uninstallCodexHooks(scope, projectDir = process.cwd(), codexHomeDir) {
5956
6763
  }
5957
6764
 
5958
6765
  // src/cli.ts
6766
+ function recordHookHealth(harness, error) {
6767
+ try {
6768
+ const dir = process.env.MINDGRAPH_RUNTIME_DIR || path5.join(os4.homedir(), ".mindgraph", "runtime");
6769
+ fs5.mkdirSync(dir, { recursive: true, mode: 448 });
6770
+ const file = path5.join(dir, "hook-health.json");
6771
+ let failures = 0;
6772
+ try {
6773
+ failures = Number(JSON.parse(fs5.readFileSync(file, "utf8")).consecutiveFailures) || 0;
6774
+ } catch {
6775
+ }
6776
+ fs5.writeFileSync(
6777
+ file,
6778
+ `${JSON.stringify({
6779
+ harness,
6780
+ lastError: error.slice(0, 500),
6781
+ lastFailureAt: (/* @__PURE__ */ new Date()).toISOString(),
6782
+ consecutiveFailures: failures + 1
6783
+ })}
6784
+ `,
6785
+ { mode: 384 }
6786
+ );
6787
+ } catch {
6788
+ }
6789
+ }
6790
+ function clearHookHealth(harness) {
6791
+ try {
6792
+ const file = path5.join(
6793
+ process.env.MINDGRAPH_RUNTIME_DIR || path5.join(os4.homedir(), ".mindgraph", "runtime"),
6794
+ "hook-health.json"
6795
+ );
6796
+ if (!fs5.existsSync(file)) return;
6797
+ const current = JSON.parse(fs5.readFileSync(file, "utf8"));
6798
+ if (!Number(current.consecutiveFailures)) return;
6799
+ fs5.writeFileSync(
6800
+ file,
6801
+ `${JSON.stringify({
6802
+ harness,
6803
+ lastOkAt: (/* @__PURE__ */ new Date()).toISOString(),
6804
+ consecutiveFailures: 0
6805
+ })}
6806
+ `,
6807
+ { mode: 384 }
6808
+ );
6809
+ } catch {
6810
+ }
6811
+ }
5959
6812
  function getClaudeDesktopConfigPath() {
5960
6813
  switch (process.platform) {
5961
6814
  case "darwin":
@@ -6031,7 +6884,10 @@ function installClaudeCode(apiKey, baseUrl, agentId) {
6031
6884
  "--env",
6032
6885
  "MINDGRAPH_HARNESS=claude-code",
6033
6886
  "--env",
6034
- `MINDGRAPH_AGENT_ID=${agentId || "claude-code"}`
6887
+ // The registration, the hooks, and the serve path must all resolve the
6888
+ // SAME identity — a differing default here made the model's fenced tool
6889
+ // calls and the hook's lease two different agents on the server.
6890
+ `MINDGRAPH_AGENT_ID=${agentId || process.env.MINDGRAPH_AGENT_ID || loadHookEnv().agentId || stableAgentId()}`
6035
6891
  ];
6036
6892
  if (baseUrl) {
6037
6893
  envArgs.push("--env", `MINDGRAPH_BASE_URL=${baseUrl}`);
@@ -6121,6 +6977,23 @@ function printStatus() {
6121
6977
  console.log(
6122
6978
  `MINDGRAPH_BASE_URL: ${process.env.MINDGRAPH_BASE_URL || "https://api.mindgraph.cloud (default)"}`
6123
6979
  );
6980
+ try {
6981
+ const healthFile = path5.join(
6982
+ process.env.MINDGRAPH_RUNTIME_DIR || path5.join(os4.homedir(), ".mindgraph", "runtime"),
6983
+ "hook-health.json"
6984
+ );
6985
+ if (fs5.existsSync(healthFile)) {
6986
+ const health = JSON.parse(fs5.readFileSync(healthFile, "utf8"));
6987
+ const failures = Number(health.consecutiveFailures) || 0;
6988
+ console.log(
6989
+ failures > 0 ? `Hooks: FAILING (${failures} consecutive; last: ${health.lastError} at ${health.lastFailureAt})` : `Hooks: healthy (last recovery ${health.lastOkAt || "unknown"})`
6990
+ );
6991
+ } else {
6992
+ console.log("Hooks: no health marker (no hook failures recorded)");
6993
+ }
6994
+ } catch {
6995
+ console.log("Hooks: health marker unreadable");
6996
+ }
6124
6997
  }
6125
6998
  function withHooksFlag() {
6126
6999
  return process.argv.includes("--hooks");
@@ -6133,7 +7006,16 @@ function finishCodeInstall(apiKey, baseUrl, withHooks, agentId) {
6133
7006
  console.log(
6134
7007
  `Installed ${result.added} and refreshed ${result.updated} MindGraph Claude Code hook entries in ${result.path}`
6135
7008
  );
6136
- const envPath = saveHookEnv({ apiKey, baseUrl, agentId });
7009
+ const envPath = saveHookEnv({
7010
+ apiKey,
7011
+ baseUrl,
7012
+ // Default the stable per-user identity only when nothing is stored —
7013
+ // an explicit or previously persisted agentId must survive reinstalls.
7014
+ agentId: agentId || process.env.MINDGRAPH_AGENT_ID || loadHookEnv().agentId || stableAgentId(),
7015
+ // The org pin follows the install environment exactly: installing
7016
+ // without MINDGRAPH_ORG_ID clears any stale pin (null = clear).
7017
+ orgId: process.env.MINDGRAPH_ORG_ID ?? null
7018
+ });
6137
7019
  console.log(`Saved hook connection settings to ${envPath} (mode 600)`);
6138
7020
  } else {
6139
7021
  console.log(
@@ -6192,7 +7074,7 @@ var DASHBOARD_URL = process.env.MINDGRAPH_DASHBOARD_URL || "https://mindgraph.cl
6192
7074
  var POLL_INTERVAL = 2e3;
6193
7075
  var POLL_TIMEOUT = 10 * 60 * 1e3;
6194
7076
  function generateCode() {
6195
- return crypto4.randomBytes(32).toString("hex");
7077
+ return crypto5.randomBytes(32).toString("hex");
6196
7078
  }
6197
7079
  function openBrowser(url) {
6198
7080
  try {
@@ -6355,10 +7237,19 @@ async function main2() {
6355
7237
  `Installed ${result.added} and refreshed ${result.updated} MindGraph ${hookHarness === "codex" ? "Codex" : "Claude Code"} hook entries in ${result.path}`
6356
7238
  );
6357
7239
  printCodegraphStatus();
6358
- if (apiKey || baseUrl || agentId) {
6359
- const envPath = saveHookEnv({ apiKey, baseUrl, agentId });
6360
- console.log(`Saved hook connection settings to ${envPath} (mode 600)`);
6361
- } else {
7240
+ const envPath = saveHookEnv({
7241
+ apiKey,
7242
+ baseUrl,
7243
+ // Default the stable per-user identity only when nothing is stored —
7244
+ // an explicit or previously persisted agentId must survive
7245
+ // reinstalls.
7246
+ agentId: agentId || process.env.MINDGRAPH_AGENT_ID || loadHookEnv().agentId || stableAgentId(),
7247
+ // The org pin follows the install environment exactly: installing
7248
+ // without MINDGRAPH_ORG_ID clears any stale pin (null = clear).
7249
+ orgId: process.env.MINDGRAPH_ORG_ID ?? null
7250
+ });
7251
+ console.log(`Saved hook connection settings to ${envPath} (mode 600)`);
7252
+ if (!apiKey && !baseUrl) {
6362
7253
  console.log(
6363
7254
  "Note: hooks resolve MINDGRAPH_API_KEY/MINDGRAPH_BASE_URL from the environment or ~/.mindgraph/hooks.json \u2014 pass --api-key/--base-url to persist them now."
6364
7255
  );
@@ -6385,6 +7276,7 @@ async function main2() {
6385
7276
  const hookApiKey = apiKey ?? stored.apiKey;
6386
7277
  const hookBaseUrl = baseUrl ?? stored.baseUrl;
6387
7278
  if (!hookApiKey) {
7279
+ recordHookHealth(hookHarness, "no-api-key");
6388
7280
  process.stdout.write("{}\n");
6389
7281
  break;
6390
7282
  }
@@ -6393,12 +7285,19 @@ async function main2() {
6393
7285
  const client2 = new MindGraph({
6394
7286
  baseUrl: hookBaseUrl || "https://api.mindgraph.cloud",
6395
7287
  apiKey: hookApiKey,
6396
- orgId: process.env.MINDGRAPH_ORG_ID,
7288
+ // Hooks run with the harness's environment, which usually lacks
7289
+ // MINDGRAPH_ORG_ID even when the MCP registration pins it — on a
7290
+ // multi-org key the hooks would otherwise track work in the key's
7291
+ // DEFAULT org while the MCP tools write to the pinned one.
7292
+ orgId: process.env.MINDGRAPH_ORG_ID || stored.orgId,
6397
7293
  maxRetries: 0,
6398
7294
  telemetrySurface: "mcp"
6399
7295
  });
6400
7296
  const hookOptions = {
6401
- agentId: process.env.MINDGRAPH_AGENT_ID || stored.agentId || hookHarness
7297
+ // No harness-name fallback here: hook-core derives a stable
7298
+ // per-user default shared by both harnesses, which is what lets a
7299
+ // task claimed under Codex rebind under Claude Code.
7300
+ agentId: process.env.MINDGRAPH_AGENT_ID || stored.agentId
6402
7301
  };
6403
7302
  const output = hookHarness === "codex" ? await runCodexHook(
6404
7303
  input,
@@ -6411,7 +7310,15 @@ async function main2() {
6411
7310
  );
6412
7311
  process.stdout.write(`${JSON.stringify(output)}
6413
7312
  `);
6414
- } catch {
7313
+ clearHookHealth(hookHarness);
7314
+ } catch (error) {
7315
+ recordHookHealth(
7316
+ hookHarness,
7317
+ error?.message || "hook failed"
7318
+ );
7319
+ console.error(
7320
+ `mindgraph-hook: failing open (${error?.message || "unknown error"})`
7321
+ );
6415
7322
  process.stdout.write("{}\n");
6416
7323
  }
6417
7324
  break;