@odla-ai/harness 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,14 +2,14 @@
2
2
 
3
3
  @odla-ai/harness is the experimental coding-task boundary shared by odla
4
4
  Studio, a trusted Mac/Linux runner, and an untrusted coding-agent container.
5
- It owns the protocol and safe runner defaults; agent implementations such as Pi
5
+ It owns the protocol and safe runner defaults; agent implementations such as Theseus
6
6
  remain replaceable adapters.
7
7
 
8
8
  Harness can stage a bounded, secret-filtered copy of a local checkout. The
9
- brokered Pi image receives no workspace mount at all: reads and mutations cross
9
+ brokered Theseus agent receives no workspace mount at all: reads and mutations cross
10
10
  the typed tool channel into that disposable snapshot. The original checkout,
11
11
  terminal credential, provider credentials, container-engine socket, and
12
- network are never available to Pi. Inference requests cross the same JSONL
12
+ network are never available to Theseus. Inference requests cross the same JSONL
13
13
  channel and are fulfilled by the control plane through @odla-ai/ai.
14
14
 
15
15
  > **Ask the runbooks first.** odla's operational procedures live in a database,
@@ -32,7 +32,7 @@ npx @odla-ai/cli code connect \
32
32
  --env dev
33
33
  ~~~
34
34
 
35
- The terminal verifies the checkout and isolation engine, builds the Pi adapter
35
+ The terminal verifies the checkout and isolation engine, builds the Theseus adapter
36
36
  bundled with the exact CLI release into a content-addressed local image, prepares
37
37
  each digest-pinned recipe image, and sends the connection request outbound to odla. The signed-in app
38
38
  owner approves it; no bearer is copied from the browser. The process stays in
@@ -49,20 +49,20 @@ otherwise Harness receives the immutable sources from the Registry. References
49
49
  are materialized under `.odla-references/<alias>` and the broker rejects every
50
50
  attempt to patch them. A checkpoint patch is computed against the selected
51
51
  primary Git base, so local developer changes remain explicit provenance
52
- alongside Pi's changes.
52
+ alongside Theseus's changes.
53
53
 
54
54
  On Apple Silicon with macOS 26, install Apple's lightweight runtime once with
55
55
  `brew install container`; `code connect` selects it and starts its user service.
56
56
  On Linux, auto requires rootless Podman. Podman Machine is the macOS fallback;
57
57
  Docker is an explicit compatibility adapter only. Recipe images must be
58
- digest-pinned; the Pi adapter's local tag contains the SHA-256 of its CLI bundle
59
- and requires no registry credential. All adapters run Pi networkless and
60
- non-root with bounded CPU/memory; Pi has no
58
+ digest-pinned; the Theseus adapter's local tag contains the SHA-256 of its CLI bundle
59
+ and requires no registry credential. All adapters run Theseus networkless and
60
+ non-root with bounded CPU/memory; Theseus has no
61
61
  checkout or engine-socket mount.
62
62
 
63
63
  ## Experimental task runner
64
64
 
65
- The older generic task queue is separate from the personal Pi terminal flow.
65
+ The older generic task queue is separate from the personal Theseus terminal flow.
66
66
  It lives under **Code → Task runners · advanced**. To exercise it, register a
67
67
  workspace and keep its runner token outside shell history:
68
68
 
@@ -93,7 +93,7 @@ new copy.
93
93
  ## Build recipes
94
94
 
95
95
  The Code terminal advertises a closed recipe catalog. The content-addressed,
96
- CLI-embedded Pi image and every registry-digest-pinned recipe are prepared
96
+ CLI-embedded Theseus adapter and every registry-digest-pinned recipe are prepared
97
97
  before approval:
98
98
 
99
99
  ~~~json
@@ -111,7 +111,7 @@ before approval:
111
111
 
112
112
  The runtime validates HTTPS, never places its credential in a request body,
113
113
  never opens a listener, and rejects malformed or cross-host binding responses.
114
- Pi reads, patches, and runs only registered build recipes through the typed
114
+ Theseus reads, patches, and runs only registered build recipes through the typed
115
115
  CaMeL broker. Checkpoint-stop captures a bounded combined patch; resume verifies
116
116
  it against the same selected Git base and exact local source descriptor.
