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.
- package/README.md +3 -3
- package/dist/cli.js +1112 -205
- package/dist/index.js +600 -94
- 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,
|
|
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
|
-
|
|
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,
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 ||
|
|
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.
|
|
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
|
|
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
|
|
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 =
|
|
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 <
|
|
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 >
|
|
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}.${
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
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
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
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
|
-
|
|
5478
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
5542
|
-
const
|
|
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 =
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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)
|
|
6319
|
+
if (["Edit", "Write", "NotebookEdit", "apply_patch"].includes(name)) {
|
|
5597
6320
|
ledger.mutatingOperations += 1;
|
|
5598
6321
|
}
|
|
5599
|
-
if (
|
|
5600
|
-
|
|
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
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
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 &&
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
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 (
|
|
6396
|
+
if (renewal) {
|
|
6397
|
+
const active = renewal;
|
|
5627
6398
|
try {
|
|
5628
6399
|
const response = object(
|
|
5629
|
-
|
|
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:
|
|
5632
|
-
session_uid:
|
|
5633
|
-
expected_version:
|
|
5634
|
-
lease_epoch:
|
|
5635
|
-
idempotency_key: `hook-heartbeat:${input.session_id}:${
|
|
5636
|
-
agent_id: options
|
|
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
|
-
|
|
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
|
|
6469
|
+
agent_id: resolveAgentId(options)
|
|
5679
6470
|
});
|
|
5680
6471
|
} catch {
|
|
5681
6472
|
}
|
|
5682
6473
|
}
|
|
5683
|
-
|
|
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
|
|
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
|
|
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" ?
|
|
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
|
-
|
|
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
|
|
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 :
|
|
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}.${
|
|
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
|
-
|
|
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({
|
|
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
|
|
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
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|