@centralu/darwin-arm64 0.1.0-beta.20 → 0.1.0-beta.22

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.
@@ -3235,7 +3235,7 @@ var require_stream = __commonJS({
3235
3235
  };
3236
3236
  duplex._final = function(callback) {
3237
3237
  if (ws2.readyState === ws2.CONNECTING) {
3238
- ws2.once("open", function open5() {
3238
+ ws2.once("open", function open6() {
3239
3239
  duplex._final(callback);
3240
3240
  });
3241
3241
  return;
@@ -3256,7 +3256,7 @@ var require_stream = __commonJS({
3256
3256
  };
3257
3257
  duplex._write = function(chunk, encoding, callback) {
3258
3258
  if (ws2.readyState === ws2.CONNECTING) {
3259
- ws2.once("open", function open5() {
3259
+ ws2.once("open", function open6() {
3260
3260
  duplex._write(chunk, encoding, callback);
3261
3261
  });
3262
3262
  return;
@@ -9373,7 +9373,7 @@ function dn(e) {
9373
9373
  function or(e) {
9374
9374
  return e.length > 0 && e.every(dn);
9375
9375
  }
9376
- function rm3(e) {
9376
+ function rm4(e) {
9377
9377
  return Is(e).some((t) => t.endsWith(".jsonl"));
9378
9378
  }
9379
9379
  function Is(e) {
@@ -9523,7 +9523,7 @@ function tS(e, t) {
9523
9523
  if (U1e(t)) return q(e, M1e);
9524
9524
  if (D1e(t)) return q(e, I1e);
9525
9525
  if (N1e(t)) return q(e, L1e);
9526
- return rm3(t) ? q(e, sm) : void 0;
9526
+ return rm4(t) ? q(e, sm) : void 0;
9527
9527
  }
9528
9528
  function z1e(e) {
9529
9529
  return j1e(`${e}.json`);
@@ -9532,7 +9532,7 @@ function j1e(e) {
9532
9532
  return Is(e).includes(F1e);
9533
9533
  }
9534
9534
  function nS(e) {
9535
- return Array.isArray(e) && e.some((t) => typeof t === "string" && rm3(t));
9535
+ return Array.isArray(e) && e.some((t) => typeof t === "string" && rm4(t));
9536
9536
  }
9537
9537
  function H1e(e) {
9538
9538
  return e === "user" || e === "project" || e === "local";
@@ -11008,7 +11008,7 @@ function L3(e) {
11008
11008
  }
11009
11009
  function U3(e) {
11010
11010
  let t = N3(e);
11011
- if (rm3(t)) return;
11011
+ if (rm4(t)) return;
11012
11012
  let n = vt(), r = [t], o = M3(e);
11013
11013
  while (o !== n && r.length <= h2e + 1) {
11014
11014
  let l = M3(o);
@@ -42960,12 +42960,12 @@ function refuse(message, io2 = realIo) {
42960
42960
  import { parseArgs } from "node:util";
42961
42961
  import { randomBytes as randomBytes6 } from "node:crypto";
42962
42962
  import { homedir as homedir14, hostname as hostname4 } from "node:os";
42963
- import { dirname as dirname11, join as join32 } from "node:path";
42963
+ import { dirname as dirname11, join as join33 } from "node:path";
42964
42964
  import { mkdirSync as mkdirSync11, writeSync as writeSync4 } from "node:fs";
42965
42965
 
42966
42966
  // packages/protocol/src/brand.ts
42967
42967
  var APP_NAME = "Centralu";
42968
- var APP_VERSION = "0.1.0-beta.20";
42968
+ var APP_VERSION = "0.1.0-beta.22";
42969
42969
  var APP_SLUG = "centralu";
42970
42970
  var CLIENT_INFO = { name: APP_SLUG, title: APP_NAME, version: APP_VERSION };
42971
42971
  var DATA_DIR = ".centralu";
@@ -58032,7 +58032,7 @@ var ApprovalDetail = external_exports.discriminatedUnion("kind", [
58032
58032
  }),
58033
58033
  /**
58034
58034
  * One project reaching another (#371) — raised by **the host** in the calling session, the first time a session of
58035
- * `from` asks to reach `to`. `access` says how: 'delegate' starts a session in `to` and gives it a task
58035
+ * `from` asks to reach `to`. `access` says how: 'delegate' gives an existing session in `to` a task
58036
58036
  * (ask_project, part B); 'apps' attaches one of `to`'s apps to the caller (part A, with `app` naming it).
58037
58037
  *
58038
58038
  * Unlike `capability`, this one has all three answers: allow once (this call only), always (remembered for this
@@ -58046,14 +58046,16 @@ var ApprovalDetail = external_exports.discriminatedUnion("kind", [
58046
58046
  id: external_exports.string(),
58047
58047
  name: external_exports.string(),
58048
58048
  /**
58049
- * Whether the person trusts the target project (#382). `false` is said on the card: a session ask_project opens
58050
- * there follows that trust. Absent from a host that predates it, read as not saying.
58049
+ * Whether the person trusts the target project (#382). `false` is said on the card: the existing target session
58050
+ * follows that trust. Absent from a host that predates it, read as not saying.
58051
58051
  */
58052
58052
  trusted: external_exports.boolean().optional()
58053
58053
  }),
58054
58054
  /** One line for a person to read: what the caller wants done there (the task, cut short; or the app's use) */
58055
58055
  text: external_exports.string(),
58056
- app: external_exports.object({ appId: external_exports.string(), name: external_exports.string() }).optional()
58056
+ app: external_exports.object({ appId: external_exports.string(), name: external_exports.string() }).optional(),
58057
+ /** The existing session chosen for ask_project; absent from older hosts. */
58058
+ session: external_exports.object({ id: external_exports.string(), name: external_exports.string() }).optional()
58057
58059
  }),
58058
58060
  /**
58059
58061
  * A session asks to start another agent in its own project (#561, start_agent) — raised by **the host** in the
@@ -58533,7 +58535,9 @@ var UiPreferences = external_exports.object({
58533
58535
  * preference, but this is the one record the person's settings already live in; the host reads
58534
58536
  * it when a session starts and on each call.
58535
58537
  */
58536
- sessionTools: external_exports.boolean()
58538
+ sessionTools: external_exports.boolean(),
58539
+ /** Running children per project: null uses preset defaults (auto unlimited, otherwise 4); 0 is unlimited. */
58540
+ agentCeiling: external_exports.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).nullable().optional()
58537
58541
  });
58538
58542
  var UiPreferencesPatch = UiPreferences.partial();
58539
58543
  var DEFAULT_UI_PREFERENCES = {
@@ -59627,14 +59631,9 @@ var SessionInfo = external_exports.object({
59627
59631
  */
59628
59632
  parentSessionId: external_exports.string().nullable().default(null),
59629
59633
  /**
59630
- * The session that asked for this one through ask_project (#371 part B) — a session in another project. Null for
59631
- * every session a person or an app started.
59632
- *
59633
- * The delegated session is an ordinary session of its own project, so the person can watch it and step in; this
59634
- * line is what marks it "asked by" and links back to the caller. On the row (store v45), so the mark survives a
59635
- * restart, and the caller's next ask reuses the session it already asked. The caller may since have been deleted:
59636
- * the id then names a session that is no longer listed. Optional rather than defaulted so a frame or a fixture
59637
- * that predates it needs no change: absent and null read the same.
59634
+ * The same-project session that started this agent (`start_agent`, #561). Person-created and app
59635
+ * sessions have no owner. Old cross-project ask_project ownership is cleared on host startup
59636
+ * (#614); cross-project tasks retain their asking session in conversation `from` instead.
59638
59637
  */
59639
59638
  askedBy: external_exports.string().nullable().optional(),
59640
59639
  ...machineField,
@@ -61760,12 +61759,12 @@ var KeeperAgentProcess = class _KeeperAgentProcess extends EventEmitter {
61760
61759
  await Promise.race([this.ready, new Promise((r) => setTimeout(r, DETACH_WAIT_MS))]);
61761
61760
  const out = this.out;
61762
61761
  if (!out) return;
61763
- await new Promise((resolve11) => {
61764
- if (out.readableEnded || out.destroyed) return resolve11();
61765
- const t = setTimeout(resolve11, DETACH_WAIT_MS);
61762
+ await new Promise((resolve12) => {
61763
+ if (out.readableEnded || out.destroyed) return resolve12();
61764
+ const t = setTimeout(resolve12, DETACH_WAIT_MS);
61766
61765
  const done = () => {
61767
61766
  clearTimeout(t);
61768
- resolve11();
61767
+ resolve12();
61769
61768
  };
61770
61769
  out.once("end", done);
61771
61770
  out.once("close", done);
@@ -61782,7 +61781,7 @@ import { createConnection } from "node:net";
61782
61781
  var CHILDREN_PROTOCOL = 1;
61783
61782
  var REQUEST_TIMEOUT_MS = 15e3;
61784
61783
  function readAnswer(sock, timeoutMs) {
61785
- return new Promise((resolve11, reject) => {
61784
+ return new Promise((resolve12, reject) => {
61786
61785
  let buf = Buffer.alloc(0);
61787
61786
  const done = (err, v) => {
61788
61787
  clearTimeout(timer);
@@ -61790,7 +61789,7 @@ function readAnswer(sock, timeoutMs) {
61790
61789
  sock.off("error", onError);
61791
61790
  sock.off("close", onClose);
61792
61791
  if (err) reject(err);
61793
- else resolve11(v);
61792
+ else resolve12(v);
61794
61793
  };
61795
61794
  const onData = (d2) => {
61796
61795
  buf = Buffer.concat([buf, d2]);
@@ -61819,7 +61818,7 @@ function readAnswer(sock, timeoutMs) {
61819
61818
  });
61820
61819
  }
61821
61820
  function connect(path2, first, timeoutMs) {
61822
- return new Promise((resolve11, reject) => {
61821
+ return new Promise((resolve12, reject) => {
61823
61822
  const sock = createConnection(path2);
61824
61823
  const onError = (e) => reject(e);
61825
61824
  sock.once("error", onError);
@@ -61833,7 +61832,7 @@ function connect(path2, first, timeoutMs) {
61833
61832
  sock.destroy();
61834
61833
  return reject(new Error(String(answer.error ?? "the keeper refused")));
61835
61834
  }
61836
- resolve11({ sock, answer });
61835
+ resolve12({ sock, answer });
61837
61836
  },
61838
61837
  (e) => {
61839
61838
  sock.destroy();
@@ -61919,12 +61918,12 @@ var KeeperChildren = class _KeeperChildren extends EventEmitter2 {
61919
61918
  request(op, body = {}) {
61920
61919
  if (this.closed) return Promise.reject(new Error("the connection to the keeper was lost"));
61921
61920
  const rid = this.nextRid++;
61922
- return new Promise((resolve11, reject) => {
61921
+ return new Promise((resolve12, reject) => {
61923
61922
  const timer = setTimeout(() => {
61924
61923
  if (this.pending.delete(rid)) reject(new Error(`the keeper did not answer ${op}`));
61925
61924
  }, REQUEST_TIMEOUT_MS);
61926
61925
  timer.unref();
61927
- this.pending.set(rid, { resolve: resolve11, reject, timer });
61926
+ this.pending.set(rid, { resolve: resolve12, reject, timer });
61928
61927
  this.sock.write(`${JSON.stringify({ ...body, op, rid })}
61929
61928
  `);
61930
61929
  });
@@ -62105,12 +62104,12 @@ var KeeperPty = class _KeeperPty {
62105
62104
  await Promise.race([this.ready, new Promise((r) => setTimeout(r, 1e3))]);
62106
62105
  const sock = this.sock;
62107
62106
  if (!sock) return;
62108
- await new Promise((resolve11) => {
62109
- if (sock.readableEnded || sock.destroyed) return resolve11();
62110
- const t = setTimeout(resolve11, 1e3);
62107
+ await new Promise((resolve12) => {
62108
+ if (sock.readableEnded || sock.destroyed) return resolve12();
62109
+ const t = setTimeout(resolve12, 1e3);
62111
62110
  const done = () => {
62112
62111
  clearTimeout(t);
62113
- resolve11();
62112
+ resolve12();
62114
62113
  };
62115
62114
  sock.once("end", done);
62116
62115
  sock.once("close", done);
@@ -62473,7 +62472,7 @@ var Drain = class {
62473
62472
  */
62474
62473
  track(label, run) {
62475
62474
  if (this.refusing) return Promise.reject(new DrainCut(REFUSED_MESSAGE));
62476
- return new Promise((resolve11, reject) => {
62475
+ return new Promise((resolve12, reject) => {
62477
62476
  let settled = false;
62478
62477
  const settle = () => {
62479
62478
  if (settled) return false;
@@ -62485,7 +62484,7 @@ var Drain = class {
62485
62484
  const entry = { label, cut: (e) => settle() && reject(e) };
62486
62485
  this.inflight.add(entry);
62487
62486
  Promise.resolve().then(run).then(
62488
- (v) => settle() && resolve11(v),
62487
+ (v) => settle() && resolve12(v),
62489
62488
  (e) => settle() && reject(e)
62490
62489
  );
62491
62490
  });
@@ -62634,7 +62633,7 @@ var HostServer = class _HostServer {
62634
62633
  return this.allowedOrigins.has(origin);
62635
62634
  }
62636
62635
  listen() {
62637
- return new Promise((resolve11, reject) => {
62636
+ return new Promise((resolve12, reject) => {
62638
62637
  const onError = (err) => {
62639
62638
  this.listenError = null;
62640
62639
  if (err.code === "EADDRINUSE") {
@@ -62651,7 +62650,7 @@ var HostServer = class _HostServer {
62651
62650
  this.listenError = null;
62652
62651
  this.http.removeListener("error", onError);
62653
62652
  const addr = this.http.address();
62654
- resolve11(typeof addr === "object" && addr ? addr.port : this.opts.port);
62653
+ resolve12(typeof addr === "object" && addr ? addr.port : this.opts.port);
62655
62654
  });
62656
62655
  });
62657
62656
  }
@@ -62909,17 +62908,17 @@ function portInUseMessage(port2, servedBy) {
62909
62908
  var VIEW_HANDOVER_KEY = "views.handover";
62910
62909
  var VIEW_HANDOVER_MAX_AGE_MS = 10 * 6e4;
62911
62910
  function recordViewHandover(settings, views2, inline, now = Date.now()) {
62912
- const open5 = views2.list().map((v) => {
62911
+ const open6 = views2.list().map((v) => {
62913
62912
  const owner = inline?.owner(v.id);
62914
62913
  return owner ? { ...v, inline: { sessionId: owner.sessionId, callId: owner.callId, tool: owner.tool } } : v;
62915
62914
  });
62916
- if (open5.length === 0) {
62915
+ if (open6.length === 0) {
62917
62916
  settings.deleteAppSetting(VIEW_HANDOVER_KEY);
62918
62917
  return 0;
62919
62918
  }
62920
- const record2 = { at: now, views: open5 };
62919
+ const record2 = { at: now, views: open6 };
62921
62920
  settings.setAppSetting(VIEW_HANDOVER_KEY, JSON.stringify(record2));
62922
- return open5.length;
62921
+ return open6.length;
62923
62922
  }
62924
62923
  function restoreViewHandover(settings, views2, inline, now = Date.now()) {
62925
62924
  const raw = settings.appSetting(VIEW_HANDOVER_KEY);
@@ -63028,15 +63027,15 @@ function buildViewCsp(csp) {
63028
63027
  const frame = sanitizeDomains(csp?.frameDomains);
63029
63028
  const base2 = sanitizeDomains(csp?.baseUriDomains);
63030
63029
  const r = resource.kept.join(" ");
63031
- const join33 = (...parts) => parts.filter(Boolean).join(" ");
63030
+ const join34 = (...parts) => parts.filter(Boolean).join(" ");
63032
63031
  const policy = [
63033
63032
  "default-src 'none'",
63034
- `script-src ${join33("'unsafe-inline'", r)}`,
63035
- `style-src ${join33("'unsafe-inline'", r)}`,
63036
- `img-src ${join33("data: blob:", r)}`,
63037
- `font-src ${join33("data:", r)}`,
63038
- `media-src ${join33("data: blob:", r)}`,
63039
- `worker-src ${join33("blob:", r)}`,
63033
+ `script-src ${join34("'unsafe-inline'", r)}`,
63034
+ `style-src ${join34("'unsafe-inline'", r)}`,
63035
+ `img-src ${join34("data: blob:", r)}`,
63036
+ `font-src ${join34("data:", r)}`,
63037
+ `media-src ${join34("data: blob:", r)}`,
63038
+ `worker-src ${join34("blob:", r)}`,
63040
63039
  `connect-src ${connect2.kept.length ? connect2.kept.join(" ") : "'none'"}`,
63041
63040
  `frame-src ${frame.kept.length ? frame.kept.join(" ") : "'none'"}`,
63042
63041
  "form-action 'none'",
@@ -63640,9 +63639,9 @@ import { join as join15 } from "node:path";
63640
63639
  import { lstatSync as lstatSync2, watch } from "node:fs";
63641
63640
 
63642
63641
  // packages/agent-host/src/dev-services/fs.ts
63643
- import { spawn } from "node:child_process";
63642
+ import { spawn as spawn2 } from "node:child_process";
63644
63643
  import { constants as constants3 } from "node:fs";
63645
- import { lstat as lstat2, mkdir, open, readdir, readlink, realpath as realpath2, rename, rm, stat as stat2, writeFile } from "node:fs/promises";
63644
+ import { lstat as lstat2, mkdir, open as open2, readdir, readlink, realpath as realpath2, rename, rm, stat as stat2, writeFile } from "node:fs/promises";
63646
63645
  import { basename, dirname as dirname2, extname, isAbsolute as isAbsolute2, join as join5, relative as relative2, resolve, sep as sep2 } from "node:path";
63647
63646
 
63648
63647
  // packages/agent-host/src/dev-services/path-guard.ts
@@ -63777,7 +63776,8 @@ function isMissingFsPath(error51) {
63777
63776
  }
63778
63777
 
63779
63778
  // packages/agent-host/src/dev-services/git-exec.ts
63780
- import { execFile } from "node:child_process";
63779
+ import { execFile, spawn } from "node:child_process";
63780
+ import { open } from "node:fs/promises";
63781
63781
  import { devNull } from "node:os";
63782
63782
  import { promisify } from "node:util";
63783
63783
 
@@ -64003,6 +64003,40 @@ async function gitArgv(cwd, args, allowRepoPrograms = false) {
64003
64003
  }
64004
64004
  async function runGit(cwd, args, opts = {}) {
64005
64005
  const argv = await gitArgv(cwd, args, opts.allowRepoPrograms);
64006
+ if (opts.outputFile) {
64007
+ const output = await open(opts.outputFile, "w", 384);
64008
+ try {
64009
+ await new Promise((resolve12, reject) => {
64010
+ const child = spawn(programPath("git"), argv, {
64011
+ cwd,
64012
+ stdio: ["pipe", output.fd, "pipe"],
64013
+ timeout: opts.timeout ?? DEFAULTS.timeout,
64014
+ ...opts.env ? { env: { ...process.env, ...opts.env } } : {}
64015
+ });
64016
+ let stderr = "";
64017
+ let overflow = false;
64018
+ child.stderr.on("data", (chunk) => {
64019
+ if (overflow) return;
64020
+ stderr += chunk.toString();
64021
+ if (Buffer.byteLength(stderr) > (opts.maxBuffer ?? DEFAULTS.maxBuffer)) {
64022
+ overflow = true;
64023
+ child.kill();
64024
+ }
64025
+ });
64026
+ child.on("error", reject);
64027
+ child.on("close", (code2, signal) => {
64028
+ if (code2 === 0 && !overflow) resolve12();
64029
+ else reject(Object.assign(new Error(overflow ? "Git stderr exceeded maxBuffer" : stderr || `Git exited with ${code2 ?? signal}`), { code: code2, stderr }));
64030
+ });
64031
+ child.stdin.on("error", () => {
64032
+ });
64033
+ child.stdin.end(opts.input);
64034
+ });
64035
+ return "";
64036
+ } finally {
64037
+ await output.close();
64038
+ }
64039
+ }
64006
64040
  const run = exec(programPath("git"), argv, {
64007
64041
  cwd,
64008
64042
  timeout: opts.timeout ?? DEFAULTS.timeout,
@@ -64163,7 +64197,7 @@ async function readTextFile(root3, rel) {
64163
64197
  const file2 = await resolveExisting(root3, rel);
64164
64198
  const pathInfo = await stat2(file2, { bigint: true });
64165
64199
  if (!pathInfo.isFile()) fail2("Path is not a regular file");
64166
- const handle = await open(file2, READ_TEXT_FLAGS);
64200
+ const handle = await open2(file2, READ_TEXT_FLAGS);
64167
64201
  try {
64168
64202
  const opened = await handle.stat({ bigint: true });
64169
64203
  if (!opened.isFile()) fail2("Path is not a regular file");
@@ -64229,7 +64263,7 @@ async function copyTree(src, dst, clone2 = cloneTree) {
64229
64263
  async function cloneTree(src, dst) {
64230
64264
  if (!clonesFiles()) return false;
64231
64265
  return new Promise((done) => {
64232
- const p = spawn("/bin/cp", ["-Rc", src, dst], { stdio: "ignore" });
64266
+ const p = spawn2("/bin/cp", ["-Rc", src, dst], { stdio: "ignore" });
64233
64267
  p.on("error", () => done(false));
64234
64268
  p.on("close", (code2) => done(code2 === 0));
64235
64269
  });
@@ -64386,7 +64420,7 @@ function isSymlink(path2) {
64386
64420
  }
64387
64421
 
64388
64422
  // packages/agent-host/src/apps/external/app-process.ts
64389
- import { spawn as spawn2 } from "node:child_process";
64423
+ import { spawn as spawn3 } from "node:child_process";
64390
64424
  import { closeSync as closeSync2, existsSync as existsSync5, mkdirSync, openSync as openSync2, renameSync as renameSync3, statSync as statSync3, writeSync as writeSync3 } from "node:fs";
64391
64425
  import { dirname as dirname3 } from "node:path";
64392
64426
 
@@ -68738,14 +68772,14 @@ function inputRequiredRoundsExceededMessage(method, maxRounds) {
68738
68772
  return `Multi-round-trip request '${method}' still required input after ${maxRounds} rounds (inputRequired.maxRounds)`;
68739
68773
  }
68740
68774
  function sleep(ms3, signal) {
68741
- return new Promise((resolve11, reject) => {
68775
+ return new Promise((resolve12, reject) => {
68742
68776
  if (signal?.aborted) {
68743
68777
  reject(signal.reason instanceof SdkError ? signal.reason : new SdkError(SdkErrorCode.RequestTimeout, String(signal.reason)));
68744
68778
  return;
68745
68779
  }
68746
68780
  const timer = setTimeout(() => {
68747
68781
  signal?.removeEventListener("abort", onAbort);
68748
- resolve11();
68782
+ resolve12();
68749
68783
  }, ms3);
68750
68784
  const onAbort = () => {
68751
68785
  clearTimeout(timer);
@@ -69584,7 +69618,7 @@ var Protocol = class {
69584
69618
  const flowStartedAt = Date.now();
69585
69619
  let onAbort;
69586
69620
  let cleanupMessageId;
69587
- return new Promise((resolve11, reject) => {
69621
+ return new Promise((resolve12, reject) => {
69588
69622
  const earlyReject = (error51) => {
69589
69623
  reject(error51);
69590
69624
  };
@@ -69653,7 +69687,7 @@ var Protocol = class {
69653
69687
  }
69654
69688
  if (decoded.kind === "invalid") return reject(decoded.error);
69655
69689
  if (decoded.kind === "input_required") {
69656
- if (options?.allowInputRequired === true) return resolve11(manualInputRequiredValue(decoded));
69690
+ if (options?.allowInputRequired === true) return resolve12(manualInputRequiredValue(decoded));
69657
69691
  const flow = {
69658
69692
  codec: codec2,
69659
69693
  request,
@@ -69665,11 +69699,11 @@ var Protocol = class {
69665
69699
  params
69666
69700
  }, resultSchema, legOptions)
69667
69701
  };
69668
- return resolve11(this._resolveNonCompleteResult(decoded, flow));
69702
+ return resolve12(this._resolveNonCompleteResult(decoded, flow));
69669
69703
  }
69670
69704
  const result = decoded.result;
69671
69705
  validateStandardSchema(resultSchema, result).then((parseResult) => {
69672
- if (parseResult.success) resolve11(parseResult.data);
69706
+ if (parseResult.success) resolve12(parseResult.data);
69673
69707
  else reject(new SdkError(SdkErrorCode.InvalidResult, `Invalid result for ${request.method}: ${parseResult.error}`));
69674
69708
  }, reject);
69675
69709
  });
@@ -72592,7 +72626,7 @@ var require_compile = /* @__PURE__ */ __commonJSMin(((exports) => {
72592
72626
  ref = (0, resolve_1.resolveUrl)(this.opts.uriResolver, baseId, ref);
72593
72627
  const schOrFunc = root3.refs[ref];
72594
72628
  if (schOrFunc) return schOrFunc;
72595
- let _sch = resolve11.call(this, root3, ref);
72629
+ let _sch = resolve12.call(this, root3, ref);
72596
72630
  if (_sch === void 0) {
72597
72631
  const schema = (_a4 = root3.localRefs) === null || _a4 === void 0 ? void 0 : _a4[ref];
72598
72632
  const { schemaId } = this.opts;
@@ -72618,7 +72652,7 @@ var require_compile = /* @__PURE__ */ __commonJSMin(((exports) => {
72618
72652
  function sameSchemaEnv(s12, s22) {
72619
72653
  return s12.schema === s22.schema && s12.root === s22.root && s12.baseId === s22.baseId;
72620
72654
  }
72621
- function resolve11(root3, ref) {
72655
+ function resolve12(root3, ref) {
72622
72656
  let sch;
72623
72657
  while (typeof (sch = this.refs[ref]) == "string") ref = sch;
72624
72658
  return sch || this.schemas[ref] || resolveSchema.call(this, root3, ref);
@@ -73068,7 +73102,7 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
73068
73102
  else if (typeof uri === "object") uri = parse7(serialize(uri, options), options);
73069
73103
  return uri;
73070
73104
  }
73071
- function resolve11(baseURI, relativeURI, options) {
73105
+ function resolve12(baseURI, relativeURI, options) {
73072
73106
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
73073
73107
  const resolved = resolveComponent(parse7(baseURI, schemelessOptions), parse7(relativeURI, schemelessOptions), schemelessOptions, true);
73074
73108
  schemelessOptions.skipEscape = true;
@@ -73242,7 +73276,7 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
73242
73276
  const fastUri = {
73243
73277
  SCHEMES,
73244
73278
  normalize: normalize2,
73245
- resolve: resolve11,
73279
+ resolve: resolve12,
73246
73280
  resolveComponent,
73247
73281
  equal,
73248
73282
  serialize,
@@ -77857,14 +77891,14 @@ var ProbeWindow = class ProbeWindow2 {
77857
77891
  */
77858
77892
  async exchange(buildRequest, timeoutMs) {
77859
77893
  const id2 = `server-discover-probe-${++this._probeCounter}`;
77860
- return new Promise((resolve11) => {
77894
+ return new Promise((resolve12) => {
77861
77895
  let settled = false;
77862
77896
  const settle = (reply) => {
77863
77897
  if (settled) return;
77864
77898
  settled = true;
77865
77899
  clearTimeout(timer);
77866
77900
  if (this._pending?.id === id2) this._pending = void 0;
77867
- resolve11(reply);
77901
+ resolve12(reply);
77868
77902
  };
77869
77903
  const timer = setTimeout(() => settle({ kind: "timeout" }), timeoutMs);
77870
77904
  this._pending = {
@@ -79070,13 +79104,13 @@ var Client = class extends Protocol {
79070
79104
  let onCallerAbort;
79071
79105
  let resolveOpening;
79072
79106
  let rejectOpening;
79073
- const opening = new Promise((resolve11, reject) => {
79074
- resolveOpening = resolve11;
79107
+ const opening = new Promise((resolve12, reject) => {
79108
+ resolveOpening = resolve12;
79075
79109
  rejectOpening = reject;
79076
79110
  });
79077
79111
  let resolveClosed;
79078
- const closed = new Promise((resolve11) => {
79079
- resolveClosed = resolve11;
79112
+ const closed = new Promise((resolve12) => {
79113
+ resolveClosed = resolve12;
79080
79114
  });
79081
79115
  const settle = (outcome) => {
79082
79116
  if (state === "closed") return;
@@ -79848,8 +79882,8 @@ var StreamTransport = class {
79848
79882
  }
79849
79883
  send(message) {
79850
79884
  if (this.closed) return Promise.reject(new Error("app connection closed"));
79851
- return new Promise((resolve11, reject) => {
79852
- this.writable.write(serializeMessage(message), (err) => err ? reject(err) : resolve11());
79885
+ return new Promise((resolve12, reject) => {
79886
+ this.writable.write(serializeMessage(message), (err) => err ? reject(err) : resolve12());
79853
79887
  });
79854
79888
  }
79855
79889
  /** Closes only our own state. Closing the pipes is the shutdown rule's job (AppProcess.stop). */
@@ -79942,7 +79976,7 @@ var AppProcess = class _AppProcess {
79942
79976
  log.note(`starting: ${spec.command} ${spec.args.join(" ")}`);
79943
79977
  const launch2 = resolveCommand(spec.command, spec.env);
79944
79978
  const spawnedAt = Date.now();
79945
- const child = spawn2(launch2.command, [...launch2.args, ...spec.args], {
79979
+ const child = spawn3(launch2.command, [...launch2.args, ...spec.args], {
79946
79980
  cwd: spec.cwd,
79947
79981
  env: spec.env,
79948
79982
  /*
@@ -80061,12 +80095,12 @@ ${tail}` : head;
80061
80095
  }
80062
80096
  waitExit(ms3) {
80063
80097
  if (!this.alive) return Promise.resolve(true);
80064
- return new Promise((resolve11) => {
80065
- const t = setTimeout(() => resolve11(false), ms3);
80098
+ return new Promise((resolve12) => {
80099
+ const t = setTimeout(() => resolve12(false), ms3);
80066
80100
  t.unref();
80067
80101
  this.exitWaiters.push(() => {
80068
80102
  clearTimeout(t);
80069
- resolve11(true);
80103
+ resolve12(true);
80070
80104
  });
80071
80105
  });
80072
80106
  }
@@ -83653,14 +83687,14 @@ function inputRequiredRoundsExceededMessage2(method, maxRounds) {
83653
83687
  return `Multi-round-trip request '${method}' still required input after ${maxRounds} rounds (inputRequired.maxRounds)`;
83654
83688
  }
83655
83689
  function sleep2(ms3, signal) {
83656
- return new Promise((resolve11, reject) => {
83690
+ return new Promise((resolve12, reject) => {
83657
83691
  if (signal?.aborted) {
83658
83692
  reject(signal.reason instanceof SdkError2 ? signal.reason : new SdkError2(SdkErrorCode2.RequestTimeout, String(signal.reason)));
83659
83693
  return;
83660
83694
  }
83661
83695
  const timer = setTimeout(() => {
83662
83696
  signal?.removeEventListener("abort", onAbort);
83663
- resolve11();
83697
+ resolve12();
83664
83698
  }, ms3);
83665
83699
  const onAbort = () => {
83666
83700
  clearTimeout(timer);
@@ -84454,7 +84488,7 @@ var Protocol2 = class {
84454
84488
  const flowStartedAt = Date.now();
84455
84489
  let onAbort;
84456
84490
  let cleanupMessageId;
84457
- return new Promise((resolve11, reject) => {
84491
+ return new Promise((resolve12, reject) => {
84458
84492
  const earlyReject = (error51) => {
84459
84493
  reject(error51);
84460
84494
  };
@@ -84523,7 +84557,7 @@ var Protocol2 = class {
84523
84557
  }
84524
84558
  if (decoded.kind === "invalid") return reject(decoded.error);
84525
84559
  if (decoded.kind === "input_required") {
84526
- if (options?.allowInputRequired === true) return resolve11(manualInputRequiredValue2(decoded));
84560
+ if (options?.allowInputRequired === true) return resolve12(manualInputRequiredValue2(decoded));
84527
84561
  const flow = {
84528
84562
  codec: codec2,
84529
84563
  request,
@@ -84535,11 +84569,11 @@ var Protocol2 = class {
84535
84569
  params
84536
84570
  }, resultSchema, legOptions)
84537
84571
  };
84538
- return resolve11(this._resolveNonCompleteResult(decoded, flow));
84572
+ return resolve12(this._resolveNonCompleteResult(decoded, flow));
84539
84573
  }
84540
84574
  const result = decoded.result;
84541
84575
  validateStandardSchema2(resultSchema, result).then((parseResult) => {
84542
- if (parseResult.success) resolve11(parseResult.data);
84576
+ if (parseResult.success) resolve12(parseResult.data);
84543
84577
  else reject(new SdkError2(SdkErrorCode2.InvalidResult, `Invalid result for ${request.method}: ${parseResult.error}`));
84544
84578
  }, reject);
84545
84579
  });
@@ -87451,7 +87485,7 @@ var require_compile2 = /* @__PURE__ */ __commonJSMin2(((exports) => {
87451
87485
  ref = (0, resolve_1.resolveUrl)(this.opts.uriResolver, baseId, ref);
87452
87486
  const schOrFunc = root3.refs[ref];
87453
87487
  if (schOrFunc) return schOrFunc;
87454
- let _sch = resolve11.call(this, root3, ref);
87488
+ let _sch = resolve12.call(this, root3, ref);
87455
87489
  if (_sch === void 0) {
87456
87490
  const schema = (_a4 = root3.localRefs) === null || _a4 === void 0 ? void 0 : _a4[ref];
87457
87491
  const { schemaId } = this.opts;
@@ -87477,7 +87511,7 @@ var require_compile2 = /* @__PURE__ */ __commonJSMin2(((exports) => {
87477
87511
  function sameSchemaEnv(s12, s22) {
87478
87512
  return s12.schema === s22.schema && s12.root === s22.root && s12.baseId === s22.baseId;
87479
87513
  }
87480
- function resolve11(root3, ref) {
87514
+ function resolve12(root3, ref) {
87481
87515
  let sch;
87482
87516
  while (typeof (sch = this.refs[ref]) == "string") ref = sch;
87483
87517
  return sch || this.schemas[ref] || resolveSchema.call(this, root3, ref);
@@ -87927,7 +87961,7 @@ var require_fast_uri2 = /* @__PURE__ */ __commonJSMin2(((exports, module) => {
87927
87961
  else if (typeof uri === "object") uri = parse7(serialize(uri, options), options);
87928
87962
  return uri;
87929
87963
  }
87930
- function resolve11(baseURI, relativeURI, options) {
87964
+ function resolve12(baseURI, relativeURI, options) {
87931
87965
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
87932
87966
  const resolved = resolveComponent(parse7(baseURI, schemelessOptions), parse7(relativeURI, schemelessOptions), schemelessOptions, true);
87933
87967
  schemelessOptions.skipEscape = true;
@@ -88101,7 +88135,7 @@ var require_fast_uri2 = /* @__PURE__ */ __commonJSMin2(((exports, module) => {
88101
88135
  const fastUri = {
88102
88136
  SCHEMES,
88103
88137
  normalize: normalize2,
88104
- resolve: resolve11,
88138
+ resolve: resolve12,
88105
88139
  resolveComponent,
88106
88140
  equal,
88107
88141
  serialize,
@@ -93534,7 +93568,7 @@ var StdioServerTransport = class {
93534
93568
  }
93535
93569
  send(message) {
93536
93570
  if (this._closed) return Promise.reject(/* @__PURE__ */ new Error("StdioServerTransport is closed"));
93537
- return new Promise((resolve11, reject) => {
93571
+ return new Promise((resolve12, reject) => {
93538
93572
  const json2 = serializeMessage2(message);
93539
93573
  let settled = false;
93540
93574
  const onError = (error51) => {
@@ -93549,14 +93583,14 @@ var StdioServerTransport = class {
93549
93583
  settled = true;
93550
93584
  this._stdout.off("error", onError);
93551
93585
  this._stdout.off("drain", onDrain2);
93552
- resolve11();
93586
+ resolve12();
93553
93587
  };
93554
93588
  this._stdout.once("error", onError);
93555
93589
  if (this._stdout.write(json2)) {
93556
93590
  if (settled) return;
93557
93591
  settled = true;
93558
93592
  this._stdout.off("error", onError);
93559
- resolve11();
93593
+ resolve12();
93560
93594
  } else if (!settled) this._stdout.once("drain", onDrain2);
93561
93595
  });
93562
93596
  }
@@ -93610,14 +93644,14 @@ var StdioConnectionChannel = class {
93610
93644
  */
93611
93645
  async whenRequestsAnswered(timeoutMs) {
93612
93646
  if (this._closed || this._pendingRequests.size === 0) return true;
93613
- return await new Promise((resolve11) => {
93647
+ return await new Promise((resolve12) => {
93614
93648
  const waiter = () => {
93615
93649
  clearTimeout(timer);
93616
- resolve11(true);
93650
+ resolve12(true);
93617
93651
  };
93618
93652
  const timer = setTimeout(() => {
93619
93653
  this._drainWaiters = this._drainWaiters.filter((pending) => pending !== waiter);
93620
- resolve11(false);
93654
+ resolve12(false);
93621
93655
  }, timeoutMs);
93622
93656
  this._drainWaiters.push(waiter);
93623
93657
  });
@@ -94046,7 +94080,7 @@ var RUN_RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
94046
94080
  var FAILURES_KEPT = 20;
94047
94081
  var SUMMARY_CHARS = 200;
94048
94082
  function announcingLedger(inner, announce) {
94049
- const open5 = /* @__PURE__ */ new Map();
94083
+ const open6 = /* @__PURE__ */ new Map();
94050
94084
  const tell = (apps) => {
94051
94085
  for (const a of apps ?? []) announce(a);
94052
94086
  };
@@ -94055,20 +94089,20 @@ function announcingLedger(inner, announce) {
94055
94089
  begin(row) {
94056
94090
  inner.begin(row);
94057
94091
  const apps = [{ projectId: row.projectId, appId: row.appId }];
94058
- for (const up2 of row.parentRunId ? open5.get(row.parentRunId) ?? [] : []) {
94092
+ for (const up2 of row.parentRunId ? open6.get(row.parentRunId) ?? [] : []) {
94059
94093
  if (!apps.some((a) => a.projectId === up2.projectId && a.appId === up2.appId)) apps.push(up2);
94060
94094
  }
94061
- open5.set(row.id, apps);
94095
+ open6.set(row.id, apps);
94062
94096
  tell(apps);
94063
94097
  },
94064
94098
  link(id2, sessionId) {
94065
94099
  inner.link(id2, sessionId);
94066
- tell(open5.get(id2));
94100
+ tell(open6.get(id2));
94067
94101
  },
94068
94102
  end(id2, e) {
94069
94103
  inner.end(id2, e);
94070
- tell(open5.get(id2));
94071
- open5.delete(id2);
94104
+ tell(open6.get(id2));
94105
+ open6.delete(id2);
94072
94106
  }
94073
94107
  };
94074
94108
  }
@@ -94530,7 +94564,7 @@ var BrokerDesk = class {
94530
94564
  try {
94531
94565
  answer = await Promise.race([
94532
94566
  asked.answer,
94533
- new Promise((resolve11) => call2.signal.aborted ? resolve11(null) : call2.signal.addEventListener("abort", () => resolve11(null), { once: true }))
94567
+ new Promise((resolve12) => call2.signal.aborted ? resolve12(null) : call2.signal.addEventListener("abort", () => resolve12(null), { once: true }))
94534
94568
  ]);
94535
94569
  } finally {
94536
94570
  call2.signal.removeEventListener("abort", leave);
@@ -96454,6 +96488,11 @@ var ExternalApps = class {
96454
96488
  }
96455
96489
  return out;
96456
96490
  }
96491
+ sessionBoundaryOpen = () => false;
96492
+ /** Host-owned policy; a request cannot claim an auto preset in its arguments. */
96493
+ setSessionBoundaryPolicy(policy) {
96494
+ this.sessionBoundaryOpen = policy;
96495
+ }
96457
96496
  /**
96458
96497
  * An app's tool list — if the app is down, **this is where it starts** (the first time it is
96459
96498
  * needed).
@@ -96462,9 +96501,9 @@ var ExternalApps = class {
96462
96501
  * read is the place it is enforced: this list becomes the source of truth both for what attaches to
96463
96502
  * a session (A-5) and for the tools a screen can call.
96464
96503
  */
96465
- async tools(ref, audience) {
96504
+ async tools(ref, audience, sessionId) {
96466
96505
  const e = this.require(ref);
96467
- const all = await this.use(e, async () => e.life.tools ?? []);
96506
+ const all = await this.use(e, async () => e.life.tools ?? [], !!sessionId && this.sessionBoundaryOpen(sessionId));
96468
96507
  return all.filter((t) => !audience || t.visibility.includes(audience)).map((t) => t.tool);
96469
96508
  }
96470
96509
  /**
@@ -96596,7 +96635,8 @@ var ExternalApps = class {
96596
96635
  return { runId, status, result, error: error51, durationMs };
96597
96636
  };
96598
96637
  if (!e.manifest) return done("rejected", null, `This app's manifest is invalid: ${e.error}`);
96599
- if (!e.scope.trusted) return done("rejected", null, "This app's project is not trusted, so Centralu does not call its apps");
96638
+ const open6 = caller.kind === "session" && this.sessionBoundaryOpen(caller.sessionId);
96639
+ if (!e.scope.trusted && !open6) return done("rejected", null, "This app's project is not trusted, so Centralu does not call its apps");
96600
96640
  const held3 = this.held(e);
96601
96641
  if (held3) return done("rejected", null, held3);
96602
96642
  if (e.life.gaveUp) return done("rejected", null, `This app stopped after failing ${this.timing.maxFailures} times in a row`);
@@ -96644,7 +96684,7 @@ var ExternalApps = class {
96644
96684
  abort.abort();
96645
96685
  for (const sig of upstream) sig.removeEventListener("abort", onUp);
96646
96686
  }
96647
- });
96687
+ }, open6);
96648
96688
  } catch (err) {
96649
96689
  return done("error", null, err.message);
96650
96690
  }
@@ -97248,11 +97288,11 @@ var ExternalApps = class {
97248
97288
  }
97249
97289
  // ── Lifecycle ──────────────────────────────────────────────────────────────────────
97250
97290
  /** An app is never idle while in use — the idle timer is set again once it is done */
97251
- async use(e, fn2) {
97291
+ async use(e, fn2, boundaryOpen = false) {
97252
97292
  e.life.inflight += 1;
97253
97293
  this.clearIdle(e);
97254
97294
  try {
97255
- return await fn2(await this.ensureRunning(e));
97295
+ return await fn2(await this.ensureRunning(e, boundaryOpen));
97256
97296
  } finally {
97257
97297
  e.life.inflight -= 1;
97258
97298
  if (e.life.inflight === 0) for (const w of e.life.idleWaiters.splice(0)) w();
@@ -97266,14 +97306,14 @@ var ExternalApps = class {
97266
97306
  */
97267
97307
  drain(e, ms3) {
97268
97308
  if (e.life.inflight === 0) return Promise.resolve(true);
97269
- return new Promise((resolve11) => {
97309
+ return new Promise((resolve12) => {
97270
97310
  const waiter = () => {
97271
97311
  clearTimeout(t);
97272
- resolve11(true);
97312
+ resolve12(true);
97273
97313
  };
97274
97314
  const t = setTimeout(() => {
97275
97315
  e.life.idleWaiters = e.life.idleWaiters.filter((w) => w !== waiter);
97276
- resolve11(false);
97316
+ resolve12(false);
97277
97317
  }, ms3);
97278
97318
  t.unref();
97279
97319
  e.life.idleWaiters.push(waiter);
@@ -97287,10 +97327,10 @@ var ExternalApps = class {
97287
97327
  * encountered restoring sessions (see the `resuming` comment in `manager.ts`). Everyone waits on the
97288
97328
  * same in-progress startup promise instead.
97289
97329
  */
97290
- ensureRunning(e) {
97330
+ ensureRunning(e, boundaryOpen = false) {
97291
97331
  const L2 = e.life;
97292
97332
  if (!e.manifest) throw new AppUnavailableError(`The app cannot start \u2014 its manifest is invalid: ${e.error}`);
97293
- if (!e.scope.trusted) {
97333
+ if (!e.scope.trusted && !boundaryOpen) {
97294
97334
  throw new AppUnavailableError("This app's project is not trusted, so the app does not start \u2014 trust the project and it will");
97295
97335
  }
97296
97336
  const held3 = this.held(e);
@@ -97955,9 +97995,9 @@ var InlineViews = class {
97955
97995
  * just opened is never the one closed.
97956
97996
  */
97957
97997
  capLive(sessionId, keep) {
97958
- const open5 = [...this.bySession.get(sessionId)?.values() ?? []].filter((x) => x.instanceId).sort((a, b2) => a.openedAt - b2.openedAt);
97998
+ const open6 = [...this.bySession.get(sessionId)?.values() ?? []].filter((x) => x.instanceId).sort((a, b2) => a.openedAt - b2.openedAt);
97959
97999
  const n = this.limits.livePerSession;
97960
- for (const old of open5.slice(0, Math.max(0, open5.length - n))) {
98000
+ for (const old of open6.slice(0, Math.max(0, open6.length - n))) {
97961
98001
  if (old === keep) continue;
97962
98002
  this.shut(old);
97963
98003
  this.deps.emit({
@@ -98092,12 +98132,12 @@ function readBook(raw) {
98092
98132
  return book;
98093
98133
  }
98094
98134
  function listenOn(port2, handler) {
98095
- return new Promise((resolve11, reject) => {
98135
+ return new Promise((resolve12, reject) => {
98096
98136
  const server2 = createServer2(handler);
98097
98137
  server2.once("error", reject);
98098
98138
  server2.listen(port2, "127.0.0.1", () => {
98099
98139
  server2.removeListener("error", reject);
98100
- resolve11(server2);
98140
+ resolve12(server2);
98101
98141
  });
98102
98142
  });
98103
98143
  }
@@ -98165,8 +98205,288 @@ var OriginPorts = class {
98165
98205
  }
98166
98206
  };
98167
98207
 
98208
+ // packages/agent-host/src/dev-services/worktree-recovery.ts
98209
+ import { randomUUID as randomUUID6 } from "node:crypto";
98210
+ import { createReadStream, existsSync as existsSync13 } from "node:fs";
98211
+ import { chmod, lstat as lstat3, mkdir as mkdir2, mkdtemp, readFile, readdir as readdir2, realpath as realpath3, rm as rm2, stat as stat3, writeFile as writeFile2 } from "node:fs/promises";
98212
+ import { tmpdir as tmpdir2 } from "node:os";
98213
+ import { join as join16, resolve as resolve2 } from "node:path";
98214
+ var WORKTREE_RECOVERY_MAX_BYTES = 256 * 1024 * 1024;
98215
+ var RECOVERY_TIMEOUT = 12e4;
98216
+ function worktreeRecoveryDir(sessionId) {
98217
+ if (!isSessionId(sessionId)) throw new Error("Invalid session id for worktree recovery");
98218
+ return join16(dataRoot(), "worktree-recovery", sessionId);
98219
+ }
98220
+ async function clearWorktreeRecovery(sessionId) {
98221
+ await rm2(worktreeRecoveryDir(sessionId), { recursive: true, force: true });
98222
+ }
98223
+ async function* gitRecords(repo, args, input) {
98224
+ const dir = await mkdtemp(join16(tmpdir2(), "cc-recovery-git-"));
98225
+ const file2 = join16(dir, "records");
98226
+ try {
98227
+ await runGit(repo, args, { outputFile: file2, timeout: RECOVERY_TIMEOUT, input });
98228
+ let pending = "";
98229
+ for await (const chunk of createReadStream(file2, { encoding: "utf8" })) {
98230
+ pending += chunk;
98231
+ let end;
98232
+ while ((end = pending.indexOf("\0")) !== -1) {
98233
+ yield pending.slice(0, end);
98234
+ pending = pending.slice(end + 1);
98235
+ }
98236
+ }
98237
+ if (pending) yield pending;
98238
+ } finally {
98239
+ await rm2(dir, { recursive: true, force: true });
98240
+ }
98241
+ }
98242
+ async function saveWorktreeRecovery(sessionId, cwd, trunk, branch) {
98243
+ const plans = [];
98244
+ let bytes2 = 0;
98245
+ const account = (size) => {
98246
+ bytes2 += size;
98247
+ if (bytes2 > WORKTREE_RECOVERY_MAX_BYTES) {
98248
+ throw new Error(`Worktree recovery exceeds the 256 MiB input limit (tracked and non-ignored untracked files plus staged changes, including submodules). No files were staged or removed; save the work elsewhere before deleting.`);
98249
+ }
98250
+ };
98251
+ const prepare = async (repo, path2, gitlinks = []) => {
98252
+ const tip = (await runGit(repo, ["rev-parse", "--verify", "HEAD"])).trim();
98253
+ const refs = ["HEAD"];
98254
+ if (!path2 && branch) {
98255
+ const ref = `refs/heads/${branch}`;
98256
+ if (await runGit(repo, ["rev-parse", "--verify", ref]).then(() => true).catch(() => false)) refs.push(ref);
98257
+ } else if (path2) {
98258
+ refs.push(...(await runGit(repo, ["for-each-ref", "--format=%(refname)", "refs/heads/"])).trim().split("\n").filter(Boolean));
98259
+ if (gitlinks.length) {
98260
+ const available = await runGit(repo, ["cat-file", "--batch-check=%(objectname) %(objecttype)"], {
98261
+ input: gitlinks.map((oid) => `${oid}^{commit}`).join("\n") + "\n",
98262
+ timeout: RECOVERY_TIMEOUT,
98263
+ // Older Git ignores the lazy-fetch switch; disallow transports there too.
98264
+ env: { GIT_NO_LAZY_FETCH: "1", GIT_ALLOW_PROTOCOL: "" }
98265
+ });
98266
+ for (const row of available.trim().split("\n")) {
98267
+ const [oid, type] = row.split(" ");
98268
+ if (type === "commit") refs.push(oid);
98269
+ }
98270
+ }
98271
+ }
98272
+ const baselineOid = !path2 ? await runGit(repo, ["rev-parse", "--verify", `${trunk}^{commit}`]).then((s) => s.trim()).catch(() => null) : null;
98273
+ const exclude = path2 ? (await runGit(repo, ["for-each-ref", "--format=^%(objectname)", "refs/remotes/"])).trim().split("\n").filter(Boolean) : baselineOid ? [`^${baselineOid}`] : [];
98274
+ const revisions = [...refs, ...exclude].join("\n") + "\n";
98275
+ const commits = Number((await runGit(repo, ["rev-list", "--count", "--stdin"], { input: revisions, timeout: RECOVERY_TIMEOUT })).trim());
98276
+ plans.push({ repo, path: path2, tip, refs, commits });
98277
+ const stagedNames = new Set((await runGit(repo, ["diff", "--cached", "--name-only", "-z", "--no-renames", tip], { timeout: RECOVERY_TIMEOUT })).split("\0"));
98278
+ const modules = [];
98279
+ const moduleCommits = /* @__PURE__ */ new Map();
98280
+ const keepGitlink = (rel, oid) => {
98281
+ modules.push(rel);
98282
+ const tips = moduleCommits.get(rel) ?? /* @__PURE__ */ new Set();
98283
+ tips.add(oid);
98284
+ moduleCommits.set(rel, tips);
98285
+ };
98286
+ const manifests = /* @__PURE__ */ new Set();
98287
+ const keepManifest = (rel, oid) => {
98288
+ if (rel === ".gitmodules" && !/^0+$/.test(oid)) manifests.add(oid);
98289
+ };
98290
+ let change = null;
98291
+ for await (const row of gitRecords(repo, ["log", "--raw", "-z", "--no-renames", "--ignore-submodules=none", "--no-abbrev", "--format=", "--root", "--diff-merges=separate", "--stdin"], revisions)) {
98292
+ if (change) {
98293
+ const [, oldMode, newMode, oldOid, newOid] = change;
98294
+ if (oldMode === "160000") keepGitlink(row, oldOid);
98295
+ if (newMode === "160000") keepGitlink(row, newOid);
98296
+ keepManifest(row, oldOid);
98297
+ keepManifest(row, newOid);
98298
+ change = null;
98299
+ } else change = row.match(/^\n*:(\d{6}) (\d{6}) ([a-f0-9]+) ([a-f0-9]+) [A-Z]$/);
98300
+ }
98301
+ for await (const row of gitRecords(repo, ["ls-tree", "-r", "-z", "HEAD"])) {
98302
+ const tab = row.indexOf(" ");
98303
+ const [mode, , oid] = row.slice(0, tab).split(" ");
98304
+ const rel = row.slice(tab + 1);
98305
+ if (mode === "160000") keepGitlink(rel, oid);
98306
+ keepManifest(rel, oid);
98307
+ }
98308
+ const stagedObjects = [];
98309
+ for await (const row of gitRecords(repo, ["ls-files", "--stage", "-z"])) {
98310
+ if (!row) continue;
98311
+ const tab = row.indexOf(" ");
98312
+ const rel = row.slice(tab + 1);
98313
+ const oid = row.split(" ")[1];
98314
+ if (row.startsWith("160000 ")) keepGitlink(rel, oid);
98315
+ else if (stagedNames.has(rel)) stagedObjects.push(oid);
98316
+ keepManifest(rel, oid);
98317
+ }
98318
+ const modulePaths = /* @__PURE__ */ new Map();
98319
+ for (const oid of manifests) {
98320
+ const config2 = await runGit(repo, ["config", "--blob", oid, "-z", "--get-regexp", "^submodule\\..*\\.path$"]).catch((e) => {
98321
+ if (e.code === 1) return "";
98322
+ throw e;
98323
+ });
98324
+ for (const row of config2.split("\0").filter(Boolean)) {
98325
+ const newline = row.indexOf("\n");
98326
+ const name = row.slice("submodule.".length, newline - ".path".length);
98327
+ const paths = modulePaths.get(name) ?? /* @__PURE__ */ new Set();
98328
+ paths.add(row.slice(newline + 1));
98329
+ modulePaths.set(name, paths);
98330
+ }
98331
+ }
98332
+ for (const paths of modulePaths.values()) {
98333
+ const tips = new Set([...paths].flatMap((rel) => [...moduleCommits.get(rel) ?? []]));
98334
+ for (const rel of paths) {
98335
+ modules.push(rel);
98336
+ moduleCommits.set(rel, /* @__PURE__ */ new Set([...moduleCommits.get(rel) ?? [], ...tips]));
98337
+ }
98338
+ }
98339
+ if (stagedObjects.length) {
98340
+ const sizes = await runGit(repo, ["cat-file", "--batch-check=%(objectsize)"], { input: stagedObjects.join("\n") + "\n" });
98341
+ for (const size of sizes.trim().split("\n")) {
98342
+ if (!/^\d+$/.test(size)) throw new Error("Could not measure staged recovery data");
98343
+ account(Number(size));
98344
+ }
98345
+ }
98346
+ const files = new Set((await runGit(repo, ["ls-files", "--cached", "--others", "--exclude-standard", "-z"])).split("\0").filter(Boolean));
98347
+ for (const rel of files) {
98348
+ const file2 = await lstat3(join16(repo, rel)).catch((e) => {
98349
+ if (e.code !== "ENOENT") throw e;
98350
+ return null;
98351
+ });
98352
+ if (file2?.isFile() || file2?.isSymbolicLink()) account(file2.size);
98353
+ else if (file2?.isDirectory() && existsSync13(join16(repo, rel, ".git"))) modules.push(rel);
98354
+ }
98355
+ for (const rel of new Set(modules)) {
98356
+ await assertExistingPath(repo, rel).catch((e) => {
98357
+ if (existsSync13(join16(repo, rel))) throw e;
98358
+ });
98359
+ if (existsSync13(join16(repo, rel, ".git"))) await prepare(join16(repo, rel), path2 ? `${path2}/${rel}` : rel, [...moduleCommits.get(rel) ?? []]);
98360
+ }
98361
+ };
98362
+ await prepare(cwd, "");
98363
+ const snapshotId = randomUUID6();
98364
+ const dir = join16(worktreeRecoveryDir(sessionId), snapshotId);
98365
+ await mkdir2(dir, { recursive: true, mode: 448 });
98366
+ const snapshot2 = { snapshot: snapshotId, tip: plans[0].tip, changedFiles: 0, commits: 0, repositories: [] };
98367
+ const patchFile = async (repo, tip, file2, env) => {
98368
+ await runGit(repo, ["diff", "--cached", "--binary", "--full-index", "--no-renames", `--output=${file2}`, tip], { env, timeout: RECOVERY_TIMEOUT });
98369
+ await chmod(file2, 384);
98370
+ if ((await stat3(file2)).size > 0) return true;
98371
+ await rm2(file2);
98372
+ return false;
98373
+ };
98374
+ try {
98375
+ for (const [n, plan] of plans.entries()) {
98376
+ const { repo, path: path2, tip, refs, commits } = plan;
98377
+ const entry = { path: path2, tip, refs, patch: false, staged: false, bundle: commits > 0 };
98378
+ snapshot2.repositories.push(entry);
98379
+ snapshot2.commits += commits;
98380
+ if (entry.bundle) {
98381
+ const temporaryRefs = [];
98382
+ const updates2 = [];
98383
+ try {
98384
+ entry.refs = [];
98385
+ for (const ref of refs) {
98386
+ if (ref === "HEAD" || ref.startsWith("refs/")) entry.refs.push(ref);
98387
+ else {
98388
+ const name = `refs/centralu-recovery/${snapshotId}/gitlink-${temporaryRefs.length}`;
98389
+ temporaryRefs.push(name);
98390
+ updates2.push(`create ${name} ${ref}`);
98391
+ entry.refs.push(name);
98392
+ }
98393
+ }
98394
+ if (updates2.length) await runGit(repo, ["update-ref", "--stdin"], { input: updates2.join("\n") + "\n", timeout: RECOVERY_TIMEOUT });
98395
+ await runGit(repo, ["bundle", "create", join16(dir, `${n}.bundle`), "--stdin"], { input: entry.refs.join("\n") + "\n", timeout: RECOVERY_TIMEOUT });
98396
+ } finally {
98397
+ if (temporaryRefs.length) await runGit(repo, ["update-ref", "--stdin"], { input: temporaryRefs.map((ref) => `delete ${ref}`).join("\n") + "\n", timeout: RECOVERY_TIMEOUT });
98398
+ }
98399
+ }
98400
+ entry.staged = await patchFile(repo, tip, join16(dir, `${n}.staged.patch`));
98401
+ const stagedNames = await runGit(repo, ["diff", "--cached", "--name-only", "-z", "--no-renames", tip], { timeout: RECOVERY_TIMEOUT });
98402
+ const env = { GIT_INDEX_FILE: join16(dir, `${n}.index`) };
98403
+ try {
98404
+ await runGit(repo, ["read-tree", tip], { env });
98405
+ await runGit(repo, ["add", "-A", "--", "."], { env, timeout: RECOVERY_TIMEOUT });
98406
+ const names = await runGit(repo, ["diff", "--cached", "--name-only", "-z", "--no-renames", tip], { env, timeout: RECOVERY_TIMEOUT });
98407
+ snapshot2.changedFiles += new Set([...names.split("\0"), ...stagedNames.split("\0")].filter(Boolean)).size;
98408
+ entry.patch = await patchFile(repo, tip, join16(dir, `${n}.patch`), env);
98409
+ } finally {
98410
+ await rm2(env.GIT_INDEX_FILE, { force: true });
98411
+ }
98412
+ }
98413
+ await writeFile2(join16(dir, "snapshot.json"), JSON.stringify(snapshot2), { mode: 384 });
98414
+ return { snapshot: snapshotId, tip: snapshot2.tip, changedFiles: snapshot2.changedFiles, commits: snapshot2.commits };
98415
+ } catch (e) {
98416
+ await rm2(dir, { recursive: true, force: true });
98417
+ throw e;
98418
+ }
98419
+ }
98420
+ async function worktreeRecoveryBytes(sessionId) {
98421
+ const dir = worktreeRecoveryDir(sessionId);
98422
+ if (!existsSync13(dir)) return 0;
98423
+ const sizes = await Promise.all((await readdir2(dir, { recursive: true })).map(async (name) => {
98424
+ const s = await stat3(join16(dir, name));
98425
+ return s.isFile() ? s.size : 0;
98426
+ }));
98427
+ return sizes.reduce((a, b2) => a + b2, 0);
98428
+ }
98429
+ async function restoreWorktreeRecovery(sessionId, repo, path2, branch, recovery, trust) {
98430
+ if (!isSessionId(recovery.snapshot)) throw new Error("Invalid recovery snapshot");
98431
+ const dir = join16(worktreeRecoveryDir(sessionId), recovery.snapshot);
98432
+ const snapshot2 = JSON.parse(await readFile(join16(dir, "snapshot.json"), "utf8"));
98433
+ const root3 = snapshot2.repositories[0];
98434
+ let extraNotice = "";
98435
+ if (root3.bundle) {
98436
+ const heads = (await runGit(repo, ["bundle", "list-heads", join16(dir, "0.bundle")])).trim().split("\n");
98437
+ for (const row of heads) {
98438
+ const [tip, ref] = row.split(" ");
98439
+ if (!tip || !ref?.startsWith("refs/heads/") || tip === recovery.tip) continue;
98440
+ const base2 = `recovered/${sessionId}-stored-branch`;
98441
+ let recovered = base2;
98442
+ for (let suffix = 1; ; suffix++) {
98443
+ const existing = await runGit(repo, ["rev-parse", "--verify", `refs/heads/${recovered}`]).then((s) => s.trim()).catch(() => null);
98444
+ if (existing === tip) break;
98445
+ if (!existing) {
98446
+ await runGit(repo, ["fetch", join16(dir, "0.bundle"), `${ref}:refs/heads/${recovered}`], { allowRepoPrograms: trust.trusted === true });
98447
+ break;
98448
+ }
98449
+ recovered = `${base2}-${suffix}`;
98450
+ }
98451
+ extraNotice += `Stored branch ${ref.slice("refs/heads/".length)} recovered as ${recovered}. `;
98452
+ }
98453
+ }
98454
+ if (!existsSync13(path2)) {
98455
+ await rm2(join16(dir, "patches-applied"), { force: true });
98456
+ if (await runGit(repo, ["show-ref", "--verify", `refs/heads/${branch}`]).then(() => true).catch(() => false)) {
98457
+ branch = `recovered/${sessionId}`;
98458
+ }
98459
+ if (root3.bundle) await runGit(repo, ["fetch", join16(dir, "0.bundle"), ...root3.refs ?? ["HEAD"]], { allowRepoPrograms: trust.trusted === true });
98460
+ await runGit(repo, ["worktree", "add", "-b", branch, path2, recovery.tip], { allowRepoPrograms: trust.trusted === true });
98461
+ if (root3.patch || root3.staged) {
98462
+ try {
98463
+ if (root3.staged) await runGit(path2, ["apply", "--cached", "--binary", join16(dir, "0.staged.patch")]);
98464
+ if (root3.patch) await runGit(path2, ["apply", "--binary", join16(dir, "0.patch")]);
98465
+ await writeFile2(join16(dir, "patches-applied"), await realpath3(path2), { mode: 384 });
98466
+ } catch {
98467
+ return { branch, notice: `Restored branch ${branch}. ${extraNotice}Patches could not all be applied. Recovery files remain at ${dir}; apply numbered .patch files manually with git apply and .staged.patch files with git apply --cached.` };
98468
+ }
98469
+ }
98470
+ } else {
98471
+ const common = async (cwd) => realpath3(resolve2(cwd, (await runGit(cwd, ["rev-parse", "--git-common-dir"])).trim()));
98472
+ if (await common(path2) !== await common(repo)) throw new Error(`The worktree path belongs to another repository. Recovery files remain at ${dir}`);
98473
+ branch = (await runGit(path2, ["symbolic-ref", "--quiet", "--short", "HEAD"])).trim();
98474
+ if (root3.patch || root3.staged) {
98475
+ const appliedTo = await readFile(join16(dir, "patches-applied"), "utf8").catch((e) => {
98476
+ if (e.code === "ENOENT") return null;
98477
+ throw e;
98478
+ });
98479
+ extraNotice += appliedTo === await realpath3(path2) ? "Saved patches were applied during the previous restore attempt; they were not reapplied on this retry. " : "The worktree already existed; saved patches were not applied on this retry and may still need manual recovery. ";
98480
+ }
98481
+ }
98482
+ return {
98483
+ branch,
98484
+ notice: `Restored branch ${branch}. ${extraNotice}Recovery files remain at ${dir}: ${recovery.changedFiles} uncommitted files, ${recovery.commits} commits absent from the trunk or submodule remotes. Numbered .patch files can be applied with git apply; .bundle files list their saved refs with git bundle list-heads and can be fetched with git fetch <bundle> <ref>:<branch>. snapshot.json names each repository and its refs. Submodule recovery is manual. These files are removed when this session is deleted for good.`
98485
+ };
98486
+ }
98487
+
98168
98488
  // packages/agent-host/src/sessions/manager.ts
98169
- import { randomUUID as randomUUID9 } from "node:crypto";
98489
+ import { randomUUID as randomUUID10 } from "node:crypto";
98170
98490
 
98171
98491
  // packages/agent-host/src/sessions/tool-cost.ts
98172
98492
  import { createHash as createHash8 } from "node:crypto";
@@ -98254,8 +98574,12 @@ and approved skills, and whether every session can look at its own project), App
98254
98574
  Shortcuts (the shortcut list), and Updates.`,
98255
98575
  updates: `# Updates
98256
98576
  Check from Settings \u2192 Updates. It reports a new version based on the npm registry, and the person
98257
- clicks to install it. The check for a new version runs on startup and every six hours, and it can
98258
- be turned off.
98577
+ clicks to install it. With automatic checking on, the check runs on startup, every six hours, when
98578
+ Updates opens, and on focus when the last check is over an hour old. With it off, use Check now.
98579
+ Ordinary checks follow npm, including dist-tag rollbacks. Installing reuses a successful check
98580
+ from the last minute or checks again. Only while installing can a higher stored version seen less
98581
+ than an hour ago win over the fresh tag. Older observations yield to npm. If the check fails, it
98582
+ uses the stored version and says so. Checks preserve install errors until the next install attempt.
98259
98583
  Once installed, the desktop app offers "Apply now": it relaunches into the new version, and running
98260
98584
  agents, terminals and commands keep going. "Apply updates automatically when idle" (off by default)
98261
98585
  installs a new version without a click and applies it once no session is working or waiting, no
@@ -98484,8 +98808,8 @@ var READER_TOOLS = [
98484
98808
  var DELEGATE_TOOLS = [
98485
98809
  {
98486
98810
  name: "ask_project",
98487
- description: "Has another Centralu project do a job in its own folder; returns its session's answer. For another project's apps, use find_apps.",
98488
- schema: external_exports.object({ project: external_exports.string(), task: external_exports.string().optional() })
98811
+ description: "Asks another project's existing session; returns its answer. For another project's apps, use find_apps.",
98812
+ schema: external_exports.object({ project: external_exports.string(), session: external_exports.string().optional(), task: external_exports.string().optional() })
98489
98813
  }
98490
98814
  ];
98491
98815
  var DELEGATE_BUDGET_CHARS = 400;
@@ -98609,7 +98933,7 @@ function askProjectText(r) {
98609
98933
  const where = `the session "${r.sessionName}" [${r.sessionId}] in ${r.project}`;
98610
98934
  if (r.state === "working") {
98611
98935
  return {
98612
- text: `${r.project} is still working on it in ${where}${r.notice ? ` \u2014 ${r.notice}` : ""}. To keep waiting, call ask_project again with project "${r.project}" and no task; the work goes on meanwhile.`
98936
+ text: `${r.project} is still working on it in ${where}${r.notice ? ` \u2014 ${r.notice}` : ""}. To keep waiting, call ask_project again with project "${r.project}", session "${r.sessionId}" and no task; the work goes on meanwhile.`
98613
98937
  };
98614
98938
  }
98615
98939
  const lines = [`${r.project} answered (from ${where}), as JSON:`, trustedJsonText(r.answer || "(no final text)")];
@@ -98628,8 +98952,9 @@ function agentRunText(r) {
98628
98952
  case "stopped":
98629
98953
  return { text: `Stopped ${who}.` };
98630
98954
  case "deleted":
98955
+ if (r.branch === void 0) return { text: `Moved ${who} to the trash. Tell the person in one line.` };
98631
98956
  return {
98632
- text: `Cleaned up ${who}: "${r.branch}" had landed on the trunk (${r.proof}), so its worktree and branch were removed and the session went to the trash. Tell the person in one line.`
98957
+ text: (r.warning ? `Moved ${who} to the trash. Warning: ${r.warning} ` : `Cleaned up ${who}: its worktree and branch "${r.branch}" were removed and the session went to the trash. `) + (r.setAside.changedFiles || r.setAside.commits ? `${r.setAside.changedFiles} uncommitted files, ${r.setAside.commits} commits not on the trunk, kept in the trash. ` : "Nothing needed setting aside. ") + "Tell the person in one line."
98633
98958
  };
98634
98959
  case "at_rest":
98635
98960
  return {
@@ -98665,7 +98990,8 @@ async function runOrchestratorTool(tools, name, args, caller = { sessionId: null
98665
98990
  const project2 = typeof args.project === "string" ? args.project.trim() : "";
98666
98991
  if (!project2) return { text: "Give a project \u2014 the name of another Centralu project.", isError: true };
98667
98992
  const task = typeof args.task === "string" && args.task.trim() ? args.task : void 0;
98668
- return askProjectText(await tools.askProject({ project: project2, ...task ? { task } : {} }, signal));
98993
+ const session2 = typeof args.session === "string" && args.session.trim() ? args.session.trim() : void 0;
98994
+ return askProjectText(await tools.askProject({ project: project2, ...session2 ? { session: session2 } : {}, ...task ? { task } : {} }, signal));
98669
98995
  }
98670
98996
  if (name === "start_agent" || name === "agent_session") {
98671
98997
  if (caller.profile !== "reader") return { text: `Not a tool of this session: ${name}`, isError: true };
@@ -99077,7 +99403,7 @@ function shortHash(value) {
99077
99403
 
99078
99404
  // packages/agent-host/src/sessions/orchestrator-home.ts
99079
99405
  import { mkdirSync as mkdirSync7 } from "node:fs";
99080
- import { join as join16 } from "node:path";
99406
+ import { join as join17 } from "node:path";
99081
99407
  var preamble = true;
99082
99408
  function setOrchestratorPreamble(on2) {
99083
99409
  preamble = on2;
@@ -99093,7 +99419,7 @@ function orchestratorRole() {
99093
99419
  ${ORCHESTRATOR_ROLE}` : ORCHESTRATOR_ROLE;
99094
99420
  }
99095
99421
  function orchestratorHome() {
99096
- const dir = join16(dataRoot(), "orchestrator");
99422
+ const dir = join17(dataRoot(), "orchestrator");
99097
99423
  mkdirSync7(dir, { recursive: true });
99098
99424
  return dir;
99099
99425
  }
@@ -99641,7 +99967,7 @@ var SessionAppsHub = class {
99641
99967
  * keep answering "yes" even after trust is revoked.
99642
99968
  */
99643
99969
  refsFor(session2) {
99644
- const usable = this.rt.list().filter((a) => !UNUSABLE.has(a.status));
99970
+ const usable = this.rt.list().filter((a) => !UNUSABLE.has(a.status) || a.status === "untrusted" && !!this.opts.boundaryOpen?.(session2.id));
99645
99971
  const hits = usable.filter((a) => givesApp(session2, a)).map((a) => ({ ref: { projectId: a.projectId, appId: a.appId }, server: appMcpServerName(a.appId) })).sort((x, y) => x.server.localeCompare(y.server));
99646
99972
  const taken = new Set(hits.map((h) => h.server));
99647
99973
  for (const x of this.opts.onDemand?.attached(session2.id) ?? []) {
@@ -99668,8 +99994,8 @@ var SessionAppsHub = class {
99668
99994
  const own = session2?.id ? this.opts.onDemand?.attached(session2.id).find((x) => sameRef(x.ref, ref)) : void 0;
99669
99995
  const onDemand = !!session2 && !!own && this.opts.onDemand.allowed(session2, app2);
99670
99996
  if (!session2 || !givesApp(session2, app2) && !onDemand) return { reachable: false, reason: "other-project" };
99671
- if (app2.status === "untrusted") return { reachable: false, reason: "untrusted" };
99672
- if (UNUSABLE.has(app2.status)) return { reachable: false, reason: "app-unusable", status: app2.status };
99997
+ if (app2.status === "untrusted" && !this.opts.boundaryOpen?.(session2.id)) return { reachable: false, reason: "untrusted" };
99998
+ if (UNUSABLE.has(app2.status) && !(app2.status === "untrusted" && this.opts.boundaryOpen?.(session2.id))) return { reachable: false, reason: "app-unusable", status: app2.status };
99673
99999
  const server2 = givesApp(session2, app2) ? appMcpServerName(app2.appId) : own.server;
99674
100000
  const attached = live?.appAttachment?.(server2) ?? "attached";
99675
100001
  if (attached === "restart") return { reachable: false, reason: "restart" };
@@ -99751,7 +100077,7 @@ var Attachment2 = class {
99751
100077
  let timer;
99752
100078
  try {
99753
100079
  const listed = await Promise.race([
99754
- this.hub.rt.tools(hit.ref, "model"),
100080
+ this.hub.rt.tools(hit.ref, "model", this.session.id),
99755
100081
  new Promise((_, reject) => {
99756
100082
  timer = setTimeout(() => reject(new Error(`did not list its tools within ${Math.round(waitMs / 1e3)}s`)), waitMs);
99757
100083
  })
@@ -99796,7 +100122,7 @@ var Attachment2 = class {
99796
100122
  let timer;
99797
100123
  const first = await Promise.race([
99798
100124
  pending.then((o) => ({ o })),
99799
- new Promise((resolve11) => timer = setTimeout(() => resolve11(null), opts.waitMs))
100125
+ new Promise((resolve12) => timer = setTimeout(() => resolve12(null), opts.waitMs))
99800
100126
  ]);
99801
100127
  clearTimeout(timer);
99802
100128
  if (first) return toResult(first.o);
@@ -99902,15 +100228,15 @@ The call has not stopped and carries on. Do not call it again: pass this id as r
99902
100228
  if (i !== -1) return Promise.resolve(this.noted.splice(i, 1)[0].callId);
99903
100229
  if (this.closed) return Promise.resolve(null);
99904
100230
  const waitMs = this.hub.opts.callJoinWaitMs ?? CALL_JOIN_WAIT_MS;
99905
- return new Promise((resolve11) => {
100231
+ return new Promise((resolve12) => {
99906
100232
  const waiter = {
99907
100233
  server: server2,
99908
100234
  tool,
99909
100235
  args: key,
99910
- resolve: resolve11,
100236
+ resolve: resolve12,
99911
100237
  timer: setTimeout(() => {
99912
100238
  this.waiters = this.waiters.filter((x) => x !== waiter);
99913
- resolve11(null);
100239
+ resolve12(null);
99914
100240
  }, waitMs)
99915
100241
  };
99916
100242
  waiter.timer.unref?.();
@@ -100021,12 +100347,12 @@ var AppAccess = class {
100021
100347
  }
100022
100348
  allowed(session2, app2) {
100023
100349
  const from = session2.projectId;
100024
- if (from === null || !this.projectTrusted(from)) return false;
100350
+ if (from === null || !this.projectTrusted(from) && !this.deps.boundaryOpen?.(session2.id)) return false;
100025
100351
  if (app2.projectId === null) return true;
100026
100352
  if (app2.projectId === from || app2.shared !== true) return false;
100027
100353
  const r = this.records(session2.id).find((x) => x.appId === app2.appId && x.projectId === app2.projectId);
100028
100354
  if (!r) return false;
100029
- return r.consent === "once" || !!this.deps.store.getProjectConsent(from, app2.projectId, "apps");
100355
+ return !!this.deps.boundaryOpen?.(session2.id) || r.consent === "once" || !!this.deps.store.getProjectConsent(from, app2.projectId, "apps");
100030
100356
  }
100031
100357
  // ── The three tools ──
100032
100358
  find(sessionId, query) {
@@ -100049,7 +100375,7 @@ var AppAccess = class {
100049
100375
  const app2 = named.app;
100050
100376
  const target = { projectId: app2.projectId, appId: app2.appId };
100051
100377
  const has = already.find((h) => h.ref.appId === target.appId && h.ref.projectId === target.projectId);
100052
- if (has) return { ok: true, server: has.server, tools: await this.toolNames(target), when: "now", already: true };
100378
+ if (has) return { ok: true, server: has.server, tools: await this.toolNames(target, sessionId), when: "now", already: true };
100053
100379
  if (!this.candidates(session2).some((a) => a.appId === app2.appId && a.projectId === app2.projectId)) {
100054
100380
  return { ok: false, error: this.whyNot(session2, app2) };
100055
100381
  }
@@ -100062,7 +100388,7 @@ var AppAccess = class {
100062
100388
  signal
100063
100389
  );
100064
100390
  if (!res.ok) return res;
100065
- consent = this.deps.store.getProjectConsent(session2.projectId, app2.projectId, "apps") ? "always" : "once";
100391
+ consent = this.deps.boundaryOpen?.(sessionId) ? "auto" : this.deps.store.getProjectConsent(session2.projectId, app2.projectId, "apps") ? "always" : "once";
100066
100392
  const again = this.gate(sessionId);
100067
100393
  if (!again.ok) return again;
100068
100394
  if (!this.candidates(again.session).some((a) => a.appId === app2.appId && a.projectId === app2.projectId)) {
@@ -100074,7 +100400,7 @@ var AppAccess = class {
100074
100400
  const server2 = this.freeServer(app2.appId, taken);
100075
100401
  this.save(sessionId, [...kept, { projectId: app2.projectId, appId: app2.appId, server: server2, consent, at: Date.now() }]);
100076
100402
  const when = this.deps.attachedChanged(sessionId);
100077
- return { ok: true, server: server2, tools: await this.toolNames(target), when };
100403
+ return { ok: true, server: server2, tools: await this.toolNames(target, sessionId), when };
100078
100404
  }
100079
100405
  detach(sessionId, ref) {
100080
100406
  const mine = this.records(sessionId);
@@ -100118,7 +100444,7 @@ var AppAccess = class {
100118
100444
  const session2 = this.deps.session(sessionId);
100119
100445
  if (!session2) return { ok: false, error: "This session cannot attach apps." };
100120
100446
  if (session2.projectId === null) return { ok: false, error: "Only a session in a project can attach apps from other projects." };
100121
- if (!this.projectTrusted(session2.projectId)) {
100447
+ if (!this.projectTrusted(session2.projectId) && !this.deps.boundaryOpen?.(sessionId, "attach from an untrusted project")) {
100122
100448
  return { ok: false, error: "This session's project is not trusted, so it cannot attach apps \u2014 the person can trust it from the project menu." };
100123
100449
  }
100124
100450
  return { ok: true, session: session2 };
@@ -100126,7 +100452,7 @@ var AppAccess = class {
100126
100452
  /** What this session could attach now: shared apps of other trusted projects, and user-folder apps, minus what it has */
100127
100453
  candidates(session2) {
100128
100454
  const have = this.deps.hub.refsFor(session2);
100129
- return this.deps.hub.rt.list().filter((a) => !UNUSABLE2.has(a.status)).filter((a) => a.projectId === null || a.projectId !== session2.projectId && a.shared === true).filter((a) => !have.some((h) => h.ref.appId === a.appId && h.ref.projectId === a.projectId)).sort((x, y) => this.refOf(x).localeCompare(this.refOf(y)));
100455
+ return this.deps.hub.rt.list().filter((a) => !UNUSABLE2.has(a.status) || a.status === "untrusted" && this.deps.boundaryOpen?.(session2.id)).filter((a) => a.projectId === null || a.projectId !== session2.projectId && a.shared === true).filter((a) => !have.some((h) => h.ref.appId === a.appId && h.ref.projectId === a.projectId)).sort((x, y) => this.refOf(x).localeCompare(this.refOf(y)));
100130
100456
  }
100131
100457
  whyNot(session2, app2) {
100132
100458
  if (app2.projectId !== null && app2.projectId === session2.projectId) return `${this.refOf(app2)} is this project's own app; it is attached already when the project is trusted.`;
@@ -100182,13 +100508,13 @@ var AppAccess = class {
100182
100508
  for (let n = 2; ; n++) if (!taken.includes(`${base2}-${n}`)) return `${base2}-${n}`;
100183
100509
  }
100184
100510
  /** The app's agent tools for the tool result — started here if never listed, within the hub's cap; empty on failure */
100185
- async toolNames(ref) {
100511
+ async toolNames(ref, sessionId) {
100186
100512
  const known = this.deps.hub.rt.knownTools(ref, "model");
100187
100513
  if (known) return known.map((t) => t.name);
100188
100514
  let timer;
100189
100515
  try {
100190
100516
  const listed = await Promise.race([
100191
- this.deps.hub.rt.tools(ref, "model"),
100517
+ this.deps.hub.rt.tools(ref, "model", sessionId),
100192
100518
  new Promise((_, reject) => timer = setTimeout(() => reject(new Error("timeout")), this.deps.toolListWaitMs ?? TOOL_LIST_WAIT_MS2))
100193
100519
  ]);
100194
100520
  return listed.map((t) => t.name);
@@ -100222,7 +100548,7 @@ function parse3(raw) {
100222
100548
  const v = JSON.parse(raw);
100223
100549
  if (!Array.isArray(v)) return [];
100224
100550
  return v.filter(
100225
- (x) => !!x && typeof x === "object" && (typeof x.projectId === "string" || x.projectId === null) && typeof x.appId === "string" && typeof x.server === "string" && x.server.startsWith(appMcpServerName("")) && (x.consent === "always" || x.consent === "once" || x.consent === "none")
100551
+ (x) => !!x && typeof x === "object" && (typeof x.projectId === "string" || x.projectId === null) && typeof x.appId === "string" && typeof x.server === "string" && x.server.startsWith(appMcpServerName("")) && (x.consent === "always" || x.consent === "once" || x.consent === "none" || x.consent === "auto")
100226
100552
  );
100227
100553
  } catch {
100228
100554
  return [];
@@ -100250,7 +100576,7 @@ var AgentRunWait = class {
100250
100576
  this.sessionName = sessionName;
100251
100577
  this.onUsage = onUsage;
100252
100578
  this.readAnswer = readAnswer2;
100253
- this.done = new Promise((resolve11, reject) => this.settle = { resolve: resolve11, reject });
100579
+ this.done = new Promise((resolve12, reject) => this.settle = { resolve: resolve12, reject });
100254
100580
  this.done.catch(() => {
100255
100581
  });
100256
100582
  }
@@ -100465,7 +100791,7 @@ function windowOf(list, id2, label, since, writes, nameOf) {
100465
100791
 
100466
100792
  // packages/agent-host/src/sessions/ask-project.ts
100467
100793
  import { realpathSync as realpathSync3, statSync as statSync8 } from "node:fs";
100468
- import { isAbsolute as isAbsolute4, relative as relative4, resolve as resolve2, sep as sep4 } from "node:path";
100794
+ import { isAbsolute as isAbsolute4, relative as relative4, resolve as resolve3, sep as sep4 } from "node:path";
100469
100795
  var ASK_WAIT_MS = 24e4;
100470
100796
  var ANSWER_MAX_CHARS = 6e3;
100471
100797
  var ANSWER_HEAD_CHARS = 4500;
@@ -100534,7 +100860,7 @@ function readableGrants(paths, projectRoot) {
100534
100860
  return { granted, outside };
100535
100861
  }
100536
100862
  function underGrant(path2, grants, cwd) {
100537
- const abs = resolve2(cwd, path2);
100863
+ const abs = resolve3(cwd, path2);
100538
100864
  const r = real(abs) ?? abs;
100539
100865
  for (const g5 of grants) {
100540
100866
  if (r === g5 || r.startsWith(g5.endsWith(sep4) ? g5 : g5 + sep4)) return true;
@@ -100835,32 +101161,32 @@ function orchestratorMemory(reader, sessionId) {
100835
101161
 
100836
101162
  // packages/agent-host/src/sessions/manager.ts
100837
101163
  import { homedir as homedir12 } from "node:os";
100838
- import { basename as basename5, isAbsolute as isAbsolute9, join as join28, relative as relative8, resolve as resolve10, sep as sep11 } from "node:path";
100839
- import { existsSync as existsSync18, rmSync as rmSync7, statSync as statSync12 } from "node:fs";
101164
+ import { basename as basename5, isAbsolute as isAbsolute9, join as join29, relative as relative8, resolve as resolve11, sep as sep11 } from "node:path";
101165
+ import { existsSync as existsSync19, rmSync as rmSync7, statSync as statSync12 } from "node:fs";
100840
101166
  import { exec as exec6 } from "node:child_process";
100841
101167
 
100842
101168
  // packages/agent-host/src/adapters/claude/index.ts
100843
101169
  init_sdk();
100844
101170
  import { homedir as homedir5 } from "node:os";
100845
- import { randomUUID as randomUUID8 } from "node:crypto";
101171
+ import { randomUUID as randomUUID9 } from "node:crypto";
100846
101172
 
100847
101173
  // packages/agent-host/src/adapters/input-receipts.ts
100848
- import { randomUUID as randomUUID7 } from "node:crypto";
101174
+ import { randomUUID as randomUUID8 } from "node:crypto";
100849
101175
  var InputReceipts = class {
100850
101176
  pending = /* @__PURE__ */ new Map();
100851
101177
  ended = /* @__PURE__ */ new Set();
100852
101178
  create(observer) {
100853
- const id2 = randomUUID7();
100854
- let resolve11;
101179
+ const id2 = randomUUID8();
101180
+ let resolve12;
100855
101181
  let reject;
100856
101182
  const done = new Promise((yes, no2) => {
100857
- resolve11 = yes;
101183
+ resolve12 = yes;
100858
101184
  reject = no2;
100859
101185
  });
100860
101186
  done.catch(() => {
100861
101187
  });
100862
101188
  const receipt = { id: id2, done, cancelled: false, completed: false };
100863
- this.pending.set(id2, { receipt, resolve: resolve11, reject, observer });
101189
+ this.pending.set(id2, { receipt, resolve: resolve12, reject, observer });
100864
101190
  return receipt;
100865
101191
  }
100866
101192
  get size() {
@@ -100971,7 +101297,7 @@ async function installedCliVersion(name, packageName, deps = realDeps2()) {
100971
101297
  // packages/agent-host/src/adapters/claude/exe-link.ts
100972
101298
  import { randomBytes as randomBytes2 } from "node:crypto";
100973
101299
  import { closeSync as closeSync6, copyFileSync as copyFileSync2, linkSync, mkdirSync as mkdirSync10, openSync as openSync6, readdirSync as readdirSync7, readFileSync as readFileSync10, readSync as readSync4, renameSync as renameSync10, rmSync as rmSync6, statSync as statSync11 } from "node:fs";
100974
- import { basename as basename4, dirname as dirname8, join as join21 } from "node:path";
101300
+ import { basename as basename4, dirname as dirname8, join as join22 } from "node:path";
100975
101301
  var PLACEHOLDER_MAX_BYTES = 1e6;
100976
101302
  var ClaudePlaceholderError = class extends Error {
100977
101303
  name = "ClaudePlaceholderError";
@@ -101007,7 +101333,7 @@ function placeholderProblem(exe, size, fs2 = realExeFs) {
101007
101333
  }
101008
101334
  if (head.toString("latin1") === "MZ") return null;
101009
101335
  const pkg = dirname8(dirname8(exe));
101010
- const installer = basename4(dirname8(exe)).toLowerCase() === "bin" ? join21(pkg, "install.cjs") : null;
101336
+ const installer = basename4(dirname8(exe)).toLowerCase() === "bin" ? join22(pkg, "install.cjs") : null;
101011
101337
  let canRerun = false;
101012
101338
  if (installer) {
101013
101339
  try {
@@ -101022,14 +101348,14 @@ function placeholderProblem(exe, size, fs2 = realExeFs) {
101022
101348
  canRerun ? `To fix it, close every Claude Code (Centralu's Claude sessions, terminals, editors) and run \`node "${installer}"\`, or reinstall with \`npm i -g @anthropic-ai/claude-code\` while no Claude Code is running.` : "To fix it, close every Claude Code (Centralu's Claude sessions, terminals, editors) and reinstall it with `npm i -g @anthropic-ai/claude-code`."
101023
101349
  ].join(" ");
101024
101350
  }
101025
- function claudeKey(exe, stat6, fs2 = realExeFs) {
101351
+ function claudeKey(exe, stat7, fs2 = realExeFs) {
101026
101352
  let version2 = null;
101027
101353
  try {
101028
- const pkg = JSON.parse(fs2.read(join21(dirname8(dirname8(exe)), "package.json")));
101354
+ const pkg = JSON.parse(fs2.read(join22(dirname8(dirname8(exe)), "package.json")));
101029
101355
  if (pkg.name === "@anthropic-ai/claude-code" && typeof pkg.version === "string" && pkg.version) version2 = pkg.version;
101030
101356
  } catch {
101031
101357
  }
101032
- const raw = version2 ? `${version2}-${stat6.size}` : `${stat6.size}-${Math.floor(stat6.mtimeMs)}`;
101358
+ const raw = version2 ? `${version2}-${stat7.size}` : `${stat7.size}-${Math.floor(stat7.mtimeMs)}`;
101033
101359
  return raw.replace(/[^A-Za-z0-9._-]/g, "_");
101034
101360
  }
101035
101361
  function windowsStart(platform = process.platform) {
@@ -101051,7 +101377,7 @@ var ClaudeLinks = class {
101051
101377
  return windowsStart(this.opts.platform);
101052
101378
  }
101053
101379
  base() {
101054
- return join21(this.opts.root(), "tools", "claude");
101380
+ return join22(this.opts.root(), "tools", "claude");
101055
101381
  }
101056
101382
  /**
101057
101383
  * What to start for the `claude.exe` found at `exe`. Off Windows, or for anything but an `.exe`
@@ -101062,26 +101388,26 @@ var ClaudeLinks = class {
101062
101388
  prepare(exe) {
101063
101389
  const direct = { path: exe, key: null };
101064
101390
  if (!this.enabled || !/\.exe$/i.test(exe)) return direct;
101065
- let stat6;
101391
+ let stat7;
101066
101392
  try {
101067
- stat6 = this.fs.stat(exe);
101393
+ stat7 = this.fs.stat(exe);
101068
101394
  } catch {
101069
101395
  return direct;
101070
101396
  }
101071
- const problem2 = placeholderProblem(exe, stat6.size, this.fs);
101397
+ const problem2 = placeholderProblem(exe, stat7.size, this.fs);
101072
101398
  if (problem2) throw new ClaudePlaceholderError(problem2);
101073
- const key = claudeKey(exe, stat6, this.fs);
101074
- const dir = join21(this.base(), key);
101075
- const target = join21(dir, "claude.exe");
101399
+ const key = claudeKey(exe, stat7, this.fs);
101400
+ const dir = join22(this.base(), key);
101401
+ const target = join22(dir, "claude.exe");
101076
101402
  const ready = () => {
101077
101403
  try {
101078
- return this.fs.stat(target).size === stat6.size;
101404
+ return this.fs.stat(target).size === stat7.size;
101079
101405
  } catch {
101080
101406
  return false;
101081
101407
  }
101082
101408
  };
101083
101409
  if (ready()) return this.use(key, target);
101084
- const tmp = join21(dir, `claude-${randomBytes2(4).toString("hex")}.tmp`);
101410
+ const tmp = join22(dir, `claude-${randomBytes2(4).toString("hex")}.tmp`);
101085
101411
  try {
101086
101412
  this.fs.mkdir(dir);
101087
101413
  try {
@@ -101141,7 +101467,7 @@ var ClaudeLinks = class {
101141
101467
  for (const name of names) {
101142
101468
  if (name === this.current || this.uses.has(name)) continue;
101143
101469
  try {
101144
- this.fs.removeDir(join21(this.base(), name));
101470
+ this.fs.removeDir(join22(this.base(), name));
101145
101471
  } catch {
101146
101472
  }
101147
101473
  }
@@ -101451,7 +101777,7 @@ async function readClaudeModels(q2) {
101451
101777
  }
101452
101778
 
101453
101779
  // packages/agent-host/src/adapters/local-process.ts
101454
- import { spawn as spawn4 } from "node:child_process";
101780
+ import { spawn as spawn5 } from "node:child_process";
101455
101781
  import { EventEmitter as EventEmitter3 } from "node:events";
101456
101782
  var EXIT_AFTER_OUTPUT_MS3 = 1e3;
101457
101783
  var STDERR_TAIL = 8 * 1024;
@@ -101481,7 +101807,7 @@ var LocalAgentProcess = class _LocalAgentProcess extends EventEmitter3 {
101481
101807
  exitReported = false;
101482
101808
  spawnedAt = Date.now();
101483
101809
  static spawn(spec, os2) {
101484
- const child = spawn4(spec.command, spec.args, {
101810
+ const child = spawn5(spec.command, spec.args, {
101485
101811
  cwd: spec.cwd,
101486
101812
  env: spec.env,
101487
101813
  stdio: ["pipe", "pipe", "pipe"],
@@ -102595,7 +102921,14 @@ function alwaysAllowKey(detail, opts = {}) {
102595
102921
  }
102596
102922
 
102597
102923
  // packages/agent-host/src/adapters/claude/cross-session.ts
102598
- function withoutCrossSessionMessaging(options) {
102924
+ function withoutCrossSessionMessaging(options, preset = "normal") {
102925
+ if (preset === "auto")
102926
+ return {
102927
+ ...options,
102928
+ settings: options.settings ?? {},
102929
+ disallowedTools: options.disallowedTools ?? [],
102930
+ env: { ...process.env, ...options.env }
102931
+ };
102599
102932
  const env = {
102600
102933
  ...process.env,
102601
102934
  ...options.env,
@@ -102658,10 +102991,19 @@ function readTarget(toolName, input) {
102658
102991
  const p = toolName === "Read" ? input.file_path : input.path;
102659
102992
  return typeof p === "string" && p ? p : null;
102660
102993
  }
102661
- function permissionOptionsFor(preset) {
102662
- if (preset === "auto") return { permissionMode: "bypassPermissions" };
102663
- if (preset === "safe") return { permissionMode: "default" };
102664
- return { resolvePermissionModeInCli: true };
102994
+ async function permissionOptionsFor(opts) {
102995
+ if (opts.permissionPreset === "auto") return { permissionMode: "bypassPermissions" };
102996
+ if (opts.permissionPreset === "safe") return { permissionMode: "default" };
102997
+ if (opts.noSettingFiles) return { permissionMode: await personsDefaultMode() ?? "default" };
102998
+ try {
102999
+ const resolved = await D9t({ cwd: opts.cwd, ...settingSourcesFor(opts) });
103000
+ const mode = qtt(resolved).permissions?.defaultMode;
103001
+ if (typeof mode === "string" && PERMISSION_MODES.includes(mode)) {
103002
+ return { permissionMode: mode };
103003
+ }
103004
+ } catch {
103005
+ }
103006
+ return { permissionMode: "default" };
102665
103007
  }
102666
103008
  var PERMISSION_MODES = ["default", "acceptEdits", "bypassPermissions", "plan", "dontAsk", "auto"];
102667
103009
  async function personsDefaultMode() {
@@ -102695,7 +103037,7 @@ var ClaudeSession = class {
102695
103037
  this.opts = opts;
102696
103038
  this.emit = emit;
102697
103039
  this.launch = launch2;
102698
- this.ended = new Promise((resolve11) => this.markEnded = resolve11);
103040
+ this.ended = new Promise((resolve12) => this.markEnded = resolve12);
102699
103041
  this.stream = new ClaudeStreamNormalizer(sessionId, () => ({
102700
103042
  model: opts.model ?? null,
102701
103043
  effort: opts.effort ?? null,
@@ -102801,7 +103143,7 @@ var ClaudeSession = class {
102801
103143
  this.orchestratorServer = orchestratorMcp(this.opts.orchestratorTools, this.opts.toolProfile, this.opts.sessionId);
102802
103144
  }
102803
103145
  const servers = this.mcpServers();
102804
- const personsMode = preset === "normal" && this.opts.noSettingFiles ? await personsDefaultMode() : null;
103146
+ const permissionOptions = await permissionOptionsFor(this.opts);
102805
103147
  if (!this.opts.processSource?.adopt) await this.launch.gate.turn();
102806
103148
  this.runKey = run?.key ?? null;
102807
103149
  this.launch.links.acquire(this.runKey);
@@ -102894,7 +103236,7 @@ var ClaudeSession = class {
102894
103236
  */
102895
103237
  ...this.opts.systemPromptAppend ? { systemPrompt: { type: "preset", preset: "claude_code", append: this.opts.systemPromptAppend } } : {}
102896
103238
  } : {},
102897
- ...personsMode ? { permissionMode: personsMode } : permissionOptionsFor(preset),
103239
+ ...permissionOptions,
102898
103240
  /*
102899
103241
  * The agent an app hands a schema and asks for output (M4 D-1). It is a per-query option,
102900
103242
  * so it can only be given when the session starts. Measured (SDK 0.3.263, CLI 2.1.282,
@@ -102946,8 +103288,8 @@ var ClaudeSession = class {
102946
103288
  if (questions.length === 0) return { behavior: "allow", updatedInput: toolInput };
102947
103289
  const requestId2 = `q-${++self2.reqCounter}`;
102948
103290
  self2.emit({ type: "question_request", sessionId: self2.sessionId, requestId: requestId2, questions });
102949
- return new Promise((resolve11) => {
102950
- self2.questions.set(requestId2, { resolve: resolve11, input: toolInput });
103291
+ return new Promise((resolve12) => {
103292
+ self2.questions.set(requestId2, { resolve: resolve12, input: toolInput });
102951
103293
  self2.releaseOnCancel(requestId2, options?.signal);
102952
103294
  });
102953
103295
  }
@@ -102956,17 +103298,17 @@ var ClaudeSession = class {
102956
103298
  if (key && self2.alwaysAllow.allows(key)) return { behavior: "allow", updatedInput: toolInput };
102957
103299
  const requestId = `req-${++self2.reqCounter}`;
102958
103300
  self2.emit({ type: "approval_request", sessionId: self2.sessionId, requestId, detail });
102959
- return new Promise((resolve11) => {
102960
- self2.pending.set(requestId, { resolve: resolve11, input: toolInput });
103301
+ return new Promise((resolve12) => {
103302
+ self2.pending.set(requestId, { resolve: resolve12, input: toolInput });
102961
103303
  self2.releaseOnCancel(requestId, options?.signal);
102962
103304
  });
102963
103305
  }
102964
- })
103306
+ }, preset)
102965
103307
  });
102966
103308
  ClaudeAdapter.liveQueries.add(q2);
102967
103309
  ClaudeAdapter.querySessions.set(q2, this.sessionId);
102968
103310
  this.releaseLostCalls(q2);
102969
- this.goalKnown = new Promise((resolve11) => this.settleGoalKnown = resolve11);
103311
+ this.goalKnown = new Promise((resolve12) => this.settleGoalKnown = resolve12);
102970
103312
  q2.supportedCommands().then((list) => {
102971
103313
  if (this.goalCommand === void 0 && Array.isArray(list)) this.goalCommand = list.some((c) => c?.name === "goal");
102972
103314
  }).catch(() => {
@@ -103136,8 +103478,8 @@ var ClaudeSession = class {
103136
103478
  * turn released, rather than left hanging; the person can send the message again.
103137
103479
  */
103138
103480
  releaseLostCalls(q2) {
103139
- const open5 = this.opts.processSource?.adopt?.openCalls ?? [];
103140
- const lost = open5.filter((c) => isOrchestratorTool(c.tool) || appToolOf(c.tool) !== null);
103481
+ const open6 = this.opts.processSource?.adopt?.openCalls ?? [];
103482
+ const lost = open6.filter((c) => isOrchestratorTool(c.tool) || appToolOf(c.tool) !== null);
103141
103483
  if (lost.length === 0) return;
103142
103484
  this.turnOpen = true;
103143
103485
  this.stream.stopped();
@@ -103194,7 +103536,7 @@ var ClaudeSession = class {
103194
103536
  send(text2) {
103195
103537
  this.sendInput(text2);
103196
103538
  }
103197
- sendInput(text2, uuid3 = randomUUID8(), note) {
103539
+ sendInput(text2, uuid3 = randomUUID9(), note) {
103198
103540
  const goal = isGoalCommand(text2);
103199
103541
  if (!this.sendGate && !(goal && this.goalCommand === void 0)) {
103200
103542
  this.deliver(text2, uuid3, !!note);
@@ -103268,11 +103610,11 @@ var ClaudeSession = class {
103268
103610
  * the model receives the answers in the CLI's own words (see the AskUserQuestion branch of `canUseTool`).
103269
103611
  */
103270
103612
  answerQuestion(requestId, answers) {
103271
- const open5 = this.questions.get(requestId);
103272
- if (!open5) return false;
103613
+ const open6 = this.questions.get(requestId);
103614
+ if (!open6) return false;
103273
103615
  this.questions.delete(requestId);
103274
103616
  const byQuestion = Object.fromEntries(answers.map((a) => [a.question, a.answers.join(", ")]));
103275
- open5.resolve({ behavior: "allow", updatedInput: { ...open5.input, answers: byQuestion } });
103617
+ open6.resolve({ behavior: "allow", updatedInput: { ...open6.input, answers: byQuestion } });
103276
103618
  this.emit({ type: "question_resolved", sessionId: this.sessionId, requestId });
103277
103619
  return true;
103278
103620
  }
@@ -103548,8 +103890,8 @@ var ClaudeSession = class {
103548
103890
  }
103549
103891
  /** Releases a choice that was still waiting on an answer — **never released silently**, for the same reason as approval. */
103550
103892
  releaseQuestions(why) {
103551
- for (const [id2, open5] of this.questions) {
103552
- open5.resolve({ behavior: "deny", message: why });
103893
+ for (const [id2, open6] of this.questions) {
103894
+ open6.resolve({ behavior: "deny", message: why });
103553
103895
  this.emit({ type: "question_resolved", sessionId: this.sessionId, requestId: id2 });
103554
103896
  }
103555
103897
  this.questions.clear();
@@ -103655,7 +103997,7 @@ var ClaudeAdapter = class _ClaudeAdapter {
103655
103997
  sleep: true
103656
103998
  };
103657
103999
  launchSettingsFingerprint(settings) {
103658
- return JSON.stringify({ v: 1, cwd: settings.cwd, permission: settings.permissionPreset, trusted: !!settings.projectTrusted, noSettings: !!settings.noSettingFiles });
104000
+ return JSON.stringify({ v: settings.permissionPreset === "safe" ? 1 : 2, cwd: settings.cwd, permission: settings.permissionPreset, trusted: !!settings.projectTrusted, noSettings: !!settings.noSettingFiles });
103659
104001
  }
103660
104002
  async detect() {
103661
104003
  const path2 = whichTool("claude");
@@ -103742,7 +104084,7 @@ var ClaudeAdapter = class _ClaudeAdapter {
103742
104084
  let timer;
103743
104085
  await Promise.race([
103744
104086
  Promise.allSettled([...this.launch.closing]),
103745
- new Promise((resolve11) => timer = setTimeout(resolve11, this.launch.exitGraceMs))
104087
+ new Promise((resolve12) => timer = setTimeout(resolve12, this.launch.exitGraceMs))
103746
104088
  ]);
103747
104089
  clearTimeout(timer);
103748
104090
  }
@@ -103752,7 +104094,7 @@ var ClaudeAdapter = class _ClaudeAdapter {
103752
104094
  import { execFile as execFile5 } from "node:child_process";
103753
104095
 
103754
104096
  // packages/agent-host/src/adapters/codex/bridge-path.ts
103755
- import { existsSync as existsSync14 } from "node:fs";
104097
+ import { existsSync as existsSync15 } from "node:fs";
103756
104098
  import { fileURLToPath as fileURLToPath3 } from "node:url";
103757
104099
  function bridgePath() {
103758
104100
  const candidates = [
@@ -103761,23 +104103,23 @@ function bridgePath() {
103761
104103
  new URL("./codex-orchestrator-bridge.mjs", import.meta.url)
103762
104104
  // the bundled build-output layout
103763
104105
  ].map((u) => fileURLToPath3(u));
103764
- const found = candidates.find((p) => existsSync14(p));
104106
+ const found = candidates.find((p) => existsSync15(p));
103765
104107
  if (!found) throw new Error(`orchestrator bridge not found: ${candidates.join(", ")}`);
103766
104108
  return found;
103767
104109
  }
103768
104110
 
103769
104111
  // packages/agent-host/src/adapters/codex/index.ts
103770
- import { existsSync as existsSync16, realpathSync as realpathSync7 } from "node:fs";
104112
+ import { existsSync as existsSync17, realpathSync as realpathSync7 } from "node:fs";
103771
104113
  import { homedir as homedir10 } from "node:os";
103772
- import { dirname as dirname9, join as join23, resolve as resolve8 } from "node:path";
104114
+ import { dirname as dirname9, join as join24, resolve as resolve9 } from "node:path";
103773
104115
  import { promisify as promisify5 } from "node:util";
103774
104116
 
103775
104117
  // packages/agent-host/src/adapters/codex/native.ts
103776
- import { existsSync as existsSync15, readFileSync as readFileSync11, realpathSync as realpathSync6 } from "node:fs";
104118
+ import { existsSync as existsSync16, readFileSync as readFileSync11, realpathSync as realpathSync6 } from "node:fs";
103777
104119
  var realDeps3 = () => ({
103778
104120
  platform: HOST_PLATFORM,
103779
104121
  arch: process.arch,
103780
- exists: existsSync15,
104122
+ exists: existsSync16,
103781
104123
  read: (p) => readFileSync11(p, "utf8"),
103782
104124
  realpath: (p) => realpathSync6(p),
103783
104125
  launch: (p) => launchFor(p),
@@ -103946,14 +104288,14 @@ var CodexClient = class {
103946
104288
  if (this.closed) return Promise.reject(new Error("codex app-server has already exited"));
103947
104289
  const id2 = `${this.idPrefix}${this.nextId++}`;
103948
104290
  this.write({ id: id2, method, params });
103949
- return new Promise((resolve11, reject) => {
104291
+ return new Promise((resolve12, reject) => {
103950
104292
  const timer = setTimeout(() => {
103951
104293
  if (this.pending.delete(id2)) reject(new Error(`No response for ${method}`));
103952
104294
  }, timeoutMs);
103953
104295
  this.pending.set(id2, {
103954
104296
  resolve: (v) => {
103955
104297
  clearTimeout(timer);
103956
- resolve11(v);
104298
+ resolve12(v);
103957
104299
  },
103958
104300
  reject: (e) => {
103959
104301
  clearTimeout(timer);
@@ -103977,12 +104319,12 @@ var CodexClient = class {
103977
104319
  if (this.closed) return;
103978
104320
  this.closed = true;
103979
104321
  this.proc.stdin.end();
103980
- const exited = await new Promise((resolve11) => {
103981
- if (this.proc.exitCode !== null || this.proc.signalCode != null || this.finished) return resolve11(true);
103982
- const t = setTimeout(() => resolve11(false), 2e3);
104322
+ const exited = await new Promise((resolve12) => {
104323
+ if (this.proc.exitCode !== null || this.proc.signalCode != null || this.finished) return resolve12(true);
104324
+ const t = setTimeout(() => resolve12(false), 2e3);
103983
104325
  this.proc.once("exit", () => {
103984
104326
  clearTimeout(t);
103985
- resolve11(true);
104327
+ resolve12(true);
103986
104328
  });
103987
104329
  });
103988
104330
  if (!exited) this.proc.kill("SIGKILL");
@@ -104003,19 +104345,19 @@ var CodexClient = class {
104003
104345
  };
104004
104346
 
104005
104347
  // packages/agent-host/src/adapters/codex/rollout.ts
104006
- import { createReadStream as createReadStream2 } from "node:fs";
104007
- import { readdir as readdir4 } from "node:fs/promises";
104348
+ import { createReadStream as createReadStream3 } from "node:fs";
104349
+ import { readdir as readdir5 } from "node:fs/promises";
104008
104350
  import { createInterface as createInterface2 } from "node:readline";
104009
104351
  import { homedir as homedir6 } from "node:os";
104010
- import { join as join22 } from "node:path";
104352
+ import { join as join23 } from "node:path";
104011
104353
  var MIN_SUMMARY_CHARS = 200;
104012
- async function findRolloutPath(threadId, sessionsDir = join22(homedir6(), ".codex", "sessions")) {
104354
+ async function findRolloutPath(threadId, sessionsDir = join23(homedir6(), ".codex", "sessions")) {
104013
104355
  try {
104014
104356
  const suffix = `-${threadId}.jsonl`;
104015
- const names = await readdir4(sessionsDir, { recursive: true });
104357
+ const names = await readdir5(sessionsDir, { recursive: true });
104016
104358
  const hits = names.filter((n) => String(n).endsWith(suffix)).sort();
104017
104359
  const hit = hits[hits.length - 1];
104018
- return hit ? join22(sessionsDir, String(hit)) : null;
104360
+ return hit ? join23(sessionsDir, String(hit)) : null;
104019
104361
  } catch {
104020
104362
  return null;
104021
104363
  }
@@ -104039,7 +104381,7 @@ async function lastCompactSummary(threadId, sessionsDir) {
104039
104381
  if (!path2) return null;
104040
104382
  try {
104041
104383
  let last = null;
104042
- const rl2 = createInterface2({ input: createReadStream2(path2, "utf8"), crlfDelay: Infinity });
104384
+ const rl2 = createInterface2({ input: createReadStream3(path2, "utf8"), crlfDelay: Infinity });
104043
104385
  for await (const line of rl2) {
104044
104386
  if (!line.includes('"compacted"')) continue;
104045
104387
  try {
@@ -104216,7 +104558,7 @@ function userInputText(content) {
104216
104558
  }
104217
104559
 
104218
104560
  // packages/agent-host/src/adapters/codex/images.ts
104219
- import { readFile as readFile2, stat as stat4 } from "node:fs/promises";
104561
+ import { readFile as readFile3, stat as stat5 } from "node:fs/promises";
104220
104562
  var IMAGE_MIMES2 = {
104221
104563
  png: "image/png",
104222
104564
  jpg: "image/jpeg",
@@ -104232,9 +104574,9 @@ async function imageEventFromDisk(sessionId, path2, maxBytes = IMAGE_MAX_BYTES)
104232
104574
  const mime = IMAGE_MIMES2[ext2];
104233
104575
  if (!mime) return fail4(`Cannot display this format (.${ext2})`);
104234
104576
  try {
104235
- const s = await stat4(path2);
104577
+ const s = await stat5(path2);
104236
104578
  if (s.size > maxBytes) return fail4(`Image is too large (${Math.round(s.size / 1048576)}MB)`);
104237
- const buf = await readFile2(path2);
104579
+ const buf = await readFile3(path2);
104238
104580
  return { type: "message_image", sessionId, mime, data: buf.toString("base64"), path: path2 };
104239
104581
  } catch (err) {
104240
104582
  return fail4(`Failed to read image: ${err.message}`);
@@ -105218,16 +105560,16 @@ var CodexChildTracker = class {
105218
105560
  };
105219
105561
 
105220
105562
  // packages/agent-host/src/adapters/codex/sandbox.ts
105221
- import { realpath as realpath5 } from "node:fs/promises";
105222
- import { resolve as resolve7 } from "node:path";
105563
+ import { realpath as realpath6 } from "node:fs/promises";
105564
+ import { resolve as resolve8 } from "node:path";
105223
105565
  async function sandboxConfig(cwd, preset, readConfig) {
105224
105566
  const roots = [];
105225
105567
  try {
105226
105568
  const stdout = await runGit(cwd, ["rev-parse", "--path-format=absolute", "--git-dir", "--git-common-dir"], { timeout: 5e3 });
105227
105569
  const [gitDir, commonDir] = stdout.trim().split(/\r?\n/);
105228
105570
  if (gitDir && commonDir) {
105229
- const gitPath = await realpath5(resolve7(cwd, gitDir));
105230
- const commonPath = await realpath5(resolve7(cwd, commonDir));
105571
+ const gitPath = await realpath6(resolve8(cwd, gitDir));
105572
+ const commonPath = await realpath6(resolve8(cwd, commonDir));
105231
105573
  if (gitPath !== commonPath) roots.push(commonPath);
105232
105574
  }
105233
105575
  } catch {
@@ -105255,7 +105597,7 @@ function permissionOptionsFor2(preset) {
105255
105597
  function repoFilesConfig(opts) {
105256
105598
  if (opts.projectTrusted === true && !opts.noSettingFiles) return {};
105257
105599
  const keys = /* @__PURE__ */ new Set();
105258
- for (const start2 of [resolve8(opts.cwd), realPathOr(opts.cwd)]) {
105600
+ for (const start2 of [resolve9(opts.cwd), realPathOr(opts.cwd)]) {
105259
105601
  for (let dir = start2; ; dir = dirname9(dir)) {
105260
105602
  keys.add(dir);
105261
105603
  if (dirname9(dir) === dir) break;
@@ -105270,7 +105612,7 @@ function realPathOr(path2) {
105270
105612
  try {
105271
105613
  return realpathSync7.native(path2);
105272
105614
  } catch {
105273
- return resolve8(path2);
105615
+ return resolve9(path2);
105274
105616
  }
105275
105617
  }
105276
105618
  var APP_APPROVAL_MODE = {
@@ -106161,10 +106503,10 @@ var CodexSession = class {
106161
106503
  return this.opts.outputSchema ? { outputSchema: this.opts.outputSchema } : {};
106162
106504
  }
106163
106505
  respondApproval(requestId, decision, _scope, matcher) {
106164
- const open5 = this.approvals.get(requestId);
106165
- if (open5 === void 0) return false;
106506
+ const open6 = this.approvals.get(requestId);
106507
+ if (open6 === void 0) return false;
106166
106508
  this.approvals.delete(requestId);
106167
- const serverId = open5.serverId;
106509
+ const serverId = open6.serverId;
106168
106510
  if (this.elicitations.delete(requestId)) {
106169
106511
  this.client.respond(serverId, {
106170
106512
  action: decision === "deny" ? "decline" : "accept",
@@ -106389,7 +106731,7 @@ function threadIdOf(res) {
106389
106731
  }
106390
106732
  function codexHome() {
106391
106733
  const custom2 = process.env.CODEX_HOME?.trim();
106392
- return custom2 ? custom2 : join23(homedir10(), ".codex");
106734
+ return custom2 ? custom2 : join24(homedir10(), ".codex");
106393
106735
  }
106394
106736
  var CODEX_VERBOSITIES = ["low", "medium", "high"];
106395
106737
  var CodexAdapter = class {
@@ -106445,7 +106787,7 @@ var CodexAdapter = class {
106445
106787
  const launch2 = launchFor(path2 ?? "codex");
106446
106788
  const { stdout } = await exec4(launch2.command, [...launch2.args, "--version"], { timeout: 5e3 });
106447
106789
  const version2 = `${stdout.trim()} \xB7 ${path2 ?? "PATH"}`;
106448
- const loggedIn = existsSync16(join23(codexHome(), "auth.json"));
106790
+ const loggedIn = existsSync17(join24(codexHome(), "auth.json"));
106449
106791
  return {
106450
106792
  tool: "codex",
106451
106793
  installed: true,
@@ -106579,7 +106921,7 @@ function registeredTool(adapters2, preferred) {
106579
106921
 
106580
106922
  // packages/agent-host/src/dev-services/store.ts
106581
106923
  import Database from "better-sqlite3";
106582
- import { existsSync as existsSync17, readFileSync as readFileSync12 } from "node:fs";
106924
+ import { existsSync as existsSync18, readFileSync as readFileSync12 } from "node:fs";
106583
106925
  import { fileURLToPath as fileURLToPath4 } from "node:url";
106584
106926
  function resolveSchemaPath() {
106585
106927
  const candidates = [
@@ -106588,7 +106930,7 @@ function resolveSchemaPath() {
106588
106930
  new URL("../../../protocol/src/schema/schema.sql", import.meta.url)
106589
106931
  // source tree
106590
106932
  ].map((u) => fileURLToPath4(u));
106591
- const found = candidates.find((p) => existsSync17(p));
106933
+ const found = candidates.find((p) => existsSync18(p));
106592
106934
  if (!found) throw new Error(`schema.sql not found: ${candidates.join(", ")}`);
106593
106935
  return found;
106594
106936
  }
@@ -106866,7 +107208,7 @@ var Store = class _Store {
106866
107208
  const probe = Object.create(_Store.prototype);
106867
107209
  const steps = probe.migrationSteps();
106868
107210
  const known = steps[steps.length - 1].to;
106869
- if (dbPath2 === ":memory:" || !existsSync17(dbPath2)) return empty(known);
107211
+ if (dbPath2 === ":memory:" || !existsSync18(dbPath2)) return empty(known);
106870
107212
  const db = new Database(dbPath2, { readonly: true, fileMustExist: true });
106871
107213
  try {
106872
107214
  probe.db = db;
@@ -108667,7 +109009,7 @@ var Store = class _Store {
108667
109009
  this.db.prepare(`DELETE FROM sessions WHERE id = ? AND deleted_at IS NOT NULL`).run(sessionId);
108668
109010
  return true;
108669
109011
  });
108670
- while (!step()) await new Promise((resolve11) => setImmediate(resolve11));
109012
+ while (!step()) await new Promise((resolve12) => setImmediate(resolve12));
108671
109013
  return true;
108672
109014
  }
108673
109015
  /** One session's messages in seq order, one transaction per chunk, letting the event loop go between chunks (#179) */
@@ -108682,7 +109024,7 @@ var Store = class _Store {
108682
109024
  if (rows.length > 0) after = rows[rows.length - 1].seq;
108683
109025
  return rows.length < chunk;
108684
109026
  });
108685
- while (!step()) await new Promise((resolve11) => setImmediate(resolve11));
109027
+ while (!step()) await new Promise((resolve12) => setImmediate(resolve12));
108686
109028
  }
108687
109029
  isTrashed(sessionId) {
108688
109030
  return !!this.db.prepare(`SELECT 1 FROM sessions WHERE id = ? AND deleted_at IS NOT NULL`).get(sessionId);
@@ -109349,7 +109691,8 @@ function parseTrashRecord(json2) {
109349
109691
  projectName: str8(r.projectName),
109350
109692
  projectPath: str8(r.projectPath),
109351
109693
  removeExternal: r.removeExternal === true,
109352
- removeWorktree: r.removeWorktree === true
109694
+ removeWorktree: r.removeWorktree === true,
109695
+ ...r.recovery && typeof r.recovery === "object" && typeof r.recovery.snapshot === "string" && typeof r.recovery.tip === "string" && typeof r.recovery.changedFiles === "number" && typeof r.recovery.commits === "number" ? { recovery: r.recovery } : {}
109353
109696
  };
109354
109697
  }
109355
109698
  var OUT_OF_TRASH = `session_id NOT IN (SELECT id FROM sessions WHERE deleted_at IS NOT NULL)`;
@@ -109374,7 +109717,7 @@ function indexedText(kind, payload) {
109374
109717
 
109375
109718
  // packages/agent-host/src/dev-services/git.ts
109376
109719
  import { execFile as execFile6 } from "node:child_process";
109377
- import { isAbsolute as isAbsolute8, relative as relative7, resolve as resolve9, sep as sep9 } from "node:path";
109720
+ import { isAbsolute as isAbsolute8, relative as relative7, resolve as resolve10, sep as sep9 } from "node:path";
109378
109721
  import { promisify as promisify6 } from "node:util";
109379
109722
  var exec5 = promisify6(execFile6);
109380
109723
  var OK2 = { timeout: 1e4, maxBuffer: 32 * 1024 * 1024 };
@@ -109446,8 +109789,8 @@ function assertLexicalGitPath(cwd, path2) {
109446
109789
  if (!path2 || path2.includes("\0") || path2.startsWith(":")) {
109447
109790
  throw Object.assign(new Error("Invalid git path"), { code: "internal" });
109448
109791
  }
109449
- const root3 = resolve9(cwd);
109450
- const target = isAbsolute8(path2) ? resolve9(path2) : resolve9(root3, path2);
109792
+ const root3 = resolve10(cwd);
109793
+ const target = isAbsolute8(path2) ? resolve10(path2) : resolve10(root3, path2);
109451
109794
  const rel = relative7(root3, target);
109452
109795
  if (rel === "") throw Object.assign(new Error("Path is the project root, not a file in it"), { code: "internal" });
109453
109796
  if (rel !== ".." && !rel.startsWith(`..${sep9}`) && !isAbsolute8(rel)) return rel;
@@ -109566,14 +109909,14 @@ async function gitIgnoredEntries(cwd, limit = 50, trust = {}) {
109566
109909
  if (paths.length === 0) return [];
109567
109910
  const sizes = /* @__PURE__ */ new Map();
109568
109911
  try {
109569
- const out = await new Promise((resolve11, reject) => {
109912
+ const out = await new Promise((resolve12, reject) => {
109570
109913
  execFile6(
109571
109914
  "du",
109572
109915
  ["-sk", ...paths.slice(0, limit)],
109573
109916
  { cwd, timeout: 5e3, maxBuffer: 1 << 20 },
109574
109917
  (err, stdout) => (
109575
109918
  // du exits non-zero if even one entry cannot be read — whatever it did print is used anyway
109576
- stdout ? resolve11(stdout) : reject(err)
109919
+ stdout ? resolve12(stdout) : reject(err)
109577
109920
  )
109578
109921
  );
109579
109922
  });
@@ -109686,8 +110029,8 @@ async function gitValidBranchName(name) {
109686
110029
  return false;
109687
110030
  }
109688
110031
  }
109689
- async function gitWorktreeRemove(repoCwd, path2, force = false) {
109690
- await git(repoCwd, ["worktree", "remove", ...force ? ["--force"] : [], path2]);
110032
+ async function gitWorktreeRemove(repoCwd, path2, force = false, withSubmodules = false) {
110033
+ await git(repoCwd, ["worktree", "remove", ...force ? ["--force", ...withSubmodules ? ["--force"] : []] : [], path2]);
109691
110034
  }
109692
110035
  async function gitBranchDelete(repoCwd, branch, trust = {}) {
109693
110036
  await git(repoCwd, ["branch", "-D", branch], trust);
@@ -109703,13 +110046,13 @@ async function gitWorktreeDirty(path2, trust = {}) {
109703
110046
  }
109704
110047
 
109705
110048
  // packages/agent-host/src/dev-services/worktree-dependencies.ts
109706
- import { lstat as lstat4, readFile as readFile3, readdir as readdir5 } from "node:fs/promises";
109707
- import { join as join24 } from "node:path";
110049
+ import { lstat as lstat5, readFile as readFile4, readdir as readdir6 } from "node:fs/promises";
110050
+ import { join as join25 } from "node:path";
109708
110051
  var LOCKFILES = ["pnpm-lock.yaml", "package-lock.json", "npm-shrinkwrap.json", "yarn.lock", "bun.lock", "bun.lockb"];
109709
110052
  async function prepareWorktreeDependencies(projectCwd, worktreeCwd) {
109710
110053
  let locked = false;
109711
110054
  for (const name of LOCKFILES) {
109712
- const target = await lstat4(join24(worktreeCwd, name)).catch((err) => {
110055
+ const target = await lstat5(join25(worktreeCwd, name)).catch((err) => {
109713
110056
  if (err.code === "ENOENT") return null;
109714
110057
  throw err;
109715
110058
  });
@@ -109718,7 +110061,7 @@ async function prepareWorktreeDependencies(projectCwd, worktreeCwd) {
109718
110061
  resolveExisting(projectCwd, name),
109719
110062
  resolveExisting(worktreeCwd, name)
109720
110063
  ]);
109721
- const [before, after] = await Promise.all([readFile3(source), readFile3(destination)]);
110064
+ const [before, after] = await Promise.all([readFile4(source), readFile4(destination)]);
109722
110065
  if (!before.equals(after)) return;
109723
110066
  locked = true;
109724
110067
  }
@@ -109727,22 +110070,22 @@ async function prepareWorktreeDependencies(projectCwd, worktreeCwd) {
109727
110070
  const folders = [""];
109728
110071
  while (folders.length) {
109729
110072
  const folder = folders.pop();
109730
- const entries = await readdir5(join24(worktreeCwd, folder), { withFileTypes: true });
110073
+ const entries = await readdir6(join25(worktreeCwd, folder), { withFileTypes: true });
109731
110074
  for (const entry of entries) {
109732
110075
  if (entry.isDirectory() && entry.name !== ".git" && entry.name !== "node_modules") {
109733
- folders.push(join24(folder, entry.name));
110076
+ folders.push(join25(folder, entry.name));
109734
110077
  }
109735
110078
  }
109736
110079
  if (folder && !entries.some((e) => e.name === "package.json" && e.isFile())) continue;
109737
110080
  if (entries.some((e) => e.name === "node_modules")) continue;
109738
- const rel = join24(folder, "node_modules");
110081
+ const rel = join25(folder, "node_modules");
109739
110082
  try {
109740
110083
  const src = await resolveExisting(projectCwd, rel);
109741
- if (!(await lstat4(src)).isDirectory()) continue;
109742
- const manifest = join24(folder, "package.json");
110084
+ if (!(await lstat5(src)).isDirectory()) continue;
110085
+ const manifest = join25(folder, "package.json");
109743
110086
  const [source, target] = await Promise.all([
109744
- resolveExisting(projectCwd, manifest).then((p) => readFile3(p)),
109745
- resolveExisting(worktreeCwd, manifest).then((p) => readFile3(p))
110087
+ resolveExisting(projectCwd, manifest).then((p) => readFile4(p)),
110088
+ resolveExisting(worktreeCwd, manifest).then((p) => readFile4(p))
109746
110089
  ]);
109747
110090
  if (!source.equals(target)) continue;
109748
110091
  const dst = await prepareCopyTarget(worktreeCwd, rel);
@@ -109764,8 +110107,8 @@ async function prepareWorktreeDependencies(projectCwd, worktreeCwd) {
109764
110107
  }
109765
110108
 
109766
110109
  // packages/agent-host/src/dev-services/file-search.ts
109767
- import { readdir as readdir6 } from "node:fs/promises";
109768
- import { join as join25 } from "node:path";
110110
+ import { readdir as readdir7 } from "node:fs/promises";
110111
+ import { join as join26 } from "node:path";
109769
110112
  var MAX_FILES2 = 2e4;
109770
110113
  var TTL_MS2 = 15e3;
109771
110114
  var WALK_DEPTH = 6;
@@ -109793,7 +110136,7 @@ async function walk(root3) {
109793
110136
  const { dir, rel, depth } = queue.shift();
109794
110137
  let entries;
109795
110138
  try {
109796
- entries = await readdir6(dir, { withFileTypes: true });
110139
+ entries = await readdir7(dir, { withFileTypes: true });
109797
110140
  } catch {
109798
110141
  continue;
109799
110142
  }
@@ -109801,7 +110144,7 @@ async function walk(root3) {
109801
110144
  if (e.name.startsWith(".") && e.name !== ".env") continue;
109802
110145
  if (e.isDirectory()) {
109803
110146
  if (SKIP.has(e.name) || depth >= WALK_DEPTH) continue;
109804
- queue.push({ dir: join25(dir, e.name), rel: wireJoin(rel, e.name), depth: depth + 1 });
110147
+ queue.push({ dir: join26(dir, e.name), rel: wireJoin(rel, e.name), depth: depth + 1 });
109805
110148
  } else {
109806
110149
  out.push(wireJoin(rel, e.name).normalize("NFC"));
109807
110150
  if (out.length >= MAX_FILES2) break;
@@ -109866,14 +110209,14 @@ async function searchFiles(root3, query, limit = 20, trust = {}) {
109866
110209
  }
109867
110210
 
109868
110211
  // packages/agent-host/src/dev-services/attachments.ts
109869
- import { mkdir as mkdir4, writeFile as writeFile2, rm as rm4 } from "node:fs/promises";
109870
- import { extname as extname3, join as join26 } from "node:path";
109871
- var root = () => join26(dataRoot(), "attachments");
110212
+ import { mkdir as mkdir5, writeFile as writeFile3, rm as rm5 } from "node:fs/promises";
110213
+ import { extname as extname3, join as join27 } from "node:path";
110214
+ var root = () => join27(dataRoot(), "attachments");
109872
110215
  function sessionDir(sessionId) {
109873
110216
  if (!isSessionId(sessionId)) {
109874
110217
  throw Object.assign(new Error(`Not a session id: ${sessionId}`), { code: "internal" });
109875
110218
  }
109876
- return join26(root(), sessionId);
110219
+ return join27(root(), sessionId);
109877
110220
  }
109878
110221
  var EXT = {
109879
110222
  "image/png": ".png",
@@ -109883,43 +110226,43 @@ var EXT = {
109883
110226
  };
109884
110227
  async function saveAttachment(sessionId, name, mime, dataBase64) {
109885
110228
  const dir = sessionDir(sessionId);
109886
- await mkdir4(dir, { recursive: true });
110229
+ await mkdir5(dir, { recursive: true });
109887
110230
  const ext2 = extname3(name) || EXT[mime] || "";
109888
- const file2 = join26(dir, `${Date.now()}-${Math.random().toString(36).slice(2, 8)}${ext2}`);
110231
+ const file2 = join27(dir, `${Date.now()}-${Math.random().toString(36).slice(2, 8)}${ext2}`);
109889
110232
  const buf = Buffer.from(dataBase64, "base64");
109890
- await writeFile2(file2, buf);
110233
+ await writeFile3(file2, buf);
109891
110234
  return { kind: mime.startsWith("image/") ? "image" : "file", path: file2, name, mime, bytes: buf.length };
109892
110235
  }
109893
110236
  async function clearAttachments(sessionId) {
109894
- await rm4(sessionDir(sessionId), { recursive: true, force: true });
110237
+ await rm5(sessionDir(sessionId), { recursive: true, force: true });
109895
110238
  }
109896
110239
  async function attachmentBytes(sessionId) {
109897
- const { readdir: readdir9, lstat: lstat6 } = await import("node:fs/promises");
110240
+ const { readdir: readdir10, lstat: lstat7 } = await import("node:fs/promises");
109898
110241
  const dir = sessionDir(sessionId);
109899
- const names = await readdir9(dir).catch(() => []);
110242
+ const names = await readdir10(dir).catch(() => []);
109900
110243
  let total = 0;
109901
110244
  for (const name of names) {
109902
- const s = await lstat6(join26(dir, name)).catch(() => null);
110245
+ const s = await lstat7(join27(dir, name)).catch(() => null);
109903
110246
  if (s?.isFile()) total += s.size;
109904
110247
  }
109905
110248
  return total;
109906
110249
  }
109907
110250
  var ATTACHMENTS_MAX_BYTES = 500 * 1048576;
109908
110251
  async function sweepAttachments(maxBytes = ATTACHMENTS_MAX_BYTES) {
109909
- const { readdir: readdir9, stat: stat6, rm: rmFile } = await import("node:fs/promises");
110252
+ const { readdir: readdir10, stat: stat7, rm: rmFile } = await import("node:fs/promises");
109910
110253
  const files = [];
109911
110254
  let dirs;
109912
110255
  try {
109913
- dirs = await readdir9(root());
110256
+ dirs = await readdir10(root());
109914
110257
  } catch {
109915
110258
  return 0;
109916
110259
  }
109917
110260
  for (const d2 of dirs) {
109918
- const dir = join26(root(), d2);
109919
- const names = await readdir9(dir).catch(() => []);
110261
+ const dir = join27(root(), d2);
110262
+ const names = await readdir10(dir).catch(() => []);
109920
110263
  for (const name of names) {
109921
- const p = join26(dir, name);
109922
- const s = await stat6(p).catch(() => null);
110264
+ const p = join27(dir, name);
110265
+ const s = await stat7(p).catch(() => null);
109923
110266
  if (s?.isFile()) files.push({ path: p, size: s.size, mtime: s.mtimeMs });
109924
110267
  }
109925
110268
  }
@@ -109939,7 +110282,7 @@ async function sweepAttachments(maxBytes = ATTACHMENTS_MAX_BYTES) {
109939
110282
 
109940
110283
  // packages/agent-host/src/dev-services/message-image.ts
109941
110284
  import { constants as constants7 } from "node:fs";
109942
- import { open as open4, realpath as realpath6 } from "node:fs/promises";
110285
+ import { open as open5, realpath as realpath7 } from "node:fs/promises";
109943
110286
  import { homedir as homedir11 } from "node:os";
109944
110287
  import { posix as posix3, sep as sep10, win32 as win326 } from "node:path";
109945
110288
  import { fileURLToPath as fileURLToPath5 } from "node:url";
@@ -109996,7 +110339,7 @@ var mb2 = (n) => `${(n / 1e6).toFixed(1)} MB`;
109996
110339
  async function readMessageImage(abs, max = IMAGE_PREVIEW_MAX_BYTES) {
109997
110340
  let file2;
109998
110341
  try {
109999
- file2 = await realpath6(abs);
110342
+ file2 = await realpath7(abs);
110000
110343
  } catch (e) {
110001
110344
  const code2 = e.code;
110002
110345
  if (code2 === "EACCES" || code2 === "EPERM") return refuse5("unreadable", `Centralu may not read ${abs}`);
@@ -110004,7 +110347,7 @@ async function readMessageImage(abs, max = IMAGE_PREVIEW_MAX_BYTES) {
110004
110347
  }
110005
110348
  let handle;
110006
110349
  try {
110007
- handle = await open4(file2, OPEN_FLAGS);
110350
+ handle = await open5(file2, OPEN_FLAGS);
110008
110351
  } catch (e) {
110009
110352
  const code2 = e.code;
110010
110353
  if (code2 === "EACCES" || code2 === "EPERM") return refuse5("unreadable", `Centralu may not read ${file2}`, file2);
@@ -110035,36 +110378,36 @@ async function readMessageImage(abs, max = IMAGE_PREVIEW_MAX_BYTES) {
110035
110378
  }
110036
110379
 
110037
110380
  // packages/agent-host/src/dev-services/handoff-notes.ts
110038
- import { lstat as lstat5, mkdir as mkdir5, readdir as readdir7, rm as rm5, writeFile as writeFile3 } from "node:fs/promises";
110039
- import { join as join27 } from "node:path";
110040
- var root2 = () => join27(dataRoot(), "handoff");
110381
+ import { lstat as lstat6, mkdir as mkdir6, readdir as readdir8, rm as rm6, writeFile as writeFile4 } from "node:fs/promises";
110382
+ import { join as join28 } from "node:path";
110383
+ var root2 = () => join28(dataRoot(), "handoff");
110041
110384
  function handoffNoteDir(projectId) {
110042
110385
  if (!isProjectId(projectId)) {
110043
110386
  throw Object.assign(new Error(`Not a project id: ${projectId}`), { code: "internal" });
110044
110387
  }
110045
- return join27(root2(), projectId);
110388
+ return join28(root2(), projectId);
110046
110389
  }
110047
110390
  function handoffNotePath(projectId, sessionId) {
110048
110391
  if (!isSessionId(sessionId)) {
110049
110392
  throw Object.assign(new Error(`Not a session id: ${sessionId}`), { code: "internal" });
110050
110393
  }
110051
- return join27(handoffNoteDir(projectId), `${sessionId}.md`);
110394
+ return join28(handoffNoteDir(projectId), `${sessionId}.md`);
110052
110395
  }
110053
110396
  async function handoffNoteBytes(projectId, sessionId) {
110054
- const s = await lstat5(handoffNotePath(projectId, sessionId)).catch(() => null);
110397
+ const s = await lstat6(handoffNotePath(projectId, sessionId)).catch(() => null);
110055
110398
  return s?.isFile() ? s.size : 0;
110056
110399
  }
110057
110400
  async function writeHandoffNote(projectId, sessionId, text2) {
110058
110401
  const path2 = handoffNotePath(projectId, sessionId);
110059
- await mkdir5(handoffNoteDir(projectId), { recursive: true });
110060
- await rm5(path2, { force: true });
110061
- await writeFile3(path2, text2, "utf8");
110402
+ await mkdir6(handoffNoteDir(projectId), { recursive: true });
110403
+ await rm6(path2, { force: true });
110404
+ await writeFile4(path2, text2, "utf8");
110062
110405
  return path2;
110063
110406
  }
110064
110407
  async function sweepHandoffNotes(owned, projectId) {
110065
110408
  let dirs;
110066
110409
  try {
110067
- dirs = projectId ? [projectId] : (await readdir7(root2(), { withFileTypes: true })).filter((d2) => d2.isDirectory()).map((d2) => d2.name);
110410
+ dirs = projectId ? [projectId] : (await readdir8(root2(), { withFileTypes: true })).filter((d2) => d2.isDirectory()).map((d2) => d2.name);
110068
110411
  } catch {
110069
110412
  return;
110070
110413
  }
@@ -110075,12 +110418,12 @@ async function sweepHandoffNotes(owned, projectId) {
110075
110418
  } catch {
110076
110419
  continue;
110077
110420
  }
110078
- const entries = await readdir7(dir, { withFileTypes: true }).catch(() => []);
110421
+ const entries = await readdir8(dir, { withFileTypes: true }).catch(() => []);
110079
110422
  for (const e of entries) {
110080
110423
  if (!e.isFile() || !e.name.endsWith(".md")) continue;
110081
110424
  const owner = e.name.slice(0, -".md".length);
110082
110425
  if (owned(owner)) continue;
110083
- await rm5(join27(dir, e.name), { force: true }).catch(() => {
110426
+ await rm6(join28(dir, e.name), { force: true }).catch(() => {
110084
110427
  });
110085
110428
  }
110086
110429
  }
@@ -110154,7 +110497,7 @@ var HUB_SENDER = "_hub-orchestrator";
110154
110497
  var builderKey = (ref) => `${ref.projectId ?? "_user"}/${ref.appId}`;
110155
110498
  var APP_BUILDERS_KEY = "apps.builders";
110156
110499
  var SessionManager = class {
110157
- constructor(store2, adapters2, emit, endpoint, worktreeRoot = join28(homedir12(), DATA_DIR, "worktrees"), keeperOpts = {}) {
110500
+ constructor(store2, adapters2, emit, endpoint, worktreeRoot = join29(homedir12(), DATA_DIR, "worktrees"), keeperOpts = {}) {
110158
110501
  this.store = store2;
110159
110502
  this.adapters = adapters2;
110160
110503
  this.emit = emit;
@@ -110167,10 +110510,17 @@ var SessionManager = class {
110167
110510
  rawEmit(withoutToolRecord(full));
110168
110511
  };
110169
110512
  const LIVE_ONLY = ["working", "waiting_approval"];
110170
- for (const s of store2.listSessions()) {
110513
+ const storedSessions = store2.listSessions();
110514
+ const storedProjects = new Map(storedSessions.map((s) => [s.id, s.projectId]));
110515
+ for (const s of storedSessions) {
110171
110516
  const stale = LIVE_ONLY.includes(s.state) && !keeperOpts.keptSessions?.has(s.id);
110172
110517
  const fixed = stale ? { ...s, state: "idle", waitingSince: null } : s;
110518
+ const ownerProject = fixed.askedBy ? storedProjects.get(fixed.askedBy) ?? store2.trashedSession(fixed.askedBy)?.record.projectId : void 0;
110519
+ const first = fixed.askedBy && ownerProject === void 0 ? store2.loadMessagesFrom(fixed.id, 0, 1)[0]?.payload : void 0;
110520
+ const oldAsk = fixed.askedBy && (ownerProject !== void 0 ? ownerProject !== fixed.projectId : first?.from?.sessionId === fixed.askedBy && first.text?.includes("through Centralu's ask_project"));
110521
+ if (oldAsk) delete fixed.askedBy;
110173
110522
  this.meta.set(s.id, fixed);
110523
+ if (oldAsk) store2.upsertSession(fixed);
110174
110524
  if (stale) {
110175
110525
  console.error(`[agent-host] stale state reset: ${s.id.slice(0, 8)} ${s.state} -> idle`);
110176
110526
  store2.upsertSession(fixed);
@@ -110311,6 +110661,8 @@ var SessionManager = class {
110311
110661
  * comment on `runAppAgent`).
110312
110662
  */
110313
110663
  agentRuns = /* @__PURE__ */ new Map();
110664
+ /** Temporary readers of a person's turn; they never own work or pin the calling session awake. */
110665
+ turnObservers = /* @__PURE__ */ new Map();
110314
110666
  /**
110315
110667
  * A card the host raises in a session's approval slot — requestId -> question: an app's capability question (M4
110316
110668
  * D-4), or one project reaching another (#371). It uses the same slot as the adapter's card (the session's
@@ -110319,8 +110671,8 @@ var SessionManager = class {
110319
110671
  */
110320
110672
  hostAsks = /* @__PURE__ */ new Map();
110321
110673
  /**
110322
- * ask_project's delegations still owed to their caller (#371 part B) — `<caller session>><target project>` -> the
110323
- * delegation. One per pair at a time: a second task while the first runs is refused, and a call with no task waits
110674
+ * ask_project's delegations still owed to their caller (#371 part B) — `<caller session>><target project>><target session>` -> the
110675
+ * delegation. One per chosen session at a time: a second task while the first runs is refused, and a call with no task waits
110324
110676
  * on this one. Removed once the caller has read the outcome; an outcome nobody comes back for goes with the caller.
110325
110677
  */
110326
110678
  delegations = /* @__PURE__ */ new Map();
@@ -110382,11 +110734,11 @@ var SessionManager = class {
110382
110734
  );
110383
110735
  }
110384
110736
  async addProject(path2) {
110385
- if (!existsSync18(path2) || !statSync12(path2).isDirectory()) {
110737
+ if (!existsSync19(path2) || !statSync12(path2).isDirectory()) {
110386
110738
  throw Object.assign(new Error(`Directory not found: ${path2}`), { code: "internal" });
110387
110739
  }
110388
110740
  const existing = this.store.findProjectByPath(path2);
110389
- const id2 = existing?.id ?? randomUUID9();
110741
+ const id2 = existing?.id ?? randomUUID10();
110390
110742
  this.store.addProject({ id: id2, path: path2, name: basename5(path2) });
110391
110743
  return this.projectInfo(id2, path2);
110392
110744
  }
@@ -110398,8 +110750,8 @@ var SessionManager = class {
110398
110750
  projectsHome = () => {
110399
110751
  throw Object.assign(new Error("This host has no default location for a new project \u2014 give a parent folder"), { code: "internal" });
110400
110752
  };
110401
- useProjectsHome(resolve11) {
110402
- this.projectsHome = resolve11;
110753
+ useProjectsHome(resolve12) {
110754
+ this.projectsHome = resolve12;
110403
110755
  }
110404
110756
  /**
110405
110757
  * The folder a new project would get, and whether a clone source is one this host clones (#515) — checked, nothing
@@ -110900,6 +111252,9 @@ var SessionManager = class {
110900
111252
  * null — the only place that can create a session with no project is `orchestrator()` below.
110901
111253
  */
110902
111254
  async createSession(params) {
111255
+ if (params.askedBy && this.meta.get(params.askedBy)?.projectId !== params.projectId) {
111256
+ throw new Error("A session cannot create a session in another project");
111257
+ }
110903
111258
  if (!params.tool) {
110904
111259
  throw Object.assign(new Error("No agent adapter is available. Install and configure an agent CLI before creating a session."), { code: "tool_not_installed" });
110905
111260
  }
@@ -110914,7 +111269,7 @@ var SessionManager = class {
110914
111269
  );
110915
111270
  }
110916
111271
  }
110917
- const id2 = randomUUID9();
111272
+ const id2 = randomUUID10();
110918
111273
  let worktree = null;
110919
111274
  if (params.worktree && params.projectId) {
110920
111275
  const summary = await gitSummary(params.cwd, this.gitTrust(params.projectId));
@@ -110927,7 +111282,7 @@ var SessionManager = class {
110927
111282
  );
110928
111283
  }
110929
111284
  const path2 = this.worktreePathFor(params.projectId, id2);
110930
- if (!this.projectTrusted(params.projectId)) {
111285
+ if (!this.projectTrusted(params.projectId) && !(params.askedBy && this.autoBoundaryOpen(params.askedBy, "worktree in an untrusted project"))) {
110931
111286
  throw Object.assign(
110932
111287
  new Error("Worktree sessions need a trusted project: checking out runs the repository's hooks and filters. Trust the project first."),
110933
111288
  { code: "internal" }
@@ -111278,14 +111633,14 @@ var SessionManager = class {
111278
111633
  }
111279
111634
  /** Tool calls the store holds for this session with no result after them (the newest 200 rows) */
111280
111635
  openToolCalls(sessionId) {
111281
- const open5 = /* @__PURE__ */ new Map();
111636
+ const open6 = /* @__PURE__ */ new Map();
111282
111637
  for (const r of this.store.loadMessages(sessionId, 200)) {
111283
111638
  const p = r.payload;
111284
111639
  if (typeof p?.callId !== "string") continue;
111285
- if (r.kind === "tool_call" && typeof p.summary?.tool === "string") open5.set(p.callId, p.summary.tool);
111286
- else if (r.kind === "tool_result") open5.delete(p.callId);
111640
+ if (r.kind === "tool_call" && typeof p.summary?.tool === "string") open6.set(p.callId, p.summary.tool);
111641
+ else if (r.kind === "tool_result") open6.delete(p.callId);
111287
111642
  }
111288
- return [...open5].map(([callId, tool]) => ({ callId, tool }));
111643
+ return [...open6].map(([callId, tool]) => ({ callId, tool }));
111289
111644
  }
111290
111645
  /**
111291
111646
  * Re-attaches the tool processes a previous host left running in the keeper (#280 step 2), so
@@ -111318,6 +111673,7 @@ var SessionManager = class {
111318
111673
  );
111319
111674
  }
111320
111675
  async doResumeSession(sessionId, adopt) {
111676
+ if (this.worktreeCleanup.has(sessionId)) throw Object.assign(new Error("The session worktree is being removed"), { code: "internal" });
111321
111677
  const m = this.meta.get(sessionId);
111322
111678
  if (!m) throw Object.assign(new Error(`Session not found: ${sessionId}`), { code: "session_not_found" });
111323
111679
  if (this.handles.has(sessionId)) return { session: m, resumed: true };
@@ -111506,6 +111862,7 @@ var SessionManager = class {
111506
111862
  * exactly as it was, and only what happens from here on piles up in the copy.
111507
111863
  */
111508
111864
  async forkConversation(sessionId) {
111865
+ if (this.worktreeCleanup.has(sessionId)) throw Object.assign(new Error("The session worktree is being removed"), { code: "internal" });
111509
111866
  const m = this.meta.get(sessionId);
111510
111867
  if (!m) throw Object.assign(new Error(`Session not found: ${sessionId}`), { code: "session_not_found" });
111511
111868
  const source = m.externalId ?? m.importedFrom;
@@ -111538,6 +111895,7 @@ var SessionManager = class {
111538
111895
  this.leaving.delete(sessionId);
111539
111896
  }
111540
111897
  }
111898
+ trashing = /* @__PURE__ */ new Map();
111541
111899
  /**
111542
111900
  * Moves a session to the trash (#204) — what deleting a session means now. Nothing is destroyed: the process
111543
111901
  * stops, and the session leaves every list (sidebar, inbox, palette, grid, search, the agents' session tools and
@@ -111556,7 +111914,19 @@ var SessionManager = class {
111556
111914
  * delete knows that layout (the Claude SDK's `deleteSession`). Left in place it is also a second way back — the
111557
111915
  * tool, and **+ → Past conversations**, still have it.
111558
111916
  */
111559
- async trashSession(sessionId, removeWorktree = false, removeExternal = false) {
111917
+ async trashSession(sessionId, removeWorktree = false, removeExternal = false, recovery) {
111918
+ const pending = this.trashing.get(sessionId);
111919
+ if (pending) return pending;
111920
+ const deleting = this.trashSessionOnce(sessionId, removeWorktree, removeExternal, recovery);
111921
+ this.trashing.set(sessionId, deleting);
111922
+ try {
111923
+ await deleting;
111924
+ } finally {
111925
+ this.trashing.delete(sessionId);
111926
+ }
111927
+ }
111928
+ async trashSessionOnce(sessionId, removeWorktree, removeExternal, recovery) {
111929
+ if (this.worktreeCleanup.has(sessionId) && !recovery) throw Object.assign(new Error("The session worktree is being removed"), { code: "internal" });
111560
111930
  const m = this.meta.get(sessionId);
111561
111931
  if (!m) {
111562
111932
  this.emit({ type: "session_deleted", sessionId });
@@ -111572,6 +111942,7 @@ var SessionManager = class {
111572
111942
  agentRun.deleted = true;
111573
111943
  agentRun.fail(new Error("the person deleted the agent session before it answered"));
111574
111944
  }
111945
+ for (const observer of this.turnObservers.get(sessionId) ?? []) observer.fail(new Error("the person deleted the session before it answered"));
111575
111946
  for (const [key, d2] of this.delegations) {
111576
111947
  if (d2.callerId !== sessionId) continue;
111577
111948
  this.delegations.delete(key);
@@ -111605,7 +111976,8 @@ var SessionManager = class {
111605
111976
  projectName: project2?.name ?? null,
111606
111977
  projectPath: project2?.path ?? null,
111607
111978
  removeExternal: removeExternal && !!externalId,
111608
- removeWorktree: removeWorktree && !!m.worktree
111979
+ removeWorktree: removeWorktree && !!m.worktree,
111980
+ ...recovery ? { recovery } : {}
111609
111981
  });
111610
111982
  this.appsHub?.sessionGone(sessionId);
111611
111983
  this.emit({ type: "session_deleted", sessionId });
@@ -111613,7 +111985,7 @@ var SessionManager = class {
111613
111985
  /** Only one trash operation per session at a time — a purge racing a restore would restore half of a deleted session */
111614
111986
  trashBusy = /* @__PURE__ */ new Set();
111615
111987
  async trashOp(sessionId, run) {
111616
- if (this.trashBusy.has(sessionId)) {
111988
+ if (this.trashBusy.has(sessionId) || this.worktreeCleanup.has(sessionId)) {
111617
111989
  throw Object.assign(new Error("That session is already being restored or deleted"), { code: "internal" });
111618
111990
  }
111619
111991
  this.trashBusy.add(sessionId);
@@ -111630,7 +112002,7 @@ var SessionManager = class {
111630
112002
  this.store.listTrash().map(async (r) => {
111631
112003
  const { record: record2 } = r;
111632
112004
  const live = record2.projectId ? projects.get(record2.projectId) : void 0;
111633
- const files = await attachmentBytes(r.id).catch(() => 0) + (record2.projectId ? await handoffNoteBytes(record2.projectId, r.id).catch(() => 0) : 0);
112005
+ const files = await attachmentBytes(r.id).catch(() => 0) + await worktreeRecoveryBytes(r.id).catch(() => 0) + (record2.projectId ? await handoffNoteBytes(record2.projectId, r.id).catch(() => 0) : 0);
111634
112006
  return {
111635
112007
  id: r.id,
111636
112008
  name: r.name,
@@ -111646,7 +112018,7 @@ var SessionManager = class {
111646
112018
  bytes: r.bytes + files,
111647
112019
  conversationFile: !r.hasConversationFile ? "none" : record2.removeExternal ? "remove" : "keep",
111648
112020
  // Only a worktree still on disk is listed — the merged-worktree cleanup removes the folder before the trash is emptied
111649
- worktree: r.worktree && existsSync18(r.worktree.path) ? { ...r.worktree, remove: record2.removeWorktree } : null
112021
+ worktree: r.worktree && existsSync19(r.worktree.path) ? { ...r.worktree, remove: record2.removeWorktree } : null
111650
112022
  };
111651
112023
  })
111652
112024
  );
@@ -111684,7 +112056,7 @@ var SessionManager = class {
111684
112056
  const home = projects.find((p) => p.id === record2.projectId) ?? (record2.projectPath ? projects.find((p) => p.path === record2.projectPath) : void 0);
111685
112057
  if (home) {
111686
112058
  projectId = home.id;
111687
- } else if (record2.projectPath && existsSync18(record2.projectPath) && statSync12(record2.projectPath).isDirectory()) {
112059
+ } else if (record2.projectPath && existsSync19(record2.projectPath) && statSync12(record2.projectPath).isDirectory()) {
111688
112060
  this.store.addProject({
111689
112061
  id: record2.projectId,
111690
112062
  path: record2.projectPath,
@@ -111703,14 +112075,21 @@ var SessionManager = class {
111703
112075
  } else if (t.session.kind === "orchestrator" && this.store.orchestratorId()) {
111704
112076
  throw Object.assign(new Error("Another orchestrator is in place \u2014 the app runs one at a time"), { code: "internal" });
111705
112077
  }
112078
+ let recovered;
112079
+ if (record2.recovery && t.session.worktree && projectId) {
112080
+ const repo = this.store.listProjects().find((p) => p.id === projectId).path;
112081
+ recovered = await restoreWorktreeRecovery(sessionId, repo, t.session.worktree.path, t.session.worktree.branch, record2.recovery, this.gitTrust(projectId));
112082
+ }
111706
112083
  const back = await this.store.restoreSession(sessionId, projectId);
111707
112084
  if (!back) throw Object.assign(new Error(`Not in the trash: ${sessionId}`), { code: "session_not_found" });
111708
112085
  const LIVE_ONLY = ["working", "waiting_approval"];
111709
112086
  const fixed = LIVE_ONLY.includes(back.state) ? { ...back, state: "idle", waitingSince: null } : back;
111710
- if (fixed !== back) this.store.upsertSession(fixed);
112087
+ if (recovered && fixed.worktree) fixed.worktree = { ...fixed.worktree, branch: recovered.branch };
112088
+ if (fixed !== back || recovered) this.store.upsertSession(fixed);
111711
112089
  this.meta.set(sessionId, fixed);
111712
112090
  const session2 = { ...this.meta.get(sessionId), live: false };
111713
112091
  this.emit({ type: "session_created", sessionId, session: session2 });
112092
+ if (recovered) this.onEvent({ type: "notice", sessionId, level: "info", from: "Centralu", label: "worktree recovery", text: recovered.notice });
111714
112093
  return { session: session2, project: registered };
111715
112094
  });
111716
112095
  }
@@ -111749,6 +112128,7 @@ var SessionManager = class {
111749
112128
  await gitWorktreeRemove(record2.projectPath, s.worktree.path, true).catch(() => {
111750
112129
  });
111751
112130
  }
112131
+ await clearWorktreeRecovery(sessionId);
111752
112132
  await this.store.purgeSession(sessionId);
111753
112133
  await clearAttachments(sessionId).catch(() => {
111754
112134
  });
@@ -111821,6 +112201,7 @@ var SessionManager = class {
111821
112201
  return live && m && live.model !== m.model ? "model_change" : void 0;
111822
112202
  }
111823
112203
  async updateSettings(sessionId, s) {
112204
+ if (this.worktreeCleanup.has(sessionId)) throw Object.assign(new Error("The session worktree is being removed"), { code: "internal" });
111824
112205
  const m = this.meta.get(sessionId);
111825
112206
  if (!m) throw Object.assign(new Error(`Session not found: ${sessionId}`), { code: "session_not_found" });
111826
112207
  if (s.model !== void 0 && s.model !== m.model) this.cacheLedger.note(sessionId, "model_change");
@@ -111937,6 +112318,7 @@ var SessionManager = class {
111937
112318
  }
111938
112319
  const run = e.sessionId ? this.agentRuns.get(e.sessionId) : void 0;
111939
112320
  run?.onEvent(e);
112321
+ if (e.sessionId) for (const observer of this.turnObservers.get(e.sessionId) ?? []) observer.onEvent(e);
111940
112322
  this.emit(seq != null ? { ...e, seq } : e);
111941
112323
  if (endedTurn && e.sessionId && !run?.tracksInputs) void this.reportBackIfAwaited(e.sessionId, e, run?.completedAnswer);
111942
112324
  if (e.type === "approval_resolved" && e.sessionId) this.raiseHostAsks(e.sessionId);
@@ -112318,6 +112700,7 @@ If you need more, you can read that session's recent conversation with read_sess
112318
112700
  async deliver(sessionId, text2, attachments, from, relayed, fromApp, fromAppVia = "inline", sendInput) {
112319
112701
  const restarting = this.deferredRestarts.get(sessionId);
112320
112702
  if (restarting) await restarting;
112703
+ if (this.worktreeCleanup.has(sessionId)) throw Object.assign(new Error("The session worktree is being removed"), { code: "internal" });
112321
112704
  const m = this.meta.get(sessionId);
112322
112705
  if (!m) throw Object.assign(new Error(`Session not found: ${sessionId}`), { code: "session_not_found" });
112323
112706
  if (!this.handles.has(sessionId)) {
@@ -112328,6 +112711,7 @@ If you need more, you can read that session's recent conversation with read_sess
112328
112711
  });
112329
112712
  }
112330
112713
  }
112714
+ if (this.worktreeCleanup.has(sessionId)) throw Object.assign(new Error("The session worktree is being removed"), { code: "internal" });
112331
112715
  const h = this.requireHandle(sessionId);
112332
112716
  this.closeStream(sessionId);
112333
112717
  const seq = this.store.nextSeq(sessionId);
@@ -112382,6 +112766,7 @@ ${attachments.map((a) => `@${a.path}`).join("\n")}` : text2;
112382
112766
  * untouched, and only the tool underneath it is swapped.
112383
112767
  */
112384
112768
  async switchTool(sessionId, tool) {
112769
+ if (this.worktreeCleanup.has(sessionId)) throw Object.assign(new Error("The session worktree is being removed"), { code: "internal" });
112385
112770
  const m = this.meta.get(sessionId);
112386
112771
  if (!m) throw Object.assign(new Error(`Session not found: ${sessionId}`), { code: "session_not_found" });
112387
112772
  if (m.tool === tool) return { ...m, live: this.handles.has(sessionId) };
@@ -112663,7 +113048,7 @@ ${attachments.map((a) => `@${a.path}`).join("\n")}` : text2;
112663
113048
  worktreePathFor(projectId, sessionId) {
112664
113049
  if (!isProjectId(projectId)) throw Object.assign(new Error(`Not a project id: ${projectId}`), { code: "internal" });
112665
113050
  if (!isSessionId(sessionId)) throw Object.assign(new Error(`Not a session id: ${sessionId}`), { code: "internal" });
112666
- return join28(this.worktreeRoot, projectId, sessionId);
113051
+ return join29(this.worktreeRoot, projectId, sessionId);
112667
113052
  }
112668
113053
  /**
112669
113054
  * Sets up a new worktree's workspace (#69): copy configured files, reuse JS dependencies, run setup.
@@ -112846,7 +113231,7 @@ ${String(stderr).slice(0, 2e3)}`
112846
113231
  const base2 = this.cwdFor(m);
112847
113232
  const paths = /* @__PURE__ */ new Set();
112848
113233
  for (const p of e.paths) {
112849
- const rel = relative8(root3, resolve10(base2, p));
113234
+ const rel = relative8(root3, resolve11(base2, p));
112850
113235
  if (!rel || rel === ".." || rel.startsWith(`..${sep11}`) || isAbsolute9(rel)) continue;
112851
113236
  paths.add(rel.replaceAll(sep11, "/"));
112852
113237
  }
@@ -113046,6 +113431,7 @@ ${String(stderr).slice(0, 2e3)}`
113046
113431
  * reports background work.
113047
113432
  */
113048
113433
  sessionIdle(sessionId) {
113434
+ if (this.worktreeCleanup.has(sessionId)) return { idle: false, reason: "turn" };
113049
113435
  const m = this.meta.get(sessionId);
113050
113436
  if (!m) throw Object.assign(new Error(`Session not found: ${sessionId}`), { code: "session_not_found" });
113051
113437
  if (this.agentRuns.get(sessionId)?.waitingForInputs) return { idle: false, reason: "turn" };
@@ -113056,7 +113442,7 @@ ${String(stderr).slice(0, 2e3)}`
113056
113442
  }
113057
113443
  /**
113058
113444
  * The sessions whose running work this one waits on (#568, the sleep rule in sleep.ts): a child it started with
113059
- * `start_agent`, or a session ask_project opened for it, while the turn it gave them runs, and a session that owes
113445
+ * `start_agent`, or a session handling its ask_project task, while the turn it gave them runs, and a session that owes
113060
113446
  * it a report (`reportBack`, `awaitingReport`). Their answer is about to come back to it.
113061
113447
  */
113062
113448
  waitsOn(sessionId) {
@@ -113099,9 +113485,10 @@ ${String(stderr).slice(0, 2e3)}`
113099
113485
  const handle = this.requireHandle(sessionId);
113100
113486
  const run = this.agentRuns.get(sessionId);
113101
113487
  if (run) run.stoppedByPerson = true;
113488
+ for (const observer of this.turnObservers.get(sessionId) ?? []) observer.stoppedByPerson = true;
113102
113489
  handle.interrupt();
113103
113490
  for (const d2 of this.delegations.values()) {
113104
- if (d2.callerId === sessionId && (!d2.agent || (d2.waiters ?? 0) > 0)) d2.stop("the person stopped the session that asked");
113491
+ if (d2.callerId === sessionId && (d2.waiters ?? 0) > 0) d2.stop("the person stopped the session that asked");
113105
113492
  }
113106
113493
  }
113107
113494
  /**
@@ -113110,6 +113497,7 @@ ${String(stderr).slice(0, 2e3)}`
113110
113497
  * process.
113111
113498
  */
113112
113499
  async restartSession(sessionId, cause2) {
113500
+ if (this.worktreeCleanup.has(sessionId)) throw Object.assign(new Error("The session worktree is being removed"), { code: "internal" });
113113
113501
  if (cause2) {
113114
113502
  this.restartReasons.set(sessionId, cause2);
113115
113503
  if (cause2 === "launch_settings") console.error(`[sessions] restarting ${sessionId}: launch settings changed or previously unrecorded`);
@@ -113717,10 +114105,11 @@ ${String(stderr).slice(0, 2e3)}`
113717
114105
  kind: "project_access",
113718
114106
  access: access2,
113719
114107
  from: { id: from.id, name: from.name },
113720
- // Said on the card (#382): a session opened there follows its trust, so the person should know before allowing
114108
+ // Said on the card (#382): the selected session there follows its trust, so the person should know before allowing
113721
114109
  to: { id: to.id, name: to.name, trusted: to.trusted },
113722
114110
  text: what.text,
113723
- ...what.app ? { app: what.app } : {}
114111
+ ...what.app ? { app: what.app } : {},
114112
+ ...what.session ? { session: what.session } : {}
113724
114113
  },
113725
114114
  signal
113726
114115
  );
@@ -113735,20 +114124,26 @@ ${String(stderr).slice(0, 2e3)}`
113735
114124
  }
113736
114125
  return { ok: false, error: `The question to the person was withdrawn before they answered, so ${to.name} was not reached.` };
113737
114126
  }
113738
- /**
113739
- * Raises a card of the host's own in a session's approval slot and waits for the person's answer (#371, #515):
113740
- * null when `signal` fires first, the card then taken down so none is left unanswerable.
113741
- */
114127
+ /** The caller's own preset, read at the boundary, never its project's or parent's preset. */
114128
+ autoBoundaryOpen(sessionId, boundary) {
114129
+ if (this.meta.get(sessionId)?.permissionPreset !== "auto") return false;
114130
+ if (boundary) console.error(`[permissions] auto session ${sessionId}: ${boundary} allowed`);
114131
+ return true;
114132
+ }
114133
+ /** Auto allows and logs; other presets raise a host card, withdrawn if the signal fires. */
113742
114134
  askHost(sessionId, detail, signal) {
113743
- return new Promise((resolve11) => {
113744
- if (signal?.aborted) return resolve11(null);
113745
- const requestId = `${detail.kind === "project_access" ? "xp" : detail.kind === "agent_start" ? "ag" : detail.kind === "centralu_update" ? "up" : "mk"}-${randomUUID9()}`;
113746
- this.hostAsks.set(requestId, { requestId, sessionId, detail, shown: false, resolve: resolve11 });
114135
+ if (signal?.aborted) return Promise.resolve(null);
114136
+ const note = detail.kind === "project_access" && !detail.to.trusted ? " (target not trusted)" : "";
114137
+ if (this.autoBoundaryOpen(sessionId, `${detail.kind}${note}`)) return Promise.resolve("allow");
114138
+ return new Promise((resolve12) => {
114139
+ if (signal?.aborted) return resolve12(null);
114140
+ const requestId = `${detail.kind === "project_access" ? "xp" : detail.kind === "agent_start" ? "ag" : detail.kind === "centralu_update" ? "up" : "mk"}-${randomUUID10()}`;
114141
+ this.hostAsks.set(requestId, { requestId, sessionId, detail, shown: false, resolve: resolve12 });
113747
114142
  signal?.addEventListener("abort", () => {
113748
114143
  if (!this.hostAsks.delete(requestId)) return;
113749
114144
  const m = this.meta.get(sessionId);
113750
114145
  if (m?.pendingApproval?.requestId === requestId) this.onEvent({ type: "approval_resolved", sessionId, requestId, decision: "deny" });
113751
- resolve11(null);
114146
+ resolve12(null);
113752
114147
  }, { once: true });
113753
114148
  this.raiseHostAsks(sessionId);
113754
114149
  });
@@ -113780,25 +114175,24 @@ ${String(stderr).slice(0, 2e3)}`
113780
114175
  * and a session that was itself asked by another project cannot ask a third: depth one, so two projects
113781
114176
  * that consented to each other cannot bounce a task between them without a person.
113782
114177
  * 2. The person's consent for the pair (`ensureProjectAccess`, kind 'delegate').
113783
- * 3. A session in the target: the idle one this caller asked there before, else a new one with that project's
113784
- * folder, instructions and default tool, under the `normal` preset — what a session the person opens there
113785
- * gets, so its own approvals stand as cards in that session, where the person sees them. Marked `askedBy`.
114178
+ * 3. An existing ordinary session in the target, named by name or id; no creation or ownership change.
114179
+ * Without a name exactly one is selected, otherwise its sessions are listed for the caller to choose.
113786
114180
  * 4. The task goes in a frame (`askFrame`) marked as sent by the caller, and the call waits for the turn, up to
113787
114181
  * `ASK_WAIT_MS`. Past it the answer is "still working" and a call with no task waits again.
113788
114182
  * 5. The final answer comes back cut to size, and the paths it names inside the target project become readable
113789
114183
  * to the caller (`readableGrants`).
113790
114184
  *
113791
114185
  * Stop on the caller, the call's own signal, or the person deleting either session ends the wait; the first two
113792
- * stop the delegated turn as well.
114186
+ * stop an assigned idle target only while waiting; a task that joined the person's turn only ends the wait.
113793
114187
  */
113794
114188
  async askProject(callerId, opts, signal) {
113795
114189
  const caller = this.meta.get(callerId);
113796
114190
  if (!caller?.projectId) return { ok: false, error: "This session belongs to no project, so it cannot ask another one" };
113797
- if (caller.askedBy) {
114191
+ if ((caller.askedBy || [...this.delegations.values()].some((d8) => d8.sessionId === callerId && !d8.agent && !d8.settled)) && !this.autoBoundaryOpen(callerId, "delegation depth")) {
113798
114192
  return { ok: false, error: "This session was itself started or asked by another session, so it cannot ask a project \u2014 say in your answer what is needed instead." };
113799
114193
  }
113800
114194
  const projects = this.store.listProjects();
113801
- if (projects.find((p) => p.id === caller.projectId)?.trusted !== true) {
114195
+ if (projects.find((p) => p.id === caller.projectId)?.trusted !== true && !this.autoBoundaryOpen(callerId, "delegate from an untrusted project")) {
113802
114196
  return { ok: false, error: "This session's project is not trusted, so it cannot ask another project \u2014 the person can trust it from the project menu." };
113803
114197
  }
113804
114198
  const wanted = opts.project.trim();
@@ -113811,62 +114205,88 @@ ${String(stderr).slice(0, 2e3)}`
113811
114205
  };
113812
114206
  }
113813
114207
  if (target.id === caller.projectId) return { ok: false, error: `${target.name} is this session's own project \u2014 do the work here.` };
113814
- const key = `${callerId}>${target.id}`;
114208
+ const candidates = [...this.meta.values()].filter(
114209
+ (s) => s.projectId === target.id && s.kind === "worker" && !s.appId && !this.builderRefOf(s)
114210
+ );
114211
+ const list = () => candidates.map((s) => JSON.stringify({
114212
+ id: s.id,
114213
+ name: s.name,
114214
+ state: s.state,
114215
+ lastActive: lastActiveOf(this.store, s.id) ?? null
114216
+ })).join("\n");
114217
+ const wantedSession = opts.session?.trim();
114218
+ const outstanding = [...this.delegations.entries()].filter(
114219
+ ([, d8]) => d8.callerId === callerId && d8.target.id === target.id && !d8.agent
114220
+ );
114221
+ if (!opts.task && !wantedSession && outstanding.length === 1) {
114222
+ const [key2, pending2] = outstanding[0];
114223
+ return this.awaitDelegation(key2, pending2, signal);
114224
+ }
114225
+ const named = wantedSession ? candidates.filter((s) => s.name.toLowerCase() === wantedSession.toLowerCase()) : candidates;
114226
+ const chosen = candidates.find((s) => s.id === wantedSession) ?? (named.length === 1 ? named[0] : void 0);
114227
+ if (!chosen) {
114228
+ return { ok: false, error: candidates.length ? `${wantedSession ? `No unique ordinary session named "${wantedSession}" in ${target.name}. ` : ""}Name a session with the session argument. Sessions (name, state, last active):
114229
+ ${list()}` : `${target.name} has no ordinary session. The person has to open one there; this call cannot create one.` };
114230
+ }
114231
+ const key = `${callerId}>${target.id}>${chosen.id}`;
113815
114232
  const pending = this.delegations.get(key);
113816
114233
  if (pending && !opts.task) return this.awaitDelegation(key, pending, signal);
113817
114234
  if (pending && !pending.settled) {
113818
- return {
113819
- ok: false,
113820
- error: `${target.name} is still working on your last task in the session "${this.meta.get(pending.sessionId)?.name ?? pending.sessionId}". Call ask_project with project "${target.name}" and no task to wait for it.`
113821
- };
113822
- }
113823
- if (pending) this.delegations.delete(key);
113824
- if (!opts.task) return { ok: false, error: `Give a task: what ${target.name} should do.` };
113825
- const consent = await this.ensureProjectAccess(callerId, target.id, "delegate", { text: taskLine(opts.task) }, signal);
114235
+ return { ok: false, error: `${target.name} is still working on your last task in the session "${chosen.name}". Call ask_project with project "${target.name}", session "${chosen.id}" and no task to wait for it.` };
114236
+ }
114237
+ if (!opts.task) return { ok: false, error: `Give a task: what ${target.name} / ${chosen.name} should do.` };
114238
+ const available = () => {
114239
+ const current = this.meta.get(chosen.id);
114240
+ if (!current || current.projectId !== target.id || current.kind !== "worker" || current.appId || this.builderRefOf(current)) return "The chosen session is no longer available";
114241
+ if (this.agentRuns.has(chosen.id) && !this.agentRuns.get(chosen.id).finished) return `"${current.name}" is busy (${current.state}) with another delegated task. Wait for it.`;
114242
+ if (inTurn(current.state) && (!this.adapters.get(current.tool)?.capabilities.midTurnInput || !this.handles.get(current.id)?.sendTracked)) return `"${current.name}" is busy (${current.state}); its CLI cannot take a mid-turn note. Try again later.`;
114243
+ };
114244
+ const busy = available();
114245
+ if (busy) return { ok: false, error: busy };
114246
+ const consent = await this.ensureProjectAccess(callerId, target.id, "delegate", {
114247
+ text: taskLine(opts.task),
114248
+ session: { id: chosen.id, name: chosen.name }
114249
+ }, signal);
113826
114250
  if (!consent.ok) return consent;
113827
- const reuse = [...this.meta.values()].filter((s) => s.askedBy === callerId && s.projectId === target.id && s.kind === "worker" && (s.state === "idle" || s.state === "waiting_input") && !this.agentRuns.has(s.id)).sort((a, b2) => b2.createdAt - a.createdAt)[0];
113828
- let delegatedId;
113829
- if (reuse) {
113830
- delegatedId = reuse.id;
113831
- } else {
113832
- const tool = this.firstTool(target.defaultTool);
113833
- if (!tool) return { ok: false, error: `Could not open a session in ${target.name}: no agent adapter is available. Install and configure an agent CLI first.` };
113834
- const adapter = this.adapters.get(tool);
113835
- const found = await adapter.detect();
113836
- if (!found.installed || !found.loggedIn) {
113837
- return { ok: false, error: `${target.name}'s agent (${adapter.descriptor.label}) cannot take the task: ${found.detail}` };
113838
- }
113839
- try {
113840
- const remembered = this.store.projectToolDefaults(target.id)[tool];
113841
- const info = await this.createSession({
113842
- projectId: target.id,
113843
- cwd: target.path,
113844
- tool,
113845
- ...remembered?.model ? { model: remembered.model } : {},
113846
- ...remembered?.effort ? { effort: remembered.effort } : {},
113847
- permissionPreset: "normal",
113848
- askedBy: callerId
113849
- });
113850
- delegatedId = info.id;
113851
- } catch (e) {
113852
- return { ok: false, error: `Could not open a session in ${target.name}: ${e.message}` };
113853
- }
113854
- const callerProject2 = projects.find((p) => p.id === caller.projectId)?.name ?? "another project";
113855
- this.rename(delegatedId, `Asked by ${callerProject2} \xB7 ${(/* @__PURE__ */ new Date()).toTimeString().slice(0, 5)}`);
113856
- }
113857
- const d2 = this.startDelegation(key, callerId, delegatedId, target);
114251
+ const changed = available();
114252
+ if (changed) return { ok: false, error: changed };
114253
+ if (signal?.aborted) return { ok: false, error: "The call was cancelled before delivery" };
114254
+ if (pending) this.delegations.delete(key);
114255
+ const delegatedId = chosen.id;
114256
+ const midTurn = inTurn(this.meta.get(delegatedId).state);
114257
+ let releaseInput;
114258
+ const d2 = this.startDelegation(key, callerId, delegatedId, target, false, midTurn);
113858
114259
  const callerProject = projects.find((p) => p.id === caller.projectId)?.name ?? "another project";
113859
114260
  try {
113860
- await this.deliver(delegatedId, askFrame(callerProject, opts.task), void 0, { sessionId: callerId, name: labelOf(this.store, caller) }, false);
114261
+ const from = { sessionId: callerId, name: labelOf(this.store, caller) };
114262
+ if (midTurn) {
114263
+ const releaseHold = d2.wait.holdInput();
114264
+ const release = () => {
114265
+ releaseHold();
114266
+ this.applyDeferredRestart(delegatedId);
114267
+ };
114268
+ releaseInput = release;
114269
+ await this.deliver(delegatedId, askFrame(callerProject, opts.task), void 0, from, false, void 0, "inline", (h, input) => {
114270
+ const fail4 = (e) => {
114271
+ d2.wait.fail(e);
114272
+ release();
114273
+ };
114274
+ void h.sendTracked(input, { complete: release, fail: fail4 }).then(release, fail4);
114275
+ });
114276
+ } else {
114277
+ await this.deliver(delegatedId, askFrame(callerProject, opts.task), void 0, from, false);
114278
+ }
113861
114279
  } catch (e) {
113862
- this.delegations.delete(key);
114280
+ d2.wait.fail(e);
114281
+ releaseInput?.();
114282
+ if (this.delegations.get(key) === d2) this.delegations.delete(key);
113863
114283
  if (this.agentRuns.get(delegatedId) === d2.wait) this.agentRuns.delete(delegatedId);
113864
114284
  return { ok: false, error: `Could not hand the task to ${target.name}: ${e.message}` };
113865
114285
  }
113866
114286
  return this.awaitDelegation(key, d2, signal);
113867
114287
  }
113868
114288
  /** Registers the wait on the delegated session's turn (the same watcher an app's agent uses, `AgentRunWait`) */
113869
- startDelegation(key, callerId, sessionId, target, agent = false) {
114289
+ startDelegation(key, callerId, sessionId, target, agent = false, joinedTurn = false) {
113870
114290
  let notice;
113871
114291
  let answerAfter = this.meta.get(sessionId)?.lastSeq ?? 0;
113872
114292
  const wait = new AgentRunWait((message) => notice = message, () => this.meta.get(sessionId)?.name ?? "the session", void 0, () => {
@@ -113874,7 +114294,13 @@ ${String(stderr).slice(0, 2e3)}`
113874
114294
  answerAfter = this.meta.get(sessionId)?.lastSeq ?? answerAfter;
113875
114295
  return answer;
113876
114296
  });
114297
+ let cancelWait;
114298
+ const waitCancelled = new Promise((resolve12) => {
114299
+ cancelWait = resolve12;
114300
+ });
113877
114301
  const d2 = {
114302
+ joinedTurn,
114303
+ waitCancelled,
113878
114304
  callerId,
113879
114305
  sessionId,
113880
114306
  target,
@@ -113887,8 +114313,12 @@ ${String(stderr).slice(0, 2e3)}`
113887
114313
  (r) => ({ ok: true, answer: r.answer }),
113888
114314
  (e) => ({ ok: false, error: e.message })
113889
114315
  ),
113890
- stop: (why) => {
114316
+ stop: (why, targetStop = false) => {
113891
114317
  if (d2.settled) return;
114318
+ if (joinedTurn && !targetStop) {
114319
+ cancelWait({ ok: false, error: `${why}; the caller stopped waiting and the target session was left running` });
114320
+ return;
114321
+ }
113892
114322
  wait.fail(new Error(`${why}, so the delegated turn was stopped`));
113893
114323
  try {
113894
114324
  this.handles.get(sessionId)?.interrupt();
@@ -113899,31 +114329,79 @@ ${String(stderr).slice(0, 2e3)}`
113899
114329
  };
113900
114330
  void d2.outcome.then((r) => {
113901
114331
  if (this.agentRuns.get(sessionId) === wait) this.agentRuns.delete(sessionId);
113902
- if (agent) void this.reportBackIfAwaited(sessionId, r.ok ? { type: "turn_complete", sessionId } : { type: "error", sessionId, error: { code: "internal", message: r.error, retryable: false } }, r.ok ? r.answer : void 0);
114332
+ if (agent || wait.tracksInputs) void this.reportBackIfAwaited(sessionId, r.ok ? { type: "turn_complete", sessionId } : { type: "error", sessionId, error: { code: "internal", message: r.error, retryable: false } }, r.ok ? r.answer : void 0);
113903
114333
  });
113904
114334
  this.agentRuns.set(sessionId, wait);
113905
114335
  this.delegations.set(key, d2);
113906
114336
  return d2;
113907
114337
  }
114338
+ /** Read an existing turn without claiming it in delegations, agentRuns or the caller's sleep graph. */
114339
+ observeTurn(child, subscribe) {
114340
+ const active = this.agentRuns.get(child.id);
114341
+ const shared = active && !active.finished ? active : void 0;
114342
+ const wait = shared ?? new AgentRunWait(
114343
+ () => {
114344
+ },
114345
+ () => child.name,
114346
+ void 0,
114347
+ () => finalAnswer(this.store.loadMessages(child.id, 50))
114348
+ );
114349
+ const project2 = this.store.listProjects().find((p) => p.id === child.projectId);
114350
+ const target = { id: child.projectId ?? "", name: project2?.name ?? "", path: this.store.sessionCwd(child.id) ?? project2?.path ?? "" };
114351
+ if (!shared && subscribe) {
114352
+ const readers = this.turnObservers.get(child.id) ?? /* @__PURE__ */ new Set();
114353
+ readers.add(wait);
114354
+ this.turnObservers.set(child.id, readers);
114355
+ }
114356
+ return {
114357
+ run: {
114358
+ // This is the target's own turn, never work owned by its observer.
114359
+ callerId: child.id,
114360
+ sessionId: child.id,
114361
+ target,
114362
+ wait,
114363
+ get settled() {
114364
+ return wait.finished;
114365
+ },
114366
+ notice: () => void 0,
114367
+ outcome: wait.done.then(
114368
+ (r) => ({ ok: true, answer: r.answer }),
114369
+ (e) => ({ ok: false, error: e.message })
114370
+ ),
114371
+ stop: (why) => {
114372
+ if (wait.finished) return;
114373
+ wait.fail(new Error(`${why}, so the turn was stopped`));
114374
+ this.handles.get(child.id)?.interrupt();
114375
+ }
114376
+ },
114377
+ release: () => {
114378
+ if (shared) return;
114379
+ const readers = this.turnObservers.get(child.id);
114380
+ readers?.delete(wait);
114381
+ if (readers?.size === 0) this.turnObservers.delete(child.id);
114382
+ }
114383
+ };
114384
+ }
113908
114385
  /** Waits for a delegation up to the bound, and turns its outcome into what the caller reads */
113909
114386
  async awaitDelegation(key, d2, signal) {
113910
114387
  const sessionName = () => this.meta.get(d2.sessionId)?.name ?? d2.sessionId;
113911
114388
  let timer;
113912
114389
  let onAbort;
113913
- const bound = new Promise((resolve11) => timer = setTimeout(() => resolve11("bound"), ASK_WAIT_MS));
113914
- const aborted2 = new Promise((resolve11) => {
114390
+ const bound = new Promise((resolve12) => timer = setTimeout(() => resolve12("bound"), ASK_WAIT_MS));
114391
+ const aborted2 = new Promise((resolve12) => {
113915
114392
  if (!signal) return;
113916
- onAbort = () => resolve11("aborted");
114393
+ onAbort = () => resolve12("aborted");
113917
114394
  if (signal.aborted) onAbort();
113918
114395
  else signal.addEventListener("abort", onAbort, { once: true });
113919
114396
  });
114397
+ d2.waiters = (d2.waiters ?? 0) + 1;
113920
114398
  try {
113921
- const r = await Promise.race([d2.outcome, bound, aborted2]);
114399
+ const r = await Promise.race([d2.outcome, ...d2.waitCancelled ? [d2.waitCancelled] : [], bound, aborted2]);
113922
114400
  if (r === "aborted") {
113923
114401
  d2.stop("the call that asked was cancelled");
113924
- await d2.outcome;
113925
- this.delegations.delete(key);
113926
- return { ok: false, error: `The call was cancelled, so ${d2.target.name}'s session "${sessionName()}" was stopped.` };
114402
+ if (!d2.joinedTurn) await d2.outcome;
114403
+ if (this.delegations.get(key) === d2) this.delegations.delete(key);
114404
+ return { ok: false, error: d2.joinedTurn ? `The call was cancelled; ${d2.target.name}'s session "${sessionName()}" was left running.` : `The call was cancelled, so ${d2.target.name}'s session "${sessionName()}" was stopped.` };
113927
114405
  }
113928
114406
  if (r === "bound") {
113929
114407
  const notice = d2.notice();
@@ -113933,7 +114411,7 @@ ${String(stderr).slice(0, 2e3)}`
113933
114411
  if (!r.ok) {
113934
114412
  return {
113935
114413
  ok: false,
113936
- error: `${d2.target.name}'s session "${sessionName()}" [${d2.sessionId}] did not finish: ${r.error}. Tell the person, or ask again with a clearer task.`
114414
+ error: d2.joinedTurn ? `The wait for ${d2.target.name}'s session "${sessionName()}" [${d2.sessionId}] ended: ${r.error}.` : `${d2.target.name}'s session "${sessionName()}" [${d2.sessionId}] did not finish: ${r.error}. Tell the person, or ask again with a clearer task.`
113937
114415
  };
113938
114416
  }
113939
114417
  const answer = r.answer ?? finalAnswer(this.store.loadMessages(d2.sessionId, 50));
@@ -113954,6 +114432,7 @@ ${String(stderr).slice(0, 2e3)}`
113954
114432
  outside
113955
114433
  };
113956
114434
  } finally {
114435
+ d2.waiters = (d2.waiters ?? 1) - 1;
113957
114436
  clearTimeout(timer);
113958
114437
  if (onAbort) signal?.removeEventListener("abort", onAbort);
113959
114438
  }
@@ -113975,19 +114454,19 @@ ${String(stderr).slice(0, 2e3)}`
113975
114454
  async startAgent(callerId, opts, signal) {
113976
114455
  const caller = this.meta.get(callerId);
113977
114456
  if (!caller?.projectId) return { ok: false, error: "This session belongs to no project, so it cannot start agents." };
113978
- if (caller.askedBy) {
114457
+ if ((caller.askedBy || [...this.delegations.values()].some((d8) => d8.sessionId === callerId && !d8.agent && !d8.settled)) && !this.autoBoundaryOpen(callerId, "delegation depth")) {
113979
114458
  return { ok: false, error: "This session was itself started or asked by another session, so it cannot start agents \u2014 say in your answer what is needed instead." };
113980
114459
  }
113981
114460
  const project2 = this.store.listProjects().find((p) => p.id === caller.projectId);
113982
114461
  if (!project2) return { ok: false, error: "This session's project is no longer registered." };
113983
- if (project2.trusted !== true) {
114462
+ if (project2.trusted !== true && !this.autoBoundaryOpen(callerId, "start in an untrusted project")) {
113984
114463
  return { ok: false, error: "This session's project is not trusted, so it cannot start agents \u2014 the person can trust it from the project menu." };
113985
114464
  }
113986
114465
  const tool = opts.tool ?? this.firstTool(project2.defaultTool);
113987
114466
  if (!tool) return { ok: false, error: "No agent adapter is available. Install and configure an agent CLI before starting an agent." };
113988
114467
  const adapter = this.adapters.get(tool);
113989
114468
  if (!adapter) return { ok: false, error: `No agent named "${tool}" here. Installed tools: ${[...this.adapters.keys()].join(", ") || "none"}.` };
113990
- const full = this.agentCeilingError(project2.id);
114469
+ const full = this.agentCeilingError(callerId, project2.id);
113991
114470
  if (full) return { ok: false, error: full };
113992
114471
  const remembered = this.store.projectToolDefaults(project2.id)[tool];
113993
114472
  const model = opts.model ?? remembered?.model ?? void 0;
@@ -114012,7 +114491,7 @@ ${String(stderr).slice(0, 2e3)}`
114012
114491
  signal
114013
114492
  );
114014
114493
  if (!consent.ok) return consent;
114015
- const fullNow = this.agentCeilingError(project2.id);
114494
+ const fullNow = this.agentCeilingError(callerId, project2.id);
114016
114495
  if (fullNow) return { ok: false, error: fullNow };
114017
114496
  const cwd = worktree ? project2.path : this.store.sessionCwd(callerId) ?? caller.worktree?.path ?? project2.path;
114018
114497
  let child;
@@ -114050,95 +114529,126 @@ ${String(stderr).slice(0, 2e3)}`
114050
114529
  return result.ok && notice ? { ...result, notice: [result.notice, notice].filter(Boolean).join("; ") } : result;
114051
114530
  }
114052
114531
  /**
114053
- * agent_session (#561): the caller works with a session it started — and only those: one whose `askedBy` is the
114054
- * caller, in the caller's project.
114532
+ * agent_session (#561): safe/normal work only with their own children; auto opens ownership within the project.
114055
114533
  *
114056
114534
  * status where it stands, without waiting (the answer, once the turn the caller gave it has ended)
114057
114535
  * wait waits on that turn up to the bound, as start_agent does
114058
114536
  * send gives it more, including during the caller's turn on adapters that accept mid-turn input; refused
114059
114537
  * while it works on the person's own message or has a higher preset than the caller
114060
114538
  * stop stops the turn the caller gave it; a turn the person started there is theirs
114061
- * delete cleans up a child's merged, clean worktree (`cleanUpWorktreeChild`, #76's hard gate, part 3)
114539
+ * delete trashes a child without a worktree, stopping it first; worktree cleanup requires an idle child (#608)
114062
114540
  */
114063
114541
  async agentSession(callerId, opts, signal) {
114542
+ if (opts.sessionId === callerId && opts.action !== "status") {
114543
+ return { ok: false, error: "agent_session cannot wait on, stop, delete or send to its own calling session." };
114544
+ }
114064
114545
  const caller = this.meta.get(callerId);
114065
114546
  const child = this.meta.get(opts.sessionId);
114066
- if (!caller?.projectId || !child || child.askedBy !== callerId || child.projectId !== caller.projectId) {
114547
+ const deletingWithoutWorktree = opts.action === "delete" && !child?.worktree;
114548
+ const startedHere = child?.askedBy === callerId || deletingWithoutWorktree && child?.parentSessionId === callerId;
114549
+ if (!caller?.projectId || !child || !startedHere && (deletingWithoutWorktree || !this.autoBoundaryOpen(callerId, "session ownership")) || child.projectId !== caller.projectId) {
114067
114550
  return { ok: false, error: `${opts.sessionId} is not a session this session started. agent_session works only on sessions start_agent started here.` };
114068
114551
  }
114552
+ if (opts.sessionId === callerId) return this.agentAtRest(child);
114069
114553
  const key = `${callerId}>${child.id}`;
114070
- const d2 = this.delegations.get(key);
114071
- switch (opts.action) {
114072
- case "status":
114073
- if (d2 && !d2.settled) return this.agentWorking(d2, false);
114074
- return d2 ? this.awaitAgent(key, d2, signal) : this.agentAtRest(child);
114075
- case "wait":
114076
- return d2 ? this.awaitAgent(key, d2, signal) : this.agentAtRest(child);
114077
- case "stop": {
114078
- if (!d2 || d2.settled) return { ok: false, error: `Nothing you gave "${child.name}" is running.` };
114079
- if (this.awaitingReport.get(child.id) === callerId) this.awaitingReport.delete(child.id);
114080
- d2.stop("the session that started it stopped it");
114081
- await d2.outcome;
114082
- this.delegations.delete(key);
114083
- return { ok: true, state: "stopped", sessionId: child.id, sessionName: child.name, tool: child.tool };
114084
- }
114085
- case "delete": {
114086
- if (d2 && !d2.settled) return { ok: false, error: `"${child.name}" is still working on what you gave it \u2014 stop it or wait first.` };
114087
- const r = await this.cleanUpWorktreeChild(child);
114088
- if (!r.ok) return r;
114089
- if (d2) this.delegations.delete(key);
114090
- return { ok: true, state: "deleted", sessionId: child.id, sessionName: child.name, tool: child.tool, branch: r.branch, proof: r.proof };
114091
- }
114092
- case "send": {
114093
- if (!presetWithin(child.permissionPreset, caller.permissionPreset)) {
114094
- return {
114095
- ok: false,
114096
- error: `"${child.name}" now has more permissions (${child.permissionPreset}) than this session (${caller.permissionPreset}), so this session cannot give it work. Tell the person.`
114097
- };
114554
+ let d2 = this.delegations.get(key);
114555
+ let releaseObservation;
114556
+ if (!d2 && this.autoBoundaryOpen(callerId)) {
114557
+ const existing = [...this.delegations.values()].find((run) => run.sessionId === child.id && !run.settled);
114558
+ if (existing) d2 = existing;
114559
+ else if (opts.action !== "delete" && (child.state === "working" || child.state === "waiting_approval")) {
114560
+ const observation = this.observeTurn(child, opts.action !== "status");
114561
+ d2 = observation.run;
114562
+ releaseObservation = observation.release;
114563
+ }
114564
+ }
114565
+ const observing = !!d2 && d2.callerId !== callerId;
114566
+ try {
114567
+ switch (opts.action) {
114568
+ case "status":
114569
+ if (d2 && !d2.settled) return this.agentWorking(d2, false, observing);
114570
+ return d2 ? await this.awaitAgent(key, d2, signal, observing) : this.agentAtRest(child);
114571
+ case "wait":
114572
+ return d2 ? await this.awaitAgent(key, d2, signal, observing) : this.agentAtRest(child);
114573
+ case "stop": {
114574
+ if (!d2 || d2.settled) return { ok: false, error: `Nothing you gave "${child.name}" is running.` };
114575
+ if (!observing && this.awaitingReport.get(child.id) === callerId) this.awaitingReport.delete(child.id);
114576
+ d2.stop("the session that started it stopped it", observing);
114577
+ await d2.outcome;
114578
+ if (!observing) this.delegations.delete(key);
114579
+ return { ok: true, state: "stopped", sessionId: child.id, sessionName: child.name, tool: child.tool };
114580
+ }
114581
+ case "delete": {
114582
+ if (!child.worktree) {
114583
+ const busy = child.state === "working" || child.state === "waiting_approval" || !!child.pendingApproval || child.pendingQuestions.length > 0 || this.agentRuns.has(child.id) && !this.agentRuns.get(child.id).finished;
114584
+ if (!this.trashing.has(child.id) && busy && (!d2 || d2.settled || d2.callerId !== callerId)) {
114585
+ return { ok: false, error: `"${child.name}" is busy with something else; the person may be using it. Wait for that turn and its pending approval or question to finish before deleting it.` };
114586
+ }
114587
+ await this.trashSession(child.id);
114588
+ if (d2) this.delegations.delete(key);
114589
+ return { ok: true, state: "deleted", sessionId: child.id, sessionName: child.name, tool: child.tool };
114590
+ }
114591
+ if (d2 && !d2.settled) return { ok: false, error: `"${child.name}" is still working on what you gave it \u2014 stop it or wait first.` };
114592
+ const r = await this.cleanUpWorktreeChild(child);
114593
+ if (!r.ok) return r;
114594
+ if (d2) this.delegations.delete(key);
114595
+ return { ok: true, state: "deleted", sessionId: child.id, sessionName: child.name, tool: child.tool, branch: r.branch, setAside: r.setAside, ...r.warning ? { warning: r.warning } : {} };
114098
114596
  }
114099
- const from = { sessionId: callerId, name: labelOf(this.store, caller) };
114100
- if (d2 && !d2.settled) {
114101
- const text2 = agentNoteFrame(from.name, opts.text ?? "");
114102
- if (!this.adapters.get(child.tool)?.capabilities.midTurnInput || !this.handles.get(child.id)?.sendTracked) {
114103
- return { ok: false, error: `"${child.name}" cannot take messages while working. Wait for it (action "wait") or stop it first.` };
114597
+ case "send": {
114598
+ if (!presetWithin(child.permissionPreset, caller.permissionPreset)) {
114599
+ return {
114600
+ ok: false,
114601
+ error: `"${child.name}" now has more permissions (${child.permissionPreset}) than this session (${caller.permissionPreset}), so this session cannot give it work. Tell the person.`
114602
+ };
114104
114603
  }
114105
- const releaseHold = d2.wait.holdInput();
114106
- const release = () => {
114107
- releaseHold();
114108
- this.applyDeferredRestart(child.id);
114109
- };
114604
+ const from = { sessionId: callerId, name: labelOf(this.store, caller) };
114605
+ if (d2 && !d2.settled) {
114606
+ const text2 = agentNoteFrame(from.name, opts.text ?? "");
114607
+ if (!this.adapters.get(child.tool)?.capabilities.midTurnInput || !this.handles.get(child.id)?.sendTracked) {
114608
+ return { ok: false, error: `"${child.name}" cannot take messages while working. Wait for it (action "wait") or stop it first.` };
114609
+ }
114610
+ const releaseHold = d2.wait.holdInput();
114611
+ const release = () => {
114612
+ releaseHold();
114613
+ this.applyDeferredRestart(child.id);
114614
+ };
114615
+ try {
114616
+ await this.deliver(child.id, text2, void 0, from, false, void 0, "inline", (h, input) => {
114617
+ const fail4 = (e) => {
114618
+ d2.wait.fail(e);
114619
+ release();
114620
+ };
114621
+ void h.sendTracked(input, { complete: release, fail: fail4 }).then(release, fail4);
114622
+ });
114623
+ } catch (e) {
114624
+ release();
114625
+ return { ok: false, error: `Could not send it: ${e.message}` };
114626
+ }
114627
+ return this.agentWorking(d2, false, observing);
114628
+ }
114629
+ if (child.state === "working" || child.state === "waiting_approval" || this.agentRuns.has(child.id) && !this.agentRuns.get(child.id).finished) {
114630
+ return { ok: false, error: `"${child.name}" is busy with something else (the person may be using it). Try again later.` };
114631
+ }
114632
+ if (d2) this.delegations.delete(key);
114633
+ const full = this.agentCeilingError(callerId, child.projectId ?? "");
114634
+ if (full) return { ok: false, error: full };
114635
+ const project2 = this.store.listProjects().find((p) => p.id === child.projectId);
114636
+ const cwd = this.store.sessionCwd(child.id) ?? child.worktree?.path ?? project2?.path ?? "";
114637
+ const next = this.startDelegation(key, callerId, child.id, { id: child.projectId ?? "", name: project2?.name ?? "", path: cwd }, true);
114110
114638
  try {
114111
- await this.deliver(child.id, text2, void 0, from, false, void 0, "inline", (h, input) => {
114112
- const fail4 = (e) => {
114113
- d2.wait.fail(e);
114114
- release();
114115
- };
114116
- void h.sendTracked(input, { complete: release, fail: fail4 }).then(release, fail4);
114117
- });
114639
+ await this.deliver(child.id, agentFrame(from.name, opts.text ?? ""), void 0, from, false);
114118
114640
  } catch (e) {
114119
- release();
114641
+ this.delegations.delete(key);
114642
+ if (this.agentRuns.get(child.id) === next.wait) this.agentRuns.delete(child.id);
114120
114643
  return { ok: false, error: `Could not send it: ${e.message}` };
114121
114644
  }
114122
- return this.agentWorking(d2, false);
114645
+ return this.awaitAgent(key, next, signal);
114123
114646
  }
114124
- if (child.state === "working" || child.state === "waiting_approval" || this.agentRuns.has(child.id) && !this.agentRuns.get(child.id).finished) {
114125
- return { ok: false, error: `"${child.name}" is busy with something else (the person may be using it). Try again later.` };
114126
- }
114127
- if (d2) this.delegations.delete(key);
114128
- const full = this.agentCeilingError(child.projectId ?? "");
114129
- if (full) return { ok: false, error: full };
114130
- const project2 = this.store.listProjects().find((p) => p.id === child.projectId);
114131
- const cwd = this.store.sessionCwd(child.id) ?? child.worktree?.path ?? project2?.path ?? "";
114132
- const next = this.startDelegation(key, callerId, child.id, { id: child.projectId ?? "", name: project2?.name ?? "", path: cwd }, true);
114133
- try {
114134
- await this.deliver(child.id, agentFrame(from.name, opts.text ?? ""), void 0, from, false);
114135
- } catch (e) {
114136
- this.delegations.delete(key);
114137
- if (this.agentRuns.get(child.id) === next.wait) this.agentRuns.delete(child.id);
114138
- return { ok: false, error: `Could not send it: ${e.message}` };
114139
- }
114140
- return this.awaitAgent(key, next, signal);
114647
+ default:
114648
+ return { ok: false, error: "Unknown agent_session action." };
114141
114649
  }
114650
+ } finally {
114651
+ releaseObservation?.();
114142
114652
  }
114143
114653
  }
114144
114654
  /**
@@ -114151,65 +114661,70 @@ ${String(stderr).slice(0, 2e3)}`
114151
114661
  if (!summary.isRepo) return { error: "This project is not a git repository, so it has no worktrees. Start the agent without one." };
114152
114662
  const asked = opts.branch?.trim();
114153
114663
  if (asked) return await gitValidBranchName(asked) ? { branch: asked } : { error: `Not a valid branch name: ${asked}` };
114154
- return { branch: agentBranch(opts.task, randomUUID9()) };
114664
+ return { branch: agentBranch(opts.task, randomUUID10()) };
114155
114665
  }
114156
- /**
114157
- * Cleans up a child's merged worktree (#76's hard gate, moved from the retired worktree manager's tool to
114158
- * agent_session delete, #561 part 3). It runs **only when losslessness is proven at this moment**: no uncommitted
114159
- * changes, and the branch's current tip has landed on the trunk (by ancestry, or by a merged pull request whose head
114160
- * is that tip). Not the cached merged badge: a commit could have landed since (TOCTOU). Every deletion outside that
114161
- * proof stays the person's, in the delete dialog. The session goes to the trash like any other (an agent never
114162
- * deletes one for good), the worktree and branch are removed, the tool's own conversation is kept, and the tip is
114163
- * logged for reflog recovery.
114164
- */
114666
+ /** Prevent a send/resume from racing the snapshot and worktree removal. */
114667
+ worktreeCleanup = /* @__PURE__ */ new Set();
114668
+ /** #608: the parent decides; local work follows the session into the trash in every preset. */
114165
114669
  async cleanUpWorktreeChild(target) {
114166
114670
  if (!target.worktree?.base || !target.projectId) {
114167
114671
  return { ok: false, error: `"${target.name}" works in no worktree of its own, so there is nothing to clean up \u2014 the person deletes a session` };
114168
114672
  }
114169
- if (target.state === "working" || target.state === "waiting_approval" || this.agentRuns.has(target.id)) {
114170
- return { ok: false, error: `"${target.name}" is still working \u2014 clean it up once its turn ends` };
114673
+ if (target.state === "working" || target.state === "waiting_approval" || this.agentRuns.has(target.id) || sessionBusy({ ...target, live: this.handles.has(target.id) })) {
114674
+ return { ok: false, error: `"${target.name}" is still working \u2014 stop it or wait first` };
114171
114675
  }
114172
- const cwd = this.cwdOf(target.projectId);
114173
- const { branch, path: path2, base: base2 } = target.worktree;
114174
- const wt2 = await gitWorktreeDirty(path2, this.gitTrust(target.projectId)).catch(() => ({ dirty: true, changedFiles: -1 }));
114175
- if (wt2.dirty) {
114176
- const n = wt2.changedFiles >= 0 ? `${wt2.changedFiles} ` : "";
114177
- return { ok: false, error: `There are ${n}uncommitted changes in "${target.name}" \u2014 have it commit (or discard) them, then delete again` };
114676
+ if (!existsSync19(target.worktree.path)) {
114677
+ return { ok: false, error: `The worktree folder for "${target.name}" is missing: ${target.worktree.path}. Restore the folder before cleanup; its branch has been left in place.` };
114178
114678
  }
114179
- const trunk = this.trunkOf(target.projectId) ?? "HEAD";
114180
- let proof = null;
114181
- if (await gitBranchMerged(cwd, branch, base2, trunk).catch(() => false)) {
114182
- proof = "trunk ancestry";
114183
- } else if (this.ghAvailable) {
114184
- const pr = await this.prLookup(cwd, branch).catch(() => null);
114185
- if (pr === "unavailable") this.ghAvailable = false;
114186
- else if (pr && pr.state === "merged") {
114187
- const tip2 = await gitRevParse(cwd, `refs/heads/${branch}`);
114188
- if (pr.headOid && tip2 && tip2 === pr.headOid) proof = `PR #${pr.number}`;
114189
- else if (pr.headOid && tip2) {
114190
- return { ok: false, error: `PR #${pr.number} was merged, but "${branch}" has a new commit since \u2014 it is deleted only once that commit has landed on the trunk too` };
114191
- }
114679
+ if (this.resuming.has(target.id) || this.deferredRestarts.has(target.id)) return { ok: false, error: `"${target.name}" is waking or restarting \u2014 wait for it first` };
114680
+ if (this.worktreeCleanup.has(target.id)) return { ok: false, error: "This worktree is already being removed" };
114681
+ this.worktreeCleanup.add(target.id);
114682
+ const { branch, path: path2 } = target.worktree;
114683
+ try {
114684
+ const cwd = this.cwdOf(target.projectId);
114685
+ const handle = this.handles.get(target.id);
114686
+ if (handle) {
114687
+ await handle.dispose();
114688
+ this.handles.delete(target.id);
114689
+ this.running.delete(target.id);
114690
+ }
114691
+ const trunk = await gitRevParse(cwd, this.trunkOf(target.projectId) ?? "HEAD");
114692
+ const setAside = await saveWorktreeRecovery(target.id, path2, trunk ?? "missing-trunk", branch);
114693
+ if (setAside.changedFiles || setAside.commits) {
114694
+ this.onEvent({
114695
+ type: "notice",
114696
+ sessionId: target.id,
114697
+ level: "info",
114698
+ from: "Centralu",
114699
+ label: "worktree recovery",
114700
+ text: `Local work saved before worktree deletion: ${setAside.changedFiles} uncommitted files, ${setAside.commits} commits not on the trunk. Recovery patches and bundles are at ${worktreeRecoveryDir(target.id)}. Restore from Settings \u2192 Trash or recover these files manually before deleting this session for good.`
114701
+ });
114192
114702
  }
114703
+ await this.trashSession(target.id, false, false, setAside);
114704
+ try {
114705
+ await gitWorktreeRemove(cwd, path2, true, true);
114706
+ } catch (e) {
114707
+ return { ok: true, branch, setAside, warning: `The session is in the trash, but worktree ${path2} and branch ${branch} were left: ${e.message}. Recovery files are at ${worktreeRecoveryDir(target.id)}.` };
114708
+ }
114709
+ try {
114710
+ await gitBranchDelete(cwd, branch, this.gitTrust(target.projectId));
114711
+ } catch (e) {
114712
+ return { ok: true, branch, setAside, warning: `The session is in the trash and its worktree was removed, but branch ${branch} was left: ${e.message}. Recovery files are at ${worktreeRecoveryDir(target.id)}.` };
114713
+ }
114714
+ return { ok: true, branch, setAside };
114715
+ } catch (e) {
114716
+ return { ok: false, error: `Could not finish removing "${branch}": ${e.message}. Any saved recovery files are at ${worktreeRecoveryDir(target.id)}.` };
114717
+ } finally {
114718
+ this.worktreeCleanup.delete(target.id);
114193
114719
  }
114194
- if (!proof) {
114195
- return {
114196
- ok: false,
114197
- error: `Could not prove that "${branch}" has landed on the trunk \u2014 it is deleted only once a merge (or a merged PR) is confirmed. Discarding it without proof is the person's job, in the delete dialog`
114198
- };
114199
- }
114200
- const tip = await gitRevParse(cwd, `refs/heads/${branch}`);
114201
- await this.trashSession(target.id);
114202
- await gitWorktreeRemove(cwd, path2, true).catch(() => {
114203
- });
114204
- await gitBranchDelete(cwd, branch, this.gitTrust(target.projectId)).catch(() => {
114205
- });
114206
- console.error(`[worktree] cleaned up ${branch} (tip ${tip?.slice(0, 8) ?? "?"}, proof: ${proof})`);
114207
- return { ok: true, branch, proof };
114208
114720
  }
114209
- /** Refuses a start when the project's children already running reach `AGENT_CEILING` (#561) */
114210
- agentCeilingError(projectId) {
114721
+ /** A saved ceiling overrides the caller's preset default: auto unlimited, otherwise AGENT_CEILING. */
114722
+ agentCeilingError(callerId, projectId) {
114723
+ const configured = this.uiPreferences().agentCeiling;
114724
+ const ceiling = configured ?? (this.autoBoundaryOpen(callerId) ? 0 : AGENT_CEILING);
114725
+ if (ceiling === 0) return null;
114211
114726
  const running = [...this.delegations.values()].filter((d2) => d2.agent && !d2.settled && this.meta.get(d2.sessionId)?.projectId === projectId).length;
114212
- if (running < AGENT_CEILING) return null;
114727
+ if (running < ceiling) return null;
114213
114728
  return `${running} agents started by this project's sessions are already working, the most at once. Wait for one (agent_session, action "wait") or stop one, then try again.`;
114214
114729
  }
114215
114730
  /**
@@ -114269,8 +114784,8 @@ ${String(stderr).slice(0, 2e3)}`
114269
114784
  * "Working": the caller is told when the turn ends (#561), through the report the orchestrator's reportBack uses, unless
114270
114785
  * a call is waiting on it by then (`awaitAgent` takes the marker down while it waits)
114271
114786
  */
114272
- agentWorking(d2, arm = true) {
114273
- if (arm) this.awaitingReport.set(d2.sessionId, d2.callerId);
114787
+ agentWorking(d2, arm = true, observing = false) {
114788
+ if (arm && !observing) this.awaitingReport.set(d2.sessionId, d2.callerId);
114274
114789
  const notice = d2.notice();
114275
114790
  return {
114276
114791
  ok: true,
@@ -114278,7 +114793,7 @@ ${String(stderr).slice(0, 2e3)}`
114278
114793
  sessionId: d2.sessionId,
114279
114794
  sessionName: this.meta.get(d2.sessionId)?.name ?? d2.sessionId,
114280
114795
  tool: this.meta.get(d2.sessionId)?.tool ?? "",
114281
- reported: this.awaitingReport.get(d2.sessionId) === d2.callerId,
114796
+ reported: !observing && this.awaitingReport.get(d2.sessionId) === d2.callerId,
114282
114797
  ...notice ? { notice } : {}
114283
114798
  };
114284
114799
  }
@@ -114295,32 +114810,43 @@ ${String(stderr).slice(0, 2e3)}`
114295
114810
  };
114296
114811
  }
114297
114812
  /** Waits on a child's turn up to the bound (#561), like `awaitDelegation`, and reads its outcome for the caller */
114298
- async awaitAgent(key, d2, signal) {
114299
- if (this.awaitingReport.get(d2.sessionId) === d2.callerId) this.awaitingReport.delete(d2.sessionId);
114813
+ async awaitAgent(key, d2, signal, observing = false) {
114814
+ if (!observing && this.awaitingReport.get(d2.sessionId) === d2.callerId) this.awaitingReport.delete(d2.sessionId);
114300
114815
  const name = () => this.meta.get(d2.sessionId)?.name ?? d2.sessionId;
114301
114816
  let timer;
114302
114817
  let onAbort;
114303
- const bound = new Promise((resolve11) => timer = setTimeout(() => resolve11("bound"), ASK_WAIT_MS));
114304
- const aborted2 = new Promise((resolve11) => {
114818
+ const bound = new Promise((resolve12) => timer = setTimeout(() => resolve12("bound"), ASK_WAIT_MS));
114819
+ const aborted2 = new Promise((resolve12) => {
114305
114820
  if (!signal) return;
114306
- onAbort = () => resolve11("aborted");
114821
+ onAbort = () => resolve12("aborted");
114307
114822
  if (signal.aborted) onAbort();
114308
114823
  else signal.addEventListener("abort", onAbort, { once: true });
114309
114824
  });
114310
- d2.waiters = (d2.waiters ?? 0) + 1;
114825
+ if (!observing) d2.waiters = (d2.waiters ?? 0) + 1;
114311
114826
  try {
114312
114827
  const r = await Promise.race([d2.outcome, bound, aborted2]);
114313
- d2.waiters--;
114828
+ if (!observing) d2.waiters = (d2.waiters ?? 1) - 1;
114314
114829
  if (r === "aborted") {
114830
+ if (observing) return { ok: false, error: `The observation was cancelled; "${name()}" was left running.` };
114315
114831
  d2.stop("the call that started it was cancelled");
114316
114832
  await d2.outcome;
114317
114833
  this.delegations.delete(key);
114318
114834
  return { ok: false, error: `The call was cancelled, so "${name()}" was stopped.` };
114319
114835
  }
114320
- if (r === "bound") return this.agentWorking(d2);
114321
- if (this.delegations.get(key) === d2) this.delegations.delete(key);
114836
+ if (r === "bound") return this.agentWorking(d2, !observing, observing);
114837
+ if (!observing && this.delegations.get(key) === d2) this.delegations.delete(key);
114322
114838
  if (!r.ok) return { ok: false, error: `"${name()}" [${d2.sessionId}] did not finish: ${r.error}. Tell the person, or give it a clearer task.` };
114323
114839
  const answer = r.answer ?? finalAnswer(this.store.loadMessages(d2.sessionId, 50));
114840
+ if (observing) return {
114841
+ ok: true,
114842
+ state: "done",
114843
+ sessionId: d2.sessionId,
114844
+ sessionName: name(),
114845
+ tool: this.meta.get(d2.sessionId)?.tool ?? "",
114846
+ answer: clipAnswer(answer, name()),
114847
+ readable: [],
114848
+ outside: []
114849
+ };
114324
114850
  const { granted, outside } = readableGrants(pathsIn(answer), d2.target.path);
114325
114851
  if (granted.length) {
114326
114852
  const set3 = this.readGrants.get(d2.callerId) ?? /* @__PURE__ */ new Set();
@@ -114343,39 +114869,39 @@ ${String(stderr).slice(0, 2e3)}`
114343
114869
  }
114344
114870
  }
114345
114871
  askCapability(q2, signal) {
114346
- return new Promise((resolve11) => {
114347
- if (signal.aborted) return resolve11(null);
114872
+ return new Promise((resolve12) => {
114873
+ if (signal.aborted) return resolve12(null);
114348
114874
  const app2 = { appId: q2.app.appId, projectId: q2.app.projectId, name: q2.appName };
114349
114875
  if (q2.origin.kind === "session" && this.meta.has(q2.origin.sessionId)) {
114350
- const requestId = `cap-${randomUUID9()}`;
114876
+ const requestId = `cap-${randomUUID10()}`;
114351
114877
  const ask = {
114352
114878
  requestId,
114353
114879
  sessionId: q2.origin.sessionId,
114354
114880
  detail: { kind: "capability", app: app2, capability: q2.capability, text: q2.text },
114355
114881
  shown: false,
114356
- resolve: (d2) => resolve11(d2 === "always" || d2 === "always_project" ? "allow" : d2)
114882
+ resolve: (d2) => resolve12(d2 === "always" || d2 === "always_project" ? "allow" : d2)
114357
114883
  };
114358
114884
  this.hostAsks.set(requestId, ask);
114359
114885
  signal.addEventListener("abort", () => {
114360
114886
  if (!this.hostAsks.delete(requestId)) return;
114361
114887
  const m = this.meta.get(ask.sessionId);
114362
114888
  if (m?.pendingApproval?.requestId === requestId) this.onEvent({ type: "approval_resolved", sessionId: ask.sessionId, requestId, decision: "deny" });
114363
- resolve11(null);
114889
+ resolve12(null);
114364
114890
  }, { once: true });
114365
114891
  this.raiseHostAsks(ask.sessionId);
114366
114892
  return;
114367
114893
  }
114368
114894
  const origin = q2.origin.kind === "view" ? q2.origin.app : q2.app;
114369
- const id2 = `q-${randomUUID9()}`;
114895
+ const id2 = `q-${randomUUID10()}`;
114370
114896
  this.appQuestions.set(id2, {
114371
114897
  question: { id: id2, app: app2, capability: q2.capability, text: q2.text, origin: { appId: origin.appId, projectId: origin.projectId }, askedAt: q2.askedAt, expiresAt: q2.expiresAt },
114372
- resolve: resolve11
114898
+ resolve: resolve12
114373
114899
  });
114374
114900
  this.emit({ type: "external_app_questions_changed" });
114375
114901
  signal.addEventListener("abort", () => {
114376
114902
  if (!this.appQuestions.delete(id2)) return;
114377
114903
  this.emit({ type: "external_app_questions_changed" });
114378
- resolve11(null);
114904
+ resolve12(null);
114379
114905
  }, { once: true });
114380
114906
  });
114381
114907
  }
@@ -114684,6 +115210,7 @@ ${String(stderr).slice(0, 2e3)}`
114684
115210
  this.appsHub?.dispose();
114685
115211
  this.appsHub = new SessionAppsHub(rt2, {
114686
115212
  ...opts,
115213
+ boundaryOpen: (id2) => this.autoBoundaryOpen(id2),
114687
115214
  onDemand: {
114688
115215
  attached: (id2) => this.appAccess?.attached(id2) ?? [],
114689
115216
  /*
@@ -114694,7 +115221,9 @@ ${String(stderr).slice(0, 2e3)}`
114694
115221
  allowed: (session2, app2) => this.toolProfileOf(session2.id) === "reader" && (this.appAccess?.allowed(session2, app2) ?? false)
114695
115222
  }
114696
115223
  });
115224
+ rt2.setSessionBoundaryPolicy((id2) => this.autoBoundaryOpen(id2));
114697
115225
  this.appAccess = new AppAccess({
115226
+ boundaryOpen: (id2, boundary) => this.autoBoundaryOpen(id2, boundary),
114698
115227
  hub: this.appsHub,
114699
115228
  store: this.store,
114700
115229
  session: (id2) => {
@@ -115034,8 +115563,8 @@ ${String(stderr).slice(0, 2e3)}`
115034
115563
  return readMessageImage(abs);
115035
115564
  }
115036
115565
  repliesName(sessionId, written) {
115037
- const open5 = this.streams.get(sessionId);
115038
- if (open5?.kind === "text" && mentions(open5.text, written)) return true;
115566
+ const open6 = this.streams.get(sessionId);
115567
+ if (open6?.kind === "text" && mentions(open6.text, written)) return true;
115039
115568
  return writtenForms(written).some((form) => this.store.replyTextsContaining(sessionId, form).some((text2) => mentions(text2, written)));
115040
115569
  }
115041
115570
  /**
@@ -115048,14 +115577,14 @@ ${String(stderr).slice(0, 2e3)}`
115048
115577
  }
115049
115578
  async loadMessages(sessionId, limit, beforeSeq) {
115050
115579
  const rows = this.store.loadMessages(sessionId, limit, beforeSeq);
115051
- const { readFile: readFile5 } = await import("node:fs/promises");
115580
+ const { readFile: readFile6 } = await import("node:fs/promises");
115052
115581
  return Promise.all(
115053
115582
  rows.map(async (r) => {
115054
115583
  if (r.kind === "image") {
115055
115584
  const p = r.payload;
115056
115585
  if (!p.path || p.note) return r;
115057
115586
  try {
115058
- const data = (await readFile5(p.path)).toString("base64");
115587
+ const data = (await readFile6(p.path)).toString("base64");
115059
115588
  return { ...r, payload: { ...p, data } };
115060
115589
  } catch {
115061
115590
  return { ...r, payload: { ...p, note: "The image was cleared and no longer exists (total-size cap)" } };
@@ -115068,7 +115597,7 @@ ${String(stderr).slice(0, 2e3)}`
115068
115597
  p.attachments.map(async (a) => {
115069
115598
  if (a.kind !== "image") return a;
115070
115599
  try {
115071
- return { ...a, data: (await readFile5(a.path)).toString("base64") };
115600
+ return { ...a, data: (await readFile6(a.path)).toString("base64") };
115072
115601
  } catch {
115073
115602
  return a;
115074
115603
  }
@@ -115082,6 +115611,7 @@ ${String(stderr).slice(0, 2e3)}`
115082
115611
  }
115083
115612
  async disposeAll() {
115084
115613
  this.watchers.close();
115614
+ for (const readers of this.turnObservers.values()) for (const observer of readers) observer.fail(new Error("Centralu is stopping"));
115085
115615
  this.appsHub?.rt.attachBrokerHost(null);
115086
115616
  for (const run of [...this.agentRuns.values()]) run.fail(new Error("Centralu is shutting down"));
115087
115617
  for (const ask of [...this.hostAsks.values()]) ask.resolve(null);
@@ -115112,6 +115642,7 @@ ${String(stderr).slice(0, 2e3)}`
115112
115642
  this.watchers.close();
115113
115643
  this.appsHub?.rt.attachBrokerHost(null);
115114
115644
  for (const run of [...this.agentRuns.values()]) run.fail(new Error("Centralu is restarting"));
115645
+ for (const readers of this.turnObservers.values()) for (const observer of readers) observer.fail(new Error("Centralu is restarting"));
115115
115646
  for (const ask of [...this.hostAsks.values()]) ask.resolve(null);
115116
115647
  this.hostAsks.clear();
115117
115648
  for (const q2 of [...this.appQuestions.values()]) q2.resolve(null);
@@ -115143,8 +115674,8 @@ import { relative as relative9 } from "node:path";
115143
115674
  // packages/agent-host/src/themes.ts
115144
115675
  import { randomBytes as randomBytes4 } from "node:crypto";
115145
115676
  import { watch as watch2 } from "node:fs";
115146
- import { mkdir as mkdir6, readdir as readdir8, readFile as readFile4, rename as rename4, stat as stat5, unlink as unlink2, writeFile as writeFile4 } from "node:fs/promises";
115147
- import { basename as basename6, join as join29 } from "node:path";
115677
+ import { mkdir as mkdir7, readdir as readdir9, readFile as readFile5, rename as rename4, stat as stat6, unlink as unlink2, writeFile as writeFile5 } from "node:fs/promises";
115678
+ import { basename as basename6, join as join30 } from "node:path";
115148
115679
  var MAX_FILE_BYTES = 256 * 1024;
115149
115680
  var SETTLE_MS = 100;
115150
115681
  var ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,59}$/;
@@ -115159,7 +115690,7 @@ var ThemeFiles = class {
115159
115690
  timer = null;
115160
115691
  /** Makes the folder, writes the schema next to the themes, and starts watching */
115161
115692
  async start() {
115162
- await mkdir6(this.dir, { recursive: true });
115693
+ await mkdir7(this.dir, { recursive: true });
115163
115694
  await this.writeSchema();
115164
115695
  try {
115165
115696
  this.watcher = watch2(this.dir, (_event, name) => {
@@ -115189,15 +115720,15 @@ var ThemeFiles = class {
115189
115720
  * a token updates it, and an unchanged one does not touch the folder (or wake the watcher).
115190
115721
  */
115191
115722
  async writeSchema() {
115192
- const path2 = join29(this.dir, THEME_SCHEMA_FILE);
115723
+ const path2 = join30(this.dir, THEME_SCHEMA_FILE);
115193
115724
  const text2 = `${JSON.stringify(themeFileJsonSchema(), null, 2)}
115194
115725
  `;
115195
- const current = await readFile4(path2, "utf8").catch(() => null);
115726
+ const current = await readFile5(path2, "utf8").catch(() => null);
115196
115727
  if (current !== text2) await this.atomicWrite(path2, text2);
115197
115728
  }
115198
115729
  /** Every `<id>.json` in the folder, read and checked. A file that cannot be read is listed with the reason. */
115199
115730
  async list() {
115200
- const names = await readdir8(this.dir).catch(() => []);
115731
+ const names = await readdir9(this.dir).catch(() => []);
115201
115732
  const entries = [];
115202
115733
  const ids = names.filter((name) => name.endsWith(".json") && name !== THEME_SCHEMA_FILE).map((name) => name.slice(0, -".json".length)).filter((id2) => ID_PATTERN.test(id2)).sort();
115203
115734
  for (const id2 of ids) entries.push(await this.read(id2));
@@ -115206,11 +115737,11 @@ var ThemeFiles = class {
115206
115737
  async read(id2) {
115207
115738
  const path2 = this.pathOf(id2);
115208
115739
  try {
115209
- const info = await stat5(path2);
115740
+ const info = await stat6(path2);
115210
115741
  if (info.size > MAX_FILE_BYTES) {
115211
115742
  return { id: id2, path: path2, name: id2, base: "dark", tokens: {}, problems: ["The file is too large to be a theme."], broken: true };
115212
115743
  }
115213
- return parseThemeFile(id2, path2, await readFile4(path2, "utf8"));
115744
+ return parseThemeFile(id2, path2, await readFile5(path2, "utf8"));
115214
115745
  } catch (e) {
115215
115746
  return { id: id2, path: path2, name: id2, base: "dark", tokens: {}, problems: [`Could not read the file: ${e.message}`], broken: true };
115216
115747
  }
@@ -115218,16 +115749,16 @@ var ThemeFiles = class {
115218
115749
  /** Writes a theme. `id` null makes a new file named after the theme (`My theme` → `my-theme.json`). */
115219
115750
  async save(id2, content) {
115220
115751
  const target = id2 ?? await this.freeId(content.name);
115221
- await mkdir6(this.dir, { recursive: true });
115752
+ await mkdir7(this.dir, { recursive: true });
115222
115753
  await this.atomicWrite(this.pathOf(target), formatThemeFile(content));
115223
115754
  return this.read(target);
115224
115755
  }
115225
115756
  /** Copies a file from elsewhere in, under a fresh id if its name is taken. Refuses what does not read as a theme. */
115226
115757
  async importFrom(source) {
115227
- const info = await stat5(source);
115758
+ const info = await stat6(source);
115228
115759
  if (!info.isFile()) throw Object.assign(new Error("That is not a file."), { code: "invalid_params" });
115229
115760
  if (info.size > MAX_FILE_BYTES) throw Object.assign(new Error("The file is too large to be a theme."), { code: "invalid_params" });
115230
- const text2 = await readFile4(source, "utf8");
115761
+ const text2 = await readFile5(source, "utf8");
115231
115762
  const parsed = parseThemeFile("import", source, text2);
115232
115763
  if (parsed.broken) throw Object.assign(new Error(parsed.problems[0] ?? "Not a theme file."), { code: "invalid_params" });
115233
115764
  const fromName = basename6(source).replace(/\.json$/i, "");
@@ -115238,13 +115769,13 @@ var ThemeFiles = class {
115238
115769
  /** The absolute path for an id, for the shell to trash or reveal. Refuses anything that is not an id. */
115239
115770
  pathOf(id2) {
115240
115771
  if (!ID_PATTERN.test(id2)) throw Object.assign(new Error(`Not a theme id: ${id2}`), { code: "invalid_params" });
115241
- return join29(this.dir, `${id2}.json`);
115772
+ return join30(this.dir, `${id2}.json`);
115242
115773
  }
115243
115774
  async freeId(name) {
115244
115775
  const base2 = name.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "theme";
115245
115776
  for (let n = 1; n < 1e3; n++) {
115246
115777
  const id2 = n === 1 ? base2 : `${base2}-${n}`;
115247
- const taken = await stat5(this.pathOf(id2)).then(
115778
+ const taken = await stat6(this.pathOf(id2)).then(
115248
115779
  () => true,
115249
115780
  () => false
115250
115781
  );
@@ -115256,7 +115787,7 @@ var ThemeFiles = class {
115256
115787
  async atomicWrite(path2, text2) {
115257
115788
  const tmp = `${path2}.${randomBytes4(6).toString("hex")}.tmp`;
115258
115789
  try {
115259
- await writeFile4(tmp, text2, "utf8");
115790
+ await writeFile5(tmp, text2, "utf8");
115260
115791
  await rename4(tmp, path2);
115261
115792
  } catch (e) {
115262
115793
  await unlink2(tmp).catch(() => {
@@ -116235,9 +116766,9 @@ function broadcastAppChanges(send, windowMs = APP_CHANGE_WINDOW_MS) {
116235
116766
  return {
116236
116767
  emit(ref, cause2 = null) {
116237
116768
  const key = JSON.stringify([ref.projectId, ref.appId]);
116238
- const open5 = pending.get(key);
116239
- if (open5) {
116240
- if (!sameCause(open5.cause, cause2)) open5.cause = null;
116769
+ const open6 = pending.get(key);
116770
+ if (open6) {
116771
+ if (!sameCause(open6.cause, cause2)) open6.cause = null;
116241
116772
  return;
116242
116773
  }
116243
116774
  const timer = setTimeout(() => {
@@ -116277,7 +116808,7 @@ var sameCause = (a, b2) => a !== null && b2 !== null && JSON.stringify(a) === JS
116277
116808
 
116278
116809
  // packages/agent-host/src/dev-services/terminal.ts
116279
116810
  import { createRequire } from "node:module";
116280
- import { existsSync as existsSync19 } from "node:fs";
116811
+ import { existsSync as existsSync20 } from "node:fs";
116281
116812
  import { win32 as win327 } from "node:path";
116282
116813
  var require2 = createRequire(import.meta.url);
116283
116814
  var SCROLLBACK_BYTES = 256 * 1024;
@@ -116509,9 +117040,9 @@ function commandShell(command, platform = process.platform, env = process.env) {
116509
117040
  }
116510
117041
  function shellPath() {
116511
117042
  const fromEnv = process.env.SHELL;
116512
- if (fromEnv && existsSync19(fromEnv)) return fromEnv;
117043
+ if (fromEnv && existsSync20(fromEnv)) return fromEnv;
116513
117044
  for (const candidate of ["/bin/zsh", "/bin/bash", "/bin/sh"]) {
116514
- if (existsSync19(candidate)) return candidate;
117045
+ if (existsSync20(candidate)) return candidate;
116515
117046
  }
116516
117047
  return process.platform === "win32" ? "powershell.exe" : "/bin/sh";
116517
117048
  }
@@ -116698,17 +117229,19 @@ var CommandRunner = class {
116698
117229
 
116699
117230
  // packages/agent-host/src/updates.ts
116700
117231
  import { execFile as execFile8 } from "node:child_process";
116701
- import { existsSync as existsSync20 } from "node:fs";
117232
+ import { existsSync as existsSync21 } from "node:fs";
116702
117233
  var REGISTRY_URL = `https://registry.npmjs.org/${APP_SLUG}/latest`;
116703
117234
  var CHECK_INTERVAL_MS = 6 * 60 * 60 * 1e3;
116704
117235
  var SCHEDULED_TIMEOUT_MS = 3e3;
116705
117236
  var FORCED_TIMEOUT_MS = 8e3;
117237
+ var INSTALL_CHECK_MAX_AGE_MS = 6e4;
117238
+ var STORED_RELEASE_MAX_AGE_MS = 60 * 60 * 1e3;
116706
117239
  var INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
116707
117240
  var NPM_RETRY_DELAY_MS = 3e4;
116708
117241
  var NPM_RETRY_WINDOW_MS = 10 * 60 * 1e3;
116709
117242
  function existingInstalledCopy() {
116710
117243
  const path2 = installedCopyPath();
116711
- return existsSync20(path2) ? path2 : null;
117244
+ return existsSync21(path2) ? path2 : null;
116712
117245
  }
116713
117246
  async function fetchLatestFromRegistry(timeoutMs) {
116714
117247
  try {
@@ -116722,16 +117255,16 @@ async function fetchLatestFromRegistry(timeoutMs) {
116722
117255
  return { ok: false, reason: "Could not reach the registry \u2014 check the network" };
116723
117256
  }
116724
117257
  }
116725
- function commandFor(file2, args, resolve11 = (f) => resolveCommand(f)) {
116726
- const launch2 = resolve11(file2);
117258
+ function commandFor(file2, args, resolve12 = (f) => resolveCommand(f)) {
117259
+ const launch2 = resolve12(file2);
116727
117260
  return { file: launch2.command, args: [...launch2.args, ...args] };
116728
117261
  }
116729
117262
  function failureDetail(file2, stderr) {
116730
117263
  const lines = stderr.split(/\r?\n/).map((l) => l.trim()).filter((l) => l && !/complete log of this run/i.test(l));
116731
117264
  return (file2 === "npm" ? lines.at(-1) : lines[0]) ?? null;
116732
117265
  }
116733
- function runCommand(file2, args, resolve11) {
116734
- const cmd = commandFor(file2, args, resolve11);
117266
+ function runCommand(file2, args, resolve12) {
117267
+ const cmd = commandFor(file2, args, resolve12);
116735
117268
  return new Promise((done, reject) => {
116736
117269
  execFile8(cmd.file, cmd.args, { timeout: INSTALL_TIMEOUT_MS, windowsHide: true }, (err, _stdout, stderr) => {
116737
117270
  if (!err) return done();
@@ -116759,7 +117292,7 @@ var UpdateService = class {
116759
117292
  readAutoApply: deps.readAutoApply ?? (() => false),
116760
117293
  writeAutoApply: deps.writeAutoApply ?? (() => {
116761
117294
  }),
116762
- wait: deps.wait ?? ((ms3) => new Promise((resolve11) => setTimeout(resolve11, ms3)))
117295
+ wait: deps.wait ?? ((ms3) => new Promise((resolve12) => setTimeout(resolve12, ms3)))
116763
117296
  };
116764
117297
  this.status = {
116765
117298
  /*
@@ -116783,6 +117316,8 @@ var UpdateService = class {
116783
117316
  }
116784
117317
  publish;
116785
117318
  status;
117319
+ stopped = false;
117320
+ installing = false;
116786
117321
  timer = null;
116787
117322
  deps;
116788
117323
  /** One check at a time — the timer and the button can land together */
@@ -116798,16 +117333,22 @@ var UpdateService = class {
116798
117333
  * closed while releases happened.
116799
117334
  */
116800
117335
  start() {
117336
+ this.stopped = false;
116801
117337
  if (!this.status.auto) return;
116802
117338
  void this.check(false);
116803
117339
  this.startTimer();
116804
117340
  }
117341
+ /** Shutdown (including a host swap): no asynchronous continuation may start more work. */
116805
117342
  stop() {
117343
+ this.stopped = true;
117344
+ this.stopTimer();
117345
+ }
117346
+ stopTimer() {
116806
117347
  if (this.timer) clearInterval(this.timer);
116807
117348
  this.timer = null;
116808
117349
  }
116809
117350
  startTimer() {
116810
- this.stop();
117351
+ this.stopTimer();
116811
117352
  this.timer = setInterval(() => void this.check(false), CHECK_INTERVAL_MS);
116812
117353
  this.timer.unref?.();
116813
117354
  }
@@ -116823,7 +117364,7 @@ var UpdateService = class {
116823
117364
  this.status = { ...this.status, auto: enabled };
116824
117365
  this.deps.writeAuto(enabled);
116825
117366
  if (!enabled) {
116826
- this.stop();
117367
+ this.stopTimer();
116827
117368
  this.emit();
116828
117369
  return this.current();
116829
117370
  }
@@ -116860,32 +117401,36 @@ var UpdateService = class {
116860
117401
  * failed, nothing about a background failure interrupts anyone.
116861
117402
  */
116862
117403
  async check(force) {
117404
+ if (this.stopped || this.installing) return this.current();
116863
117405
  if (this.inFlight) return this.inFlight;
116864
117406
  if (!force && !this.status.auto) return this.current();
116865
117407
  if (!force && this.status.checkedAt !== null && !this.isDue()) return this.current();
116866
117408
  if (this.status.phase === "updating" || this.status.phase === "waiting_for_npm" || this.status.phase === "restart_required") return this.current();
116867
- this.status = { ...this.status, phase: "checking" };
117409
+ const failure3 = this.status.phase === "failed" ? { error: this.status.error } : null;
117410
+ if (!failure3) this.status = { ...this.status, phase: "checking" };
116868
117411
  this.emit();
116869
- this.inFlight = this.runCheck(force);
117412
+ this.inFlight = this.runCheck(force, failure3);
116870
117413
  try {
116871
117414
  return await this.inFlight;
116872
117415
  } finally {
116873
117416
  this.inFlight = null;
116874
117417
  }
116875
117418
  }
116876
- async runCheck(force) {
117419
+ async runCheck(force, failure3) {
116877
117420
  const res = await this.deps.fetchLatest(force ? FORCED_TIMEOUT_MS : SCHEDULED_TIMEOUT_MS);
117421
+ if (this.stopped || this.installing) return this.current();
116878
117422
  if (!res.ok) {
116879
- this.status = { ...this.status, phase: "idle", error: res.reason };
117423
+ this.status = { ...this.status, phase: failure3 ? "failed" : "idle", error: failure3 ? failure3.error : res.reason };
116880
117424
  this.emit();
116881
117425
  return this.current();
116882
117426
  }
117427
+ const latest = res.version;
116883
117428
  this.status = {
116884
117429
  ...this.status,
116885
- latest: res.version,
116886
- newer: isNewerVersion(res.version, this.status.current),
116887
- phase: "idle",
116888
- error: null,
117430
+ latest,
117431
+ newer: isNewerVersion(latest, this.status.current),
117432
+ phase: failure3 ? "failed" : "idle",
117433
+ error: failure3?.error ?? null,
116889
117434
  checkedAt: this.deps.now()
116890
117435
  };
116891
117436
  this.emit();
@@ -116901,13 +117446,14 @@ var UpdateService = class {
116901
117446
  * rest of the story arrives as `update_status` events.
116902
117447
  */
116903
117448
  apply() {
116904
- if (this.status.phase === "updating" || this.status.phase === "waiting_for_npm") return this.current();
117449
+ if (this.stopped || this.installing || this.status.phase === "restart_required") return this.current();
116905
117450
  const latest = this.status.latest;
116906
117451
  if (!this.status.newer || !latest) {
116907
117452
  this.status = { ...this.status, phase: "failed", error: "There is no newer version to install" };
116908
117453
  this.emit();
116909
117454
  return this.current();
116910
117455
  }
117456
+ this.installing = true;
116911
117457
  this.status = { ...this.status, phase: "updating", error: null };
116912
117458
  this.emit();
116913
117459
  void this.runApply(latest);
@@ -116917,16 +117463,42 @@ var UpdateService = class {
116917
117463
  let packageInstalled = false;
116918
117464
  const copy = this.deps.installedCopy();
116919
117465
  try {
117466
+ await this.inFlight;
117467
+ if (this.stopped) return;
117468
+ const age2 = this.status.checkedAt === null ? Infinity : this.deps.now() - this.status.checkedAt;
117469
+ if (age2 < 0 || age2 >= INSTALL_CHECK_MAX_AGE_MS) {
117470
+ const fresh = await this.deps.fetchLatest(FORCED_TIMEOUT_MS);
117471
+ if (this.stopped) return;
117472
+ if (fresh.ok) {
117473
+ const now = this.deps.now();
117474
+ const storedAge = this.status.checkedAt === null ? Infinity : now - this.status.checkedAt;
117475
+ const keepStored = storedAge >= 0 && storedAge < STORED_RELEASE_MAX_AGE_MS && isNewerVersion(version2, fresh.version);
117476
+ if (!keepStored) version2 = fresh.version;
117477
+ this.status = { ...this.status, latest: version2, newer: isNewerVersion(version2, this.status.current), checkedAt: keepStored ? this.status.checkedAt : now };
117478
+ if (!this.status.newer) {
117479
+ this.status = { ...this.status, phase: "idle", error: null };
117480
+ return;
117481
+ }
117482
+ } else {
117483
+ this.status = { ...this.status, error: `Could not refresh latest (${fresh.reason}); using previously checked ${version2}.` };
117484
+ }
117485
+ }
117486
+ this.emit();
116920
117487
  await this.installPackage(version2);
117488
+ if (this.stopped) return;
116921
117489
  packageInstalled = true;
116922
117490
  if (copy) await this.deps.run(APP_SLUG, ["install"]);
116923
- this.status = { ...this.status, phase: "restart_required", error: null };
117491
+ if (this.stopped) return;
117492
+ this.status = { ...this.status, phase: "restart_required" };
116924
117493
  } catch (e) {
117494
+ if (this.stopped) return;
116925
117495
  const reason = e.message;
116926
117496
  const error51 = packageInstalled ? `npm installed ${version2}, but the copy at ${copy} was not replaced (${reason}). Quit ${APP_NAME}, then run "${APP_SLUG} install" in a terminal.` : reason;
116927
117497
  this.status = { ...this.status, phase: "failed", error: error51 };
117498
+ } finally {
117499
+ this.installing = false;
117500
+ if (!this.stopped) this.emit();
116928
117501
  }
116929
- this.emit();
116930
117502
  }
116931
117503
  /**
116932
117504
  * A just-published release can be visible from `latest` before its package is on the npm edge
@@ -116936,16 +117508,20 @@ var UpdateService = class {
116936
117508
  */
116937
117509
  async installPackage(version2) {
116938
117510
  const started = this.deps.now();
117511
+ const notice = this.status.error;
116939
117512
  for (; ; ) {
117513
+ if (this.stopped) return;
116940
117514
  try {
116941
117515
  await this.deps.run("npm", ["i", "-g", "--prefer-online", `${APP_SLUG}@${version2}`]);
116942
117516
  return;
116943
117517
  } catch (err) {
117518
+ if (this.stopped) return;
116944
117519
  if (!npmHasNotServedVersion(err) || this.deps.now() - started >= NPM_RETRY_WINDOW_MS) throw err;
116945
- this.status = { ...this.status, phase: "waiting_for_npm", error: `Waiting for npm to serve ${version2}` };
117520
+ this.status = { ...this.status, phase: "waiting_for_npm", error: `Waiting for npm to serve ${version2}${notice ? `. ${notice}` : ""}` };
116946
117521
  this.emit();
116947
117522
  await this.deps.wait(NPM_RETRY_DELAY_MS);
116948
- this.status = { ...this.status, phase: "updating", error: null };
117523
+ if (this.stopped) return;
117524
+ this.status = { ...this.status, phase: "updating", error: notice };
116949
117525
  this.emit();
116950
117526
  }
116951
117527
  }
@@ -117305,7 +117881,7 @@ var SessionSleeper = class {
117305
117881
  // packages/agent-host/src/dev-services/instance-lock.ts
117306
117882
  import { execFileSync as execFileSync4 } from "node:child_process";
117307
117883
  import { readFileSync as readFileSync13, unlinkSync as unlinkSync3, writeFileSync as writeFileSync7 } from "node:fs";
117308
- import { dirname as dirname10, join as join30 } from "node:path";
117884
+ import { dirname as dirname10, join as join31 } from "node:path";
117309
117885
  import Database2 from "better-sqlite3";
117310
117886
  var OWNERSHIP_FILE = "host-ownership.sqlite";
117311
117887
  var held = /* @__PURE__ */ new Set();
@@ -117339,7 +117915,7 @@ function parseHolder(raw) {
117339
117915
  function acquireInstanceLock(dbPath2, startOf = processStartTime) {
117340
117916
  if (dbPath2 === ":memory:") return { ok: true, release: () => {
117341
117917
  } };
117342
- const lockPath = join30(dirname10(dbPath2), "host.lock");
117918
+ const lockPath = join31(dirname10(dbPath2), "host.lock");
117343
117919
  const readHolder = () => {
117344
117920
  try {
117345
117921
  return parseHolder(readFileSync13(lockPath, "utf8"));
@@ -117347,7 +117923,7 @@ function acquireInstanceLock(dbPath2, startOf = processStartTime) {
117347
117923
  return null;
117348
117924
  }
117349
117925
  };
117350
- const owner = takeOwnership(join30(dirname10(dbPath2), OWNERSHIP_FILE));
117926
+ const owner = takeOwnership(join31(dirname10(dbPath2), OWNERSHIP_FILE));
117351
117927
  if (!owner) {
117352
117928
  const holder = readHolder();
117353
117929
  return { ok: false, heldByPid: holder && Number.isInteger(holder.pid) && holder.pid > 0 ? holder.pid : null, lockPath };
@@ -117668,9 +118244,9 @@ var ControlChannel = class {
117668
118244
  }
117669
118245
  next(op) {
117670
118246
  if (this.ended) return Promise.reject(new Error("the keeper closed the control channel"));
117671
- return new Promise((resolve11, reject) => {
118247
+ return new Promise((resolve12, reject) => {
117672
118248
  const list = this.waiters.get(op) ?? [];
117673
- list.push({ resolve: resolve11, reject });
118249
+ list.push({ resolve: resolve12, reject });
117674
118250
  this.waiters.set(op, list);
117675
118251
  });
117676
118252
  }
@@ -117775,7 +118351,7 @@ async function runDrain(bound, deps) {
117775
118351
  }
117776
118352
 
117777
118353
  // packages/agent-host/src/links/install.ts
117778
- import { existsSync as existsSync21, readFileSync as readFileSync14 } from "node:fs";
118354
+ import { existsSync as existsSync22, readFileSync as readFileSync14 } from "node:fs";
117779
118355
  import { hostname as hostname3 } from "node:os";
117780
118356
  import { fileURLToPath as fileURLToPath6 } from "node:url";
117781
118357
  import { gzipSync } from "node:zlib";
@@ -117836,7 +118412,7 @@ async function verifiedPackage(name, version2, opts = {}) {
117836
118412
  }
117837
118413
 
117838
118414
  // packages/agent-host/src/links/tunnel.ts
117839
- import { spawn as spawn5 } from "node:child_process";
118415
+ import { spawn as spawn6 } from "node:child_process";
117840
118416
  import { createConnection as createConnection2, createServer as createServer3 } from "node:net";
117841
118417
  function parseConnectionLine(stdout) {
117842
118418
  const lines = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
@@ -118099,8 +118675,8 @@ var SshTunnel = class {
118099
118675
  if (this.closing) throw new Error(`The link to ${this.opts.target} was closed`);
118100
118676
  }
118101
118677
  run(args, stdin) {
118102
- return new Promise((resolve11, reject) => {
118103
- const child = spawn5(this.opts.ssh ?? "ssh", args, { env: this.opts.env ?? process.env, stdio: [stdin === void 0 ? "ignore" : "pipe", "pipe", "pipe"] });
118678
+ return new Promise((resolve12, reject) => {
118679
+ const child = spawn6(this.opts.ssh ?? "ssh", args, { env: this.opts.env ?? process.env, stdio: [stdin === void 0 ? "ignore" : "pipe", "pipe", "pipe"] });
118104
118680
  if (stdin !== void 0) {
118105
118681
  child.stdin.on("error", () => {
118106
118682
  });
@@ -118116,7 +118692,7 @@ var SshTunnel = class {
118116
118692
  this.asking.delete(child);
118117
118693
  reject(e);
118118
118694
  });
118119
- child.once("close", (code2) => resolve11({ code: code2, stdout, stderr }));
118695
+ child.once("close", (code2) => resolve12({ code: code2, stdout, stderr }));
118120
118696
  });
118121
118697
  }
118122
118698
  config() {
@@ -118159,11 +118735,11 @@ var SshTunnel = class {
118159
118735
  this.opts.target,
118160
118736
  ...keepAlive ? [keepAlive] : []
118161
118737
  ];
118162
- const child = spawn5(this.opts.ssh ?? "ssh", args, { env: this.opts.env ?? process.env, stdio: ["ignore", "ignore", "pipe"] });
118738
+ const child = spawn6(this.opts.ssh ?? "ssh", args, { env: this.opts.env ?? process.env, stdio: ["ignore", "ignore", "pipe"] });
118163
118739
  this.child = child;
118164
118740
  let stderr = "";
118165
118741
  child.stderr.on("data", (d2) => stderr += String(d2));
118166
- return new Promise((resolve11, reject) => {
118742
+ return new Promise((resolve12, reject) => {
118167
118743
  let settled = false;
118168
118744
  const deadline = Date.now() + (this.opts.forwardTimeoutMs ?? 2e4);
118169
118745
  const fail4 = (message) => {
@@ -118182,7 +118758,7 @@ var SshTunnel = class {
118182
118758
  if (await accepts(localPort)) {
118183
118759
  settled = true;
118184
118760
  this.opts.log?.(`[links] forward up: 127.0.0.1:${localPort} -> ${this.opts.target}:127.0.0.1:${remotePort}`);
118185
- return resolve11();
118761
+ return resolve12();
118186
118762
  }
118187
118763
  if (Date.now() > deadline) {
118188
118764
  this.ended.add(child);
@@ -118200,12 +118776,12 @@ var SshTunnel = class {
118200
118776
  this.child = null;
118201
118777
  if (!child || child.exitCode !== null || child.signalCode !== null) return;
118202
118778
  this.ended.add(child);
118203
- await new Promise((resolve11) => {
118204
- child.once("exit", () => resolve11());
118779
+ await new Promise((resolve12) => {
118780
+ child.once("exit", () => resolve12());
118205
118781
  child.kill("SIGTERM");
118206
118782
  setTimeout(() => {
118207
118783
  child.kill("SIGKILL");
118208
- resolve11();
118784
+ resolve12();
118209
118785
  }, 2e3).unref();
118210
118786
  });
118211
118787
  }
@@ -118215,29 +118791,29 @@ function lastLine(s) {
118215
118791
  return [...lines].reverse().find((l) => /error|denied|refused|timed out|not found|cannot|could not/i.test(l)) ?? lines.at(-1) ?? "";
118216
118792
  }
118217
118793
  function accepts(port2) {
118218
- return new Promise((resolve11) => {
118794
+ return new Promise((resolve12) => {
118219
118795
  const s = createConnection2({ host: "127.0.0.1", port: port2 });
118220
118796
  s.once("connect", () => {
118221
118797
  s.destroy();
118222
- resolve11(true);
118798
+ resolve12(true);
118223
118799
  });
118224
- s.once("error", () => resolve11(false));
118800
+ s.once("error", () => resolve12(false));
118225
118801
  });
118226
118802
  }
118227
118803
  function portFree(port2) {
118228
- return new Promise((resolve11) => {
118804
+ return new Promise((resolve12) => {
118229
118805
  const probe = createServer3();
118230
- probe.once("error", () => resolve11(false));
118231
- probe.listen(port2, "127.0.0.1", () => probe.close(() => resolve11(true)));
118806
+ probe.once("error", () => resolve12(false));
118807
+ probe.listen(port2, "127.0.0.1", () => probe.close(() => resolve12(true)));
118232
118808
  });
118233
118809
  }
118234
118810
  function freePort() {
118235
- return new Promise((resolve11, reject) => {
118811
+ return new Promise((resolve12, reject) => {
118236
118812
  const probe = createServer3();
118237
118813
  probe.once("error", reject);
118238
118814
  probe.listen(0, "127.0.0.1", () => {
118239
118815
  const { port: port2 } = probe.address();
118240
- probe.close(() => resolve11(port2));
118816
+ probe.close(() => resolve12(port2));
118241
118817
  });
118242
118818
  });
118243
118819
  }
@@ -118453,7 +119029,7 @@ async function installRemote(o) {
118453
119029
  function bundled(name, source) {
118454
119030
  for (const url2 of [new URL(`./${name}`, import.meta.url), new URL(source, import.meta.url)]) {
118455
119031
  const file2 = fileURLToPath6(url2);
118456
- if (existsSync21(file2)) return readFileSync14(file2, "utf8");
119032
+ if (existsSync22(file2)) return readFileSync14(file2, "utf8");
118457
119033
  }
118458
119034
  throw new Error(`${name} is missing from this host`);
118459
119035
  }
@@ -118511,7 +119087,7 @@ async function removeAutostart(c) {
118511
119087
  }
118512
119088
 
118513
119089
  // packages/agent-host/src/links/probe.ts
118514
- import { spawn as spawn6 } from "node:child_process";
119090
+ import { spawn as spawn7 } from "node:child_process";
118515
119091
  var PROBE_MARK = "CENTRALU-PROBE";
118516
119092
  var PROBE_PS = [
118517
119093
  "$ErrorActionPreference = 'SilentlyContinue'",
@@ -118556,7 +119132,7 @@ function probeTarget(target, opts = {}) {
118556
119132
  return Promise.reject(Object.assign(new Error("Not an ssh target: use a host alias from ~/.ssh/config, or user@host"), { code: "internal" }));
118557
119133
  }
118558
119134
  const args = [...opts.configFile ? ["-F", opts.configFile] : [], "-T", "-o", "BatchMode=yes", "-o", "ConnectTimeout=10", "--", t, probeCommand()];
118559
- return new Promise((resolve11) => {
119135
+ return new Promise((resolve12) => {
118560
119136
  let stdout = "";
118561
119137
  let stderr = "";
118562
119138
  let done = false;
@@ -118564,9 +119140,9 @@ function probeTarget(target, opts = {}) {
118564
119140
  if (done) return;
118565
119141
  done = true;
118566
119142
  clearTimeout(timer);
118567
- resolve11(parseProbe(t, run));
119143
+ resolve12(parseProbe(t, run));
118568
119144
  };
118569
- const child = spawn6(opts.ssh ?? "ssh", args, { env: opts.env ?? process.env, stdio: ["ignore", "pipe", "pipe"] });
119145
+ const child = spawn7(opts.ssh ?? "ssh", args, { env: opts.env ?? process.env, stdio: ["ignore", "pipe", "pipe"] });
118570
119146
  const timer = setTimeout(() => {
118571
119147
  child.kill("SIGTERM");
118572
119148
  finish2({ code: 255, stdout: "", stderr: `ssh to ${t} did not answer in time` });
@@ -118579,10 +119155,10 @@ function probeTarget(target, opts = {}) {
118579
119155
  }
118580
119156
 
118581
119157
  // packages/agent-host/src/links/ssh-config.ts
118582
- import { spawn as spawn7 } from "node:child_process";
119158
+ import { spawn as spawn8 } from "node:child_process";
118583
119159
  import { readdirSync as readdirSync8, readFileSync as readFileSync15, statSync as statSync13 } from "node:fs";
118584
119160
  import { homedir as homedir13 } from "node:os";
118585
- import { isAbsolute as isAbsolute10, join as join31, normalize, parse as parse6, sep as sep12 } from "node:path";
119161
+ import { isAbsolute as isAbsolute10, join as join32, normalize, parse as parse6, sep as sep12 } from "node:path";
118586
119162
  var MAX_DEPTH2 = 16;
118587
119163
  function splitConfigLine(raw) {
118588
119164
  const m = /^\s*([^\s=#]+)\s*(?:=\s*)?([\s\S]*)$/.exec(raw);
@@ -118648,7 +119224,7 @@ function expandGlob(path2) {
118648
119224
  const next = [];
118649
119225
  for (const base2 of found) {
118650
119226
  if (!MAGIC.test(seg)) {
118651
- next.push(join31(base2, seg));
119227
+ next.push(join32(base2, seg));
118652
119228
  continue;
118653
119229
  }
118654
119230
  let names;
@@ -118660,7 +119236,7 @@ function expandGlob(path2) {
118660
119236
  const re2 = segmentRe(seg);
118661
119237
  for (const name of names.sort()) {
118662
119238
  if (name.startsWith(".") && !seg.startsWith(".")) continue;
118663
- if (re2.test(name)) next.push(join31(base2, name));
119239
+ if (re2.test(name)) next.push(join32(base2, name));
118664
119240
  }
118665
119241
  }
118666
119242
  found = next;
@@ -118715,11 +119291,11 @@ function readSshConfig(opts) {
118715
119291
  }
118716
119292
  for (const arg of split2.args) {
118717
119293
  let path2 = arg;
118718
- if (path2.startsWith("~/")) path2 = join31(opts.home, path2.slice(2));
119294
+ if (path2.startsWith("~/")) path2 = join32(opts.home, path2.slice(2));
118719
119295
  else if (path2.startsWith("~")) {
118720
119296
  notes.push(`An Include of another user's folder (${where(file2, n + 1)}) was not read`);
118721
119297
  continue;
118722
- } else if (!isAbsolute10(path2)) path2 = join31(opts.home, ".ssh", path2);
119298
+ } else if (!isAbsolute10(path2)) path2 = join32(opts.home, ".ssh", path2);
118723
119299
  for (const f of expandGlob(path2)) read(f, depth + 1);
118724
119300
  }
118725
119301
  }
@@ -118742,7 +119318,7 @@ function parseSshG(stdout) {
118742
119318
  return { hostName: first.get("hostname") || null, user: first.get("user") || null, port: Number.isInteger(port2) && port2 > 0 ? port2 : null };
118743
119319
  }
118744
119320
  function resolveAlias(alias, opts = {}) {
118745
- return new Promise((resolve11) => {
119321
+ return new Promise((resolve12) => {
118746
119322
  const args = [...opts.configFile ? ["-F", opts.configFile] : [], "-G", "--", alias];
118747
119323
  let stdout = "";
118748
119324
  let stderr = "";
@@ -118751,10 +119327,10 @@ function resolveAlias(alias, opts = {}) {
118751
119327
  if (done) return;
118752
119328
  done = true;
118753
119329
  clearTimeout(timer);
118754
- resolve11(r);
119330
+ resolve12(r);
118755
119331
  };
118756
119332
  const none = (error51) => ({ hostName: null, user: null, port: null, error: error51 });
118757
- const child = spawn7(opts.ssh ?? "ssh", args, { env: opts.env ?? process.env, stdio: ["ignore", "pipe", "pipe"] });
119333
+ const child = spawn8(opts.ssh ?? "ssh", args, { env: opts.env ?? process.env, stdio: ["ignore", "pipe", "pipe"] });
118758
119334
  const timer = setTimeout(() => {
118759
119335
  child.kill("SIGKILL");
118760
119336
  finish2(none("ssh -G did not answer"));
@@ -118769,18 +119345,18 @@ var MAX_HOSTS = 100;
118769
119345
  var RESOLVE_AT_ONCE = 8;
118770
119346
  async function listSshHosts(opts = {}) {
118771
119347
  const home = opts.home ?? homedir13();
118772
- const file2 = opts.configFile ?? join31(home, ".ssh", "config");
119348
+ const file2 = opts.configFile ?? join32(home, ".ssh", "config");
118773
119349
  const read = readSshConfig({ file: file2, home });
118774
119350
  const notes = [...read.notes];
118775
119351
  const listed = read.hosts.slice(0, MAX_HOSTS);
118776
119352
  if (read.hosts.length > MAX_HOSTS) notes.push(`Only the first ${MAX_HOSTS} of ${read.hosts.length} hosts are listed; type any other below`);
118777
- const resolve11 = opts.resolve ?? ((alias) => resolveAlias(alias, opts));
119353
+ const resolve12 = opts.resolve ?? ((alias) => resolveAlias(alias, opts));
118778
119354
  const resolved = new Array(listed.length);
118779
119355
  let next = 0;
118780
119356
  const worker = async () => {
118781
119357
  while (next < listed.length) {
118782
119358
  const i = next++;
118783
- resolved[i] = await resolve11(listed[i].alias);
119359
+ resolved[i] = await resolve12(listed[i].alias);
118784
119360
  }
118785
119361
  };
118786
119362
  await Promise.all(Array.from({ length: Math.min(RESOLVE_AT_ONCE, listed.length) }, worker));
@@ -119269,11 +119845,11 @@ var RemoteClient = class {
119269
119845
  if (!this.ready || !this.ws) return Promise.reject(linkError("The machine is not reachable right now", true));
119270
119846
  const ws2 = this.ws;
119271
119847
  const id2 = String(this.nextId++);
119272
- return new Promise((resolve11, reject) => {
119848
+ return new Promise((resolve12, reject) => {
119273
119849
  const timer = setTimeout(() => {
119274
119850
  if (this.pending.delete(id2)) reject(Object.assign(new Error(`The machine did not answer ${method} in time`), { code: "internal", retryable: true }));
119275
119851
  }, this.opts.callTimeoutMs ?? DEFAULT_CALL_TIMEOUT_MS);
119276
- this.pending.set(id2, { resolve: resolve11, reject, timer });
119852
+ this.pending.set(id2, { resolve: resolve12, reject, timer });
119277
119853
  ws2.send(JSON.stringify({ kind: "rpc", id: id2, method, params }));
119278
119854
  });
119279
119855
  }
@@ -119636,14 +120212,14 @@ var LinkedMachine = class {
119636
120212
  if (this.client?.connected) return this.client.call("host.activity", {}).then(read, () => null);
119637
120213
  const e = this.endpoint;
119638
120214
  if (!e?.url || this.versions?.compatible === false || this.held) return Promise.resolve(null);
119639
- return new Promise((resolve11) => {
120215
+ return new Promise((resolve12) => {
119640
120216
  let settled = false;
119641
120217
  const done = (v) => {
119642
120218
  if (settled) return;
119643
120219
  settled = true;
119644
120220
  clearTimeout(timer);
119645
120221
  client.close();
119646
- resolve11(v);
120222
+ resolve12(v);
119647
120223
  };
119648
120224
  const client = new RemoteClient(
119649
120225
  { ...this.deps.client, url: e.url, token: e.token, handshakeTimeoutMs: 5e3, callTimeoutMs: 5e3 },
@@ -119717,16 +120293,16 @@ var LinkedMachine = class {
119717
120293
  const started = await this.tunnel.startHost();
119718
120294
  this.hostStarted = { ...started, at: Date.now() };
119719
120295
  },
119720
- answers: (version2, ms3) => new Promise((resolve11) => {
120296
+ answers: (version2, ms3) => new Promise((resolve12) => {
119721
120297
  const waiter = (v) => {
119722
120298
  if (v !== version2) return;
119723
120299
  clearTimeout(timer);
119724
120300
  this.helloWaiters.delete(waiter);
119725
- resolve11(true);
120301
+ resolve12(true);
119726
120302
  };
119727
120303
  const timer = setTimeout(() => {
119728
120304
  this.helloWaiters.delete(waiter);
119729
- void hold().then(() => resolve11(false));
120305
+ void hold().then(() => resolve12(false));
119730
120306
  }, this.deps.checkMs ?? ms3);
119731
120307
  this.helloWaiters.add(waiter);
119732
120308
  this.held = false;
@@ -120875,7 +121451,7 @@ function storeMirror(store2) {
120875
121451
  var start = new StartSequence();
120876
121452
  start.at("launch variables");
120877
121453
  var launch = takeLaunchEnv();
120878
- var BUILD = true ? "c6afa2e" : "dev";
121454
+ var BUILD = true ? "8dd78b6" : "dev";
120879
121455
  var { values } = parseArgs({
120880
121456
  options: {
120881
121457
  port: { type: "string", default: "5175" },
@@ -120896,12 +121472,12 @@ start.at("data folder");
120896
121472
  var movedNote = null;
120897
121473
  function defaultDbPath() {
120898
121474
  const isDev = process.env.CC_DEV === "1";
120899
- const dir = join32(homedir14(), isDev ? DATA_DIR_DEV : DATA_DIR);
120900
- const legacy = join32(homedir14(), isDev ? DATA_DIR_LEGACY.dev : DATA_DIR_LEGACY.prod);
121475
+ const dir = join33(homedir14(), isDev ? DATA_DIR_DEV : DATA_DIR);
121476
+ const legacy = join33(homedir14(), isDev ? DATA_DIR_LEGACY.dev : DATA_DIR_LEGACY.prod);
120901
121477
  movedNote = migrateLegacyDataDir(legacy, dir) ? `[agent-host] data folder moved: ${legacy} -> ${dir}` : null;
120902
121478
  mkdirSync11(dir, { recursive: true });
120903
121479
  process.env.CC_DATA_DIR = dir;
120904
- return join32(dir, "store.db");
121480
+ return join33(dir, "store.db");
120905
121481
  }
120906
121482
  var dbPath = values.memory ? ":memory:" : values.db ?? defaultDbPath();
120907
121483
  start.at("log");
@@ -120979,7 +121555,7 @@ var mgr = new SessionManager(
120979
121555
  },
120980
121556
  (sessionId) => bridgeAddress(launch.frontDoor, port, token, sessionId),
120981
121557
  // Worktrees are created next to the data folder — dev and the packaged app never touch each other's worktrees
120982
- join32(dirname11(dbPath), "worktrees"),
121558
+ join33(dirname11(dbPath), "worktrees"),
120983
121559
  held2 ? { processes: held2.processes, keptSessions: held2.kept.sessionIds } : {}
120984
121560
  );
120985
121561
  mgr.useProjectsHome(() => projectsHome());
@@ -121013,7 +121589,7 @@ if (held2) {
121013
121589
  }
121014
121590
  var AUTO_UPDATE_CHECK_KEY = "updates.auto";
121015
121591
  var AUTO_APPLY_UPDATES_KEY = "updates.autoApply";
121016
- var themes = new ThemeFiles(join32(dataRoot(), "themes"), () => events.push({ type: "themes_changed" }));
121592
+ var themes = new ThemeFiles(join33(dataRoot(), "themes"), () => events.push({ type: "themes_changed" }));
121017
121593
  await themes.start().catch((e) => console.error(`[themes] the themes folder is unavailable: ${e.message}`));
121018
121594
  var updates = new UpdateService((status) => events.push({ type: "update_status", status }), {
121019
121595
  // On by default when nothing has been saved yet. Since the check is read-only and every failure
@@ -121232,7 +121808,7 @@ var endings = wireEndings({
121232
121808
  heldChildren: held2 !== null,
121233
121809
  leave: (mode, handOver) => void shutdown.leave(mode, handOver),
121234
121810
  force: (why) => shutdown.force(why),
121235
- record: crashRecorder(join32(dirname11(dbPath), "host-errors.log"), (line) => console.error(line)),
121811
+ record: crashRecorder(join33(dirname11(dbPath), "host-errors.log"), (line) => console.error(line)),
121236
121812
  stdin: process.stdin,
121237
121813
  watchParent: values["watch-parent"] === true,
121238
121814
  log: (line) => console.error(line)