117
117
 
@@ -128,7 +128,7 @@ attempt.cancel, and attempt.complete.
128
128
 
129
129
  ## CaMeL tool and build boundary
130
130
 
131
- `createCodeToolBroker` is the trusted implementation for the three Pi tools:
131
+ `createCodeToolBroker` is the trusted implementation for the three Theseus tools:
132
132
  registered staged-file reads, confined unified patches, and app-registered
133
133
  build recipes. Each request is labelled and evaluated by @odla-ai/camel before
134
134
  an effect. Patch data remains Prompt-Unsafe payload; file and recipe selectors
@@ -882,7 +882,7 @@ var CodeRuntimeCheckpointManager = class {
882
882
  await this.options.event(command, { type: "message", actor: "system", body: prepared.note }, active.conversationRefs).catch(() => void 0);
883
883
  await active.workspace.cleanup();
884
884
  await this.options.event(command, { type: "status", status: "checkpointed" }, active.conversationRefs).catch(() => void 0);
885
- return { status: "checkpointed", checkpoint: prepared.checkpoint, message: "Pi stopped at a portable checkpoint" };
885
+ return { status: "checkpointed", checkpoint: prepared.checkpoint, message: "Theseus stopped at a portable checkpoint" };
886
886
  }
887
887
  async acknowledged(command, result) {
888
888
  if (command.kind !== "checkpoint_stop" || result.status !== "checkpointed") return false;
@@ -1118,7 +1118,7 @@ async function materializeCommandWorkspace(input) {
1118
1118
  }
1119
1119
 
1120
1120
  // src/code-agent-skill.ts
1121
- var V1_SYSTEM_PROMPT = `You are Pi, the coding agent inside an odla Code harness.
1121
+ var V1_SYSTEM_PROMPT = `You are Theseus, the coding agent inside an odla Code harness.
1122
1122
  Use only the odla_read, odla_apply_git_diff, and odla_run_recipe tools.
1123
1123
  For mutations, call odla_apply_git_diff with raw git diff text. It must start
1124
1124
  with "diff --git a/<path> b/<path>", include matching "---" and "+++" file
@@ -1128,7 +1128,10 @@ Never claim a build or test passed unless odla_run_recipe returned that result.`
1128
1128
  var V2_SYSTEM_PROMPT = `You are the coding agent inside an odla Code harness.
1129
1129
  Start by orienting: odla_list shows the files in the workspace and odla_search
1130
1130
  finds a literal string across them. Prefer those over guessing a path.
1131
- Then odla_read a bounded range, and odla_apply_git_diff to mutate.
1131
+ Then odla_read a bounded range, and odla_apply_git_diff to mutate. When you
1132
+ need several independent searches or file ranges, issue those read-only calls
1133
+ together in one turn; their results stay ordered and the harness overlaps them.
1134
+ Never issue odla_apply_git_diff or odla_run_recipe alongside another tool call.
1132
1135
  For mutations, call odla_apply_git_diff with raw git diff text. It must start
1133
1136
  with "diff --git a/<path> b/<path>", include matching "---" and "+++" file
1134
1137
  headers and numbered "@@" hunks, and never use "*** Begin Patch" wrappers.
@@ -1159,18 +1162,26 @@ var SYSTEM_PROMPT_FOR = {
1159
1162
  };
1160
1163
  function codeSkill(opts) {
1161
1164
  let seq = 0;
1165
+ let nextCompletion = 1;
1166
+ const completed = /* @__PURE__ */ new Map();
1162
1167
  const call = async (tool, input, signal) => {
1168
+ const sequence = ++seq;
1163
1169
  const startedAt = Date.now();
1164
1170
  const response2 = await opts.broker.execute(
1165
1171
  { lease: opts.lease, workspaceDir: opts.workspaceDir, signal },
1166
- { requestId: `bench-${tool}-${++seq}`, tool, input }
1172
+ { requestId: `bench-${tool}-${sequence}`, tool, input }
1167
1173
  );
1168
- opts.onToolCall?.({
1174
+ completed.set(sequence, {
1169
1175
  tool,
1170
1176
  ok: response2.ok,
1171
1177
  durationMs: Date.now() - startedAt,
1172
1178
  ...response2.ok ? {} : { error: String(response2.content).slice(0, 300) }
1173
1179
  });
1180
+ while (completed.has(nextCompletion)) {
1181
+ const completion = completed.get(nextCompletion);
1182
+ completed.delete(nextCompletion++);
1183
+ opts.onToolCall?.(completion);
1184
+ }
1174
1185
  return { content: response2.content, isError: !response2.ok };
1175
1186
  };
1176
1187
  const read2 = {
@@ -1186,6 +1197,7 @@ function codeSkill(opts) {
1186
1197
  },
1187
1198
  additionalProperties: false
1188
1199
  },
1200
+ concurrency: "parallel",
1189
1201
  handler: (input, ctx) => call("sandbox.read", input, ctx.signal)
1190
1202
  };
1191
1203
  const applyPatch = {
@@ -1201,11 +1213,17 @@ function codeSkill(opts) {
1201
1213
  };
1202
1214
  const runRecipe = {
1203
1215
  name: "odla_run_recipe",
1204
- description: "Run one app-registered build or test recipe through CaMeL policy.",
1216
+ description: `Run one app-registered build or test recipe through CaMeL policy.${opts.recipeIds?.length ? ` Available recipes: ${opts.recipeIds.join(", ")}.` : ""}`,
1205
1217
  inputSchema: {
1206
1218
  type: "object",
1207
1219
  required: ["recipeId"],
1208
- properties: { recipeId: { type: "string", minLength: 1, maxLength: 120, pattern: "^[a-zA-Z0-9._:-]+$" } },
1220
+ properties: { recipeId: {
1221
+ type: "string",
1222
+ minLength: 1,
1223
+ maxLength: 120,
1224
+ pattern: "^[a-zA-Z0-9._:-]+$",
1225
+ ...opts.recipeIds?.length ? { enum: [...opts.recipeIds] } : {}
1226
+ } },
1209
1227
  additionalProperties: false
1210
1228
  },
1211
1229
  handler: (input, ctx) => call("sandbox.run_recipe", input, ctx.signal)
@@ -1221,6 +1239,7 @@ function codeSkill(opts) {
1221
1239
  },
1222
1240
  additionalProperties: false
1223
1241
  },
1242
+ concurrency: "parallel",
1224
1243
  handler: (input, ctx) => call("sandbox.list", input, ctx.signal)
1225
1244
  };
1226
1245
  const searchFiles = {
@@ -1237,11 +1256,13 @@ function codeSkill(opts) {
1237
1256
  },
1238
1257
  additionalProperties: false
1239
1258
  },
1259
+ concurrency: "parallel",
1240
1260
  handler: (input, ctx) => call("sandbox.search", input, ctx.signal)
1241
1261
  };
1242
1262
  const graphTool = (name, tool, description, required) => ({
1243
1263
  name,
1244
1264
  description,
1265
+ concurrency: "parallel",
1245
1266
  inputSchema: {
1246
1267
  type: "object",
1247
1268
  ...required ? { required: ["query"] } : {},
@@ -1293,6 +1314,7 @@ async function runCodeAgent(options) {
1293
1314
  lease: options.lease,
1294
1315
  workspaceDir: options.workspaceDir,
1295
1316
  surface,
1317
+ ...options.recipeIds ? { recipeIds: options.recipeIds } : {},
1296
1318
  onToolCall: (call) => {
1297
1319
  toolCalls.push(call);
1298
1320
  options.onToolCall?.(call);
@@ -1324,7 +1346,7 @@ async function runCodeAgent(options) {
1324
1346
  import { extractText } from "@odla-ai/ai";
1325
1347
  async function runCodeAgentAttempt(options) {
1326
1348
  try {
1327
- const surface = options.surface ?? "v2";
1349
+ const surface = options.surface ?? "v3";
1328
1350
  const { run } = await runCodeAgent({
1329
1351
  inference: options.inference,
1330
1352
  broker: options.broker,
@@ -1335,6 +1357,7 @@ async function runCodeAgentAttempt(options) {
1335
1357
  // id only labels the request the control plane is about to rewrite.
1336
1358
  model: "brokered",
1337
1359
  surface,
1360
+ ...options.recipeIds ? { recipeIds: options.recipeIds } : {},
1338
1361
  ...options.maxSteps === void 0 ? {} : { maxSteps: options.maxSteps },
1339
1362
  ...options.budget ? { budget: options.budget } : {},
1340
1363
  ...options.signal ? { signal: options.signal } : {},
@@ -1429,11 +1452,30 @@ function createCodeRuntimeInference(options) {
1429
1452
  }
1430
1453
 
1431
1454
  // src/code-tool-discovery.ts
1455
+ import { spawn as spawn3 } from "child_process";
1432
1456
  import { readFile as readFile2, readdir as readdir2 } from "fs/promises";
1433
1457
  import { relative as relative2, resolve as resolve4 } from "path";
1434
1458
  var DEFAULT_MAX_FILES = 2e4;
1435
1459
  var DEFAULT_MAX_RESULTS = 100;
1436
1460
  var DEFAULT_MAX_FILE_BYTES = 512 * 1024;
1461
+ function createWorkspaceFileRegistry(limit = DEFAULT_MAX_FILES, enumerate = registeredFiles) {
1462
+ const cache2 = /* @__PURE__ */ new Map();
1463
+ return {
1464
+ files(root) {
1465
+ const existing = cache2.get(root);
1466
+ if (existing) return existing;
1467
+ const pending = enumerate(root, limit).then((paths) => Object.freeze(paths));
1468
+ cache2.set(root, pending);
1469
+ void pending.catch(() => {
1470
+ if (cache2.get(root) === pending) cache2.delete(root);
1471
+ });
1472
+ return pending;
1473
+ },
1474
+ invalidate(root) {
1475
+ cache2.delete(root);
1476
+ }
1477
+ };
1478
+ }
1437
1479
  async function registeredFiles(root, limit = DEFAULT_MAX_FILES) {
1438
1480
  const paths = [];
1439
1481
  const walk = async (directory) => {
@@ -1464,28 +1506,122 @@ function listWorkspace(paths, options = {}) {
1464
1506
  return scoped.slice(0, max);
1465
1507
  }
1466
1508
  async function searchWorkspace(root, paths, options) {
1467
- const query = options.caseSensitive === false ? options.query.toLowerCase() : options.query;
1468
- if (!query) throw new TypeError("search query must be a non-empty string");
1509
+ options.signal?.throwIfAborted();
1510
+ if (!options.query) throw new TypeError("search query must be a non-empty string");
1469
1511
  const maxResults = options.maxResults ?? DEFAULT_MAX_RESULTS;
1470
1512
  const maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
1471
1513
  const scoped = listWorkspace(paths, { ...options.prefix ? { prefix: options.prefix } : {}, maxEntries: paths.length });
1514
+ if (scoped.length === 0) return [];
1515
+ try {
1516
+ return await nativeSearch(root, scoped, { ...options, maxResults, maxFileBytes });
1517
+ } catch (error) {
1518
+ options.signal?.throwIfAborted();
1519
+ return fallbackSearch(root, scoped, { ...options, maxResults, maxFileBytes });
1520
+ }
1521
+ }
1522
+ var MAX_NATIVE_ARG_BYTES = 96 * 1024;
1523
+ async function nativeSearch(root, paths, options) {
1524
+ const batches = [];
1525
+ let batch = [];
1526
+ let bytes = 0;
1527
+ for (const path of paths) {
1528
+ const size = Buffer.byteLength(path) + 1;
1529
+ if (batch.length > 0 && bytes + size > MAX_NATIVE_ARG_BYTES) {
1530
+ batches.push(batch);
1531
+ batch = [];
1532
+ bytes = 0;
1533
+ }
1534
+ batch.push(path);
1535
+ bytes += size;
1536
+ }
1537
+ if (batch.length > 0) batches.push(batch);
1538
+ const matches = [];
1539
+ for (const files of batches) {
1540
+ const remaining = options.maxResults - matches.length;
1541
+ if (remaining <= 0) break;
1542
+ matches.push(...await nativeSearchBatch(root, files, options, remaining));
1543
+ }
1544
+ return matches;
1545
+ }
1546
+ function nativeSearchBatch(root, paths, options, remaining) {
1547
+ return new Promise((resolveMatches, reject) => {
1548
+ const args = [
1549
+ "--fixed-strings",
1550
+ "--json",
1551
+ "--no-messages",
1552
+ "--sort=path",
1553
+ `--max-filesize=${options.maxFileBytes}`,
1554
+ "--max-columns=4096",
1555
+ "--max-columns-preview",
1556
+ options.caseSensitive === false ? "--ignore-case" : "--case-sensitive",
1557
+ "--",
1558
+ options.query,
1559
+ ...paths
1560
+ ];
1561
+ const child = spawn3("rg", args, {
1562
+ cwd: root,
1563
+ stdio: ["ignore", "pipe", "ignore"],
1564
+ ...options.signal ? { signal: options.signal } : {}
1565
+ });
1566
+ const matches = [];
1567
+ let carry = "";
1568
+ let stopped = false;
1569
+ const consume = (line) => {
1570
+ if (matches.length >= remaining) return;
1571
+ let event;
1572
+ try {
1573
+ event = JSON.parse(line);
1574
+ } catch {
1575
+ return;
1576
+ }
1577
+ const path = event.data?.path?.text;
1578
+ const lineNumber = event.data?.line_number;
1579
+ const source = event.data?.lines?.text;
1580
+ if (event.type !== "match" || path === void 0 || lineNumber === void 0 || source === void 0) return;
1581
+ matches.push({ path, line: lineNumber, text: source.trim().slice(0, 240) });
1582
+ if (matches.length >= remaining) {
1583
+ stopped = true;
1584
+ child.kill();
1585
+ }
1586
+ };
1587
+ child.stdout.setEncoding("utf8");
1588
+ child.stdout.on("data", (chunk) => {
1589
+ carry += chunk;
1590
+ let newline = carry.indexOf("\n");
1591
+ while (newline >= 0) {
1592
+ consume(carry.slice(0, newline));
1593
+ carry = carry.slice(newline + 1);
1594
+ newline = carry.indexOf("\n");
1595
+ }
1596
+ });
1597
+ child.once("error", reject);
1598
+ child.once("close", (code) => {
1599
+ if (carry) consume(carry);
1600
+ if (stopped || code === 0 || code === 1) resolveMatches(matches);
1601
+ else reject(new Error(`native search exited with status ${code ?? "unknown"}`));
1602
+ });
1603
+ });
1604
+ }
1605
+ async function fallbackSearch(root, scoped, options) {
1606
+ const query = options.caseSensitive === false ? options.query.toLowerCase() : options.query;
1472
1607
  const matches = [];
1473
1608
  for (const path of scoped) {
1474
- if (matches.length >= maxResults) break;
1609
+ options.signal?.throwIfAborted();
1610
+ if (matches.length >= options.maxResults) break;
1475
1611
  let source;
1476
1612
  try {
1477
1613
  source = await readFile2(resolve4(root, path));
1478
1614
  } catch {
1479
1615
  continue;
1480
1616
  }
1481
- if (source.byteLength > maxFileBytes || source.includes(0)) continue;
1617
+ if (source.byteLength > options.maxFileBytes || source.includes(0)) continue;
1482
1618
  const lines = source.toString("utf8").split("\n");
1483
1619
  for (let index = 0; index < lines.length; index += 1) {
1484
1620
  const raw = lines[index];
1485
1621
  const haystack = options.caseSensitive === false ? raw.toLowerCase() : raw;
1486
1622
  if (!haystack.includes(query)) continue;
1487
1623
  matches.push({ path, line: index + 1, text: raw.trim().slice(0, 240) });
1488
- if (matches.length >= maxResults) break;
1624
+ if (matches.length >= options.maxResults) break;
1489
1625
  }
1490
1626
  }
1491
1627
  return matches;
@@ -1770,6 +1906,9 @@ function workspaceGraphs(workspaceDir, paths) {
1770
1906
  cache.set(workspaceDir, built);
1771
1907
  return built;
1772
1908
  }
1909
+ function forgetWorkspaceGraphs(workspaceDir) {
1910
+ cache.delete(workspaceDir);
1911
+ }
1773
1912
  var shortId = (id) => id.slice(id.indexOf(":") + 1);
1774
1913
  function renderOverview(graphs, prefix) {
1775
1914
  const rows = rollup(graphs.graph, FILE, prefix === void 0 ? {} : { prefix });
@@ -1816,7 +1955,7 @@ var GRAPH_TOOLS = /* @__PURE__ */ new Set([
1816
1955
  "sandbox.who_imports",
1817
1956
  "sandbox.who_touches"
1818
1957
  ]);
1819
- async function read(context, request, options, policy) {
1958
+ async function read(context, request, options, policy, registry) {
1820
1959
  exactKeys(request.input, ["path", "startLine", "endLine"]);
1821
1960
  const path = stringField(request.input, "path");
1822
1961
  const startLine = optionalInteger(request.input.startLine) ?? 1;
@@ -1824,7 +1963,7 @@ async function read(context, request, options, policy) {
1824
1963
  if (endLine < startLine || endLine - startLine + 1 > (options.maxReadLines ?? 2e3)) {
1825
1964
  throw new TypeError("requested line range exceeds its bound");
1826
1965
  }
1827
- const paths = await registeredFiles(context.workspaceDir, 2e4);
1966
+ const paths = await registry.files(context.workspaceDir);
1828
1967
  if (!paths.includes(path)) {
1829
1968
  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.`);
1830
1969
  }
@@ -1844,13 +1983,13 @@ async function read(context, request, options, policy) {
1844
1983
  }
1845
1984
  return response(request, true, content, { path, startLine, endLine: Math.min(endLine, lines.length) });
1846
1985
  }
1847
- async function list(context, request, options, policy) {
1986
+ async function list(context, request, options, policy, registry) {
1848
1987
  exactKeys(request.input, ["prefix", "maxEntries"]);
1849
1988
  const raw = request.input.prefix;
1850
1989
  const prefix = typeof raw === "string" && raw.length > 0 ? raw : void 0;
1851
1990
  const maxEntries = optionalInteger(request.input.maxEntries) ?? 1e3;
1852
1991
  if (maxEntries > 5e3) throw new TypeError("maxEntries exceeds its bound");
1853
- const paths = await registeredFiles(context.workspaceDir, 2e4);
1992
+ const paths = await registry.files(context.workspaceDir);
1854
1993
  const allowed = await policy.list(policyContext(context, request, options, { paths, ...prefix ? { prefix } : {} }));
1855
1994
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
1856
1995
  const entries = listWorkspace(paths, { ...prefix ? { prefix } : {}, maxEntries });
@@ -1868,7 +2007,7 @@ async function list(context, request, options, policy) {
1868
2007
  { count: entries.length, truncated }
1869
2008
  );
1870
2009
  }
1871
- async function search(context, request, options, policy) {
2010
+ async function search(context, request, options, policy, registry) {
1872
2011
  exactKeys(request.input, ["query", "prefix", "maxResults", "caseSensitive"]);
1873
2012
  const query = stringField(request.input, "query");
1874
2013
  if (query.length > 512) throw new TypeError("search query exceeds its bound");
@@ -1877,21 +2016,22 @@ async function search(context, request, options, policy) {
1877
2016
  const maxResults = optionalInteger(request.input.maxResults) ?? 100;
1878
2017
  if (maxResults > 500) throw new TypeError("maxResults exceeds its bound");
1879
2018
  const caseSensitive = request.input.caseSensitive === void 0 ? true : request.input.caseSensitive === true;
1880
- const paths = await registeredFiles(context.workspaceDir, 2e4);
2019
+ const paths = await registry.files(context.workspaceDir);
1881
2020
  const allowed = await policy.search(policyContext(context, request, options, { paths, query, ...prefix ? { prefix } : {} }));
1882
2021
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
1883
2022
  const matches = await searchWorkspace(context.workspaceDir, paths, {
1884
2023
  query,
1885
2024
  maxResults,
1886
2025
  caseSensitive,
1887
- ...prefix ? { prefix } : {}
2026
+ ...prefix ? { prefix } : {},
2027
+ ...context.signal ? { signal: context.signal } : {}
1888
2028
  });
1889
2029
  if (!matches.length) return response(request, true, `No match for "${query}".`, { count: 0 });
1890
2030
  return response(request, true, matches.map((match) => `${match.path}:${match.line}: ${match.text}`).join("\n"), {
1891
2031
  count: matches.length
1892
2032
  });
1893
2033
  }
1894
- async function graphQuery(context, request, options, policy) {
2034
+ async function graphQuery(context, request, options, policy, registry) {
1895
2035
  exactKeys(request.input, ["query"]);
1896
2036
  const raw = request.input.query;
1897
2037
  const query = typeof raw === "string" ? raw : "";
@@ -1901,7 +2041,7 @@ async function graphQuery(context, request, options, policy) {
1901
2041
  selector: query
1902
2042
  }));
1903
2043
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
1904
- const paths = await registeredFiles(context.workspaceDir, 2e4);
2044
+ const paths = await registry.files(context.workspaceDir);
1905
2045
  const graphs = await workspaceGraphs(context.workspaceDir, paths);
1906
2046
  if (request.tool === "sandbox.overview") {
1907
2047
  return response(request, true, renderOverview(graphs, query || void 0));
@@ -1917,23 +2057,38 @@ function createCodeToolBroker(options) {
1917
2057
  validateOptions(options);
1918
2058
  const recipes = new Map(options.recipes.map((recipe2) => [recipe2.id, recipe2]));
1919
2059
  const policy = createCodePolicyGate(options);
1920
- let tail = Promise.resolve();
2060
+ const registry = createWorkspaceFileRegistry();
2061
+ let barrier = Promise.resolve();
2062
+ const activeReads = /* @__PURE__ */ new Set();
1921
2063
  return {
1922
2064
  execute(context, request) {
1923
- const result = tail.then(() => route(context, request, options, recipes, policy));
1924
- tail = result.then(() => void 0, () => void 0);
2065
+ if (isReadTool(request.tool)) {
2066
+ const result2 = barrier.then(() => route(context, request, options, recipes, policy, registry));
2067
+ const settled = result2.then(() => void 0, () => void 0);
2068
+ activeReads.add(settled);
2069
+ void settled.then(() => {
2070
+ activeReads.delete(settled);
2071
+ });
2072
+ return result2;
2073
+ }
2074
+ const earlierReads = [...activeReads];
2075
+ const result = barrier.then(() => Promise.all(earlierReads)).then(() => route(context, request, options, recipes, policy, registry));
2076
+ barrier = result.then(() => void 0, () => void 0);
1925
2077
  return result;
1926
2078
  }
1927
2079
  };
1928
2080
  }
1929
- async function route(context, request, options, recipes, policy) {
2081
+ function isReadTool(tool) {
2082
+ return tool === "sandbox.read" || tool === "sandbox.list" || tool === "sandbox.search" || GRAPH_TOOLS.has(tool);
2083
+ }
2084
+ async function route(context, request, options, recipes, policy, registry) {
1930
2085
  try {
1931
2086
  if (context.signal?.aborted) throw new TypeError("tool request was cancelled");
1932
- if (request.tool === "sandbox.read") return await read(context, request, options, policy);
1933
- if (request.tool === "sandbox.list") return await list(context, request, options, policy);
1934
- if (request.tool === "sandbox.search") return await search(context, request, options, policy);
1935
- if (GRAPH_TOOLS.has(request.tool)) return await graphQuery(context, request, options, policy);
1936
- if (request.tool === "sandbox.apply_patch") return await patch(context, request, options, policy);
2087
+ if (request.tool === "sandbox.read") return await read(context, request, options, policy, registry);
2088
+ if (request.tool === "sandbox.list") return await list(context, request, options, policy, registry);
2089
+ if (request.tool === "sandbox.search") return await search(context, request, options, policy, registry);
2090
+ if (GRAPH_TOOLS.has(request.tool)) return await graphQuery(context, request, options, policy, registry);
2091
+ if (request.tool === "sandbox.apply_patch") return await patch(context, request, options, policy, registry);
1937
2092
  return await recipe(context, request, options, recipes, policy);
1938
2093
  } catch (reason) {
1939
2094
  return response(request, false, toolFailureMessage(reason));
@@ -1948,7 +2103,7 @@ function toolFailureMessage(reason) {
1948
2103
  if (code === "EACCES" || code === "EPERM") return "that path is not readable through this tool";
1949
2104
  return "tool failed closed";
1950
2105
  }
1951
- async function patch(context, request, options, policy) {
2106
+ async function patch(context, request, options, policy, registry) {
1952
2107
  exactKeys(request.input, ["patch"]);
1953
2108
  const value = stringField(request.input, "patch");
1954
2109
  const paths = validateCodePatch(value, options.maxPatchBytes ?? 256 * 1024);
@@ -1958,6 +2113,8 @@ async function patch(context, request, options, policy) {
1958
2113
  const allowed = await policy.patch(policyContext(context, request, options, { patch: value }));
1959
2114
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
1960
2115
  await applyCodePatch(context.workspaceDir, value, paths);
2116
+ registry.invalidate(context.workspaceDir);
2117
+ forgetWorkspaceGraphs(context.workspaceDir);
1961
2118
  return response(request, true, `Applied patch to ${paths.length} file(s).`, { paths });
1962
2119
  }
1963
2120
  async function recipe(context, request, options, recipes, policy) {
@@ -2349,7 +2506,7 @@ var digestRuntimeValue = (value) => `sha256:${createHash3("sha256").update(value
2349
2506
  var runtimeErrorMessage = (value) => value instanceof Error ? value.message : String(value);
2350
2507
 
2351
2508
  // src/code-runtime-engine.ts
2352
- var CodePiRuntimeEngine = class {
2509
+ var TheseusRuntimeEngine = class {
2353
2510
  constructor(options) {
2354
2511
  this.options = options;
2355
2512
  this.#attempt = options.runAgentAttempt ?? runCodeAgentAttempt;
@@ -2435,7 +2592,7 @@ var CodePiRuntimeEngine = class {
2435
2592
  await this.#failure(command, active, detail);
2436
2593
  return null;
2437
2594
  });
2438
- return { status: "running", message: resume ? "Pi resumed from a portable checkpoint" : "Pi started" };
2595
+ return { status: "running", message: resume ? "Theseus resumed from a portable checkpoint" : "Theseus started" };
2439
2596
  }
2440
2597
  /**
2441
2598
  * Pursue a goal: attempt, judge with the clean verifier, re-prompt from what
@@ -2523,7 +2680,7 @@ var CodePiRuntimeEngine = class {
2523
2680
  await this.#failure(command, active, detail);
2524
2681
  return null;
2525
2682
  });
2526
- return { status: "running", message: "Pi accepted the owner prompt" };
2683
+ return { status: "running", message: "Theseus accepted the owner prompt" };
2527
2684
  }
2528
2685
  async #runAttempt(command, metadata, active) {
2529
2686
  const lease = fakeCodeLease(command, metadata);
@@ -2548,7 +2705,8 @@ var CodePiRuntimeEngine = class {
2548
2705
  lease,
2549
2706
  workspaceDir: active.workspace.workspaceDir,
2550
2707
  prompt: metadata.prompt,
2551
- signal: active.abort.signal
2708
+ signal: active.abort.signal,
2709
+ recipeIds: this.options.recipes.map((recipe2) => recipe2.id)
2552
2710
  });
2553
2711
  const closing = result.finalText.trim();
2554
2712
  const completed = result.status === "completed" && Boolean(closing);
@@ -2612,7 +2770,7 @@ var CodePiRuntimeEngine = class {
2612
2770
  }
2613
2771
  }
2614
2772
  async #diagnostic(command, active, value) {
2615
- const detail = value.trim().slice(0, 2e3) || "Pi runtime failed";
2773
+ const detail = value.trim().slice(0, 2e3) || "Theseus runtime failed";
2616
2774
  this.options.onDiagnostic?.(detail);
2617
2775
  await this.#event(
2618
2776
  command,
@@ -2666,6 +2824,6 @@ export {
2666
2824
  renderMemories,
2667
2825
  hazardFromAttempt,
2668
2826
  runGoal,
2669
- CodePiRuntimeEngine
2827
+ TheseusRuntimeEngine
2670
2828
  };
2671
- //# sourceMappingURL=chunk-ZYNGL5SC.js.map
2829
+ //# sourceMappingURL=chunk-GYWQM76X.js.map