@odla-ai/harness 0.7.0 → 0.8.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/node.cjs CHANGED
@@ -323,8 +323,8 @@ async function runContainerAttempt(options) {
323
323
  let stopped = false;
324
324
  let exited = false;
325
325
  child.stderr.setEncoding("utf8");
326
- child.stderr.on("data", (text) => {
327
- if (stderr.length < 64 * 1024) stderr += text.slice(0, 64 * 1024 - stderr.length);
326
+ child.stderr.on("data", (text2) => {
327
+ if (stderr.length < 64 * 1024) stderr += text2.slice(0, 64 * 1024 - stderr.length);
328
328
  });
329
329
  const stop = (reason) => {
330
330
  if (stopped || exited) return;
@@ -497,8 +497,8 @@ async function materializeGitTree(source, commitSha, options = {}) {
497
497
  const maxFiles = options.maxFiles ?? 2e4;
498
498
  const maxBytes = options.maxBytes ?? 512 * 1024 * 1024;
499
499
  const inventory = (await gitOutput(sourceDir, ["ls-tree", "-rz", commitSha], 16 * 1024 * 1024)).toString("utf8").split("\0").filter(Boolean);
500
- const entries = inventory.flatMap((record4) => {
501
- const match = /^(100644|100755) blob ([0-9a-f]{40,64})\t([\s\S]+)$/.exec(record4);
500
+ const entries = inventory.flatMap((record5) => {
501
+ const match = /^(100644|100755) blob ([0-9a-f]{40,64})\t([\s\S]+)$/.exec(record5);
502
502
  return match && allowedWorkspacePath(match[3]) ? [{ mode: match[1], hash: match[2], path: match[3] }] : [];
503
503
  });
504
504
  if (entries.length > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
@@ -583,12 +583,12 @@ async function gitSourceFiles(sourceDir, maxFiles, maxBytes) {
583
583
  });
584
584
  if (outputBytes > 8 * 1024 * 1024) throw new Error("git file inventory exceeds 8 MiB");
585
585
  if (code !== 0) throw new Error(`git file inventory failed: ${Buffer.concat(stderr).toString("utf8").slice(0, 1e3)}`);
586
- const paths = Buffer.concat(stdout).toString("utf8").split("\0").filter(Boolean).sort();
587
- if (paths.length > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
586
+ const paths2 = Buffer.concat(stdout).toString("utf8").split("\0").filter(Boolean).sort();
587
+ if (paths2.length > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
588
588
  const root = (0, import_node_path4.resolve)(sourceDir);
589
589
  const files = [];
590
590
  let bytes = 0;
591
- for (const relativePath of paths) {
591
+ for (const relativePath of paths2) {
592
592
  if (!allowedWorkspacePath(relativePath)) continue;
593
593
  const source = (0, import_node_path4.resolve)(root, relativePath);
594
594
  if (!source.startsWith(`${root}${import_node_path4.sep}`)) throw new TypeError("git file path escapes workspace");
@@ -796,8 +796,8 @@ async function runLeasedAttempt(lease, options) {
796
796
  allowUnpinnedImage: options.allowUnpinnedImage,
797
797
  workspaceAccess: options.workspaceAccess ?? (options.toolBroker ? "none" : "read-write"),
798
798
  signal: controller.signal,
799
- onStderr: async (text) => {
800
- await append(options.control, lease, "agent.stderr", "agent", { text: text.slice(0, 64 * 1024) });
799
+ onStderr: async (text2) => {
800
+ await append(options.control, lease, "agent.stderr", "agent", { text: text2.slice(0, 64 * 1024) });
801
801
  },
802
802
  onMessage: async (message2) => {
803
803
  if (message2.type === "event") {
@@ -1204,7 +1204,7 @@ function validateCodePatch(rawPatch, maxBytes) {
1204
1204
  if (FORBIDDEN.test(patch2) || /(?:old|new)(?: file)? mode 120000/.test(patch2)) {
1205
1205
  throw new TypeError("patch uses a forbidden binary, link, mode, rename, or copy operation");
1206
1206
  }
1207
- const paths = [];
1207
+ const paths2 = [];
1208
1208
  const lines = patch2.split("\n");
1209
1209
  for (let index = 0; index < lines.length; index += 1) {
1210
1210
  const line = lines[index];
@@ -1220,10 +1220,10 @@ function validateCodePatch(rawPatch, maxBytes) {
1220
1220
  if (!validHeaderPath(oldPath, path, "a") || !validHeaderPath(newPath, path, "b")) {
1221
1221
  throw new TypeError("patch file headers do not match the declared path");
1222
1222
  }
1223
- paths.push(path);
1223
+ paths2.push(path);
1224
1224
  }
1225
- if (!paths.length || new Set(paths).size !== paths.length) throw new TypeError("patch has no diffs or repeats a path");
1226
- return paths;
1225
+ if (!paths2.length || new Set(paths2).size !== paths2.length) throw new TypeError("patch has no diffs or repeats a path");
1226
+ return paths2;
1227
1227
  }
1228
1228
  function validHeaderPath(value, path, prefix) {
1229
1229
  return value === "/dev/null" || value === `${prefix}/${path}`;
@@ -1248,11 +1248,11 @@ function describePatchFailure(patch2, detail) {
1248
1248
  const hint = hunks.length > 0 && contextless ? " A hunk has no context lines; include at least one unchanged line above or below each change." : "";
1249
1249
  return `patch did not apply: ${detail}${hint}`;
1250
1250
  }
1251
- async function applyCodePatch(workspaceDir, rawPatch, paths) {
1251
+ async function applyCodePatch(workspaceDir, rawPatch, paths2) {
1252
1252
  const patch2 = stripPatchEnvelope(rawPatch);
1253
1253
  await gitApply(workspaceDir, patch2, true);
1254
1254
  await gitApply(workspaceDir, patch2, false);
1255
- for (const path of paths) {
1255
+ for (const path of paths2) {
1256
1256
  try {
1257
1257
  const info = await (0, import_promises6.lstat)(resolveCodePath(workspaceDir, path));
1258
1258
  if (info.isSymbolicLink() || !info.isFile() && !info.isDirectory()) {
@@ -1274,8 +1274,8 @@ function gitApply(cwd, patch2, check) {
1274
1274
  });
1275
1275
  let stderr = "";
1276
1276
  child.stderr.setEncoding("utf8");
1277
- child.stderr.on("data", (text) => {
1278
- if (stderr.length < 4e3) stderr += text.slice(0, 4e3);
1277
+ child.stderr.on("data", (text2) => {
1278
+ if (stderr.length < 4e3) stderr += text2.slice(0, 4e3);
1279
1279
  });
1280
1280
  child.once("error", reject);
1281
1281
  child.once("exit", (code) => code === 0 ? accept() : reject(new TypeError(describePatchFailure(patch2, stderr.trim().slice(0, 500)))));
@@ -1301,8 +1301,8 @@ async function restoreCodeWorkspaceCheckpoint(input) {
1301
1301
  const workspace = await stageWorkspace(input.trustedBaseDir, input.stage);
1302
1302
  try {
1303
1303
  if (checkpoint.patch) {
1304
- const paths = validateCodePatch(checkpoint.patch, 256 * 1024);
1305
- await applyCodePatch(workspace.workspaceDir, checkpoint.patch, paths);
1304
+ const paths2 = validateCodePatch(checkpoint.patch, 256 * 1024);
1305
+ await applyCodePatch(workspace.workspaceDir, checkpoint.patch, paths2);
1306
1306
  }
1307
1307
  return { workspace, checkpoint };
1308
1308
  } catch (error) {
@@ -1471,8 +1471,8 @@ async function verifyCodeCandidate(input) {
1471
1471
  try {
1472
1472
  const baseDigest = await digestStagedWorkspace(staged.workspaceDir, limits);
1473
1473
  if (baseDigest !== input.trustedBaseDigest) throw new TypeError("trusted base does not match its registered digest");
1474
- const paths = validateCodePatch(input.candidatePatch, policy.maximumPatchBytes);
1475
- await applyCodePatch(staged.workspaceDir, input.candidatePatch, paths);
1474
+ const paths2 = validateCodePatch(input.candidatePatch, policy.maximumPatchBytes);
1475
+ await applyCodePatch(staged.workspaceDir, input.candidatePatch, paths2);
1476
1476
  const sourceDigest = await digestStagedWorkspace(staged.workspaceDir, limits);
1477
1477
  const policyDigest = digestPolicy(policy);
1478
1478
  const patchDigest = digestBytes(input.candidatePatch);
@@ -1481,7 +1481,7 @@ async function verifyCodeCandidate(input) {
1481
1481
  trustedBaseDigest: input.trustedBaseDigest,
1482
1482
  patchDigest
1483
1483
  });
1484
- const changedTests = changedTestPaths(paths, policy);
1484
+ const changedTests = changedTestPaths(paths2, policy);
1485
1485
  if (changedTests.length > policy.maximumChangedTests) throw new TypeError("candidate changes too many test files");
1486
1486
  const recipes = [];
1487
1487
  const logs = [];
@@ -1548,8 +1548,8 @@ function validate(input) {
1548
1548
  }
1549
1549
  return result;
1550
1550
  }
1551
- function changedTestPaths(paths, policy) {
1552
- return paths.filter((path) => policy.testPathSuffixes.some((suffix) => path.endsWith(suffix)) || policy.testPathPrefixes.some((prefix) => path.startsWith(prefix) || path.includes(`/${prefix}`))).sort();
1551
+ function changedTestPaths(paths2, policy) {
1552
+ return paths2.filter((path) => policy.testPathSuffixes.some((suffix) => path.endsWith(suffix)) || policy.testPathPrefixes.some((prefix) => path.startsWith(prefix) || path.includes(`/${prefix}`))).sort();
1553
1553
  }
1554
1554
  function recipeReceipt(recipe2, result, artifacts) {
1555
1555
  const status = result.timedOut ? "timed_out" : result.outputLimitExceeded ? "output_limited" : result.exitCode === 0 && artifacts.every((item) => item.status === "verified") ? "passed" : "failed";
@@ -1983,7 +1983,10 @@ Never claim a build or test passed unless odla_run_recipe returned that result.`
1983
1983
  var V2_SYSTEM_PROMPT = `You are the coding agent inside an odla Code harness.
1984
1984
  Start by orienting: odla_list shows the files in the workspace and odla_search
1985
1985
  finds a literal string across them. Prefer those over guessing a path.
1986
- Then odla_read a bounded range, and odla_apply_git_diff to mutate.
1986
+ Then odla_read a bounded range, and odla_apply_git_diff to mutate. When you
1987
+ need several independent searches or file ranges, issue those read-only calls
1988
+ together in one turn; their results stay ordered and the harness overlaps them.
1989
+ Never issue odla_apply_git_diff or odla_run_recipe alongside another tool call.
1987
1990
  For mutations, call odla_apply_git_diff with raw git diff text. It must start
1988
1991
  with "diff --git a/<path> b/<path>", include matching "---" and "+++" file
1989
1992
  headers and numbered "@@" hunks, and never use "*** Begin Patch" wrappers.
@@ -2014,18 +2017,26 @@ var SYSTEM_PROMPT_FOR = {
2014
2017
  };
2015
2018
  function codeSkill(opts) {
2016
2019
  let seq = 0;
2020
+ let nextCompletion = 1;
2021
+ const completed = /* @__PURE__ */ new Map();
2017
2022
  const call = async (tool, input, signal) => {
2023
+ const sequence = ++seq;
2018
2024
  const startedAt = Date.now();
2019
2025
  const response2 = await opts.broker.execute(
2020
2026
  { lease: opts.lease, workspaceDir: opts.workspaceDir, signal },
2021
- { requestId: `bench-${tool}-${++seq}`, tool, input }
2027
+ { requestId: `bench-${tool}-${sequence}`, tool, input }
2022
2028
  );
2023
- opts.onToolCall?.({
2029
+ completed.set(sequence, {
2024
2030
  tool,
2025
2031
  ok: response2.ok,
2026
2032
  durationMs: Date.now() - startedAt,
2027
2033
  ...response2.ok ? {} : { error: String(response2.content).slice(0, 300) }
2028
2034
  });
2035
+ while (completed.has(nextCompletion)) {
2036
+ const completion = completed.get(nextCompletion);
2037
+ completed.delete(nextCompletion++);
2038
+ opts.onToolCall?.(completion);
2039
+ }
2029
2040
  return { content: response2.content, isError: !response2.ok };
2030
2041
  };
2031
2042
  const read2 = {
@@ -2041,6 +2052,7 @@ function codeSkill(opts) {
2041
2052
  },
2042
2053
  additionalProperties: false
2043
2054
  },
2055
+ concurrency: "parallel",
2044
2056
  handler: (input, ctx) => call("sandbox.read", input, ctx.signal)
2045
2057
  };
2046
2058
  const applyPatch = {
@@ -2082,6 +2094,7 @@ function codeSkill(opts) {
2082
2094
  },
2083
2095
  additionalProperties: false
2084
2096
  },
2097
+ concurrency: "parallel",
2085
2098
  handler: (input, ctx) => call("sandbox.list", input, ctx.signal)
2086
2099
  };
2087
2100
  const searchFiles = {
@@ -2098,11 +2111,13 @@ function codeSkill(opts) {
2098
2111
  },
2099
2112
  additionalProperties: false
2100
2113
  },
2114
+ concurrency: "parallel",
2101
2115
  handler: (input, ctx) => call("sandbox.search", input, ctx.signal)
2102
2116
  };
2103
2117
  const graphTool = (name, tool, description, required) => ({
2104
2118
  name,
2105
2119
  description,
2120
+ concurrency: "parallel",
2106
2121
  inputSchema: {
2107
2122
  type: "object",
2108
2123
  ...required ? { required: ["query"] } : {},
@@ -2405,16 +2420,16 @@ function createCodePolicyGate(options) {
2405
2420
  }
2406
2421
  };
2407
2422
  }
2408
- function directoryPrefixes(paths) {
2423
+ function directoryPrefixes(paths2) {
2409
2424
  const prefixes = /* @__PURE__ */ new Set(["."]);
2410
- for (const path of paths) {
2425
+ for (const path of paths2) {
2411
2426
  const parts = path.split("/");
2412
2427
  for (let index = 1; index < parts.length; index += 1) prefixes.add(parts.slice(0, index).join("/"));
2413
2428
  }
2414
2429
  return [...prefixes].sort();
2415
2430
  }
2416
- async function safePrefix(base, paths, prefix) {
2417
- const prefixes = directoryPrefixes(paths);
2431
+ async function safePrefix(base, paths2, prefix) {
2432
+ const prefixes = directoryPrefixes(paths2);
2418
2433
  const conversions = await conversionRegistry(
2419
2434
  [await registeredPolicy("code.prefix.v1", "code.prefixes.v1", prefixes)],
2420
2435
  { "code.prefixes.v1": prefixes }
@@ -2532,13 +2547,32 @@ function response(request, ok, content, details) {
2532
2547
  var import_promises11 = require("fs/promises");
2533
2548
 
2534
2549
  // src/code-tool-discovery.ts
2550
+ var import_node_child_process6 = require("child_process");
2535
2551
  var import_promises9 = require("fs/promises");
2536
2552
  var import_node_path9 = require("path");
2537
2553
  var DEFAULT_MAX_FILES = 2e4;
2538
2554
  var DEFAULT_MAX_RESULTS = 100;
2539
2555
  var DEFAULT_MAX_FILE_BYTES = 512 * 1024;
2556
+ function createWorkspaceFileRegistry(limit = DEFAULT_MAX_FILES, enumerate = registeredFiles) {
2557
+ const cache2 = /* @__PURE__ */ new Map();
2558
+ return {
2559
+ files(root) {
2560
+ const existing = cache2.get(root);
2561
+ if (existing) return existing;
2562
+ const pending = enumerate(root, limit).then((paths2) => Object.freeze(paths2));
2563
+ cache2.set(root, pending);
2564
+ void pending.catch(() => {
2565
+ if (cache2.get(root) === pending) cache2.delete(root);
2566
+ });
2567
+ return pending;
2568
+ },
2569
+ invalidate(root) {
2570
+ cache2.delete(root);
2571
+ }
2572
+ };
2573
+ }
2540
2574
  async function registeredFiles(root, limit = DEFAULT_MAX_FILES) {
2541
- const paths = [];
2575
+ const paths2 = [];
2542
2576
  const walk = async (directory) => {
2543
2577
  for (const entry of await (0, import_promises9.readdir)(directory, { withFileTypes: true })) {
2544
2578
  if (SKIP_WORKSPACE_DIRS.has(entry.name)) continue;
@@ -2552,43 +2586,137 @@ async function registeredFiles(root, limit = DEFAULT_MAX_FILES) {
2552
2586
  } catch {
2553
2587
  continue;
2554
2588
  }
2555
- paths.push(path);
2556
- if (paths.length > limit) throw new TypeError("workspace file registry exceeds its bound");
2589
+ paths2.push(path);
2590
+ if (paths2.length > limit) throw new TypeError("workspace file registry exceeds its bound");
2557
2591
  }
2558
2592
  }
2559
2593
  };
2560
2594
  await walk((0, import_node_path9.resolve)(root));
2561
- return paths.sort();
2595
+ return paths2.sort();
2562
2596
  }
2563
- function listWorkspace(paths, options = {}) {
2597
+ function listWorkspace(paths2, options = {}) {
2564
2598
  const max = options.maxEntries ?? 1e3;
2565
2599
  const prefix = options.prefix?.replace(/\/+$/, "");
2566
- const scoped = prefix ? paths.filter((path) => path === prefix || path.startsWith(`${prefix}/`)) : [...paths];
2600
+ const scoped = prefix ? paths2.filter((path) => path === prefix || path.startsWith(`${prefix}/`)) : [...paths2];
2567
2601
  return scoped.slice(0, max);
2568
2602
  }
2569
- async function searchWorkspace(root, paths, options) {
2570
- const query = options.caseSensitive === false ? options.query.toLowerCase() : options.query;
2571
- if (!query) throw new TypeError("search query must be a non-empty string");
2603
+ async function searchWorkspace(root, paths2, options) {
2604
+ options.signal?.throwIfAborted();
2605
+ if (!options.query) throw new TypeError("search query must be a non-empty string");
2572
2606
  const maxResults = options.maxResults ?? DEFAULT_MAX_RESULTS;
2573
2607
  const maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
2574
- const scoped = listWorkspace(paths, { ...options.prefix ? { prefix: options.prefix } : {}, maxEntries: paths.length });
2608
+ const scoped = listWorkspace(paths2, { ...options.prefix ? { prefix: options.prefix } : {}, maxEntries: paths2.length });
2609
+ if (scoped.length === 0) return [];
2610
+ try {
2611
+ return await nativeSearch(root, scoped, { ...options, maxResults, maxFileBytes });
2612
+ } catch (error) {
2613
+ options.signal?.throwIfAborted();
2614
+ return fallbackSearch(root, scoped, { ...options, maxResults, maxFileBytes });
2615
+ }
2616
+ }
2617
+ var MAX_NATIVE_ARG_BYTES = 96 * 1024;
2618
+ async function nativeSearch(root, paths2, options) {
2619
+ const batches = [];
2620
+ let batch = [];
2621
+ let bytes = 0;
2622
+ for (const path of paths2) {
2623
+ const size = Buffer.byteLength(path) + 1;
2624
+ if (batch.length > 0 && bytes + size > MAX_NATIVE_ARG_BYTES) {
2625
+ batches.push(batch);
2626
+ batch = [];
2627
+ bytes = 0;
2628
+ }
2629
+ batch.push(path);
2630
+ bytes += size;
2631
+ }
2632
+ if (batch.length > 0) batches.push(batch);
2633
+ const matches = [];
2634
+ for (const files of batches) {
2635
+ const remaining = options.maxResults - matches.length;
2636
+ if (remaining <= 0) break;
2637
+ matches.push(...await nativeSearchBatch(root, files, options, remaining));
2638
+ }
2639
+ return matches;
2640
+ }
2641
+ function nativeSearchBatch(root, paths2, options, remaining) {
2642
+ return new Promise((resolveMatches, reject) => {
2643
+ const args = [
2644
+ "--fixed-strings",
2645
+ "--json",
2646
+ "--no-messages",
2647
+ "--sort=path",
2648
+ `--max-filesize=${options.maxFileBytes}`,
2649
+ "--max-columns=4096",
2650
+ "--max-columns-preview",
2651
+ options.caseSensitive === false ? "--ignore-case" : "--case-sensitive",
2652
+ "--",
2653
+ options.query,
2654
+ ...paths2
2655
+ ];
2656
+ const child = (0, import_node_child_process6.spawn)("rg", args, {
2657
+ cwd: root,
2658
+ stdio: ["ignore", "pipe", "ignore"],
2659
+ ...options.signal ? { signal: options.signal } : {}
2660
+ });
2661
+ const matches = [];
2662
+ let carry = "";
2663
+ let stopped = false;
2664
+ const consume = (line) => {
2665
+ if (matches.length >= remaining) return;
2666
+ let event;
2667
+ try {
2668
+ event = JSON.parse(line);
2669
+ } catch {
2670
+ return;
2671
+ }
2672
+ const path = event.data?.path?.text;
2673
+ const lineNumber = event.data?.line_number;
2674
+ const source = event.data?.lines?.text;
2675
+ if (event.type !== "match" || path === void 0 || lineNumber === void 0 || source === void 0) return;
2676
+ matches.push({ path, line: lineNumber, text: source.trim().slice(0, 240) });
2677
+ if (matches.length >= remaining) {
2678
+ stopped = true;
2679
+ child.kill();
2680
+ }
2681
+ };
2682
+ child.stdout.setEncoding("utf8");
2683
+ child.stdout.on("data", (chunk) => {
2684
+ carry += chunk;
2685
+ let newline = carry.indexOf("\n");
2686
+ while (newline >= 0) {
2687
+ consume(carry.slice(0, newline));
2688
+ carry = carry.slice(newline + 1);
2689
+ newline = carry.indexOf("\n");
2690
+ }
2691
+ });
2692
+ child.once("error", reject);
2693
+ child.once("close", (code) => {
2694
+ if (carry) consume(carry);
2695
+ if (stopped || code === 0 || code === 1) resolveMatches(matches);
2696
+ else reject(new Error(`native search exited with status ${code ?? "unknown"}`));
2697
+ });
2698
+ });
2699
+ }
2700
+ async function fallbackSearch(root, scoped, options) {
2701
+ const query = options.caseSensitive === false ? options.query.toLowerCase() : options.query;
2575
2702
  const matches = [];
2576
2703
  for (const path of scoped) {
2577
- if (matches.length >= maxResults) break;
2704
+ options.signal?.throwIfAborted();
2705
+ if (matches.length >= options.maxResults) break;
2578
2706
  let source;
2579
2707
  try {
2580
2708
  source = await (0, import_promises9.readFile)((0, import_node_path9.resolve)(root, path));
2581
2709
  } catch {
2582
2710
  continue;
2583
2711
  }
2584
- if (source.byteLength > maxFileBytes || source.includes(0)) continue;
2712
+ if (source.byteLength > options.maxFileBytes || source.includes(0)) continue;
2585
2713
  const lines = source.toString("utf8").split("\n");
2586
2714
  for (let index = 0; index < lines.length; index += 1) {
2587
2715
  const raw = lines[index];
2588
2716
  const haystack = options.caseSensitive === false ? raw.toLowerCase() : raw;
2589
2717
  if (!haystack.includes(query)) continue;
2590
2718
  matches.push({ path, line: index + 1, text: raw.trim().slice(0, 240) });
2591
- if (matches.length >= maxResults) break;
2719
+ if (matches.length >= options.maxResults) break;
2592
2720
  }
2593
2721
  }
2594
2722
  return matches;
@@ -2600,7 +2728,7 @@ var import_node_path10 = require("path");
2600
2728
  var import_graph = require("@odla-ai/graph");
2601
2729
  var import_code4 = require("@odla-ai/graph/code");
2602
2730
  var cache = /* @__PURE__ */ new Map();
2603
- function workspaceGraphs(workspaceDir, paths) {
2731
+ function workspaceGraphs(workspaceDir, paths2) {
2604
2732
  const existing = cache.get(workspaceDir);
2605
2733
  if (existing) return existing;
2606
2734
  const read2 = (path) => (0, import_promises10.readFile)((0, import_node_path10.join)(workspaceDir, path), "utf8");
@@ -2608,11 +2736,14 @@ function workspaceGraphs(workspaceDir, paths) {
2608
2736
  // No knownTables: a staged workspace may not carry migrations, and a filter
2609
2737
  // that silently drops every table is worse than an unfiltered one. Callers
2610
2738
  // with ground truth should build the graph themselves.
2611
- graph: await (0, import_code4.buildCodeGraph)({ paths, read: read2, data: { ignore: (path) => path.includes(".generated.") } })
2739
+ graph: await (0, import_code4.buildCodeGraph)({ paths: paths2, read: read2, data: { ignore: (path) => path.includes(".generated.") } })
2612
2740
  }))();
2613
2741
  cache.set(workspaceDir, built);
2614
2742
  return built;
2615
2743
  }
2744
+ function forgetWorkspaceGraphs(workspaceDir) {
2745
+ cache.delete(workspaceDir);
2746
+ }
2616
2747
  var shortId = (id) => id.slice(id.indexOf(":") + 1);
2617
2748
  function renderOverview(graphs, prefix) {
2618
2749
  const rows = (0, import_graph.rollup)(graphs.graph, import_code4.FILE, prefix === void 0 ? {} : { prefix });
@@ -2659,7 +2790,7 @@ var GRAPH_TOOLS = /* @__PURE__ */ new Set([
2659
2790
  "sandbox.who_imports",
2660
2791
  "sandbox.who_touches"
2661
2792
  ]);
2662
- async function read(context, request, options, policy) {
2793
+ async function read(context, request, options, policy, registry) {
2663
2794
  exactKeys(request.input, ["path", "startLine", "endLine"]);
2664
2795
  const path = stringField(request.input, "path");
2665
2796
  const startLine = optionalInteger(request.input.startLine) ?? 1;
@@ -2667,11 +2798,11 @@ async function read(context, request, options, policy) {
2667
2798
  if (endLine < startLine || endLine - startLine + 1 > (options.maxReadLines ?? 2e3)) {
2668
2799
  throw new TypeError("requested line range exceeds its bound");
2669
2800
  }
2670
- const paths = await registeredFiles(context.workspaceDir, 2e4);
2671
- if (!paths.includes(path)) {
2801
+ const paths2 = await registry.files(context.workspaceDir);
2802
+ if (!paths2.includes(path)) {
2672
2803
  throw new TypeError(`no such file in the staged workspace: "${path}". Use sandbox.overview, sandbox.where_is or sandbox.search to find the correct path.`);
2673
2804
  }
2674
- const allowed = await policy.read(policyContext(context, request, options, { paths, path, startLine, endLine }));
2805
+ const allowed = await policy.read(policyContext(context, request, options, { paths: paths2, path, startLine, endLine }));
2675
2806
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
2676
2807
  const target = resolveCodePath(context.workspaceDir, path);
2677
2808
  const info = await (0, import_promises11.stat)(target);
@@ -2687,22 +2818,22 @@ async function read(context, request, options, policy) {
2687
2818
  }
2688
2819
  return response(request, true, content, { path, startLine, endLine: Math.min(endLine, lines.length) });
2689
2820
  }
2690
- async function list(context, request, options, policy) {
2821
+ async function list(context, request, options, policy, registry) {
2691
2822
  exactKeys(request.input, ["prefix", "maxEntries"]);
2692
2823
  const raw = request.input.prefix;
2693
2824
  const prefix = typeof raw === "string" && raw.length > 0 ? raw : void 0;
2694
2825
  const maxEntries = optionalInteger(request.input.maxEntries) ?? 1e3;
2695
2826
  if (maxEntries > 5e3) throw new TypeError("maxEntries exceeds its bound");
2696
- const paths = await registeredFiles(context.workspaceDir, 2e4);
2697
- const allowed = await policy.list(policyContext(context, request, options, { paths, ...prefix ? { prefix } : {} }));
2827
+ const paths2 = await registry.files(context.workspaceDir);
2828
+ const allowed = await policy.list(policyContext(context, request, options, { paths: paths2, ...prefix ? { prefix } : {} }));
2698
2829
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
2699
- const entries = listWorkspace(paths, { ...prefix ? { prefix } : {}, maxEntries });
2830
+ const entries = listWorkspace(paths2, { ...prefix ? { prefix } : {}, maxEntries });
2700
2831
  if (!entries.length) {
2701
2832
  return response(request, true, prefix ? `No files under "${prefix}".` : "Workspace is empty.", { count: 0 });
2702
2833
  }
2703
- const truncated = entries.length < paths.length && entries.length === maxEntries;
2704
- const hint = !prefix && paths.length > 500 ? `
2705
- \u2026 ${paths.length} files total. sandbox.overview is far cheaper for orientation; use a prefix here once you know the area.` : "";
2834
+ const truncated = entries.length < paths2.length && entries.length === maxEntries;
2835
+ const hint = !prefix && paths2.length > 500 ? `
2836
+ \u2026 ${paths2.length} files total. sandbox.overview is far cheaper for orientation; use a prefix here once you know the area.` : "";
2706
2837
  return response(
2707
2838
  request,
2708
2839
  true,
@@ -2711,7 +2842,7 @@ async function list(context, request, options, policy) {
2711
2842
  { count: entries.length, truncated }
2712
2843
  );
2713
2844
  }
2714
- async function search(context, request, options, policy) {
2845
+ async function search(context, request, options, policy, registry) {
2715
2846
  exactKeys(request.input, ["query", "prefix", "maxResults", "caseSensitive"]);
2716
2847
  const query = stringField(request.input, "query");
2717
2848
  if (query.length > 512) throw new TypeError("search query exceeds its bound");
@@ -2720,21 +2851,22 @@ async function search(context, request, options, policy) {
2720
2851
  const maxResults = optionalInteger(request.input.maxResults) ?? 100;
2721
2852
  if (maxResults > 500) throw new TypeError("maxResults exceeds its bound");
2722
2853
  const caseSensitive = request.input.caseSensitive === void 0 ? true : request.input.caseSensitive === true;
2723
- const paths = await registeredFiles(context.workspaceDir, 2e4);
2724
- const allowed = await policy.search(policyContext(context, request, options, { paths, query, ...prefix ? { prefix } : {} }));
2854
+ const paths2 = await registry.files(context.workspaceDir);
2855
+ const allowed = await policy.search(policyContext(context, request, options, { paths: paths2, query, ...prefix ? { prefix } : {} }));
2725
2856
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
2726
- const matches = await searchWorkspace(context.workspaceDir, paths, {
2857
+ const matches = await searchWorkspace(context.workspaceDir, paths2, {
2727
2858
  query,
2728
2859
  maxResults,
2729
2860
  caseSensitive,
2730
- ...prefix ? { prefix } : {}
2861
+ ...prefix ? { prefix } : {},
2862
+ ...context.signal ? { signal: context.signal } : {}
2731
2863
  });
2732
2864
  if (!matches.length) return response(request, true, `No match for "${query}".`, { count: 0 });
2733
2865
  return response(request, true, matches.map((match) => `${match.path}:${match.line}: ${match.text}`).join("\n"), {
2734
2866
  count: matches.length
2735
2867
  });
2736
2868
  }
2737
- async function graphQuery(context, request, options, policy) {
2869
+ async function graphQuery(context, request, options, policy, registry) {
2738
2870
  exactKeys(request.input, ["query"]);
2739
2871
  const raw = request.input.query;
2740
2872
  const query = typeof raw === "string" ? raw : "";
@@ -2744,8 +2876,8 @@ async function graphQuery(context, request, options, policy) {
2744
2876
  selector: query
2745
2877
  }));
2746
2878
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
2747
- const paths = await registeredFiles(context.workspaceDir, 2e4);
2748
- const graphs = await workspaceGraphs(context.workspaceDir, paths);
2879
+ const paths2 = await registry.files(context.workspaceDir);
2880
+ const graphs = await workspaceGraphs(context.workspaceDir, paths2);
2749
2881
  if (request.tool === "sandbox.overview") {
2750
2882
  return response(request, true, renderOverview(graphs, query || void 0));
2751
2883
  }
@@ -2760,23 +2892,38 @@ function createCodeToolBroker(options) {
2760
2892
  validateOptions(options);
2761
2893
  const recipes = new Map(options.recipes.map((recipe2) => [recipe2.id, recipe2]));
2762
2894
  const policy = createCodePolicyGate(options);
2763
- let tail = Promise.resolve();
2895
+ const registry = createWorkspaceFileRegistry();
2896
+ let barrier = Promise.resolve();
2897
+ const activeReads = /* @__PURE__ */ new Set();
2764
2898
  return {
2765
2899
  execute(context, request) {
2766
- const result = tail.then(() => route(context, request, options, recipes, policy));
2767
- tail = result.then(() => void 0, () => void 0);
2900
+ if (isReadTool(request.tool)) {
2901
+ const result2 = barrier.then(() => route(context, request, options, recipes, policy, registry));
2902
+ const settled = result2.then(() => void 0, () => void 0);
2903
+ activeReads.add(settled);
2904
+ void settled.then(() => {
2905
+ activeReads.delete(settled);
2906
+ });
2907
+ return result2;
2908
+ }
2909
+ const earlierReads = [...activeReads];
2910
+ const result = barrier.then(() => Promise.all(earlierReads)).then(() => route(context, request, options, recipes, policy, registry));
2911
+ barrier = result.then(() => void 0, () => void 0);
2768
2912
  return result;
2769
2913
  }
2770
2914
  };
2771
2915
  }
2772
- async function route(context, request, options, recipes, policy) {
2916
+ function isReadTool(tool) {
2917
+ return tool === "sandbox.read" || tool === "sandbox.list" || tool === "sandbox.search" || GRAPH_TOOLS.has(tool);
2918
+ }
2919
+ async function route(context, request, options, recipes, policy, registry) {
2773
2920
  try {
2774
2921
  if (context.signal?.aborted) throw new TypeError("tool request was cancelled");
2775
- if (request.tool === "sandbox.read") return await read(context, request, options, policy);
2776
- if (request.tool === "sandbox.list") return await list(context, request, options, policy);
2777
- if (request.tool === "sandbox.search") return await search(context, request, options, policy);
2778
- if (GRAPH_TOOLS.has(request.tool)) return await graphQuery(context, request, options, policy);
2779
- if (request.tool === "sandbox.apply_patch") return await patch(context, request, options, policy);
2922
+ if (request.tool === "sandbox.read") return await read(context, request, options, policy, registry);
2923
+ if (request.tool === "sandbox.list") return await list(context, request, options, policy, registry);
2924
+ if (request.tool === "sandbox.search") return await search(context, request, options, policy, registry);
2925
+ if (GRAPH_TOOLS.has(request.tool)) return await graphQuery(context, request, options, policy, registry);
2926
+ if (request.tool === "sandbox.apply_patch") return await patch(context, request, options, policy, registry);
2780
2927
  return await recipe(context, request, options, recipes, policy);
2781
2928
  } catch (reason) {
2782
2929
  return response(request, false, toolFailureMessage(reason));
@@ -2791,17 +2938,19 @@ function toolFailureMessage(reason) {
2791
2938
  if (code === "EACCES" || code === "EPERM") return "that path is not readable through this tool";
2792
2939
  return "tool failed closed";
2793
2940
  }
2794
- async function patch(context, request, options, policy) {
2941
+ async function patch(context, request, options, policy, registry) {
2795
2942
  exactKeys(request.input, ["patch"]);
2796
2943
  const value = stringField(request.input, "patch");
2797
- const paths = validateCodePatch(value, options.maxPatchBytes ?? 256 * 1024);
2798
- if (paths.some((path) => options.readOnlyPrefixes?.some((prefix) => path === prefix || path.startsWith(`${prefix}/`)))) {
2944
+ const paths2 = validateCodePatch(value, options.maxPatchBytes ?? 256 * 1024);
2945
+ if (paths2.some((path) => options.readOnlyPrefixes?.some((prefix) => path === prefix || path.startsWith(`${prefix}/`)))) {
2799
2946
  throw new TypeError("patch targets a read-only reference source");
2800
2947
  }
2801
2948
  const allowed = await policy.patch(policyContext(context, request, options, { patch: value }));
2802
2949
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
2803
- await applyCodePatch(context.workspaceDir, value, paths);
2804
- return response(request, true, `Applied patch to ${paths.length} file(s).`, { paths });
2950
+ await applyCodePatch(context.workspaceDir, value, paths2);
2951
+ registry.invalidate(context.workspaceDir);
2952
+ forgetWorkspaceGraphs(context.workspaceDir);
2953
+ return response(request, true, `Applied patch to ${paths2.length} file(s).`, { paths: paths2 });
2805
2954
  }
2806
2955
  async function recipe(context, request, options, recipes, policy) {
2807
2956
  exactKeys(request.input, ["recipeId"]);
@@ -3185,12 +3334,129 @@ var import_node_crypto4 = require("crypto");
3185
3334
  async function appendCodeRuntimeEvent(control, command, event, refs) {
3186
3335
  const eventId = `${command.commandId.slice(0, 45)}:${refs.length + 1}`;
3187
3336
  refs.push(eventId);
3188
- const bounded = event.type === "message" ? { ...event, body: event.body.trim().slice(0, 2e4) || `${event.actor} event` } : event;
3337
+ const attributed = { ...event, interactionId: command.commandId };
3338
+ const bounded = attributed.type === "message" ? { ...attributed, body: attributed.body.trim().slice(0, 2e4) || `${attributed.actor} event` } : attributed;
3189
3339
  await control.appendSessionEvent(command.sessionId, eventId, bounded);
3190
3340
  }
3191
3341
  var digestRuntimeValue = (value) => `sha256:${(0, import_node_crypto4.createHash)("sha256").update(value).digest("hex")}`;
3192
3342
  var runtimeErrorMessage = (value) => value instanceof Error ? value.message : String(value);
3193
3343
 
3344
+ // src/code-tool-presentation.ts
3345
+ var text = (value, maximum) => {
3346
+ if (typeof value !== "string") return void 0;
3347
+ const bounded = value.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ").trim();
3348
+ return bounded ? bounded.slice(0, maximum) : void 0;
3349
+ };
3350
+ var integer2 = (value) => Number.isSafeInteger(value) && Number(value) >= 0 ? Number(value) : void 0;
3351
+ var record4 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
3352
+ var excerpt = (value, tail = false) => {
3353
+ if (typeof value !== "string") return void 0;
3354
+ const safe = value.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ");
3355
+ const source = (tail ? safe.slice(-1e4) : safe.slice(0, 1e4)).trim();
3356
+ if (!source) return void 0;
3357
+ const lines = source.split("\n").filter((line) => line.trim()).map((line) => line.slice(0, 240));
3358
+ const selected = tail ? lines.slice(-10) : lines.slice(0, 10);
3359
+ return text(selected.join("\n"), 2400);
3360
+ };
3361
+ var paths = (value) => {
3362
+ if (!Array.isArray(value)) return void 0;
3363
+ const items = value.flatMap((item) => {
3364
+ const path = text(item, 1024);
3365
+ return path ? [path] : [];
3366
+ }).slice(0, 12);
3367
+ return items.length ? items : void 0;
3368
+ };
3369
+ function patchStats(value) {
3370
+ if (typeof value !== "string") return {};
3371
+ let additions = 0;
3372
+ let deletions = 0;
3373
+ for (const line of value.slice(0, 262144).split("\n")) {
3374
+ if (line.startsWith("+++") || line.startsWith("---")) continue;
3375
+ if (line.startsWith("+")) additions += 1;
3376
+ else if (line.startsWith("-")) deletions += 1;
3377
+ }
3378
+ return { ...additions ? { additions } : {}, ...deletions ? { deletions } : {} };
3379
+ }
3380
+ function searchResults(value) {
3381
+ if (typeof value !== "string") return void 0;
3382
+ const results = value.split("\n").flatMap((line) => {
3383
+ const match = /^([^:\n]{1,1024}):(\d+):\s?(.*)$/.exec(line);
3384
+ if (!match) return [];
3385
+ const lineNumber = Number(match[2]);
3386
+ const itemText = text(match[3], 240);
3387
+ if (!Number.isSafeInteger(lineNumber) || lineNumber < 1) return [];
3388
+ return [{ path: match[1], line: lineNumber, ...itemText ? { text: itemText } : {} }];
3389
+ }).slice(0, 5);
3390
+ return results.length ? results : void 0;
3391
+ }
3392
+ function codeToolRequestPresentation(request) {
3393
+ const input = request.input;
3394
+ if (request.tool === "sandbox.read") {
3395
+ const path = text(input.path, 1024);
3396
+ if (!path) return void 0;
3397
+ const startLine = integer2(input.startLine);
3398
+ const endLine = integer2(input.endLine);
3399
+ return { kind: "read", path, ...startLine ? { startLine } : {}, ...endLine ? { endLine } : {} };
3400
+ }
3401
+ if (request.tool === "sandbox.list") {
3402
+ const scope = text(input.prefix, 1024);
3403
+ return { kind: "list", ...scope ? { scope } : {} };
3404
+ }
3405
+ if (request.tool === "sandbox.search" || request.tool === "sandbox.overview" || request.tool === "sandbox.where_is" || request.tool === "sandbox.who_imports" || request.tool === "sandbox.who_touches") {
3406
+ const query = text(input.query, 512);
3407
+ const scope = request.tool === "sandbox.search" ? text(input.prefix, 1024) : void 0;
3408
+ if (request.tool === "sandbox.search" && !query) return void 0;
3409
+ return { kind: "query", ...query ? { query } : {}, ...scope ? { scope } : {} };
3410
+ }
3411
+ if (request.tool === "sandbox.apply_patch") {
3412
+ return { kind: "patch", ...patchStats(input.patch) };
3413
+ }
3414
+ const recipeId = text(input.recipeId, 120);
3415
+ return recipeId ? { kind: "recipe", recipeId } : void 0;
3416
+ }
3417
+ function codeToolResultPresentation(request, response2) {
3418
+ const started = codeToolRequestPresentation(request);
3419
+ if (!started || !response2.ok) return started;
3420
+ const details = record4(response2.details);
3421
+ if (started.kind === "read") {
3422
+ return {
3423
+ ...started,
3424
+ ...integer2(details?.startLine) ? { startLine: integer2(details?.startLine) } : {},
3425
+ ...integer2(details?.endLine) ? { endLine: integer2(details?.endLine) } : {},
3426
+ ...excerpt(response2.content) ? { excerpt: excerpt(response2.content) } : {}
3427
+ };
3428
+ }
3429
+ if (started.kind === "list") {
3430
+ const listed = response2.content.split("\n").filter((line) => line && !line.startsWith("\u2026") && !line.startsWith("Workspace ")).map((line) => text(line, 1024)).filter((line) => Boolean(line)).slice(0, 8);
3431
+ return {
3432
+ ...started,
3433
+ ...integer2(details?.count) !== void 0 ? { count: integer2(details?.count) } : {},
3434
+ ...listed.length ? { paths: listed } : {}
3435
+ };
3436
+ }
3437
+ if (started.kind === "query") {
3438
+ const results = request.tool === "sandbox.search" ? searchResults(response2.content) : void 0;
3439
+ const resultExcerpt = request.tool === "sandbox.search" ? void 0 : excerpt(response2.content);
3440
+ return {
3441
+ ...started,
3442
+ ...integer2(details?.count) !== void 0 ? { count: integer2(details?.count) } : {},
3443
+ ...results ? { results } : {},
3444
+ ...resultExcerpt ? { excerpt: resultExcerpt } : {}
3445
+ };
3446
+ }
3447
+ if (started.kind === "patch") {
3448
+ return { ...started, ...paths(details?.paths) ? { paths: paths(details?.paths) } : {} };
3449
+ }
3450
+ const output = response2.content.replace(/^Recipe [^\n]*\.?\s*/u, "");
3451
+ return {
3452
+ ...started,
3453
+ ...integer2(details?.exitCode) !== void 0 ? { exitCode: integer2(details?.exitCode) } : {},
3454
+ ...typeof details?.timedOut === "boolean" ? { timedOut: details.timedOut } : {},
3455
+ ...typeof details?.outputLimitExceeded === "boolean" ? { outputLimitExceeded: details.outputLimitExceeded } : {},
3456
+ ...excerpt(output, true) ? { excerpt: excerpt(output, true) } : {}
3457
+ };
3458
+ }
3459
+
3194
3460
  // src/code-runtime-engine.ts
3195
3461
  var TheseusRuntimeEngine = class {
3196
3462
  constructor(options) {
@@ -3425,18 +3691,29 @@ var TheseusRuntimeEngine = class {
3425
3691
  return {
3426
3692
  execute: async (context, request) => {
3427
3693
  const startedAt = Date.now();
3694
+ const operationId = digestRuntimeValue(`${command.commandId}:${request.requestId}`);
3695
+ const startedPresentation = codeToolRequestPresentation(request);
3428
3696
  await this.#event(
3429
3697
  command,
3430
- { type: "tool", phase: "started", tool: request.tool },
3698
+ {
3699
+ type: "tool",
3700
+ phase: "started",
3701
+ tool: request.tool,
3702
+ operationId,
3703
+ ...startedPresentation ? { presentation: startedPresentation } : {}
3704
+ },
3431
3705
  active.conversationRefs
3432
3706
  ).catch(() => void 0);
3433
3707
  const response2 = await broker.execute(context, request);
3708
+ const completedPresentation = codeToolResultPresentation(request, response2);
3434
3709
  await this.#event(command, {
3435
3710
  type: "tool",
3436
3711
  phase: "completed",
3437
3712
  tool: request.tool,
3438
3713
  ok: response2.ok,
3439
- durationMs: Date.now() - startedAt
3714
+ durationMs: Date.now() - startedAt,
3715
+ operationId,
3716
+ ...completedPresentation ? { presentation: completedPresentation } : {}
3440
3717
  }, active.conversationRefs).catch(() => void 0);
3441
3718
  return response2;
3442
3719
  }
@@ -3681,9 +3958,9 @@ var MEASURED_PREMIUM = Object.freeze({
3681
3958
  decomposePerSubGoal: 0.23
3682
3959
  });
3683
3960
  var actionable = (feedback) => {
3684
- const text = feedback.trim();
3685
- if (text.length < 12) return false;
3686
- return /\b(expected|assert|error|fail(?:ed|ure)?|exit|line \d+|\.[a-z]{1,4}:\d+)\b/i.test(text) || /\.(js|ts|tsx|jsx|mjs|cjs|py|go|rs|java|rb)\b/i.test(text);
3961
+ const text2 = feedback.trim();
3962
+ if (text2.length < 12) return false;
3963
+ return /\b(expected|assert|error|fail(?:ed|ure)?|exit|line \d+|\.[a-z]{1,4}:\d+)\b/i.test(text2) || /\.(js|ts|tsx|jsx|mjs|cjs|py|go|rs|java|rb)\b/i.test(text2);
3687
3964
  };
3688
3965
  function chooseStrategy(signals = {}) {
3689
3966
  const width = Math.max(1, signals.width ?? 3);