mindgraph-mcp 0.14.8 → 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
  }
@@ -2651,6 +2736,38 @@ async function handleCommit(client2, args) {
2651
2736
  return err3(`Unknown commit action: ${action}`);
2652
2737
  }
2653
2738
  }
2739
+ async function repositoryScopeUidsForTask(client2, options) {
2740
+ const scopeUids = [...new Set(options.scopeUids ?? [])];
2741
+ const cwd = options.invocationContext?.cwd;
2742
+ const repoId = options.invocationContext?.repoId;
2743
+ if (!options.repo && !cwd && !repoId) {
2744
+ return options.scopeUids;
2745
+ }
2746
+ const agentId = options.agentId ?? process.env.MINDGRAPH_AGENT_ID ?? "mcp";
2747
+ const fallbackSpaceUid = options.repoSpaceUid ?? `space:agent:${agentId}`;
2748
+ const env = { ...process.env };
2749
+ if (!options.repo && repoId) {
2750
+ env.MINDGRAPH_REPO_ID = repoId;
2751
+ }
2752
+ const adapter = new CodegraphAdapter({
2753
+ cwd: cwd ?? process.cwd(),
2754
+ env
2755
+ });
2756
+ const repository = await adapter.resolveRepository(options.repo, fallbackSpaceUid);
2757
+ const anchored = await anchorRepository(
2758
+ client2,
2759
+ repository,
2760
+ repository.spaceUid ?? fallbackSpaceUid,
2761
+ agentId
2762
+ );
2763
+ if (!anchored.uid) {
2764
+ throw new Error(
2765
+ `repository scope ${repository.repoId} could not be materialized (${anchored.status ?? "missing uid"})`
2766
+ );
2767
+ }
2768
+ scopeUids.push(anchored.uid);
2769
+ return [...new Set(scopeUids)];
2770
+ }
2654
2771
  async function handlePlan(client2, args) {
2655
2772
  const {
2656
2773
  action,
@@ -2681,6 +2798,9 @@ async function handlePlan(client2, args) {
2681
2798
  release_lease,
2682
2799
  override_reason,
2683
2800
  scope_uids,
2801
+ repo,
2802
+ repo_space_uid,
2803
+ invocation_context,
2684
2804
  limit,
2685
2805
  confidence,
2686
2806
  salience,
@@ -2710,7 +2830,14 @@ async function handlePlan(client2, args) {
2710
2830
  case "checkpoint_iteration":
2711
2831
  case "block_task":
2712
2832
  case "complete_task":
2713
- case "abandon_iteration":
2833
+ case "abandon_iteration": {
2834
+ const resolvedScopeUids = action === "create_task" ? await repositoryScopeUidsForTask(client2, {
2835
+ scopeUids: scope_uids,
2836
+ repo,
2837
+ repoSpaceUid: repo_space_uid,
2838
+ invocationContext: invocation_context,
2839
+ agentId: agent_id
2840
+ }) : scope_uids;
2714
2841
  return ok3(
2715
2842
  await client2.plan({
2716
2843
  action,
@@ -2738,7 +2865,7 @@ async function handlePlan(client2, args) {
2738
2865
  allow_takeover,
2739
2866
  release_lease,
2740
2867
  override_reason,
2741
- scope_uids,
2868
+ scope_uids: resolvedScopeUids,
2742
2869
  limit,
2743
2870
  input_snapshot,
2744
2871
  output_snapshot,
@@ -2753,6 +2880,7 @@ async function handlePlan(client2, args) {
2753
2880
  agent_id
2754
2881
  })
2755
2882
  );
2883
+ }
2756
2884
  // Execution tracking
2757
2885
  case "start_execution":
2758
2886
  return ok3(
@@ -3310,6 +3438,7 @@ var init_tools = __esm({
3310
3438
  "use strict";
3311
3439
  init_error_detail();
3312
3440
  init_code_tool();
3441
+ init_codegraph();
3313
3442
  init_sync_tool();
3314
3443
  TOOLS = [
3315
3444
  {
@@ -3774,7 +3903,7 @@ var init_tools = __esm({
3774
3903
  scope_uids: {
3775
3904
  type: "array",
3776
3905
  items: { type: "string" },
3777
- description: "Ordinary target UIDs that bound deterministic resume selection"
3906
+ description: "Required target UIDs for create_task and ordinary target UIDs that bound deterministic resume selection"
3778
3907
  },
3779
3908
  limit: {
3780
3909
  type: "number",
@@ -4778,7 +4907,7 @@ async function main() {
4778
4907
  const transport = new import_stdio.StdioServerTransport();
4779
4908
  await server.connect(transport);
4780
4909
  console.error(
4781
- `MindGraph MCP server v0.7.1 running on stdio (${BASE_URL})`
4910
+ `MindGraph MCP server v${"0.15.0"} running on stdio (${BASE_URL})`
4782
4911
  );
4783
4912
  }
4784
4913
  var import_server, import_stdio, import_types, API_KEY, BASE_URL, AGENT_ID, ORG_ID, PROFILE, HARNESS, client, INSTRUCTIONS, server, GOVERNANCE_CONFIG, PROMPTS;
@@ -4789,13 +4918,14 @@ var init_src = __esm({
4789
4918
  import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
4790
4919
  import_types = require("@modelcontextprotocol/sdk/types.js");
4791
4920
  init_dist();
4921
+ init_hook_env();
4792
4922
  init_tools();
4793
4923
  init_generated_tools();
4794
4924
  init_governance();
4795
4925
  init_resources();
4796
4926
  API_KEY = process.env.MINDGRAPH_API_KEY;
4797
4927
  BASE_URL = process.env.MINDGRAPH_BASE_URL || "https://api.mindgraph.cloud";
4798
- AGENT_ID = process.env.MINDGRAPH_AGENT_ID || "mcp";
4928
+ AGENT_ID = process.env.MINDGRAPH_AGENT_ID || loadHookEnv().agentId || stableAgentId();
4799
4929
  ORG_ID = process.env.MINDGRAPH_ORG_ID;
4800
4930
  PROFILE = process.env.MINDGRAPH_PROFILE || "general";
4801
4931
  HARNESS = process.env.MINDGRAPH_HARNESS || "generic";
@@ -4917,7 +5047,7 @@ Capture knowledge when the user shares something worth remembering:
4917
5047
  server = new import_server.Server(
4918
5048
  {
4919
5049
  name: "mindgraph",
4920
- version: "0.14.8"
5050
+ version: "0.15.0"
4921
5051
  },
4922
5052
  {
4923
5053
  capabilities: {
@@ -5138,46 +5268,10 @@ ${conversation}`
5138
5268
  var fs5 = __toESM(require("fs"));
5139
5269
  var path5 = __toESM(require("path"));
5140
5270
  var os4 = __toESM(require("os"));
5141
- var crypto4 = __toESM(require("crypto"));
5271
+ var crypto5 = __toESM(require("crypto"));
5142
5272
  var import_child_process = require("child_process");
5143
5273
  init_dist();
5144
-
5145
- // src/hook-env.ts
5146
- var import_node_fs = __toESM(require("fs"));
5147
- var import_node_os = __toESM(require("os"));
5148
- var import_node_path = __toESM(require("path"));
5149
- function hookEnvPath(dir) {
5150
- return import_node_path.default.join(dir || import_node_path.default.join(import_node_os.default.homedir(), ".mindgraph"), "hooks.json");
5151
- }
5152
- function loadHookEnv(dir) {
5153
- try {
5154
- const raw = import_node_fs.default.readFileSync(hookEnvPath(dir), "utf8");
5155
- const parsed = JSON.parse(raw);
5156
- return {
5157
- baseUrl: typeof parsed.baseUrl === "string" ? parsed.baseUrl : void 0,
5158
- apiKey: typeof parsed.apiKey === "string" ? parsed.apiKey : void 0,
5159
- agentId: typeof parsed.agentId === "string" ? parsed.agentId : void 0
5160
- };
5161
- } catch {
5162
- return {};
5163
- }
5164
- }
5165
- function saveHookEnv(env, dir) {
5166
- const file = hookEnvPath(dir);
5167
- import_node_fs.default.mkdirSync(import_node_path.default.dirname(file), { recursive: true });
5168
- const existing = loadHookEnv(dir);
5169
- const merged = {
5170
- baseUrl: env.baseUrl ?? existing.baseUrl,
5171
- apiKey: env.apiKey ?? existing.apiKey,
5172
- agentId: env.agentId ?? existing.agentId
5173
- };
5174
- import_node_fs.default.writeFileSync(file, `${JSON.stringify(merged, null, 2)}
5175
- `, {
5176
- mode: 384
5177
- });
5178
- import_node_fs.default.chmodSync(file, 384);
5179
- return file;
5180
- }
5274
+ init_hook_env();
5181
5275
 
5182
5276
  // src/cli-args.ts
5183
5277
  function parseArgs(argv) {
@@ -5248,12 +5342,14 @@ function classifyMcpAddFailure(e) {
5248
5342
  }
5249
5343
 
5250
5344
  // src/hook-core.ts
5251
- var import_node_crypto2 = __toESM(require("crypto"));
5345
+ var import_node_crypto3 = __toESM(require("crypto"));
5252
5346
  var import_node_fs3 = __toESM(require("fs"));
5253
5347
  var import_node_os3 = __toESM(require("os"));
5254
5348
  var import_node_path3 = __toESM(require("path"));
5255
5349
  var import_node_child_process2 = require("child_process");
5256
5350
  init_codegraph();
5351
+ init_code_tool();
5352
+ init_hook_env();
5257
5353
  var NOOP = { kind: "noop" };
5258
5354
  function defaultLedger(sessionId, options) {
5259
5355
  return {
@@ -5275,7 +5371,7 @@ function runtimeDir(options) {
5275
5371
  }
5276
5372
  function ledgerPath(sessionId, options) {
5277
5373
  const harness = "harness" in options ? options.harness : "claude-code";
5278
- 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");
5279
5375
  return import_node_path3.default.join(runtimeDir(options), `${key}.json`);
5280
5376
  }
5281
5377
  function waitBriefly(ms) {
@@ -5290,14 +5386,14 @@ function withLedger(sessionId, options, update) {
5290
5386
  }
5291
5387
  const lock = `${file}.lock`;
5292
5388
  let lockFd;
5293
- for (let attempt = 0; attempt < 50; attempt++) {
5389
+ for (let attempt = 0; attempt < 400; attempt++) {
5294
5390
  try {
5295
5391
  lockFd = import_node_fs3.default.openSync(lock, "wx", 384);
5296
5392
  break;
5297
5393
  } catch (error) {
5298
5394
  if (error.code !== "EEXIST") throw error;
5299
5395
  try {
5300
- 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);
5301
5397
  } catch {
5302
5398
  }
5303
5399
  waitBriefly(10);
@@ -5317,7 +5413,7 @@ function withLedger(sessionId, options, update) {
5317
5413
  }
5318
5414
  const result = update(ledger);
5319
5415
  ledger.updatedAt = Date.now();
5320
- const temporary = `${file}.${process.pid}.${import_node_crypto2.default.randomUUID()}.tmp`;
5416
+ const temporary = `${file}.${process.pid}.${import_node_crypto3.default.randomUUID()}.tmp`;
5321
5417
  import_node_fs3.default.writeFileSync(temporary, `${JSON.stringify(ledger)}
5322
5418
  `, { mode: 384 });
5323
5419
  import_node_fs3.default.renameSync(temporary, file);
@@ -5352,17 +5448,13 @@ function worktreeState(cwd) {
5352
5448
  const status = gitValue(cwd, ["status", "--porcelain"]);
5353
5449
  return status === void 0 ? "unknown" : status ? "dirty" : "clean";
5354
5450
  }
5355
- async function invocationContext(input, options) {
5356
- const repository = await new CodegraphAdapter({
5357
- cwd: input.cwd,
5358
- workspaceFile: options.workspaceFile
5359
- }).resolveRepository();
5451
+ function invocationContext(input, options, repoId) {
5360
5452
  return {
5361
5453
  harness: options.harness,
5362
5454
  harnessSessionId: input.session_id,
5363
5455
  harnessTurnId: input.turn_id,
5364
5456
  cwd: input.cwd,
5365
- repoId: repository.repoId,
5457
+ repoId,
5366
5458
  branch: gitValue(input.cwd, ["branch", "--show-current"]),
5367
5459
  commit: gitValue(input.cwd, ["rev-parse", "HEAD"]),
5368
5460
  worktreeState: worktreeState(input.cwd),
@@ -5370,6 +5462,9 @@ async function invocationContext(input, options) {
5370
5462
  injectedBy: "hook"
5371
5463
  };
5372
5464
  }
5465
+ function resolveAgentId(options) {
5466
+ return options.agentId || process.env.MINDGRAPH_AGENT_ID || stableAgentId();
5467
+ }
5373
5468
  function object(value) {
5374
5469
  return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
5375
5470
  }
@@ -5397,56 +5492,198 @@ function number(value) {
5397
5492
  function string(value) {
5398
5493
  return typeof value === "string" && value ? value : void 0;
5399
5494
  }
5400
- function boundedBrief(brief) {
5401
- const encoded = JSON.stringify(brief, null, 2);
5402
- 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)}
5403
5617
  \u2026[bounded work brief truncated]`;
5404
- return [
5405
- "MindGraph durable work brief (authoritative across sessions/harnesses):",
5406
- capped,
5407
- "Use the harness todo list only for local execution details; do not duplicate this durable Task there."
5408
- ].join("\n");
5409
5618
  }
5410
5619
  async function repositoryScopeUids(input, client2, options, agentId) {
5411
- if (!client2.entity) return [];
5620
+ if (!client2.entity) return { uids: [], failures: [] };
5412
5621
  const adapter = new CodegraphAdapter({
5413
5622
  cwd: input.cwd,
5414
5623
  workspaceFile: options.workspaceFile
5415
5624
  });
5416
5625
  const configured = await adapter.resolveRepositories();
5417
5626
  const repositories = configured.length > 0 ? configured : [await adapter.resolveRepository()];
5627
+ const failures = [];
5418
5628
  const resolved = await Promise.all(
5419
- repositories.map(
5420
- (repository) => client2.entity({
5421
- action: "resolve_identity",
5422
- identity: {
5423
- namespace: "external.code",
5424
- key_version: 1,
5425
- key: repositoryIdentityKey(repository)
5426
- },
5427
- agent_id: agentId
5428
- }).catch(() => void 0)
5429
- )
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
+ })
5430
5669
  );
5431
- return [
5432
- ...new Set(
5433
- resolved.flatMap((response) => {
5434
- const payload = object(response);
5435
- return payload?.status === "existing" && string(payload.uid) ? [string(payload.uid)] : [];
5436
- })
5437
- )
5438
- ];
5670
+ return {
5671
+ uids: [...new Set(resolved.filter((uid) => Boolean(uid)))],
5672
+ failures
5673
+ };
5439
5674
  }
5440
- function isOwnPriorWork(brief, agentId) {
5675
+ function isOwnPriorWork(brief, agentId, harness) {
5441
5676
  const reason = string(brief?.selection_reason);
5442
5677
  if (reason === "owned_live_lease" || reason === "same_agent_session_rebind") {
5443
5678
  return true;
5444
5679
  }
5445
- 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;
5446
5682
  }
5447
5683
  async function sessionStart(input, client2, options) {
5448
- const agentId = options.agentId || process.env.MINDGRAPH_AGENT_ID || options.harness;
5684
+ const agentId = resolveAgentId(options);
5449
5685
  const harnessLabel = options.harness === "claude-code" ? "Claude Code" : "Codex";
5686
+ const nowSeconds = (options.now || (() => Date.now() / 1e3))();
5450
5687
  const opened = object(
5451
5688
  await client2.session({
5452
5689
  action: "open",
@@ -5458,7 +5695,21 @@ async function sessionStart(input, client2, options) {
5458
5695
  })
5459
5696
  );
5460
5697
  const sessionUid = string(opened?.uid);
5461
- 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;
5462
5713
  let brief = object(
5463
5714
  await client2.plan({
5464
5715
  action: "resume_work",
@@ -5469,9 +5720,19 @@ async function sessionStart(input, client2, options) {
5469
5720
  );
5470
5721
  const task = object(brief?.task);
5471
5722
  const lease = object(brief?.lease);
5472
- 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)) {
5473
5734
  try {
5474
- const claimed = object(
5735
+ claimed = object(
5475
5736
  await client2.plan({
5476
5737
  action: "claim_task",
5477
5738
  task_uid: task.uid,
@@ -5481,35 +5742,63 @@ async function sessionStart(input, client2, options) {
5481
5742
  agent_id: agentId
5482
5743
  })
5483
5744
  );
5484
- brief = object(
5485
- await client2.plan({
5486
- action: "resume_work",
5487
- task_uid: task.uid,
5488
- session_uid: sessionUid,
5489
- ...scopeUids.length > 0 ? { scope_uids: scopeUids } : {},
5490
- agent_id: agentId
5491
- })
5492
- );
5493
- if (brief && claimed) brief.claim = claimed;
5494
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;
5495
5763
  }
5496
5764
  }
5497
- const briefText = boundedBrief(brief || { status: "no_eligible_work" });
5498
- 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");
5499
5773
  const shouldInject = withLedger(input.session_id, options, (ledger) => {
5500
5774
  const changed = ledger.lastBriefHash !== briefHash;
5501
5775
  const reinjectCompact = options.reinjectUnchangedCompact && input.source === "compact";
5502
5776
  ledger.sessionUid = sessionUid;
5503
- ledger.baselineWorktree = worktreeState(input.cwd);
5504
- const selectedTask = object(brief?.task);
5505
- const selectedLease = object(brief?.lease) || object(brief?.claim);
5506
- const executions = Array.isArray(brief?.recent_executions) ? brief?.recent_executions : [];
5507
- const latest = object(executions[0]);
5508
- ledger.taskUid = string(selectedTask?.uid);
5509
- ledger.taskVersion = number(selectedTask?.version);
5510
- ledger.leaseEpoch = number(selectedLease?.lease_epoch);
5511
- ledger.leaseExpiresAt = number(selectedLease?.lease_expires_at);
5512
- 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
+ }
5513
5802
  ledger.lastBriefHash = briefHash;
5514
5803
  return changed || reinjectCompact;
5515
5804
  });
@@ -5518,82 +5807,173 @@ async function sessionStart(input, client2, options) {
5518
5807
  async function preTool(input, options) {
5519
5808
  const original = { ...input.tool_input || {} };
5520
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
+ }
5521
5823
  const updatedInput = {
5522
5824
  ...original,
5523
- invocation_context: await invocationContext(input, options)
5825
+ invocation_context: invocationContext(input, options, repoId)
5524
5826
  };
5525
5827
  if (ledger.sessionUid) updatedInput.session_uid = ledger.sessionUid;
5526
5828
  if (ledger.taskUid && updatedInput.task_uid === void 0) {
5527
5829
  updatedInput.task_uid = ledger.taskUid;
5528
5830
  }
5529
- 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) {
5530
5836
  updatedInput.work_uid = ledger.taskUid;
5531
5837
  }
5532
- if (ledger.executionUid && updatedInput.execution_uid === void 0) {
5838
+ if (targetsLedgerTask && (fencedAction || isCapture) && ledger.executionUid && updatedInput.execution_uid === void 0) {
5533
5839
  updatedInput.execution_uid = ledger.executionUid;
5534
5840
  }
5535
- if (ledger.leaseEpoch !== void 0 && updatedInput.lease_epoch === void 0) {
5841
+ if (targetsLedgerTask && fencedAction && ledger.leaseEpoch !== void 0 && updatedInput.lease_epoch === void 0) {
5536
5842
  updatedInput.lease_epoch = ledger.leaseEpoch;
5537
5843
  }
5538
- if (ledger.taskVersion !== void 0 && updatedInput.expected_version === void 0) {
5844
+ if (targetsLedgerTask && fencedAction && ledger.taskVersion !== void 0 && updatedInput.expected_version === void 0) {
5539
5845
  updatedInput.expected_version = ledger.taskVersion;
5540
5846
  }
5541
5847
  return { kind: "rewrite", updatedInput };
5542
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
+ ]);
5543
5858
  async function postTool(input, client2, options) {
5544
5859
  const name = input.tool_name || "";
5545
5860
  const args = input.tool_input || {};
5546
5861
  const action = string(args.action);
5547
5862
  const payload = responsePayload(input.tool_response);
5548
5863
  const nowSeconds = (options.now || (() => Date.now() / 1e3))();
5549
- let heartbeat;
5864
+ let renewal;
5550
5865
  withLedger(input.session_id, options, (ledger) => {
5551
5866
  ledger.materialToolCalls += 1;
5552
- if (["Edit", "Write", "NotebookEdit", "apply_patch"].includes(name) || name === "Bash" && action !== "read") {
5867
+ if (["Edit", "Write", "NotebookEdit", "apply_patch"].includes(name)) {
5553
5868
  ledger.mutatingOperations += 1;
5554
5869
  }
5555
- if (name.includes("mindgraph_capture") && !payload?.error) {
5556
- 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
+ }
5557
5874
  }
5558
5875
  if (name.includes("mindgraph_plan")) {
5559
- if (action === "start_iteration" && !payload?.error) {
5560
- ledger.executionUid = string(payload?.execution_uid) || ledger.executionUid;
5561
- }
5562
- if (["checkpoint_iteration", "block_task", "complete_task", "abandon_iteration"].includes(
5563
- action || ""
5564
- ) && !payload?.error) {
5565
- ledger.iterationCheckpointed = true;
5566
- if (action !== "block_task") ledger.executionUid = void 0;
5567
- }
5568
- ledger.taskVersion = number(payload?.task_version) || number(payload?.version) || ledger.taskVersion;
5569
- ledger.leaseEpoch = number(payload?.lease_epoch) || ledger.leaseEpoch;
5570
- 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
+ }
5571
5916
  }
5572
- if (ledger.taskUid && ledger.sessionUid && ledger.taskVersion !== void 0 && ledger.leaseEpoch !== void 0 && ledger.leaseExpiresAt !== void 0 && ledger.leaseExpiresAt - nowSeconds <= 60) {
5573
- heartbeat = {
5574
- taskUid: ledger.taskUid,
5575
- sessionUid: ledger.sessionUid,
5576
- taskVersion: ledger.taskVersion,
5577
- leaseEpoch: ledger.leaseEpoch
5578
- };
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
+ }
5579
5941
  }
5580
5942
  return void 0;
5581
5943
  });
5582
- if (heartbeat) {
5944
+ if (renewal) {
5945
+ const active = renewal;
5583
5946
  try {
5584
5947
  const response = object(
5585
- 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({
5586
5961
  action: "heartbeat",
5587
- task_uid: heartbeat.taskUid,
5588
- session_uid: heartbeat.sessionUid,
5589
- expected_version: heartbeat.taskVersion,
5590
- lease_epoch: heartbeat.leaseEpoch,
5591
- idempotency_key: `hook-heartbeat:${input.session_id}:${heartbeat.taskUid}:${Math.floor(nowSeconds / 60)}`,
5592
- 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)
5593
5968
  })
5594
5969
  );
5595
5970
  withLedger(input.session_id, options, (ledger) => {
5596
- 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;
5597
5977
  return void 0;
5598
5978
  });
5599
5979
  } catch {
@@ -5601,6 +5981,9 @@ async function postTool(input, client2, options) {
5601
5981
  }
5602
5982
  return NOOP;
5603
5983
  }
5984
+ function payloadIsError(payload) {
5985
+ return Boolean(payload && payload.error);
5986
+ }
5604
5987
  function stop(input, options) {
5605
5988
  if (input.stop_hook_active) return NOOP;
5606
5989
  return withLedger(input.session_id, options, (ledger) => {
@@ -5631,17 +6014,20 @@ async function sessionEnd(input, client2, options) {
5631
6014
  release_lease: true,
5632
6015
  summary: `${harnessLabel} SessionEnd: ${input.reason || "interrupted"}`,
5633
6016
  idempotency_key: `session-end:${input.session_id}:${ledger.executionUid}`,
5634
- agent_id: options.agentId || options.harness
6017
+ agent_id: resolveAgentId(options)
5635
6018
  });
5636
6019
  } catch {
5637
6020
  }
5638
6021
  }
5639
- 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)) {
5640
6026
  try {
5641
6027
  await client2.session({
5642
6028
  action: "close",
5643
6029
  session_uid: ledger.sessionUid,
5644
- agent_id: options.agentId || options.harness
6030
+ agent_id: resolveAgentId(options)
5645
6031
  });
5646
6032
  } catch {
5647
6033
  }
@@ -5751,7 +6137,7 @@ async function runCodexHook(input, client2, options = {}) {
5751
6137
  var import_node_fs4 = __toESM(require("fs"));
5752
6138
  var import_node_os4 = __toESM(require("os"));
5753
6139
  var import_node_path4 = __toESM(require("path"));
5754
- var import_node_crypto3 = __toESM(require("crypto"));
6140
+ var import_node_crypto4 = __toESM(require("crypto"));
5755
6141
  var OWNER_MARKER = "--owner mindgraph";
5756
6142
  var RUNNER_RELATIVE = ".mindgraph/bin/mindgraph-hook.cjs";
5757
6143
  function command(harness) {
@@ -5795,11 +6181,14 @@ function desiredEntries(harness) {
5795
6181
  // inherited 20s edge in both harnesses: the hooks correctly failed
5796
6182
  // open, but no brief reached the model. The same installed runner
5797
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.
5798
6187
  hooks: [
5799
6188
  ownedHook(
5800
6189
  harness,
5801
6190
  30,
5802
- harness === "codex" ? 3e3 : void 0
6191
+ harness === "codex" ? 1e4 : void 0
5803
6192
  )
5804
6193
  ]
5805
6194
  }
@@ -5807,13 +6196,23 @@ function desiredEntries(harness) {
5807
6196
  PreToolUse: [
5808
6197
  {
5809
6198
  matcher: "mcp__mindgraph__.*",
5810
- 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)]
5811
6203
  }
5812
6204
  ],
5813
- 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)] }],
5814
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.
5815
6214
  SessionEnd: [
5816
- { hooks: [ownedHook(harness, harness === "codex" ? 3 : 8)] }
6215
+ { hooks: [ownedHook(harness, harness === "codex" ? 3 : 30)] }
5817
6216
  ]
5818
6217
  };
5819
6218
  return harness === "claude-code" ? {
@@ -5834,7 +6233,7 @@ function entryHasOwnedHook(value) {
5834
6233
  }
5835
6234
  function writeSettings(file, settings) {
5836
6235
  import_node_fs4.default.mkdirSync(import_node_path4.default.dirname(file), { recursive: true });
5837
- const temporary = `${file}.${process.pid}.${import_node_crypto3.default.randomUUID()}.tmp`;
6236
+ const temporary = `${file}.${process.pid}.${import_node_crypto4.default.randomUUID()}.tmp`;
5838
6237
  import_node_fs4.default.writeFileSync(temporary, `${JSON.stringify(settings, null, 2)}
5839
6238
  `, {
5840
6239
  mode: 384
@@ -5912,6 +6311,52 @@ function uninstallCodexHooks(scope, projectDir = process.cwd(), codexHomeDir) {
5912
6311
  }
5913
6312
 
5914
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
+ }
5915
6360
  function getClaudeDesktopConfigPath() {
5916
6361
  switch (process.platform) {
5917
6362
  case "darwin":
@@ -5987,7 +6432,10 @@ function installClaudeCode(apiKey, baseUrl, agentId) {
5987
6432
  "--env",
5988
6433
  "MINDGRAPH_HARNESS=claude-code",
5989
6434
  "--env",
5990
- `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()}`
5991
6439
  ];
5992
6440
  if (baseUrl) {
5993
6441
  envArgs.push("--env", `MINDGRAPH_BASE_URL=${baseUrl}`);
@@ -6077,6 +6525,23 @@ function printStatus() {
6077
6525
  console.log(
6078
6526
  `MINDGRAPH_BASE_URL: ${process.env.MINDGRAPH_BASE_URL || "https://api.mindgraph.cloud (default)"}`
6079
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
+ }
6080
6545
  }
6081
6546
  function withHooksFlag() {
6082
6547
  return process.argv.includes("--hooks");
@@ -6089,7 +6554,16 @@ function finishCodeInstall(apiKey, baseUrl, withHooks, agentId) {
6089
6554
  console.log(
6090
6555
  `Installed ${result.added} and refreshed ${result.updated} MindGraph Claude Code hook entries in ${result.path}`
6091
6556
  );
6092
- 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
+ });
6093
6567
  console.log(`Saved hook connection settings to ${envPath} (mode 600)`);
6094
6568
  } else {
6095
6569
  console.log(
@@ -6148,7 +6622,7 @@ var DASHBOARD_URL = process.env.MINDGRAPH_DASHBOARD_URL || "https://mindgraph.cl
6148
6622
  var POLL_INTERVAL = 2e3;
6149
6623
  var POLL_TIMEOUT = 10 * 60 * 1e3;
6150
6624
  function generateCode() {
6151
- return crypto4.randomBytes(32).toString("hex");
6625
+ return crypto5.randomBytes(32).toString("hex");
6152
6626
  }
6153
6627
  function openBrowser(url) {
6154
6628
  try {
@@ -6311,10 +6785,19 @@ async function main2() {
6311
6785
  `Installed ${result.added} and refreshed ${result.updated} MindGraph ${hookHarness === "codex" ? "Codex" : "Claude Code"} hook entries in ${result.path}`
6312
6786
  );
6313
6787
  printCodegraphStatus();
6314
- if (apiKey || baseUrl || agentId) {
6315
- const envPath = saveHookEnv({ apiKey, baseUrl, agentId });
6316
- console.log(`Saved hook connection settings to ${envPath} (mode 600)`);
6317
- } 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) {
6318
6801
  console.log(
6319
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."
6320
6803
  );
@@ -6341,6 +6824,7 @@ async function main2() {
6341
6824
  const hookApiKey = apiKey ?? stored.apiKey;
6342
6825
  const hookBaseUrl = baseUrl ?? stored.baseUrl;
6343
6826
  if (!hookApiKey) {
6827
+ recordHookHealth(hookHarness, "no-api-key");
6344
6828
  process.stdout.write("{}\n");
6345
6829
  break;
6346
6830
  }
@@ -6349,12 +6833,19 @@ async function main2() {
6349
6833
  const client2 = new MindGraph({
6350
6834
  baseUrl: hookBaseUrl || "https://api.mindgraph.cloud",
6351
6835
  apiKey: hookApiKey,
6352
- 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,
6353
6841
  maxRetries: 0,
6354
6842
  telemetrySurface: "mcp"
6355
6843
  });
6356
6844
  const hookOptions = {
6357
- 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
6358
6849
  };
6359
6850
  const output = hookHarness === "codex" ? await runCodexHook(
6360
6851
  input,
@@ -6367,7 +6858,15 @@ async function main2() {
6367
6858
  );
6368
6859
  process.stdout.write(`${JSON.stringify(output)}
6369
6860
  `);
6370
- } 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
+ );
6371
6870
  process.stdout.write("{}\n");
6372
6871
  }
6373
6872
  break;