@coreplane/switchboard 1.220.1 → 1.221.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -4139,6 +4139,7 @@ var init_residentSteps = __esm({
4139
4139
  "ops-dir": "preparing a directory",
4140
4140
  "stage-dir": "preparing a directory",
4141
4141
  stat: "checking a file",
4142
+ "boot-id": "reading the container's identity",
4142
4143
  touch: "touching a marker",
4143
4144
  nproc: "counting CPUs",
4144
4145
  mutex_wait: "waiting for the workspace",
@@ -9419,7 +9420,7 @@ async function parseResidentBody(res) {
9419
9420
  }
9420
9421
  return sanitizeResidentBody(data);
9421
9422
  }
9422
- var DETACH_TIMEOUT_MS, ResidentNeedsRefError, ResidentOperations, ResidentExecutor;
9423
+ var DETACH_TIMEOUT_MS, ResidentNeedsRefError, ResidentReuseRefusedError, ResidentOperations, ResidentExecutor;
9423
9424
  var init_resident = __esm({
9424
9425
  "../../src/execution/resident.ts"() {
9425
9426
  "use strict";
@@ -9451,6 +9452,16 @@ var init_resident = __esm({
9451
9452
  defaultRef;
9452
9453
  needs = "ref";
9453
9454
  };
9455
+ ResidentReuseRefusedError = class extends Error {
9456
+ constructor(resource, why) {
9457
+ super(`resident attach: ${resource} cannot reuse this thread's worktree (${why})`);
9458
+ this.resource = resource;
9459
+ this.why = why;
9460
+ this.name = "ResidentReuseRefusedError";
9461
+ }
9462
+ resource;
9463
+ why;
9464
+ };
9454
9465
  ResidentOperations = class {
9455
9466
  constructor(opts) {
9456
9467
  this.opts = opts;
@@ -9608,6 +9619,7 @@ var init_resident = __esm({
9608
9619
  if (this.opts.refHint) body.refHint = this.opts.refHint;
9609
9620
  if (this.opts.readonly) body.readonly = true;
9610
9621
  if (this.opts.sha) body.sha = this.opts.sha;
9622
+ if (this.opts.reuse) body.reuse = true;
9611
9623
  const answered = await this.call("/attach", body, timeoutMs, void 0, span);
9612
9624
  const data = answered.data;
9613
9625
  const status3 = answered.status === 200 && typeof data.error === "string" && typeof data.status === "number" ? data.status : answered.status;
@@ -9621,6 +9633,7 @@ var init_resident = __esm({
9621
9633
  sha: data.sha,
9622
9634
  ...typeof data.workspace === "string" && data.workspace ? { workspace: data.workspace } : {},
9623
9635
  ...typeof data.user === "string" && data.user ? { user: data.user } : {},
9636
+ ...typeof data.container === "string" && data.container ? { container: data.container } : {},
9624
9637
  ...trace.length > 0 ? { trace } : {},
9625
9638
  ...typeof data.attachMs === "number" && Number.isFinite(data.attachMs) ? { attachMs: data.attachMs } : {}
9626
9639
  };
@@ -9640,6 +9653,8 @@ var init_resident = __esm({
9640
9653
  const defaultRef = typeof data.defaultRef === "string" && data.defaultRef ? data.defaultRef : void 0;
9641
9654
  return traced(new ResidentNeedsRefError(this.opts.resource, defaultRef));
9642
9655
  }
9656
+ if (status3 === 409 && data.needs === "recreate")
9657
+ return traced(new ResidentReuseRefusedError(this.opts.resource, err2));
9643
9658
  if (status3 === 404) return traced(new Error(`resident attach: ${this.opts.resource} is not onboarded (${err2})`));
9644
9659
  return traced(new Error(`resident attach failed for ${this.opts.resource}: ${err2}`));
9645
9660
  }
@@ -9994,11 +10009,33 @@ var init_githubApp = __esm({
9994
10009
  // ../../src/execution/factory.ts
9995
10010
  import { resolve as resolve7 } from "node:path";
9996
10011
  import { mkdirSync as mkdirSync7 } from "node:fs";
10012
+ function workspaceBindingOf(value) {
10013
+ if (typeof value !== "object" || value === null) return void 0;
10014
+ const v = value;
10015
+ if (typeof v.backend !== "string" || !BACKENDS.includes(v.backend)) return void 0;
10016
+ return {
10017
+ backend: v.backend,
10018
+ ...typeof v.workspace === "string" && v.workspace ? { workspace: v.workspace } : {},
10019
+ ...typeof v.user === "string" && v.user ? { user: v.user } : {},
10020
+ ...typeof v.container === "string" && v.container ? { container: v.container } : {}
10021
+ };
10022
+ }
10023
+ function workspaceBindingFor(selection, machine = "repo-resident") {
10024
+ if (machine === "none" || selection.backend === void 0) return void 0;
10025
+ const b = selection.binding;
10026
+ return {
10027
+ backend: selection.backend,
10028
+ ...b?.workspace !== void 0 ? { workspace: b.workspace } : {},
10029
+ ...b?.user !== void 0 ? { user: b.user } : {},
10030
+ ...b?.container !== void 0 ? { container: b.container } : {}
10031
+ };
10032
+ }
9997
10033
  async function makeExecutor(opts, ctx, span) {
9998
10034
  const machine = ctx.profile.machine;
9999
10035
  if (machine === "none") {
10000
10036
  return { executor: new NullExecutor(ctx.agent.name), backend: "local" };
10001
10037
  }
10038
+ if (ctx.reattach !== void 0) return reattachWorkspace(opts, ctx, ctx.reattach, span);
10002
10039
  if (machine === "blank") {
10003
10040
  return {
10004
10041
  executor: await makePerThreadExecutor(opts, { threadKey: ctx.threadKey, resolveEnvs: async () => ({}) }),
@@ -10056,6 +10093,58 @@ async function makeExecutor(opts, ctx, span) {
10056
10093
  ...failedAttach ? { trace: failedAttach.steps } : {}
10057
10094
  };
10058
10095
  }
10096
+ async function reattachWorkspace(opts, ctx, recorded, span) {
10097
+ const refuse3 = (why) => new WorkspaceReattachRefusedError(recorded, oneLine(why));
10098
+ if (recorded.backend !== "resident") {
10099
+ const input = ctx.profile.machine === "blank" ? { threadKey: ctx.threadKey, resolveEnvs: async () => ({}) } : perThreadCheckout(ctx);
10100
+ return { executor: await makePerThreadExecutor(opts, input), backend: perThreadBackend(opts) };
10101
+ }
10102
+ const resident = opts.execution?.resident;
10103
+ if (!resident) throw refuse3("no resident backend is configured in this process");
10104
+ if (!ctx.repo) throw refuse3("the run's row names no repository to re-attach on");
10105
+ const tokenEnv = resident.tokenEnv ?? "RESIDENT_OPERATOR_TOKEN";
10106
+ const token2 = processSecrets.named(tokenEnv);
10107
+ if (!token2) throw new Error(`execution.resident is configured but ${tokenEnv} is not set`);
10108
+ const resource = repoResourceId(ctx.repo);
10109
+ const probe = await probeResident(resident, token2, resource, span);
10110
+ if (probe.kind === "unreachable") throw refuse3(`resident unreachable (${probe.error})`);
10111
+ if (!isServiceable(probe.state, probe.reason))
10112
+ throw refuse3(`resident ${probe.state}${probe.reason ? ` (${probe.reason})` : ""}`);
10113
+ const nonWarm = probe.state === "warm" ? void 0 : oneLine(`${probe.state}${probe.reason ? ` (${probe.reason})` : ""}`);
10114
+ const executor = new ResidentExecutor({
10115
+ baseUrl: resident.baseUrl,
10116
+ token: token2.reveal(),
10117
+ resource,
10118
+ threadKey: ctx.threadKey,
10119
+ refHint: ctx.ref,
10120
+ readonly: ctx.profile.identity === "read" ? true : void 0,
10121
+ sha: ctx.headSha,
10122
+ reuse: true
10123
+ });
10124
+ let binding;
10125
+ try {
10126
+ binding = await executor.attach(span);
10127
+ } catch (err2) {
10128
+ throw refuse3(err2 instanceof Error ? err2.message : String(err2));
10129
+ }
10130
+ const same = (recordedValue, answered) => recordedValue === void 0 || answered === void 0 || recordedValue === answered;
10131
+ if (!same(recorded.workspace, binding.workspace) || !same(recorded.user, binding.user)) {
10132
+ const name = (workspace, user) => `${workspace ?? "(unnamed)"} as ${user ?? "(unnamed)"}`;
10133
+ throw refuse3(
10134
+ `the resident's worktree for this thread is ${name(binding.workspace, binding.user)}, not the run's recorded ${name(recorded.workspace, recorded.user)}`
10135
+ );
10136
+ }
10137
+ const where = `${resource.replace(/^repo:/, "")} \xB7 ${binding.ref}@${binding.sha.slice(0, 7)}`;
10138
+ return {
10139
+ executor,
10140
+ resident: true,
10141
+ backend: "resident",
10142
+ ...binding.trace ? { trace: binding.trace } : {},
10143
+ ...binding.attachMs !== void 0 ? { attachMs: binding.attachMs } : {},
10144
+ binding,
10145
+ note: nonWarm ? `resident ${nonWarm} \xB7 ${where} \xB7 re-attached to the run's worktree` : `resident \xB7 ${where} \xB7 re-attached to the run's worktree`
10146
+ };
10147
+ }
10059
10148
  async function openResident(opts, nonWarm, span) {
10060
10149
  let executor = new ResidentExecutor(opts);
10061
10150
  let binding;
@@ -10206,7 +10295,7 @@ function perThreadBackend(opts) {
10206
10295
  const type = opts.execution?.type ?? "local";
10207
10296
  return type === "e2b" ? "e2b" : type === "cloudflare" ? "sandbox" : "local";
10208
10297
  }
10209
- var PROBE_OUTAGE_WINDOW_MS, probeOutage, NullExecutor;
10298
+ var BACKENDS, WorkspaceReattachRefusedError, PROBE_OUTAGE_WINDOW_MS, probeOutage, NullExecutor;
10210
10299
  var init_factory = __esm({
10211
10300
  "../../src/execution/factory.ts"() {
10212
10301
  "use strict";
@@ -10221,6 +10310,17 @@ var init_factory = __esm({
10221
10310
  init_residentState();
10222
10311
  init_clock();
10223
10312
  init_secrets2();
10313
+ BACKENDS = ["local", "resident", "sandbox", "e2b"];
10314
+ WorkspaceReattachRefusedError = class extends Error {
10315
+ constructor(recorded, why) {
10316
+ super(`the run's workspace on the ${recorded.backend} backend could not be re-attached: ${why}`);
10317
+ this.recorded = recorded;
10318
+ this.why = why;
10319
+ this.name = "WorkspaceReattachRefusedError";
10320
+ }
10321
+ recorded;
10322
+ why;
10323
+ };
10224
10324
  PROBE_OUTAGE_WINDOW_MS = 3e4;
10225
10325
  NullExecutor = class {
10226
10326
  constructor(agentName) {
@@ -20508,7 +20608,7 @@ function createRunsService(deps) {
20508
20608
  const analyze = deps.analyze ?? ((events, opts) => analyzeRunFriction(events, opts));
20509
20609
  const clock3 = deps.clock ?? systemClock;
20510
20610
  const warn = deps.warn ?? ((m) => console.warn(m));
20511
- const describe11 = (err2) => err2 instanceof Error ? err2.message : String(err2);
20611
+ const describe12 = (err2) => err2 instanceof Error ? err2.message : String(err2);
20512
20612
  let listing;
20513
20613
  const liveRows = () => {
20514
20614
  if (!ledger) return null;
@@ -20528,7 +20628,7 @@ function createRunsService(deps) {
20528
20628
  try {
20529
20629
  rows = await pending2;
20530
20630
  } catch (err2) {
20531
- warn(`[runs] run ledger list failed \u2014 showing this process's runs only: ${describe11(err2)}`);
20631
+ warn(`[runs] run ledger list failed \u2014 showing this process's runs only: ${describe12(err2)}`);
20532
20632
  return [];
20533
20633
  }
20534
20634
  const foreign = rows.filter((row) => !registry.getById(row.runId));
@@ -20538,7 +20638,7 @@ function createRunsService(deps) {
20538
20638
  try {
20539
20639
  events = await ledger.readEvents(row.runId);
20540
20640
  } catch (err2) {
20541
- warn(`[runs] run ledger events read failed for ${row.runId}: ${describe11(err2)}`);
20641
+ warn(`[runs] run ledger events read failed for ${row.runId}: ${describe12(err2)}`);
20542
20642
  }
20543
20643
  return ledgerView(row, events);
20544
20644
  })
@@ -20552,7 +20652,7 @@ function createRunsService(deps) {
20552
20652
  if (!row) return null;
20553
20653
  return { row, events: await ledger.readEvents(id) };
20554
20654
  } catch (err2) {
20555
- warn(`[runs] run ledger read failed for ${id}: ${describe11(err2)}`);
20655
+ warn(`[runs] run ledger read failed for ${id}: ${describe12(err2)}`);
20556
20656
  return null;
20557
20657
  }
20558
20658
  };
@@ -20570,7 +20670,7 @@ function createRunsService(deps) {
20570
20670
  row = await storeSummary(id);
20571
20671
  } catch (err2) {
20572
20672
  warn(
20573
- `[runs] history store read failed for ${id} \u2014 serving the registry row without its record: ${describe11(err2)}`
20673
+ `[runs] history store read failed for ${id} \u2014 serving the registry row without its record: ${describe12(err2)}`
20574
20674
  );
20575
20675
  return {};
20576
20676
  }
@@ -20741,7 +20841,7 @@ function createRunsService(deps) {
20741
20841
  const r = await ledger.requestStop(id, mode);
20742
20842
  if (r.ok) return { ok: true, value: { id, mode, state: "stopping" } };
20743
20843
  } catch (err2) {
20744
- warn(`[runs] run ledger stop failed for ${id}: ${describe11(err2)}`);
20844
+ warn(`[runs] run ledger stop failed for ${id}: ${describe12(err2)}`);
20745
20845
  }
20746
20846
  }
20747
20847
  return await storeSummary(id) ? conflict : notFound;
@@ -26605,9 +26705,10 @@ var init_runs2 = __esm({
26605
26705
  request.budget = budget;
26606
26706
  }
26607
26707
  const spawn7 = ctx.spawn ?? nullSpawnCapability;
26708
+ const conversation = ctx.conversation ? await ctx.conversation() : void 0;
26608
26709
  const out = await spawn7.spawn(request, {
26609
26710
  remainingMs: ctx.remainingMs?.() ?? Number.POSITIVE_INFINITY,
26610
- ...ctx.conversation ? { conversation: ctx.conversation() } : {}
26711
+ ...conversation ? { conversation } : {}
26611
26712
  });
26612
26713
  if (out.kind === "refused") return `spawn refused (${out.reason}): ${out.message}`;
26613
26714
  return `spawned a ${preset} run: ${out.runId} in thread ${out.threadKey}${out.url ? ` (${out.url})` : ""} \u2014 it runs on its own; await_runs waits for it and returns its reply; get_run_status ${out.runId} answers its progress meanwhile.`;
@@ -26786,6 +26887,252 @@ var init_runs2 = __esm({
26786
26887
  }
26787
26888
  });
26788
26889
 
26890
+ // ../../src/core/dispatch/staging.ts
26891
+ function stagedBasename(index, name) {
26892
+ return `${index}-${safeBasename(name)}`;
26893
+ }
26894
+ function pullCommandFor(url, basename4) {
26895
+ return `mkdir -p ${ATTACHMENTS_DIR} && curl -fsS -o ${shellQuote(`${ATTACHMENTS_DIR}/${basename4}`)} ${shellQuote(url)}`;
26896
+ }
26897
+ function excludeCommand() {
26898
+ const entry = shellQuote(`${ATTACHMENTS_DIR}/`);
26899
+ return `mkdir -p .git/info && { grep -qxF ${entry} .git/info/exclude 2>/dev/null || echo ${entry} >> .git/info/exclude; }`;
26900
+ }
26901
+ function formatSize(bytes) {
26902
+ if (bytes >= 1e9) return `${(bytes / 1e9).toFixed(1)} GB`;
26903
+ if (bytes >= 1e6) return `${Math.round(bytes / 1e6)} MB`;
26904
+ if (bytes >= 1e3) return `${Math.round(bytes / 1e3)} KB`;
26905
+ return `${bytes} B`;
26906
+ }
26907
+ function attachmentsLine(outcomes) {
26908
+ const landed = outcomes.filter((o) => o.error === void 0);
26909
+ const failed = outcomes.filter((o) => o.error !== void 0);
26910
+ const parts = [];
26911
+ if (landed.length > 0) {
26912
+ parts.push(
26913
+ `Attached files are in ./${ATTACHMENTS_DIR}/: ${landed.map(
26914
+ (o) => `${o.basename} (${formatSize(o.file.size)}, ${o.file.type}${o.earlier ? ", from earlier in the thread" : ""})`
26915
+ ).join(", ")}`
26916
+ );
26917
+ }
26918
+ for (const o of failed) parts.push(`${o.file.name} could not be staged: ${o.error}`);
26919
+ return parts.join(". ");
26920
+ }
26921
+ function noWorkspaceLine(files) {
26922
+ return files.map(
26923
+ (f) => `this agent has no workspace for ${f.name} (${formatSize(f.size)}, ${f.type}); ask \`agent:coding\` to work with it`
26924
+ ).join(". ");
26925
+ }
26926
+ function stagingIndex() {
26927
+ let n2 = 0;
26928
+ return () => ++n2;
26929
+ }
26930
+ async function copyStaged(files, deps) {
26931
+ return Promise.all(
26932
+ files.map(async (file) => {
26933
+ const index = deps.nextIndex();
26934
+ const basename4 = stagedBasename(index, file.name);
26935
+ const key = inboundKey(deps.threadKey, file.messageId, index, file.name);
26936
+ try {
26937
+ await deps.store.copyFromUrl({ url: file.url, size: file.size, key });
26938
+ deps.publish?.({
26939
+ type: "artifact",
26940
+ direction: "in",
26941
+ key,
26942
+ name: file.name,
26943
+ size: file.size,
26944
+ contentType: file.type
26945
+ });
26946
+ return { file, basename: basename4, key };
26947
+ } catch (err2) {
26948
+ return { file, basename: basename4, key, error: `the copy into the store failed: ${describe4(err2)}` };
26949
+ }
26950
+ })
26951
+ );
26952
+ }
26953
+ async function pullStaged(outcomes, deps) {
26954
+ const landed = outcomes.filter((o) => o.error === void 0);
26955
+ if (landed.length === 0) return [...outcomes];
26956
+ const timeoutMs = deps.timeoutMs ?? BASH_TIMEOUT_MAX_MS;
26957
+ const opts = { timeoutMs, ...deps.signal ? { signal: deps.signal } : {} };
26958
+ if (deps.resident) await deps.executor.exec(excludeCommand(), opts);
26959
+ const pulled = /* @__PURE__ */ new Map();
26960
+ for (const o of landed) {
26961
+ const url = await deps.store.presignGet(o.key);
26962
+ const out = await deps.executor.exec(pullCommandFor(url, o.basename), opts);
26963
+ pulled.set(o.key, parseExitPrefix(out).failed ? `the pull into the workspace failed: ${out.trim()}` : void 0);
26964
+ }
26965
+ return outcomes.map((o) => {
26966
+ const error = o.error ?? pulled.get(o.key);
26967
+ if (error !== void 0) return { ...o, error };
26968
+ const { error: _dropped, ...landedOutcome } = o;
26969
+ return landedOutcome;
26970
+ });
26971
+ }
26972
+ function stageThreadArtifacts(artifacts, deps) {
26973
+ const outcomes = [];
26974
+ for (const a of artifacts) {
26975
+ const file = { name: a.name, size: a.size, type: a.contentType };
26976
+ if (a.held === void 0) {
26977
+ outcomes.push({
26978
+ file,
26979
+ basename: safeBasename(a.name),
26980
+ key: a.key,
26981
+ earlier: true,
26982
+ error: "the store could not be asked whether it still holds it"
26983
+ });
26984
+ continue;
26985
+ }
26986
+ if (!a.held) {
26987
+ outcomes.push({
26988
+ file,
26989
+ basename: safeBasename(a.name),
26990
+ key: a.key,
26991
+ earlier: true,
26992
+ error: "the store no longer holds it (its retention passed)"
26993
+ });
26994
+ continue;
26995
+ }
26996
+ const basename4 = stagedBasename(deps.nextIndex(), a.name);
26997
+ deps.publish?.({
26998
+ type: "artifact",
26999
+ direction: "in",
27000
+ key: a.key,
27001
+ name: a.name,
27002
+ size: a.size,
27003
+ contentType: a.contentType
27004
+ });
27005
+ outcomes.push({ file, basename: basename4, key: a.key, earlier: true });
27006
+ }
27007
+ return outcomes;
27008
+ }
27009
+ async function stageIntoWorkspace(files, deps) {
27010
+ const outcomes = await pullStaged(await copyStaged(files, deps), deps);
27011
+ return { line: attachmentsLine(outcomes), outcomes };
27012
+ }
27013
+ var ATTACHMENTS_DIR, WorkspaceFiles, describe4;
27014
+ var init_staging = __esm({
27015
+ "../../src/core/dispatch/staging.ts"() {
27016
+ "use strict";
27017
+ init_keys();
27018
+ init_bashTimeout();
27019
+ init_shellQuote();
27020
+ init_runEvents();
27021
+ ATTACHMENTS_DIR = "attachments";
27022
+ WorkspaceFiles = class {
27023
+ paths = /* @__PURE__ */ new Map();
27024
+ /** Every outcome that landed: its key now names a workspace path. */
27025
+ record(outcomes) {
27026
+ for (const o of outcomes) if (o.error === void 0) this.paths.set(o.key, `${ATTACHMENTS_DIR}/${o.basename}`);
27027
+ }
27028
+ pathOf(key) {
27029
+ return this.paths.get(key);
27030
+ }
27031
+ };
27032
+ describe4 = (err2) => err2 instanceof Error ? err2.message : String(err2);
27033
+ }
27034
+ });
27035
+
27036
+ // ../../src/core/dispatch/threadAssets.ts
27037
+ function threadAssetsOf(runsOldestFirst) {
27038
+ const seen = /* @__PURE__ */ new Set();
27039
+ const out = [];
27040
+ for (const run2 of runsOldestFirst) {
27041
+ for (const e of run2.events) {
27042
+ if (e.type !== "artifact" || seen.has(e.key)) continue;
27043
+ seen.add(e.key);
27044
+ out.push({
27045
+ key: e.key,
27046
+ name: e.name,
27047
+ size: e.size,
27048
+ contentType: e.contentType,
27049
+ direction: e.direction,
27050
+ runId: run2.id,
27051
+ ...e.seq !== void 0 ? { seq: e.seq } : {}
27052
+ });
27053
+ }
27054
+ }
27055
+ return out;
27056
+ }
27057
+ async function readThreadAssets(deps, threadKey, warn = (line3) => console.warn(line3)) {
27058
+ const runs = [];
27059
+ let before;
27060
+ try {
27061
+ for (; ; ) {
27062
+ const page2 = await deps.runs.listRuns({
27063
+ status: "all",
27064
+ visibleTo: { kind: "all" },
27065
+ threadKey,
27066
+ limit: THREAD_ASSETS_PAGE,
27067
+ ...before !== void 0 ? { before: before.finishedAt, beforeId: before.id } : {}
27068
+ });
27069
+ runs.push(...page2.runs);
27070
+ if (page2.storeUnavailable) {
27071
+ warn(
27072
+ `[thread] ${threadKey}: listing its runs for their files answered live rows only after ${runs.length} run(s) \u2014 the run store was unavailable`
27073
+ );
27074
+ break;
27075
+ }
27076
+ if (page2.nextBefore === void 0) break;
27077
+ before = page2.nextBefore;
27078
+ }
27079
+ } catch (err2) {
27080
+ warn(
27081
+ `[thread] ${threadKey}: listing its runs for their files stopped after ${runs.length} run(s) \u2014 ${describe5(err2)}`
27082
+ );
27083
+ }
27084
+ const perRun = [];
27085
+ for (const run2 of runs.reverse()) {
27086
+ const events = [];
27087
+ let afterSeq;
27088
+ try {
27089
+ for (; ; ) {
27090
+ const r = await deps.runs.getRunEvents(run2.id, afterSeq === void 0 ? {} : { afterSeq });
27091
+ if (!r.ok) {
27092
+ warn(`[thread] ${run2.id}: reading its files stopped after ${events.length} event(s) \u2014 ${r.error}`);
27093
+ break;
27094
+ }
27095
+ events.push(...r.value.events);
27096
+ if (r.value.nextAfterSeq === void 0) break;
27097
+ afterSeq = r.value.nextAfterSeq;
27098
+ }
27099
+ } catch (err2) {
27100
+ warn(`[thread] ${run2.id}: reading its files failed after ${events.length} event(s) \u2014 ${describe5(err2)}`);
27101
+ }
27102
+ perRun.push({ id: run2.id, events });
27103
+ }
27104
+ return Promise.all(
27105
+ threadAssetsOf(perRun).map(async (asset) => {
27106
+ try {
27107
+ return { ...asset, held: await deps.store.head(asset.key) !== null };
27108
+ } catch (err2) {
27109
+ warn(`[thread] ${asset.key}: the store could not be asked whether it holds it \u2014 ${describe5(err2)}`);
27110
+ return asset;
27111
+ }
27112
+ })
27113
+ );
27114
+ }
27115
+ function describeAsset(asset, path) {
27116
+ const what = `${asset.name} (${formatSize(asset.size)}, ${asset.contentType})`;
27117
+ const origin = asset.direction === "in" ? "received on this thread" : `produced by run ${asset.runId}`;
27118
+ return `${what} \u2014 ${origin}; ${whereIs(asset, path)}`;
27119
+ }
27120
+ function whereIs(asset, path) {
27121
+ if (path !== void 0) return `in this workspace at ./${path}`;
27122
+ if (asset.held === false) return "no longer in the store (its retention passed)";
27123
+ if (asset.held === void 0) return "the store could not be asked whether it still holds it";
27124
+ return asset.direction === "out" ? "on that run's page, not in this workspace" : "in the store, not in this workspace";
27125
+ }
27126
+ var THREAD_ASSETS_PAGE, describe5;
27127
+ var init_threadAssets = __esm({
27128
+ "../../src/core/dispatch/threadAssets.ts"() {
27129
+ "use strict";
27130
+ init_staging();
27131
+ THREAD_ASSETS_PAGE = 50;
27132
+ describe5 = (err2) => err2 instanceof Error ? err2.message : String(err2);
27133
+ }
27134
+ });
27135
+
26789
27136
  // ../../src/core/runLedger/sessionLog.ts
26790
27137
  function sessionKey(threadKey, agent) {
26791
27138
  return `${threadKey}:${agent ?? "-"}`;
@@ -26807,31 +27154,50 @@ var init_sessionLog = __esm({
26807
27154
  });
26808
27155
 
26809
27156
  // ../../src/tools/session.ts
26810
- function sessionCapabilityFor(run2, ledger) {
27157
+ function sessionCapabilityFor(run2, ledger, assets) {
26811
27158
  const session = run2?.session;
26812
27159
  if (!session) return void 0;
26813
27160
  return {
26814
27161
  session,
26815
27162
  search: (query, limit) => ledger.searchSession(session.key, query, limit),
26816
27163
  readTurn: async (idx) => (await ledger.readSession(session.key, idx, idx)).messages[0],
27164
+ readConversation: async () => (await ledger.readSession(session.key, session.seedFrom)).messages,
26817
27165
  readNotepad: () => ledger.readNotepad(session.key),
26818
- writeNotepad: (text) => ledger.writeNotepad(session.key, text)
27166
+ writeNotepad: (text) => ledger.writeNotepad(session.key, text),
27167
+ ...assets ? { assets: assets.read, workspacePathOf: assets.pathOf } : {}
26819
27168
  };
26820
27169
  }
27170
+ function assetView(asset, path) {
27171
+ return {
27172
+ name: asset.name,
27173
+ size: asset.size,
27174
+ type: asset.contentType,
27175
+ [asset.direction === "in" ? "received" : "produced"]: `run ${asset.runId}`,
27176
+ key: asset.key,
27177
+ where: whereIs(asset, path)
27178
+ };
27179
+ }
27180
+ function filesNamedIn(text, catalogue) {
27181
+ return catalogue.filter((a) => [a.name, safeBasename(a.name)].some((n2) => nameAt2(n2).test(text)));
27182
+ }
26821
27183
  async function mayRead(ctx) {
26822
27184
  if (!ctx.runs?.runId) return false;
26823
27185
  const res = await ctx.runs.service.getRun(ctx.runs.runId);
26824
27186
  return res.ok && authorize(ctx.runs.actor, "runs:read", runResource(res.value)).allow;
26825
27187
  }
26826
- var UNAVAILABLE3, DEFAULT_HITS, MAX_HITS, SNIPPET_CHARS, snippetOf, recallTool, notesTool, SESSION_TOOLS;
27188
+ var FILE_BEARING, nameAt2, UNAVAILABLE3, DEFAULT_HITS, MAX_HITS, SNIPPET_CHARS, snippetOf, recallTool, notesTool, SESSION_TOOLS;
26827
27189
  var init_session = __esm({
26828
27190
  "../../src/tools/session.ts"() {
26829
27191
  "use strict";
27192
+ init_keys();
26830
27193
  init_authorize();
27194
+ init_threadAssets();
26831
27195
  init_runEvents();
26832
27196
  init_sessionLog();
26833
27197
  init_runRecord();
26834
27198
  init_runsService();
27199
+ FILE_BEARING = /attachments\/|attach_file|attached files/i;
27200
+ nameAt2 = (name) => new RegExp(`(?<![A-Za-z0-9._])${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![A-Za-z0-9._-])`);
26835
27201
  UNAVAILABLE3 = "the session tools are not available in this context: this run has no session log.";
26836
27202
  DEFAULT_HITS = 5;
26837
27203
  MAX_HITS = 50;
@@ -26842,7 +27208,7 @@ var init_session = __esm({
26842
27208
  };
26843
27209
  recallTool = {
26844
27210
  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).",
27211
+ description: "Search this conversation's whole log \u2014 every turn of this thread on this agent, earlier runs' included and everything a compaction removed from your window \u2014 for words, and get the matching turns in relevance order with their turn numbers and a snippet; or pass `turn` to read one turn whole (a tool result, a command, a message). Use it when you need something said or seen earlier that is not in front of you: a failing test's name, a command's output, a decision. `limit` defaults to 5 (at most 50). A hit that names a file of this thread carries the file's key and where it is for this run; `assets: true` lists every file the thread received or its runs produced instead of searching.",
26846
27212
  inputSchema: {
26847
27213
  type: "object",
26848
27214
  properties: {
@@ -26851,6 +27217,10 @@ var init_session = __esm({
26851
27217
  turn: {
26852
27218
  type: "integer",
26853
27219
  description: "A turn number from an earlier recall: returns that turn whole instead of searching"
27220
+ },
27221
+ assets: {
27222
+ type: "boolean",
27223
+ description: "List the thread's files \u2014 received on its messages or produced by its runs \u2014 with their keys and where each is for this run, instead of searching"
26854
27224
  }
26855
27225
  }
26856
27226
  },
@@ -26859,13 +27229,30 @@ var init_session = __esm({
26859
27229
  if (!ctx.session) return UNAVAILABLE3;
26860
27230
  const turn = typeof input.turn === "number" ? Math.trunc(input.turn) : void 0;
26861
27231
  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: [] });
27232
+ const listAssets = input.assets === true;
27233
+ if (turn === void 0 && query.length === 0 && !listAssets)
27234
+ return "recall needs a query, a turn number or assets: true.";
27235
+ if (!await mayRead(ctx)) {
27236
+ return JSON.stringify(turn !== void 0 ? { turn, content: null } : listAssets ? { assets: [] } : { hits: [] });
27237
+ }
26864
27238
  if (turn !== void 0) {
26865
27239
  const message3 = turn >= 0 ? await ctx.session.readTurn(turn) : void 0;
26866
27240
  if (!message3) return `no turn ${turn} in this session's log.`;
26867
27241
  return JSON.stringify({ turn, role: message3.role, content: message3.content });
26868
27242
  }
27243
+ if (listAssets) {
27244
+ if (!ctx.session.assets) {
27245
+ return JSON.stringify({
27246
+ assets: [],
27247
+ note: "this deployment has no artifact store: the thread's files are not catalogued"
27248
+ });
27249
+ }
27250
+ const assets = await ctx.session.assets();
27251
+ return JSON.stringify({
27252
+ assets: assets.map((a) => assetView(a, ctx.session.workspacePathOf?.(a.key))),
27253
+ ...assets.length === 0 ? { note: "no run of this thread has received or produced a file" } : {}
27254
+ });
27255
+ }
26869
27256
  const limit = Math.min(
26870
27257
  MAX_HITS,
26871
27258
  Math.max(1, typeof input.limit === "number" ? Math.trunc(input.limit) : DEFAULT_HITS)
@@ -26875,12 +27262,17 @@ var init_session = __esm({
26875
27262
  if (hits.length === 0)
26876
27263
  notes.push("no turn of this session's log matches; your notes (notes {}) may hold what you look for");
26877
27264
  if (ctx.session.session.seedFrom === 0) notes.push("this is the first run of its session: the log begins with it");
27265
+ const catalogue = ctx.session.assets && hits.some((h) => FILE_BEARING.test(h.text)) ? await ctx.session.assets() : [];
26878
27266
  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
- })),
27267
+ hits: hits.map((h) => {
27268
+ const files = filesNamedIn(h.text, catalogue);
27269
+ return {
27270
+ turn: h.idx,
27271
+ ...h.role !== void 0 ? { role: h.role } : {},
27272
+ snippet: snippetOf(h.text),
27273
+ ...files.length > 0 ? { files: files.map((a) => assetView(a, ctx.session.workspacePathOf?.(a.key))) } : {}
27274
+ };
27275
+ }),
26884
27276
  ...gaps.length ? {
26885
27277
  gaps: `the log has a gap at ${gaps.map((g) => `turn ${g}`).join(", ")}: a run before it detached from the ledger, so turns it saw after that point are not here`
26886
27278
  } : {},
@@ -27545,7 +27937,11 @@ function imperativeRule(writers) {
27545
27937
  }
27546
27938
  function attachRule(attachers) {
27547
27939
  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.`;
27940
+ return [
27941
+ `Only ${names} can attach or post a file into the thread (the \`attach_file\` tool).`,
27942
+ `A request that names attach_file routes to ${names}, whatever it asks the tool to do \u2014 a probe, a test or a diagnostic of the tool is still a call to it.`,
27943
+ "A request that asks for a file, a screenshot, a recording or an attachment to be posted, attached or sent back routes there too, however read-only the rest of it sounds; a read-only preset can describe a file but never post one."
27944
+ ].join(" ");
27549
27945
  }
27550
27946
  function tidyReason(reason) {
27551
27947
  const line3 = oneLine(redactAndCap(reason, ROUTE_REASON_CAP));
@@ -28613,7 +29009,8 @@ async function attachRoundWorkspace(input) {
28613
29009
  profile,
28614
29010
  repo: input.round.repo,
28615
29011
  ref: input.round.ref,
28616
- headSha: input.round.headSha
29012
+ headSha: input.round.headSha,
29013
+ ...input.round.reattach !== void 0 ? { reattach: input.round.reattach } : {}
28617
29014
  },
28618
29015
  input.span
28619
29016
  );
@@ -29240,157 +29637,6 @@ var init_messages = __esm({
29240
29637
  }
29241
29638
  });
29242
29639
 
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
29640
  // ../../src/core/modelProxy/runBearers.ts
29395
29641
  import { createHash as createHash4, randomBytes as randomBytes4, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
29396
29642
  function token(runId3, secret2) {
@@ -30093,7 +30339,7 @@ async function reserveRun(deps, ctx) {
30093
30339
  return void 0;
30094
30340
  }
30095
30341
  async function attachWorkspace(deps, ctx) {
30096
- const { msg, io, refuse: refuse3, card, shell, closeLines, clock: clock3, agent, profile, repoCtx, root } = ctx;
30342
+ const { msg, io, refuse: refuse3, card, shell, closeLines, clock: clock3, agent, profile, repoCtx, root, reattach } = ctx;
30097
30343
  let round2;
30098
30344
  try {
30099
30345
  round2 = await root.span("dispatch.workspace.attach", async (span) => {
@@ -30118,7 +30364,8 @@ async function attachWorkspace(deps, ctx) {
30118
30364
  profile,
30119
30365
  repo: repoCtx.repo,
30120
30366
  ref: repoCtx.ref,
30121
- headSha: repoCtx.headSha
30367
+ headSha: repoCtx.headSha,
30368
+ ...reattach !== void 0 ? { reattach } : {}
30122
30369
  },
30123
30370
  logKey: msg.threadKey,
30124
30371
  span
@@ -30145,6 +30392,7 @@ async function attachWorkspace(deps, ctx) {
30145
30392
  });
30146
30393
  return { kind: "refused", reason: "which_branch" };
30147
30394
  }
30395
+ if (err2 instanceof WorkspaceReattachRefusedError) return { kind: "reattach_refused", why: err2.why };
30148
30396
  throw err2;
30149
30397
  }
30150
30398
  return { kind: "attached", round: round2 };
@@ -30181,6 +30429,10 @@ function sessionNotesBlock(session) {
30181
30429
  parts.push(
30182
30430
  "SUMMARY OF THE EARLIER CONVERSATION (written when the context was compacted; every earlier turn is still reachable with `recall`):\n" + summary
30183
30431
  );
30432
+ if (session.files && session.files.length > 0)
30433
+ parts.push(
30434
+ "FILES OF THIS THREAD (received on its messages or produced by its runs, as their records name them; `recall {assets: true}` lists them with their keys):\n" + session.files.map((line3) => `- ${line3}`).join("\n")
30435
+ );
30184
30436
  return parts.length ? parts.join("\n\n") : void 0;
30185
30437
  }
30186
30438
  async function composePrompt(deps, ctx) {
@@ -30280,6 +30532,7 @@ var init_provision = __esm({
30280
30532
  init_contract3();
30281
30533
  init_registry();
30282
30534
  init_profile();
30535
+ init_factory();
30283
30536
  init_residentTrace();
30284
30537
  init_resident();
30285
30538
  init_memory();
@@ -30333,6 +30586,7 @@ async function claimRun(deps, ctx) {
30333
30586
  } = ctx;
30334
30587
  const { resident, binding } = selection;
30335
30588
  let ledgerRun = ctx.ledgerRun;
30589
+ const workspaceBinding = workspaceBindingFor(selection, profile.machine);
30336
30590
  if (!resume && reserved) {
30337
30591
  const ledger = deps.runLedger;
30338
30592
  const opened = await root.span(
@@ -30366,6 +30620,7 @@ async function claimRun(deps, ctx) {
30366
30620
  ...requestRow !== void 0 ? { request: requestRow } : {}
30367
30621
  },
30368
30622
  card: card.handle ?? null,
30623
+ ...workspaceBinding !== void 0 ? { state: { binding: workspaceBinding } } : {},
30369
30624
  // The row reserved before the attach (item 42), promoted in place;
30370
30625
  // its hooks (a stop, a fence) were wired at the reservation and stay.
30371
30626
  reservation: reserved,
@@ -30422,6 +30677,7 @@ var init_run2 = __esm({
30422
30677
  init_workspace();
30423
30678
  init_web();
30424
30679
  init_githubApi();
30680
+ init_factory();
30425
30681
  init_runRegistry();
30426
30682
  init_secrets2();
30427
30683
  webCapability = () => sharedWeb ??= makeWebCapability(processSecrets);
@@ -30454,6 +30710,17 @@ const URL_ENV = "SWITCHBOARD_HARNESS_URL";
30454
30710
  /** How long the tool_call hook keeps asking an unreachable bot before it blocks. */
30455
30711
  const AUTHORIZE_WAIT_MS = 90_000;
30456
30712
  const AUTHORIZE_RETRY_MS = 2_000;
30713
+ /** How long one request for a relayed tool's result may take before it is asked
30714
+ * again: over the bot's window (30 s, after which it answers that the call is
30715
+ * still running), under this runtime's own five-minute header timeout on a
30716
+ * fetch, so a hung connection costs a minute of the wait, not five. */
30717
+ const TOOL_REQUEST_TIMEOUT_MS = 60_000;
30718
+ /** How long a relayed call keeps asking a bot it cannot reach before it fails; the cadence is the hook's. */
30719
+ const TOOL_WAIT_MS = 90_000;
30720
+ /** A bot that says a call is still running is asked again after the window it
30721
+ * held the request for; one that answered sooner is asked again no sooner
30722
+ * than this, so a misbehaving bot is never asked in a hot loop. */
30723
+ const TOOL_REASK_FLOOR_MS = 1_000;
30457
30724
 
30458
30725
  function settings() {
30459
30726
  const url = process.env[URL_ENV];
@@ -30527,6 +30794,49 @@ async function authorize(event) {
30527
30794
  }
30528
30795
  }
30529
30796
 
30797
+ /** pi's abort signal for the call, with this request's own timeout beside it
30798
+ * where the runtime can combine the two; pi's alone where it cannot. */
30799
+ function requestSignal(signal) {
30800
+ const timeout = typeof AbortSignal.timeout === "function" ? AbortSignal.timeout(TOOL_REQUEST_TIMEOUT_MS) : undefined;
30801
+ if (!timeout) return signal;
30802
+ if (!signal) return timeout;
30803
+ return typeof AbortSignal.any === "function" ? AbortSignal.any([signal, timeout]) : signal;
30804
+ }
30805
+
30806
+ /** A relayed tool's result. The call is posted with its id; a bot that answers
30807
+ * that the call is still running (a wait on other runs can take minutes) is
30808
+ * asked again with the same id until it answers: the bot runs the call once
30809
+ * and every later ask joins it. A bot that cannot be reached or is failing (a
30810
+ * network error, a timed-out request, a 5xx) is asked again every two seconds
30811
+ * until the wait is up, then the call fails naming why; a refusal at the door
30812
+ * (a 4xx) fails it at once; an abort from pi ends the asking. */
30813
+ async function relay(toolCallId, tool, input, signal) {
30814
+ let lastAnswered = Date.now();
30815
+ let lastError = "";
30816
+ for (;;) {
30817
+ if (signal && signal.aborted) throw new Error("switchboard harness: " + tool + " was aborted before the bot answered");
30818
+ const asked = Date.now();
30819
+ let answer;
30820
+ try {
30821
+ answer = await call("POST", "/harness/tool", { toolCallId, tool, input }, requestSignal(signal));
30822
+ } catch (err) {
30823
+ if (signal && signal.aborted) throw err;
30824
+ const status = err instanceof HarnessAnswerError ? err.status : undefined;
30825
+ if (status !== undefined && status >= 400 && status < 500) throw err;
30826
+ lastError = err instanceof Error ? err.message : String(err);
30827
+ if (Date.now() - lastAnswered >= TOOL_WAIT_MS) {
30828
+ throw new Error("switchboard harness: the bot did not answer " + tool + " for 90 s (" + lastError + ")");
30829
+ }
30830
+ await sleep(AUTHORIZE_RETRY_MS);
30831
+ continue;
30832
+ }
30833
+ lastAnswered = Date.now();
30834
+ if (!answer.pending) return answer;
30835
+ const held = Date.now() - asked;
30836
+ if (held < TOOL_REASK_FLOOR_MS) await sleep(TOOL_REASK_FLOOR_MS - held);
30837
+ }
30838
+ }
30839
+
30530
30840
  function relayTool(def) {
30531
30841
  return {
30532
30842
  name: def.name,
@@ -30534,7 +30844,7 @@ function relayTool(def) {
30534
30844
  description: def.description,
30535
30845
  parameters: def.inputSchema,
30536
30846
  async execute(toolCallId, params, signal) {
30537
- const answer = await call("POST", "/harness/tool", { toolCallId, tool: def.name, input: params }, signal);
30847
+ const answer = await relay(toolCallId, def.name, params, signal);
30538
30848
  if (answer.isError) throw new Error(answer.content.map((c) => (c.type === "text" ? c.text : "")).join("\\n"));
30539
30849
  return { content: answer.content, details: {} };
30540
30850
  },
@@ -30733,6 +31043,9 @@ function readLogScript(path, offset, maxBytes) {
30733
31043
  function aliveScript(pid) {
30734
31044
  return `kill -0 ${pid} 2>/dev/null && echo alive || echo dead`;
30735
31045
  }
31046
+ function identityScript() {
31047
+ return "cat /proc/sys/kernel/random/boot_id 2>/dev/null || true";
31048
+ }
30736
31049
  function killScript(pid) {
30737
31050
  return `kill -TERM -- -${pid} 2>/dev/null; kill -TERM ${pid} 2>/dev/null; sleep 1; kill -KILL -- -${pid} 2>/dev/null; kill -KILL ${pid} 2>/dev/null; true`;
30738
31051
  }
@@ -30749,7 +31062,7 @@ function stdoutOf(operation, out) {
30749
31062
  const stdout = cut >= 0 ? out.slice(0, cut) : out;
30750
31063
  return stdout === "(no output)" ? "" : stdout;
30751
31064
  }
30752
- var PiContainerError, WRITE_CHUNK_CHARS, INLINE_LINE_CHARS, LOG_READ_BYTES, MESSAGE_UPDATE_MARK, LOG_FILTER, STDERR_MARK, OP_TIMEOUT_MS, ExecPiContainer;
31065
+ var PiContainerError, WRITE_CHUNK_CHARS, INLINE_LINE_CHARS, LOG_READ_BYTES, MESSAGE_UPDATE_MARK, LOG_FILTER, IDENTITY_WORD, STDERR_MARK, OP_TIMEOUT_MS, ExecPiContainer;
30753
31066
  var init_container = __esm({
30754
31067
  "../../src/core/harness/pi/container.ts"() {
30755
31068
  "use strict";
@@ -30769,6 +31082,7 @@ var init_container = __esm({
30769
31082
  LOG_READ_BYTES = 48 * 1024;
30770
31083
  MESSAGE_UPDATE_MARK = '"type":"message_update"';
30771
31084
  LOG_FILTER = `grep --line-buffered -v ${shellQuote(MESSAGE_UPDATE_MARK)}`;
31085
+ IDENTITY_WORD = /^[A-Za-z0-9-]{1,64}$/;
30772
31086
  STDERR_MARK = "\n--- stderr ---\n";
30773
31087
  OP_TIMEOUT_MS = 6e4;
30774
31088
  ExecPiContainer = class {
@@ -30782,6 +31096,12 @@ var init_container = __esm({
30782
31096
  async makeRoot(runId3) {
30783
31097
  return piRunPaths(runId3);
30784
31098
  }
31099
+ /** The start script never changes directory, so pi runs where the executor
31100
+ * runs every command: the thread's checkout, which the harness names (a
31101
+ * resident's `/exec` runs in the thread's worktree). No command runs here. */
31102
+ cwd(_paths, checkout) {
31103
+ return checkout;
31104
+ }
30785
31105
  async writeFile(path, content) {
30786
31106
  for (const script of writeFileScripts(path, content)) stdoutOf("write", await this.exec(script));
30787
31107
  }
@@ -30807,6 +31127,16 @@ var init_container = __esm({
30807
31127
  async alive(pid) {
30808
31128
  return stdoutOf("alive", await this.exec(aliveScript(pid))).trim() === "alive";
30809
31129
  }
31130
+ /** One word or nothing: an empty answer, a malformed one or a command the
31131
+ * executor could not run is no identity: a judgement never rests on a guess. */
31132
+ async identity() {
31133
+ try {
31134
+ const word = stdoutOf("identity", await this.exec(identityScript())).trim();
31135
+ return IDENTITY_WORD.test(word) ? word : void 0;
31136
+ } catch {
31137
+ return void 0;
31138
+ }
31139
+ }
30810
31140
  async kill(pid) {
30811
31141
  stdoutOf("kill", await this.exec(killScript(pid)));
30812
31142
  }
@@ -30938,6 +31268,14 @@ var init_botHostContainer = __esm({
30938
31268
  throw new PiContainerError("root", message2(err2));
30939
31269
  }
30940
31270
  }
31271
+ /** The run's own root: `start` spawns pi in it, so that is the directory a
31272
+ * session written for this root names, present on this host in this
31273
+ * generation. The checkout the harness names is a container path this host
31274
+ * does not have (a `none` run has no workspace), and the previous
31275
+ * generation's root is gone with its pi: neither is where pi runs. */
31276
+ cwd(paths, _checkout) {
31277
+ return paths.dir;
31278
+ }
30941
31279
  /** Parents at 700, the file at 600, exact bytes, created and never
30942
31280
  * replaced: under a root this container made nothing is there before the
30943
31281
  * harness writes it, so a path already present is a named failure, never
@@ -31058,6 +31396,12 @@ var init_botHostContainer = __esm({
31058
31396
  const child = this.byPid.get(pid);
31059
31397
  return child !== void 0 && !child.exited;
31060
31398
  }
31399
+ /** The bot host names no container: its pi is a child of one bot generation
31400
+ * and never outlives it, so a resume judges it by its pid alone (`alive`
31401
+ * answers no for a pid this container did not start). */
31402
+ async identity() {
31403
+ return void 0;
31404
+ }
31061
31405
  /** The group first (pi leads it), then pi itself: TERM, the grace, KILL;
31062
31406
  * a pid this container never started, or one already ended, is nothing to end. */
31063
31407
  async kill(pid) {
@@ -31383,6 +31727,11 @@ var init_bridge2 = __esm({
31383
31727
  openSpan(callId) {
31384
31728
  return this.openTools.get(callId)?.span;
31385
31729
  }
31730
+ /** Whether the bridge has read the call's start off the log and not yet its
31731
+ * end: what a relayed request that arrived ahead of the poll waits for. */
31732
+ callOpen(callId) {
31733
+ return this.openTools.has(callId);
31734
+ }
31386
31735
  /** End whatever tool spans a stopped pi left open, so no span outlives the run. */
31387
31736
  closeOpenSpans(reason) {
31388
31737
  for (const [callId, open2] of this.openTools) {
@@ -31704,7 +32053,8 @@ function piHarnessFactsOf(value) {
31704
32053
  logOffset: v.logOffset,
31705
32054
  ...typeof v.sessionFile === "string" ? { sessionFile: v.sessionFile } : {},
31706
32055
  ...typeof v.bearerHash === "string" ? { bearerHash: v.bearerHash } : {},
31707
- ...typeof v.root === "string" ? { root: v.root } : {}
32056
+ ...typeof v.root === "string" ? { root: v.root } : {},
32057
+ ...typeof v.container === "string" ? { container: v.container } : {}
31708
32058
  };
31709
32059
  }
31710
32060
  function relayedTools(tools) {
@@ -31773,6 +32123,24 @@ async function runPiHarness(deps, run2) {
31773
32123
  const deadline = now() + remainingMs;
31774
32124
  const warnAt = deadline - Math.min(3 * 6e4, run2.agent.maxMinutes * 15e3);
31775
32125
  run2.toolContext.remainingMs = () => deadline - now();
32126
+ if (run2.conversation) {
32127
+ const readConversation = run2.conversation;
32128
+ run2.toolContext.conversation = async () => {
32129
+ try {
32130
+ return await readConversation();
32131
+ } catch (err2) {
32132
+ emit({
32133
+ type: "run_note",
32134
+ kind: "seed",
32135
+ summary: redactAndCap(
32136
+ `the conversation could not be read from the session log for a child's seed (${err2 instanceof Error ? err2.message : String(err2)}); a child spawned now starts from its own thread`,
32137
+ 300
32138
+ )
32139
+ });
32140
+ return void 0;
32141
+ }
32142
+ };
32143
+ }
31776
32144
  bridge.turns = run2.resume?.turn ?? 0;
31777
32145
  const mirror = new PiMirror({
31778
32146
  onStep: run2.onStep,
@@ -31802,7 +32170,11 @@ async function runPiHarness(deps, run2) {
31802
32170
  emit,
31803
32171
  toolSpan: (callId) => bridge.openSpan(callId),
31804
32172
  gateSaw: (callId) => bridge.gateSaw(callId),
31805
- toolsBlocked
32173
+ toolsBlocked,
32174
+ callSeen: async (callId) => {
32175
+ for (let waited = 0; !bridge.callOpen(callId) && waited < CALL_SEEN_WAIT_MS; waited += CALL_SEEN_TICK_MS)
32176
+ await deps.sleep(CALL_SEEN_TICK_MS);
32177
+ }
31806
32178
  };
31807
32179
  const forget = deps.registry.register(live);
31808
32180
  let pid;
@@ -31819,21 +32191,27 @@ async function runPiHarness(deps, run2) {
31819
32191
  const recorded = run2.resume?.facts;
31820
32192
  let reattached = false;
31821
32193
  let ended;
32194
+ let elsewhere;
32195
+ const here = await container.identity();
31822
32196
  const honoured = (hash) => hash !== void 0 && (deps.bearers === void 0 || deps.bearers.adopt(run2.runId, hash));
31823
32197
  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
- });
32198
+ if (recorded.container !== void 0 && here !== void 0 && recorded.container !== here) {
32199
+ elsewhere = `pi is elsewhere: the row's pi (pid ${recorded.pid}) ran in container ${recorded.container}, not the one this run was handed (${here}), so it was neither probed nor ended here`;
32200
+ } else {
32201
+ const alive = await container.alive(recorded.pid);
32202
+ if (alive && recorded.root !== void 0 && honoured(recorded.bearerHash)) {
32203
+ reattached = true;
32204
+ paths = piRunPathsAt(recorded.root);
32205
+ } else if (alive) {
32206
+ ended = recorded.root === void 0 ? "named no directory for its pi" : "carried no bearer this generation could honour for its pi";
32207
+ await container.kill(recorded.pid).catch(() => {
32208
+ });
32209
+ }
31832
32210
  }
31833
32211
  }
31834
32212
  if (reattached && recorded && paths !== void 0) {
31835
32213
  pid = recorded.pid;
31836
- facts2 = { ...recorded };
32214
+ facts2 = { ...recorded, ...recorded.container === void 0 && here !== void 0 ? { container: here } : {} };
31837
32215
  transport = new PiRpcTransport({
31838
32216
  container,
31839
32217
  paths,
@@ -31849,7 +32227,7 @@ async function runPiHarness(deps, run2) {
31849
32227
  );
31850
32228
  } else {
31851
32229
  paths = await container.makeRoot(run2.runId);
31852
- if (recorded?.root !== void 0 && recorded.root !== paths.dir)
32230
+ if (recorded?.root !== void 0 && recorded.root !== paths.dir && elsewhere === void 0)
31853
32231
  await container.remove(piRunPathsAt(recorded.root)).catch(() => {
31854
32232
  });
31855
32233
  const spec = {
@@ -31882,7 +32260,7 @@ async function runPiHarness(deps, run2) {
31882
32260
  piSessionFile(
31883
32261
  session.messages,
31884
32262
  {
31885
- cwd: run2.rules.checkout,
32263
+ cwd: container.cwd(paths, run2.rules.checkout),
31886
32264
  model: {
31887
32265
  provider: run2.model.provider,
31888
32266
  id: run2.model.id,
@@ -31896,7 +32274,7 @@ async function runPiHarness(deps, run2) {
31896
32274
  }
31897
32275
  if (run2.resume) {
31898
32276
  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";
32277
+ const how = elsewhere !== void 0 ? `${elsewhere}, and pi restarted` : ended !== void 0 && recorded !== void 0 ? `the row ${ended} (pid ${recorded.pid}), so it was ended and pi restarted` : "pi restarted";
31900
32278
  note(
31901
32279
  "resumed",
31902
32280
  `resumed after a restart: ${how} on the mirrored transcript \u2014 ${lost} call(s) were in flight, each answered with a restart note; ${Math.round(remainingMs / 6e4)} min of budget left`
@@ -31906,7 +32284,13 @@ async function runPiHarness(deps, run2) {
31906
32284
  for (const file of piLaunchFiles(launch)) await container.writeFile(file.path, file.content);
31907
32285
  ({ pid } = await container.start({ paths, args: piLaunchArgs(launch), env: piLaunchEnv(launch, deps.bearer) }));
31908
32286
  const bearerHash = bearerHashOf(deps.bearer);
31909
- facts2 = { pid, logOffset: 0, root: paths.dir, ...bearerHash !== void 0 ? { bearerHash } : {} };
32287
+ facts2 = {
32288
+ pid,
32289
+ logOffset: 0,
32290
+ root: paths.dir,
32291
+ ...bearerHash !== void 0 ? { bearerHash } : {},
32292
+ ...here !== void 0 ? { container: here } : {}
32293
+ };
31910
32294
  save();
31911
32295
  transport = new PiRpcTransport({ container, paths, pid, pollMs: deps.pollMs ?? 750, sleep: deps.sleep });
31912
32296
  }
@@ -32101,7 +32485,7 @@ ${stagedLine}` : prompt,
32101
32485
  agentSpan?.end(hardStopped || bypass ? "error" : "ok");
32102
32486
  }
32103
32487
  }
32104
- var NATIVE_WORKSPACE_TOOLS, FINALE_TIMEOUT_MS2, CONTINUE_PROMPT, PromptRefused, GateBypassed;
32488
+ var NATIVE_WORKSPACE_TOOLS, FINALE_TIMEOUT_MS2, CALL_SEEN_WAIT_MS, CALL_SEEN_TICK_MS, CONTINUE_PROMPT, PromptRefused, GateBypassed;
32105
32489
  var init_harness = __esm({
32106
32490
  "../../src/core/harness/pi/harness.ts"() {
32107
32491
  "use strict";
@@ -32116,6 +32500,8 @@ var init_harness = __esm({
32116
32500
  init_transport();
32117
32501
  NATIVE_WORKSPACE_TOOLS = /* @__PURE__ */ new Set(["bash", "read_file", "write_file"]);
32118
32502
  FINALE_TIMEOUT_MS2 = 3 * 6e4;
32503
+ CALL_SEEN_WAIT_MS = 3e3;
32504
+ CALL_SEEN_TICK_MS = 50;
32119
32505
  CONTINUE_PROMPT = "Continue where you left off: the bot restarted mid-run, so re-check the effects of your last command before relying on them.";
32120
32506
  PromptRefused = class extends Error {
32121
32507
  constructor(reason) {
@@ -32970,6 +33356,7 @@ async function runLoop2(deps, ctx) {
32970
33356
  executor,
32971
33357
  resident: round2.selection.resident !== void 0
32972
33358
  });
33359
+ ctx.workspaceFiles?.record(staged.outcomes);
32973
33360
  return staged.line;
32974
33361
  } : void 0;
32975
33362
  let artifactSeq = 0;
@@ -33018,10 +33405,19 @@ async function runLoop2(deps, ctx) {
33018
33405
  });
33019
33406
  if (harness === "pi" && piFacts) {
33020
33407
  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
- });
33408
+ const here = await container.identity();
33409
+ if (piFacts.container === void 0 || here === void 0 || piFacts.container === here) {
33410
+ await container.kill(piFacts.pid).catch(() => {
33411
+ });
33412
+ if (piFacts.root !== void 0) await container.remove(piRunPathsAt(piFacts.root)).catch(() => {
33413
+ });
33414
+ } else {
33415
+ onEvent({
33416
+ type: "run_note",
33417
+ kind: "resumed",
33418
+ summary: `the run's pi (pid ${piFacts.pid}) ran in container ${piFacts.container}, not the one this run was handed (${here}): it was not ended here`
33419
+ });
33420
+ }
33025
33421
  }
33026
33422
  answer = answerUnderEnding(finish2.answer, loopEnding, agent.maxMinutes);
33027
33423
  } else if (harness === "pi") {
@@ -33066,7 +33462,7 @@ async function runLoop2(deps, ctx) {
33066
33462
  messages,
33067
33463
  tools: relayedTools(mergeTools(TOOLSETS[agent.toolset] ?? [], mcpForRun?.tools)),
33068
33464
  toolContext,
33069
- ...session ? { notepad: () => session.readNotepad() } : {},
33465
+ ...session ? { notepad: () => session.readNotepad(), conversation: () => session.readConversation() } : {},
33070
33466
  rules: {
33071
33467
  checkout: binding?.workspace ?? "/workspace",
33072
33468
  ...ownBranch !== void 0 ? { branch: ownBranch } : {},
@@ -33884,7 +34280,7 @@ async function plan(deps, input) {
33884
34280
  try {
33885
34281
  planId = planIdOf(request.planPath);
33886
34282
  } catch (err2) {
33887
- return { ok: false, reply: `\u{1F6AB} ${describe5(err2)}` };
34283
+ return { ok: false, reply: `\u{1F6AB} ${describe6(err2)}` };
33888
34284
  }
33889
34285
  let text;
33890
34286
  try {
@@ -33892,7 +34288,7 @@ async function plan(deps, input) {
33892
34288
  } catch (err2) {
33893
34289
  return {
33894
34290
  ok: false,
33895
- reply: `\u{1F6AB} The plan \`${request.planPath}\` could not be read at \`${base}\` in ${entry.repo}: ${describe5(err2)}`
34291
+ reply: `\u{1F6AB} The plan \`${request.planPath}\` could not be read at \`${base}\` in ${entry.repo}: ${describe6(err2)}`
33896
34292
  };
33897
34293
  }
33898
34294
  const graph = parsePlanGraph(text, planId);
@@ -33905,7 +34301,7 @@ async function plan(deps, input) {
33905
34301
  try {
33906
34302
  selected = openPlanCursor(graph, request.units).order;
33907
34303
  } catch (err2) {
33908
- return { ok: false, reply: `\u{1F6AB} ${describe5(err2)}` };
34304
+ return { ok: false, reply: `\u{1F6AB} ${describe6(err2)}` };
33909
34305
  }
33910
34306
  return { ok: true, planned: { kind: "plan", planId, path: request.planPath, base, graph, selected, identity: identity2 } };
33911
34307
  }
@@ -34021,7 +34417,7 @@ async function start(deps, input, instance, units, where, write) {
34021
34417
  try {
34022
34418
  answer = await deps.create(instance.id);
34023
34419
  } catch (err2) {
34024
- answer = { kind: "unanswered", reason: describe5(err2) };
34420
+ answer = { kind: "unanswered", reason: describe6(err2) };
34025
34421
  }
34026
34422
  switch (answer.kind) {
34027
34423
  case "created": {
@@ -34043,7 +34439,7 @@ async function start(deps, input, instance, units, where, write) {
34043
34439
  );
34044
34440
  }
34045
34441
  }
34046
- var refused3, describe5, RUNNING, ENDED;
34442
+ var refused3, describe6, RUNNING, ENDED;
34047
34443
  var init_handOff = __esm({
34048
34444
  "../../src/core/coordinator/handOff.ts"() {
34049
34445
  "use strict";
@@ -34051,7 +34447,7 @@ var init_handOff = __esm({
34051
34447
  init_coordinator();
34052
34448
  init_briefs();
34053
34449
  refused3 = (reply) => ({ status: "aborted", reply });
34054
- describe5 = (err2) => err2 instanceof Error ? err2.message : String(err2);
34450
+ describe6 = (err2) => err2 instanceof Error ? err2.message : String(err2);
34055
34451
  RUNNING = /* @__PURE__ */ new Set(["queued", "running", "paused", "waiting", "waitingForPause"]);
34056
34452
  ENDED = /* @__PURE__ */ new Set(["complete", "errored", "terminated"]);
34057
34453
  }
@@ -34744,6 +35140,77 @@ var init_settle = __esm({
34744
35140
  }
34745
35141
  });
34746
35142
 
35143
+ // ../../src/core/dispatch/reattach.ts
35144
+ function carriedWorkspaceBinding(row) {
35145
+ const recorded = workspaceBindingOf(row.state.binding);
35146
+ if (recorded) return recorded;
35147
+ if (row.meta.selection === "resident")
35148
+ return { backend: "resident", ...row.meta.workspace ? { workspace: row.meta.workspace } : {} };
35149
+ if (row.meta.selection === "sandbox" || row.meta.selection === "local") return { backend: row.meta.selection };
35150
+ return void 0;
35151
+ }
35152
+ function lostWorkspaceNote(why, restarts) {
35153
+ const lost = `resumed after a restart: the run's workspace could not be re-attached (${why})`;
35154
+ return restarts ? `${lost}; the run restarts from its request as a new run in this thread` : `${lost}, and the row's request cannot be read, so the run ends here; re-send it to run it again`;
35155
+ }
35156
+ async function abandonLostWorkspace(ctx) {
35157
+ const { refuse: refuse3, card, shell, closeLines, clock: clock3, run: run2, registry, resume, ledgerRun, why } = ctx;
35158
+ const restored = messageFromInbox(resume.row.meta.request ?? {}, resume.row.startedAt);
35159
+ const summary = lostWorkspaceNote(why, restored !== void 0);
35160
+ const note = { type: "run_note", kind: "resumed", summary, at: clock3() };
35161
+ await refuse3("workspace_lost", async () => {
35162
+ registry.publish(run2.id, note);
35163
+ await card.done(
35164
+ shell.close({
35165
+ kind: "not_started",
35166
+ icon: "\u{1F501}",
35167
+ reason: restored ? "workspace lost across the restart; restarting from the request" : "workspace lost across the restart; re-send to run again",
35168
+ ...closeLines(clock3(), false)
35169
+ })
35170
+ );
35171
+ if (ledgerRun)
35172
+ await closeResumedRow(
35173
+ ledgerRun,
35174
+ { ...resume, events: [...resume.events, { ...note, seq: resume.lastSeq + 1 }] },
35175
+ "the run's workspace could not be re-attached"
35176
+ );
35177
+ });
35178
+ console.log(
35179
+ `[resume] ${resume.row.threadKey} run ${resume.row.runId} ${restored ? "restarts from its request" : "ends"}: ${why}`
35180
+ );
35181
+ return restored?.msg;
35182
+ }
35183
+ function prepareRestartTurn(deps, ctx) {
35184
+ const { request, pending: pending2, clock: clock3 } = ctx;
35185
+ const merged = mergeFollowUps(pending2);
35186
+ const receivedAt = clock3();
35187
+ const msg = {
35188
+ ...request,
35189
+ ...merged ? {
35190
+ text: `${request.text}
35191
+
35192
+ ${merged.text}`,
35193
+ ...merged.images || request.images ? { images: [...request.images ?? [], ...merged.images ?? []] } : {},
35194
+ ...merged.documents || request.documents ? { documents: [...request.documents ?? [], ...merged.documents ?? []] } : {},
35195
+ ...merged.staged || request.staged ? { staged: [...request.staged ?? [], ...merged.staged ?? []] } : {}
35196
+ } : {},
35197
+ receivedAt,
35198
+ originAt: void 0
35199
+ };
35200
+ const trace = startRequestRoot(deps, { channel: channelOf(msg.channelId), receivedAt });
35201
+ return { msg, opts: { trace } };
35202
+ }
35203
+ var init_reattach = __esm({
35204
+ "../../src/core/dispatch/reattach.ts"() {
35205
+ "use strict";
35206
+ init_factory();
35207
+ init_inboxMessage();
35208
+ init_requestTrace();
35209
+ init_threadAdmission();
35210
+ init_admission();
35211
+ }
35212
+ });
35213
+
34747
35214
  // ../../src/core/dispatch/lineage.ts
34748
35215
  function lineageOf(newest) {
34749
35216
  if (newest === void 0 || newest.parentRunId === void 0) return void 0;
@@ -34803,31 +35270,6 @@ var init_lineage = __esm({
34803
35270
  });
34804
35271
 
34805
35272
  // ../../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
35273
  async function readThread(service, threadKey) {
34832
35274
  try {
34833
35275
  const page2 = await service.listRuns({
@@ -34869,7 +35311,6 @@ var THREAD_READ_LIMIT;
34869
35311
  var init_thread = __esm({
34870
35312
  "../../src/core/dispatch/thread.ts"() {
34871
35313
  "use strict";
34872
- init_staging();
34873
35314
  THREAD_READ_LIMIT = 8;
34874
35315
  }
34875
35316
  });
@@ -34989,22 +35430,22 @@ function createRunEnding(deps) {
34989
35430
  try {
34990
35431
  deps.registry.seal(id, replyOk === void 0 ? {} : { replyOk });
34991
35432
  } catch (err2) {
34992
- log(`[ending] seal ${id}: ${describe6(err2)}`);
35433
+ log(`[ending] seal ${id}: ${describe7(err2)}`);
34993
35434
  }
34994
35435
  try {
34995
35436
  afterSeal?.();
34996
35437
  } catch (err2) {
34997
- log(`[ending] after seal ${id}: ${describe6(err2)}`);
35438
+ log(`[ending] after seal ${id}: ${describe7(err2)}`);
34998
35439
  }
34999
35440
  }
35000
35441
  for (const entry of pending2.splice(0)) {
35001
35442
  try {
35002
35443
  const result = deps.registry.seal(entry.runId);
35003
35444
  void Promise.resolve(entry.write(result, entry.failedAfterFinish)).catch(
35004
- (err2) => log(`[ending] record ${entry.runId}: ${describe6(err2)}`)
35445
+ (err2) => log(`[ending] record ${entry.runId}: ${describe7(err2)}`)
35005
35446
  );
35006
35447
  } catch (err2) {
35007
- log(`[ending] record ${entry.runId}: ${describe6(err2)}`);
35448
+ log(`[ending] record ${entry.runId}: ${describe7(err2)}`);
35008
35449
  }
35009
35450
  }
35010
35451
  };
@@ -35014,7 +35455,7 @@ function createRunEnding(deps) {
35014
35455
  try {
35015
35456
  deps.onFinished?.(runId3);
35016
35457
  } catch (err2) {
35017
- log(`[ending] finished ${runId3}: ${describe6(err2)}`);
35458
+ log(`[ending] finished ${runId3}: ${describe7(err2)}`);
35018
35459
  }
35019
35460
  },
35020
35461
  register(entry) {
@@ -35042,7 +35483,7 @@ function createRunEnding(deps) {
35042
35483
  }
35043
35484
  };
35044
35485
  }
35045
- function describe6(err2) {
35486
+ function describe7(err2) {
35046
35487
  return err2 instanceof Error ? err2.message : String(err2);
35047
35488
  }
35048
35489
  var init_runEnding = __esm({
@@ -35104,6 +35545,8 @@ async function dispatch(deps, msg, io, opts = {}) {
35104
35545
  let reserved;
35105
35546
  let requestRow;
35106
35547
  let fencedWhileAttaching = false;
35548
+ let resumeRowClosed = false;
35549
+ let restartRequest;
35107
35550
  const reservationHooks = {
35108
35551
  onStop: (mode) => void registered?.control.requestStop(mode),
35109
35552
  onFenced: () => {
@@ -35344,18 +35787,21 @@ async function dispatch(deps, msg, io, opts = {}) {
35344
35787
  await tellLineage({ kind: "started", runId: runId3 });
35345
35788
  const staged = !resume && deps.artifacts && msg.staged && msg.staged.length > 0 ? msg.staged : [];
35346
35789
  const nextStagedIndex = stagingIndex();
35790
+ const workspaceFiles = new WorkspaceFiles();
35347
35791
  const stagedCopies = staged.length > 0 && agent.machine !== "none" ? copyStaged(staged, {
35348
35792
  store: deps.artifacts,
35349
35793
  threadKey: msg.threadKey,
35350
35794
  nextIndex: nextStagedIndex,
35351
35795
  publish: (e) => registry.publish(runId3, e)
35352
35796
  }) : 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
- })
35797
+ const threadAssets = !resume && deps.artifacts && thread && thread.length > 0 ? readThreadAssets({ runs: runsService, store: deps.artifacts }, msg.threadKey).then(
35798
+ (assets) => assets.filter((a) => a.runId !== runId3)
35799
+ ) : void 0;
35800
+ const earlierStaged = threadAssets && agent.machine !== "none" ? threadAssets.then(
35801
+ (assets) => stageThreadArtifacts(
35802
+ assets.filter((a) => a.direction === "in"),
35803
+ { nextIndex: nextStagedIndex, publish: (e) => registry.publish(runId3, e) }
35804
+ )
35359
35805
  ) : void 0;
35360
35806
  const reservation = await reserveRun(deps, {
35361
35807
  msg,
@@ -35382,6 +35828,7 @@ async function dispatch(deps, msg, io, opts = {}) {
35382
35828
  reserved = reservation.reserved;
35383
35829
  requestRow = reservation.requestRow;
35384
35830
  }
35831
+ const reattach = resume ? carriedWorkspaceBinding(resume.row) : void 0;
35385
35832
  const attach = await attachWorkspace(deps, {
35386
35833
  msg,
35387
35834
  io,
@@ -35393,10 +35840,35 @@ async function dispatch(deps, msg, io, opts = {}) {
35393
35840
  agent,
35394
35841
  profile,
35395
35842
  repoCtx,
35396
- root
35843
+ root,
35844
+ ...reattach !== void 0 ? { reattach } : {}
35397
35845
  });
35398
35846
  if (attach.kind === "refused") return ended;
35847
+ if (attach.kind === "reattach_refused") {
35848
+ if (resume) {
35849
+ restartRequest = await abandonLostWorkspace({
35850
+ msg,
35851
+ io,
35852
+ refuse: refuse3,
35853
+ card,
35854
+ shell,
35855
+ closeLines,
35856
+ clock: clock3,
35857
+ run: run2,
35858
+ registry,
35859
+ resume,
35860
+ ledgerRun,
35861
+ why: attach.why
35862
+ });
35863
+ resumeRowClosed = true;
35864
+ }
35865
+ return ended;
35866
+ }
35399
35867
  const { round: round2 } = attach;
35868
+ if (resume && ledgerRun) {
35869
+ const rebound = workspaceBindingFor(round2.selection, profile.machine);
35870
+ if (rebound !== void 0) ledgerRun.setState({ binding: rebound });
35871
+ }
35400
35872
  const { executor, note, resident } = round2.selection;
35401
35873
  if (fencedWhileAttaching) {
35402
35874
  console.log(
@@ -35408,13 +35880,16 @@ async function dispatch(deps, msg, io, opts = {}) {
35408
35880
  }
35409
35881
  const earlier2 = earlierStaged ? await earlierStaged : [];
35410
35882
  if (staged.length > 0 || earlier2.length > 0) {
35411
- const line3 = stagedCopies || earlier2.length > 0 ? attachmentsLine(
35412
- await pullStaged([...stagedCopies ? await stagedCopies : [], ...earlier2], {
35883
+ let line3;
35884
+ if (stagedCopies || earlier2.length > 0) {
35885
+ const pulled = await pullStaged([...stagedCopies ? await stagedCopies : [], ...earlier2], {
35413
35886
  store: deps.artifacts,
35414
35887
  executor,
35415
35888
  resident: resident !== void 0
35416
- })
35417
- ) : noWorkspaceLine(staged);
35889
+ });
35890
+ workspaceFiles.record(pulled);
35891
+ line3 = attachmentsLine(pulled);
35892
+ } else line3 = noWorkspaceLine(staged);
35418
35893
  const request = messages[messages.length - 1];
35419
35894
  if (request && request.role === "user" && line3) {
35420
35895
  request.content = Array.isArray(request.content) ? [...request.content, { type: "text", text: line3 }] : [
@@ -35444,6 +35919,7 @@ async function dispatch(deps, msg, io, opts = {}) {
35444
35919
  if (headGate.headAdopted) publishMeta(repoCtx);
35445
35920
  const isPrReview = agent.name === "review" && repoCtx.repo !== void 0 && repoCtx.pr !== void 0;
35446
35921
  const isCodingPrRun = agent.toolset === "full";
35922
+ const threadFiles = threadAssets ? (await threadAssets).map((a) => describeAsset(a, workspaceFiles.pathOf(a.key))) : [];
35447
35923
  const prompt = await composePrompt(deps, {
35448
35924
  msg,
35449
35925
  agent,
@@ -35460,11 +35936,13 @@ async function dispatch(deps, msg, io, opts = {}) {
35460
35936
  root,
35461
35937
  ...contractBlock !== void 0 && agent.name === "review" ? { contract: contractBlock } : {},
35462
35938
  // 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) ? {
35939
+ // the newest compaction's summary the seed brought back, and the thread's
35940
+ // files (record 0033) with where each is for this run, for the prompt.
35941
+ ...session?.notepad !== void 0 || session?.summary !== void 0 || threadFiles.length > 0 ? {
35465
35942
  session: {
35466
- ...session.notepad !== void 0 ? { notepad: session.notepad } : {},
35467
- ...session.summary !== void 0 ? { summary: session.summary } : {}
35943
+ ...session?.notepad !== void 0 ? { notepad: session.notepad } : {},
35944
+ ...session?.summary !== void 0 ? { summary: session.summary } : {},
35945
+ ...threadFiles.length > 0 ? { files: threadFiles } : {}
35468
35946
  }
35469
35947
  } : {}
35470
35948
  });
@@ -35524,7 +36002,14 @@ async function dispatch(deps, msg, io, opts = {}) {
35524
36002
  seed,
35525
36003
  ...session ? { seedLog: session.log } : {}
35526
36004
  });
35527
- const sessionTools = sessionCapabilityFor(ledgerRun, deps.runLedger);
36005
+ const sessionTools = sessionCapabilityFor(
36006
+ ledgerRun,
36007
+ deps.runLedger,
36008
+ deps.artifacts ? {
36009
+ read: () => readThreadAssets({ runs: runsService, store: deps.artifacts }, msg.threadKey),
36010
+ pathOf: (key) => workspaceFiles.pathOf(key)
36011
+ } : void 0
36012
+ );
35528
36013
  const { spawn: spawn7, runs, steer, wait } = runToolCapabilities(
35529
36014
  { core: deps, dispatch, registry, clock: clock3 },
35530
36015
  {
@@ -35544,6 +36029,7 @@ async function dispatch(deps, msg, io, opts = {}) {
35544
36029
  profile,
35545
36030
  resolved,
35546
36031
  stagingIndex: nextStagedIndex,
36032
+ workspaceFiles,
35547
36033
  provider,
35548
36034
  model,
35549
36035
  messages,
@@ -35646,7 +36132,7 @@ async function dispatch(deps, msg, io, opts = {}) {
35646
36132
  clearInterval(setupHeartbeat);
35647
36133
  ending.drain(void 0);
35648
36134
  if (registered) deps.runBearers?.revoke(registered.id);
35649
- if (resume && ledgerRun && !runLoopStarted) {
36135
+ if (resume && ledgerRun && !runLoopStarted && !resumeRowClosed) {
35650
36136
  const adopted = ledgerRun;
35651
36137
  await root.span(
35652
36138
  "post.history_write",
@@ -35661,7 +36147,15 @@ async function dispatch(deps, msg, io, opts = {}) {
35661
36147
  const stopMode = settled.kind === "handed-on" ? void 0 : settled.stopMode;
35662
36148
  ended.status = caught ? "failed" : refused5 ? "refused" : stopMode ? "stopped" : "completed";
35663
36149
  root.end(caught ? "error" : "ok", { status: ended.status });
35664
- if (settled.kind === "handed-on") {
36150
+ if (restartRequest) {
36151
+ const pending2 = settled.kind === "handed-on" ? settled.pending : [];
36152
+ const restart2 = prepareRestartTurn(deps, { request: restartRequest, pending: pending2, clock: clock3 });
36153
+ await dispatch(deps, restart2.msg, io, restart2.opts).catch(
36154
+ (err2) => console.error(
36155
+ `[dispatch] ${msg.threadKey} restart after a lost workspace failed: ${err2 instanceof Error ? err2.message : String(err2)}`
36156
+ )
36157
+ );
36158
+ } else if (settled.kind === "handed-on") {
35665
36159
  const fresh = prepareFreshTurn(deps, { agent: settled.agent, pending: settled.pending, clock: clock3 });
35666
36160
  await dispatch(deps, fresh.msg, fresh.io, fresh.opts).catch(
35667
36161
  (err2) => console.error(
@@ -35704,10 +36198,13 @@ var init_dispatcher = __esm({
35704
36198
  init_contract();
35705
36199
  init_codingChild();
35706
36200
  init_settle();
36201
+ init_reattach();
36202
+ init_factory();
35707
36203
  init_lineage();
35708
36204
  init_seed();
35709
36205
  init_session();
35710
36206
  init_thread();
36207
+ init_threadAssets();
35711
36208
  init_select();
35712
36209
  init_spawn();
35713
36210
  init_runsService();
@@ -35718,7 +36215,7 @@ var init_dispatcher = __esm({
35718
36215
  });
35719
36216
 
35720
36217
  // ../../src/core/runHistoryWriter.ts
35721
- function describe7(err2) {
36218
+ function describe8(err2) {
35722
36219
  return err2 instanceof Error ? err2.message : String(err2);
35723
36220
  }
35724
36221
  function createRunHistoryWriter(opts) {
@@ -35735,7 +36232,7 @@ function createRunHistoryWriter(opts) {
35735
36232
  try {
35736
36233
  opts.onPersisted?.(id);
35737
36234
  } catch (err2) {
35738
- opts.warn(`[run-history] onPersisted hook failed for ${id}: ${describe7(err2)}`);
36235
+ opts.warn(`[run-history] onPersisted hook failed for ${id}: ${describe8(err2)}`);
35739
36236
  }
35740
36237
  };
35741
36238
  const attemptAll = async (record2, flag2, sink, span) => {
@@ -35758,12 +36255,12 @@ function createRunHistoryWriter(opts) {
35758
36255
  }
35759
36256
  if (err2 instanceof PermanentStoreError) {
35760
36257
  failures++;
35761
- opts.warn(`[run-history] ${record2.id} not persisted (permanent, not retried): ${describe7(err2)}`);
36258
+ opts.warn(`[run-history] ${record2.id} not persisted (permanent, not retried): ${describe8(err2)}`);
35762
36259
  return;
35763
36260
  }
35764
36261
  if (attempt >= attempts) {
35765
36262
  failures++;
35766
- opts.warn(`[run-history] ${record2.id} not persisted after ${attempts} attempts: ${describe7(err2)}`);
36263
+ opts.warn(`[run-history] ${record2.id} not persisted after ${attempts} attempts: ${describe8(err2)}`);
35767
36264
  return;
35768
36265
  }
35769
36266
  await sleep2(jittered(RUN_HISTORY_RETRY_DELAYS_MS[attempt - 1]));
@@ -35783,7 +36280,7 @@ function createRunHistoryWriter(opts) {
35783
36280
  }
35784
36281
  const p = attemptAll(record2, flag2, writeOpts?.via ?? opts.store, writeOpts?.span).catch((err2) => {
35785
36282
  failures++;
35786
- opts.warn(`[run-history] ${record2.id} writer failed unexpectedly: ${describe7(err2)}`);
36283
+ opts.warn(`[run-history] ${record2.id} writer failed unexpectedly: ${describe8(err2)}`);
35787
36284
  }).finally(() => {
35788
36285
  if (flag2) provisionalFlags.delete(flag2);
35789
36286
  void inFlight.delete(p);
@@ -36200,7 +36697,7 @@ function createLedgerWriteThrough(opts) {
36200
36697
  return { outcome: "untracked" };
36201
36698
  }
36202
36699
  if (err2 instanceof PermanentStoreError || attempt >= claimAttempts) {
36203
- warn(`[ledger] ${req.threadKey} not tracked: claim failed after ${attempt} attempt(s): ${describe8(err2)}`);
36700
+ warn(`[ledger] ${req.threadKey} not tracked: claim failed after ${attempt} attempt(s): ${describe9(err2)}`);
36204
36701
  return { outcome: "untracked" };
36205
36702
  }
36206
36703
  await sleep2(RETRY_MS[Math.min(attempt, RETRY_MS.length) - 1]);
@@ -36362,7 +36859,7 @@ function createLedgerWriteThrough(opts) {
36362
36859
  if (!recorded.ok) this.detach(`seed record refused (${recorded.reason})`);
36363
36860
  else this.seeded = true;
36364
36861
  } catch (err2) {
36365
- this.detach(`seed failed: ${describe8(err2)}`);
36862
+ this.detach(`seed failed: ${describe9(err2)}`);
36366
36863
  }
36367
36864
  }
36368
36865
  async step(report2) {
@@ -36389,7 +36886,7 @@ function createLedgerWriteThrough(opts) {
36389
36886
  return;
36390
36887
  } catch (err2) {
36391
36888
  if (err2 instanceof RouteMissingError || err2 instanceof PermanentStoreError || attempt >= 2) {
36392
- this.detach(`step ${record2.step} failed: ${describe8(err2)}`);
36889
+ this.detach(`step ${record2.step} failed: ${describe9(err2)}`);
36393
36890
  return;
36394
36891
  }
36395
36892
  await sleep2(RETRY_MS[0]);
@@ -36422,7 +36919,7 @@ function createLedgerWriteThrough(opts) {
36422
36919
  } catch (err2) {
36423
36920
  if (err2 instanceof RouteMissingError || err2 instanceof PermanentStoreError || attempt >= 2) {
36424
36921
  this.stateDirty = true;
36425
- warn(`[ledger] ${this.threadKey} state not written: ${describe8(err2)}`);
36922
+ warn(`[ledger] ${this.threadKey} state not written: ${describe9(err2)}`);
36426
36923
  return;
36427
36924
  }
36428
36925
  await sleep2(RETRY_MS[0]);
@@ -36440,7 +36937,7 @@ function createLedgerWriteThrough(opts) {
36440
36937
  this.detach(`finishing refused (fenced)`);
36441
36938
  return "fenced";
36442
36939
  } catch (err2) {
36443
- warn(`[ledger] ${this.threadKey} finishing failed: ${describe8(err2)}`);
36940
+ warn(`[ledger] ${this.threadKey} finishing failed: ${describe9(err2)}`);
36444
36941
  return "unavailable";
36445
36942
  }
36446
36943
  }
@@ -36455,7 +36952,7 @@ function createLedgerWriteThrough(opts) {
36455
36952
  warn(`[ledger] ${this.threadKey} abandon refused (fenced) \u2014 the row is another generation's`);
36456
36953
  } catch (err2) {
36457
36954
  warn(
36458
- `[ledger] ${this.threadKey} abandon failed: ${describe8(err2)} \u2014 the sweep will take the row once its lease lapses`
36955
+ `[ledger] ${this.threadKey} abandon failed: ${describe9(err2)} \u2014 the sweep will take the row once its lease lapses`
36459
36956
  );
36460
36957
  }
36461
36958
  }
@@ -36476,7 +36973,7 @@ function createLedgerWriteThrough(opts) {
36476
36973
  this.onStop(result.stop);
36477
36974
  }
36478
36975
  } catch (err2) {
36479
- warn(`[ledger] ${this.threadKey} heartbeat failed: ${describe8(err2)}`);
36976
+ warn(`[ledger] ${this.threadKey} heartbeat failed: ${describe9(err2)}`);
36480
36977
  }
36481
36978
  }
36482
36979
  stopHeartbeat() {
@@ -36554,7 +37051,7 @@ function createLedgerWriteThrough(opts) {
36554
37051
  warn(`[ledger] inbox push refused for run ${runId3}: no live row \u2014 the follow-up rides in memory only`);
36555
37052
  return void 0;
36556
37053
  } catch (err2) {
36557
- warn(`[ledger] inbox push failed for run ${runId3}: ${describe8(err2)} \u2014 the follow-up rides in memory only`);
37054
+ warn(`[ledger] inbox push failed for run ${runId3}: ${describe9(err2)} \u2014 the follow-up rides in memory only`);
36558
37055
  return void 0;
36559
37056
  }
36560
37057
  },
@@ -36563,7 +37060,7 @@ function createLedgerWriteThrough(opts) {
36563
37060
  return await ledger.readInbox(runId3, afterSeq);
36564
37061
  } catch (err2) {
36565
37062
  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`
37063
+ err2 instanceof RouteMissingError ? `[ledger] state Worker has no inbox read route \u2014 run ${runId3} resumes with the reclaim's inbox snapshot only` : `[ledger] inbox read failed for run ${runId3}: ${describe9(err2)} \u2014 resuming with the reclaim's snapshot only`
36567
37064
  );
36568
37065
  return [];
36569
37066
  }
@@ -36585,12 +37082,12 @@ function createLedgerWriteThrough(opts) {
36585
37082
  for (const r of candidates) if (markedSet.has(r.runId)) r.markHandedOff();
36586
37083
  return { marked };
36587
37084
  } catch (err2) {
36588
- return { marked: [], failed: describe8(err2) };
37085
+ return { marked: [], failed: describe9(err2) };
36589
37086
  }
36590
37087
  }
36591
37088
  };
36592
37089
  }
36593
- var NullLedgerWriteThrough, NullLedgerRun, RETRY_MS, describe8;
37090
+ var NullLedgerWriteThrough, NullLedgerRun, RETRY_MS, describe9;
36594
37091
  var init_writeThrough = __esm({
36595
37092
  "../../src/core/runLedger/writeThrough.ts"() {
36596
37093
  "use strict";
@@ -36670,7 +37167,7 @@ var init_writeThrough = __esm({
36670
37167
  }
36671
37168
  };
36672
37169
  RETRY_MS = [200, 800];
36673
- describe8 = (err2) => err2 instanceof Error ? err2.message : String(err2);
37170
+ describe9 = (err2) => err2 instanceof Error ? err2.message : String(err2);
36674
37171
  }
36675
37172
  });
36676
37173
 
@@ -41316,7 +41813,7 @@ async function reclaimRuns(opts) {
41316
41813
  reclaimed = await ledger.reclaim(gen, now(), LEASE_MS);
41317
41814
  } catch (err2) {
41318
41815
  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`
41816
+ err2 instanceof RouteMissingError ? "[reclaim] state Worker has no run-ledger routes \u2014 nothing to take over" : `[reclaim] reclaim failed: ${describe10(err2)} \u2014 runs the last generation left stay on the ledger until the next boot`
41320
41817
  );
41321
41818
  return outcome;
41322
41819
  }
@@ -41384,8 +41881,8 @@ async function reclaimRuns(opts) {
41384
41881
  `[reclaim] ${row.runId} ${row.threadKey} closed ${status3} (from ${run2.reclaimedFrom}; ${why}; ${events.length} event(s))`
41385
41882
  );
41386
41883
  } 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`);
41884
+ outcome.failed.push({ runId: row.runId, error: describe10(err2) });
41885
+ warn(`[reclaim] ${row.runId} ${row.threadKey}: ${describe10(err2)} \u2014 left on the ledger for the next boot`);
41389
41886
  }
41390
41887
  }
41391
41888
  try {
@@ -41401,7 +41898,7 @@ async function reclaimRuns(opts) {
41401
41898
  });
41402
41899
  }
41403
41900
  } catch (err2) {
41404
- warn(`[reclaim] listing live runs failed: ${describe9(err2)} \u2014 the card sweep runs without the ledger's guard`);
41901
+ warn(`[reclaim] listing live runs failed: ${describe10(err2)} \u2014 the card sweep runs without the ledger's guard`);
41405
41902
  }
41406
41903
  if (reclaimed.length > 0 || outcome.liveElsewhere.length > 0) {
41407
41904
  log(
@@ -41423,7 +41920,7 @@ function startReclaimSweep(opts) {
41423
41920
  await opts.onOutcome(outcome);
41424
41921
  }
41425
41922
  } catch (err2) {
41426
- warn(`[reclaim] sweep failed: ${describe9(err2)}`);
41923
+ warn(`[reclaim] sweep failed: ${describe10(err2)}`);
41427
41924
  } finally {
41428
41925
  running = false;
41429
41926
  }
@@ -41449,7 +41946,7 @@ async function completenessVerdict(ledger, row, lastStep) {
41449
41946
  const source2 = transcriptSource(row.meta);
41450
41947
  transcript = source2.kind === "session" ? await ledger.readSession(source2.key, source2.from) : await ledger.readTranscript(row.runId);
41451
41948
  } catch (err2) {
41452
- return { resumable: false, why: `transcript unreadable (${describe9(err2)})` };
41949
+ return { resumable: false, why: `transcript unreadable (${describe10(err2)})` };
41453
41950
  }
41454
41951
  if (!transcript.complete) return { resumable: false, why: `transcript incomplete: ${transcript.gap}` };
41455
41952
  const verdict = transcriptCompleteness({
@@ -41470,7 +41967,7 @@ async function completenessVerdict(ledger, row, lastStep) {
41470
41967
  return { resumable: false, why: verdict.why };
41471
41968
  }
41472
41969
  }
41473
- var TERMINAL, describe9;
41970
+ var TERMINAL, describe10;
41474
41971
  var init_boot = __esm({
41475
41972
  "../../src/core/boot.ts"() {
41476
41973
  "use strict";
@@ -41487,7 +41984,7 @@ var init_boot = __esm({
41487
41984
  "failed",
41488
41985
  "interrupted"
41489
41986
  ]);
41490
- describe9 = (err2) => err2 instanceof Error ? err2.message : String(err2);
41987
+ describe10 = (err2) => err2 instanceof Error ? err2.message : String(err2);
41491
41988
  }
41492
41989
  });
41493
41990
 
@@ -42336,6 +42833,17 @@ var init_toolRules = __esm({
42336
42833
  });
42337
42834
 
42338
42835
  // ../../src/core/harness/pi/relay.ts
42836
+ async function relayToolCall(harness, calls, ask, opts = {}) {
42837
+ const answer = calls.join(ask.toolCallId, () => runRelayedTool(harness, ask, { signal: calls.signal }));
42838
+ const window = new AbortController();
42839
+ const sleep2 = opts.sleep ?? sleepUnlessAborted;
42840
+ const progress = await Promise.race([
42841
+ answer.then((a) => ({ done: true, answer: a })),
42842
+ sleep2(opts.windowMs ?? RELAY_POLL_WINDOW_MS, window.signal).then(() => ({ done: false }))
42843
+ ]);
42844
+ window.abort();
42845
+ return progress;
42846
+ }
42339
42847
  function relayedToolDefinitions(harness) {
42340
42848
  return harness.tools.map(({ name, description, inputSchema }) => ({ name, description, inputSchema }));
42341
42849
  }
@@ -42356,18 +42864,23 @@ function authorizeToolCall(harness, ask) {
42356
42864
  if (verdict.verdict === "allowed") return { allow: true };
42357
42865
  return refuse3(verdict.reason);
42358
42866
  }
42359
- async function runRelayedTool(harness, ask) {
42867
+ async function runRelayedTool(harness, ask, opts = {}) {
42360
42868
  const tool = harness.tools.find((t) => t.name === ask.tool);
42361
42869
  if (!tool) return { content: [{ type: "text", text: `Unknown tool: ${ask.tool}` }], isError: true };
42362
42870
  const span = harness.toolSpan(ask.toolCallId);
42363
42871
  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) };
42872
+ const readConversation = base.conversation;
42873
+ const ctx = {
42874
+ ...span ? {
42875
+ ...base,
42876
+ span,
42877
+ executor: new TracingExecutor(base.executor, span, harness.backend),
42878
+ publish: (e) => harness.emit(withSpanId2(e, span.id)),
42879
+ ...base.github?.api.withSpan ? { github: { ...base.github, api: base.github.api.withSpan(span) } } : {}
42880
+ } : { ...base, publish: (e) => harness.emit(e) },
42881
+ ...opts.signal && !base.signal ? { signal: opts.signal } : {},
42882
+ ...readConversation && harness.callSeen ? { conversation: () => harness.callSeen(ask.toolCallId).then(() => readConversation()) } : {}
42883
+ };
42371
42884
  try {
42372
42885
  const input = typeof ask.input === "object" && ask.input !== null ? ask.input : {};
42373
42886
  const output = await tool.run(input, ctx);
@@ -42395,30 +42908,64 @@ function withSpanId2(e, spanId) {
42395
42908
  return e;
42396
42909
  }
42397
42910
  }
42398
- var HarnessRegistry;
42911
+ var HarnessRegistry, RELAY_POLL_WINDOW_MS, RelayedCalls;
42399
42912
  var init_relay = __esm({
42400
42913
  "../../src/core/harness/pi/relay.ts"() {
42401
42914
  "use strict";
42402
42915
  init_tracingExecutor();
42403
42916
  init_types3();
42917
+ init_awaitChildren();
42404
42918
  init_runEvents();
42405
42919
  init_toolRules();
42406
42920
  HarnessRegistry = class {
42407
42921
  live = /* @__PURE__ */ new Map();
42408
- /** The run drives a pi from here; the returned function forgets it. */
42922
+ /** The run drives a pi from here, its relayed calls kept beside it; the
42923
+ * returned function forgets it and ends whatever call still runs. */
42409
42924
  register(harness) {
42410
- this.live.set(harness.runId, harness);
42925
+ const entry = { harness, calls: new RelayedCalls() };
42926
+ this.live.set(harness.runId, entry);
42411
42927
  return () => {
42412
- if (this.live.get(harness.runId) === harness) this.live.delete(harness.runId);
42928
+ if (this.live.get(harness.runId) !== entry) return;
42929
+ this.live.delete(harness.runId);
42930
+ entry.calls.end();
42413
42931
  };
42414
42932
  }
42415
42933
  get(runId3) {
42416
- return this.live.get(runId3);
42934
+ return this.live.get(runId3)?.harness;
42935
+ }
42936
+ /** The run's relayed calls, for as long as it is registered. */
42937
+ calls(runId3) {
42938
+ return this.live.get(runId3)?.calls;
42417
42939
  }
42418
42940
  size() {
42419
42941
  return this.live.size;
42420
42942
  }
42421
42943
  };
42944
+ RELAY_POLL_WINDOW_MS = 3e4;
42945
+ RelayedCalls = class {
42946
+ calls = /* @__PURE__ */ new Map();
42947
+ ending = new AbortController();
42948
+ get size() {
42949
+ return this.calls.size;
42950
+ }
42951
+ /** Aborted when the run ends: the signal every call here runs under. */
42952
+ get signal() {
42953
+ return this.ending.signal;
42954
+ }
42955
+ /** The call's answer as a promise: started by `start` on the first ask, the same promise after. */
42956
+ join(callId, start3) {
42957
+ let answer = this.calls.get(callId);
42958
+ if (!answer) {
42959
+ answer = start3();
42960
+ this.calls.set(callId, answer);
42961
+ }
42962
+ return answer;
42963
+ }
42964
+ end() {
42965
+ this.ending.abort();
42966
+ this.calls.clear();
42967
+ }
42968
+ };
42422
42969
  }
42423
42970
  });
42424
42971
 
@@ -42467,7 +43014,15 @@ async function handleHarnessRequest(deps, req) {
42467
43014
  deps.log?.(`[harness] run=${door.runId} authorize ${ask.tool} \u2192 ${answer2.allow ? "allow" : "refuse"}`);
42468
43015
  return { status: 200, body: { ...answer2 } };
42469
43016
  }
42470
- const answer = await runRelayedTool(harness, ask);
43017
+ const progress = await relayToolCall(harness, deps.harnesses.calls(door.runId), ask, {
43018
+ ...deps.relayWindowMs !== void 0 ? { windowMs: deps.relayWindowMs } : {},
43019
+ ...deps.sleep ? { sleep: deps.sleep } : {}
43020
+ });
43021
+ if (!progress.done) {
43022
+ deps.log?.(`[harness] run=${door.runId} tool ${ask.tool} \u2192 pending`);
43023
+ return { status: 202, body: { pending: true, toolCallId: ask.toolCallId } };
43024
+ }
43025
+ const { answer } = progress;
42471
43026
  deps.log?.(`[harness] run=${door.runId} tool ${ask.tool} \u2192 ${answer.isError ? "error" : "ok"}`);
42472
43027
  return { status: 200, body: { ...answer } };
42473
43028
  }
@@ -42803,7 +43358,7 @@ async function openThreadFromRequester(deps, instance, lead, at) {
42803
43358
  }
42804
43359
  };
42805
43360
  } catch (err2) {
42806
- return { ok: false, response: json(502, { ok: false, error: "thread_failed", message: describe10(err2), at }) };
43361
+ return { ok: false, response: json(502, { ok: false, error: "thread_failed", message: describe11(err2), at }) };
42807
43362
  }
42808
43363
  }
42809
43364
  async function ensureReviewThread(deps, instance, row, at) {
@@ -42929,8 +43484,8 @@ async function spawn6(body, deps) {
42929
43484
  try {
42930
43485
  turn = await composeChild(req.brief, instance, row, briefReaders(deps, instance, io));
42931
43486
  } 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 });
43487
+ log(`[coordinator] ${instance.id} ${req.step}: the brief could not be composed: ${describe11(err2)}`);
43488
+ return json(502, { ok: false, error: "brief_failed", message: describe11(err2), at });
42934
43489
  }
42935
43490
  } else {
42936
43491
  turn = { prompt: req.prompt };
@@ -42980,7 +43535,7 @@ async function spawn6(body, deps) {
42980
43535
  );
42981
43536
  void settled.then((end) => {
42982
43537
  if (end.kind === "threw")
42983
- log(`[coordinator] ${instance.id} ${req.step}: the child's dispatch threw: ${describe10(end.err)}`);
43538
+ log(`[coordinator] ${instance.id} ${req.step}: the child's dispatch threw: ${describe11(end.err)}`);
42984
43539
  });
42985
43540
  const first = await Promise.race([started.then((id) => ({ kind: "started", id })), settled]);
42986
43541
  if (first.kind === "started" || startedId !== void 0) {
@@ -42988,7 +43543,7 @@ async function spawn6(body, deps) {
42988
43543
  log(`[coordinator] ${instance.id} ${req.step}: spawned ${req.preset} run ${runId3} in ${threadKey}`);
42989
43544
  return json(200, { ok: true, runId: runId3, threadKey, at });
42990
43545
  }
42991
- if (first.kind === "threw") return json(502, { ok: false, error: "spawn_failed", message: describe10(first.err), at });
43546
+ if (first.kind === "threw") return json(502, { ok: false, error: "spawn_failed", message: describe11(first.err), at });
42992
43547
  const refusal = first.outcome.refusal;
42993
43548
  if (refusal === "coordinator_thread_live") {
42994
43549
  const now = await liveOnThread(deps.runs, instance, threadKey);
@@ -43141,7 +43696,7 @@ async function prCheck(body, deps) {
43141
43696
  at
43142
43697
  });
43143
43698
  } catch (err2) {
43144
- return json(502, { ok: false, error: "github_unavailable", message: describe10(err2), at });
43699
+ return json(502, { ok: false, error: "github_unavailable", message: describe11(err2), at });
43145
43700
  }
43146
43701
  }
43147
43702
  async function plan2(body, deps) {
@@ -43250,7 +43805,7 @@ async function branch(body, deps) {
43250
43805
  await deps.createBranchRef(instance.repo, name, base);
43251
43806
  return json(200, { ok: true, branch: name, base, at });
43252
43807
  } catch (err2) {
43253
- return json(200, { ok: false, reason: describe10(err2), at });
43808
+ return json(200, { ok: false, reason: describe11(err2), at });
43254
43809
  }
43255
43810
  }
43256
43811
  function unitLines(units) {
@@ -43302,7 +43857,7 @@ async function round(body, deps) {
43302
43857
  instance,
43303
43858
  units.map((u) => u.unit === updated.unit ? updated : u)
43304
43859
  ).catch(
43305
- (err2) => (deps.log ?? console.warn)(`[coordinator] ${instance.id}: the card could not be redrawn: ${describe10(err2)}`)
43860
+ (err2) => (deps.log ?? console.warn)(`[coordinator] ${instance.id}: the card could not be redrawn: ${describe11(err2)}`)
43306
43861
  );
43307
43862
  return json(200, { ok: true, at });
43308
43863
  }
@@ -43336,7 +43891,7 @@ async function unitEnd(body, deps) {
43336
43891
  told = true;
43337
43892
  } catch (err2) {
43338
43893
  (deps.log ?? console.warn)(
43339
- `[coordinator] ${instance.id} ${row.unit}: the report could not be posted: ${describe10(err2)}`
43894
+ `[coordinator] ${instance.id} ${row.unit}: the report could not be posted: ${describe11(err2)}`
43340
43895
  );
43341
43896
  }
43342
43897
  }
@@ -43350,7 +43905,7 @@ ${ending.report}` + (rendered !== void 0 ? `
43350
43905
  ${rendered}` : "");
43351
43906
  await deps.github.commentIssue(instance.repo, row.issue, comment).catch(
43352
43907
  (err2) => (deps.log ?? console.warn)(
43353
- `[coordinator] ${instance.id} ${row.unit}: the board comment could not be posted: ${describe10(err2)}`
43908
+ `[coordinator] ${instance.id} ${row.unit}: the board comment could not be posted: ${describe11(err2)}`
43354
43909
  )
43355
43910
  );
43356
43911
  }
@@ -43405,7 +43960,7 @@ async function merge(body, deps, subject2) {
43405
43960
  try {
43406
43961
  facts2 = await deps.fetchPrFacts(pr);
43407
43962
  } catch (err2) {
43408
- return json(502, { ok: false, error: "github_unavailable", message: describe10(err2), at });
43963
+ return json(502, { ok: false, error: "github_unavailable", message: describe11(err2), at });
43409
43964
  }
43410
43965
  if (facts2 === void 0)
43411
43966
  return json(502, { ok: false, error: "github_unavailable", message: `${where} could not be read`, at });
@@ -43457,7 +44012,7 @@ async function merge(body, deps, subject2) {
43457
44012
  title: facts2.title ?? `Merge pull request #${pr.number}`
43458
44013
  });
43459
44014
  } catch (err2) {
43460
- return json(502, { ok: false, error: "github_unavailable", message: describe10(err2), at });
44015
+ return json(502, { ok: false, error: "github_unavailable", message: describe11(err2), at });
43461
44016
  }
43462
44017
  if (!merged.ok) {
43463
44018
  log(
@@ -43632,13 +44187,13 @@ function createAdminCoordinatorHandler(deps) {
43632
44187
  }
43633
44188
  write(res, await answerCoordinatorStep(door, read.body, deps));
43634
44189
  } catch (err2) {
43635
- (deps.log ?? console.error)(`[coordinator] ${describe10(err2)}`);
44190
+ (deps.log ?? console.error)(`[coordinator] ${describe11(err2)}`);
43636
44191
  write(res, json(500, { ok: false, error: "internal error" }));
43637
44192
  }
43638
44193
  })();
43639
44194
  };
43640
44195
  }
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;
44196
+ var COORDINATOR_ADMIN_PREFIX, MAX_SPAWN_PROMPT_CHARS, FINISHED_LOOKBACK_PAGES, MAX_ADMIN_BODY_BYTES, UNIT_ID, PRESETS_OF_KIND, invalid, json, describe11, EVERY_RUN, REVIEW_POSTED_CHECKS, REVIEW_POSTED_RECHECK_MS, ROUND_OUTCOMES, ENDING_ICON, STEPS;
43642
44197
  var init_adminCoordinator = __esm({
43643
44198
  "../../src/channels/adminCoordinator.ts"() {
43644
44199
  "use strict";
@@ -43671,7 +44226,7 @@ var init_adminCoordinator = __esm({
43671
44226
  };
43672
44227
  invalid = (error) => ({ ok: false, error });
43673
44228
  json = (status3, body) => ({ status: status3, body });
43674
- describe10 = (err2) => err2 instanceof Error ? err2.message : String(err2);
44229
+ describe11 = (err2) => err2 instanceof Error ? err2.message : String(err2);
43675
44230
  EVERY_RUN = { kind: "all" };
43676
44231
  REVIEW_POSTED_CHECKS = 3;
43677
44232
  REVIEW_POSTED_RECHECK_MS = 2e3;