@coreplane/switchboard 1.220.0 → 1.221.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
@@ -408,8 +408,11 @@ var init_store = __esm({
408
408
  const res = await this.fetchImpl(signed);
409
409
  if (res.status === 404) return null;
410
410
  if (!res.ok) throw new Error(`artifact store: HEAD ${key} answered HTTP ${res.status}`);
411
- const size = Number(res.headers.get("content-length"));
412
- if (!Number.isFinite(size) || size < 0) throw new Error(`artifact store: HEAD ${key} answered without a length`);
411
+ const length = res.headers.get("content-length");
412
+ const size = length === null ? Number.NaN : Number(length);
413
+ if (!Number.isFinite(size) || size < 0) {
414
+ throw new Error(`artifact store: HEAD ${key} answered HTTP ${res.status} without a length`);
415
+ }
413
416
  return { size, contentType: res.headers.get("content-type") ?? "application/octet-stream" };
414
417
  }
415
418
  async get(key) {
@@ -4136,6 +4139,7 @@ var init_residentSteps = __esm({
4136
4139
  "ops-dir": "preparing a directory",
4137
4140
  "stage-dir": "preparing a directory",
4138
4141
  stat: "checking a file",
4142
+ "boot-id": "reading the container's identity",
4139
4143
  touch: "touching a marker",
4140
4144
  nproc: "counting CPUs",
4141
4145
  mutex_wait: "waiting for the workspace",
@@ -7993,6 +7997,9 @@ function parseExitPrefix(output) {
7993
7997
  if (!m) return { failed: false, exitCode: 0 };
7994
7998
  return /^\d+$/.test(m[1]) ? { failed: true, exitCode: Number(m[1]) } : { failed: true };
7995
7999
  }
8000
+ function toolTextFailed(output) {
8001
+ return /^\s*error:/i.test(stripAnsi(output));
8002
+ }
7996
8003
  function capClean(clean) {
7997
8004
  const text = clean.trim();
7998
8005
  if (text.length <= TOOL_OUTPUT_CAP) return text;
@@ -9413,7 +9420,7 @@ async function parseResidentBody(res) {
9413
9420
  }
9414
9421
  return sanitizeResidentBody(data);
9415
9422
  }
9416
- var DETACH_TIMEOUT_MS, ResidentNeedsRefError, ResidentOperations, ResidentExecutor;
9423
+ var DETACH_TIMEOUT_MS, ResidentNeedsRefError, ResidentReuseRefusedError, ResidentOperations, ResidentExecutor;
9417
9424
  var init_resident = __esm({
9418
9425
  "../../src/execution/resident.ts"() {
9419
9426
  "use strict";
@@ -9445,6 +9452,16 @@ var init_resident = __esm({
9445
9452
  defaultRef;
9446
9453
  needs = "ref";
9447
9454
  };
9455
+ ResidentReuseRefusedError = class extends Error {
9456
+ constructor(resource, why) {
9457
+ super(`resident attach: ${resource} cannot reuse this thread's worktree (${why})`);
9458
+ this.resource = resource;
9459
+ this.why = why;
9460
+ this.name = "ResidentReuseRefusedError";
9461
+ }
9462
+ resource;
9463
+ why;
9464
+ };
9448
9465
  ResidentOperations = class {
9449
9466
  constructor(opts) {
9450
9467
  this.opts = opts;
@@ -9602,6 +9619,7 @@ var init_resident = __esm({
9602
9619
  if (this.opts.refHint) body.refHint = this.opts.refHint;
9603
9620
  if (this.opts.readonly) body.readonly = true;
9604
9621
  if (this.opts.sha) body.sha = this.opts.sha;
9622
+ if (this.opts.reuse) body.reuse = true;
9605
9623
  const answered = await this.call("/attach", body, timeoutMs, void 0, span);
9606
9624
  const data = answered.data;
9607
9625
  const status3 = answered.status === 200 && typeof data.error === "string" && typeof data.status === "number" ? data.status : answered.status;
@@ -9615,6 +9633,7 @@ var init_resident = __esm({
9615
9633
  sha: data.sha,
9616
9634
  ...typeof data.workspace === "string" && data.workspace ? { workspace: data.workspace } : {},
9617
9635
  ...typeof data.user === "string" && data.user ? { user: data.user } : {},
9636
+ ...typeof data.container === "string" && data.container ? { container: data.container } : {},
9618
9637
  ...trace.length > 0 ? { trace } : {},
9619
9638
  ...typeof data.attachMs === "number" && Number.isFinite(data.attachMs) ? { attachMs: data.attachMs } : {}
9620
9639
  };
@@ -9634,6 +9653,8 @@ var init_resident = __esm({
9634
9653
  const defaultRef = typeof data.defaultRef === "string" && data.defaultRef ? data.defaultRef : void 0;
9635
9654
  return traced(new ResidentNeedsRefError(this.opts.resource, defaultRef));
9636
9655
  }
9656
+ if (status3 === 409 && data.needs === "recreate")
9657
+ return traced(new ResidentReuseRefusedError(this.opts.resource, err2));
9637
9658
  if (status3 === 404) return traced(new Error(`resident attach: ${this.opts.resource} is not onboarded (${err2})`));
9638
9659
  return traced(new Error(`resident attach failed for ${this.opts.resource}: ${err2}`));
9639
9660
  }
@@ -9988,11 +10009,33 @@ var init_githubApp = __esm({
9988
10009
  // ../../src/execution/factory.ts
9989
10010
  import { resolve as resolve7 } from "node:path";
9990
10011
  import { mkdirSync as mkdirSync7 } from "node:fs";
10012
+ function workspaceBindingOf(value) {
10013
+ if (typeof value !== "object" || value === null) return void 0;
10014
+ const v = value;
10015
+ if (typeof v.backend !== "string" || !BACKENDS.includes(v.backend)) return void 0;
10016
+ return {
10017
+ backend: v.backend,
10018
+ ...typeof v.workspace === "string" && v.workspace ? { workspace: v.workspace } : {},
10019
+ ...typeof v.user === "string" && v.user ? { user: v.user } : {},
10020
+ ...typeof v.container === "string" && v.container ? { container: v.container } : {}
10021
+ };
10022
+ }
10023
+ function workspaceBindingFor(selection, machine = "repo-resident") {
10024
+ if (machine === "none" || selection.backend === void 0) return void 0;
10025
+ const b = selection.binding;
10026
+ return {
10027
+ backend: selection.backend,
10028
+ ...b?.workspace !== void 0 ? { workspace: b.workspace } : {},
10029
+ ...b?.user !== void 0 ? { user: b.user } : {},
10030
+ ...b?.container !== void 0 ? { container: b.container } : {}
10031
+ };
10032
+ }
9991
10033
  async function makeExecutor(opts, ctx, span) {
9992
10034
  const machine = ctx.profile.machine;
9993
10035
  if (machine === "none") {
9994
10036
  return { executor: new NullExecutor(ctx.agent.name), backend: "local" };
9995
10037
  }
10038
+ if (ctx.reattach !== void 0) return reattachWorkspace(opts, ctx, ctx.reattach, span);
9996
10039
  if (machine === "blank") {
9997
10040
  return {
9998
10041
  executor: await makePerThreadExecutor(opts, { threadKey: ctx.threadKey, resolveEnvs: async () => ({}) }),
@@ -10050,6 +10093,58 @@ async function makeExecutor(opts, ctx, span) {
10050
10093
  ...failedAttach ? { trace: failedAttach.steps } : {}
10051
10094
  };
10052
10095
  }
10096
+ async function reattachWorkspace(opts, ctx, recorded, span) {
10097
+ const refuse3 = (why) => new WorkspaceReattachRefusedError(recorded, oneLine(why));
10098
+ if (recorded.backend !== "resident") {
10099
+ const input = ctx.profile.machine === "blank" ? { threadKey: ctx.threadKey, resolveEnvs: async () => ({}) } : perThreadCheckout(ctx);
10100
+ return { executor: await makePerThreadExecutor(opts, input), backend: perThreadBackend(opts) };
10101
+ }
10102
+ const resident = opts.execution?.resident;
10103
+ if (!resident) throw refuse3("no resident backend is configured in this process");
10104
+ if (!ctx.repo) throw refuse3("the run's row names no repository to re-attach on");
10105
+ const tokenEnv = resident.tokenEnv ?? "RESIDENT_OPERATOR_TOKEN";
10106
+ const token2 = processSecrets.named(tokenEnv);
10107
+ if (!token2) throw new Error(`execution.resident is configured but ${tokenEnv} is not set`);
10108
+ const resource = repoResourceId(ctx.repo);
10109
+ const probe = await probeResident(resident, token2, resource, span);
10110
+ if (probe.kind === "unreachable") throw refuse3(`resident unreachable (${probe.error})`);
10111
+ if (!isServiceable(probe.state, probe.reason))
10112
+ throw refuse3(`resident ${probe.state}${probe.reason ? ` (${probe.reason})` : ""}`);
10113
+ const nonWarm = probe.state === "warm" ? void 0 : oneLine(`${probe.state}${probe.reason ? ` (${probe.reason})` : ""}`);
10114
+ const executor = new ResidentExecutor({
10115
+ baseUrl: resident.baseUrl,
10116
+ token: token2.reveal(),
10117
+ resource,
10118
+ threadKey: ctx.threadKey,
10119
+ refHint: ctx.ref,
10120
+ readonly: ctx.profile.identity === "read" ? true : void 0,
10121
+ sha: ctx.headSha,
10122
+ reuse: true
10123
+ });
10124
+ let binding;
10125
+ try {
10126
+ binding = await executor.attach(span);
10127
+ } catch (err2) {
10128
+ throw refuse3(err2 instanceof Error ? err2.message : String(err2));
10129
+ }
10130
+ const same = (recordedValue, answered) => recordedValue === void 0 || answered === void 0 || recordedValue === answered;
10131
+ if (!same(recorded.workspace, binding.workspace) || !same(recorded.user, binding.user)) {
10132
+ const name = (workspace, user) => `${workspace ?? "(unnamed)"} as ${user ?? "(unnamed)"}`;
10133
+ throw refuse3(
10134
+ `the resident's worktree for this thread is ${name(binding.workspace, binding.user)}, not the run's recorded ${name(recorded.workspace, recorded.user)}`
10135
+ );
10136
+ }
10137
+ const where = `${resource.replace(/^repo:/, "")} \xB7 ${binding.ref}@${binding.sha.slice(0, 7)}`;
10138
+ return {
10139
+ executor,
10140
+ resident: true,
10141
+ backend: "resident",
10142
+ ...binding.trace ? { trace: binding.trace } : {},
10143
+ ...binding.attachMs !== void 0 ? { attachMs: binding.attachMs } : {},
10144
+ binding,
10145
+ note: nonWarm ? `resident ${nonWarm} \xB7 ${where} \xB7 re-attached to the run's worktree` : `resident \xB7 ${where} \xB7 re-attached to the run's worktree`
10146
+ };
10147
+ }
10053
10148
  async function openResident(opts, nonWarm, span) {
10054
10149
  let executor = new ResidentExecutor(opts);
10055
10150
  let binding;
@@ -10200,7 +10295,7 @@ function perThreadBackend(opts) {
10200
10295
  const type = opts.execution?.type ?? "local";
10201
10296
  return type === "e2b" ? "e2b" : type === "cloudflare" ? "sandbox" : "local";
10202
10297
  }
10203
- var PROBE_OUTAGE_WINDOW_MS, probeOutage, NullExecutor;
10298
+ var BACKENDS, WorkspaceReattachRefusedError, PROBE_OUTAGE_WINDOW_MS, probeOutage, NullExecutor;
10204
10299
  var init_factory = __esm({
10205
10300
  "../../src/execution/factory.ts"() {
10206
10301
  "use strict";
@@ -10215,6 +10310,17 @@ var init_factory = __esm({
10215
10310
  init_residentState();
10216
10311
  init_clock();
10217
10312
  init_secrets2();
10313
+ BACKENDS = ["local", "resident", "sandbox", "e2b"];
10314
+ WorkspaceReattachRefusedError = class extends Error {
10315
+ constructor(recorded, why) {
10316
+ super(`the run's workspace on the ${recorded.backend} backend could not be re-attached: ${why}`);
10317
+ this.recorded = recorded;
10318
+ this.why = why;
10319
+ this.name = "WorkspaceReattachRefusedError";
10320
+ }
10321
+ recorded;
10322
+ why;
10323
+ };
10218
10324
  PROBE_OUTAGE_WINDOW_MS = 3e4;
10219
10325
  NullExecutor = class {
10220
10326
  constructor(agentName) {
@@ -20502,7 +20608,7 @@ function createRunsService(deps) {
20502
20608
  const analyze = deps.analyze ?? ((events, opts) => analyzeRunFriction(events, opts));
20503
20609
  const clock3 = deps.clock ?? systemClock;
20504
20610
  const warn = deps.warn ?? ((m) => console.warn(m));
20505
- const describe11 = (err2) => err2 instanceof Error ? err2.message : String(err2);
20611
+ const describe12 = (err2) => err2 instanceof Error ? err2.message : String(err2);
20506
20612
  let listing;
20507
20613
  const liveRows = () => {
20508
20614
  if (!ledger) return null;
@@ -20522,7 +20628,7 @@ function createRunsService(deps) {
20522
20628
  try {
20523
20629
  rows = await pending2;
20524
20630
  } catch (err2) {
20525
- warn(`[runs] run ledger list failed \u2014 showing this process's runs only: ${describe11(err2)}`);
20631
+ warn(`[runs] run ledger list failed \u2014 showing this process's runs only: ${describe12(err2)}`);
20526
20632
  return [];
20527
20633
  }
20528
20634
  const foreign = rows.filter((row) => !registry.getById(row.runId));
@@ -20532,7 +20638,7 @@ function createRunsService(deps) {
20532
20638
  try {
20533
20639
  events = await ledger.readEvents(row.runId);
20534
20640
  } catch (err2) {
20535
- warn(`[runs] run ledger events read failed for ${row.runId}: ${describe11(err2)}`);
20641
+ warn(`[runs] run ledger events read failed for ${row.runId}: ${describe12(err2)}`);
20536
20642
  }
20537
20643
  return ledgerView(row, events);
20538
20644
  })
@@ -20546,7 +20652,7 @@ function createRunsService(deps) {
20546
20652
  if (!row) return null;
20547
20653
  return { row, events: await ledger.readEvents(id) };
20548
20654
  } catch (err2) {
20549
- warn(`[runs] run ledger read failed for ${id}: ${describe11(err2)}`);
20655
+ warn(`[runs] run ledger read failed for ${id}: ${describe12(err2)}`);
20550
20656
  return null;
20551
20657
  }
20552
20658
  };
@@ -20564,7 +20670,7 @@ function createRunsService(deps) {
20564
20670
  row = await storeSummary(id);
20565
20671
  } catch (err2) {
20566
20672
  warn(
20567
- `[runs] history store read failed for ${id} \u2014 serving the registry row without its record: ${describe11(err2)}`
20673
+ `[runs] history store read failed for ${id} \u2014 serving the registry row without its record: ${describe12(err2)}`
20568
20674
  );
20569
20675
  return {};
20570
20676
  }
@@ -20735,7 +20841,7 @@ function createRunsService(deps) {
20735
20841
  const r = await ledger.requestStop(id, mode);
20736
20842
  if (r.ok) return { ok: true, value: { id, mode, state: "stopping" } };
20737
20843
  } catch (err2) {
20738
- warn(`[runs] run ledger stop failed for ${id}: ${describe11(err2)}`);
20844
+ warn(`[runs] run ledger stop failed for ${id}: ${describe12(err2)}`);
20739
20845
  }
20740
20846
  }
20741
20847
  return await storeSummary(id) ? conflict : notFound;
@@ -25933,7 +26039,11 @@ function fileNameOf2(path) {
25933
26039
  return segments2[segments2.length - 1] ?? path;
25934
26040
  }
25935
26041
  function putCommandFor(path, contentType, url) {
25936
- return `curl -fsS -T ${shellQuote(path)} -H ${shellQuote(`Content-Type: ${contentType}`)} ${shellQuote(url)}`;
26042
+ return `curl -fsS -T ${shellQuote(path)} -H ${shellQuote(`Content-Type: ${contentType}`)} -o /dev/null -w ${shellQuote(PUT_REPORT_FORMAT)} ${shellQuote(url)}`;
26043
+ }
26044
+ function parsePutReport(output) {
26045
+ const m = /(\d{3}) (\d{1,15})\s*$/.exec(output);
26046
+ return m ? { httpCode: Number(m[1]), sentBytes: Number(m[2]) } : null;
25937
26047
  }
25938
26048
  function postCommandFor(path, url) {
25939
26049
  return `curl -fsS --upload-file ${shellQuote(path)} -X POST ${shellQuote(url)}`;
@@ -25968,11 +26078,14 @@ async function attachThroughStore(path, name, lead, artifacts, ctx) {
25968
26078
  if (parseExitPrefix(putOut).failed) {
25969
26079
  return `error: the upload of ${name} to the artifact store failed: ${putOut.trim()}`;
25970
26080
  }
26081
+ const report2 = parsePutReport(putOut);
26082
+ const curlSaid = report2 ? `; curl reported HTTP ${report2.httpCode} after sending ${report2.sentBytes} bytes` : "";
25971
26083
  const head = await artifacts.store.head(key);
25972
- if (!head)
25973
- return `error: the artifact store holds nothing under ${key} after the upload of ${name} \u2014 nothing was posted`;
26084
+ if (!head) {
26085
+ return `error: the artifact store holds nothing under ${key} after the upload of ${name}${curlSaid} \u2014 nothing was posted`;
26086
+ }
25974
26087
  if (head.size !== size) {
25975
- return `error: the artifact store holds ${head.size} bytes for ${name}, not the ${size} measured \u2014 nothing was posted`;
26088
+ return `error: the artifact store holds ${head.size} bytes for ${name}, not the ${size} measured${curlSaid} \u2014 nothing was posted`;
25976
26089
  }
25977
26090
  ctx.publish?.({ type: "artifact", direction: "out", key, name, size, contentType });
25978
26091
  if (!ctx.uploadTicket) {
@@ -26000,7 +26113,7 @@ async function attachThroughStore(path, name, lead, artifacts, ctx) {
26000
26113
  }
26001
26114
  return `attached ${name} (${size} bytes) to the conversation and the run page`;
26002
26115
  }
26003
- var MAX_ARTIFACT_BYTES, TRANSFER_FLOOR_BYTES_PER_MS, TRANSFER_SETUP_MS, STAT_TIMEOUT_MS, describe2, secs, attachFileTool;
26116
+ var MAX_ARTIFACT_BYTES, TRANSFER_FLOOR_BYTES_PER_MS, TRANSFER_SETUP_MS, STAT_TIMEOUT_MS, describe2, secs, PUT_REPORT_FORMAT, attachFileTool;
26004
26117
  var init_attach = __esm({
26005
26118
  "../../src/tools/attach.ts"() {
26006
26119
  "use strict";
@@ -26016,8 +26129,10 @@ var init_attach = __esm({
26016
26129
  STAT_TIMEOUT_MS = 3e4;
26017
26130
  describe2 = (err2) => err2 instanceof Error ? err2.message : String(err2);
26018
26131
  secs = (ms) => Math.max(0, Math.round(ms / 1e3));
26132
+ PUT_REPORT_FORMAT = "\\n%{http_code} %{size_upload}";
26019
26133
  attachFileTool = {
26020
26134
  name: "attach_file",
26135
+ failsInText: true,
26021
26136
  description: `Post a file from the workspace into the conversation so the person sees it inline \u2014 a screenshot (e.g. from \`playwright screenshot\`), a rendered PDF, a recording, a log. Use it whenever you produce an image worth showing: a link to a file is not a picture. Whole files only: up to ${MAX_ARTIFACT_BYTES} bytes (1 GiB) where the artifact store is configured, ${MAX_READ_BYTES} bytes otherwise \u2014 the result says which.`,
26022
26137
  inputSchema: {
26023
26138
  type: "object",
@@ -26557,6 +26672,7 @@ var init_runs2 = __esm({
26557
26672
  CHILDREN_PAGES_MAX = 25;
26558
26673
  spawnRunTool = {
26559
26674
  name: "spawn_run",
26675
+ failsInText: true,
26560
26676
  description: `Start a child run as the person who asked you: an ordinary Switchboard run of the named preset, in a thread of its own in this channel, under their permissions \u2014 what they could start by hand with \`agent:<preset>\`. The child is a reader of this conversation: it starts from this conversation's text so far (every user and assistant turn, never your tool calls or their results) plus \`prompt\`, its one new turn \u2014 so say what it should do. Presets a child can run: ${CHILD_PRESETS().join(", ")}; a preset that writes (${WRITE_PRESETS().join(", ")}) is refused \`spawn_identity\`. \`repo\` (owner/name) for a preset that works in a repository (${REPO_PRESETS().join(", ")}); \`budget\` narrows the child's wall clock in whole minutes (at least 2; it is also capped by what is left of yours). Returns the child's run id, its thread and a link, or a refusal by name: a preset or repository the requester may not use, a boundary the child's profile exceeds, the fan-out cap, or a channel that cannot open a thread. A child cannot spawn children.`,
26561
26677
  inputSchema: {
26562
26678
  type: "object",
@@ -26589,9 +26705,10 @@ var init_runs2 = __esm({
26589
26705
  request.budget = budget;
26590
26706
  }
26591
26707
  const spawn7 = ctx.spawn ?? nullSpawnCapability;
26708
+ const conversation = ctx.conversation ? await ctx.conversation() : void 0;
26592
26709
  const out = await spawn7.spawn(request, {
26593
26710
  remainingMs: ctx.remainingMs?.() ?? Number.POSITIVE_INFINITY,
26594
- ...ctx.conversation ? { conversation: ctx.conversation() } : {}
26711
+ ...conversation ? { conversation } : {}
26595
26712
  });
26596
26713
  if (out.kind === "refused") return `spawn refused (${out.reason}): ${out.message}`;
26597
26714
  return `spawned a ${preset} run: ${out.runId} in thread ${out.threadKey}${out.url ? ` (${out.url})` : ""} \u2014 it runs on its own; await_runs waits for it and returns its reply; get_run_status ${out.runId} answers its progress meanwhile.`;
@@ -26599,6 +26716,7 @@ var init_runs2 = __esm({
26599
26716
  };
26600
26717
  sendToRunTool = {
26601
26718
  name: "send_to_run",
26719
+ failsInText: true,
26602
26720
  description: "Steer one of your live children: `text` reaches it as a follow-up at its next step, the way a reply in its thread would \u2014 sent as the person who asked you, and recorded on the child's run page as coming from this run. Use it when the request changed or a child is heading the wrong way. Refused by name: `not_found` (an unknown id, or a run the requester may not read), `not_child` (a run you did not spawn), `not_live` (it already ended \u2014 its status is given, nothing is sent; get_run_status has its reply), or the child's agent's allowlist. A child that finishes before its next step never reads it.",
26603
26721
  inputSchema: {
26604
26722
  type: "object",
@@ -26638,6 +26756,7 @@ var init_runs2 = __esm({
26638
26756
  AWAIT_IDS_MAX = 50;
26639
26757
  awaitRunsTool = {
26640
26758
  name: "await_runs",
26759
+ failsInText: true,
26641
26760
  description: "Wait for runs \u2014 normally your children \u2014 to end, and get each one's end as data: its terminal status (completed, failed, refused, stopped_soft, stopped_hard, interrupted) with its final reply wrapped as untrusted content; `running` for one still live when the wait was cut; `not_found` for an unknown id or one the requester may not read. The wait ends at the first of: every named run ended; `timeoutMinutes` (optional, whole minutes); the edge of your own budget (a minute before your clock runs out \u2014 write up what came back and name what is still running, which keeps running); a stop; a follow-up landing in this thread (it rides your next turn). `ended` says which. An interrupted child is reported, never restarted. A child is its thread: when a person's reply in a child's thread started a later run there, the child's row follows that run \u2014 `continuedBy` names it, and `status` and `finalReply` are its \u2014 never the child's first reply alone.",
26642
26761
  inputSchema: {
26643
26762
  type: "object",
@@ -26768,6 +26887,252 @@ var init_runs2 = __esm({
26768
26887
  }
26769
26888
  });
26770
26889
 
26890
+ // ../../src/core/dispatch/staging.ts
26891
+ function stagedBasename(index, name) {
26892
+ return `${index}-${safeBasename(name)}`;
26893
+ }
26894
+ function pullCommandFor(url, basename4) {
26895
+ return `mkdir -p ${ATTACHMENTS_DIR} && curl -fsS -o ${shellQuote(`${ATTACHMENTS_DIR}/${basename4}`)} ${shellQuote(url)}`;
26896
+ }
26897
+ function excludeCommand() {
26898
+ const entry = shellQuote(`${ATTACHMENTS_DIR}/`);
26899
+ return `mkdir -p .git/info && { grep -qxF ${entry} .git/info/exclude 2>/dev/null || echo ${entry} >> .git/info/exclude; }`;
26900
+ }
26901
+ function formatSize(bytes) {
26902
+ if (bytes >= 1e9) return `${(bytes / 1e9).toFixed(1)} GB`;
26903
+ if (bytes >= 1e6) return `${Math.round(bytes / 1e6)} MB`;
26904
+ if (bytes >= 1e3) return `${Math.round(bytes / 1e3)} KB`;
26905
+ return `${bytes} B`;
26906
+ }
26907
+ function attachmentsLine(outcomes) {
26908
+ const landed = outcomes.filter((o) => o.error === void 0);
26909
+ const failed = outcomes.filter((o) => o.error !== void 0);
26910
+ const parts = [];
26911
+ if (landed.length > 0) {
26912
+ parts.push(
26913
+ `Attached files are in ./${ATTACHMENTS_DIR}/: ${landed.map(
26914
+ (o) => `${o.basename} (${formatSize(o.file.size)}, ${o.file.type}${o.earlier ? ", from earlier in the thread" : ""})`
26915
+ ).join(", ")}`
26916
+ );
26917
+ }
26918
+ for (const o of failed) parts.push(`${o.file.name} could not be staged: ${o.error}`);
26919
+ return parts.join(". ");
26920
+ }
26921
+ function noWorkspaceLine(files) {
26922
+ return files.map(
26923
+ (f) => `this agent has no workspace for ${f.name} (${formatSize(f.size)}, ${f.type}); ask \`agent:coding\` to work with it`
26924
+ ).join(". ");
26925
+ }
26926
+ function stagingIndex() {
26927
+ let n2 = 0;
26928
+ return () => ++n2;
26929
+ }
26930
+ async function copyStaged(files, deps) {
26931
+ return Promise.all(
26932
+ files.map(async (file) => {
26933
+ const index = deps.nextIndex();
26934
+ const basename4 = stagedBasename(index, file.name);
26935
+ const key = inboundKey(deps.threadKey, file.messageId, index, file.name);
26936
+ try {
26937
+ await deps.store.copyFromUrl({ url: file.url, size: file.size, key });
26938
+ deps.publish?.({
26939
+ type: "artifact",
26940
+ direction: "in",
26941
+ key,
26942
+ name: file.name,
26943
+ size: file.size,
26944
+ contentType: file.type
26945
+ });
26946
+ return { file, basename: basename4, key };
26947
+ } catch (err2) {
26948
+ return { file, basename: basename4, key, error: `the copy into the store failed: ${describe4(err2)}` };
26949
+ }
26950
+ })
26951
+ );
26952
+ }
26953
+ async function pullStaged(outcomes, deps) {
26954
+ const landed = outcomes.filter((o) => o.error === void 0);
26955
+ if (landed.length === 0) return [...outcomes];
26956
+ const timeoutMs = deps.timeoutMs ?? BASH_TIMEOUT_MAX_MS;
26957
+ const opts = { timeoutMs, ...deps.signal ? { signal: deps.signal } : {} };
26958
+ if (deps.resident) await deps.executor.exec(excludeCommand(), opts);
26959
+ const pulled = /* @__PURE__ */ new Map();
26960
+ for (const o of landed) {
26961
+ const url = await deps.store.presignGet(o.key);
26962
+ const out = await deps.executor.exec(pullCommandFor(url, o.basename), opts);
26963
+ pulled.set(o.key, parseExitPrefix(out).failed ? `the pull into the workspace failed: ${out.trim()}` : void 0);
26964
+ }
26965
+ return outcomes.map((o) => {
26966
+ const error = o.error ?? pulled.get(o.key);
26967
+ if (error !== void 0) return { ...o, error };
26968
+ const { error: _dropped, ...landedOutcome } = o;
26969
+ return landedOutcome;
26970
+ });
26971
+ }
26972
+ function stageThreadArtifacts(artifacts, deps) {
26973
+ const outcomes = [];
26974
+ for (const a of artifacts) {
26975
+ const file = { name: a.name, size: a.size, type: a.contentType };
26976
+ if (a.held === void 0) {
26977
+ outcomes.push({
26978
+ file,
26979
+ basename: safeBasename(a.name),
26980
+ key: a.key,
26981
+ earlier: true,
26982
+ error: "the store could not be asked whether it still holds it"
26983
+ });
26984
+ continue;
26985
+ }
26986
+ if (!a.held) {
26987
+ outcomes.push({
26988
+ file,
26989
+ basename: safeBasename(a.name),
26990
+ key: a.key,
26991
+ earlier: true,
26992
+ error: "the store no longer holds it (its retention passed)"
26993
+ });
26994
+ continue;
26995
+ }
26996
+ const basename4 = stagedBasename(deps.nextIndex(), a.name);
26997
+ deps.publish?.({
26998
+ type: "artifact",
26999
+ direction: "in",
27000
+ key: a.key,
27001
+ name: a.name,
27002
+ size: a.size,
27003
+ contentType: a.contentType
27004
+ });
27005
+ outcomes.push({ file, basename: basename4, key: a.key, earlier: true });
27006
+ }
27007
+ return outcomes;
27008
+ }
27009
+ async function stageIntoWorkspace(files, deps) {
27010
+ const outcomes = await pullStaged(await copyStaged(files, deps), deps);
27011
+ return { line: attachmentsLine(outcomes), outcomes };
27012
+ }
27013
+ var ATTACHMENTS_DIR, WorkspaceFiles, describe4;
27014
+ var init_staging = __esm({
27015
+ "../../src/core/dispatch/staging.ts"() {
27016
+ "use strict";
27017
+ init_keys();
27018
+ init_bashTimeout();
27019
+ init_shellQuote();
27020
+ init_runEvents();
27021
+ ATTACHMENTS_DIR = "attachments";
27022
+ WorkspaceFiles = class {
27023
+ paths = /* @__PURE__ */ new Map();
27024
+ /** Every outcome that landed: its key now names a workspace path. */
27025
+ record(outcomes) {
27026
+ for (const o of outcomes) if (o.error === void 0) this.paths.set(o.key, `${ATTACHMENTS_DIR}/${o.basename}`);
27027
+ }
27028
+ pathOf(key) {
27029
+ return this.paths.get(key);
27030
+ }
27031
+ };
27032
+ describe4 = (err2) => err2 instanceof Error ? err2.message : String(err2);
27033
+ }
27034
+ });
27035
+
27036
+ // ../../src/core/dispatch/threadAssets.ts
27037
+ function threadAssetsOf(runsOldestFirst) {
27038
+ const seen = /* @__PURE__ */ new Set();
27039
+ const out = [];
27040
+ for (const run2 of runsOldestFirst) {
27041
+ for (const e of run2.events) {
27042
+ if (e.type !== "artifact" || seen.has(e.key)) continue;
27043
+ seen.add(e.key);
27044
+ out.push({
27045
+ key: e.key,
27046
+ name: e.name,
27047
+ size: e.size,
27048
+ contentType: e.contentType,
27049
+ direction: e.direction,
27050
+ runId: run2.id,
27051
+ ...e.seq !== void 0 ? { seq: e.seq } : {}
27052
+ });
27053
+ }
27054
+ }
27055
+ return out;
27056
+ }
27057
+ async function readThreadAssets(deps, threadKey, warn = (line3) => console.warn(line3)) {
27058
+ const runs = [];
27059
+ let before;
27060
+ try {
27061
+ for (; ; ) {
27062
+ const page2 = await deps.runs.listRuns({
27063
+ status: "all",
27064
+ visibleTo: { kind: "all" },
27065
+ threadKey,
27066
+ limit: THREAD_ASSETS_PAGE,
27067
+ ...before !== void 0 ? { before: before.finishedAt, beforeId: before.id } : {}
27068
+ });
27069
+ runs.push(...page2.runs);
27070
+ if (page2.storeUnavailable) {
27071
+ warn(
27072
+ `[thread] ${threadKey}: listing its runs for their files answered live rows only after ${runs.length} run(s) \u2014 the run store was unavailable`
27073
+ );
27074
+ break;
27075
+ }
27076
+ if (page2.nextBefore === void 0) break;
27077
+ before = page2.nextBefore;
27078
+ }
27079
+ } catch (err2) {
27080
+ warn(
27081
+ `[thread] ${threadKey}: listing its runs for their files stopped after ${runs.length} run(s) \u2014 ${describe5(err2)}`
27082
+ );
27083
+ }
27084
+ const perRun = [];
27085
+ for (const run2 of runs.reverse()) {
27086
+ const events = [];
27087
+ let afterSeq;
27088
+ try {
27089
+ for (; ; ) {
27090
+ const r = await deps.runs.getRunEvents(run2.id, afterSeq === void 0 ? {} : { afterSeq });
27091
+ if (!r.ok) {
27092
+ warn(`[thread] ${run2.id}: reading its files stopped after ${events.length} event(s) \u2014 ${r.error}`);
27093
+ break;
27094
+ }
27095
+ events.push(...r.value.events);
27096
+ if (r.value.nextAfterSeq === void 0) break;
27097
+ afterSeq = r.value.nextAfterSeq;
27098
+ }
27099
+ } catch (err2) {
27100
+ warn(`[thread] ${run2.id}: reading its files failed after ${events.length} event(s) \u2014 ${describe5(err2)}`);
27101
+ }
27102
+ perRun.push({ id: run2.id, events });
27103
+ }
27104
+ return Promise.all(
27105
+ threadAssetsOf(perRun).map(async (asset) => {
27106
+ try {
27107
+ return { ...asset, held: await deps.store.head(asset.key) !== null };
27108
+ } catch (err2) {
27109
+ warn(`[thread] ${asset.key}: the store could not be asked whether it holds it \u2014 ${describe5(err2)}`);
27110
+ return asset;
27111
+ }
27112
+ })
27113
+ );
27114
+ }
27115
+ function describeAsset(asset, path) {
27116
+ const what = `${asset.name} (${formatSize(asset.size)}, ${asset.contentType})`;
27117
+ const origin = asset.direction === "in" ? "received on this thread" : `produced by run ${asset.runId}`;
27118
+ return `${what} \u2014 ${origin}; ${whereIs(asset, path)}`;
27119
+ }
27120
+ function whereIs(asset, path) {
27121
+ if (path !== void 0) return `in this workspace at ./${path}`;
27122
+ if (asset.held === false) return "no longer in the store (its retention passed)";
27123
+ if (asset.held === void 0) return "the store could not be asked whether it still holds it";
27124
+ return asset.direction === "out" ? "on that run's page, not in this workspace" : "in the store, not in this workspace";
27125
+ }
27126
+ var THREAD_ASSETS_PAGE, describe5;
27127
+ var init_threadAssets = __esm({
27128
+ "../../src/core/dispatch/threadAssets.ts"() {
27129
+ "use strict";
27130
+ init_staging();
27131
+ THREAD_ASSETS_PAGE = 50;
27132
+ describe5 = (err2) => err2 instanceof Error ? err2.message : String(err2);
27133
+ }
27134
+ });
27135
+
26771
27136
  // ../../src/core/runLedger/sessionLog.ts
26772
27137
  function sessionKey(threadKey, agent) {
26773
27138
  return `${threadKey}:${agent ?? "-"}`;
@@ -26789,31 +27154,50 @@ var init_sessionLog = __esm({
26789
27154
  });
26790
27155
 
26791
27156
  // ../../src/tools/session.ts
26792
- function sessionCapabilityFor(run2, ledger) {
27157
+ function sessionCapabilityFor(run2, ledger, assets) {
26793
27158
  const session = run2?.session;
26794
27159
  if (!session) return void 0;
26795
27160
  return {
26796
27161
  session,
26797
27162
  search: (query, limit) => ledger.searchSession(session.key, query, limit),
26798
27163
  readTurn: async (idx) => (await ledger.readSession(session.key, idx, idx)).messages[0],
27164
+ readConversation: async () => (await ledger.readSession(session.key, session.seedFrom)).messages,
26799
27165
  readNotepad: () => ledger.readNotepad(session.key),
26800
- writeNotepad: (text) => ledger.writeNotepad(session.key, text)
27166
+ writeNotepad: (text) => ledger.writeNotepad(session.key, text),
27167
+ ...assets ? { assets: assets.read, workspacePathOf: assets.pathOf } : {}
27168
+ };
27169
+ }
27170
+ function assetView(asset, path) {
27171
+ return {
27172
+ name: asset.name,
27173
+ size: asset.size,
27174
+ type: asset.contentType,
27175
+ [asset.direction === "in" ? "received" : "produced"]: `run ${asset.runId}`,
27176
+ key: asset.key,
27177
+ where: whereIs(asset, path)
26801
27178
  };
26802
27179
  }
27180
+ function filesNamedIn(text, catalogue) {
27181
+ return catalogue.filter((a) => [a.name, safeBasename(a.name)].some((n2) => nameAt2(n2).test(text)));
27182
+ }
26803
27183
  async function mayRead(ctx) {
26804
27184
  if (!ctx.runs?.runId) return false;
26805
27185
  const res = await ctx.runs.service.getRun(ctx.runs.runId);
26806
27186
  return res.ok && authorize(ctx.runs.actor, "runs:read", runResource(res.value)).allow;
26807
27187
  }
26808
- var UNAVAILABLE3, DEFAULT_HITS, MAX_HITS, SNIPPET_CHARS, snippetOf, recallTool, notesTool, SESSION_TOOLS;
27188
+ var FILE_BEARING, nameAt2, UNAVAILABLE3, DEFAULT_HITS, MAX_HITS, SNIPPET_CHARS, snippetOf, recallTool, notesTool, SESSION_TOOLS;
26809
27189
  var init_session = __esm({
26810
27190
  "../../src/tools/session.ts"() {
26811
27191
  "use strict";
27192
+ init_keys();
26812
27193
  init_authorize();
27194
+ init_threadAssets();
26813
27195
  init_runEvents();
26814
27196
  init_sessionLog();
26815
27197
  init_runRecord();
26816
27198
  init_runsService();
27199
+ FILE_BEARING = /attachments\/|attach_file|attached files/i;
27200
+ nameAt2 = (name) => new RegExp(`(?<![A-Za-z0-9._])${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![A-Za-z0-9._-])`);
26817
27201
  UNAVAILABLE3 = "the session tools are not available in this context: this run has no session log.";
26818
27202
  DEFAULT_HITS = 5;
26819
27203
  MAX_HITS = 50;
@@ -26824,7 +27208,7 @@ var init_session = __esm({
26824
27208
  };
26825
27209
  recallTool = {
26826
27210
  name: "recall",
26827
- description: "Search this conversation's whole log \u2014 every turn of this thread on this agent, earlier runs' included and everything a compaction removed from your window \u2014 for words, and get the matching turns in relevance order with their turn numbers and a snippet; or pass `turn` to read one turn whole (a tool result, a command, a message). Use it when you need something said or seen earlier that is not in front of you: a failing test's name, a command's output, a decision. `limit` defaults to 5 (at most 50).",
27211
+ description: "Search this conversation's whole log \u2014 every turn of this thread on this agent, earlier runs' included and everything a compaction removed from your window \u2014 for words, and get the matching turns in relevance order with their turn numbers and a snippet; or pass `turn` to read one turn whole (a tool result, a command, a message). Use it when you need something said or seen earlier that is not in front of you: a failing test's name, a command's output, a decision. `limit` defaults to 5 (at most 50). A hit that names a file of this thread carries the file's key and where it is for this run; `assets: true` lists every file the thread received or its runs produced instead of searching.",
26828
27212
  inputSchema: {
26829
27213
  type: "object",
26830
27214
  properties: {
@@ -26833,6 +27217,10 @@ var init_session = __esm({
26833
27217
  turn: {
26834
27218
  type: "integer",
26835
27219
  description: "A turn number from an earlier recall: returns that turn whole instead of searching"
27220
+ },
27221
+ assets: {
27222
+ type: "boolean",
27223
+ description: "List the thread's files \u2014 received on its messages or produced by its runs \u2014 with their keys and where each is for this run, instead of searching"
26836
27224
  }
26837
27225
  }
26838
27226
  },
@@ -26841,13 +27229,30 @@ var init_session = __esm({
26841
27229
  if (!ctx.session) return UNAVAILABLE3;
26842
27230
  const turn = typeof input.turn === "number" ? Math.trunc(input.turn) : void 0;
26843
27231
  const query = typeof input.query === "string" ? input.query.trim() : "";
26844
- if (turn === void 0 && query.length === 0) return "recall needs a query or a turn number.";
26845
- if (!await mayRead(ctx)) return JSON.stringify(turn !== void 0 ? { turn, content: null } : { hits: [] });
27232
+ const listAssets = input.assets === true;
27233
+ if (turn === void 0 && query.length === 0 && !listAssets)
27234
+ return "recall needs a query, a turn number or assets: true.";
27235
+ if (!await mayRead(ctx)) {
27236
+ return JSON.stringify(turn !== void 0 ? { turn, content: null } : listAssets ? { assets: [] } : { hits: [] });
27237
+ }
26846
27238
  if (turn !== void 0) {
26847
27239
  const message3 = turn >= 0 ? await ctx.session.readTurn(turn) : void 0;
26848
27240
  if (!message3) return `no turn ${turn} in this session's log.`;
26849
27241
  return JSON.stringify({ turn, role: message3.role, content: message3.content });
26850
27242
  }
27243
+ if (listAssets) {
27244
+ if (!ctx.session.assets) {
27245
+ return JSON.stringify({
27246
+ assets: [],
27247
+ note: "this deployment has no artifact store: the thread's files are not catalogued"
27248
+ });
27249
+ }
27250
+ const assets = await ctx.session.assets();
27251
+ return JSON.stringify({
27252
+ assets: assets.map((a) => assetView(a, ctx.session.workspacePathOf?.(a.key))),
27253
+ ...assets.length === 0 ? { note: "no run of this thread has received or produced a file" } : {}
27254
+ });
27255
+ }
26851
27256
  const limit = Math.min(
26852
27257
  MAX_HITS,
26853
27258
  Math.max(1, typeof input.limit === "number" ? Math.trunc(input.limit) : DEFAULT_HITS)
@@ -26857,12 +27262,17 @@ var init_session = __esm({
26857
27262
  if (hits.length === 0)
26858
27263
  notes.push("no turn of this session's log matches; your notes (notes {}) may hold what you look for");
26859
27264
  if (ctx.session.session.seedFrom === 0) notes.push("this is the first run of its session: the log begins with it");
27265
+ const catalogue = ctx.session.assets && hits.some((h) => FILE_BEARING.test(h.text)) ? await ctx.session.assets() : [];
26860
27266
  return JSON.stringify({
26861
- hits: hits.map((h) => ({
26862
- turn: h.idx,
26863
- ...h.role !== void 0 ? { role: h.role } : {},
26864
- snippet: snippetOf(h.text)
26865
- })),
27267
+ hits: hits.map((h) => {
27268
+ const files = filesNamedIn(h.text, catalogue);
27269
+ return {
27270
+ turn: h.idx,
27271
+ ...h.role !== void 0 ? { role: h.role } : {},
27272
+ snippet: snippetOf(h.text),
27273
+ ...files.length > 0 ? { files: files.map((a) => assetView(a, ctx.session.workspacePathOf?.(a.key))) } : {}
27274
+ };
27275
+ }),
26866
27276
  ...gaps.length ? {
26867
27277
  gaps: `the log has a gap at ${gaps.map((g) => `turn ${g}`).join(", ")}: a run before it detached from the ledger, so turns it saw after that point are not here`
26868
27278
  } : {},
@@ -27037,6 +27447,7 @@ ${raw.trim()}`;
27037
27447
  };
27038
27448
  submitVerdictTool = {
27039
27449
  name: "submit_verdict",
27450
+ failsInText: true,
27040
27451
  description: "Record your review verdict. REQUIRED before your final message when reviewing a PR: `approve` when there are no blocking issues (nits alone are not blocking), `request_changes` otherwise. Switchboard writes the verdict as the first line of the GitHub comment itself (`LGTM:` only for approve); a review with no submitted verdict is posted as NOT approving. Call it once, after your analysis; a later call replaces the earlier one. `head` is the commit you reviewed \u2014 run `git rev-parse HEAD` in the checkout you read and tested and pass its output; Switchboard posts to the PR only if that commit IS the PR's head, so a review of the wrong branch can never land on a PR. Enumerate EVERY issue you report in `findings` with STABLE ids assigned in order (F1, F2, \u2026) \u2014 a fix round references findings by these ids, so never renumber them. Severity is exactly one of blocking|major|minor|nit; the entry carries the file (plus line when it points at one) and a one-line title, while the full explanation stays in your review text keyed by the same ids. An `approve` carrying a `blocking` finding is downgraded to `request_changes` \u2014 approve only when nothing blocking remains.",
27041
27452
  inputSchema: {
27042
27453
  type: "object",
@@ -27087,6 +27498,7 @@ ${raw.trim()}`;
27087
27498
  };
27088
27499
  submitDispositionsTool = {
27089
27500
  name: "submit_dispositions",
27501
+ failsInText: true,
27090
27502
  description: "Record one disposition per review finding after addressing them: `fixed` (the finding is addressed in your pushed code) or `declined` (deliberately not doing it \u2014 the note says why). `findingId` is the finding's stable id from the review (F1, F2, \u2026) \u2014 use exactly those ids; an id the review never issued answers nothing and is dropped when the plan runner reads your record. Every finding gets exactly one entry, every severity included (nits too). Call it once with the complete set after your last push; a later call replaces the earlier one. The set rides this run's record, where the plan runner reads it for the re-review.",
27091
27503
  inputSchema: {
27092
27504
  type: "object",
@@ -27118,6 +27530,7 @@ ${raw.trim()}`;
27118
27530
  };
27119
27531
  submitPrDescriptionTool = {
27120
27532
  name: "submit_pr_description",
27533
+ failsInText: true,
27121
27534
  description: "Submit the PR description as a typed object. REQUIRED after pushing your branch: Switchboard renders the GitHub PR body from this object at the pushed head and opens (or updates) the pull request itself \u2014 never open a PR yourself. Fields map 1:1 to the rendered sections; `title` becomes the PR's title; tour anchors are (path, from, to) line ranges at your pushed head. Call it after your last push; if you push again afterwards, call it again \u2014 the last valid call wins. An invalid object returns an error naming the field to fix; correct it and resubmit.",
27122
27535
  inputSchema: {
27123
27536
  type: "object",
@@ -27211,6 +27624,7 @@ ${raw.trim()}`;
27211
27624
  });
27212
27625
  submitHandoffTool = {
27213
27626
  name: "submit_handoff",
27627
+ failsInText: true,
27214
27628
  description: "Submit the unit handoff as a typed object \u2014 REQUIRED when your first user turn carries a `## Contract` block: once, after submit_pr_description and before your final message. `deviations`: where you departed from the unit as written (from, to, why); `followUps`: what you found and did not do, and where it belongs (what, where); `unproven`: which of the unit's test scenarios or criteria you could not prove, and why (criterion, why). Switchboard records it on the run and posts it to the unit's board issue, where a person decides each row. Submit empty lists when there is nothing to say \u2014 never skip it. A later call replaces the earlier one; an invalid object returns an error naming the field to fix.",
27215
27629
  inputSchema: {
27216
27630
  type: "object",
@@ -27523,7 +27937,11 @@ function imperativeRule(writers) {
27523
27937
  }
27524
27938
  function attachRule(attachers) {
27525
27939
  const names = attachers.map((a) => `\`${a}\``).join(" or ");
27526
- return `Only ${names} can attach or post a file into the thread (the \`attach_file\` tool). A request that asks for a file, a screenshot, a recording or an attachment to be posted, attached or sent back \u2014 or that names attach_file \u2014 routes there, however read-only the rest of it sounds; a read-only preset can describe a file but never post one.`;
27940
+ return [
27941
+ `Only ${names} can attach or post a file into the thread (the \`attach_file\` tool).`,
27942
+ `A request that names attach_file routes to ${names}, whatever it asks the tool to do \u2014 a probe, a test or a diagnostic of the tool is still a call to it.`,
27943
+ "A request that asks for a file, a screenshot, a recording or an attachment to be posted, attached or sent back routes there too, however read-only the rest of it sounds; a read-only preset can describe a file but never post one."
27944
+ ].join(" ");
27527
27945
  }
27528
27946
  function tidyReason(reason) {
27529
27947
  const line3 = oneLine(redactAndCap(reason, ROUTE_REASON_CAP));
@@ -27994,17 +28412,17 @@ ${stagedLine}` : prompt }];
27994
28412
  try {
27995
28413
  const output = await untilHardStop(tool.run(tu.input ?? {}, ctx));
27996
28414
  const text2 = toolResultText(output);
27997
- const exit = tu.name === "bash" ? parseExitPrefix(text2) : void 0;
28415
+ const exit = tu.name === "bash" ? parseExitPrefix(text2) : { failed: tool.failsInText === true && toolTextFailed(text2) };
27998
28416
  emit({
27999
28417
  type: "tool_result",
28000
28418
  tool: tu.name,
28001
- ok: !exit?.failed,
28419
+ ok: !exit.failed,
28002
28420
  callId: tu.id,
28003
- ...exit?.exitCode !== void 0 ? { exitCode: exit.exitCode } : {},
28421
+ ...exit.exitCode !== void 0 ? { exitCode: exit.exitCode } : {},
28004
28422
  ...prepareToolResult(text2),
28005
28423
  ...spanId
28006
28424
  });
28007
- settle2(!exit?.failed, exit?.exitCode !== void 0 ? { exitCode: exit.exitCode } : {});
28425
+ settle2(!exit.failed, exit.exitCode !== void 0 ? { exitCode: exit.exitCode } : {});
28008
28426
  return { type: "tool_result", toolUseId: tu.id, content: capToolResultContent(output) };
28009
28427
  } catch (err2) {
28010
28428
  if (err2 instanceof HardStopError) throw err2;
@@ -28591,7 +29009,8 @@ async function attachRoundWorkspace(input) {
28591
29009
  profile,
28592
29010
  repo: input.round.repo,
28593
29011
  ref: input.round.ref,
28594
- headSha: input.round.headSha
29012
+ headSha: input.round.headSha,
29013
+ ...input.round.reattach !== void 0 ? { reattach: input.round.reattach } : {}
28595
29014
  },
28596
29015
  input.span
28597
29016
  );
@@ -29218,157 +29637,6 @@ var init_messages = __esm({
29218
29637
  }
29219
29638
  });
29220
29639
 
29221
- // ../../src/core/dispatch/staging.ts
29222
- function stagedBasename(index, name) {
29223
- return `${index}-${safeBasename(name)}`;
29224
- }
29225
- function pullCommandFor(url, basename4) {
29226
- return `mkdir -p ${ATTACHMENTS_DIR} && curl -fsS -o ${shellQuote(`${ATTACHMENTS_DIR}/${basename4}`)} ${shellQuote(url)}`;
29227
- }
29228
- function excludeCommand() {
29229
- const entry = shellQuote(`${ATTACHMENTS_DIR}/`);
29230
- return `mkdir -p .git/info && { grep -qxF ${entry} .git/info/exclude 2>/dev/null || echo ${entry} >> .git/info/exclude; }`;
29231
- }
29232
- function formatSize(bytes) {
29233
- if (bytes >= 1e9) return `${(bytes / 1e9).toFixed(1)} GB`;
29234
- if (bytes >= 1e6) return `${Math.round(bytes / 1e6)} MB`;
29235
- if (bytes >= 1e3) return `${Math.round(bytes / 1e3)} KB`;
29236
- return `${bytes} B`;
29237
- }
29238
- function attachmentsLine(outcomes) {
29239
- const landed = outcomes.filter((o) => o.error === void 0);
29240
- const failed = outcomes.filter((o) => o.error !== void 0);
29241
- const parts = [];
29242
- if (landed.length > 0) {
29243
- parts.push(
29244
- `Attached files are in ./${ATTACHMENTS_DIR}/: ${landed.map(
29245
- (o) => `${o.basename} (${formatSize(o.file.size)}, ${o.file.type}${o.earlier ? ", from earlier in the thread" : ""})`
29246
- ).join(", ")}`
29247
- );
29248
- }
29249
- for (const o of failed) parts.push(`${o.file.name} could not be staged: ${o.error}`);
29250
- return parts.join(". ");
29251
- }
29252
- function noWorkspaceLine(files) {
29253
- return files.map(
29254
- (f) => `this agent has no workspace for ${f.name} (${formatSize(f.size)}, ${f.type}); ask \`agent:coding\` to work with it`
29255
- ).join(". ");
29256
- }
29257
- function stagingIndex() {
29258
- let n2 = 0;
29259
- return () => ++n2;
29260
- }
29261
- async function copyStaged(files, deps) {
29262
- return Promise.all(
29263
- files.map(async (file) => {
29264
- const index = deps.nextIndex();
29265
- const basename4 = stagedBasename(index, file.name);
29266
- const key = inboundKey(deps.threadKey, file.messageId, index, file.name);
29267
- try {
29268
- await deps.store.copyFromUrl({ url: file.url, size: file.size, key });
29269
- deps.publish?.({
29270
- type: "artifact",
29271
- direction: "in",
29272
- key,
29273
- name: file.name,
29274
- size: file.size,
29275
- contentType: file.type
29276
- });
29277
- return { file, basename: basename4, key };
29278
- } catch (err2) {
29279
- return { file, basename: basename4, key, error: `the copy into the store failed: ${describe4(err2)}` };
29280
- }
29281
- })
29282
- );
29283
- }
29284
- async function pullStaged(outcomes, deps) {
29285
- const landed = outcomes.filter((o) => o.error === void 0);
29286
- if (landed.length === 0) return [...outcomes];
29287
- const timeoutMs = deps.timeoutMs ?? BASH_TIMEOUT_MAX_MS;
29288
- const opts = { timeoutMs, ...deps.signal ? { signal: deps.signal } : {} };
29289
- if (deps.resident) await deps.executor.exec(excludeCommand(), opts);
29290
- const pulled = /* @__PURE__ */ new Map();
29291
- for (const o of landed) {
29292
- const url = await deps.store.presignGet(o.key);
29293
- const out = await deps.executor.exec(pullCommandFor(url, o.basename), opts);
29294
- pulled.set(o.key, parseExitPrefix(out).failed ? `the pull into the workspace failed: ${out.trim()}` : void 0);
29295
- }
29296
- return outcomes.map((o) => {
29297
- const error = o.error ?? pulled.get(o.key);
29298
- if (error !== void 0) return { ...o, error };
29299
- const { error: _dropped, ...landedOutcome } = o;
29300
- return landedOutcome;
29301
- });
29302
- }
29303
- function threadInboundArtifacts(runsOldestFirst) {
29304
- const seen = /* @__PURE__ */ new Set();
29305
- const out = [];
29306
- for (const events of runsOldestFirst) {
29307
- for (const e of events) {
29308
- if (e.type !== "artifact" || e.direction !== "in" || seen.has(e.key)) continue;
29309
- seen.add(e.key);
29310
- out.push({ key: e.key, name: e.name, size: e.size, contentType: e.contentType });
29311
- }
29312
- }
29313
- return out;
29314
- }
29315
- async function stageThreadArtifacts(artifacts, deps) {
29316
- const outcomes = [];
29317
- for (const a of artifacts) {
29318
- const file = { name: a.name, size: a.size, type: a.contentType };
29319
- let held;
29320
- try {
29321
- held = await deps.store.head(a.key) !== null;
29322
- } catch (err2) {
29323
- outcomes.push({
29324
- file,
29325
- basename: safeBasename(a.name),
29326
- key: a.key,
29327
- earlier: true,
29328
- error: `the store could not be asked: ${describe4(err2)}`
29329
- });
29330
- continue;
29331
- }
29332
- if (!held) {
29333
- outcomes.push({
29334
- file,
29335
- basename: safeBasename(a.name),
29336
- key: a.key,
29337
- earlier: true,
29338
- error: "the store no longer holds it (its retention passed)"
29339
- });
29340
- continue;
29341
- }
29342
- const basename4 = stagedBasename(deps.nextIndex(), a.name);
29343
- deps.publish?.({
29344
- type: "artifact",
29345
- direction: "in",
29346
- key: a.key,
29347
- name: a.name,
29348
- size: a.size,
29349
- contentType: a.contentType
29350
- });
29351
- outcomes.push({ file, basename: basename4, key: a.key, earlier: true });
29352
- }
29353
- return outcomes;
29354
- }
29355
- async function stageIntoWorkspace(files, deps) {
29356
- const outcomes = await pullStaged(await copyStaged(files, deps), deps);
29357
- return { line: attachmentsLine(outcomes), outcomes };
29358
- }
29359
- var ATTACHMENTS_DIR, describe4;
29360
- var init_staging = __esm({
29361
- "../../src/core/dispatch/staging.ts"() {
29362
- "use strict";
29363
- init_keys();
29364
- init_bashTimeout();
29365
- init_shellQuote();
29366
- init_runEvents();
29367
- ATTACHMENTS_DIR = "attachments";
29368
- describe4 = (err2) => err2 instanceof Error ? err2.message : String(err2);
29369
- }
29370
- });
29371
-
29372
29640
  // ../../src/core/modelProxy/runBearers.ts
29373
29641
  import { createHash as createHash4, randomBytes as randomBytes4, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
29374
29642
  function token(runId3, secret2) {
@@ -30071,7 +30339,7 @@ async function reserveRun(deps, ctx) {
30071
30339
  return void 0;
30072
30340
  }
30073
30341
  async function attachWorkspace(deps, ctx) {
30074
- const { msg, io, refuse: refuse3, card, shell, closeLines, clock: clock3, agent, profile, repoCtx, root } = ctx;
30342
+ const { msg, io, refuse: refuse3, card, shell, closeLines, clock: clock3, agent, profile, repoCtx, root, reattach } = ctx;
30075
30343
  let round2;
30076
30344
  try {
30077
30345
  round2 = await root.span("dispatch.workspace.attach", async (span) => {
@@ -30096,7 +30364,8 @@ async function attachWorkspace(deps, ctx) {
30096
30364
  profile,
30097
30365
  repo: repoCtx.repo,
30098
30366
  ref: repoCtx.ref,
30099
- headSha: repoCtx.headSha
30367
+ headSha: repoCtx.headSha,
30368
+ ...reattach !== void 0 ? { reattach } : {}
30100
30369
  },
30101
30370
  logKey: msg.threadKey,
30102
30371
  span
@@ -30123,6 +30392,7 @@ async function attachWorkspace(deps, ctx) {
30123
30392
  });
30124
30393
  return { kind: "refused", reason: "which_branch" };
30125
30394
  }
30395
+ if (err2 instanceof WorkspaceReattachRefusedError) return { kind: "reattach_refused", why: err2.why };
30126
30396
  throw err2;
30127
30397
  }
30128
30398
  return { kind: "attached", round: round2 };
@@ -30159,6 +30429,10 @@ function sessionNotesBlock(session) {
30159
30429
  parts.push(
30160
30430
  "SUMMARY OF THE EARLIER CONVERSATION (written when the context was compacted; every earlier turn is still reachable with `recall`):\n" + summary
30161
30431
  );
30432
+ if (session.files && session.files.length > 0)
30433
+ parts.push(
30434
+ "FILES OF THIS THREAD (received on its messages or produced by its runs, as their records name them; `recall {assets: true}` lists them with their keys):\n" + session.files.map((line3) => `- ${line3}`).join("\n")
30435
+ );
30162
30436
  return parts.length ? parts.join("\n\n") : void 0;
30163
30437
  }
30164
30438
  async function composePrompt(deps, ctx) {
@@ -30258,6 +30532,7 @@ var init_provision = __esm({
30258
30532
  init_contract3();
30259
30533
  init_registry();
30260
30534
  init_profile();
30535
+ init_factory();
30261
30536
  init_residentTrace();
30262
30537
  init_resident();
30263
30538
  init_memory();
@@ -30311,6 +30586,7 @@ async function claimRun(deps, ctx) {
30311
30586
  } = ctx;
30312
30587
  const { resident, binding } = selection;
30313
30588
  let ledgerRun = ctx.ledgerRun;
30589
+ const workspaceBinding = workspaceBindingFor(selection, profile.machine);
30314
30590
  if (!resume && reserved) {
30315
30591
  const ledger = deps.runLedger;
30316
30592
  const opened = await root.span(
@@ -30344,6 +30620,7 @@ async function claimRun(deps, ctx) {
30344
30620
  ...requestRow !== void 0 ? { request: requestRow } : {}
30345
30621
  },
30346
30622
  card: card.handle ?? null,
30623
+ ...workspaceBinding !== void 0 ? { state: { binding: workspaceBinding } } : {},
30347
30624
  // The row reserved before the attach (item 42), promoted in place;
30348
30625
  // its hooks (a stop, a fence) were wired at the reservation and stay.
30349
30626
  reservation: reserved,
@@ -30400,6 +30677,7 @@ var init_run2 = __esm({
30400
30677
  init_workspace();
30401
30678
  init_web();
30402
30679
  init_githubApi();
30680
+ init_factory();
30403
30681
  init_runRegistry();
30404
30682
  init_secrets2();
30405
30683
  webCapability = () => sharedWeb ??= makeWebCapability(processSecrets);
@@ -30432,6 +30710,17 @@ const URL_ENV = "SWITCHBOARD_HARNESS_URL";
30432
30710
  /** How long the tool_call hook keeps asking an unreachable bot before it blocks. */
30433
30711
  const AUTHORIZE_WAIT_MS = 90_000;
30434
30712
  const AUTHORIZE_RETRY_MS = 2_000;
30713
+ /** How long one request for a relayed tool's result may take before it is asked
30714
+ * again: over the bot's window (30 s, after which it answers that the call is
30715
+ * still running), under this runtime's own five-minute header timeout on a
30716
+ * fetch, so a hung connection costs a minute of the wait, not five. */
30717
+ const TOOL_REQUEST_TIMEOUT_MS = 60_000;
30718
+ /** How long a relayed call keeps asking a bot it cannot reach before it fails; the cadence is the hook's. */
30719
+ const TOOL_WAIT_MS = 90_000;
30720
+ /** A bot that says a call is still running is asked again after the window it
30721
+ * held the request for; one that answered sooner is asked again no sooner
30722
+ * than this, so a misbehaving bot is never asked in a hot loop. */
30723
+ const TOOL_REASK_FLOOR_MS = 1_000;
30435
30724
 
30436
30725
  function settings() {
30437
30726
  const url = process.env[URL_ENV];
@@ -30505,6 +30794,49 @@ async function authorize(event) {
30505
30794
  }
30506
30795
  }
30507
30796
 
30797
+ /** pi's abort signal for the call, with this request's own timeout beside it
30798
+ * where the runtime can combine the two; pi's alone where it cannot. */
30799
+ function requestSignal(signal) {
30800
+ const timeout = typeof AbortSignal.timeout === "function" ? AbortSignal.timeout(TOOL_REQUEST_TIMEOUT_MS) : undefined;
30801
+ if (!timeout) return signal;
30802
+ if (!signal) return timeout;
30803
+ return typeof AbortSignal.any === "function" ? AbortSignal.any([signal, timeout]) : signal;
30804
+ }
30805
+
30806
+ /** A relayed tool's result. The call is posted with its id; a bot that answers
30807
+ * that the call is still running (a wait on other runs can take minutes) is
30808
+ * asked again with the same id until it answers: the bot runs the call once
30809
+ * and every later ask joins it. A bot that cannot be reached or is failing (a
30810
+ * network error, a timed-out request, a 5xx) is asked again every two seconds
30811
+ * until the wait is up, then the call fails naming why; a refusal at the door
30812
+ * (a 4xx) fails it at once; an abort from pi ends the asking. */
30813
+ async function relay(toolCallId, tool, input, signal) {
30814
+ let lastAnswered = Date.now();
30815
+ let lastError = "";
30816
+ for (;;) {
30817
+ if (signal && signal.aborted) throw new Error("switchboard harness: " + tool + " was aborted before the bot answered");
30818
+ const asked = Date.now();
30819
+ let answer;
30820
+ try {
30821
+ answer = await call("POST", "/harness/tool", { toolCallId, tool, input }, requestSignal(signal));
30822
+ } catch (err) {
30823
+ if (signal && signal.aborted) throw err;
30824
+ const status = err instanceof HarnessAnswerError ? err.status : undefined;
30825
+ if (status !== undefined && status >= 400 && status < 500) throw err;
30826
+ lastError = err instanceof Error ? err.message : String(err);
30827
+ if (Date.now() - lastAnswered >= TOOL_WAIT_MS) {
30828
+ throw new Error("switchboard harness: the bot did not answer " + tool + " for 90 s (" + lastError + ")");
30829
+ }
30830
+ await sleep(AUTHORIZE_RETRY_MS);
30831
+ continue;
30832
+ }
30833
+ lastAnswered = Date.now();
30834
+ if (!answer.pending) return answer;
30835
+ const held = Date.now() - asked;
30836
+ if (held < TOOL_REASK_FLOOR_MS) await sleep(TOOL_REASK_FLOOR_MS - held);
30837
+ }
30838
+ }
30839
+
30508
30840
  function relayTool(def) {
30509
30841
  return {
30510
30842
  name: def.name,
@@ -30512,7 +30844,7 @@ function relayTool(def) {
30512
30844
  description: def.description,
30513
30845
  parameters: def.inputSchema,
30514
30846
  async execute(toolCallId, params, signal) {
30515
- const answer = await call("POST", "/harness/tool", { toolCallId, tool: def.name, input: params }, signal);
30847
+ const answer = await relay(toolCallId, def.name, params, signal);
30516
30848
  if (answer.isError) throw new Error(answer.content.map((c) => (c.type === "text" ? c.text : "")).join("\\n"));
30517
30849
  return { content: answer.content, details: {} };
30518
30850
  },
@@ -30711,6 +31043,9 @@ function readLogScript(path, offset, maxBytes) {
30711
31043
  function aliveScript(pid) {
30712
31044
  return `kill -0 ${pid} 2>/dev/null && echo alive || echo dead`;
30713
31045
  }
31046
+ function identityScript() {
31047
+ return "cat /proc/sys/kernel/random/boot_id 2>/dev/null || true";
31048
+ }
30714
31049
  function killScript(pid) {
30715
31050
  return `kill -TERM -- -${pid} 2>/dev/null; kill -TERM ${pid} 2>/dev/null; sleep 1; kill -KILL -- -${pid} 2>/dev/null; kill -KILL ${pid} 2>/dev/null; true`;
30716
31051
  }
@@ -30727,7 +31062,7 @@ function stdoutOf(operation, out) {
30727
31062
  const stdout = cut >= 0 ? out.slice(0, cut) : out;
30728
31063
  return stdout === "(no output)" ? "" : stdout;
30729
31064
  }
30730
- var PiContainerError, WRITE_CHUNK_CHARS, INLINE_LINE_CHARS, LOG_READ_BYTES, MESSAGE_UPDATE_MARK, LOG_FILTER, STDERR_MARK, OP_TIMEOUT_MS, ExecPiContainer;
31065
+ var PiContainerError, WRITE_CHUNK_CHARS, INLINE_LINE_CHARS, LOG_READ_BYTES, MESSAGE_UPDATE_MARK, LOG_FILTER, IDENTITY_WORD, STDERR_MARK, OP_TIMEOUT_MS, ExecPiContainer;
30731
31066
  var init_container = __esm({
30732
31067
  "../../src/core/harness/pi/container.ts"() {
30733
31068
  "use strict";
@@ -30747,6 +31082,7 @@ var init_container = __esm({
30747
31082
  LOG_READ_BYTES = 48 * 1024;
30748
31083
  MESSAGE_UPDATE_MARK = '"type":"message_update"';
30749
31084
  LOG_FILTER = `grep --line-buffered -v ${shellQuote(MESSAGE_UPDATE_MARK)}`;
31085
+ IDENTITY_WORD = /^[A-Za-z0-9-]{1,64}$/;
30750
31086
  STDERR_MARK = "\n--- stderr ---\n";
30751
31087
  OP_TIMEOUT_MS = 6e4;
30752
31088
  ExecPiContainer = class {
@@ -30760,6 +31096,12 @@ var init_container = __esm({
30760
31096
  async makeRoot(runId3) {
30761
31097
  return piRunPaths(runId3);
30762
31098
  }
31099
+ /** The start script never changes directory, so pi runs where the executor
31100
+ * runs every command: the thread's checkout, which the harness names (a
31101
+ * resident's `/exec` runs in the thread's worktree). No command runs here. */
31102
+ cwd(_paths, checkout) {
31103
+ return checkout;
31104
+ }
30763
31105
  async writeFile(path, content) {
30764
31106
  for (const script of writeFileScripts(path, content)) stdoutOf("write", await this.exec(script));
30765
31107
  }
@@ -30785,6 +31127,16 @@ var init_container = __esm({
30785
31127
  async alive(pid) {
30786
31128
  return stdoutOf("alive", await this.exec(aliveScript(pid))).trim() === "alive";
30787
31129
  }
31130
+ /** One word or nothing: an empty answer, a malformed one or a command the
31131
+ * executor could not run is no identity: a judgement never rests on a guess. */
31132
+ async identity() {
31133
+ try {
31134
+ const word = stdoutOf("identity", await this.exec(identityScript())).trim();
31135
+ return IDENTITY_WORD.test(word) ? word : void 0;
31136
+ } catch {
31137
+ return void 0;
31138
+ }
31139
+ }
30788
31140
  async kill(pid) {
30789
31141
  stdoutOf("kill", await this.exec(killScript(pid)));
30790
31142
  }
@@ -30916,6 +31268,14 @@ var init_botHostContainer = __esm({
30916
31268
  throw new PiContainerError("root", message2(err2));
30917
31269
  }
30918
31270
  }
31271
+ /** The run's own root: `start` spawns pi in it, so that is the directory a
31272
+ * session written for this root names, present on this host in this
31273
+ * generation. The checkout the harness names is a container path this host
31274
+ * does not have (a `none` run has no workspace), and the previous
31275
+ * generation's root is gone with its pi: neither is where pi runs. */
31276
+ cwd(paths, _checkout) {
31277
+ return paths.dir;
31278
+ }
30919
31279
  /** Parents at 700, the file at 600, exact bytes, created and never
30920
31280
  * replaced: under a root this container made nothing is there before the
30921
31281
  * harness writes it, so a path already present is a named failure, never
@@ -31036,6 +31396,12 @@ var init_botHostContainer = __esm({
31036
31396
  const child = this.byPid.get(pid);
31037
31397
  return child !== void 0 && !child.exited;
31038
31398
  }
31399
+ /** The bot host names no container: its pi is a child of one bot generation
31400
+ * and never outlives it, so a resume judges it by its pid alone (`alive`
31401
+ * answers no for a pid this container did not start). */
31402
+ async identity() {
31403
+ return void 0;
31404
+ }
31039
31405
  /** The group first (pi leads it), then pi itself: TERM, the grace, KILL;
31040
31406
  * a pid this container never started, or one already ended, is nothing to end. */
31041
31407
  async kill(pid) {
@@ -31289,7 +31655,7 @@ var init_bridge2 = __esm({
31289
31655
  const tool = open2?.tool ?? str5(event.toolName);
31290
31656
  const isError = event.isError === true;
31291
31657
  const text = piResultText(event.result);
31292
- const exit = tool === "bash" ? piBashExit(text, isError) : { failed: isError, ...isError ? {} : {} };
31658
+ const exit = tool === "bash" ? piBashExit(text, isError) : { failed: isError || this.deps.textFailing?.has(tool) === true && toolTextFailed(text) };
31293
31659
  const ok2 = !exit.failed;
31294
31660
  this.emit({
31295
31661
  type: "tool_result",
@@ -31361,6 +31727,11 @@ var init_bridge2 = __esm({
31361
31727
  openSpan(callId) {
31362
31728
  return this.openTools.get(callId)?.span;
31363
31729
  }
31730
+ /** Whether the bridge has read the call's start off the log and not yet its
31731
+ * end: what a relayed request that arrived ahead of the poll waits for. */
31732
+ callOpen(callId) {
31733
+ return this.openTools.has(callId);
31734
+ }
31364
31735
  /** End whatever tool spans a stopped pi left open, so no span outlives the run. */
31365
31736
  closeOpenSpans(reason) {
31366
31737
  for (const [callId, open2] of this.openTools) {
@@ -31682,7 +32053,8 @@ function piHarnessFactsOf(value) {
31682
32053
  logOffset: v.logOffset,
31683
32054
  ...typeof v.sessionFile === "string" ? { sessionFile: v.sessionFile } : {},
31684
32055
  ...typeof v.bearerHash === "string" ? { bearerHash: v.bearerHash } : {},
31685
- ...typeof v.root === "string" ? { root: v.root } : {}
32056
+ ...typeof v.root === "string" ? { root: v.root } : {},
32057
+ ...typeof v.container === "string" ? { container: v.container } : {}
31686
32058
  };
31687
32059
  }
31688
32060
  function relayedTools(tools) {
@@ -31739,12 +32111,36 @@ async function runPiHarness(deps, run2) {
31739
32111
  run2.onProgress?.(summary);
31740
32112
  emit({ type: "run_note", kind, summary, ...mode ? { mode } : {} });
31741
32113
  };
31742
- const bridge = new PiBridge({ emit, onProgress: run2.onProgress, agentSpan, clock: clock3 });
32114
+ const bridge = new PiBridge({
32115
+ emit,
32116
+ onProgress: run2.onProgress,
32117
+ agentSpan,
32118
+ clock: clock3,
32119
+ textFailing: new Set(run2.tools.filter((t) => t.failsInText).map((t) => t.name))
32120
+ });
31743
32121
  let paths;
31744
32122
  const remainingMs = run2.resume?.remainingMs ?? run2.agent.maxMinutes * 6e4;
31745
32123
  const deadline = now() + remainingMs;
31746
32124
  const warnAt = deadline - Math.min(3 * 6e4, run2.agent.maxMinutes * 15e3);
31747
32125
  run2.toolContext.remainingMs = () => deadline - now();
32126
+ if (run2.conversation) {
32127
+ const readConversation = run2.conversation;
32128
+ run2.toolContext.conversation = async () => {
32129
+ try {
32130
+ return await readConversation();
32131
+ } catch (err2) {
32132
+ emit({
32133
+ type: "run_note",
32134
+ kind: "seed",
32135
+ summary: redactAndCap(
32136
+ `the conversation could not be read from the session log for a child's seed (${err2 instanceof Error ? err2.message : String(err2)}); a child spawned now starts from its own thread`,
32137
+ 300
32138
+ )
32139
+ });
32140
+ return void 0;
32141
+ }
32142
+ };
32143
+ }
31748
32144
  bridge.turns = run2.resume?.turn ?? 0;
31749
32145
  const mirror = new PiMirror({
31750
32146
  onStep: run2.onStep,
@@ -31774,7 +32170,11 @@ async function runPiHarness(deps, run2) {
31774
32170
  emit,
31775
32171
  toolSpan: (callId) => bridge.openSpan(callId),
31776
32172
  gateSaw: (callId) => bridge.gateSaw(callId),
31777
- toolsBlocked
32173
+ toolsBlocked,
32174
+ callSeen: async (callId) => {
32175
+ for (let waited = 0; !bridge.callOpen(callId) && waited < CALL_SEEN_WAIT_MS; waited += CALL_SEEN_TICK_MS)
32176
+ await deps.sleep(CALL_SEEN_TICK_MS);
32177
+ }
31778
32178
  };
31779
32179
  const forget = deps.registry.register(live);
31780
32180
  let pid;
@@ -31791,21 +32191,27 @@ async function runPiHarness(deps, run2) {
31791
32191
  const recorded = run2.resume?.facts;
31792
32192
  let reattached = false;
31793
32193
  let ended;
32194
+ let elsewhere;
32195
+ const here = await container.identity();
31794
32196
  const honoured = (hash) => hash !== void 0 && (deps.bearers === void 0 || deps.bearers.adopt(run2.runId, hash));
31795
32197
  if (recorded !== void 0) {
31796
- const alive = await container.alive(recorded.pid);
31797
- if (alive && recorded.root !== void 0 && honoured(recorded.bearerHash)) {
31798
- reattached = true;
31799
- paths = piRunPathsAt(recorded.root);
31800
- } else if (alive) {
31801
- ended = recorded.root === void 0 ? "named no directory for its pi" : "carried no bearer this generation could honour for its pi";
31802
- await container.kill(recorded.pid).catch(() => {
31803
- });
32198
+ if (recorded.container !== void 0 && here !== void 0 && recorded.container !== here) {
32199
+ elsewhere = `pi is elsewhere: the row's pi (pid ${recorded.pid}) ran in container ${recorded.container}, not the one this run was handed (${here}), so it was neither probed nor ended here`;
32200
+ } else {
32201
+ const alive = await container.alive(recorded.pid);
32202
+ if (alive && recorded.root !== void 0 && honoured(recorded.bearerHash)) {
32203
+ reattached = true;
32204
+ paths = piRunPathsAt(recorded.root);
32205
+ } else if (alive) {
32206
+ ended = recorded.root === void 0 ? "named no directory for its pi" : "carried no bearer this generation could honour for its pi";
32207
+ await container.kill(recorded.pid).catch(() => {
32208
+ });
32209
+ }
31804
32210
  }
31805
32211
  }
31806
32212
  if (reattached && recorded && paths !== void 0) {
31807
32213
  pid = recorded.pid;
31808
- facts2 = { ...recorded };
32214
+ facts2 = { ...recorded, ...recorded.container === void 0 && here !== void 0 ? { container: here } : {} };
31809
32215
  transport = new PiRpcTransport({
31810
32216
  container,
31811
32217
  paths,
@@ -31821,7 +32227,7 @@ async function runPiHarness(deps, run2) {
31821
32227
  );
31822
32228
  } else {
31823
32229
  paths = await container.makeRoot(run2.runId);
31824
- if (recorded?.root !== void 0 && recorded.root !== paths.dir)
32230
+ if (recorded?.root !== void 0 && recorded.root !== paths.dir && elsewhere === void 0)
31825
32231
  await container.remove(piRunPathsAt(recorded.root)).catch(() => {
31826
32232
  });
31827
32233
  const spec = {
@@ -31854,7 +32260,7 @@ async function runPiHarness(deps, run2) {
31854
32260
  piSessionFile(
31855
32261
  session.messages,
31856
32262
  {
31857
- cwd: run2.rules.checkout,
32263
+ cwd: container.cwd(paths, run2.rules.checkout),
31858
32264
  model: {
31859
32265
  provider: run2.model.provider,
31860
32266
  id: run2.model.id,
@@ -31868,7 +32274,7 @@ async function runPiHarness(deps, run2) {
31868
32274
  }
31869
32275
  if (run2.resume) {
31870
32276
  const lost = run2.resume.settlements.length;
31871
- const how = ended !== void 0 && recorded !== void 0 ? `the row ${ended} (pid ${recorded.pid}), so it was ended and pi restarted` : "pi restarted";
32277
+ const how = elsewhere !== void 0 ? `${elsewhere}, and pi restarted` : ended !== void 0 && recorded !== void 0 ? `the row ${ended} (pid ${recorded.pid}), so it was ended and pi restarted` : "pi restarted";
31872
32278
  note(
31873
32279
  "resumed",
31874
32280
  `resumed after a restart: ${how} on the mirrored transcript \u2014 ${lost} call(s) were in flight, each answered with a restart note; ${Math.round(remainingMs / 6e4)} min of budget left`
@@ -31878,7 +32284,13 @@ async function runPiHarness(deps, run2) {
31878
32284
  for (const file of piLaunchFiles(launch)) await container.writeFile(file.path, file.content);
31879
32285
  ({ pid } = await container.start({ paths, args: piLaunchArgs(launch), env: piLaunchEnv(launch, deps.bearer) }));
31880
32286
  const bearerHash = bearerHashOf(deps.bearer);
31881
- facts2 = { pid, logOffset: 0, root: paths.dir, ...bearerHash !== void 0 ? { bearerHash } : {} };
32287
+ facts2 = {
32288
+ pid,
32289
+ logOffset: 0,
32290
+ root: paths.dir,
32291
+ ...bearerHash !== void 0 ? { bearerHash } : {},
32292
+ ...here !== void 0 ? { container: here } : {}
32293
+ };
31882
32294
  save();
31883
32295
  transport = new PiRpcTransport({ container, paths, pid, pollMs: deps.pollMs ?? 750, sleep: deps.sleep });
31884
32296
  }
@@ -32073,7 +32485,7 @@ ${stagedLine}` : prompt,
32073
32485
  agentSpan?.end(hardStopped || bypass ? "error" : "ok");
32074
32486
  }
32075
32487
  }
32076
- var NATIVE_WORKSPACE_TOOLS, FINALE_TIMEOUT_MS2, CONTINUE_PROMPT, PromptRefused, GateBypassed;
32488
+ var NATIVE_WORKSPACE_TOOLS, FINALE_TIMEOUT_MS2, CALL_SEEN_WAIT_MS, CALL_SEEN_TICK_MS, CONTINUE_PROMPT, PromptRefused, GateBypassed;
32077
32489
  var init_harness = __esm({
32078
32490
  "../../src/core/harness/pi/harness.ts"() {
32079
32491
  "use strict";
@@ -32088,6 +32500,8 @@ var init_harness = __esm({
32088
32500
  init_transport();
32089
32501
  NATIVE_WORKSPACE_TOOLS = /* @__PURE__ */ new Set(["bash", "read_file", "write_file"]);
32090
32502
  FINALE_TIMEOUT_MS2 = 3 * 6e4;
32503
+ CALL_SEEN_WAIT_MS = 3e3;
32504
+ CALL_SEEN_TICK_MS = 50;
32091
32505
  CONTINUE_PROMPT = "Continue where you left off: the bot restarted mid-run, so re-check the effects of your last command before relying on them.";
32092
32506
  PromptRefused = class extends Error {
32093
32507
  constructor(reason) {
@@ -32942,6 +33356,7 @@ async function runLoop2(deps, ctx) {
32942
33356
  executor,
32943
33357
  resident: round2.selection.resident !== void 0
32944
33358
  });
33359
+ ctx.workspaceFiles?.record(staged.outcomes);
32945
33360
  return staged.line;
32946
33361
  } : void 0;
32947
33362
  let artifactSeq = 0;
@@ -32990,10 +33405,19 @@ async function runLoop2(deps, ctx) {
32990
33405
  });
32991
33406
  if (harness === "pi" && piFacts) {
32992
33407
  const container = deps.harness?.containerFor?.(executor, profile.machine) ?? piContainerFor(executor, profile.machine);
32993
- await container.kill(piFacts.pid).catch(() => {
32994
- });
32995
- if (piFacts.root !== void 0) await container.remove(piRunPathsAt(piFacts.root)).catch(() => {
32996
- });
33408
+ const here = await container.identity();
33409
+ if (piFacts.container === void 0 || here === void 0 || piFacts.container === here) {
33410
+ await container.kill(piFacts.pid).catch(() => {
33411
+ });
33412
+ if (piFacts.root !== void 0) await container.remove(piRunPathsAt(piFacts.root)).catch(() => {
33413
+ });
33414
+ } else {
33415
+ onEvent({
33416
+ type: "run_note",
33417
+ kind: "resumed",
33418
+ summary: `the run's pi (pid ${piFacts.pid}) ran in container ${piFacts.container}, not the one this run was handed (${here}): it was not ended here`
33419
+ });
33420
+ }
32997
33421
  }
32998
33422
  answer = answerUnderEnding(finish2.answer, loopEnding, agent.maxMinutes);
32999
33423
  } else if (harness === "pi") {
@@ -33038,7 +33462,7 @@ async function runLoop2(deps, ctx) {
33038
33462
  messages,
33039
33463
  tools: relayedTools(mergeTools(TOOLSETS[agent.toolset] ?? [], mcpForRun?.tools)),
33040
33464
  toolContext,
33041
- ...session ? { notepad: () => session.readNotepad() } : {},
33465
+ ...session ? { notepad: () => session.readNotepad(), conversation: () => session.readConversation() } : {},
33042
33466
  rules: {
33043
33467
  checkout: binding?.workspace ?? "/workspace",
33044
33468
  ...ownBranch !== void 0 ? { branch: ownBranch } : {},
@@ -33856,7 +34280,7 @@ async function plan(deps, input) {
33856
34280
  try {
33857
34281
  planId = planIdOf(request.planPath);
33858
34282
  } catch (err2) {
33859
- return { ok: false, reply: `\u{1F6AB} ${describe5(err2)}` };
34283
+ return { ok: false, reply: `\u{1F6AB} ${describe6(err2)}` };
33860
34284
  }
33861
34285
  let text;
33862
34286
  try {
@@ -33864,7 +34288,7 @@ async function plan(deps, input) {
33864
34288
  } catch (err2) {
33865
34289
  return {
33866
34290
  ok: false,
33867
- reply: `\u{1F6AB} The plan \`${request.planPath}\` could not be read at \`${base}\` in ${entry.repo}: ${describe5(err2)}`
34291
+ reply: `\u{1F6AB} The plan \`${request.planPath}\` could not be read at \`${base}\` in ${entry.repo}: ${describe6(err2)}`
33868
34292
  };
33869
34293
  }
33870
34294
  const graph = parsePlanGraph(text, planId);
@@ -33877,7 +34301,7 @@ async function plan(deps, input) {
33877
34301
  try {
33878
34302
  selected = openPlanCursor(graph, request.units).order;
33879
34303
  } catch (err2) {
33880
- return { ok: false, reply: `\u{1F6AB} ${describe5(err2)}` };
34304
+ return { ok: false, reply: `\u{1F6AB} ${describe6(err2)}` };
33881
34305
  }
33882
34306
  return { ok: true, planned: { kind: "plan", planId, path: request.planPath, base, graph, selected, identity: identity2 } };
33883
34307
  }
@@ -33993,7 +34417,7 @@ async function start(deps, input, instance, units, where, write) {
33993
34417
  try {
33994
34418
  answer = await deps.create(instance.id);
33995
34419
  } catch (err2) {
33996
- answer = { kind: "unanswered", reason: describe5(err2) };
34420
+ answer = { kind: "unanswered", reason: describe6(err2) };
33997
34421
  }
33998
34422
  switch (answer.kind) {
33999
34423
  case "created": {
@@ -34015,7 +34439,7 @@ async function start(deps, input, instance, units, where, write) {
34015
34439
  );
34016
34440
  }
34017
34441
  }
34018
- var refused3, describe5, RUNNING, ENDED;
34442
+ var refused3, describe6, RUNNING, ENDED;
34019
34443
  var init_handOff = __esm({
34020
34444
  "../../src/core/coordinator/handOff.ts"() {
34021
34445
  "use strict";
@@ -34023,7 +34447,7 @@ var init_handOff = __esm({
34023
34447
  init_coordinator();
34024
34448
  init_briefs();
34025
34449
  refused3 = (reply) => ({ status: "aborted", reply });
34026
- describe5 = (err2) => err2 instanceof Error ? err2.message : String(err2);
34450
+ describe6 = (err2) => err2 instanceof Error ? err2.message : String(err2);
34027
34451
  RUNNING = /* @__PURE__ */ new Set(["queued", "running", "paused", "waiting", "waitingForPause"]);
34028
34452
  ENDED = /* @__PURE__ */ new Set(["complete", "errored", "terminated"]);
34029
34453
  }
@@ -34716,6 +35140,77 @@ var init_settle = __esm({
34716
35140
  }
34717
35141
  });
34718
35142
 
35143
+ // ../../src/core/dispatch/reattach.ts
35144
+ function carriedWorkspaceBinding(row) {
35145
+ const recorded = workspaceBindingOf(row.state.binding);
35146
+ if (recorded) return recorded;
35147
+ if (row.meta.selection === "resident")
35148
+ return { backend: "resident", ...row.meta.workspace ? { workspace: row.meta.workspace } : {} };
35149
+ if (row.meta.selection === "sandbox" || row.meta.selection === "local") return { backend: row.meta.selection };
35150
+ return void 0;
35151
+ }
35152
+ function lostWorkspaceNote(why, restarts) {
35153
+ const lost = `resumed after a restart: the run's workspace could not be re-attached (${why})`;
35154
+ return restarts ? `${lost}; the run restarts from its request as a new run in this thread` : `${lost}, and the row's request cannot be read, so the run ends here; re-send it to run it again`;
35155
+ }
35156
+ async function abandonLostWorkspace(ctx) {
35157
+ const { refuse: refuse3, card, shell, closeLines, clock: clock3, run: run2, registry, resume, ledgerRun, why } = ctx;
35158
+ const restored = messageFromInbox(resume.row.meta.request ?? {}, resume.row.startedAt);
35159
+ const summary = lostWorkspaceNote(why, restored !== void 0);
35160
+ const note = { type: "run_note", kind: "resumed", summary, at: clock3() };
35161
+ await refuse3("workspace_lost", async () => {
35162
+ registry.publish(run2.id, note);
35163
+ await card.done(
35164
+ shell.close({
35165
+ kind: "not_started",
35166
+ icon: "\u{1F501}",
35167
+ reason: restored ? "workspace lost across the restart; restarting from the request" : "workspace lost across the restart; re-send to run again",
35168
+ ...closeLines(clock3(), false)
35169
+ })
35170
+ );
35171
+ if (ledgerRun)
35172
+ await closeResumedRow(
35173
+ ledgerRun,
35174
+ { ...resume, events: [...resume.events, { ...note, seq: resume.lastSeq + 1 }] },
35175
+ "the run's workspace could not be re-attached"
35176
+ );
35177
+ });
35178
+ console.log(
35179
+ `[resume] ${resume.row.threadKey} run ${resume.row.runId} ${restored ? "restarts from its request" : "ends"}: ${why}`
35180
+ );
35181
+ return restored?.msg;
35182
+ }
35183
+ function prepareRestartTurn(deps, ctx) {
35184
+ const { request, pending: pending2, clock: clock3 } = ctx;
35185
+ const merged = mergeFollowUps(pending2);
35186
+ const receivedAt = clock3();
35187
+ const msg = {
35188
+ ...request,
35189
+ ...merged ? {
35190
+ text: `${request.text}
35191
+
35192
+ ${merged.text}`,
35193
+ ...merged.images || request.images ? { images: [...request.images ?? [], ...merged.images ?? []] } : {},
35194
+ ...merged.documents || request.documents ? { documents: [...request.documents ?? [], ...merged.documents ?? []] } : {},
35195
+ ...merged.staged || request.staged ? { staged: [...request.staged ?? [], ...merged.staged ?? []] } : {}
35196
+ } : {},
35197
+ receivedAt,
35198
+ originAt: void 0
35199
+ };
35200
+ const trace = startRequestRoot(deps, { channel: channelOf(msg.channelId), receivedAt });
35201
+ return { msg, opts: { trace } };
35202
+ }
35203
+ var init_reattach = __esm({
35204
+ "../../src/core/dispatch/reattach.ts"() {
35205
+ "use strict";
35206
+ init_factory();
35207
+ init_inboxMessage();
35208
+ init_requestTrace();
35209
+ init_threadAdmission();
35210
+ init_admission();
35211
+ }
35212
+ });
35213
+
34719
35214
  // ../../src/core/dispatch/lineage.ts
34720
35215
  function lineageOf(newest) {
34721
35216
  if (newest === void 0 || newest.parentRunId === void 0) return void 0;
@@ -34775,31 +35270,6 @@ var init_lineage = __esm({
34775
35270
  });
34776
35271
 
34777
35272
  // ../../src/core/dispatch/thread.ts
34778
- async function readThreadArtifacts(service, thread, warn = (line3) => console.warn(line3)) {
34779
- const perRun = [];
34780
- for (const run2 of [...thread].reverse()) {
34781
- const events = [];
34782
- let afterSeq;
34783
- try {
34784
- for (; ; ) {
34785
- const r = await service.getRunEvents(run2.id, afterSeq === void 0 ? {} : { afterSeq });
34786
- if (!r.ok) {
34787
- warn(`[thread] ${run2.id}: reading its received files stopped after ${events.length} event(s) \u2014 ${r.error}`);
34788
- break;
34789
- }
34790
- events.push(...r.value.events);
34791
- if (r.value.nextAfterSeq === void 0) break;
34792
- afterSeq = r.value.nextAfterSeq;
34793
- }
34794
- } catch (err2) {
34795
- warn(
34796
- `[thread] ${run2.id}: reading its received files failed after ${events.length} event(s) \u2014 ${err2 instanceof Error ? err2.message : String(err2)}`
34797
- );
34798
- }
34799
- perRun.push(events);
34800
- }
34801
- return threadInboundArtifacts(perRun);
34802
- }
34803
35273
  async function readThread(service, threadKey) {
34804
35274
  try {
34805
35275
  const page2 = await service.listRuns({
@@ -34841,7 +35311,6 @@ var THREAD_READ_LIMIT;
34841
35311
  var init_thread = __esm({
34842
35312
  "../../src/core/dispatch/thread.ts"() {
34843
35313
  "use strict";
34844
- init_staging();
34845
35314
  THREAD_READ_LIMIT = 8;
34846
35315
  }
34847
35316
  });
@@ -34961,22 +35430,22 @@ function createRunEnding(deps) {
34961
35430
  try {
34962
35431
  deps.registry.seal(id, replyOk === void 0 ? {} : { replyOk });
34963
35432
  } catch (err2) {
34964
- log(`[ending] seal ${id}: ${describe6(err2)}`);
35433
+ log(`[ending] seal ${id}: ${describe7(err2)}`);
34965
35434
  }
34966
35435
  try {
34967
35436
  afterSeal?.();
34968
35437
  } catch (err2) {
34969
- log(`[ending] after seal ${id}: ${describe6(err2)}`);
35438
+ log(`[ending] after seal ${id}: ${describe7(err2)}`);
34970
35439
  }
34971
35440
  }
34972
35441
  for (const entry of pending2.splice(0)) {
34973
35442
  try {
34974
35443
  const result = deps.registry.seal(entry.runId);
34975
35444
  void Promise.resolve(entry.write(result, entry.failedAfterFinish)).catch(
34976
- (err2) => log(`[ending] record ${entry.runId}: ${describe6(err2)}`)
35445
+ (err2) => log(`[ending] record ${entry.runId}: ${describe7(err2)}`)
34977
35446
  );
34978
35447
  } catch (err2) {
34979
- log(`[ending] record ${entry.runId}: ${describe6(err2)}`);
35448
+ log(`[ending] record ${entry.runId}: ${describe7(err2)}`);
34980
35449
  }
34981
35450
  }
34982
35451
  };
@@ -34986,7 +35455,7 @@ function createRunEnding(deps) {
34986
35455
  try {
34987
35456
  deps.onFinished?.(runId3);
34988
35457
  } catch (err2) {
34989
- log(`[ending] finished ${runId3}: ${describe6(err2)}`);
35458
+ log(`[ending] finished ${runId3}: ${describe7(err2)}`);
34990
35459
  }
34991
35460
  },
34992
35461
  register(entry) {
@@ -35014,7 +35483,7 @@ function createRunEnding(deps) {
35014
35483
  }
35015
35484
  };
35016
35485
  }
35017
- function describe6(err2) {
35486
+ function describe7(err2) {
35018
35487
  return err2 instanceof Error ? err2.message : String(err2);
35019
35488
  }
35020
35489
  var init_runEnding = __esm({
@@ -35076,6 +35545,8 @@ async function dispatch(deps, msg, io, opts = {}) {
35076
35545
  let reserved;
35077
35546
  let requestRow;
35078
35547
  let fencedWhileAttaching = false;
35548
+ let resumeRowClosed = false;
35549
+ let restartRequest;
35079
35550
  const reservationHooks = {
35080
35551
  onStop: (mode) => void registered?.control.requestStop(mode),
35081
35552
  onFenced: () => {
@@ -35316,18 +35787,21 @@ async function dispatch(deps, msg, io, opts = {}) {
35316
35787
  await tellLineage({ kind: "started", runId: runId3 });
35317
35788
  const staged = !resume && deps.artifacts && msg.staged && msg.staged.length > 0 ? msg.staged : [];
35318
35789
  const nextStagedIndex = stagingIndex();
35790
+ const workspaceFiles = new WorkspaceFiles();
35319
35791
  const stagedCopies = staged.length > 0 && agent.machine !== "none" ? copyStaged(staged, {
35320
35792
  store: deps.artifacts,
35321
35793
  threadKey: msg.threadKey,
35322
35794
  nextIndex: nextStagedIndex,
35323
35795
  publish: (e) => registry.publish(runId3, e)
35324
35796
  }) : void 0;
35325
- const earlierStaged = !resume && deps.artifacts && agent.machine !== "none" && thread && thread.length > 0 ? readThreadArtifacts(runsService, thread).then(
35326
- (found) => stageThreadArtifacts(found, {
35327
- store: deps.artifacts,
35328
- nextIndex: nextStagedIndex,
35329
- publish: (e) => registry.publish(runId3, e)
35330
- })
35797
+ const threadAssets = !resume && deps.artifacts && thread && thread.length > 0 ? readThreadAssets({ runs: runsService, store: deps.artifacts }, msg.threadKey).then(
35798
+ (assets) => assets.filter((a) => a.runId !== runId3)
35799
+ ) : void 0;
35800
+ const earlierStaged = threadAssets && agent.machine !== "none" ? threadAssets.then(
35801
+ (assets) => stageThreadArtifacts(
35802
+ assets.filter((a) => a.direction === "in"),
35803
+ { nextIndex: nextStagedIndex, publish: (e) => registry.publish(runId3, e) }
35804
+ )
35331
35805
  ) : void 0;
35332
35806
  const reservation = await reserveRun(deps, {
35333
35807
  msg,
@@ -35354,6 +35828,7 @@ async function dispatch(deps, msg, io, opts = {}) {
35354
35828
  reserved = reservation.reserved;
35355
35829
  requestRow = reservation.requestRow;
35356
35830
  }
35831
+ const reattach = resume ? carriedWorkspaceBinding(resume.row) : void 0;
35357
35832
  const attach = await attachWorkspace(deps, {
35358
35833
  msg,
35359
35834
  io,
@@ -35365,10 +35840,35 @@ async function dispatch(deps, msg, io, opts = {}) {
35365
35840
  agent,
35366
35841
  profile,
35367
35842
  repoCtx,
35368
- root
35843
+ root,
35844
+ ...reattach !== void 0 ? { reattach } : {}
35369
35845
  });
35370
35846
  if (attach.kind === "refused") return ended;
35847
+ if (attach.kind === "reattach_refused") {
35848
+ if (resume) {
35849
+ restartRequest = await abandonLostWorkspace({
35850
+ msg,
35851
+ io,
35852
+ refuse: refuse3,
35853
+ card,
35854
+ shell,
35855
+ closeLines,
35856
+ clock: clock3,
35857
+ run: run2,
35858
+ registry,
35859
+ resume,
35860
+ ledgerRun,
35861
+ why: attach.why
35862
+ });
35863
+ resumeRowClosed = true;
35864
+ }
35865
+ return ended;
35866
+ }
35371
35867
  const { round: round2 } = attach;
35868
+ if (resume && ledgerRun) {
35869
+ const rebound = workspaceBindingFor(round2.selection, profile.machine);
35870
+ if (rebound !== void 0) ledgerRun.setState({ binding: rebound });
35871
+ }
35372
35872
  const { executor, note, resident } = round2.selection;
35373
35873
  if (fencedWhileAttaching) {
35374
35874
  console.log(
@@ -35380,13 +35880,16 @@ async function dispatch(deps, msg, io, opts = {}) {
35380
35880
  }
35381
35881
  const earlier2 = earlierStaged ? await earlierStaged : [];
35382
35882
  if (staged.length > 0 || earlier2.length > 0) {
35383
- const line3 = stagedCopies || earlier2.length > 0 ? attachmentsLine(
35384
- await pullStaged([...stagedCopies ? await stagedCopies : [], ...earlier2], {
35883
+ let line3;
35884
+ if (stagedCopies || earlier2.length > 0) {
35885
+ const pulled = await pullStaged([...stagedCopies ? await stagedCopies : [], ...earlier2], {
35385
35886
  store: deps.artifacts,
35386
35887
  executor,
35387
35888
  resident: resident !== void 0
35388
- })
35389
- ) : noWorkspaceLine(staged);
35889
+ });
35890
+ workspaceFiles.record(pulled);
35891
+ line3 = attachmentsLine(pulled);
35892
+ } else line3 = noWorkspaceLine(staged);
35390
35893
  const request = messages[messages.length - 1];
35391
35894
  if (request && request.role === "user" && line3) {
35392
35895
  request.content = Array.isArray(request.content) ? [...request.content, { type: "text", text: line3 }] : [
@@ -35416,6 +35919,7 @@ async function dispatch(deps, msg, io, opts = {}) {
35416
35919
  if (headGate.headAdopted) publishMeta(repoCtx);
35417
35920
  const isPrReview = agent.name === "review" && repoCtx.repo !== void 0 && repoCtx.pr !== void 0;
35418
35921
  const isCodingPrRun = agent.toolset === "full";
35922
+ const threadFiles = threadAssets ? (await threadAssets).map((a) => describeAsset(a, workspaceFiles.pathOf(a.key))) : [];
35419
35923
  const prompt = await composePrompt(deps, {
35420
35924
  msg,
35421
35925
  agent,
@@ -35432,11 +35936,13 @@ async function dispatch(deps, msg, io, opts = {}) {
35432
35936
  root,
35433
35937
  ...contractBlock !== void 0 && agent.name === "review" ? { contract: contractBlock } : {},
35434
35938
  // What the session already knows (session-log item 10): the notepad and
35435
- // the newest compaction's summary the seed brought back, for the prompt.
35436
- ...session && (session.notepad !== void 0 || session.summary !== void 0) ? {
35939
+ // the newest compaction's summary the seed brought back, and the thread's
35940
+ // files (record 0033) with where each is for this run, for the prompt.
35941
+ ...session?.notepad !== void 0 || session?.summary !== void 0 || threadFiles.length > 0 ? {
35437
35942
  session: {
35438
- ...session.notepad !== void 0 ? { notepad: session.notepad } : {},
35439
- ...session.summary !== void 0 ? { summary: session.summary } : {}
35943
+ ...session?.notepad !== void 0 ? { notepad: session.notepad } : {},
35944
+ ...session?.summary !== void 0 ? { summary: session.summary } : {},
35945
+ ...threadFiles.length > 0 ? { files: threadFiles } : {}
35440
35946
  }
35441
35947
  } : {}
35442
35948
  });
@@ -35496,7 +36002,14 @@ async function dispatch(deps, msg, io, opts = {}) {
35496
36002
  seed,
35497
36003
  ...session ? { seedLog: session.log } : {}
35498
36004
  });
35499
- const sessionTools = sessionCapabilityFor(ledgerRun, deps.runLedger);
36005
+ const sessionTools = sessionCapabilityFor(
36006
+ ledgerRun,
36007
+ deps.runLedger,
36008
+ deps.artifacts ? {
36009
+ read: () => readThreadAssets({ runs: runsService, store: deps.artifacts }, msg.threadKey),
36010
+ pathOf: (key) => workspaceFiles.pathOf(key)
36011
+ } : void 0
36012
+ );
35500
36013
  const { spawn: spawn7, runs, steer, wait } = runToolCapabilities(
35501
36014
  { core: deps, dispatch, registry, clock: clock3 },
35502
36015
  {
@@ -35516,6 +36029,7 @@ async function dispatch(deps, msg, io, opts = {}) {
35516
36029
  profile,
35517
36030
  resolved,
35518
36031
  stagingIndex: nextStagedIndex,
36032
+ workspaceFiles,
35519
36033
  provider,
35520
36034
  model,
35521
36035
  messages,
@@ -35618,7 +36132,7 @@ async function dispatch(deps, msg, io, opts = {}) {
35618
36132
  clearInterval(setupHeartbeat);
35619
36133
  ending.drain(void 0);
35620
36134
  if (registered) deps.runBearers?.revoke(registered.id);
35621
- if (resume && ledgerRun && !runLoopStarted) {
36135
+ if (resume && ledgerRun && !runLoopStarted && !resumeRowClosed) {
35622
36136
  const adopted = ledgerRun;
35623
36137
  await root.span(
35624
36138
  "post.history_write",
@@ -35633,7 +36147,15 @@ async function dispatch(deps, msg, io, opts = {}) {
35633
36147
  const stopMode = settled.kind === "handed-on" ? void 0 : settled.stopMode;
35634
36148
  ended.status = caught ? "failed" : refused5 ? "refused" : stopMode ? "stopped" : "completed";
35635
36149
  root.end(caught ? "error" : "ok", { status: ended.status });
35636
- if (settled.kind === "handed-on") {
36150
+ if (restartRequest) {
36151
+ const pending2 = settled.kind === "handed-on" ? settled.pending : [];
36152
+ const restart2 = prepareRestartTurn(deps, { request: restartRequest, pending: pending2, clock: clock3 });
36153
+ await dispatch(deps, restart2.msg, io, restart2.opts).catch(
36154
+ (err2) => console.error(
36155
+ `[dispatch] ${msg.threadKey} restart after a lost workspace failed: ${err2 instanceof Error ? err2.message : String(err2)}`
36156
+ )
36157
+ );
36158
+ } else if (settled.kind === "handed-on") {
35637
36159
  const fresh = prepareFreshTurn(deps, { agent: settled.agent, pending: settled.pending, clock: clock3 });
35638
36160
  await dispatch(deps, fresh.msg, fresh.io, fresh.opts).catch(
35639
36161
  (err2) => console.error(
@@ -35676,10 +36198,13 @@ var init_dispatcher = __esm({
35676
36198
  init_contract();
35677
36199
  init_codingChild();
35678
36200
  init_settle();
36201
+ init_reattach();
36202
+ init_factory();
35679
36203
  init_lineage();
35680
36204
  init_seed();
35681
36205
  init_session();
35682
36206
  init_thread();
36207
+ init_threadAssets();
35683
36208
  init_select();
35684
36209
  init_spawn();
35685
36210
  init_runsService();
@@ -35690,7 +36215,7 @@ var init_dispatcher = __esm({
35690
36215
  });
35691
36216
 
35692
36217
  // ../../src/core/runHistoryWriter.ts
35693
- function describe7(err2) {
36218
+ function describe8(err2) {
35694
36219
  return err2 instanceof Error ? err2.message : String(err2);
35695
36220
  }
35696
36221
  function createRunHistoryWriter(opts) {
@@ -35707,7 +36232,7 @@ function createRunHistoryWriter(opts) {
35707
36232
  try {
35708
36233
  opts.onPersisted?.(id);
35709
36234
  } catch (err2) {
35710
- opts.warn(`[run-history] onPersisted hook failed for ${id}: ${describe7(err2)}`);
36235
+ opts.warn(`[run-history] onPersisted hook failed for ${id}: ${describe8(err2)}`);
35711
36236
  }
35712
36237
  };
35713
36238
  const attemptAll = async (record2, flag2, sink, span) => {
@@ -35730,12 +36255,12 @@ function createRunHistoryWriter(opts) {
35730
36255
  }
35731
36256
  if (err2 instanceof PermanentStoreError) {
35732
36257
  failures++;
35733
- opts.warn(`[run-history] ${record2.id} not persisted (permanent, not retried): ${describe7(err2)}`);
36258
+ opts.warn(`[run-history] ${record2.id} not persisted (permanent, not retried): ${describe8(err2)}`);
35734
36259
  return;
35735
36260
  }
35736
36261
  if (attempt >= attempts) {
35737
36262
  failures++;
35738
- opts.warn(`[run-history] ${record2.id} not persisted after ${attempts} attempts: ${describe7(err2)}`);
36263
+ opts.warn(`[run-history] ${record2.id} not persisted after ${attempts} attempts: ${describe8(err2)}`);
35739
36264
  return;
35740
36265
  }
35741
36266
  await sleep2(jittered(RUN_HISTORY_RETRY_DELAYS_MS[attempt - 1]));
@@ -35755,7 +36280,7 @@ function createRunHistoryWriter(opts) {
35755
36280
  }
35756
36281
  const p = attemptAll(record2, flag2, writeOpts?.via ?? opts.store, writeOpts?.span).catch((err2) => {
35757
36282
  failures++;
35758
- opts.warn(`[run-history] ${record2.id} writer failed unexpectedly: ${describe7(err2)}`);
36283
+ opts.warn(`[run-history] ${record2.id} writer failed unexpectedly: ${describe8(err2)}`);
35759
36284
  }).finally(() => {
35760
36285
  if (flag2) provisionalFlags.delete(flag2);
35761
36286
  void inFlight.delete(p);
@@ -36172,7 +36697,7 @@ function createLedgerWriteThrough(opts) {
36172
36697
  return { outcome: "untracked" };
36173
36698
  }
36174
36699
  if (err2 instanceof PermanentStoreError || attempt >= claimAttempts) {
36175
- warn(`[ledger] ${req.threadKey} not tracked: claim failed after ${attempt} attempt(s): ${describe8(err2)}`);
36700
+ warn(`[ledger] ${req.threadKey} not tracked: claim failed after ${attempt} attempt(s): ${describe9(err2)}`);
36176
36701
  return { outcome: "untracked" };
36177
36702
  }
36178
36703
  await sleep2(RETRY_MS[Math.min(attempt, RETRY_MS.length) - 1]);
@@ -36334,7 +36859,7 @@ function createLedgerWriteThrough(opts) {
36334
36859
  if (!recorded.ok) this.detach(`seed record refused (${recorded.reason})`);
36335
36860
  else this.seeded = true;
36336
36861
  } catch (err2) {
36337
- this.detach(`seed failed: ${describe8(err2)}`);
36862
+ this.detach(`seed failed: ${describe9(err2)}`);
36338
36863
  }
36339
36864
  }
36340
36865
  async step(report2) {
@@ -36361,7 +36886,7 @@ function createLedgerWriteThrough(opts) {
36361
36886
  return;
36362
36887
  } catch (err2) {
36363
36888
  if (err2 instanceof RouteMissingError || err2 instanceof PermanentStoreError || attempt >= 2) {
36364
- this.detach(`step ${record2.step} failed: ${describe8(err2)}`);
36889
+ this.detach(`step ${record2.step} failed: ${describe9(err2)}`);
36365
36890
  return;
36366
36891
  }
36367
36892
  await sleep2(RETRY_MS[0]);
@@ -36394,7 +36919,7 @@ function createLedgerWriteThrough(opts) {
36394
36919
  } catch (err2) {
36395
36920
  if (err2 instanceof RouteMissingError || err2 instanceof PermanentStoreError || attempt >= 2) {
36396
36921
  this.stateDirty = true;
36397
- warn(`[ledger] ${this.threadKey} state not written: ${describe8(err2)}`);
36922
+ warn(`[ledger] ${this.threadKey} state not written: ${describe9(err2)}`);
36398
36923
  return;
36399
36924
  }
36400
36925
  await sleep2(RETRY_MS[0]);
@@ -36412,7 +36937,7 @@ function createLedgerWriteThrough(opts) {
36412
36937
  this.detach(`finishing refused (fenced)`);
36413
36938
  return "fenced";
36414
36939
  } catch (err2) {
36415
- warn(`[ledger] ${this.threadKey} finishing failed: ${describe8(err2)}`);
36940
+ warn(`[ledger] ${this.threadKey} finishing failed: ${describe9(err2)}`);
36416
36941
  return "unavailable";
36417
36942
  }
36418
36943
  }
@@ -36427,7 +36952,7 @@ function createLedgerWriteThrough(opts) {
36427
36952
  warn(`[ledger] ${this.threadKey} abandon refused (fenced) \u2014 the row is another generation's`);
36428
36953
  } catch (err2) {
36429
36954
  warn(
36430
- `[ledger] ${this.threadKey} abandon failed: ${describe8(err2)} \u2014 the sweep will take the row once its lease lapses`
36955
+ `[ledger] ${this.threadKey} abandon failed: ${describe9(err2)} \u2014 the sweep will take the row once its lease lapses`
36431
36956
  );
36432
36957
  }
36433
36958
  }
@@ -36448,7 +36973,7 @@ function createLedgerWriteThrough(opts) {
36448
36973
  this.onStop(result.stop);
36449
36974
  }
36450
36975
  } catch (err2) {
36451
- warn(`[ledger] ${this.threadKey} heartbeat failed: ${describe8(err2)}`);
36976
+ warn(`[ledger] ${this.threadKey} heartbeat failed: ${describe9(err2)}`);
36452
36977
  }
36453
36978
  }
36454
36979
  stopHeartbeat() {
@@ -36526,7 +37051,7 @@ function createLedgerWriteThrough(opts) {
36526
37051
  warn(`[ledger] inbox push refused for run ${runId3}: no live row \u2014 the follow-up rides in memory only`);
36527
37052
  return void 0;
36528
37053
  } catch (err2) {
36529
- warn(`[ledger] inbox push failed for run ${runId3}: ${describe8(err2)} \u2014 the follow-up rides in memory only`);
37054
+ warn(`[ledger] inbox push failed for run ${runId3}: ${describe9(err2)} \u2014 the follow-up rides in memory only`);
36530
37055
  return void 0;
36531
37056
  }
36532
37057
  },
@@ -36535,7 +37060,7 @@ function createLedgerWriteThrough(opts) {
36535
37060
  return await ledger.readInbox(runId3, afterSeq);
36536
37061
  } catch (err2) {
36537
37062
  warn(
36538
- err2 instanceof RouteMissingError ? `[ledger] state Worker has no inbox read route \u2014 run ${runId3} resumes with the reclaim's inbox snapshot only` : `[ledger] inbox read failed for run ${runId3}: ${describe8(err2)} \u2014 resuming with the reclaim's snapshot only`
37063
+ err2 instanceof RouteMissingError ? `[ledger] state Worker has no inbox read route \u2014 run ${runId3} resumes with the reclaim's inbox snapshot only` : `[ledger] inbox read failed for run ${runId3}: ${describe9(err2)} \u2014 resuming with the reclaim's snapshot only`
36539
37064
  );
36540
37065
  return [];
36541
37066
  }
@@ -36557,12 +37082,12 @@ function createLedgerWriteThrough(opts) {
36557
37082
  for (const r of candidates) if (markedSet.has(r.runId)) r.markHandedOff();
36558
37083
  return { marked };
36559
37084
  } catch (err2) {
36560
- return { marked: [], failed: describe8(err2) };
37085
+ return { marked: [], failed: describe9(err2) };
36561
37086
  }
36562
37087
  }
36563
37088
  };
36564
37089
  }
36565
- var NullLedgerWriteThrough, NullLedgerRun, RETRY_MS, describe8;
37090
+ var NullLedgerWriteThrough, NullLedgerRun, RETRY_MS, describe9;
36566
37091
  var init_writeThrough = __esm({
36567
37092
  "../../src/core/runLedger/writeThrough.ts"() {
36568
37093
  "use strict";
@@ -36642,7 +37167,7 @@ var init_writeThrough = __esm({
36642
37167
  }
36643
37168
  };
36644
37169
  RETRY_MS = [200, 800];
36645
- describe8 = (err2) => err2 instanceof Error ? err2.message : String(err2);
37170
+ describe9 = (err2) => err2 instanceof Error ? err2.message : String(err2);
36646
37171
  }
36647
37172
  });
36648
37173
 
@@ -41288,7 +41813,7 @@ async function reclaimRuns(opts) {
41288
41813
  reclaimed = await ledger.reclaim(gen, now(), LEASE_MS);
41289
41814
  } catch (err2) {
41290
41815
  warn(
41291
- err2 instanceof RouteMissingError ? "[reclaim] state Worker has no run-ledger routes \u2014 nothing to take over" : `[reclaim] reclaim failed: ${describe9(err2)} \u2014 runs the last generation left stay on the ledger until the next boot`
41816
+ err2 instanceof RouteMissingError ? "[reclaim] state Worker has no run-ledger routes \u2014 nothing to take over" : `[reclaim] reclaim failed: ${describe10(err2)} \u2014 runs the last generation left stay on the ledger until the next boot`
41292
41817
  );
41293
41818
  return outcome;
41294
41819
  }
@@ -41356,8 +41881,8 @@ async function reclaimRuns(opts) {
41356
41881
  `[reclaim] ${row.runId} ${row.threadKey} closed ${status3} (from ${run2.reclaimedFrom}; ${why}; ${events.length} event(s))`
41357
41882
  );
41358
41883
  } catch (err2) {
41359
- outcome.failed.push({ runId: row.runId, error: describe9(err2) });
41360
- warn(`[reclaim] ${row.runId} ${row.threadKey}: ${describe9(err2)} \u2014 left on the ledger for the next boot`);
41884
+ outcome.failed.push({ runId: row.runId, error: describe10(err2) });
41885
+ warn(`[reclaim] ${row.runId} ${row.threadKey}: ${describe10(err2)} \u2014 left on the ledger for the next boot`);
41361
41886
  }
41362
41887
  }
41363
41888
  try {
@@ -41373,7 +41898,7 @@ async function reclaimRuns(opts) {
41373
41898
  });
41374
41899
  }
41375
41900
  } catch (err2) {
41376
- warn(`[reclaim] listing live runs failed: ${describe9(err2)} \u2014 the card sweep runs without the ledger's guard`);
41901
+ warn(`[reclaim] listing live runs failed: ${describe10(err2)} \u2014 the card sweep runs without the ledger's guard`);
41377
41902
  }
41378
41903
  if (reclaimed.length > 0 || outcome.liveElsewhere.length > 0) {
41379
41904
  log(
@@ -41395,7 +41920,7 @@ function startReclaimSweep(opts) {
41395
41920
  await opts.onOutcome(outcome);
41396
41921
  }
41397
41922
  } catch (err2) {
41398
- warn(`[reclaim] sweep failed: ${describe9(err2)}`);
41923
+ warn(`[reclaim] sweep failed: ${describe10(err2)}`);
41399
41924
  } finally {
41400
41925
  running = false;
41401
41926
  }
@@ -41421,7 +41946,7 @@ async function completenessVerdict(ledger, row, lastStep) {
41421
41946
  const source2 = transcriptSource(row.meta);
41422
41947
  transcript = source2.kind === "session" ? await ledger.readSession(source2.key, source2.from) : await ledger.readTranscript(row.runId);
41423
41948
  } catch (err2) {
41424
- return { resumable: false, why: `transcript unreadable (${describe9(err2)})` };
41949
+ return { resumable: false, why: `transcript unreadable (${describe10(err2)})` };
41425
41950
  }
41426
41951
  if (!transcript.complete) return { resumable: false, why: `transcript incomplete: ${transcript.gap}` };
41427
41952
  const verdict = transcriptCompleteness({
@@ -41442,7 +41967,7 @@ async function completenessVerdict(ledger, row, lastStep) {
41442
41967
  return { resumable: false, why: verdict.why };
41443
41968
  }
41444
41969
  }
41445
- var TERMINAL, describe9;
41970
+ var TERMINAL, describe10;
41446
41971
  var init_boot = __esm({
41447
41972
  "../../src/core/boot.ts"() {
41448
41973
  "use strict";
@@ -41459,7 +41984,7 @@ var init_boot = __esm({
41459
41984
  "failed",
41460
41985
  "interrupted"
41461
41986
  ]);
41462
- describe9 = (err2) => err2 instanceof Error ? err2.message : String(err2);
41987
+ describe10 = (err2) => err2 instanceof Error ? err2.message : String(err2);
41463
41988
  }
41464
41989
  });
41465
41990
 
@@ -42308,6 +42833,17 @@ var init_toolRules = __esm({
42308
42833
  });
42309
42834
 
42310
42835
  // ../../src/core/harness/pi/relay.ts
42836
+ async function relayToolCall(harness, calls, ask, opts = {}) {
42837
+ const answer = calls.join(ask.toolCallId, () => runRelayedTool(harness, ask, { signal: calls.signal }));
42838
+ const window = new AbortController();
42839
+ const sleep2 = opts.sleep ?? sleepUnlessAborted;
42840
+ const progress = await Promise.race([
42841
+ answer.then((a) => ({ done: true, answer: a })),
42842
+ sleep2(opts.windowMs ?? RELAY_POLL_WINDOW_MS, window.signal).then(() => ({ done: false }))
42843
+ ]);
42844
+ window.abort();
42845
+ return progress;
42846
+ }
42311
42847
  function relayedToolDefinitions(harness) {
42312
42848
  return harness.tools.map(({ name, description, inputSchema }) => ({ name, description, inputSchema }));
42313
42849
  }
@@ -42328,18 +42864,23 @@ function authorizeToolCall(harness, ask) {
42328
42864
  if (verdict.verdict === "allowed") return { allow: true };
42329
42865
  return refuse3(verdict.reason);
42330
42866
  }
42331
- async function runRelayedTool(harness, ask) {
42867
+ async function runRelayedTool(harness, ask, opts = {}) {
42332
42868
  const tool = harness.tools.find((t) => t.name === ask.tool);
42333
42869
  if (!tool) return { content: [{ type: "text", text: `Unknown tool: ${ask.tool}` }], isError: true };
42334
42870
  const span = harness.toolSpan(ask.toolCallId);
42335
42871
  const base = harness.toolContext;
42336
- const ctx = span ? {
42337
- ...base,
42338
- span,
42339
- executor: new TracingExecutor(base.executor, span, harness.backend),
42340
- publish: (e) => harness.emit(withSpanId2(e, span.id)),
42341
- ...base.github?.api.withSpan ? { github: { ...base.github, api: base.github.api.withSpan(span) } } : {}
42342
- } : { ...base, publish: (e) => harness.emit(e) };
42872
+ const readConversation = base.conversation;
42873
+ const ctx = {
42874
+ ...span ? {
42875
+ ...base,
42876
+ span,
42877
+ executor: new TracingExecutor(base.executor, span, harness.backend),
42878
+ publish: (e) => harness.emit(withSpanId2(e, span.id)),
42879
+ ...base.github?.api.withSpan ? { github: { ...base.github, api: base.github.api.withSpan(span) } } : {}
42880
+ } : { ...base, publish: (e) => harness.emit(e) },
42881
+ ...opts.signal && !base.signal ? { signal: opts.signal } : {},
42882
+ ...readConversation && harness.callSeen ? { conversation: () => harness.callSeen(ask.toolCallId).then(() => readConversation()) } : {}
42883
+ };
42343
42884
  try {
42344
42885
  const input = typeof ask.input === "object" && ask.input !== null ? ask.input : {};
42345
42886
  const output = await tool.run(input, ctx);
@@ -42367,30 +42908,64 @@ function withSpanId2(e, spanId) {
42367
42908
  return e;
42368
42909
  }
42369
42910
  }
42370
- var HarnessRegistry;
42911
+ var HarnessRegistry, RELAY_POLL_WINDOW_MS, RelayedCalls;
42371
42912
  var init_relay = __esm({
42372
42913
  "../../src/core/harness/pi/relay.ts"() {
42373
42914
  "use strict";
42374
42915
  init_tracingExecutor();
42375
42916
  init_types3();
42917
+ init_awaitChildren();
42376
42918
  init_runEvents();
42377
42919
  init_toolRules();
42378
42920
  HarnessRegistry = class {
42379
42921
  live = /* @__PURE__ */ new Map();
42380
- /** The run drives a pi from here; the returned function forgets it. */
42922
+ /** The run drives a pi from here, its relayed calls kept beside it; the
42923
+ * returned function forgets it and ends whatever call still runs. */
42381
42924
  register(harness) {
42382
- this.live.set(harness.runId, harness);
42925
+ const entry = { harness, calls: new RelayedCalls() };
42926
+ this.live.set(harness.runId, entry);
42383
42927
  return () => {
42384
- if (this.live.get(harness.runId) === harness) this.live.delete(harness.runId);
42928
+ if (this.live.get(harness.runId) !== entry) return;
42929
+ this.live.delete(harness.runId);
42930
+ entry.calls.end();
42385
42931
  };
42386
42932
  }
42387
42933
  get(runId3) {
42388
- return this.live.get(runId3);
42934
+ return this.live.get(runId3)?.harness;
42935
+ }
42936
+ /** The run's relayed calls, for as long as it is registered. */
42937
+ calls(runId3) {
42938
+ return this.live.get(runId3)?.calls;
42389
42939
  }
42390
42940
  size() {
42391
42941
  return this.live.size;
42392
42942
  }
42393
42943
  };
42944
+ RELAY_POLL_WINDOW_MS = 3e4;
42945
+ RelayedCalls = class {
42946
+ calls = /* @__PURE__ */ new Map();
42947
+ ending = new AbortController();
42948
+ get size() {
42949
+ return this.calls.size;
42950
+ }
42951
+ /** Aborted when the run ends: the signal every call here runs under. */
42952
+ get signal() {
42953
+ return this.ending.signal;
42954
+ }
42955
+ /** The call's answer as a promise: started by `start` on the first ask, the same promise after. */
42956
+ join(callId, start3) {
42957
+ let answer = this.calls.get(callId);
42958
+ if (!answer) {
42959
+ answer = start3();
42960
+ this.calls.set(callId, answer);
42961
+ }
42962
+ return answer;
42963
+ }
42964
+ end() {
42965
+ this.ending.abort();
42966
+ this.calls.clear();
42967
+ }
42968
+ };
42394
42969
  }
42395
42970
  });
42396
42971
 
@@ -42439,7 +43014,15 @@ async function handleHarnessRequest(deps, req) {
42439
43014
  deps.log?.(`[harness] run=${door.runId} authorize ${ask.tool} \u2192 ${answer2.allow ? "allow" : "refuse"}`);
42440
43015
  return { status: 200, body: { ...answer2 } };
42441
43016
  }
42442
- const answer = await runRelayedTool(harness, ask);
43017
+ const progress = await relayToolCall(harness, deps.harnesses.calls(door.runId), ask, {
43018
+ ...deps.relayWindowMs !== void 0 ? { windowMs: deps.relayWindowMs } : {},
43019
+ ...deps.sleep ? { sleep: deps.sleep } : {}
43020
+ });
43021
+ if (!progress.done) {
43022
+ deps.log?.(`[harness] run=${door.runId} tool ${ask.tool} \u2192 pending`);
43023
+ return { status: 202, body: { pending: true, toolCallId: ask.toolCallId } };
43024
+ }
43025
+ const { answer } = progress;
42443
43026
  deps.log?.(`[harness] run=${door.runId} tool ${ask.tool} \u2192 ${answer.isError ? "error" : "ok"}`);
42444
43027
  return { status: 200, body: { ...answer } };
42445
43028
  }
@@ -42775,7 +43358,7 @@ async function openThreadFromRequester(deps, instance, lead, at) {
42775
43358
  }
42776
43359
  };
42777
43360
  } catch (err2) {
42778
- return { ok: false, response: json(502, { ok: false, error: "thread_failed", message: describe10(err2), at }) };
43361
+ return { ok: false, response: json(502, { ok: false, error: "thread_failed", message: describe11(err2), at }) };
42779
43362
  }
42780
43363
  }
42781
43364
  async function ensureReviewThread(deps, instance, row, at) {
@@ -42901,8 +43484,8 @@ async function spawn6(body, deps) {
42901
43484
  try {
42902
43485
  turn = await composeChild(req.brief, instance, row, briefReaders(deps, instance, io));
42903
43486
  } catch (err2) {
42904
- log(`[coordinator] ${instance.id} ${req.step}: the brief could not be composed: ${describe10(err2)}`);
42905
- return json(502, { ok: false, error: "brief_failed", message: describe10(err2), at });
43487
+ log(`[coordinator] ${instance.id} ${req.step}: the brief could not be composed: ${describe11(err2)}`);
43488
+ return json(502, { ok: false, error: "brief_failed", message: describe11(err2), at });
42906
43489
  }
42907
43490
  } else {
42908
43491
  turn = { prompt: req.prompt };
@@ -42952,7 +43535,7 @@ async function spawn6(body, deps) {
42952
43535
  );
42953
43536
  void settled.then((end) => {
42954
43537
  if (end.kind === "threw")
42955
- log(`[coordinator] ${instance.id} ${req.step}: the child's dispatch threw: ${describe10(end.err)}`);
43538
+ log(`[coordinator] ${instance.id} ${req.step}: the child's dispatch threw: ${describe11(end.err)}`);
42956
43539
  });
42957
43540
  const first = await Promise.race([started.then((id) => ({ kind: "started", id })), settled]);
42958
43541
  if (first.kind === "started" || startedId !== void 0) {
@@ -42960,7 +43543,7 @@ async function spawn6(body, deps) {
42960
43543
  log(`[coordinator] ${instance.id} ${req.step}: spawned ${req.preset} run ${runId3} in ${threadKey}`);
42961
43544
  return json(200, { ok: true, runId: runId3, threadKey, at });
42962
43545
  }
42963
- if (first.kind === "threw") return json(502, { ok: false, error: "spawn_failed", message: describe10(first.err), at });
43546
+ if (first.kind === "threw") return json(502, { ok: false, error: "spawn_failed", message: describe11(first.err), at });
42964
43547
  const refusal = first.outcome.refusal;
42965
43548
  if (refusal === "coordinator_thread_live") {
42966
43549
  const now = await liveOnThread(deps.runs, instance, threadKey);
@@ -43113,7 +43696,7 @@ async function prCheck(body, deps) {
43113
43696
  at
43114
43697
  });
43115
43698
  } catch (err2) {
43116
- return json(502, { ok: false, error: "github_unavailable", message: describe10(err2), at });
43699
+ return json(502, { ok: false, error: "github_unavailable", message: describe11(err2), at });
43117
43700
  }
43118
43701
  }
43119
43702
  async function plan2(body, deps) {
@@ -43222,7 +43805,7 @@ async function branch(body, deps) {
43222
43805
  await deps.createBranchRef(instance.repo, name, base);
43223
43806
  return json(200, { ok: true, branch: name, base, at });
43224
43807
  } catch (err2) {
43225
- return json(200, { ok: false, reason: describe10(err2), at });
43808
+ return json(200, { ok: false, reason: describe11(err2), at });
43226
43809
  }
43227
43810
  }
43228
43811
  function unitLines(units) {
@@ -43274,7 +43857,7 @@ async function round(body, deps) {
43274
43857
  instance,
43275
43858
  units.map((u) => u.unit === updated.unit ? updated : u)
43276
43859
  ).catch(
43277
- (err2) => (deps.log ?? console.warn)(`[coordinator] ${instance.id}: the card could not be redrawn: ${describe10(err2)}`)
43860
+ (err2) => (deps.log ?? console.warn)(`[coordinator] ${instance.id}: the card could not be redrawn: ${describe11(err2)}`)
43278
43861
  );
43279
43862
  return json(200, { ok: true, at });
43280
43863
  }
@@ -43308,7 +43891,7 @@ async function unitEnd(body, deps) {
43308
43891
  told = true;
43309
43892
  } catch (err2) {
43310
43893
  (deps.log ?? console.warn)(
43311
- `[coordinator] ${instance.id} ${row.unit}: the report could not be posted: ${describe10(err2)}`
43894
+ `[coordinator] ${instance.id} ${row.unit}: the report could not be posted: ${describe11(err2)}`
43312
43895
  );
43313
43896
  }
43314
43897
  }
@@ -43322,7 +43905,7 @@ ${ending.report}` + (rendered !== void 0 ? `
43322
43905
  ${rendered}` : "");
43323
43906
  await deps.github.commentIssue(instance.repo, row.issue, comment).catch(
43324
43907
  (err2) => (deps.log ?? console.warn)(
43325
- `[coordinator] ${instance.id} ${row.unit}: the board comment could not be posted: ${describe10(err2)}`
43908
+ `[coordinator] ${instance.id} ${row.unit}: the board comment could not be posted: ${describe11(err2)}`
43326
43909
  )
43327
43910
  );
43328
43911
  }
@@ -43377,7 +43960,7 @@ async function merge(body, deps, subject2) {
43377
43960
  try {
43378
43961
  facts2 = await deps.fetchPrFacts(pr);
43379
43962
  } catch (err2) {
43380
- return json(502, { ok: false, error: "github_unavailable", message: describe10(err2), at });
43963
+ return json(502, { ok: false, error: "github_unavailable", message: describe11(err2), at });
43381
43964
  }
43382
43965
  if (facts2 === void 0)
43383
43966
  return json(502, { ok: false, error: "github_unavailable", message: `${where} could not be read`, at });
@@ -43429,7 +44012,7 @@ async function merge(body, deps, subject2) {
43429
44012
  title: facts2.title ?? `Merge pull request #${pr.number}`
43430
44013
  });
43431
44014
  } catch (err2) {
43432
- return json(502, { ok: false, error: "github_unavailable", message: describe10(err2), at });
44015
+ return json(502, { ok: false, error: "github_unavailable", message: describe11(err2), at });
43433
44016
  }
43434
44017
  if (!merged.ok) {
43435
44018
  log(
@@ -43604,13 +44187,13 @@ function createAdminCoordinatorHandler(deps) {
43604
44187
  }
43605
44188
  write(res, await answerCoordinatorStep(door, read.body, deps));
43606
44189
  } catch (err2) {
43607
- (deps.log ?? console.error)(`[coordinator] ${describe10(err2)}`);
44190
+ (deps.log ?? console.error)(`[coordinator] ${describe11(err2)}`);
43608
44191
  write(res, json(500, { ok: false, error: "internal error" }));
43609
44192
  }
43610
44193
  })();
43611
44194
  };
43612
44195
  }
43613
- var COORDINATOR_ADMIN_PREFIX, MAX_SPAWN_PROMPT_CHARS, FINISHED_LOOKBACK_PAGES, MAX_ADMIN_BODY_BYTES, UNIT_ID, PRESETS_OF_KIND, invalid, json, describe10, EVERY_RUN, REVIEW_POSTED_CHECKS, REVIEW_POSTED_RECHECK_MS, ROUND_OUTCOMES, ENDING_ICON, STEPS;
44196
+ var COORDINATOR_ADMIN_PREFIX, MAX_SPAWN_PROMPT_CHARS, FINISHED_LOOKBACK_PAGES, MAX_ADMIN_BODY_BYTES, UNIT_ID, PRESETS_OF_KIND, invalid, json, describe11, EVERY_RUN, REVIEW_POSTED_CHECKS, REVIEW_POSTED_RECHECK_MS, ROUND_OUTCOMES, ENDING_ICON, STEPS;
43614
44197
  var init_adminCoordinator = __esm({
43615
44198
  "../../src/channels/adminCoordinator.ts"() {
43616
44199
  "use strict";
@@ -43643,7 +44226,7 @@ var init_adminCoordinator = __esm({
43643
44226
  };
43644
44227
  invalid = (error) => ({ ok: false, error });
43645
44228
  json = (status3, body) => ({ status: status3, body });
43646
- describe10 = (err2) => err2 instanceof Error ? err2.message : String(err2);
44229
+ describe11 = (err2) => err2 instanceof Error ? err2.message : String(err2);
43647
44230
  EVERY_RUN = { kind: "all" };
43648
44231
  REVIEW_POSTED_CHECKS = 3;
43649
44232
  REVIEW_POSTED_RECHECK_MS = 2e3;