mindgraph-mcp 0.14.9 → 0.15.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/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();
@@ -2405,6 +2478,18 @@ async function handleTool(client2, name, args) {
2405
2478
  ].includes(action);
2406
2479
  return attach ? await attachCodeRefsToToolResult(client2, result, args) : result;
2407
2480
  } catch (e) {
2481
+ const lifted = conflictState(e);
2482
+ if (Object.keys(lifted).length > 0) {
2483
+ return {
2484
+ content: [
2485
+ {
2486
+ type: "text",
2487
+ text: JSON.stringify({ error: errorDetail(e), ...lifted })
2488
+ }
2489
+ ],
2490
+ isError: true
2491
+ };
2492
+ }
2408
2493
  return err3(errorDetail(e));
2409
2494
  }
2410
2495
  }
@@ -4822,7 +4907,7 @@ async function main() {
4822
4907
  const transport = new import_stdio.StdioServerTransport();
4823
4908
  await server.connect(transport);
4824
4909
  console.error(
4825
- `MindGraph MCP server v0.7.1 running on stdio (${BASE_URL})`
4910
+ `MindGraph MCP server v${"0.15.0"} running on stdio (${BASE_URL})`
4826
4911
  );
4827
4912
  }
4828
4913
  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 +4918,14 @@ var init_src = __esm({
4833
4918
  import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
4834
4919
  import_types = require("@modelcontextprotocol/sdk/types.js");
4835
4920
  init_dist();
4921
+ init_hook_env();
4836
4922
  init_tools();
4837
4923
  init_generated_tools();
4838
4924
  init_governance();
4839
4925
  init_resources();
4840
4926
  API_KEY = process.env.MINDGRAPH_API_KEY;
4841
4927
  BASE_URL = process.env.MINDGRAPH_BASE_URL || "https://api.mindgraph.cloud";
4842
- AGENT_ID = process.env.MINDGRAPH_AGENT_ID || "mcp";
4928
+ AGENT_ID = process.env.MINDGRAPH_AGENT_ID || loadHookEnv().agentId || stableAgentId();
4843
4929
  ORG_ID = process.env.MINDGRAPH_ORG_ID;
4844
4930
  PROFILE = process.env.MINDGRAPH_PROFILE || "general";
4845
4931
  HARNESS = process.env.MINDGRAPH_HARNESS || "generic";
@@ -4961,7 +5047,7 @@ Capture knowledge when the user shares something worth remembering:
4961
5047
  server = new import_server.Server(
4962
5048
  {
4963
5049
  name: "mindgraph",
4964
- version: "0.14.9"
5050
+ version: "0.15.0"
4965
5051
  },
4966
5052
  {
4967
5053
  capabilities: {
@@ -5182,46 +5268,10 @@ ${conversation}`
5182
5268
  var fs5 = __toESM(require("fs"));
5183
5269
  var path5 = __toESM(require("path"));
5184
5270
  var os4 = __toESM(require("os"));
5185
- var crypto4 = __toESM(require("crypto"));
5271
+ var crypto5 = __toESM(require("crypto"));
5186
5272
  var import_child_process = require("child_process");
5187
5273
  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
- }
5274
+ init_hook_env();
5225
5275
 
5226
5276
  // src/cli-args.ts
5227
5277
  function parseArgs(argv) {
@@ -5292,12 +5342,14 @@ function classifyMcpAddFailure(e) {
5292
5342
  }
5293
5343
 
5294
5344
  // src/hook-core.ts
5295
- var import_node_crypto2 = __toESM(require("crypto"));
5345
+ var import_node_crypto3 = __toESM(require("crypto"));
5296
5346
  var import_node_fs3 = __toESM(require("fs"));
5297
5347
  var import_node_os3 = __toESM(require("os"));
5298
5348
  var import_node_path3 = __toESM(require("path"));
5299
5349
  var import_node_child_process2 = require("child_process");
5300
5350
  init_codegraph();
5351
+ init_code_tool();
5352
+ init_hook_env();
5301
5353
  var NOOP = { kind: "noop" };
5302
5354
  function defaultLedger(sessionId, options) {
5303
5355
  return {
@@ -5319,7 +5371,7 @@ function runtimeDir(options) {
5319
5371
  }
5320
5372
  function ledgerPath(sessionId, options) {
5321
5373
  const harness = "harness" in options ? options.harness : "claude-code";
5322
- const key = import_node_crypto2.default.createHash("sha256").update(`${harness}\0${sessionId}`).digest("hex");
5374
+ const key = import_node_crypto3.default.createHash("sha256").update(`${harness}\0${sessionId}`).digest("hex");
5323
5375
  return import_node_path3.default.join(runtimeDir(options), `${key}.json`);
5324
5376
  }
5325
5377
  function waitBriefly(ms) {
@@ -5334,14 +5386,14 @@ function withLedger(sessionId, options, update) {
5334
5386
  }
5335
5387
  const lock = `${file}.lock`;
5336
5388
  let lockFd;
5337
- for (let attempt = 0; attempt < 50; attempt++) {
5389
+ for (let attempt = 0; attempt < 400; attempt++) {
5338
5390
  try {
5339
5391
  lockFd = import_node_fs3.default.openSync(lock, "wx", 384);
5340
5392
  break;
5341
5393
  } catch (error) {
5342
5394
  if (error.code !== "EEXIST") throw error;
5343
5395
  try {
5344
- if (Date.now() - import_node_fs3.default.statSync(lock).mtimeMs > 1e4) import_node_fs3.default.unlinkSync(lock);
5396
+ if (Date.now() - import_node_fs3.default.statSync(lock).mtimeMs > 3e3) import_node_fs3.default.unlinkSync(lock);
5345
5397
  } catch {
5346
5398
  }
5347
5399
  waitBriefly(10);
@@ -5361,7 +5413,7 @@ function withLedger(sessionId, options, update) {
5361
5413
  }
5362
5414
  const result = update(ledger);
5363
5415
  ledger.updatedAt = Date.now();
5364
- const temporary = `${file}.${process.pid}.${import_node_crypto2.default.randomUUID()}.tmp`;
5416
+ const temporary = `${file}.${process.pid}.${import_node_crypto3.default.randomUUID()}.tmp`;
5365
5417
  import_node_fs3.default.writeFileSync(temporary, `${JSON.stringify(ledger)}
5366
5418
  `, { mode: 384 });
5367
5419
  import_node_fs3.default.renameSync(temporary, file);
@@ -5396,17 +5448,13 @@ function worktreeState(cwd) {
5396
5448
  const status = gitValue(cwd, ["status", "--porcelain"]);
5397
5449
  return status === void 0 ? "unknown" : status ? "dirty" : "clean";
5398
5450
  }
5399
- async function invocationContext(input, options) {
5400
- const repository = await new CodegraphAdapter({
5401
- cwd: input.cwd,
5402
- workspaceFile: options.workspaceFile
5403
- }).resolveRepository();
5451
+ function invocationContext(input, options, repoId) {
5404
5452
  return {
5405
5453
  harness: options.harness,
5406
5454
  harnessSessionId: input.session_id,
5407
5455
  harnessTurnId: input.turn_id,
5408
5456
  cwd: input.cwd,
5409
- repoId: repository.repoId,
5457
+ repoId,
5410
5458
  branch: gitValue(input.cwd, ["branch", "--show-current"]),
5411
5459
  commit: gitValue(input.cwd, ["rev-parse", "HEAD"]),
5412
5460
  worktreeState: worktreeState(input.cwd),
@@ -5414,6 +5462,9 @@ async function invocationContext(input, options) {
5414
5462
  injectedBy: "hook"
5415
5463
  };
5416
5464
  }
5465
+ function resolveAgentId(options) {
5466
+ return options.agentId || process.env.MINDGRAPH_AGENT_ID || stableAgentId();
5467
+ }
5417
5468
  function object(value) {
5418
5469
  return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
5419
5470
  }
@@ -5441,56 +5492,198 @@ function number(value) {
5441
5492
  function string(value) {
5442
5493
  return typeof value === "string" && value ? value : void 0;
5443
5494
  }
5444
- function boundedBrief(brief) {
5445
- const encoded = JSON.stringify(brief, null, 2);
5446
- const capped = encoded.length <= 9e3 ? encoded : `${encoded.slice(0, 8900)}
5495
+ var BRIEF_CHAR_BUDGET = 9e3;
5496
+ function renderBrief(brief, meta) {
5497
+ let bounded = false;
5498
+ const clipTracked = (value, max) => {
5499
+ const text = string(value);
5500
+ if (!text) return void 0;
5501
+ if (text.length <= max) return text;
5502
+ bounded = true;
5503
+ return `${text.slice(0, max - 1)}\u2026`;
5504
+ };
5505
+ const nodeLine = (value, summaryMax) => {
5506
+ const node = object(value);
5507
+ if (!node) return void 0;
5508
+ const label = clipTracked(node.label, 160) || "(unlabeled)";
5509
+ const summary = clipTracked(node.summary, summaryMax);
5510
+ const uid = string(node.uid);
5511
+ return `- ${label}${uid ? ` [${uid}]` : ""}${summary && summary !== label ? ` \u2014 ${summary}` : ""}`;
5512
+ };
5513
+ const lines = [
5514
+ "MindGraph durable work brief (authoritative across sessions/harnesses):"
5515
+ ];
5516
+ if (meta.scopeNote) lines.push(meta.scopeNote);
5517
+ const task = object(brief?.task);
5518
+ const scopedOut = number(brief?.scoped_out_tasks);
5519
+ if (!task) {
5520
+ lines.push("No durable task is eligible for this session's scope.");
5521
+ if (scopedOut) {
5522
+ lines.push(
5523
+ `${scopedOut} durable task(s) exist outside this repository scope \u2014 resume one explicitly by task_uid if that is intended.`
5524
+ );
5525
+ }
5526
+ } else {
5527
+ const props = object(task.props);
5528
+ const details = [
5529
+ `status ${string(props?.status) || "unknown"}`,
5530
+ string(props?.priority) ? `priority ${string(props?.priority)}` : void 0,
5531
+ number(task.version) !== void 0 ? `v${number(task.version)}` : void 0
5532
+ ].filter(Boolean);
5533
+ lines.push(
5534
+ `Task: ${clipTracked(task.label, 200) || "(unlabeled)"} [${string(task.uid) || "?"}] \u2014 ${details.join(", ")}`
5535
+ );
5536
+ const description = clipTracked(props?.description, 400);
5537
+ if (description) lines.push(` ${description}`);
5538
+ const reason = string(brief?.selection_reason);
5539
+ if (reason) lines.push(`Selection: ${reason}`);
5540
+ const lease = object(brief?.claim) || object(brief?.lease);
5541
+ if (meta.claimed) {
5542
+ const epoch = number(lease?.lease_epoch);
5543
+ lines.push(
5544
+ `Claimed: yes${epoch !== void 0 ? ` (lease epoch ${epoch})` : ""} \u2014 this session holds the lease.`
5545
+ );
5546
+ } else if (meta.claimFailed) {
5547
+ lines.push(
5548
+ "Claimed: no \u2014 the claim attempt failed (another session may hold the lease). Treat as context; claim_task explicitly before mutating it."
5549
+ );
5550
+ } else {
5551
+ lines.push(
5552
+ "Claimed: no \u2014 backlog context, not owned. claim_task deliberately before starting this work."
5553
+ );
5554
+ }
5555
+ const goal = nodeLine(brief?.goal, 200);
5556
+ if (goal) lines.push(`Goal:
5557
+ ${goal}`);
5558
+ const steps = Array.isArray(brief?.next_steps) ? brief.next_steps : [];
5559
+ if (steps.length > 5) bounded = true;
5560
+ const stepLines = steps.slice(0, 5).map((step) => nodeLine(step, 160)).filter(Boolean);
5561
+ if (stepLines.length) lines.push(`Next steps:
5562
+ ${stepLines.join("\n")}`);
5563
+ const blockers = Array.isArray(brief?.blockers) ? brief.blockers : [];
5564
+ if (blockers.length > 5) bounded = true;
5565
+ const blockerLines = blockers.slice(0, 5).map((blocker) => nodeLine(blocker, 200)).filter(Boolean);
5566
+ if (blockerLines.length) lines.push(`Blockers:
5567
+ ${blockerLines.join("\n")}`);
5568
+ const active = object(brief?.active_execution);
5569
+ if (active) {
5570
+ const iteration = number(active.iteration) ?? number(object(active.props)?.iteration);
5571
+ lines.push(
5572
+ `Active execution: [${string(active.uid) || "?"}] iteration ${iteration ?? "?"} still running from a prior session \u2014 checkpoint or abandon it.`
5573
+ );
5574
+ }
5575
+ const poison = object(brief?.poison_task) || string(brief?.warning);
5576
+ if (poison) {
5577
+ lines.push(
5578
+ `Warning: ${typeof poison === "string" ? clipTracked(poison, 300) : "recent executions of this task repeatedly failed \u2014 investigate before retrying."}`
5579
+ );
5580
+ }
5581
+ }
5582
+ const optional = [];
5583
+ const knowledge = Array.isArray(brief?.knowledge) ? brief.knowledge : [];
5584
+ if (knowledge.length > 5) bounded = true;
5585
+ const knowledgeLines = knowledge.slice(0, 5).map((item) => nodeLine(item, 350)).filter(Boolean);
5586
+ if (knowledgeLines.length) {
5587
+ optional.push(`Relevant knowledge:
5588
+ ${knowledgeLines.join("\n")}`);
5589
+ }
5590
+ const targets = Array.isArray(brief?.code_targets) ? brief.code_targets : [];
5591
+ const targetLabels = targets.slice(0, 10).map((target) => clipTracked(object(target)?.label, 120)).filter(Boolean);
5592
+ if (targetLabels.length) {
5593
+ const extra = targets.length - targetLabels.length;
5594
+ if (extra > 0) bounded = true;
5595
+ optional.push(
5596
+ `Code targets: ${targetLabels.join(", ")}${extra > 0 ? ` (+${extra} more)` : ""}`
5597
+ );
5598
+ }
5599
+ const footer = task ? "Use the harness todo list only for local execution details; do not duplicate this durable Task there." : void 0;
5600
+ let used = lines.join("\n").length + (footer ? footer.length + 1 : 0);
5601
+ for (const section of optional) {
5602
+ if (used + section.length + 1 > BRIEF_CHAR_BUDGET) {
5603
+ bounded = true;
5604
+ break;
5605
+ }
5606
+ lines.push(section);
5607
+ used += section.length + 1;
5608
+ }
5609
+ if (bounded) {
5610
+ lines.push(
5611
+ "\u2026[bounded work brief truncated \u2014 fetch full node content by uid if needed]"
5612
+ );
5613
+ }
5614
+ if (footer) lines.push(footer);
5615
+ const rendered = lines.join("\n");
5616
+ return rendered.length <= BRIEF_CHAR_BUDGET ? rendered : `${rendered.slice(0, BRIEF_CHAR_BUDGET - 40)}
5447
5617
  \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
5618
  }
5454
5619
  async function repositoryScopeUids(input, client2, options, agentId) {
5455
- if (!client2.entity) return [];
5620
+ if (!client2.entity) return { uids: [], failures: [] };
5456
5621
  const adapter = new CodegraphAdapter({
5457
5622
  cwd: input.cwd,
5458
5623
  workspaceFile: options.workspaceFile
5459
5624
  });
5460
5625
  const configured = await adapter.resolveRepositories();
5461
5626
  const repositories = configured.length > 0 ? configured : [await adapter.resolveRepository()];
5627
+ const failures = [];
5462
5628
  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
- )
5629
+ repositories.map(async (repository) => {
5630
+ try {
5631
+ const payload = object(
5632
+ await client2.entity({
5633
+ action: "resolve_identity",
5634
+ identity: {
5635
+ namespace: "external.code",
5636
+ key_version: 1,
5637
+ key: repositoryIdentityKey(repository)
5638
+ },
5639
+ agent_id: agentId
5640
+ })
5641
+ );
5642
+ if (payload?.status === "existing" && string(payload.uid)) {
5643
+ return string(payload.uid);
5644
+ }
5645
+ if (payload?.status === "absent") {
5646
+ const anchored = await anchorRepository(
5647
+ client2,
5648
+ repository,
5649
+ repository.spaceUid ?? `space:agent:${agentId}`,
5650
+ agentId
5651
+ );
5652
+ if (anchored?.uid) return anchored.uid;
5653
+ failures.push(
5654
+ `${repository.repoId}: anchor ${anchored?.status ?? "failed"}`
5655
+ );
5656
+ return void 0;
5657
+ }
5658
+ failures.push(
5659
+ `${repository.repoId}: ${string(payload?.status) || "unresolved"}`
5660
+ );
5661
+ return void 0;
5662
+ } catch (error) {
5663
+ failures.push(
5664
+ `${repository.repoId}: ${error?.message || "resolve failed"}`
5665
+ );
5666
+ return void 0;
5667
+ }
5668
+ })
5474
5669
  );
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
- ];
5670
+ return {
5671
+ uids: [...new Set(resolved.filter((uid) => Boolean(uid)))],
5672
+ failures
5673
+ };
5483
5674
  }
5484
- function isOwnPriorWork(brief, agentId) {
5675
+ function isOwnPriorWork(brief, agentId, harness) {
5485
5676
  const reason = string(brief?.selection_reason);
5486
5677
  if (reason === "owned_live_lease" || reason === "same_agent_session_rebind") {
5487
5678
  return true;
5488
5679
  }
5489
- return string(object(brief?.lease)?.lease_owner_agent_id) === agentId;
5680
+ const owner = string(object(brief?.lease)?.lease_owner_agent_id);
5681
+ return owner === agentId || owner === harness;
5490
5682
  }
5491
5683
  async function sessionStart(input, client2, options) {
5492
- const agentId = options.agentId || process.env.MINDGRAPH_AGENT_ID || options.harness;
5684
+ const agentId = resolveAgentId(options);
5493
5685
  const harnessLabel = options.harness === "claude-code" ? "Claude Code" : "Codex";
5686
+ const nowSeconds = (options.now || (() => Date.now() / 1e3))();
5494
5687
  const opened = object(
5495
5688
  await client2.session({
5496
5689
  action: "open",
@@ -5502,7 +5695,21 @@ async function sessionStart(input, client2, options) {
5502
5695
  })
5503
5696
  );
5504
5697
  const sessionUid = string(opened?.uid);
5505
- const scopeUids = await repositoryScopeUids(input, client2, options, agentId);
5698
+ const baselineWorktree = worktreeState(input.cwd);
5699
+ if (sessionUid) {
5700
+ withLedger(input.session_id, options, (ledger) => {
5701
+ ledger.sessionUid = sessionUid;
5702
+ ledger.baselineWorktree = baselineWorktree;
5703
+ return void 0;
5704
+ });
5705
+ }
5706
+ const scope = await repositoryScopeUids(input, client2, options, agentId);
5707
+ if (scope.failures.length > 0) {
5708
+ console.error(
5709
+ `mindgraph-hook: ${scope.failures.length} repository scope(s) failed to resolve (${scope.failures.join("; ")}); resume runs ${scope.uids.length > 0 ? "narrowed" : "UNSCOPED"}`
5710
+ );
5711
+ }
5712
+ const scopeUids = scope.uids;
5506
5713
  let brief = object(
5507
5714
  await client2.plan({
5508
5715
  action: "resume_work",
@@ -5513,9 +5720,19 @@ async function sessionStart(input, client2, options) {
5513
5720
  );
5514
5721
  const task = object(brief?.task);
5515
5722
  const lease = object(brief?.lease);
5516
- if (sessionUid && string(task?.uid) && number(task?.version) && isOwnPriorWork(brief, agentId)) {
5723
+ const prior = readLedger(input.session_id, options);
5724
+ const leaseExpiry = number(lease?.lease_expires_at);
5725
+ const leaseLive = leaseExpiry !== void 0 && leaseExpiry > nowSeconds;
5726
+ const heldByThisSession = leaseLive && prior.taskUid !== void 0 && prior.taskUid === string(task?.uid) && prior.leaseEpoch === number(lease?.lease_epoch);
5727
+ let claimed;
5728
+ let claimFailed = false;
5729
+ if (sessionUid && string(task?.uid) && number(task?.version) && isOwnPriorWork(brief, agentId, options.harness) && // A LIVE lease belongs to whichever session claimed it — claiming from
5730
+ // here would fence that session out mid-work (the released server
5731
+ // rebinds same-owner live leases without allow_takeover). Re-claim live
5732
+ // leases only when this very session already holds them (re-entry).
5733
+ (!leaseLive || heldByThisSession)) {
5517
5734
  try {
5518
- const claimed = object(
5735
+ claimed = object(
5519
5736
  await client2.plan({
5520
5737
  action: "claim_task",
5521
5738
  task_uid: task.uid,
@@ -5525,35 +5742,63 @@ async function sessionStart(input, client2, options) {
5525
5742
  agent_id: agentId
5526
5743
  })
5527
5744
  );
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
5745
  } catch {
5746
+ claimFailed = true;
5747
+ }
5748
+ if (claimed) {
5749
+ try {
5750
+ const reResumed = object(
5751
+ await client2.plan({
5752
+ action: "resume_work",
5753
+ task_uid: task.uid,
5754
+ session_uid: sessionUid,
5755
+ ...scopeUids.length > 0 ? { scope_uids: scopeUids } : {},
5756
+ agent_id: agentId
5757
+ })
5758
+ );
5759
+ if (reResumed) brief = reResumed;
5760
+ } catch {
5761
+ }
5762
+ if (brief) brief.claim = claimed;
5539
5763
  }
5540
5764
  }
5541
- const briefText = boundedBrief(brief || { status: "no_eligible_work" });
5542
- const briefHash = import_node_crypto2.default.createHash("sha256").update(briefText).digest("hex");
5765
+ const stillHeld = !claimed && claimFailed && heldByThisSession;
5766
+ 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;
5767
+ const briefText = renderBrief(brief || { status: "no_eligible_work" }, {
5768
+ claimed: Boolean(claimed) || stillHeld,
5769
+ claimFailed: claimFailed && !stillHeld,
5770
+ scopeNote
5771
+ });
5772
+ const briefHash = import_node_crypto3.default.createHash("sha256").update(briefText).digest("hex");
5543
5773
  const shouldInject = withLedger(input.session_id, options, (ledger) => {
5544
5774
  const changed = ledger.lastBriefHash !== briefHash;
5545
5775
  const reinjectCompact = options.reinjectUnchangedCompact && input.source === "compact";
5546
5776
  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);
5777
+ ledger.baselineWorktree = baselineWorktree;
5778
+ if (claimed) {
5779
+ const claimLease = object(brief?.claim) || object(brief?.lease);
5780
+ const selectedTask = object(brief?.task);
5781
+ ledger.taskUid = string(selectedTask?.uid);
5782
+ ledger.taskVersion = number(claimed.task_version) ?? number(selectedTask?.version) ?? void 0;
5783
+ ledger.leaseEpoch = number(claimLease?.lease_epoch);
5784
+ ledger.leaseExpiresAt = number(claimLease?.lease_expires_at);
5785
+ ledger.lastLeaseRenewalAt = nowSeconds;
5786
+ const running = [
5787
+ object(brief?.active_execution),
5788
+ ...Array.isArray(brief?.recent_executions) ? brief.recent_executions.map(object) : []
5789
+ ].find(
5790
+ (execution) => execution && (string(object(execution.props)?.status) ?? string(execution.status)) === "running"
5791
+ );
5792
+ ledger.executionUid = running ? string(running.uid) : void 0;
5793
+ } else if (claimFailed && ledger.taskUid !== void 0 && ledger.taskUid === string(object(brief?.task)?.uid) && ledger.lastLeaseRenewalAt !== void 0) {
5794
+ } else {
5795
+ ledger.taskUid = void 0;
5796
+ ledger.taskVersion = void 0;
5797
+ ledger.leaseEpoch = void 0;
5798
+ ledger.leaseExpiresAt = void 0;
5799
+ ledger.executionUid = void 0;
5800
+ ledger.lastLeaseRenewalAt = void 0;
5801
+ }
5557
5802
  ledger.lastBriefHash = briefHash;
5558
5803
  return changed || reinjectCompact;
5559
5804
  });
@@ -5562,82 +5807,173 @@ async function sessionStart(input, client2, options) {
5562
5807
  async function preTool(input, options) {
5563
5808
  const original = { ...input.tool_input || {} };
5564
5809
  const ledger = readLedger(input.session_id, options);
5810
+ let repoId = ledger.repoContextCwd === input.cwd && ledger.repoContextAt !== void 0 && Date.now() - ledger.repoContextAt < 3e5 ? ledger.repoContextRepoId : void 0;
5811
+ if (!repoId) {
5812
+ repoId = (await new CodegraphAdapter({
5813
+ cwd: input.cwd,
5814
+ workspaceFile: options.workspaceFile
5815
+ }).resolveRepository()).repoId;
5816
+ withLedger(input.session_id, options, (fresh) => {
5817
+ fresh.repoContextCwd = input.cwd;
5818
+ fresh.repoContextRepoId = repoId;
5819
+ fresh.repoContextAt = Date.now();
5820
+ return void 0;
5821
+ });
5822
+ }
5565
5823
  const updatedInput = {
5566
5824
  ...original,
5567
- invocation_context: await invocationContext(input, options)
5825
+ invocation_context: invocationContext(input, options, repoId)
5568
5826
  };
5569
5827
  if (ledger.sessionUid) updatedInput.session_uid = ledger.sessionUid;
5570
5828
  if (ledger.taskUid && updatedInput.task_uid === void 0) {
5571
5829
  updatedInput.task_uid = ledger.taskUid;
5572
5830
  }
5573
- if (ledger.taskUid && input.tool_name?.includes("mindgraph_capture") && updatedInput.work_uid === void 0) {
5831
+ const modelTask = string(original.task_uid);
5832
+ const targetsLedgerTask = ledger.taskUid !== void 0 && (modelTask === void 0 || modelTask === ledger.taskUid);
5833
+ const isCapture = Boolean(input.tool_name?.includes("mindgraph_capture"));
5834
+ const fencedAction = Boolean(input.tool_name?.includes("mindgraph_plan")) && FENCED_PLAN_ACTIONS.has(string(original.action) || "");
5835
+ if (ledger.taskUid && isCapture && updatedInput.work_uid === void 0) {
5574
5836
  updatedInput.work_uid = ledger.taskUid;
5575
5837
  }
5576
- if (ledger.executionUid && updatedInput.execution_uid === void 0) {
5838
+ if (targetsLedgerTask && (fencedAction || isCapture) && ledger.executionUid && updatedInput.execution_uid === void 0) {
5577
5839
  updatedInput.execution_uid = ledger.executionUid;
5578
5840
  }
5579
- if (ledger.leaseEpoch !== void 0 && updatedInput.lease_epoch === void 0) {
5841
+ if (targetsLedgerTask && fencedAction && ledger.leaseEpoch !== void 0 && updatedInput.lease_epoch === void 0) {
5580
5842
  updatedInput.lease_epoch = ledger.leaseEpoch;
5581
5843
  }
5582
- if (ledger.taskVersion !== void 0 && updatedInput.expected_version === void 0) {
5844
+ if (targetsLedgerTask && fencedAction && ledger.taskVersion !== void 0 && updatedInput.expected_version === void 0) {
5583
5845
  updatedInput.expected_version = ledger.taskVersion;
5584
5846
  }
5585
5847
  return { kind: "rewrite", updatedInput };
5586
5848
  }
5849
+ var FENCED_PLAN_ACTIONS = /* @__PURE__ */ new Set([
5850
+ "claim_task",
5851
+ "heartbeat",
5852
+ "start_iteration",
5853
+ "checkpoint_iteration",
5854
+ "block_task",
5855
+ "complete_task",
5856
+ "abandon_iteration"
5857
+ ]);
5587
5858
  async function postTool(input, client2, options) {
5588
5859
  const name = input.tool_name || "";
5589
5860
  const args = input.tool_input || {};
5590
5861
  const action = string(args.action);
5591
5862
  const payload = responsePayload(input.tool_response);
5592
5863
  const nowSeconds = (options.now || (() => Date.now() / 1e3))();
5593
- let heartbeat;
5864
+ let renewal;
5594
5865
  withLedger(input.session_id, options, (ledger) => {
5595
5866
  ledger.materialToolCalls += 1;
5596
- if (["Edit", "Write", "NotebookEdit", "apply_patch"].includes(name) || name === "Bash" && action !== "read") {
5867
+ if (["Edit", "Write", "NotebookEdit", "apply_patch"].includes(name)) {
5597
5868
  ledger.mutatingOperations += 1;
5598
5869
  }
5599
- if (name.includes("mindgraph_capture") && !payload?.error) {
5600
- ledger.memoryWritten = true;
5870
+ if (!payload?.error) {
5871
+ if (name.includes("mindgraph_capture") && ["lesson", "journal"].includes(action || "") || name.includes("mindgraph_commit") && action === "resolve_decision" || name.includes("mindgraph_plan") && action === "assess_risk") {
5872
+ ledger.memoryWritten = true;
5873
+ }
5601
5874
  }
5602
5875
  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;
5876
+ const targetTask = string(args.task_uid) || ledger.taskUid;
5877
+ const aboutLedgerTask = targetTask !== void 0 && targetTask === ledger.taskUid;
5878
+ if (!payload?.error) {
5879
+ if (action === "claim_task" && targetTask) {
5880
+ const rebindToOther = targetTask !== ledger.taskUid;
5881
+ ledger.taskUid = targetTask;
5882
+ ledger.taskVersion = number(payload?.task_version) ?? number(payload?.version) ?? (rebindToOther ? void 0 : ledger.taskVersion);
5883
+ ledger.leaseEpoch = number(payload?.lease_epoch) ?? (rebindToOther ? void 0 : ledger.leaseEpoch);
5884
+ ledger.leaseExpiresAt = number(payload?.lease_expires_at) ?? (rebindToOther ? void 0 : ledger.leaseExpiresAt);
5885
+ if (rebindToOther) ledger.executionUid = void 0;
5886
+ ledger.lastLeaseRenewalAt = nowSeconds;
5887
+ }
5888
+ if (action === "start_iteration") {
5889
+ ledger.executionUid = string(payload?.execution_uid) || ledger.executionUid;
5890
+ }
5891
+ if (["checkpoint_iteration", "block_task", "complete_task", "abandon_iteration"].includes(
5892
+ action || ""
5893
+ )) {
5894
+ ledger.iterationCheckpointed = true;
5895
+ if (action !== "block_task") ledger.executionUid = void 0;
5896
+ }
5897
+ if (action === "complete_task" && aboutLedgerTask) {
5898
+ ledger.taskUid = void 0;
5899
+ ledger.taskVersion = void 0;
5900
+ ledger.leaseEpoch = void 0;
5901
+ ledger.leaseExpiresAt = void 0;
5902
+ ledger.lastLeaseRenewalAt = void 0;
5903
+ } else if (aboutLedgerTask && action !== "claim_task") {
5904
+ ledger.taskVersion = number(payload?.task_version) ?? (string(payload?.uid) === ledger.taskUid ? number(payload?.version) : void 0) ?? ledger.taskVersion;
5905
+ ledger.leaseEpoch = number(payload?.lease_epoch) ?? ledger.leaseEpoch;
5906
+ if (number(payload?.lease_expires_at) !== void 0) {
5907
+ ledger.leaseExpiresAt = number(payload?.lease_expires_at);
5908
+ ledger.lastLeaseRenewalAt = nowSeconds;
5909
+ }
5910
+ }
5911
+ } else if (aboutLedgerTask) {
5912
+ ledger.taskVersion = number(payload?.current_version) ?? ledger.taskVersion;
5913
+ ledger.leaseEpoch = number(payload?.current_epoch) ?? ledger.leaseEpoch;
5914
+ ledger.leaseExpiresAt = number(payload?.lease_expires_at) ?? ledger.leaseExpiresAt;
5915
+ }
5615
5916
  }
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
- };
5917
+ 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
5918
+ // may renew: 0.14.9 ledgers carry adopted-but-never-claimed fence
5919
+ // state, and a lapse-reclaim from one would auto-seize a task this
5920
+ // session never owned — the incident class all of this exists to kill.
5921
+ ledger.lastLeaseRenewalAt !== void 0) {
5922
+ const expiresIn = ledger.leaseExpiresAt - nowSeconds;
5923
+ const sinceRenewal = nowSeconds - ledger.lastLeaseRenewalAt;
5924
+ if (expiresIn <= 0) {
5925
+ renewal = {
5926
+ kind: "reclaim",
5927
+ taskUid: ledger.taskUid,
5928
+ sessionUid: ledger.sessionUid,
5929
+ taskVersion: ledger.taskVersion,
5930
+ leaseEpoch: ledger.leaseEpoch
5931
+ };
5932
+ } else if (expiresIn <= 60 || sinceRenewal >= 180) {
5933
+ renewal = {
5934
+ kind: "heartbeat",
5935
+ taskUid: ledger.taskUid,
5936
+ sessionUid: ledger.sessionUid,
5937
+ taskVersion: ledger.taskVersion,
5938
+ leaseEpoch: ledger.leaseEpoch
5939
+ };
5940
+ }
5623
5941
  }
5624
5942
  return void 0;
5625
5943
  });
5626
- if (heartbeat) {
5944
+ if (renewal) {
5945
+ const active = renewal;
5627
5946
  try {
5628
5947
  const response = object(
5629
- await client2.plan({
5948
+ active.kind === "reclaim" ? (
5949
+ // The lease already lapsed: a heartbeat can only 409. Re-claim
5950
+ // rebinds the same owner's expired lease (bumping the epoch) so
5951
+ // durable tracking survives a >TTL quiet stretch mid-session.
5952
+ await client2.plan({
5953
+ action: "claim_task",
5954
+ task_uid: active.taskUid,
5955
+ session_uid: active.sessionUid,
5956
+ expected_version: active.taskVersion,
5957
+ idempotency_key: `hook-reclaim:${input.session_id}:${active.taskUid}:${active.leaseEpoch}:${Math.floor(nowSeconds / 60)}`,
5958
+ agent_id: resolveAgentId(options)
5959
+ })
5960
+ ) : await client2.plan({
5630
5961
  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
5962
+ task_uid: active.taskUid,
5963
+ session_uid: active.sessionUid,
5964
+ expected_version: active.taskVersion,
5965
+ lease_epoch: active.leaseEpoch,
5966
+ idempotency_key: `hook-heartbeat:${input.session_id}:${active.taskUid}:${active.leaseEpoch}:${active.taskVersion}:${Math.floor(nowSeconds / 60)}`,
5967
+ agent_id: resolveAgentId(options)
5637
5968
  })
5638
5969
  );
5639
5970
  withLedger(input.session_id, options, (ledger) => {
5640
- ledger.leaseExpiresAt = number(response?.lease_expires_at) || ledger.leaseExpiresAt;
5971
+ if (payloadIsError(response)) return void 0;
5972
+ if (ledger.taskUid !== active.taskUid) return void 0;
5973
+ ledger.taskVersion = number(response?.task_version) ?? ledger.taskVersion;
5974
+ ledger.leaseEpoch = number(response?.lease_epoch) ?? ledger.leaseEpoch;
5975
+ ledger.leaseExpiresAt = number(response?.lease_expires_at) ?? ledger.leaseExpiresAt;
5976
+ ledger.lastLeaseRenewalAt = nowSeconds;
5641
5977
  return void 0;
5642
5978
  });
5643
5979
  } catch {
@@ -5645,6 +5981,9 @@ async function postTool(input, client2, options) {
5645
5981
  }
5646
5982
  return NOOP;
5647
5983
  }
5984
+ function payloadIsError(payload) {
5985
+ return Boolean(payload && payload.error);
5986
+ }
5648
5987
  function stop(input, options) {
5649
5988
  if (input.stop_hook_active) return NOOP;
5650
5989
  return withLedger(input.session_id, options, (ledger) => {
@@ -5675,17 +6014,20 @@ async function sessionEnd(input, client2, options) {
5675
6014
  release_lease: true,
5676
6015
  summary: `${harnessLabel} SessionEnd: ${input.reason || "interrupted"}`,
5677
6016
  idempotency_key: `session-end:${input.session_id}:${ledger.executionUid}`,
5678
- agent_id: options.agentId || options.harness
6017
+ agent_id: resolveAgentId(options)
5679
6018
  });
5680
6019
  } catch {
5681
6020
  }
5682
6021
  }
5683
- if (ledger.sessionUid) {
6022
+ const abandonRan = Boolean(
6023
+ ledger.executionUid && ledger.taskUid && ledger.sessionUid && ledger.taskVersion !== void 0 && ledger.leaseEpoch !== void 0
6024
+ );
6025
+ if (ledger.sessionUid && (options.harness !== "codex" || !abandonRan)) {
5684
6026
  try {
5685
6027
  await client2.session({
5686
6028
  action: "close",
5687
6029
  session_uid: ledger.sessionUid,
5688
- agent_id: options.agentId || options.harness
6030
+ agent_id: resolveAgentId(options)
5689
6031
  });
5690
6032
  } catch {
5691
6033
  }
@@ -5795,7 +6137,7 @@ async function runCodexHook(input, client2, options = {}) {
5795
6137
  var import_node_fs4 = __toESM(require("fs"));
5796
6138
  var import_node_os4 = __toESM(require("os"));
5797
6139
  var import_node_path4 = __toESM(require("path"));
5798
- var import_node_crypto3 = __toESM(require("crypto"));
6140
+ var import_node_crypto4 = __toESM(require("crypto"));
5799
6141
  var OWNER_MARKER = "--owner mindgraph";
5800
6142
  var RUNNER_RELATIVE = ".mindgraph/bin/mindgraph-hook.cjs";
5801
6143
  function command(harness) {
@@ -5839,11 +6181,14 @@ function desiredEntries(harness) {
5839
6181
  // inherited 20s edge in both harnesses: the hooks correctly failed
5840
6182
  // open, but no brief reached the model. The same installed runner
5841
6183
  // completed in 16.84s once warm. Keep a 30s cold-tenant margin.
6184
+ // The declared Codex context limit must not undercut the brief's own
6185
+ // 9,000-char budget: at 3,000 the harness applied a SECOND, tighter
6186
+ // truncation the renderer knew nothing about.
5842
6187
  hooks: [
5843
6188
  ownedHook(
5844
6189
  harness,
5845
6190
  30,
5846
- harness === "codex" ? 3e3 : void 0
6191
+ harness === "codex" ? 1e4 : void 0
5847
6192
  )
5848
6193
  ]
5849
6194
  }
@@ -5851,13 +6196,23 @@ function desiredEntries(harness) {
5851
6196
  PreToolUse: [
5852
6197
  {
5853
6198
  matcher: "mcp__mindgraph__.*",
5854
- hooks: [ownedHook(harness, 3)]
6199
+ // Repository resolution over a large multi-repo workspace plus three
6200
+ // git probes can exceed 3s on the first (uncached) call; a killed
6201
+ // PreToolUse means the call goes through untagged.
6202
+ hooks: [ownedHook(harness, 5)]
5855
6203
  }
5856
6204
  ],
5857
- PostToolUse: [{ matcher: ".*", hooks: [ownedHook(harness, 5)] }],
6205
+ // PostToolUse awaits the lease renewal round-trip, which fires exactly
6206
+ // when the tenant is cold (post-idle reclaim) — 5s killed it mid-claim.
6207
+ PostToolUse: [{ matcher: ".*", hooks: [ownedHook(harness, 10)] }],
5858
6208
  Stop: [{ hooks: [ownedHook(harness, 5)] }],
6209
+ // SessionEnd makes up to two sequential cloud calls (abandon_iteration +
6210
+ // session close) — the SAME cold-tenant latency SessionStart needs 30s
6211
+ // for. Claude honors the configured budget; Codex clamps SessionEnd to a
6212
+ // 3s platform cap regardless of this value (see codex-hooks.ts wire
6213
+ // map), so the codex adapter instead makes at most ONE call there.
5859
6214
  SessionEnd: [
5860
- { hooks: [ownedHook(harness, harness === "codex" ? 3 : 8)] }
6215
+ { hooks: [ownedHook(harness, harness === "codex" ? 3 : 30)] }
5861
6216
  ]
5862
6217
  };
5863
6218
  return harness === "claude-code" ? {
@@ -5878,7 +6233,7 @@ function entryHasOwnedHook(value) {
5878
6233
  }
5879
6234
  function writeSettings(file, settings) {
5880
6235
  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`;
6236
+ const temporary = `${file}.${process.pid}.${import_node_crypto4.default.randomUUID()}.tmp`;
5882
6237
  import_node_fs4.default.writeFileSync(temporary, `${JSON.stringify(settings, null, 2)}
5883
6238
  `, {
5884
6239
  mode: 384
@@ -5956,6 +6311,52 @@ function uninstallCodexHooks(scope, projectDir = process.cwd(), codexHomeDir) {
5956
6311
  }
5957
6312
 
5958
6313
  // src/cli.ts
6314
+ function recordHookHealth(harness, error) {
6315
+ try {
6316
+ const dir = process.env.MINDGRAPH_RUNTIME_DIR || path5.join(os4.homedir(), ".mindgraph", "runtime");
6317
+ fs5.mkdirSync(dir, { recursive: true, mode: 448 });
6318
+ const file = path5.join(dir, "hook-health.json");
6319
+ let failures = 0;
6320
+ try {
6321
+ failures = Number(JSON.parse(fs5.readFileSync(file, "utf8")).consecutiveFailures) || 0;
6322
+ } catch {
6323
+ }
6324
+ fs5.writeFileSync(
6325
+ file,
6326
+ `${JSON.stringify({
6327
+ harness,
6328
+ lastError: error.slice(0, 500),
6329
+ lastFailureAt: (/* @__PURE__ */ new Date()).toISOString(),
6330
+ consecutiveFailures: failures + 1
6331
+ })}
6332
+ `,
6333
+ { mode: 384 }
6334
+ );
6335
+ } catch {
6336
+ }
6337
+ }
6338
+ function clearHookHealth(harness) {
6339
+ try {
6340
+ const file = path5.join(
6341
+ process.env.MINDGRAPH_RUNTIME_DIR || path5.join(os4.homedir(), ".mindgraph", "runtime"),
6342
+ "hook-health.json"
6343
+ );
6344
+ if (!fs5.existsSync(file)) return;
6345
+ const current = JSON.parse(fs5.readFileSync(file, "utf8"));
6346
+ if (!Number(current.consecutiveFailures)) return;
6347
+ fs5.writeFileSync(
6348
+ file,
6349
+ `${JSON.stringify({
6350
+ harness,
6351
+ lastOkAt: (/* @__PURE__ */ new Date()).toISOString(),
6352
+ consecutiveFailures: 0
6353
+ })}
6354
+ `,
6355
+ { mode: 384 }
6356
+ );
6357
+ } catch {
6358
+ }
6359
+ }
5959
6360
  function getClaudeDesktopConfigPath() {
5960
6361
  switch (process.platform) {
5961
6362
  case "darwin":
@@ -6031,7 +6432,10 @@ function installClaudeCode(apiKey, baseUrl, agentId) {
6031
6432
  "--env",
6032
6433
  "MINDGRAPH_HARNESS=claude-code",
6033
6434
  "--env",
6034
- `MINDGRAPH_AGENT_ID=${agentId || "claude-code"}`
6435
+ // The registration, the hooks, and the serve path must all resolve the
6436
+ // SAME identity — a differing default here made the model's fenced tool
6437
+ // calls and the hook's lease two different agents on the server.
6438
+ `MINDGRAPH_AGENT_ID=${agentId || process.env.MINDGRAPH_AGENT_ID || loadHookEnv().agentId || stableAgentId()}`
6035
6439
  ];
6036
6440
  if (baseUrl) {
6037
6441
  envArgs.push("--env", `MINDGRAPH_BASE_URL=${baseUrl}`);
@@ -6121,6 +6525,23 @@ function printStatus() {
6121
6525
  console.log(
6122
6526
  `MINDGRAPH_BASE_URL: ${process.env.MINDGRAPH_BASE_URL || "https://api.mindgraph.cloud (default)"}`
6123
6527
  );
6528
+ try {
6529
+ const healthFile = path5.join(
6530
+ process.env.MINDGRAPH_RUNTIME_DIR || path5.join(os4.homedir(), ".mindgraph", "runtime"),
6531
+ "hook-health.json"
6532
+ );
6533
+ if (fs5.existsSync(healthFile)) {
6534
+ const health = JSON.parse(fs5.readFileSync(healthFile, "utf8"));
6535
+ const failures = Number(health.consecutiveFailures) || 0;
6536
+ console.log(
6537
+ failures > 0 ? `Hooks: FAILING (${failures} consecutive; last: ${health.lastError} at ${health.lastFailureAt})` : `Hooks: healthy (last recovery ${health.lastOkAt || "unknown"})`
6538
+ );
6539
+ } else {
6540
+ console.log("Hooks: no health marker (no hook failures recorded)");
6541
+ }
6542
+ } catch {
6543
+ console.log("Hooks: health marker unreadable");
6544
+ }
6124
6545
  }
6125
6546
  function withHooksFlag() {
6126
6547
  return process.argv.includes("--hooks");
@@ -6133,7 +6554,16 @@ function finishCodeInstall(apiKey, baseUrl, withHooks, agentId) {
6133
6554
  console.log(
6134
6555
  `Installed ${result.added} and refreshed ${result.updated} MindGraph Claude Code hook entries in ${result.path}`
6135
6556
  );
6136
- const envPath = saveHookEnv({ apiKey, baseUrl, agentId });
6557
+ const envPath = saveHookEnv({
6558
+ apiKey,
6559
+ baseUrl,
6560
+ // Default the stable per-user identity only when nothing is stored —
6561
+ // an explicit or previously persisted agentId must survive reinstalls.
6562
+ agentId: agentId || process.env.MINDGRAPH_AGENT_ID || loadHookEnv().agentId || stableAgentId(),
6563
+ // The org pin follows the install environment exactly: installing
6564
+ // without MINDGRAPH_ORG_ID clears any stale pin (null = clear).
6565
+ orgId: process.env.MINDGRAPH_ORG_ID ?? null
6566
+ });
6137
6567
  console.log(`Saved hook connection settings to ${envPath} (mode 600)`);
6138
6568
  } else {
6139
6569
  console.log(
@@ -6192,7 +6622,7 @@ var DASHBOARD_URL = process.env.MINDGRAPH_DASHBOARD_URL || "https://mindgraph.cl
6192
6622
  var POLL_INTERVAL = 2e3;
6193
6623
  var POLL_TIMEOUT = 10 * 60 * 1e3;
6194
6624
  function generateCode() {
6195
- return crypto4.randomBytes(32).toString("hex");
6625
+ return crypto5.randomBytes(32).toString("hex");
6196
6626
  }
6197
6627
  function openBrowser(url) {
6198
6628
  try {
@@ -6355,10 +6785,19 @@ async function main2() {
6355
6785
  `Installed ${result.added} and refreshed ${result.updated} MindGraph ${hookHarness === "codex" ? "Codex" : "Claude Code"} hook entries in ${result.path}`
6356
6786
  );
6357
6787
  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 {
6788
+ const envPath = saveHookEnv({
6789
+ apiKey,
6790
+ baseUrl,
6791
+ // Default the stable per-user identity only when nothing is stored —
6792
+ // an explicit or previously persisted agentId must survive
6793
+ // reinstalls.
6794
+ agentId: agentId || process.env.MINDGRAPH_AGENT_ID || loadHookEnv().agentId || stableAgentId(),
6795
+ // The org pin follows the install environment exactly: installing
6796
+ // without MINDGRAPH_ORG_ID clears any stale pin (null = clear).
6797
+ orgId: process.env.MINDGRAPH_ORG_ID ?? null
6798
+ });
6799
+ console.log(`Saved hook connection settings to ${envPath} (mode 600)`);
6800
+ if (!apiKey && !baseUrl) {
6362
6801
  console.log(
6363
6802
  "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
6803
  );
@@ -6385,6 +6824,7 @@ async function main2() {
6385
6824
  const hookApiKey = apiKey ?? stored.apiKey;
6386
6825
  const hookBaseUrl = baseUrl ?? stored.baseUrl;
6387
6826
  if (!hookApiKey) {
6827
+ recordHookHealth(hookHarness, "no-api-key");
6388
6828
  process.stdout.write("{}\n");
6389
6829
  break;
6390
6830
  }
@@ -6393,12 +6833,19 @@ async function main2() {
6393
6833
  const client2 = new MindGraph({
6394
6834
  baseUrl: hookBaseUrl || "https://api.mindgraph.cloud",
6395
6835
  apiKey: hookApiKey,
6396
- orgId: process.env.MINDGRAPH_ORG_ID,
6836
+ // Hooks run with the harness's environment, which usually lacks
6837
+ // MINDGRAPH_ORG_ID even when the MCP registration pins it — on a
6838
+ // multi-org key the hooks would otherwise track work in the key's
6839
+ // DEFAULT org while the MCP tools write to the pinned one.
6840
+ orgId: process.env.MINDGRAPH_ORG_ID || stored.orgId,
6397
6841
  maxRetries: 0,
6398
6842
  telemetrySurface: "mcp"
6399
6843
  });
6400
6844
  const hookOptions = {
6401
- agentId: process.env.MINDGRAPH_AGENT_ID || stored.agentId || hookHarness
6845
+ // No harness-name fallback here: hook-core derives a stable
6846
+ // per-user default shared by both harnesses, which is what lets a
6847
+ // task claimed under Codex rebind under Claude Code.
6848
+ agentId: process.env.MINDGRAPH_AGENT_ID || stored.agentId
6402
6849
  };
6403
6850
  const output = hookHarness === "codex" ? await runCodexHook(
6404
6851
  input,
@@ -6411,7 +6858,15 @@ async function main2() {
6411
6858
  );
6412
6859
  process.stdout.write(`${JSON.stringify(output)}
6413
6860
  `);
6414
- } catch {
6861
+ clearHookHealth(hookHarness);
6862
+ } catch (error) {
6863
+ recordHookHealth(
6864
+ hookHarness,
6865
+ error?.message || "hook failed"
6866
+ );
6867
+ console.error(
6868
+ `mindgraph-hook: failing open (${error?.message || "unknown error"})`
6869
+ );
6415
6870
  process.stdout.write("{}\n");
6416
6871
  }
6417
6872
  break;