@coreplane/switchboard 1.220.1 → 1.221.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -356,13 +356,14 @@ function amzDate(now) {
356
356
  function encodeKey(key) {
357
357
  return key.split("/").map(encodeURIComponent).join("/");
358
358
  }
359
- var PRESIGN_TTL_SECONDS, R2ArtifactStore;
359
+ var PRESIGN_TTL_SECONDS, IDENTITY, R2ArtifactStore;
360
360
  var init_store = __esm({
361
361
  "../../src/artifacts/store.ts"() {
362
362
  "use strict";
363
363
  init_clock();
364
364
  init_config();
365
365
  PRESIGN_TTL_SECONDS = ARTIFACT_DEFAULTS.presignTtlSeconds;
366
+ IDENTITY = { "accept-encoding": "identity" };
366
367
  R2ArtifactStore = class {
367
368
  bucket;
368
369
  client;
@@ -402,7 +403,7 @@ var init_store = __esm({
402
403
  return this.presign(key, "GET");
403
404
  }
404
405
  async head(key) {
405
- const signed = await this.client.sign(new Request(this.objectUrl(key), { method: "HEAD" }), {
406
+ const signed = await this.client.sign(new Request(this.objectUrl(key), { method: "HEAD", headers: IDENTITY }), {
406
407
  aws: { datetime: amzDate(this.clock()) }
407
408
  });
408
409
  const res = await this.fetchImpl(signed);
@@ -416,7 +417,7 @@ var init_store = __esm({
416
417
  return { size, contentType: res.headers.get("content-type") ?? "application/octet-stream" };
417
418
  }
418
419
  async get(key) {
419
- const signed = await this.client.sign(new Request(this.objectUrl(key), { method: "GET" }), {
420
+ const signed = await this.client.sign(new Request(this.objectUrl(key), { method: "GET", headers: IDENTITY }), {
420
421
  aws: { datetime: amzDate(this.clock()) }
421
422
  });
422
423
  const res = await this.fetchImpl(signed);
@@ -4139,6 +4140,7 @@ var init_residentSteps = __esm({
4139
4140
  "ops-dir": "preparing a directory",
4140
4141
  "stage-dir": "preparing a directory",
4141
4142
  stat: "checking a file",
4143
+ "boot-id": "reading the container's identity",
4142
4144
  touch: "touching a marker",
4143
4145
  nproc: "counting CPUs",
4144
4146
  mutex_wait: "waiting for the workspace",
@@ -9419,7 +9421,7 @@ async function parseResidentBody(res) {
9419
9421
  }
9420
9422
  return sanitizeResidentBody(data);
9421
9423
  }
9422
- var DETACH_TIMEOUT_MS, ResidentNeedsRefError, ResidentOperations, ResidentExecutor;
9424
+ var DETACH_TIMEOUT_MS, ResidentNeedsRefError, ResidentReuseRefusedError, ResidentOperations, ResidentExecutor;
9423
9425
  var init_resident = __esm({
9424
9426
  "../../src/execution/resident.ts"() {
9425
9427
  "use strict";
@@ -9451,6 +9453,16 @@ var init_resident = __esm({
9451
9453
  defaultRef;
9452
9454
  needs = "ref";
9453
9455
  };
9456
+ ResidentReuseRefusedError = class extends Error {
9457
+ constructor(resource, why) {
9458
+ super(`resident attach: ${resource} cannot reuse this thread's worktree (${why})`);
9459
+ this.resource = resource;
9460
+ this.why = why;
9461
+ this.name = "ResidentReuseRefusedError";
9462
+ }
9463
+ resource;
9464
+ why;
9465
+ };
9454
9466
  ResidentOperations = class {
9455
9467
  constructor(opts) {
9456
9468
  this.opts = opts;
@@ -9608,6 +9620,7 @@ var init_resident = __esm({
9608
9620
  if (this.opts.refHint) body.refHint = this.opts.refHint;
9609
9621
  if (this.opts.readonly) body.readonly = true;
9610
9622
  if (this.opts.sha) body.sha = this.opts.sha;
9623
+ if (this.opts.reuse) body.reuse = true;
9611
9624
  const answered = await this.call("/attach", body, timeoutMs, void 0, span);
9612
9625
  const data = answered.data;
9613
9626
  const status3 = answered.status === 200 && typeof data.error === "string" && typeof data.status === "number" ? data.status : answered.status;
@@ -9621,6 +9634,7 @@ var init_resident = __esm({
9621
9634
  sha: data.sha,
9622
9635
  ...typeof data.workspace === "string" && data.workspace ? { workspace: data.workspace } : {},
9623
9636
  ...typeof data.user === "string" && data.user ? { user: data.user } : {},
9637
+ ...typeof data.container === "string" && data.container ? { container: data.container } : {},
9624
9638
  ...trace.length > 0 ? { trace } : {},
9625
9639
  ...typeof data.attachMs === "number" && Number.isFinite(data.attachMs) ? { attachMs: data.attachMs } : {}
9626
9640
  };
@@ -9640,6 +9654,8 @@ var init_resident = __esm({
9640
9654
  const defaultRef = typeof data.defaultRef === "string" && data.defaultRef ? data.defaultRef : void 0;
9641
9655
  return traced(new ResidentNeedsRefError(this.opts.resource, defaultRef));
9642
9656
  }
9657
+ if (status3 === 409 && data.needs === "recreate")
9658
+ return traced(new ResidentReuseRefusedError(this.opts.resource, err2));
9643
9659
  if (status3 === 404) return traced(new Error(`resident attach: ${this.opts.resource} is not onboarded (${err2})`));
9644
9660
  return traced(new Error(`resident attach failed for ${this.opts.resource}: ${err2}`));
9645
9661
  }
@@ -9994,11 +10010,33 @@ var init_githubApp = __esm({
9994
10010
  // ../../src/execution/factory.ts
9995
10011
  import { resolve as resolve7 } from "node:path";
9996
10012
  import { mkdirSync as mkdirSync7 } from "node:fs";
10013
+ function workspaceBindingOf(value) {
10014
+ if (typeof value !== "object" || value === null) return void 0;
10015
+ const v = value;
10016
+ if (typeof v.backend !== "string" || !BACKENDS.includes(v.backend)) return void 0;
10017
+ return {
10018
+ backend: v.backend,
10019
+ ...typeof v.workspace === "string" && v.workspace ? { workspace: v.workspace } : {},
10020
+ ...typeof v.user === "string" && v.user ? { user: v.user } : {},
10021
+ ...typeof v.container === "string" && v.container ? { container: v.container } : {}
10022
+ };
10023
+ }
10024
+ function workspaceBindingFor(selection, machine = "repo-resident") {
10025
+ if (machine === "none" || selection.backend === void 0) return void 0;
10026
+ const b = selection.binding;
10027
+ return {
10028
+ backend: selection.backend,
10029
+ ...b?.workspace !== void 0 ? { workspace: b.workspace } : {},
10030
+ ...b?.user !== void 0 ? { user: b.user } : {},
10031
+ ...b?.container !== void 0 ? { container: b.container } : {}
10032
+ };
10033
+ }
9997
10034
  async function makeExecutor(opts, ctx, span) {
9998
10035
  const machine = ctx.profile.machine;
9999
10036
  if (machine === "none") {
10000
10037
  return { executor: new NullExecutor(ctx.agent.name), backend: "local" };
10001
10038
  }
10039
+ if (ctx.reattach !== void 0) return reattachWorkspace(opts, ctx, ctx.reattach, span);
10002
10040
  if (machine === "blank") {
10003
10041
  return {
10004
10042
  executor: await makePerThreadExecutor(opts, { threadKey: ctx.threadKey, resolveEnvs: async () => ({}) }),
@@ -10056,6 +10094,58 @@ async function makeExecutor(opts, ctx, span) {
10056
10094
  ...failedAttach ? { trace: failedAttach.steps } : {}
10057
10095
  };
10058
10096
  }
10097
+ async function reattachWorkspace(opts, ctx, recorded, span) {
10098
+ const refuse3 = (why) => new WorkspaceReattachRefusedError(recorded, oneLine(why));
10099
+ if (recorded.backend !== "resident") {
10100
+ const input = ctx.profile.machine === "blank" ? { threadKey: ctx.threadKey, resolveEnvs: async () => ({}) } : perThreadCheckout(ctx);
10101
+ return { executor: await makePerThreadExecutor(opts, input), backend: perThreadBackend(opts) };
10102
+ }
10103
+ const resident = opts.execution?.resident;
10104
+ if (!resident) throw refuse3("no resident backend is configured in this process");
10105
+ if (!ctx.repo) throw refuse3("the run's row names no repository to re-attach on");
10106
+ const tokenEnv = resident.tokenEnv ?? "RESIDENT_OPERATOR_TOKEN";
10107
+ const token2 = processSecrets.named(tokenEnv);
10108
+ if (!token2) throw new Error(`execution.resident is configured but ${tokenEnv} is not set`);
10109
+ const resource = repoResourceId(ctx.repo);
10110
+ const probe = await probeResident(resident, token2, resource, span);
10111
+ if (probe.kind === "unreachable") throw refuse3(`resident unreachable (${probe.error})`);
10112
+ if (!isServiceable(probe.state, probe.reason))
10113
+ throw refuse3(`resident ${probe.state}${probe.reason ? ` (${probe.reason})` : ""}`);
10114
+ const nonWarm = probe.state === "warm" ? void 0 : oneLine(`${probe.state}${probe.reason ? ` (${probe.reason})` : ""}`);
10115
+ const executor = new ResidentExecutor({
10116
+ baseUrl: resident.baseUrl,
10117
+ token: token2.reveal(),
10118
+ resource,
10119
+ threadKey: ctx.threadKey,
10120
+ refHint: ctx.ref,
10121
+ readonly: ctx.profile.identity === "read" ? true : void 0,
10122
+ sha: ctx.headSha,
10123
+ reuse: true
10124
+ });
10125
+ let binding;
10126
+ try {
10127
+ binding = await executor.attach(span);
10128
+ } catch (err2) {
10129
+ throw refuse3(err2 instanceof Error ? err2.message : String(err2));
10130
+ }
10131
+ const same = (recordedValue, answered) => recordedValue === void 0 || answered === void 0 || recordedValue === answered;
10132
+ if (!same(recorded.workspace, binding.workspace) || !same(recorded.user, binding.user)) {
10133
+ const name = (workspace, user) => `${workspace ?? "(unnamed)"} as ${user ?? "(unnamed)"}`;
10134
+ throw refuse3(
10135
+ `the resident's worktree for this thread is ${name(binding.workspace, binding.user)}, not the run's recorded ${name(recorded.workspace, recorded.user)}`
10136
+ );
10137
+ }
10138
+ const where = `${resource.replace(/^repo:/, "")} \xB7 ${binding.ref}@${binding.sha.slice(0, 7)}`;
10139
+ return {
10140
+ executor,
10141
+ resident: true,
10142
+ backend: "resident",
10143
+ ...binding.trace ? { trace: binding.trace } : {},
10144
+ ...binding.attachMs !== void 0 ? { attachMs: binding.attachMs } : {},
10145
+ binding,
10146
+ 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`
10147
+ };
10148
+ }
10059
10149
  async function openResident(opts, nonWarm, span) {
10060
10150
  let executor = new ResidentExecutor(opts);
10061
10151
  let binding;
@@ -10206,7 +10296,7 @@ function perThreadBackend(opts) {
10206
10296
  const type = opts.execution?.type ?? "local";
10207
10297
  return type === "e2b" ? "e2b" : type === "cloudflare" ? "sandbox" : "local";
10208
10298
  }
10209
- var PROBE_OUTAGE_WINDOW_MS, probeOutage, NullExecutor;
10299
+ var BACKENDS, WorkspaceReattachRefusedError, PROBE_OUTAGE_WINDOW_MS, probeOutage, NullExecutor;
10210
10300
  var init_factory = __esm({
10211
10301
  "../../src/execution/factory.ts"() {
10212
10302
  "use strict";
@@ -10221,6 +10311,17 @@ var init_factory = __esm({
10221
10311
  init_residentState();
10222
10312
  init_clock();
10223
10313
  init_secrets2();
10314
+ BACKENDS = ["local", "resident", "sandbox", "e2b"];
10315
+ WorkspaceReattachRefusedError = class extends Error {
10316
+ constructor(recorded, why) {
10317
+ super(`the run's workspace on the ${recorded.backend} backend could not be re-attached: ${why}`);
10318
+ this.recorded = recorded;
10319
+ this.why = why;
10320
+ this.name = "WorkspaceReattachRefusedError";
10321
+ }
10322
+ recorded;
10323
+ why;
10324
+ };
10224
10325
  PROBE_OUTAGE_WINDOW_MS = 3e4;
10225
10326
  NullExecutor = class {
10226
10327
  constructor(agentName) {
@@ -20508,7 +20609,7 @@ function createRunsService(deps) {
20508
20609
  const analyze = deps.analyze ?? ((events, opts) => analyzeRunFriction(events, opts));
20509
20610
  const clock3 = deps.clock ?? systemClock;
20510
20611
  const warn = deps.warn ?? ((m) => console.warn(m));
20511
- const describe11 = (err2) => err2 instanceof Error ? err2.message : String(err2);
20612
+ const describe12 = (err2) => err2 instanceof Error ? err2.message : String(err2);
20512
20613
  let listing;
20513
20614
  const liveRows = () => {
20514
20615
  if (!ledger) return null;
@@ -20528,7 +20629,7 @@ function createRunsService(deps) {
20528
20629
  try {
20529
20630
  rows = await pending2;
20530
20631
  } catch (err2) {
20531
- warn(`[runs] run ledger list failed \u2014 showing this process's runs only: ${describe11(err2)}`);
20632
+ warn(`[runs] run ledger list failed \u2014 showing this process's runs only: ${describe12(err2)}`);
20532
20633
  return [];
20533
20634
  }
20534
20635
  const foreign = rows.filter((row) => !registry.getById(row.runId));
@@ -20538,7 +20639,7 @@ function createRunsService(deps) {
20538
20639
  try {
20539
20640
  events = await ledger.readEvents(row.runId);
20540
20641
  } catch (err2) {
20541
- warn(`[runs] run ledger events read failed for ${row.runId}: ${describe11(err2)}`);
20642
+ warn(`[runs] run ledger events read failed for ${row.runId}: ${describe12(err2)}`);
20542
20643
  }
20543
20644
  return ledgerView(row, events);
20544
20645
  })
@@ -20552,7 +20653,7 @@ function createRunsService(deps) {
20552
20653
  if (!row) return null;
20553
20654
  return { row, events: await ledger.readEvents(id) };
20554
20655
  } catch (err2) {
20555
- warn(`[runs] run ledger read failed for ${id}: ${describe11(err2)}`);
20656
+ warn(`[runs] run ledger read failed for ${id}: ${describe12(err2)}`);
20556
20657
  return null;
20557
20658
  }
20558
20659
  };
@@ -20570,7 +20671,7 @@ function createRunsService(deps) {
20570
20671
  row = await storeSummary(id);
20571
20672
  } catch (err2) {
20572
20673
  warn(
20573
- `[runs] history store read failed for ${id} \u2014 serving the registry row without its record: ${describe11(err2)}`
20674
+ `[runs] history store read failed for ${id} \u2014 serving the registry row without its record: ${describe12(err2)}`
20574
20675
  );
20575
20676
  return {};
20576
20677
  }
@@ -20741,7 +20842,7 @@ function createRunsService(deps) {
20741
20842
  const r = await ledger.requestStop(id, mode);
20742
20843
  if (r.ok) return { ok: true, value: { id, mode, state: "stopping" } };
20743
20844
  } catch (err2) {
20744
- warn(`[runs] run ledger stop failed for ${id}: ${describe11(err2)}`);
20845
+ warn(`[runs] run ledger stop failed for ${id}: ${describe12(err2)}`);
20745
20846
  }
20746
20847
  }
20747
20848
  return await storeSummary(id) ? conflict : notFound;
@@ -26605,9 +26706,10 @@ var init_runs2 = __esm({
26605
26706
  request.budget = budget;
26606
26707
  }
26607
26708
  const spawn7 = ctx.spawn ?? nullSpawnCapability;
26709
+ const conversation = ctx.conversation ? await ctx.conversation() : void 0;
26608
26710
  const out = await spawn7.spawn(request, {
26609
26711
  remainingMs: ctx.remainingMs?.() ?? Number.POSITIVE_INFINITY,
26610
- ...ctx.conversation ? { conversation: ctx.conversation() } : {}
26712
+ ...conversation ? { conversation } : {}
26611
26713
  });
26612
26714
  if (out.kind === "refused") return `spawn refused (${out.reason}): ${out.message}`;
26613
26715
  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.`;
@@ -26786,6 +26888,252 @@ var init_runs2 = __esm({
26786
26888
  }
26787
26889
  });
26788
26890
 
26891
+ // ../../src/core/dispatch/staging.ts
26892
+ function stagedBasename(index, name) {
26893
+ return `${index}-${safeBasename(name)}`;
26894
+ }
26895
+ function pullCommandFor(url, basename4) {
26896
+ return `mkdir -p ${ATTACHMENTS_DIR} && curl -fsS -o ${shellQuote(`${ATTACHMENTS_DIR}/${basename4}`)} ${shellQuote(url)}`;
26897
+ }
26898
+ function excludeCommand() {
26899
+ const entry = shellQuote(`${ATTACHMENTS_DIR}/`);
26900
+ return `mkdir -p .git/info && { grep -qxF ${entry} .git/info/exclude 2>/dev/null || echo ${entry} >> .git/info/exclude; }`;
26901
+ }
26902
+ function formatSize(bytes) {
26903
+ if (bytes >= 1e9) return `${(bytes / 1e9).toFixed(1)} GB`;
26904
+ if (bytes >= 1e6) return `${Math.round(bytes / 1e6)} MB`;
26905
+ if (bytes >= 1e3) return `${Math.round(bytes / 1e3)} KB`;
26906
+ return `${bytes} B`;
26907
+ }
26908
+ function attachmentsLine(outcomes) {
26909
+ const landed = outcomes.filter((o) => o.error === void 0);
26910
+ const failed = outcomes.filter((o) => o.error !== void 0);
26911
+ const parts = [];
26912
+ if (landed.length > 0) {
26913
+ parts.push(
26914
+ `Attached files are in ./${ATTACHMENTS_DIR}/: ${landed.map(
26915
+ (o) => `${o.basename} (${formatSize(o.file.size)}, ${o.file.type}${o.earlier ? ", from earlier in the thread" : ""})`
26916
+ ).join(", ")}`
26917
+ );
26918
+ }
26919
+ for (const o of failed) parts.push(`${o.file.name} could not be staged: ${o.error}`);
26920
+ return parts.join(". ");
26921
+ }
26922
+ function noWorkspaceLine(files) {
26923
+ return files.map(
26924
+ (f) => `this agent has no workspace for ${f.name} (${formatSize(f.size)}, ${f.type}); ask \`agent:coding\` to work with it`
26925
+ ).join(". ");
26926
+ }
26927
+ function stagingIndex() {
26928
+ let n2 = 0;
26929
+ return () => ++n2;
26930
+ }
26931
+ async function copyStaged(files, deps) {
26932
+ return Promise.all(
26933
+ files.map(async (file) => {
26934
+ const index = deps.nextIndex();
26935
+ const basename4 = stagedBasename(index, file.name);
26936
+ const key = inboundKey(deps.threadKey, file.messageId, index, file.name);
26937
+ try {
26938
+ await deps.store.copyFromUrl({ url: file.url, size: file.size, key });
26939
+ deps.publish?.({
26940
+ type: "artifact",
26941
+ direction: "in",
26942
+ key,
26943
+ name: file.name,
26944
+ size: file.size,
26945
+ contentType: file.type
26946
+ });
26947
+ return { file, basename: basename4, key };
26948
+ } catch (err2) {
26949
+ return { file, basename: basename4, key, error: `the copy into the store failed: ${describe4(err2)}` };
26950
+ }
26951
+ })
26952
+ );
26953
+ }
26954
+ async function pullStaged(outcomes, deps) {
26955
+ const landed = outcomes.filter((o) => o.error === void 0);
26956
+ if (landed.length === 0) return [...outcomes];
26957
+ const timeoutMs = deps.timeoutMs ?? BASH_TIMEOUT_MAX_MS;
26958
+ const opts = { timeoutMs, ...deps.signal ? { signal: deps.signal } : {} };
26959
+ if (deps.resident) await deps.executor.exec(excludeCommand(), opts);
26960
+ const pulled = /* @__PURE__ */ new Map();
26961
+ for (const o of landed) {
26962
+ const url = await deps.store.presignGet(o.key);
26963
+ const out = await deps.executor.exec(pullCommandFor(url, o.basename), opts);
26964
+ pulled.set(o.key, parseExitPrefix(out).failed ? `the pull into the workspace failed: ${out.trim()}` : void 0);
26965
+ }
26966
+ return outcomes.map((o) => {
26967
+ const error = o.error ?? pulled.get(o.key);
26968
+ if (error !== void 0) return { ...o, error };
26969
+ const { error: _dropped, ...landedOutcome } = o;
26970
+ return landedOutcome;
26971
+ });
26972
+ }
26973
+ function stageThreadArtifacts(artifacts, deps) {
26974
+ const outcomes = [];
26975
+ for (const a of artifacts) {
26976
+ const file = { name: a.name, size: a.size, type: a.contentType };
26977
+ if (a.held === void 0) {
26978
+ outcomes.push({
26979
+ file,
26980
+ basename: safeBasename(a.name),
26981
+ key: a.key,
26982
+ earlier: true,
26983
+ error: "the store could not be asked whether it still holds it"
26984
+ });
26985
+ continue;
26986
+ }
26987
+ if (!a.held) {
26988
+ outcomes.push({
26989
+ file,
26990
+ basename: safeBasename(a.name),
26991
+ key: a.key,
26992
+ earlier: true,
26993
+ error: "the store no longer holds it (its retention passed)"
26994
+ });
26995
+ continue;
26996
+ }
26997
+ const basename4 = stagedBasename(deps.nextIndex(), a.name);
26998
+ deps.publish?.({
26999
+ type: "artifact",
27000
+ direction: "in",
27001
+ key: a.key,
27002
+ name: a.name,
27003
+ size: a.size,
27004
+ contentType: a.contentType
27005
+ });
27006
+ outcomes.push({ file, basename: basename4, key: a.key, earlier: true });
27007
+ }
27008
+ return outcomes;
27009
+ }
27010
+ async function stageIntoWorkspace(files, deps) {
27011
+ const outcomes = await pullStaged(await copyStaged(files, deps), deps);
27012
+ return { line: attachmentsLine(outcomes), outcomes };
27013
+ }
27014
+ var ATTACHMENTS_DIR, WorkspaceFiles, describe4;
27015
+ var init_staging = __esm({
27016
+ "../../src/core/dispatch/staging.ts"() {
27017
+ "use strict";
27018
+ init_keys();
27019
+ init_bashTimeout();
27020
+ init_shellQuote();
27021
+ init_runEvents();
27022
+ ATTACHMENTS_DIR = "attachments";
27023
+ WorkspaceFiles = class {
27024
+ paths = /* @__PURE__ */ new Map();
27025
+ /** Every outcome that landed: its key now names a workspace path. */
27026
+ record(outcomes) {
27027
+ for (const o of outcomes) if (o.error === void 0) this.paths.set(o.key, `${ATTACHMENTS_DIR}/${o.basename}`);
27028
+ }
27029
+ pathOf(key) {
27030
+ return this.paths.get(key);
27031
+ }
27032
+ };
27033
+ describe4 = (err2) => err2 instanceof Error ? err2.message : String(err2);
27034
+ }
27035
+ });
27036
+
27037
+ // ../../src/core/dispatch/threadAssets.ts
27038
+ function threadAssetsOf(runsOldestFirst) {
27039
+ const seen = /* @__PURE__ */ new Set();
27040
+ const out = [];
27041
+ for (const run2 of runsOldestFirst) {
27042
+ for (const e of run2.events) {
27043
+ if (e.type !== "artifact" || seen.has(e.key)) continue;
27044
+ seen.add(e.key);
27045
+ out.push({
27046
+ key: e.key,
27047
+ name: e.name,
27048
+ size: e.size,
27049
+ contentType: e.contentType,
27050
+ direction: e.direction,
27051
+ runId: run2.id,
27052
+ ...e.seq !== void 0 ? { seq: e.seq } : {}
27053
+ });
27054
+ }
27055
+ }
27056
+ return out;
27057
+ }
27058
+ async function readThreadAssets(deps, threadKey, warn = (line3) => console.warn(line3)) {
27059
+ const runs = [];
27060
+ let before;
27061
+ try {
27062
+ for (; ; ) {
27063
+ const page2 = await deps.runs.listRuns({
27064
+ status: "all",
27065
+ visibleTo: { kind: "all" },
27066
+ threadKey,
27067
+ limit: THREAD_ASSETS_PAGE,
27068
+ ...before !== void 0 ? { before: before.finishedAt, beforeId: before.id } : {}
27069
+ });
27070
+ runs.push(...page2.runs);
27071
+ if (page2.storeUnavailable) {
27072
+ warn(
27073
+ `[thread] ${threadKey}: listing its runs for their files answered live rows only after ${runs.length} run(s) \u2014 the run store was unavailable`
27074
+ );
27075
+ break;
27076
+ }
27077
+ if (page2.nextBefore === void 0) break;
27078
+ before = page2.nextBefore;
27079
+ }
27080
+ } catch (err2) {
27081
+ warn(
27082
+ `[thread] ${threadKey}: listing its runs for their files stopped after ${runs.length} run(s) \u2014 ${describe5(err2)}`
27083
+ );
27084
+ }
27085
+ const perRun = [];
27086
+ for (const run2 of runs.reverse()) {
27087
+ const events = [];
27088
+ let afterSeq;
27089
+ try {
27090
+ for (; ; ) {
27091
+ const r = await deps.runs.getRunEvents(run2.id, afterSeq === void 0 ? {} : { afterSeq });
27092
+ if (!r.ok) {
27093
+ warn(`[thread] ${run2.id}: reading its files stopped after ${events.length} event(s) \u2014 ${r.error}`);
27094
+ break;
27095
+ }
27096
+ events.push(...r.value.events);
27097
+ if (r.value.nextAfterSeq === void 0) break;
27098
+ afterSeq = r.value.nextAfterSeq;
27099
+ }
27100
+ } catch (err2) {
27101
+ warn(`[thread] ${run2.id}: reading its files failed after ${events.length} event(s) \u2014 ${describe5(err2)}`);
27102
+ }
27103
+ perRun.push({ id: run2.id, events });
27104
+ }
27105
+ return Promise.all(
27106
+ threadAssetsOf(perRun).map(async (asset) => {
27107
+ try {
27108
+ return { ...asset, held: await deps.store.head(asset.key) !== null };
27109
+ } catch (err2) {
27110
+ warn(`[thread] ${asset.key}: the store could not be asked whether it holds it \u2014 ${describe5(err2)}`);
27111
+ return asset;
27112
+ }
27113
+ })
27114
+ );
27115
+ }
27116
+ function describeAsset(asset, path) {
27117
+ const what = `${asset.name} (${formatSize(asset.size)}, ${asset.contentType})`;
27118
+ const origin = asset.direction === "in" ? "received on this thread" : `produced by run ${asset.runId}`;
27119
+ return `${what} \u2014 ${origin}; ${whereIs(asset, path)}`;
27120
+ }
27121
+ function whereIs(asset, path) {
27122
+ if (path !== void 0) return `in this workspace at ./${path}`;
27123
+ if (asset.held === false) return "no longer in the store (its retention passed)";
27124
+ if (asset.held === void 0) return "the store could not be asked whether it still holds it";
27125
+ return asset.direction === "out" ? "on that run's page, not in this workspace" : "in the store, not in this workspace";
27126
+ }
27127
+ var THREAD_ASSETS_PAGE, describe5;
27128
+ var init_threadAssets = __esm({
27129
+ "../../src/core/dispatch/threadAssets.ts"() {
27130
+ "use strict";
27131
+ init_staging();
27132
+ THREAD_ASSETS_PAGE = 50;
27133
+ describe5 = (err2) => err2 instanceof Error ? err2.message : String(err2);
27134
+ }
27135
+ });
27136
+
26789
27137
  // ../../src/core/runLedger/sessionLog.ts
26790
27138
  function sessionKey(threadKey, agent) {
26791
27139
  return `${threadKey}:${agent ?? "-"}`;
@@ -26807,31 +27155,50 @@ var init_sessionLog = __esm({
26807
27155
  });
26808
27156
 
26809
27157
  // ../../src/tools/session.ts
26810
- function sessionCapabilityFor(run2, ledger) {
27158
+ function sessionCapabilityFor(run2, ledger, assets) {
26811
27159
  const session = run2?.session;
26812
27160
  if (!session) return void 0;
26813
27161
  return {
26814
27162
  session,
26815
27163
  search: (query, limit) => ledger.searchSession(session.key, query, limit),
26816
27164
  readTurn: async (idx) => (await ledger.readSession(session.key, idx, idx)).messages[0],
27165
+ readConversation: async () => (await ledger.readSession(session.key, session.seedFrom)).messages,
26817
27166
  readNotepad: () => ledger.readNotepad(session.key),
26818
- writeNotepad: (text) => ledger.writeNotepad(session.key, text)
27167
+ writeNotepad: (text) => ledger.writeNotepad(session.key, text),
27168
+ ...assets ? { assets: assets.read, workspacePathOf: assets.pathOf } : {}
26819
27169
  };
26820
27170
  }
27171
+ function assetView(asset, path) {
27172
+ return {
27173
+ name: asset.name,
27174
+ size: asset.size,
27175
+ type: asset.contentType,
27176
+ [asset.direction === "in" ? "received" : "produced"]: `run ${asset.runId}`,
27177
+ key: asset.key,
27178
+ where: whereIs(asset, path)
27179
+ };
27180
+ }
27181
+ function filesNamedIn(text, catalogue) {
27182
+ return catalogue.filter((a) => [a.name, safeBasename(a.name)].some((n2) => nameAt2(n2).test(text)));
27183
+ }
26821
27184
  async function mayRead(ctx) {
26822
27185
  if (!ctx.runs?.runId) return false;
26823
27186
  const res = await ctx.runs.service.getRun(ctx.runs.runId);
26824
27187
  return res.ok && authorize(ctx.runs.actor, "runs:read", runResource(res.value)).allow;
26825
27188
  }
26826
- var UNAVAILABLE3, DEFAULT_HITS, MAX_HITS, SNIPPET_CHARS, snippetOf, recallTool, notesTool, SESSION_TOOLS;
27189
+ var FILE_BEARING, nameAt2, UNAVAILABLE3, DEFAULT_HITS, MAX_HITS, SNIPPET_CHARS, snippetOf, recallTool, notesTool, SESSION_TOOLS;
26827
27190
  var init_session = __esm({
26828
27191
  "../../src/tools/session.ts"() {
26829
27192
  "use strict";
27193
+ init_keys();
26830
27194
  init_authorize();
27195
+ init_threadAssets();
26831
27196
  init_runEvents();
26832
27197
  init_sessionLog();
26833
27198
  init_runRecord();
26834
27199
  init_runsService();
27200
+ FILE_BEARING = /attachments\/|attach_file|attached files/i;
27201
+ nameAt2 = (name) => new RegExp(`(?<![A-Za-z0-9._])${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![A-Za-z0-9._-])`);
26835
27202
  UNAVAILABLE3 = "the session tools are not available in this context: this run has no session log.";
26836
27203
  DEFAULT_HITS = 5;
26837
27204
  MAX_HITS = 50;
@@ -26842,7 +27209,7 @@ var init_session = __esm({
26842
27209
  };
26843
27210
  recallTool = {
26844
27211
  name: "recall",
26845
- 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).",
27212
+ 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.",
26846
27213
  inputSchema: {
26847
27214
  type: "object",
26848
27215
  properties: {
@@ -26851,6 +27218,10 @@ var init_session = __esm({
26851
27218
  turn: {
26852
27219
  type: "integer",
26853
27220
  description: "A turn number from an earlier recall: returns that turn whole instead of searching"
27221
+ },
27222
+ assets: {
27223
+ type: "boolean",
27224
+ 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"
26854
27225
  }
26855
27226
  }
26856
27227
  },
@@ -26859,13 +27230,30 @@ var init_session = __esm({
26859
27230
  if (!ctx.session) return UNAVAILABLE3;
26860
27231
  const turn = typeof input.turn === "number" ? Math.trunc(input.turn) : void 0;
26861
27232
  const query = typeof input.query === "string" ? input.query.trim() : "";
26862
- if (turn === void 0 && query.length === 0) return "recall needs a query or a turn number.";
26863
- if (!await mayRead(ctx)) return JSON.stringify(turn !== void 0 ? { turn, content: null } : { hits: [] });
27233
+ const listAssets = input.assets === true;
27234
+ if (turn === void 0 && query.length === 0 && !listAssets)
27235
+ return "recall needs a query, a turn number or assets: true.";
27236
+ if (!await mayRead(ctx)) {
27237
+ return JSON.stringify(turn !== void 0 ? { turn, content: null } : listAssets ? { assets: [] } : { hits: [] });
27238
+ }
26864
27239
  if (turn !== void 0) {
26865
27240
  const message3 = turn >= 0 ? await ctx.session.readTurn(turn) : void 0;
26866
27241
  if (!message3) return `no turn ${turn} in this session's log.`;
26867
27242
  return JSON.stringify({ turn, role: message3.role, content: message3.content });
26868
27243
  }
27244
+ if (listAssets) {
27245
+ if (!ctx.session.assets) {
27246
+ return JSON.stringify({
27247
+ assets: [],
27248
+ note: "this deployment has no artifact store: the thread's files are not catalogued"
27249
+ });
27250
+ }
27251
+ const assets = await ctx.session.assets();
27252
+ return JSON.stringify({
27253
+ assets: assets.map((a) => assetView(a, ctx.session.workspacePathOf?.(a.key))),
27254
+ ...assets.length === 0 ? { note: "no run of this thread has received or produced a file" } : {}
27255
+ });
27256
+ }
26869
27257
  const limit = Math.min(
26870
27258
  MAX_HITS,
26871
27259
  Math.max(1, typeof input.limit === "number" ? Math.trunc(input.limit) : DEFAULT_HITS)
@@ -26875,12 +27263,17 @@ var init_session = __esm({
26875
27263
  if (hits.length === 0)
26876
27264
  notes.push("no turn of this session's log matches; your notes (notes {}) may hold what you look for");
26877
27265
  if (ctx.session.session.seedFrom === 0) notes.push("this is the first run of its session: the log begins with it");
27266
+ const catalogue = ctx.session.assets && hits.some((h) => FILE_BEARING.test(h.text)) ? await ctx.session.assets() : [];
26878
27267
  return JSON.stringify({
26879
- hits: hits.map((h) => ({
26880
- turn: h.idx,
26881
- ...h.role !== void 0 ? { role: h.role } : {},
26882
- snippet: snippetOf(h.text)
26883
- })),
27268
+ hits: hits.map((h) => {
27269
+ const files = filesNamedIn(h.text, catalogue);
27270
+ return {
27271
+ turn: h.idx,
27272
+ ...h.role !== void 0 ? { role: h.role } : {},
27273
+ snippet: snippetOf(h.text),
27274
+ ...files.length > 0 ? { files: files.map((a) => assetView(a, ctx.session.workspacePathOf?.(a.key))) } : {}
27275
+ };
27276
+ }),
26884
27277
  ...gaps.length ? {
26885
27278
  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`
26886
27279
  } : {},
@@ -27545,7 +27938,11 @@ function imperativeRule(writers) {
27545
27938
  }
27546
27939
  function attachRule(attachers) {
27547
27940
  const names = attachers.map((a) => `\`${a}\``).join(" or ");
27548
- 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.`;
27941
+ return [
27942
+ `Only ${names} can attach or post a file into the thread (the \`attach_file\` tool).`,
27943
+ `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.`,
27944
+ "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."
27945
+ ].join(" ");
27549
27946
  }
27550
27947
  function tidyReason(reason) {
27551
27948
  const line3 = oneLine(redactAndCap(reason, ROUTE_REASON_CAP));
@@ -28613,7 +29010,8 @@ async function attachRoundWorkspace(input) {
28613
29010
  profile,
28614
29011
  repo: input.round.repo,
28615
29012
  ref: input.round.ref,
28616
- headSha: input.round.headSha
29013
+ headSha: input.round.headSha,
29014
+ ...input.round.reattach !== void 0 ? { reattach: input.round.reattach } : {}
28617
29015
  },
28618
29016
  input.span
28619
29017
  );
@@ -29240,157 +29638,6 @@ var init_messages = __esm({
29240
29638
  }
29241
29639
  });
29242
29640
 
29243
- // ../../src/core/dispatch/staging.ts
29244
- function stagedBasename(index, name) {
29245
- return `${index}-${safeBasename(name)}`;
29246
- }
29247
- function pullCommandFor(url, basename4) {
29248
- return `mkdir -p ${ATTACHMENTS_DIR} && curl -fsS -o ${shellQuote(`${ATTACHMENTS_DIR}/${basename4}`)} ${shellQuote(url)}`;
29249
- }
29250
- function excludeCommand() {
29251
- const entry = shellQuote(`${ATTACHMENTS_DIR}/`);
29252
- return `mkdir -p .git/info && { grep -qxF ${entry} .git/info/exclude 2>/dev/null || echo ${entry} >> .git/info/exclude; }`;
29253
- }
29254
- function formatSize(bytes) {
29255
- if (bytes >= 1e9) return `${(bytes / 1e9).toFixed(1)} GB`;
29256
- if (bytes >= 1e6) return `${Math.round(bytes / 1e6)} MB`;
29257
- if (bytes >= 1e3) return `${Math.round(bytes / 1e3)} KB`;
29258
- return `${bytes} B`;
29259
- }
29260
- function attachmentsLine(outcomes) {
29261
- const landed = outcomes.filter((o) => o.error === void 0);
29262
- const failed = outcomes.filter((o) => o.error !== void 0);
29263
- const parts = [];
29264
- if (landed.length > 0) {
29265
- parts.push(
29266
- `Attached files are in ./${ATTACHMENTS_DIR}/: ${landed.map(
29267
- (o) => `${o.basename} (${formatSize(o.file.size)}, ${o.file.type}${o.earlier ? ", from earlier in the thread" : ""})`
29268
- ).join(", ")}`
29269
- );
29270
- }
29271
- for (const o of failed) parts.push(`${o.file.name} could not be staged: ${o.error}`);
29272
- return parts.join(". ");
29273
- }
29274
- function noWorkspaceLine(files) {
29275
- return files.map(
29276
- (f) => `this agent has no workspace for ${f.name} (${formatSize(f.size)}, ${f.type}); ask \`agent:coding\` to work with it`
29277
- ).join(". ");
29278
- }
29279
- function stagingIndex() {
29280
- let n2 = 0;
29281
- return () => ++n2;
29282
- }
29283
- async function copyStaged(files, deps) {
29284
- return Promise.all(
29285
- files.map(async (file) => {
29286
- const index = deps.nextIndex();
29287
- const basename4 = stagedBasename(index, file.name);
29288
- const key = inboundKey(deps.threadKey, file.messageId, index, file.name);
29289
- try {
29290
- await deps.store.copyFromUrl({ url: file.url, size: file.size, key });
29291
- deps.publish?.({
29292
- type: "artifact",
29293
- direction: "in",
29294
- key,
29295
- name: file.name,
29296
- size: file.size,
29297
- contentType: file.type
29298
- });
29299
- return { file, basename: basename4, key };
29300
- } catch (err2) {
29301
- return { file, basename: basename4, key, error: `the copy into the store failed: ${describe4(err2)}` };
29302
- }
29303
- })
29304
- );
29305
- }
29306
- async function pullStaged(outcomes, deps) {
29307
- const landed = outcomes.filter((o) => o.error === void 0);
29308
- if (landed.length === 0) return [...outcomes];
29309
- const timeoutMs = deps.timeoutMs ?? BASH_TIMEOUT_MAX_MS;
29310
- const opts = { timeoutMs, ...deps.signal ? { signal: deps.signal } : {} };
29311
- if (deps.resident) await deps.executor.exec(excludeCommand(), opts);
29312
- const pulled = /* @__PURE__ */ new Map();
29313
- for (const o of landed) {
29314
- const url = await deps.store.presignGet(o.key);
29315
- const out = await deps.executor.exec(pullCommandFor(url, o.basename), opts);
29316
- pulled.set(o.key, parseExitPrefix(out).failed ? `the pull into the workspace failed: ${out.trim()}` : void 0);
29317
- }
29318
- return outcomes.map((o) => {
29319
- const error = o.error ?? pulled.get(o.key);
29320
- if (error !== void 0) return { ...o, error };
29321
- const { error: _dropped, ...landedOutcome } = o;
29322
- return landedOutcome;
29323
- });
29324
- }
29325
- function threadInboundArtifacts(runsOldestFirst) {
29326
- const seen = /* @__PURE__ */ new Set();
29327
- const out = [];
29328
- for (const events of runsOldestFirst) {
29329
- for (const e of events) {
29330
- if (e.type !== "artifact" || e.direction !== "in" || seen.has(e.key)) continue;
29331
- seen.add(e.key);
29332
- out.push({ key: e.key, name: e.name, size: e.size, contentType: e.contentType });
29333
- }
29334
- }
29335
- return out;
29336
- }
29337
- async function stageThreadArtifacts(artifacts, deps) {
29338
- const outcomes = [];
29339
- for (const a of artifacts) {
29340
- const file = { name: a.name, size: a.size, type: a.contentType };
29341
- let held;
29342
- try {
29343
- held = await deps.store.head(a.key) !== null;
29344
- } catch (err2) {
29345
- outcomes.push({
29346
- file,
29347
- basename: safeBasename(a.name),
29348
- key: a.key,
29349
- earlier: true,
29350
- error: `the store could not be asked: ${describe4(err2)}`
29351
- });
29352
- continue;
29353
- }
29354
- if (!held) {
29355
- outcomes.push({
29356
- file,
29357
- basename: safeBasename(a.name),
29358
- key: a.key,
29359
- earlier: true,
29360
- error: "the store no longer holds it (its retention passed)"
29361
- });
29362
- continue;
29363
- }
29364
- const basename4 = stagedBasename(deps.nextIndex(), a.name);
29365
- deps.publish?.({
29366
- type: "artifact",
29367
- direction: "in",
29368
- key: a.key,
29369
- name: a.name,
29370
- size: a.size,
29371
- contentType: a.contentType
29372
- });
29373
- outcomes.push({ file, basename: basename4, key: a.key, earlier: true });
29374
- }
29375
- return outcomes;
29376
- }
29377
- async function stageIntoWorkspace(files, deps) {
29378
- const outcomes = await pullStaged(await copyStaged(files, deps), deps);
29379
- return { line: attachmentsLine(outcomes), outcomes };
29380
- }
29381
- var ATTACHMENTS_DIR, describe4;
29382
- var init_staging = __esm({
29383
- "../../src/core/dispatch/staging.ts"() {
29384
- "use strict";
29385
- init_keys();
29386
- init_bashTimeout();
29387
- init_shellQuote();
29388
- init_runEvents();
29389
- ATTACHMENTS_DIR = "attachments";
29390
- describe4 = (err2) => err2 instanceof Error ? err2.message : String(err2);
29391
- }
29392
- });
29393
-
29394
29641
  // ../../src/core/modelProxy/runBearers.ts
29395
29642
  import { createHash as createHash4, randomBytes as randomBytes4, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
29396
29643
  function token(runId3, secret2) {
@@ -30093,7 +30340,7 @@ async function reserveRun(deps, ctx) {
30093
30340
  return void 0;
30094
30341
  }
30095
30342
  async function attachWorkspace(deps, ctx) {
30096
- const { msg, io, refuse: refuse3, card, shell, closeLines, clock: clock3, agent, profile, repoCtx, root } = ctx;
30343
+ const { msg, io, refuse: refuse3, card, shell, closeLines, clock: clock3, agent, profile, repoCtx, root, reattach } = ctx;
30097
30344
  let round2;
30098
30345
  try {
30099
30346
  round2 = await root.span("dispatch.workspace.attach", async (span) => {
@@ -30118,7 +30365,8 @@ async function attachWorkspace(deps, ctx) {
30118
30365
  profile,
30119
30366
  repo: repoCtx.repo,
30120
30367
  ref: repoCtx.ref,
30121
- headSha: repoCtx.headSha
30368
+ headSha: repoCtx.headSha,
30369
+ ...reattach !== void 0 ? { reattach } : {}
30122
30370
  },
30123
30371
  logKey: msg.threadKey,
30124
30372
  span
@@ -30145,6 +30393,7 @@ async function attachWorkspace(deps, ctx) {
30145
30393
  });
30146
30394
  return { kind: "refused", reason: "which_branch" };
30147
30395
  }
30396
+ if (err2 instanceof WorkspaceReattachRefusedError) return { kind: "reattach_refused", why: err2.why };
30148
30397
  throw err2;
30149
30398
  }
30150
30399
  return { kind: "attached", round: round2 };
@@ -30181,6 +30430,10 @@ function sessionNotesBlock(session) {
30181
30430
  parts.push(
30182
30431
  "SUMMARY OF THE EARLIER CONVERSATION (written when the context was compacted; every earlier turn is still reachable with `recall`):\n" + summary
30183
30432
  );
30433
+ if (session.files && session.files.length > 0)
30434
+ parts.push(
30435
+ "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")
30436
+ );
30184
30437
  return parts.length ? parts.join("\n\n") : void 0;
30185
30438
  }
30186
30439
  async function composePrompt(deps, ctx) {
@@ -30280,6 +30533,7 @@ var init_provision = __esm({
30280
30533
  init_contract3();
30281
30534
  init_registry();
30282
30535
  init_profile();
30536
+ init_factory();
30283
30537
  init_residentTrace();
30284
30538
  init_resident();
30285
30539
  init_memory();
@@ -30333,6 +30587,7 @@ async function claimRun(deps, ctx) {
30333
30587
  } = ctx;
30334
30588
  const { resident, binding } = selection;
30335
30589
  let ledgerRun = ctx.ledgerRun;
30590
+ const workspaceBinding = workspaceBindingFor(selection, profile.machine);
30336
30591
  if (!resume && reserved) {
30337
30592
  const ledger = deps.runLedger;
30338
30593
  const opened = await root.span(
@@ -30366,6 +30621,7 @@ async function claimRun(deps, ctx) {
30366
30621
  ...requestRow !== void 0 ? { request: requestRow } : {}
30367
30622
  },
30368
30623
  card: card.handle ?? null,
30624
+ ...workspaceBinding !== void 0 ? { state: { binding: workspaceBinding } } : {},
30369
30625
  // The row reserved before the attach (item 42), promoted in place;
30370
30626
  // its hooks (a stop, a fence) were wired at the reservation and stay.
30371
30627
  reservation: reserved,
@@ -30422,6 +30678,7 @@ var init_run2 = __esm({
30422
30678
  init_workspace();
30423
30679
  init_web();
30424
30680
  init_githubApi();
30681
+ init_factory();
30425
30682
  init_runRegistry();
30426
30683
  init_secrets2();
30427
30684
  webCapability = () => sharedWeb ??= makeWebCapability(processSecrets);
@@ -30454,6 +30711,17 @@ const URL_ENV = "SWITCHBOARD_HARNESS_URL";
30454
30711
  /** How long the tool_call hook keeps asking an unreachable bot before it blocks. */
30455
30712
  const AUTHORIZE_WAIT_MS = 90_000;
30456
30713
  const AUTHORIZE_RETRY_MS = 2_000;
30714
+ /** How long one request for a relayed tool's result may take before it is asked
30715
+ * again: over the bot's window (30 s, after which it answers that the call is
30716
+ * still running), under this runtime's own five-minute header timeout on a
30717
+ * fetch, so a hung connection costs a minute of the wait, not five. */
30718
+ const TOOL_REQUEST_TIMEOUT_MS = 60_000;
30719
+ /** How long a relayed call keeps asking a bot it cannot reach before it fails; the cadence is the hook's. */
30720
+ const TOOL_WAIT_MS = 90_000;
30721
+ /** A bot that says a call is still running is asked again after the window it
30722
+ * held the request for; one that answered sooner is asked again no sooner
30723
+ * than this, so a misbehaving bot is never asked in a hot loop. */
30724
+ const TOOL_REASK_FLOOR_MS = 1_000;
30457
30725
 
30458
30726
  function settings() {
30459
30727
  const url = process.env[URL_ENV];
@@ -30527,6 +30795,49 @@ async function authorize(event) {
30527
30795
  }
30528
30796
  }
30529
30797
 
30798
+ /** pi's abort signal for the call, with this request's own timeout beside it
30799
+ * where the runtime can combine the two; pi's alone where it cannot. */
30800
+ function requestSignal(signal) {
30801
+ const timeout = typeof AbortSignal.timeout === "function" ? AbortSignal.timeout(TOOL_REQUEST_TIMEOUT_MS) : undefined;
30802
+ if (!timeout) return signal;
30803
+ if (!signal) return timeout;
30804
+ return typeof AbortSignal.any === "function" ? AbortSignal.any([signal, timeout]) : signal;
30805
+ }
30806
+
30807
+ /** A relayed tool's result. The call is posted with its id; a bot that answers
30808
+ * that the call is still running (a wait on other runs can take minutes) is
30809
+ * asked again with the same id until it answers: the bot runs the call once
30810
+ * and every later ask joins it. A bot that cannot be reached or is failing (a
30811
+ * network error, a timed-out request, a 5xx) is asked again every two seconds
30812
+ * until the wait is up, then the call fails naming why; a refusal at the door
30813
+ * (a 4xx) fails it at once; an abort from pi ends the asking. */
30814
+ async function relay(toolCallId, tool, input, signal) {
30815
+ let lastAnswered = Date.now();
30816
+ let lastError = "";
30817
+ for (;;) {
30818
+ if (signal && signal.aborted) throw new Error("switchboard harness: " + tool + " was aborted before the bot answered");
30819
+ const asked = Date.now();
30820
+ let answer;
30821
+ try {
30822
+ answer = await call("POST", "/harness/tool", { toolCallId, tool, input }, requestSignal(signal));
30823
+ } catch (err) {
30824
+ if (signal && signal.aborted) throw err;
30825
+ const status = err instanceof HarnessAnswerError ? err.status : undefined;
30826
+ if (status !== undefined && status >= 400 && status < 500) throw err;
30827
+ lastError = err instanceof Error ? err.message : String(err);
30828
+ if (Date.now() - lastAnswered >= TOOL_WAIT_MS) {
30829
+ throw new Error("switchboard harness: the bot did not answer " + tool + " for 90 s (" + lastError + ")");
30830
+ }
30831
+ await sleep(AUTHORIZE_RETRY_MS);
30832
+ continue;
30833
+ }
30834
+ lastAnswered = Date.now();
30835
+ if (!answer.pending) return answer;
30836
+ const held = Date.now() - asked;
30837
+ if (held < TOOL_REASK_FLOOR_MS) await sleep(TOOL_REASK_FLOOR_MS - held);
30838
+ }
30839
+ }
30840
+
30530
30841
  function relayTool(def) {
30531
30842
  return {
30532
30843
  name: def.name,
@@ -30534,7 +30845,7 @@ function relayTool(def) {
30534
30845
  description: def.description,
30535
30846
  parameters: def.inputSchema,
30536
30847
  async execute(toolCallId, params, signal) {
30537
- const answer = await call("POST", "/harness/tool", { toolCallId, tool: def.name, input: params }, signal);
30848
+ const answer = await relay(toolCallId, def.name, params, signal);
30538
30849
  if (answer.isError) throw new Error(answer.content.map((c) => (c.type === "text" ? c.text : "")).join("\\n"));
30539
30850
  return { content: answer.content, details: {} };
30540
30851
  },
@@ -30733,6 +31044,9 @@ function readLogScript(path, offset, maxBytes) {
30733
31044
  function aliveScript(pid) {
30734
31045
  return `kill -0 ${pid} 2>/dev/null && echo alive || echo dead`;
30735
31046
  }
31047
+ function identityScript() {
31048
+ return "cat /proc/sys/kernel/random/boot_id 2>/dev/null || true";
31049
+ }
30736
31050
  function killScript(pid) {
30737
31051
  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`;
30738
31052
  }
@@ -30749,7 +31063,7 @@ function stdoutOf(operation, out) {
30749
31063
  const stdout = cut >= 0 ? out.slice(0, cut) : out;
30750
31064
  return stdout === "(no output)" ? "" : stdout;
30751
31065
  }
30752
- var PiContainerError, WRITE_CHUNK_CHARS, INLINE_LINE_CHARS, LOG_READ_BYTES, MESSAGE_UPDATE_MARK, LOG_FILTER, STDERR_MARK, OP_TIMEOUT_MS, ExecPiContainer;
31066
+ var PiContainerError, WRITE_CHUNK_CHARS, INLINE_LINE_CHARS, LOG_READ_BYTES, MESSAGE_UPDATE_MARK, LOG_FILTER, IDENTITY_WORD, STDERR_MARK, OP_TIMEOUT_MS, ExecPiContainer;
30753
31067
  var init_container = __esm({
30754
31068
  "../../src/core/harness/pi/container.ts"() {
30755
31069
  "use strict";
@@ -30769,6 +31083,7 @@ var init_container = __esm({
30769
31083
  LOG_READ_BYTES = 48 * 1024;
30770
31084
  MESSAGE_UPDATE_MARK = '"type":"message_update"';
30771
31085
  LOG_FILTER = `grep --line-buffered -v ${shellQuote(MESSAGE_UPDATE_MARK)}`;
31086
+ IDENTITY_WORD = /^[A-Za-z0-9-]{1,64}$/;
30772
31087
  STDERR_MARK = "\n--- stderr ---\n";
30773
31088
  OP_TIMEOUT_MS = 6e4;
30774
31089
  ExecPiContainer = class {
@@ -30782,6 +31097,12 @@ var init_container = __esm({
30782
31097
  async makeRoot(runId3) {
30783
31098
  return piRunPaths(runId3);
30784
31099
  }
31100
+ /** The start script never changes directory, so pi runs where the executor
31101
+ * runs every command: the thread's checkout, which the harness names (a
31102
+ * resident's `/exec` runs in the thread's worktree). No command runs here. */
31103
+ cwd(_paths, checkout) {
31104
+ return checkout;
31105
+ }
30785
31106
  async writeFile(path, content) {
30786
31107
  for (const script of writeFileScripts(path, content)) stdoutOf("write", await this.exec(script));
30787
31108
  }
@@ -30807,6 +31128,16 @@ var init_container = __esm({
30807
31128
  async alive(pid) {
30808
31129
  return stdoutOf("alive", await this.exec(aliveScript(pid))).trim() === "alive";
30809
31130
  }
31131
+ /** One word or nothing: an empty answer, a malformed one or a command the
31132
+ * executor could not run is no identity: a judgement never rests on a guess. */
31133
+ async identity() {
31134
+ try {
31135
+ const word = stdoutOf("identity", await this.exec(identityScript())).trim();
31136
+ return IDENTITY_WORD.test(word) ? word : void 0;
31137
+ } catch {
31138
+ return void 0;
31139
+ }
31140
+ }
30810
31141
  async kill(pid) {
30811
31142
  stdoutOf("kill", await this.exec(killScript(pid)));
30812
31143
  }
@@ -30938,6 +31269,14 @@ var init_botHostContainer = __esm({
30938
31269
  throw new PiContainerError("root", message2(err2));
30939
31270
  }
30940
31271
  }
31272
+ /** The run's own root: `start` spawns pi in it, so that is the directory a
31273
+ * session written for this root names, present on this host in this
31274
+ * generation. The checkout the harness names is a container path this host
31275
+ * does not have (a `none` run has no workspace), and the previous
31276
+ * generation's root is gone with its pi: neither is where pi runs. */
31277
+ cwd(paths, _checkout) {
31278
+ return paths.dir;
31279
+ }
30941
31280
  /** Parents at 700, the file at 600, exact bytes, created and never
30942
31281
  * replaced: under a root this container made nothing is there before the
30943
31282
  * harness writes it, so a path already present is a named failure, never
@@ -31058,6 +31397,12 @@ var init_botHostContainer = __esm({
31058
31397
  const child = this.byPid.get(pid);
31059
31398
  return child !== void 0 && !child.exited;
31060
31399
  }
31400
+ /** The bot host names no container: its pi is a child of one bot generation
31401
+ * and never outlives it, so a resume judges it by its pid alone (`alive`
31402
+ * answers no for a pid this container did not start). */
31403
+ async identity() {
31404
+ return void 0;
31405
+ }
31061
31406
  /** The group first (pi leads it), then pi itself: TERM, the grace, KILL;
31062
31407
  * a pid this container never started, or one already ended, is nothing to end. */
31063
31408
  async kill(pid) {
@@ -31220,8 +31565,13 @@ var init_bridge2 = __esm({
31220
31565
  break;
31221
31566
  case "message_end":
31222
31567
  if (isRecord3(event.message)) {
31223
- out.message = event.message;
31224
- if (event.message.role === "assistant") this.onAssistant(event.message, out);
31568
+ if (event.message.role === "assistant") {
31569
+ const assistant = event.message;
31570
+ this.onAssistant(assistant, out);
31571
+ if (assistant.stopReason !== "error") out.message = event.message;
31572
+ } else {
31573
+ out.message = event.message;
31574
+ }
31225
31575
  }
31226
31576
  break;
31227
31577
  case "tool_execution_start":
@@ -31383,6 +31733,11 @@ var init_bridge2 = __esm({
31383
31733
  openSpan(callId) {
31384
31734
  return this.openTools.get(callId)?.span;
31385
31735
  }
31736
+ /** Whether the bridge has read the call's start off the log and not yet its
31737
+ * end: what a relayed request that arrived ahead of the poll waits for. */
31738
+ callOpen(callId) {
31739
+ return this.openTools.has(callId);
31740
+ }
31386
31741
  /** End whatever tool spans a stopped pi left open, so no span outlives the run. */
31387
31742
  closeOpenSpans(reason) {
31388
31743
  for (const [callId, open2] of this.openTools) {
@@ -31562,6 +31917,7 @@ var init_mirror = __esm({
31562
31917
  if (!this.deps.onStep) return;
31563
31918
  const chat = chatMessageOf(message3);
31564
31919
  if (!chat) return;
31920
+ if (chat.content.length === 0) return;
31565
31921
  if (chat.role === "user") {
31566
31922
  if (!this.seedEchoed && !chat.content.some((p) => p.type === "tool_result")) {
31567
31923
  this.seedEchoed = true;
@@ -31704,7 +32060,8 @@ function piHarnessFactsOf(value) {
31704
32060
  logOffset: v.logOffset,
31705
32061
  ...typeof v.sessionFile === "string" ? { sessionFile: v.sessionFile } : {},
31706
32062
  ...typeof v.bearerHash === "string" ? { bearerHash: v.bearerHash } : {},
31707
- ...typeof v.root === "string" ? { root: v.root } : {}
32063
+ ...typeof v.root === "string" ? { root: v.root } : {},
32064
+ ...typeof v.container === "string" ? { container: v.container } : {}
31708
32065
  };
31709
32066
  }
31710
32067
  function relayedTools(tools) {
@@ -31773,6 +32130,24 @@ async function runPiHarness(deps, run2) {
31773
32130
  const deadline = now() + remainingMs;
31774
32131
  const warnAt = deadline - Math.min(3 * 6e4, run2.agent.maxMinutes * 15e3);
31775
32132
  run2.toolContext.remainingMs = () => deadline - now();
32133
+ if (run2.conversation) {
32134
+ const readConversation = run2.conversation;
32135
+ run2.toolContext.conversation = async () => {
32136
+ try {
32137
+ return await readConversation();
32138
+ } catch (err2) {
32139
+ emit({
32140
+ type: "run_note",
32141
+ kind: "seed",
32142
+ summary: redactAndCap(
32143
+ `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`,
32144
+ 300
32145
+ )
32146
+ });
32147
+ return void 0;
32148
+ }
32149
+ };
32150
+ }
31776
32151
  bridge.turns = run2.resume?.turn ?? 0;
31777
32152
  const mirror = new PiMirror({
31778
32153
  onStep: run2.onStep,
@@ -31802,7 +32177,11 @@ async function runPiHarness(deps, run2) {
31802
32177
  emit,
31803
32178
  toolSpan: (callId) => bridge.openSpan(callId),
31804
32179
  gateSaw: (callId) => bridge.gateSaw(callId),
31805
- toolsBlocked
32180
+ toolsBlocked,
32181
+ callSeen: async (callId) => {
32182
+ for (let waited = 0; !bridge.callOpen(callId) && waited < CALL_SEEN_WAIT_MS; waited += CALL_SEEN_TICK_MS)
32183
+ await deps.sleep(CALL_SEEN_TICK_MS);
32184
+ }
31806
32185
  };
31807
32186
  const forget = deps.registry.register(live);
31808
32187
  let pid;
@@ -31819,21 +32198,27 @@ async function runPiHarness(deps, run2) {
31819
32198
  const recorded = run2.resume?.facts;
31820
32199
  let reattached = false;
31821
32200
  let ended;
32201
+ let elsewhere;
32202
+ const here = await container.identity();
31822
32203
  const honoured = (hash) => hash !== void 0 && (deps.bearers === void 0 || deps.bearers.adopt(run2.runId, hash));
31823
32204
  if (recorded !== void 0) {
31824
- const alive = await container.alive(recorded.pid);
31825
- if (alive && recorded.root !== void 0 && honoured(recorded.bearerHash)) {
31826
- reattached = true;
31827
- paths = piRunPathsAt(recorded.root);
31828
- } else if (alive) {
31829
- ended = recorded.root === void 0 ? "named no directory for its pi" : "carried no bearer this generation could honour for its pi";
31830
- await container.kill(recorded.pid).catch(() => {
31831
- });
32205
+ if (recorded.container !== void 0 && here !== void 0 && recorded.container !== here) {
32206
+ 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`;
32207
+ } else {
32208
+ const alive = await container.alive(recorded.pid);
32209
+ if (alive && recorded.root !== void 0 && honoured(recorded.bearerHash)) {
32210
+ reattached = true;
32211
+ paths = piRunPathsAt(recorded.root);
32212
+ } else if (alive) {
32213
+ ended = recorded.root === void 0 ? "named no directory for its pi" : "carried no bearer this generation could honour for its pi";
32214
+ await container.kill(recorded.pid).catch(() => {
32215
+ });
32216
+ }
31832
32217
  }
31833
32218
  }
31834
32219
  if (reattached && recorded && paths !== void 0) {
31835
32220
  pid = recorded.pid;
31836
- facts2 = { ...recorded };
32221
+ facts2 = { ...recorded, ...recorded.container === void 0 && here !== void 0 ? { container: here } : {} };
31837
32222
  transport = new PiRpcTransport({
31838
32223
  container,
31839
32224
  paths,
@@ -31849,7 +32234,7 @@ async function runPiHarness(deps, run2) {
31849
32234
  );
31850
32235
  } else {
31851
32236
  paths = await container.makeRoot(run2.runId);
31852
- if (recorded?.root !== void 0 && recorded.root !== paths.dir)
32237
+ if (recorded?.root !== void 0 && recorded.root !== paths.dir && elsewhere === void 0)
31853
32238
  await container.remove(piRunPathsAt(recorded.root)).catch(() => {
31854
32239
  });
31855
32240
  const spec = {
@@ -31882,7 +32267,7 @@ async function runPiHarness(deps, run2) {
31882
32267
  piSessionFile(
31883
32268
  session.messages,
31884
32269
  {
31885
- cwd: run2.rules.checkout,
32270
+ cwd: container.cwd(paths, run2.rules.checkout),
31886
32271
  model: {
31887
32272
  provider: run2.model.provider,
31888
32273
  id: run2.model.id,
@@ -31896,7 +32281,7 @@ async function runPiHarness(deps, run2) {
31896
32281
  }
31897
32282
  if (run2.resume) {
31898
32283
  const lost = run2.resume.settlements.length;
31899
- const how = ended !== void 0 && recorded !== void 0 ? `the row ${ended} (pid ${recorded.pid}), so it was ended and pi restarted` : "pi restarted";
32284
+ 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";
31900
32285
  note(
31901
32286
  "resumed",
31902
32287
  `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`
@@ -31906,7 +32291,13 @@ async function runPiHarness(deps, run2) {
31906
32291
  for (const file of piLaunchFiles(launch)) await container.writeFile(file.path, file.content);
31907
32292
  ({ pid } = await container.start({ paths, args: piLaunchArgs(launch), env: piLaunchEnv(launch, deps.bearer) }));
31908
32293
  const bearerHash = bearerHashOf(deps.bearer);
31909
- facts2 = { pid, logOffset: 0, root: paths.dir, ...bearerHash !== void 0 ? { bearerHash } : {} };
32294
+ facts2 = {
32295
+ pid,
32296
+ logOffset: 0,
32297
+ root: paths.dir,
32298
+ ...bearerHash !== void 0 ? { bearerHash } : {},
32299
+ ...here !== void 0 ? { container: here } : {}
32300
+ };
31910
32301
  save();
31911
32302
  transport = new PiRpcTransport({ container, paths, pid, pollMs: deps.pollMs ?? 750, sleep: deps.sleep });
31912
32303
  }
@@ -32101,7 +32492,7 @@ ${stagedLine}` : prompt,
32101
32492
  agentSpan?.end(hardStopped || bypass ? "error" : "ok");
32102
32493
  }
32103
32494
  }
32104
- var NATIVE_WORKSPACE_TOOLS, FINALE_TIMEOUT_MS2, CONTINUE_PROMPT, PromptRefused, GateBypassed;
32495
+ var NATIVE_WORKSPACE_TOOLS, FINALE_TIMEOUT_MS2, CALL_SEEN_WAIT_MS, CALL_SEEN_TICK_MS, CONTINUE_PROMPT, PromptRefused, GateBypassed;
32105
32496
  var init_harness = __esm({
32106
32497
  "../../src/core/harness/pi/harness.ts"() {
32107
32498
  "use strict";
@@ -32116,6 +32507,8 @@ var init_harness = __esm({
32116
32507
  init_transport();
32117
32508
  NATIVE_WORKSPACE_TOOLS = /* @__PURE__ */ new Set(["bash", "read_file", "write_file"]);
32118
32509
  FINALE_TIMEOUT_MS2 = 3 * 6e4;
32510
+ CALL_SEEN_WAIT_MS = 3e3;
32511
+ CALL_SEEN_TICK_MS = 50;
32119
32512
  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.";
32120
32513
  PromptRefused = class extends Error {
32121
32514
  constructor(reason) {
@@ -32970,6 +33363,7 @@ async function runLoop2(deps, ctx) {
32970
33363
  executor,
32971
33364
  resident: round2.selection.resident !== void 0
32972
33365
  });
33366
+ ctx.workspaceFiles?.record(staged.outcomes);
32973
33367
  return staged.line;
32974
33368
  } : void 0;
32975
33369
  let artifactSeq = 0;
@@ -33018,10 +33412,19 @@ async function runLoop2(deps, ctx) {
33018
33412
  });
33019
33413
  if (harness === "pi" && piFacts) {
33020
33414
  const container = deps.harness?.containerFor?.(executor, profile.machine) ?? piContainerFor(executor, profile.machine);
33021
- await container.kill(piFacts.pid).catch(() => {
33022
- });
33023
- if (piFacts.root !== void 0) await container.remove(piRunPathsAt(piFacts.root)).catch(() => {
33024
- });
33415
+ const here = await container.identity();
33416
+ if (piFacts.container === void 0 || here === void 0 || piFacts.container === here) {
33417
+ await container.kill(piFacts.pid).catch(() => {
33418
+ });
33419
+ if (piFacts.root !== void 0) await container.remove(piRunPathsAt(piFacts.root)).catch(() => {
33420
+ });
33421
+ } else {
33422
+ onEvent({
33423
+ type: "run_note",
33424
+ kind: "resumed",
33425
+ 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`
33426
+ });
33427
+ }
33025
33428
  }
33026
33429
  answer = answerUnderEnding(finish2.answer, loopEnding, agent.maxMinutes);
33027
33430
  } else if (harness === "pi") {
@@ -33066,7 +33469,7 @@ async function runLoop2(deps, ctx) {
33066
33469
  messages,
33067
33470
  tools: relayedTools(mergeTools(TOOLSETS[agent.toolset] ?? [], mcpForRun?.tools)),
33068
33471
  toolContext,
33069
- ...session ? { notepad: () => session.readNotepad() } : {},
33472
+ ...session ? { notepad: () => session.readNotepad(), conversation: () => session.readConversation() } : {},
33070
33473
  rules: {
33071
33474
  checkout: binding?.workspace ?? "/workspace",
33072
33475
  ...ownBranch !== void 0 ? { branch: ownBranch } : {},
@@ -33884,7 +34287,7 @@ async function plan(deps, input) {
33884
34287
  try {
33885
34288
  planId = planIdOf(request.planPath);
33886
34289
  } catch (err2) {
33887
- return { ok: false, reply: `\u{1F6AB} ${describe5(err2)}` };
34290
+ return { ok: false, reply: `\u{1F6AB} ${describe6(err2)}` };
33888
34291
  }
33889
34292
  let text;
33890
34293
  try {
@@ -33892,7 +34295,7 @@ async function plan(deps, input) {
33892
34295
  } catch (err2) {
33893
34296
  return {
33894
34297
  ok: false,
33895
- reply: `\u{1F6AB} The plan \`${request.planPath}\` could not be read at \`${base}\` in ${entry.repo}: ${describe5(err2)}`
34298
+ reply: `\u{1F6AB} The plan \`${request.planPath}\` could not be read at \`${base}\` in ${entry.repo}: ${describe6(err2)}`
33896
34299
  };
33897
34300
  }
33898
34301
  const graph = parsePlanGraph(text, planId);
@@ -33905,7 +34308,7 @@ async function plan(deps, input) {
33905
34308
  try {
33906
34309
  selected = openPlanCursor(graph, request.units).order;
33907
34310
  } catch (err2) {
33908
- return { ok: false, reply: `\u{1F6AB} ${describe5(err2)}` };
34311
+ return { ok: false, reply: `\u{1F6AB} ${describe6(err2)}` };
33909
34312
  }
33910
34313
  return { ok: true, planned: { kind: "plan", planId, path: request.planPath, base, graph, selected, identity: identity2 } };
33911
34314
  }
@@ -34021,7 +34424,7 @@ async function start(deps, input, instance, units, where, write) {
34021
34424
  try {
34022
34425
  answer = await deps.create(instance.id);
34023
34426
  } catch (err2) {
34024
- answer = { kind: "unanswered", reason: describe5(err2) };
34427
+ answer = { kind: "unanswered", reason: describe6(err2) };
34025
34428
  }
34026
34429
  switch (answer.kind) {
34027
34430
  case "created": {
@@ -34043,7 +34446,7 @@ async function start(deps, input, instance, units, where, write) {
34043
34446
  );
34044
34447
  }
34045
34448
  }
34046
- var refused3, describe5, RUNNING, ENDED;
34449
+ var refused3, describe6, RUNNING, ENDED;
34047
34450
  var init_handOff = __esm({
34048
34451
  "../../src/core/coordinator/handOff.ts"() {
34049
34452
  "use strict";
@@ -34051,7 +34454,7 @@ var init_handOff = __esm({
34051
34454
  init_coordinator();
34052
34455
  init_briefs();
34053
34456
  refused3 = (reply) => ({ status: "aborted", reply });
34054
- describe5 = (err2) => err2 instanceof Error ? err2.message : String(err2);
34457
+ describe6 = (err2) => err2 instanceof Error ? err2.message : String(err2);
34055
34458
  RUNNING = /* @__PURE__ */ new Set(["queued", "running", "paused", "waiting", "waitingForPause"]);
34056
34459
  ENDED = /* @__PURE__ */ new Set(["complete", "errored", "terminated"]);
34057
34460
  }
@@ -34744,6 +35147,77 @@ var init_settle = __esm({
34744
35147
  }
34745
35148
  });
34746
35149
 
35150
+ // ../../src/core/dispatch/reattach.ts
35151
+ function carriedWorkspaceBinding(row) {
35152
+ const recorded = workspaceBindingOf(row.state.binding);
35153
+ if (recorded) return recorded;
35154
+ if (row.meta.selection === "resident")
35155
+ return { backend: "resident", ...row.meta.workspace ? { workspace: row.meta.workspace } : {} };
35156
+ if (row.meta.selection === "sandbox" || row.meta.selection === "local") return { backend: row.meta.selection };
35157
+ return void 0;
35158
+ }
35159
+ function lostWorkspaceNote(why, restarts) {
35160
+ const lost = `resumed after a restart: the run's workspace could not be re-attached (${why})`;
35161
+ 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`;
35162
+ }
35163
+ async function abandonLostWorkspace(ctx) {
35164
+ const { refuse: refuse3, card, shell, closeLines, clock: clock3, run: run2, registry, resume, ledgerRun, why } = ctx;
35165
+ const restored = messageFromInbox(resume.row.meta.request ?? {}, resume.row.startedAt);
35166
+ const summary = lostWorkspaceNote(why, restored !== void 0);
35167
+ const note = { type: "run_note", kind: "resumed", summary, at: clock3() };
35168
+ await refuse3("workspace_lost", async () => {
35169
+ registry.publish(run2.id, note);
35170
+ await card.done(
35171
+ shell.close({
35172
+ kind: "not_started",
35173
+ icon: "\u{1F501}",
35174
+ reason: restored ? "workspace lost across the restart; restarting from the request" : "workspace lost across the restart; re-send to run again",
35175
+ ...closeLines(clock3(), false)
35176
+ })
35177
+ );
35178
+ if (ledgerRun)
35179
+ await closeResumedRow(
35180
+ ledgerRun,
35181
+ { ...resume, events: [...resume.events, { ...note, seq: resume.lastSeq + 1 }] },
35182
+ "the run's workspace could not be re-attached"
35183
+ );
35184
+ });
35185
+ console.log(
35186
+ `[resume] ${resume.row.threadKey} run ${resume.row.runId} ${restored ? "restarts from its request" : "ends"}: ${why}`
35187
+ );
35188
+ return restored?.msg;
35189
+ }
35190
+ function prepareRestartTurn(deps, ctx) {
35191
+ const { request, pending: pending2, clock: clock3 } = ctx;
35192
+ const merged = mergeFollowUps(pending2);
35193
+ const receivedAt = clock3();
35194
+ const msg = {
35195
+ ...request,
35196
+ ...merged ? {
35197
+ text: `${request.text}
35198
+
35199
+ ${merged.text}`,
35200
+ ...merged.images || request.images ? { images: [...request.images ?? [], ...merged.images ?? []] } : {},
35201
+ ...merged.documents || request.documents ? { documents: [...request.documents ?? [], ...merged.documents ?? []] } : {},
35202
+ ...merged.staged || request.staged ? { staged: [...request.staged ?? [], ...merged.staged ?? []] } : {}
35203
+ } : {},
35204
+ receivedAt,
35205
+ originAt: void 0
35206
+ };
35207
+ const trace = startRequestRoot(deps, { channel: channelOf(msg.channelId), receivedAt });
35208
+ return { msg, opts: { trace } };
35209
+ }
35210
+ var init_reattach = __esm({
35211
+ "../../src/core/dispatch/reattach.ts"() {
35212
+ "use strict";
35213
+ init_factory();
35214
+ init_inboxMessage();
35215
+ init_requestTrace();
35216
+ init_threadAdmission();
35217
+ init_admission();
35218
+ }
35219
+ });
35220
+
34747
35221
  // ../../src/core/dispatch/lineage.ts
34748
35222
  function lineageOf(newest) {
34749
35223
  if (newest === void 0 || newest.parentRunId === void 0) return void 0;
@@ -34803,31 +35277,6 @@ var init_lineage = __esm({
34803
35277
  });
34804
35278
 
34805
35279
  // ../../src/core/dispatch/thread.ts
34806
- async function readThreadArtifacts(service, thread, warn = (line3) => console.warn(line3)) {
34807
- const perRun = [];
34808
- for (const run2 of [...thread].reverse()) {
34809
- const events = [];
34810
- let afterSeq;
34811
- try {
34812
- for (; ; ) {
34813
- const r = await service.getRunEvents(run2.id, afterSeq === void 0 ? {} : { afterSeq });
34814
- if (!r.ok) {
34815
- warn(`[thread] ${run2.id}: reading its received files stopped after ${events.length} event(s) \u2014 ${r.error}`);
34816
- break;
34817
- }
34818
- events.push(...r.value.events);
34819
- if (r.value.nextAfterSeq === void 0) break;
34820
- afterSeq = r.value.nextAfterSeq;
34821
- }
34822
- } catch (err2) {
34823
- warn(
34824
- `[thread] ${run2.id}: reading its received files failed after ${events.length} event(s) \u2014 ${err2 instanceof Error ? err2.message : String(err2)}`
34825
- );
34826
- }
34827
- perRun.push(events);
34828
- }
34829
- return threadInboundArtifacts(perRun);
34830
- }
34831
35280
  async function readThread(service, threadKey) {
34832
35281
  try {
34833
35282
  const page2 = await service.listRuns({
@@ -34869,7 +35318,6 @@ var THREAD_READ_LIMIT;
34869
35318
  var init_thread = __esm({
34870
35319
  "../../src/core/dispatch/thread.ts"() {
34871
35320
  "use strict";
34872
- init_staging();
34873
35321
  THREAD_READ_LIMIT = 8;
34874
35322
  }
34875
35323
  });
@@ -34989,22 +35437,22 @@ function createRunEnding(deps) {
34989
35437
  try {
34990
35438
  deps.registry.seal(id, replyOk === void 0 ? {} : { replyOk });
34991
35439
  } catch (err2) {
34992
- log(`[ending] seal ${id}: ${describe6(err2)}`);
35440
+ log(`[ending] seal ${id}: ${describe7(err2)}`);
34993
35441
  }
34994
35442
  try {
34995
35443
  afterSeal?.();
34996
35444
  } catch (err2) {
34997
- log(`[ending] after seal ${id}: ${describe6(err2)}`);
35445
+ log(`[ending] after seal ${id}: ${describe7(err2)}`);
34998
35446
  }
34999
35447
  }
35000
35448
  for (const entry of pending2.splice(0)) {
35001
35449
  try {
35002
35450
  const result = deps.registry.seal(entry.runId);
35003
35451
  void Promise.resolve(entry.write(result, entry.failedAfterFinish)).catch(
35004
- (err2) => log(`[ending] record ${entry.runId}: ${describe6(err2)}`)
35452
+ (err2) => log(`[ending] record ${entry.runId}: ${describe7(err2)}`)
35005
35453
  );
35006
35454
  } catch (err2) {
35007
- log(`[ending] record ${entry.runId}: ${describe6(err2)}`);
35455
+ log(`[ending] record ${entry.runId}: ${describe7(err2)}`);
35008
35456
  }
35009
35457
  }
35010
35458
  };
@@ -35014,7 +35462,7 @@ function createRunEnding(deps) {
35014
35462
  try {
35015
35463
  deps.onFinished?.(runId3);
35016
35464
  } catch (err2) {
35017
- log(`[ending] finished ${runId3}: ${describe6(err2)}`);
35465
+ log(`[ending] finished ${runId3}: ${describe7(err2)}`);
35018
35466
  }
35019
35467
  },
35020
35468
  register(entry) {
@@ -35042,7 +35490,7 @@ function createRunEnding(deps) {
35042
35490
  }
35043
35491
  };
35044
35492
  }
35045
- function describe6(err2) {
35493
+ function describe7(err2) {
35046
35494
  return err2 instanceof Error ? err2.message : String(err2);
35047
35495
  }
35048
35496
  var init_runEnding = __esm({
@@ -35104,6 +35552,8 @@ async function dispatch(deps, msg, io, opts = {}) {
35104
35552
  let reserved;
35105
35553
  let requestRow;
35106
35554
  let fencedWhileAttaching = false;
35555
+ let resumeRowClosed = false;
35556
+ let restartRequest;
35107
35557
  const reservationHooks = {
35108
35558
  onStop: (mode) => void registered?.control.requestStop(mode),
35109
35559
  onFenced: () => {
@@ -35344,18 +35794,21 @@ async function dispatch(deps, msg, io, opts = {}) {
35344
35794
  await tellLineage({ kind: "started", runId: runId3 });
35345
35795
  const staged = !resume && deps.artifacts && msg.staged && msg.staged.length > 0 ? msg.staged : [];
35346
35796
  const nextStagedIndex = stagingIndex();
35797
+ const workspaceFiles = new WorkspaceFiles();
35347
35798
  const stagedCopies = staged.length > 0 && agent.machine !== "none" ? copyStaged(staged, {
35348
35799
  store: deps.artifacts,
35349
35800
  threadKey: msg.threadKey,
35350
35801
  nextIndex: nextStagedIndex,
35351
35802
  publish: (e) => registry.publish(runId3, e)
35352
35803
  }) : void 0;
35353
- const earlierStaged = !resume && deps.artifacts && agent.machine !== "none" && thread && thread.length > 0 ? readThreadArtifacts(runsService, thread).then(
35354
- (found) => stageThreadArtifacts(found, {
35355
- store: deps.artifacts,
35356
- nextIndex: nextStagedIndex,
35357
- publish: (e) => registry.publish(runId3, e)
35358
- })
35804
+ const threadAssets = !resume && deps.artifacts && thread && thread.length > 0 ? readThreadAssets({ runs: runsService, store: deps.artifacts }, msg.threadKey).then(
35805
+ (assets) => assets.filter((a) => a.runId !== runId3)
35806
+ ) : void 0;
35807
+ const earlierStaged = threadAssets && agent.machine !== "none" ? threadAssets.then(
35808
+ (assets) => stageThreadArtifacts(
35809
+ assets.filter((a) => a.direction === "in"),
35810
+ { nextIndex: nextStagedIndex, publish: (e) => registry.publish(runId3, e) }
35811
+ )
35359
35812
  ) : void 0;
35360
35813
  const reservation = await reserveRun(deps, {
35361
35814
  msg,
@@ -35382,6 +35835,7 @@ async function dispatch(deps, msg, io, opts = {}) {
35382
35835
  reserved = reservation.reserved;
35383
35836
  requestRow = reservation.requestRow;
35384
35837
  }
35838
+ const reattach = resume ? carriedWorkspaceBinding(resume.row) : void 0;
35385
35839
  const attach = await attachWorkspace(deps, {
35386
35840
  msg,
35387
35841
  io,
@@ -35393,10 +35847,35 @@ async function dispatch(deps, msg, io, opts = {}) {
35393
35847
  agent,
35394
35848
  profile,
35395
35849
  repoCtx,
35396
- root
35850
+ root,
35851
+ ...reattach !== void 0 ? { reattach } : {}
35397
35852
  });
35398
35853
  if (attach.kind === "refused") return ended;
35854
+ if (attach.kind === "reattach_refused") {
35855
+ if (resume) {
35856
+ restartRequest = await abandonLostWorkspace({
35857
+ msg,
35858
+ io,
35859
+ refuse: refuse3,
35860
+ card,
35861
+ shell,
35862
+ closeLines,
35863
+ clock: clock3,
35864
+ run: run2,
35865
+ registry,
35866
+ resume,
35867
+ ledgerRun,
35868
+ why: attach.why
35869
+ });
35870
+ resumeRowClosed = true;
35871
+ }
35872
+ return ended;
35873
+ }
35399
35874
  const { round: round2 } = attach;
35875
+ if (resume && ledgerRun) {
35876
+ const rebound = workspaceBindingFor(round2.selection, profile.machine);
35877
+ if (rebound !== void 0) ledgerRun.setState({ binding: rebound });
35878
+ }
35400
35879
  const { executor, note, resident } = round2.selection;
35401
35880
  if (fencedWhileAttaching) {
35402
35881
  console.log(
@@ -35408,13 +35887,16 @@ async function dispatch(deps, msg, io, opts = {}) {
35408
35887
  }
35409
35888
  const earlier2 = earlierStaged ? await earlierStaged : [];
35410
35889
  if (staged.length > 0 || earlier2.length > 0) {
35411
- const line3 = stagedCopies || earlier2.length > 0 ? attachmentsLine(
35412
- await pullStaged([...stagedCopies ? await stagedCopies : [], ...earlier2], {
35890
+ let line3;
35891
+ if (stagedCopies || earlier2.length > 0) {
35892
+ const pulled = await pullStaged([...stagedCopies ? await stagedCopies : [], ...earlier2], {
35413
35893
  store: deps.artifacts,
35414
35894
  executor,
35415
35895
  resident: resident !== void 0
35416
- })
35417
- ) : noWorkspaceLine(staged);
35896
+ });
35897
+ workspaceFiles.record(pulled);
35898
+ line3 = attachmentsLine(pulled);
35899
+ } else line3 = noWorkspaceLine(staged);
35418
35900
  const request = messages[messages.length - 1];
35419
35901
  if (request && request.role === "user" && line3) {
35420
35902
  request.content = Array.isArray(request.content) ? [...request.content, { type: "text", text: line3 }] : [
@@ -35444,6 +35926,7 @@ async function dispatch(deps, msg, io, opts = {}) {
35444
35926
  if (headGate.headAdopted) publishMeta(repoCtx);
35445
35927
  const isPrReview = agent.name === "review" && repoCtx.repo !== void 0 && repoCtx.pr !== void 0;
35446
35928
  const isCodingPrRun = agent.toolset === "full";
35929
+ const threadFiles = threadAssets ? (await threadAssets).map((a) => describeAsset(a, workspaceFiles.pathOf(a.key))) : [];
35447
35930
  const prompt = await composePrompt(deps, {
35448
35931
  msg,
35449
35932
  agent,
@@ -35460,11 +35943,13 @@ async function dispatch(deps, msg, io, opts = {}) {
35460
35943
  root,
35461
35944
  ...contractBlock !== void 0 && agent.name === "review" ? { contract: contractBlock } : {},
35462
35945
  // What the session already knows (session-log item 10): the notepad and
35463
- // the newest compaction's summary the seed brought back, for the prompt.
35464
- ...session && (session.notepad !== void 0 || session.summary !== void 0) ? {
35946
+ // the newest compaction's summary the seed brought back, and the thread's
35947
+ // files (record 0033) with where each is for this run, for the prompt.
35948
+ ...session?.notepad !== void 0 || session?.summary !== void 0 || threadFiles.length > 0 ? {
35465
35949
  session: {
35466
- ...session.notepad !== void 0 ? { notepad: session.notepad } : {},
35467
- ...session.summary !== void 0 ? { summary: session.summary } : {}
35950
+ ...session?.notepad !== void 0 ? { notepad: session.notepad } : {},
35951
+ ...session?.summary !== void 0 ? { summary: session.summary } : {},
35952
+ ...threadFiles.length > 0 ? { files: threadFiles } : {}
35468
35953
  }
35469
35954
  } : {}
35470
35955
  });
@@ -35524,7 +36009,14 @@ async function dispatch(deps, msg, io, opts = {}) {
35524
36009
  seed,
35525
36010
  ...session ? { seedLog: session.log } : {}
35526
36011
  });
35527
- const sessionTools = sessionCapabilityFor(ledgerRun, deps.runLedger);
36012
+ const sessionTools = sessionCapabilityFor(
36013
+ ledgerRun,
36014
+ deps.runLedger,
36015
+ deps.artifacts ? {
36016
+ read: () => readThreadAssets({ runs: runsService, store: deps.artifacts }, msg.threadKey),
36017
+ pathOf: (key) => workspaceFiles.pathOf(key)
36018
+ } : void 0
36019
+ );
35528
36020
  const { spawn: spawn7, runs, steer, wait } = runToolCapabilities(
35529
36021
  { core: deps, dispatch, registry, clock: clock3 },
35530
36022
  {
@@ -35544,6 +36036,7 @@ async function dispatch(deps, msg, io, opts = {}) {
35544
36036
  profile,
35545
36037
  resolved,
35546
36038
  stagingIndex: nextStagedIndex,
36039
+ workspaceFiles,
35547
36040
  provider,
35548
36041
  model,
35549
36042
  messages,
@@ -35646,7 +36139,7 @@ async function dispatch(deps, msg, io, opts = {}) {
35646
36139
  clearInterval(setupHeartbeat);
35647
36140
  ending.drain(void 0);
35648
36141
  if (registered) deps.runBearers?.revoke(registered.id);
35649
- if (resume && ledgerRun && !runLoopStarted) {
36142
+ if (resume && ledgerRun && !runLoopStarted && !resumeRowClosed) {
35650
36143
  const adopted = ledgerRun;
35651
36144
  await root.span(
35652
36145
  "post.history_write",
@@ -35661,7 +36154,15 @@ async function dispatch(deps, msg, io, opts = {}) {
35661
36154
  const stopMode = settled.kind === "handed-on" ? void 0 : settled.stopMode;
35662
36155
  ended.status = caught ? "failed" : refused5 ? "refused" : stopMode ? "stopped" : "completed";
35663
36156
  root.end(caught ? "error" : "ok", { status: ended.status });
35664
- if (settled.kind === "handed-on") {
36157
+ if (restartRequest) {
36158
+ const pending2 = settled.kind === "handed-on" ? settled.pending : [];
36159
+ const restart2 = prepareRestartTurn(deps, { request: restartRequest, pending: pending2, clock: clock3 });
36160
+ await dispatch(deps, restart2.msg, io, restart2.opts).catch(
36161
+ (err2) => console.error(
36162
+ `[dispatch] ${msg.threadKey} restart after a lost workspace failed: ${err2 instanceof Error ? err2.message : String(err2)}`
36163
+ )
36164
+ );
36165
+ } else if (settled.kind === "handed-on") {
35665
36166
  const fresh = prepareFreshTurn(deps, { agent: settled.agent, pending: settled.pending, clock: clock3 });
35666
36167
  await dispatch(deps, fresh.msg, fresh.io, fresh.opts).catch(
35667
36168
  (err2) => console.error(
@@ -35704,10 +36205,13 @@ var init_dispatcher = __esm({
35704
36205
  init_contract();
35705
36206
  init_codingChild();
35706
36207
  init_settle();
36208
+ init_reattach();
36209
+ init_factory();
35707
36210
  init_lineage();
35708
36211
  init_seed();
35709
36212
  init_session();
35710
36213
  init_thread();
36214
+ init_threadAssets();
35711
36215
  init_select();
35712
36216
  init_spawn();
35713
36217
  init_runsService();
@@ -35718,7 +36222,7 @@ var init_dispatcher = __esm({
35718
36222
  });
35719
36223
 
35720
36224
  // ../../src/core/runHistoryWriter.ts
35721
- function describe7(err2) {
36225
+ function describe8(err2) {
35722
36226
  return err2 instanceof Error ? err2.message : String(err2);
35723
36227
  }
35724
36228
  function createRunHistoryWriter(opts) {
@@ -35735,7 +36239,7 @@ function createRunHistoryWriter(opts) {
35735
36239
  try {
35736
36240
  opts.onPersisted?.(id);
35737
36241
  } catch (err2) {
35738
- opts.warn(`[run-history] onPersisted hook failed for ${id}: ${describe7(err2)}`);
36242
+ opts.warn(`[run-history] onPersisted hook failed for ${id}: ${describe8(err2)}`);
35739
36243
  }
35740
36244
  };
35741
36245
  const attemptAll = async (record2, flag2, sink, span) => {
@@ -35758,12 +36262,12 @@ function createRunHistoryWriter(opts) {
35758
36262
  }
35759
36263
  if (err2 instanceof PermanentStoreError) {
35760
36264
  failures++;
35761
- opts.warn(`[run-history] ${record2.id} not persisted (permanent, not retried): ${describe7(err2)}`);
36265
+ opts.warn(`[run-history] ${record2.id} not persisted (permanent, not retried): ${describe8(err2)}`);
35762
36266
  return;
35763
36267
  }
35764
36268
  if (attempt >= attempts) {
35765
36269
  failures++;
35766
- opts.warn(`[run-history] ${record2.id} not persisted after ${attempts} attempts: ${describe7(err2)}`);
36270
+ opts.warn(`[run-history] ${record2.id} not persisted after ${attempts} attempts: ${describe8(err2)}`);
35767
36271
  return;
35768
36272
  }
35769
36273
  await sleep2(jittered(RUN_HISTORY_RETRY_DELAYS_MS[attempt - 1]));
@@ -35783,7 +36287,7 @@ function createRunHistoryWriter(opts) {
35783
36287
  }
35784
36288
  const p = attemptAll(record2, flag2, writeOpts?.via ?? opts.store, writeOpts?.span).catch((err2) => {
35785
36289
  failures++;
35786
- opts.warn(`[run-history] ${record2.id} writer failed unexpectedly: ${describe7(err2)}`);
36290
+ opts.warn(`[run-history] ${record2.id} writer failed unexpectedly: ${describe8(err2)}`);
35787
36291
  }).finally(() => {
35788
36292
  if (flag2) provisionalFlags.delete(flag2);
35789
36293
  void inFlight.delete(p);
@@ -36200,7 +36704,7 @@ function createLedgerWriteThrough(opts) {
36200
36704
  return { outcome: "untracked" };
36201
36705
  }
36202
36706
  if (err2 instanceof PermanentStoreError || attempt >= claimAttempts) {
36203
- warn(`[ledger] ${req.threadKey} not tracked: claim failed after ${attempt} attempt(s): ${describe8(err2)}`);
36707
+ warn(`[ledger] ${req.threadKey} not tracked: claim failed after ${attempt} attempt(s): ${describe9(err2)}`);
36204
36708
  return { outcome: "untracked" };
36205
36709
  }
36206
36710
  await sleep2(RETRY_MS[Math.min(attempt, RETRY_MS.length) - 1]);
@@ -36362,7 +36866,7 @@ function createLedgerWriteThrough(opts) {
36362
36866
  if (!recorded.ok) this.detach(`seed record refused (${recorded.reason})`);
36363
36867
  else this.seeded = true;
36364
36868
  } catch (err2) {
36365
- this.detach(`seed failed: ${describe8(err2)}`);
36869
+ this.detach(`seed failed: ${describe9(err2)}`);
36366
36870
  }
36367
36871
  }
36368
36872
  async step(report2) {
@@ -36389,7 +36893,7 @@ function createLedgerWriteThrough(opts) {
36389
36893
  return;
36390
36894
  } catch (err2) {
36391
36895
  if (err2 instanceof RouteMissingError || err2 instanceof PermanentStoreError || attempt >= 2) {
36392
- this.detach(`step ${record2.step} failed: ${describe8(err2)}`);
36896
+ this.detach(`step ${record2.step} failed: ${describe9(err2)}`);
36393
36897
  return;
36394
36898
  }
36395
36899
  await sleep2(RETRY_MS[0]);
@@ -36422,7 +36926,7 @@ function createLedgerWriteThrough(opts) {
36422
36926
  } catch (err2) {
36423
36927
  if (err2 instanceof RouteMissingError || err2 instanceof PermanentStoreError || attempt >= 2) {
36424
36928
  this.stateDirty = true;
36425
- warn(`[ledger] ${this.threadKey} state not written: ${describe8(err2)}`);
36929
+ warn(`[ledger] ${this.threadKey} state not written: ${describe9(err2)}`);
36426
36930
  return;
36427
36931
  }
36428
36932
  await sleep2(RETRY_MS[0]);
@@ -36440,7 +36944,7 @@ function createLedgerWriteThrough(opts) {
36440
36944
  this.detach(`finishing refused (fenced)`);
36441
36945
  return "fenced";
36442
36946
  } catch (err2) {
36443
- warn(`[ledger] ${this.threadKey} finishing failed: ${describe8(err2)}`);
36947
+ warn(`[ledger] ${this.threadKey} finishing failed: ${describe9(err2)}`);
36444
36948
  return "unavailable";
36445
36949
  }
36446
36950
  }
@@ -36455,7 +36959,7 @@ function createLedgerWriteThrough(opts) {
36455
36959
  warn(`[ledger] ${this.threadKey} abandon refused (fenced) \u2014 the row is another generation's`);
36456
36960
  } catch (err2) {
36457
36961
  warn(
36458
- `[ledger] ${this.threadKey} abandon failed: ${describe8(err2)} \u2014 the sweep will take the row once its lease lapses`
36962
+ `[ledger] ${this.threadKey} abandon failed: ${describe9(err2)} \u2014 the sweep will take the row once its lease lapses`
36459
36963
  );
36460
36964
  }
36461
36965
  }
@@ -36476,7 +36980,7 @@ function createLedgerWriteThrough(opts) {
36476
36980
  this.onStop(result.stop);
36477
36981
  }
36478
36982
  } catch (err2) {
36479
- warn(`[ledger] ${this.threadKey} heartbeat failed: ${describe8(err2)}`);
36983
+ warn(`[ledger] ${this.threadKey} heartbeat failed: ${describe9(err2)}`);
36480
36984
  }
36481
36985
  }
36482
36986
  stopHeartbeat() {
@@ -36554,7 +37058,7 @@ function createLedgerWriteThrough(opts) {
36554
37058
  warn(`[ledger] inbox push refused for run ${runId3}: no live row \u2014 the follow-up rides in memory only`);
36555
37059
  return void 0;
36556
37060
  } catch (err2) {
36557
- warn(`[ledger] inbox push failed for run ${runId3}: ${describe8(err2)} \u2014 the follow-up rides in memory only`);
37061
+ warn(`[ledger] inbox push failed for run ${runId3}: ${describe9(err2)} \u2014 the follow-up rides in memory only`);
36558
37062
  return void 0;
36559
37063
  }
36560
37064
  },
@@ -36563,7 +37067,7 @@ function createLedgerWriteThrough(opts) {
36563
37067
  return await ledger.readInbox(runId3, afterSeq);
36564
37068
  } catch (err2) {
36565
37069
  warn(
36566
- 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`
37070
+ 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`
36567
37071
  );
36568
37072
  return [];
36569
37073
  }
@@ -36585,12 +37089,12 @@ function createLedgerWriteThrough(opts) {
36585
37089
  for (const r of candidates) if (markedSet.has(r.runId)) r.markHandedOff();
36586
37090
  return { marked };
36587
37091
  } catch (err2) {
36588
- return { marked: [], failed: describe8(err2) };
37092
+ return { marked: [], failed: describe9(err2) };
36589
37093
  }
36590
37094
  }
36591
37095
  };
36592
37096
  }
36593
- var NullLedgerWriteThrough, NullLedgerRun, RETRY_MS, describe8;
37097
+ var NullLedgerWriteThrough, NullLedgerRun, RETRY_MS, describe9;
36594
37098
  var init_writeThrough = __esm({
36595
37099
  "../../src/core/runLedger/writeThrough.ts"() {
36596
37100
  "use strict";
@@ -36670,7 +37174,7 @@ var init_writeThrough = __esm({
36670
37174
  }
36671
37175
  };
36672
37176
  RETRY_MS = [200, 800];
36673
- describe8 = (err2) => err2 instanceof Error ? err2.message : String(err2);
37177
+ describe9 = (err2) => err2 instanceof Error ? err2.message : String(err2);
36674
37178
  }
36675
37179
  });
36676
37180
 
@@ -41316,7 +41820,7 @@ async function reclaimRuns(opts) {
41316
41820
  reclaimed = await ledger.reclaim(gen, now(), LEASE_MS);
41317
41821
  } catch (err2) {
41318
41822
  warn(
41319
- 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`
41823
+ 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`
41320
41824
  );
41321
41825
  return outcome;
41322
41826
  }
@@ -41384,8 +41888,8 @@ async function reclaimRuns(opts) {
41384
41888
  `[reclaim] ${row.runId} ${row.threadKey} closed ${status3} (from ${run2.reclaimedFrom}; ${why}; ${events.length} event(s))`
41385
41889
  );
41386
41890
  } catch (err2) {
41387
- outcome.failed.push({ runId: row.runId, error: describe9(err2) });
41388
- warn(`[reclaim] ${row.runId} ${row.threadKey}: ${describe9(err2)} \u2014 left on the ledger for the next boot`);
41891
+ outcome.failed.push({ runId: row.runId, error: describe10(err2) });
41892
+ warn(`[reclaim] ${row.runId} ${row.threadKey}: ${describe10(err2)} \u2014 left on the ledger for the next boot`);
41389
41893
  }
41390
41894
  }
41391
41895
  try {
@@ -41401,7 +41905,7 @@ async function reclaimRuns(opts) {
41401
41905
  });
41402
41906
  }
41403
41907
  } catch (err2) {
41404
- warn(`[reclaim] listing live runs failed: ${describe9(err2)} \u2014 the card sweep runs without the ledger's guard`);
41908
+ warn(`[reclaim] listing live runs failed: ${describe10(err2)} \u2014 the card sweep runs without the ledger's guard`);
41405
41909
  }
41406
41910
  if (reclaimed.length > 0 || outcome.liveElsewhere.length > 0) {
41407
41911
  log(
@@ -41423,7 +41927,7 @@ function startReclaimSweep(opts) {
41423
41927
  await opts.onOutcome(outcome);
41424
41928
  }
41425
41929
  } catch (err2) {
41426
- warn(`[reclaim] sweep failed: ${describe9(err2)}`);
41930
+ warn(`[reclaim] sweep failed: ${describe10(err2)}`);
41427
41931
  } finally {
41428
41932
  running = false;
41429
41933
  }
@@ -41449,7 +41953,7 @@ async function completenessVerdict(ledger, row, lastStep) {
41449
41953
  const source2 = transcriptSource(row.meta);
41450
41954
  transcript = source2.kind === "session" ? await ledger.readSession(source2.key, source2.from) : await ledger.readTranscript(row.runId);
41451
41955
  } catch (err2) {
41452
- return { resumable: false, why: `transcript unreadable (${describe9(err2)})` };
41956
+ return { resumable: false, why: `transcript unreadable (${describe10(err2)})` };
41453
41957
  }
41454
41958
  if (!transcript.complete) return { resumable: false, why: `transcript incomplete: ${transcript.gap}` };
41455
41959
  const verdict = transcriptCompleteness({
@@ -41470,7 +41974,7 @@ async function completenessVerdict(ledger, row, lastStep) {
41470
41974
  return { resumable: false, why: verdict.why };
41471
41975
  }
41472
41976
  }
41473
- var TERMINAL, describe9;
41977
+ var TERMINAL, describe10;
41474
41978
  var init_boot = __esm({
41475
41979
  "../../src/core/boot.ts"() {
41476
41980
  "use strict";
@@ -41487,7 +41991,7 @@ var init_boot = __esm({
41487
41991
  "failed",
41488
41992
  "interrupted"
41489
41993
  ]);
41490
- describe9 = (err2) => err2 instanceof Error ? err2.message : String(err2);
41994
+ describe10 = (err2) => err2 instanceof Error ? err2.message : String(err2);
41491
41995
  }
41492
41996
  });
41493
41997
 
@@ -42336,6 +42840,17 @@ var init_toolRules = __esm({
42336
42840
  });
42337
42841
 
42338
42842
  // ../../src/core/harness/pi/relay.ts
42843
+ async function relayToolCall(harness, calls, ask, opts = {}) {
42844
+ const answer = calls.join(ask.toolCallId, () => runRelayedTool(harness, ask, { signal: calls.signal }));
42845
+ const window = new AbortController();
42846
+ const sleep2 = opts.sleep ?? sleepUnlessAborted;
42847
+ const progress = await Promise.race([
42848
+ answer.then((a) => ({ done: true, answer: a })),
42849
+ sleep2(opts.windowMs ?? RELAY_POLL_WINDOW_MS, window.signal).then(() => ({ done: false }))
42850
+ ]);
42851
+ window.abort();
42852
+ return progress;
42853
+ }
42339
42854
  function relayedToolDefinitions(harness) {
42340
42855
  return harness.tools.map(({ name, description, inputSchema }) => ({ name, description, inputSchema }));
42341
42856
  }
@@ -42356,18 +42871,23 @@ function authorizeToolCall(harness, ask) {
42356
42871
  if (verdict.verdict === "allowed") return { allow: true };
42357
42872
  return refuse3(verdict.reason);
42358
42873
  }
42359
- async function runRelayedTool(harness, ask) {
42874
+ async function runRelayedTool(harness, ask, opts = {}) {
42360
42875
  const tool = harness.tools.find((t) => t.name === ask.tool);
42361
42876
  if (!tool) return { content: [{ type: "text", text: `Unknown tool: ${ask.tool}` }], isError: true };
42362
42877
  const span = harness.toolSpan(ask.toolCallId);
42363
42878
  const base = harness.toolContext;
42364
- const ctx = span ? {
42365
- ...base,
42366
- span,
42367
- executor: new TracingExecutor(base.executor, span, harness.backend),
42368
- publish: (e) => harness.emit(withSpanId2(e, span.id)),
42369
- ...base.github?.api.withSpan ? { github: { ...base.github, api: base.github.api.withSpan(span) } } : {}
42370
- } : { ...base, publish: (e) => harness.emit(e) };
42879
+ const readConversation = base.conversation;
42880
+ const ctx = {
42881
+ ...span ? {
42882
+ ...base,
42883
+ span,
42884
+ executor: new TracingExecutor(base.executor, span, harness.backend),
42885
+ publish: (e) => harness.emit(withSpanId2(e, span.id)),
42886
+ ...base.github?.api.withSpan ? { github: { ...base.github, api: base.github.api.withSpan(span) } } : {}
42887
+ } : { ...base, publish: (e) => harness.emit(e) },
42888
+ ...opts.signal && !base.signal ? { signal: opts.signal } : {},
42889
+ ...readConversation && harness.callSeen ? { conversation: () => harness.callSeen(ask.toolCallId).then(() => readConversation()) } : {}
42890
+ };
42371
42891
  try {
42372
42892
  const input = typeof ask.input === "object" && ask.input !== null ? ask.input : {};
42373
42893
  const output = await tool.run(input, ctx);
@@ -42395,30 +42915,64 @@ function withSpanId2(e, spanId) {
42395
42915
  return e;
42396
42916
  }
42397
42917
  }
42398
- var HarnessRegistry;
42918
+ var HarnessRegistry, RELAY_POLL_WINDOW_MS, RelayedCalls;
42399
42919
  var init_relay = __esm({
42400
42920
  "../../src/core/harness/pi/relay.ts"() {
42401
42921
  "use strict";
42402
42922
  init_tracingExecutor();
42403
42923
  init_types3();
42924
+ init_awaitChildren();
42404
42925
  init_runEvents();
42405
42926
  init_toolRules();
42406
42927
  HarnessRegistry = class {
42407
42928
  live = /* @__PURE__ */ new Map();
42408
- /** The run drives a pi from here; the returned function forgets it. */
42929
+ /** The run drives a pi from here, its relayed calls kept beside it; the
42930
+ * returned function forgets it and ends whatever call still runs. */
42409
42931
  register(harness) {
42410
- this.live.set(harness.runId, harness);
42932
+ const entry = { harness, calls: new RelayedCalls() };
42933
+ this.live.set(harness.runId, entry);
42411
42934
  return () => {
42412
- if (this.live.get(harness.runId) === harness) this.live.delete(harness.runId);
42935
+ if (this.live.get(harness.runId) !== entry) return;
42936
+ this.live.delete(harness.runId);
42937
+ entry.calls.end();
42413
42938
  };
42414
42939
  }
42415
42940
  get(runId3) {
42416
- return this.live.get(runId3);
42941
+ return this.live.get(runId3)?.harness;
42942
+ }
42943
+ /** The run's relayed calls, for as long as it is registered. */
42944
+ calls(runId3) {
42945
+ return this.live.get(runId3)?.calls;
42417
42946
  }
42418
42947
  size() {
42419
42948
  return this.live.size;
42420
42949
  }
42421
42950
  };
42951
+ RELAY_POLL_WINDOW_MS = 3e4;
42952
+ RelayedCalls = class {
42953
+ calls = /* @__PURE__ */ new Map();
42954
+ ending = new AbortController();
42955
+ get size() {
42956
+ return this.calls.size;
42957
+ }
42958
+ /** Aborted when the run ends: the signal every call here runs under. */
42959
+ get signal() {
42960
+ return this.ending.signal;
42961
+ }
42962
+ /** The call's answer as a promise: started by `start` on the first ask, the same promise after. */
42963
+ join(callId, start3) {
42964
+ let answer = this.calls.get(callId);
42965
+ if (!answer) {
42966
+ answer = start3();
42967
+ this.calls.set(callId, answer);
42968
+ }
42969
+ return answer;
42970
+ }
42971
+ end() {
42972
+ this.ending.abort();
42973
+ this.calls.clear();
42974
+ }
42975
+ };
42422
42976
  }
42423
42977
  });
42424
42978
 
@@ -42467,7 +43021,15 @@ async function handleHarnessRequest(deps, req) {
42467
43021
  deps.log?.(`[harness] run=${door.runId} authorize ${ask.tool} \u2192 ${answer2.allow ? "allow" : "refuse"}`);
42468
43022
  return { status: 200, body: { ...answer2 } };
42469
43023
  }
42470
- const answer = await runRelayedTool(harness, ask);
43024
+ const progress = await relayToolCall(harness, deps.harnesses.calls(door.runId), ask, {
43025
+ ...deps.relayWindowMs !== void 0 ? { windowMs: deps.relayWindowMs } : {},
43026
+ ...deps.sleep ? { sleep: deps.sleep } : {}
43027
+ });
43028
+ if (!progress.done) {
43029
+ deps.log?.(`[harness] run=${door.runId} tool ${ask.tool} \u2192 pending`);
43030
+ return { status: 202, body: { pending: true, toolCallId: ask.toolCallId } };
43031
+ }
43032
+ const { answer } = progress;
42471
43033
  deps.log?.(`[harness] run=${door.runId} tool ${ask.tool} \u2192 ${answer.isError ? "error" : "ok"}`);
42472
43034
  return { status: 200, body: { ...answer } };
42473
43035
  }
@@ -42803,7 +43365,7 @@ async function openThreadFromRequester(deps, instance, lead, at) {
42803
43365
  }
42804
43366
  };
42805
43367
  } catch (err2) {
42806
- return { ok: false, response: json(502, { ok: false, error: "thread_failed", message: describe10(err2), at }) };
43368
+ return { ok: false, response: json(502, { ok: false, error: "thread_failed", message: describe11(err2), at }) };
42807
43369
  }
42808
43370
  }
42809
43371
  async function ensureReviewThread(deps, instance, row, at) {
@@ -42929,8 +43491,8 @@ async function spawn6(body, deps) {
42929
43491
  try {
42930
43492
  turn = await composeChild(req.brief, instance, row, briefReaders(deps, instance, io));
42931
43493
  } catch (err2) {
42932
- log(`[coordinator] ${instance.id} ${req.step}: the brief could not be composed: ${describe10(err2)}`);
42933
- return json(502, { ok: false, error: "brief_failed", message: describe10(err2), at });
43494
+ log(`[coordinator] ${instance.id} ${req.step}: the brief could not be composed: ${describe11(err2)}`);
43495
+ return json(502, { ok: false, error: "brief_failed", message: describe11(err2), at });
42934
43496
  }
42935
43497
  } else {
42936
43498
  turn = { prompt: req.prompt };
@@ -42980,7 +43542,7 @@ async function spawn6(body, deps) {
42980
43542
  );
42981
43543
  void settled.then((end) => {
42982
43544
  if (end.kind === "threw")
42983
- log(`[coordinator] ${instance.id} ${req.step}: the child's dispatch threw: ${describe10(end.err)}`);
43545
+ log(`[coordinator] ${instance.id} ${req.step}: the child's dispatch threw: ${describe11(end.err)}`);
42984
43546
  });
42985
43547
  const first = await Promise.race([started.then((id) => ({ kind: "started", id })), settled]);
42986
43548
  if (first.kind === "started" || startedId !== void 0) {
@@ -42988,7 +43550,7 @@ async function spawn6(body, deps) {
42988
43550
  log(`[coordinator] ${instance.id} ${req.step}: spawned ${req.preset} run ${runId3} in ${threadKey}`);
42989
43551
  return json(200, { ok: true, runId: runId3, threadKey, at });
42990
43552
  }
42991
- if (first.kind === "threw") return json(502, { ok: false, error: "spawn_failed", message: describe10(first.err), at });
43553
+ if (first.kind === "threw") return json(502, { ok: false, error: "spawn_failed", message: describe11(first.err), at });
42992
43554
  const refusal = first.outcome.refusal;
42993
43555
  if (refusal === "coordinator_thread_live") {
42994
43556
  const now = await liveOnThread(deps.runs, instance, threadKey);
@@ -43141,7 +43703,7 @@ async function prCheck(body, deps) {
43141
43703
  at
43142
43704
  });
43143
43705
  } catch (err2) {
43144
- return json(502, { ok: false, error: "github_unavailable", message: describe10(err2), at });
43706
+ return json(502, { ok: false, error: "github_unavailable", message: describe11(err2), at });
43145
43707
  }
43146
43708
  }
43147
43709
  async function plan2(body, deps) {
@@ -43250,7 +43812,7 @@ async function branch(body, deps) {
43250
43812
  await deps.createBranchRef(instance.repo, name, base);
43251
43813
  return json(200, { ok: true, branch: name, base, at });
43252
43814
  } catch (err2) {
43253
- return json(200, { ok: false, reason: describe10(err2), at });
43815
+ return json(200, { ok: false, reason: describe11(err2), at });
43254
43816
  }
43255
43817
  }
43256
43818
  function unitLines(units) {
@@ -43302,7 +43864,7 @@ async function round(body, deps) {
43302
43864
  instance,
43303
43865
  units.map((u) => u.unit === updated.unit ? updated : u)
43304
43866
  ).catch(
43305
- (err2) => (deps.log ?? console.warn)(`[coordinator] ${instance.id}: the card could not be redrawn: ${describe10(err2)}`)
43867
+ (err2) => (deps.log ?? console.warn)(`[coordinator] ${instance.id}: the card could not be redrawn: ${describe11(err2)}`)
43306
43868
  );
43307
43869
  return json(200, { ok: true, at });
43308
43870
  }
@@ -43336,7 +43898,7 @@ async function unitEnd(body, deps) {
43336
43898
  told = true;
43337
43899
  } catch (err2) {
43338
43900
  (deps.log ?? console.warn)(
43339
- `[coordinator] ${instance.id} ${row.unit}: the report could not be posted: ${describe10(err2)}`
43901
+ `[coordinator] ${instance.id} ${row.unit}: the report could not be posted: ${describe11(err2)}`
43340
43902
  );
43341
43903
  }
43342
43904
  }
@@ -43350,7 +43912,7 @@ ${ending.report}` + (rendered !== void 0 ? `
43350
43912
  ${rendered}` : "");
43351
43913
  await deps.github.commentIssue(instance.repo, row.issue, comment).catch(
43352
43914
  (err2) => (deps.log ?? console.warn)(
43353
- `[coordinator] ${instance.id} ${row.unit}: the board comment could not be posted: ${describe10(err2)}`
43915
+ `[coordinator] ${instance.id} ${row.unit}: the board comment could not be posted: ${describe11(err2)}`
43354
43916
  )
43355
43917
  );
43356
43918
  }
@@ -43405,7 +43967,7 @@ async function merge(body, deps, subject2) {
43405
43967
  try {
43406
43968
  facts2 = await deps.fetchPrFacts(pr);
43407
43969
  } catch (err2) {
43408
- return json(502, { ok: false, error: "github_unavailable", message: describe10(err2), at });
43970
+ return json(502, { ok: false, error: "github_unavailable", message: describe11(err2), at });
43409
43971
  }
43410
43972
  if (facts2 === void 0)
43411
43973
  return json(502, { ok: false, error: "github_unavailable", message: `${where} could not be read`, at });
@@ -43457,7 +44019,7 @@ async function merge(body, deps, subject2) {
43457
44019
  title: facts2.title ?? `Merge pull request #${pr.number}`
43458
44020
  });
43459
44021
  } catch (err2) {
43460
- return json(502, { ok: false, error: "github_unavailable", message: describe10(err2), at });
44022
+ return json(502, { ok: false, error: "github_unavailable", message: describe11(err2), at });
43461
44023
  }
43462
44024
  if (!merged.ok) {
43463
44025
  log(
@@ -43632,13 +44194,13 @@ function createAdminCoordinatorHandler(deps) {
43632
44194
  }
43633
44195
  write(res, await answerCoordinatorStep(door, read.body, deps));
43634
44196
  } catch (err2) {
43635
- (deps.log ?? console.error)(`[coordinator] ${describe10(err2)}`);
44197
+ (deps.log ?? console.error)(`[coordinator] ${describe11(err2)}`);
43636
44198
  write(res, json(500, { ok: false, error: "internal error" }));
43637
44199
  }
43638
44200
  })();
43639
44201
  };
43640
44202
  }
43641
- 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;
44203
+ 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;
43642
44204
  var init_adminCoordinator = __esm({
43643
44205
  "../../src/channels/adminCoordinator.ts"() {
43644
44206
  "use strict";
@@ -43671,7 +44233,7 @@ var init_adminCoordinator = __esm({
43671
44233
  };
43672
44234
  invalid = (error) => ({ ok: false, error });
43673
44235
  json = (status3, body) => ({ status: status3, body });
43674
- describe10 = (err2) => err2 instanceof Error ? err2.message : String(err2);
44236
+ describe11 = (err2) => err2 instanceof Error ? err2.message : String(err2);
43675
44237
  EVERY_RUN = { kind: "all" };
43676
44238
  REVIEW_POSTED_CHECKS = 3;
43677
44239
  REVIEW_POSTED_RECHECK_MS = 2e3;