@centralu/win32-x64 0.1.0-beta.19 → 0.1.0-beta.21

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.
@@ -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.19";
42968
+ var APP_VERSION = "0.1.0-beta.21";
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
@@ -58249,6 +58251,8 @@ var AdapterCapabilities = external_exports.object({
58249
58251
  * The idle check (#297) needs that difference — a session whose background work is invisible is never called idle.
58250
58252
  */
58251
58253
  backgroundTasks: external_exports.boolean().default(false),
58254
+ /** Whether send accepts input during a running turn without interrupting it (#592). Absent is unsupported. */
58255
+ midTurnInput: external_exports.boolean().optional(),
58252
58256
  /**
58253
58257
  * Whether this tool can defer individual Centralu tool definitions until the model asks for
58254
58258
  * them. This is a serving property, not a list of tool names: the host uses it when it counts
@@ -58531,7 +58535,9 @@ var UiPreferences = external_exports.object({
58531
58535
  * preference, but this is the one record the person's settings already live in; the host reads
58532
58536
  * it when a session starts and on each call.
58533
58537
  */
58534
- 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()
58535
58541
  });
58536
58542
  var UiPreferencesPatch = UiPreferences.partial();
58537
58543
  var DEFAULT_UI_PREFERENCES = {
@@ -59625,14 +59631,9 @@ var SessionInfo = external_exports.object({
59625
59631
  */
59626
59632
  parentSessionId: external_exports.string().nullable().default(null),
59627
59633
  /**
59628
- * The session that asked for this one through ask_project (#371 part B) — a session in another project. Null for
59629
- * every session a person or an app started.
59630
- *
59631
- * The delegated session is an ordinary session of its own project, so the person can watch it and step in; this
59632
- * line is what marks it "asked by" and links back to the caller. On the row (store v45), so the mark survives a
59633
- * restart, and the caller's next ask reuses the session it already asked. The caller may since have been deleted:
59634
- * the id then names a session that is no longer listed. Optional rather than defaulted so a frame or a fixture
59635
- * 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.
59636
59637
  */
59637
59638
  askedBy: external_exports.string().nullable().optional(),
59638
59639
  ...machineField,
@@ -59837,6 +59838,14 @@ var MessageImage = external_exports.discriminatedUnion("ok", [
59837
59838
  external_exports.object({ ok: external_exports.literal(true), mime: external_exports.string(), data: external_exports.string(), file: external_exports.string().optional() }),
59838
59839
  external_exports.object({ ok: external_exports.literal(false), reason: MessageImageRefusal, message: external_exports.string(), file: external_exports.string().optional() })
59839
59840
  ]);
59841
+ var AppHomeViewResult = external_exports.object({
59842
+ instanceId: external_exports.string(),
59843
+ tool: external_exports.string(),
59844
+ resourceUri: external_exports.string(),
59845
+ toolInput: external_exports.record(external_exports.string(), external_exports.unknown()),
59846
+ toolResult: external_exports.looseObject({ content: external_exports.array(external_exports.record(external_exports.string(), external_exports.unknown())) }),
59847
+ runId: external_exports.string()
59848
+ });
59840
59849
  var RpcMethods = {
59841
59850
  "agents.createSession": { params: CreateSessionParams, result: SessionInfo },
59842
59851
  "agents.send": {
@@ -60433,7 +60442,8 @@ var RpcMethods = {
60433
60442
  * tool-result).
60434
60443
  *
60435
60444
  * If there is no `home`, or that tool did not declare a screen, or the app cannot be reached
60436
- * (not trusted, or failed to start), this fails with a reason and leaves no instance behind. If
60445
+ * (not trusted, or failed to start), this fails with a reason and leaves no live screen behind. A
60446
+ * keyed open keeps only a failure reservation during the lease grace period, without holding the app. If
60437
60447
  * the app itself answered with a failure, this still opens — drawing a failure is also the
60438
60448
  * screen's job.
60439
60449
  *
@@ -60442,21 +60452,16 @@ var RpcMethods = {
60442
60452
  * machine. Absent: this host's own app, as before.
60443
60453
  */
60444
60454
  "apps.openView": {
60455
+ // openKey: one client-chosen UUID per logical open. Repeats reuse the instance and home outcome,
60456
+ // including after a planned host handover, so a lost answer never runs the home tool twice.
60445
60457
  /*
60446
60458
  * `lease` (#392, additive): the view belongs to this window's connection. Once the connection ends, the host waits
60447
60459
  * a few minutes for a window to claim it (`apps.holdViews`) and then closes it, so a reloaded or closed window does
60448
60460
  * not hold its apps open for good. Without it (an older window, or a linked machine's view) the view lives until
60449
60461
  * `apps.closeView`.
60450
60462
  */
60451
- params: external_exports.object({ appId: AppId, projectId: ProjectId.nullable(), ...machineParam, lease: external_exports.boolean().optional() }),
60452
- result: external_exports.object({
60453
- instanceId: external_exports.string(),
60454
- tool: external_exports.string(),
60455
- resourceUri: external_exports.string(),
60456
- toolInput: external_exports.record(external_exports.string(), external_exports.unknown()),
60457
- toolResult: external_exports.looseObject({ content: external_exports.array(external_exports.record(external_exports.string(), external_exports.unknown())) }),
60458
- runId: external_exports.string()
60459
- })
60463
+ params: external_exports.object({ appId: AppId, projectId: ProjectId.nullable(), ...machineParam, lease: external_exports.boolean().optional(), openKey: external_exports.string().uuid().optional() }),
60464
+ result: AppHomeViewResult
60460
60465
  },
60461
60466
  /**
60462
60467
  * Closes the pinned screen (M4 B-2). Releases the app the instance was holding on to — an app
@@ -60467,6 +60472,11 @@ var RpcMethods = {
60467
60472
  params: external_exports.object({ instanceId: external_exports.string() }),
60468
60473
  result: external_exports.object({ ok: external_exports.literal(true) })
60469
60474
  },
60475
+ /** Reclaims a keyed home open after losing its answer; never calls the home tool. */
60476
+ "apps.recoverView": {
60477
+ params: external_exports.object({ appId: AppId, projectId: ProjectId.nullable(), ...machineParam, lease: external_exports.boolean().optional(), openKey: external_exports.string().uuid() }),
60478
+ result: AppHomeViewResult
60479
+ },
60470
60480
  /**
60471
60481
  * A window claims the pinned screens it shows (#392), after every reconnect: each leased instance
60472
60482
  * named becomes this connection's again and stops waiting to be closed. `missing` lists the ids
@@ -61749,12 +61759,12 @@ var KeeperAgentProcess = class _KeeperAgentProcess extends EventEmitter {
61749
61759
  await Promise.race([this.ready, new Promise((r) => setTimeout(r, DETACH_WAIT_MS))]);
61750
61760
  const out = this.out;
61751
61761
  if (!out) return;
61752
- await new Promise((resolve11) => {
61753
- if (out.readableEnded || out.destroyed) return resolve11();
61754
- 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);
61755
61765
  const done = () => {
61756
61766
  clearTimeout(t);
61757
- resolve11();
61767
+ resolve12();
61758
61768
  };
61759
61769
  out.once("end", done);
61760
61770
  out.once("close", done);
@@ -61771,7 +61781,7 @@ import { createConnection } from "node:net";
61771
61781
  var CHILDREN_PROTOCOL = 1;
61772
61782
  var REQUEST_TIMEOUT_MS = 15e3;
61773
61783
  function readAnswer(sock, timeoutMs) {
61774
- return new Promise((resolve11, reject) => {
61784
+ return new Promise((resolve12, reject) => {
61775
61785
  let buf = Buffer.alloc(0);
61776
61786
  const done = (err, v) => {
61777
61787
  clearTimeout(timer);
@@ -61779,7 +61789,7 @@ function readAnswer(sock, timeoutMs) {
61779
61789
  sock.off("error", onError);
61780
61790
  sock.off("close", onClose);
61781
61791
  if (err) reject(err);
61782
- else resolve11(v);
61792
+ else resolve12(v);
61783
61793
  };
61784
61794
  const onData = (d2) => {
61785
61795
  buf = Buffer.concat([buf, d2]);
@@ -61808,7 +61818,7 @@ function readAnswer(sock, timeoutMs) {
61808
61818
  });
61809
61819
  }
61810
61820
  function connect(path2, first, timeoutMs) {
61811
- return new Promise((resolve11, reject) => {
61821
+ return new Promise((resolve12, reject) => {
61812
61822
  const sock = createConnection(path2);
61813
61823
  const onError = (e) => reject(e);
61814
61824
  sock.once("error", onError);
@@ -61822,7 +61832,7 @@ function connect(path2, first, timeoutMs) {
61822
61832
  sock.destroy();
61823
61833
  return reject(new Error(String(answer.error ?? "the keeper refused")));
61824
61834
  }
61825
- resolve11({ sock, answer });
61835
+ resolve12({ sock, answer });
61826
61836
  },
61827
61837
  (e) => {
61828
61838
  sock.destroy();
@@ -61908,12 +61918,12 @@ var KeeperChildren = class _KeeperChildren extends EventEmitter2 {
61908
61918
  request(op, body = {}) {
61909
61919
  if (this.closed) return Promise.reject(new Error("the connection to the keeper was lost"));
61910
61920
  const rid = this.nextRid++;
61911
- return new Promise((resolve11, reject) => {
61921
+ return new Promise((resolve12, reject) => {
61912
61922
  const timer = setTimeout(() => {
61913
61923
  if (this.pending.delete(rid)) reject(new Error(`the keeper did not answer ${op}`));
61914
61924
  }, REQUEST_TIMEOUT_MS);
61915
61925
  timer.unref();
61916
- this.pending.set(rid, { resolve: resolve11, reject, timer });
61926
+ this.pending.set(rid, { resolve: resolve12, reject, timer });
61917
61927
  this.sock.write(`${JSON.stringify({ ...body, op, rid })}
61918
61928
  `);
61919
61929
  });
@@ -62094,12 +62104,12 @@ var KeeperPty = class _KeeperPty {
62094
62104
  await Promise.race([this.ready, new Promise((r) => setTimeout(r, 1e3))]);
62095
62105
  const sock = this.sock;
62096
62106
  if (!sock) return;
62097
- await new Promise((resolve11) => {
62098
- if (sock.readableEnded || sock.destroyed) return resolve11();
62099
- const t = setTimeout(resolve11, 1e3);
62107
+ await new Promise((resolve12) => {
62108
+ if (sock.readableEnded || sock.destroyed) return resolve12();
62109
+ const t = setTimeout(resolve12, 1e3);
62100
62110
  const done = () => {
62101
62111
  clearTimeout(t);
62102
- resolve11();
62112
+ resolve12();
62103
62113
  };
62104
62114
  sock.once("end", done);
62105
62115
  sock.once("close", done);
@@ -62462,7 +62472,7 @@ var Drain = class {
62462
62472
  */
62463
62473
  track(label, run) {
62464
62474
  if (this.refusing) return Promise.reject(new DrainCut(REFUSED_MESSAGE));
62465
- return new Promise((resolve11, reject) => {
62475
+ return new Promise((resolve12, reject) => {
62466
62476
  let settled = false;
62467
62477
  const settle = () => {
62468
62478
  if (settled) return false;
@@ -62474,7 +62484,7 @@ var Drain = class {
62474
62484
  const entry = { label, cut: (e) => settle() && reject(e) };
62475
62485
  this.inflight.add(entry);
62476
62486
  Promise.resolve().then(run).then(
62477
- (v) => settle() && resolve11(v),
62487
+ (v) => settle() && resolve12(v),
62478
62488
  (e) => settle() && reject(e)
62479
62489
  );
62480
62490
  });
@@ -62623,7 +62633,7 @@ var HostServer = class _HostServer {
62623
62633
  return this.allowedOrigins.has(origin);
62624
62634
  }
62625
62635
  listen() {
62626
- return new Promise((resolve11, reject) => {
62636
+ return new Promise((resolve12, reject) => {
62627
62637
  const onError = (err) => {
62628
62638
  this.listenError = null;
62629
62639
  if (err.code === "EADDRINUSE") {
@@ -62640,7 +62650,7 @@ var HostServer = class _HostServer {
62640
62650
  this.listenError = null;
62641
62651
  this.http.removeListener("error", onError);
62642
62652
  const addr = this.http.address();
62643
- resolve11(typeof addr === "object" && addr ? addr.port : this.opts.port);
62653
+ resolve12(typeof addr === "object" && addr ? addr.port : this.opts.port);
62644
62654
  });
62645
62655
  });
62646
62656
  }
@@ -63017,15 +63027,15 @@ function buildViewCsp(csp) {
63017
63027
  const frame = sanitizeDomains(csp?.frameDomains);
63018
63028
  const base2 = sanitizeDomains(csp?.baseUriDomains);
63019
63029
  const r = resource.kept.join(" ");
63020
- const join33 = (...parts) => parts.filter(Boolean).join(" ");
63030
+ const join34 = (...parts) => parts.filter(Boolean).join(" ");
63021
63031
  const policy = [
63022
63032
  "default-src 'none'",
63023
- `script-src ${join33("'unsafe-inline'", r)}`,
63024
- `style-src ${join33("'unsafe-inline'", r)}`,
63025
- `img-src ${join33("data: blob:", r)}`,
63026
- `font-src ${join33("data:", r)}`,
63027
- `media-src ${join33("data: blob:", r)}`,
63028
- `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)}`,
63029
63039
  `connect-src ${connect2.kept.length ? connect2.kept.join(" ") : "'none'"}`,
63030
63040
  `frame-src ${frame.kept.length ? frame.kept.join(" ") : "'none'"}`,
63031
63041
  "form-action 'none'",
@@ -63175,6 +63185,7 @@ var ViewHost = class _ViewHost {
63175
63185
  }
63176
63186
  opts;
63177
63187
  instances = /* @__PURE__ */ new Map();
63188
+ openingHomes = /* @__PURE__ */ new Map();
63178
63189
  origins = /* @__PURE__ */ new Map();
63179
63190
  /** Origin servers being closed, by key: one started again for the same key waits for its port to be free */
63180
63191
  closingOrigins = /* @__PURE__ */ new Map();
@@ -63212,6 +63223,42 @@ var ViewHost = class _ViewHost {
63212
63223
  this.instances.set(id2, { id: id2, app: ref, uri, doc: null, release, lease });
63213
63224
  return { instanceId: id2 };
63214
63225
  }
63226
+ /** A logical home open may run its tool once, even when its answer is lost in a swap. */
63227
+ homeOpen(app2, key, holder) {
63228
+ const inst = [...this.instances.values()].find((i) => i.home?.key === key);
63229
+ if (!inst) return null;
63230
+ if (!sameApp(inst.app, app2)) fail("This open key belongs to another app");
63231
+ if (inst.home.outcome) {
63232
+ if (holder !== void 0) this.hold([inst.id], holder);
63233
+ return Promise.resolve(inst.home.outcome);
63234
+ }
63235
+ if (inst.home.error) return Promise.reject(new Error(inst.home.error));
63236
+ const opening = this.openingHomes.get(inst.id);
63237
+ if (opening) {
63238
+ if (holder !== void 0) this.hold([inst.id], holder);
63239
+ return opening;
63240
+ }
63241
+ return Promise.reject(new Error(
63242
+ "Host restarted while the home tool was running \u2014 the app may or may not have done this; check before opening again"
63243
+ ));
63244
+ }
63245
+ rememberHome(instanceId, key, work) {
63246
+ const inst = this.instances.get(instanceId);
63247
+ inst.home = { key };
63248
+ const opening = work().then((outcome) => {
63249
+ inst.home.outcome = outcome;
63250
+ return outcome;
63251
+ }, (e) => {
63252
+ inst.home.error = e instanceof Error ? e.message : String(e);
63253
+ inst.release?.();
63254
+ inst.release = null;
63255
+ inst.lease = { holder: null, orphanSince: Date.now() };
63256
+ this.scheduleSweep();
63257
+ throw e;
63258
+ }).finally(() => this.openingHomes.delete(instanceId));
63259
+ this.openingHomes.set(instanceId, opening);
63260
+ return opening;
63261
+ }
63215
63262
  close(instanceId) {
63216
63263
  this.drop(instanceId);
63217
63264
  }
@@ -63265,7 +63312,7 @@ var ViewHost = class _ViewHost {
63265
63312
  }
63266
63313
  /** Every open instance, oldest first — what a planned hand-over records (view-handover.ts) */
63267
63314
  list() {
63268
- return [...this.instances.values()].map((i) => ({ id: i.id, app: { ...i.app }, uri: i.uri, ...i.lease ? { leased: true } : {} }));
63315
+ return [...this.instances.values()].map((i) => ({ id: i.id, app: { ...i.app }, uri: i.uri, ...i.lease ? { leased: true } : {}, ...i.home ? { home: i.home } : {} }));
63269
63316
  }
63270
63317
  /**
63271
63318
  * Opens again, **under their old ids**, the instances a previous host handed over (#280: a build
@@ -63292,8 +63339,22 @@ var ViewHost = class _ViewHost {
63292
63339
  this.log(`[agent-host] view ${v.uri} (${_ViewHost.originKey(ref)}) not restored: ${err.message}`);
63293
63340
  continue;
63294
63341
  }
63295
- const lease = v.leased === true ? { holder: null, orphanSince: now } : null;
63296
- this.instances.set(v.id, { id: v.id, app: ref, uri: v.uri, doc: null, release, lease });
63342
+ let lease = v.leased === true ? { holder: null, orphanSince: now } : null;
63343
+ let home;
63344
+ if (v.home && typeof v.home.key === "string" && RpcMethods["apps.openView"].params.shape.openKey.safeParse(v.home.key).success) {
63345
+ const outcome = RpcMethods["apps.openView"].result.safeParse(v.home.outcome);
63346
+ home = {
63347
+ key: v.home.key,
63348
+ ...outcome.success && outcome.data.instanceId === v.id ? { outcome: outcome.data } : {},
63349
+ ...typeof v.home.error === "string" ? { error: v.home.error } : {}
63350
+ };
63351
+ }
63352
+ if (home && !home.outcome) {
63353
+ release?.();
63354
+ release = null;
63355
+ lease = { holder: null, orphanSince: now };
63356
+ }
63357
+ this.instances.set(v.id, { id: v.id, app: ref, uri: v.uri, doc: null, release, lease, ...home ? { home } : {} });
63297
63358
  opened.push(v.id);
63298
63359
  }
63299
63360
  if (opened.some((id2) => this.instances.get(id2)?.lease)) this.scheduleSweep();
@@ -68676,14 +68737,14 @@ function inputRequiredRoundsExceededMessage(method, maxRounds) {
68676
68737
  return `Multi-round-trip request '${method}' still required input after ${maxRounds} rounds (inputRequired.maxRounds)`;
68677
68738
  }
68678
68739
  function sleep(ms3, signal) {
68679
- return new Promise((resolve11, reject) => {
68740
+ return new Promise((resolve12, reject) => {
68680
68741
  if (signal?.aborted) {
68681
68742
  reject(signal.reason instanceof SdkError ? signal.reason : new SdkError(SdkErrorCode.RequestTimeout, String(signal.reason)));
68682
68743
  return;
68683
68744
  }
68684
68745
  const timer = setTimeout(() => {
68685
68746
  signal?.removeEventListener("abort", onAbort);
68686
- resolve11();
68747
+ resolve12();
68687
68748
  }, ms3);
68688
68749
  const onAbort = () => {
68689
68750
  clearTimeout(timer);
@@ -69522,7 +69583,7 @@ var Protocol = class {
69522
69583
  const flowStartedAt = Date.now();
69523
69584
  let onAbort;
69524
69585
  let cleanupMessageId;
69525
- return new Promise((resolve11, reject) => {
69586
+ return new Promise((resolve12, reject) => {
69526
69587
  const earlyReject = (error51) => {
69527
69588
  reject(error51);
69528
69589
  };
@@ -69591,7 +69652,7 @@ var Protocol = class {
69591
69652
  }
69592
69653
  if (decoded.kind === "invalid") return reject(decoded.error);
69593
69654
  if (decoded.kind === "input_required") {
69594
- if (options?.allowInputRequired === true) return resolve11(manualInputRequiredValue(decoded));
69655
+ if (options?.allowInputRequired === true) return resolve12(manualInputRequiredValue(decoded));
69595
69656
  const flow = {
69596
69657
  codec: codec2,
69597
69658
  request,
@@ -69603,11 +69664,11 @@ var Protocol = class {
69603
69664
  params
69604
69665
  }, resultSchema, legOptions)
69605
69666
  };
69606
- return resolve11(this._resolveNonCompleteResult(decoded, flow));
69667
+ return resolve12(this._resolveNonCompleteResult(decoded, flow));
69607
69668
  }
69608
69669
  const result = decoded.result;
69609
69670
  validateStandardSchema(resultSchema, result).then((parseResult) => {
69610
- if (parseResult.success) resolve11(parseResult.data);
69671
+ if (parseResult.success) resolve12(parseResult.data);
69611
69672
  else reject(new SdkError(SdkErrorCode.InvalidResult, `Invalid result for ${request.method}: ${parseResult.error}`));
69612
69673
  }, reject);
69613
69674
  });
@@ -72530,7 +72591,7 @@ var require_compile = /* @__PURE__ */ __commonJSMin(((exports) => {
72530
72591
  ref = (0, resolve_1.resolveUrl)(this.opts.uriResolver, baseId, ref);
72531
72592
  const schOrFunc = root3.refs[ref];
72532
72593
  if (schOrFunc) return schOrFunc;
72533
- let _sch = resolve11.call(this, root3, ref);
72594
+ let _sch = resolve12.call(this, root3, ref);
72534
72595
  if (_sch === void 0) {
72535
72596
  const schema = (_a4 = root3.localRefs) === null || _a4 === void 0 ? void 0 : _a4[ref];
72536
72597
  const { schemaId } = this.opts;
@@ -72556,7 +72617,7 @@ var require_compile = /* @__PURE__ */ __commonJSMin(((exports) => {
72556
72617
  function sameSchemaEnv(s12, s22) {
72557
72618
  return s12.schema === s22.schema && s12.root === s22.root && s12.baseId === s22.baseId;
72558
72619
  }
72559
- function resolve11(root3, ref) {
72620
+ function resolve12(root3, ref) {
72560
72621
  let sch;
72561
72622
  while (typeof (sch = this.refs[ref]) == "string") ref = sch;
72562
72623
  return sch || this.schemas[ref] || resolveSchema.call(this, root3, ref);
@@ -73006,7 +73067,7 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
73006
73067
  else if (typeof uri === "object") uri = parse7(serialize(uri, options), options);
73007
73068
  return uri;
73008
73069
  }
73009
- function resolve11(baseURI, relativeURI, options) {
73070
+ function resolve12(baseURI, relativeURI, options) {
73010
73071
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
73011
73072
  const resolved = resolveComponent(parse7(baseURI, schemelessOptions), parse7(relativeURI, schemelessOptions), schemelessOptions, true);
73012
73073
  schemelessOptions.skipEscape = true;
@@ -73180,7 +73241,7 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
73180
73241
  const fastUri = {
73181
73242
  SCHEMES,
73182
73243
  normalize: normalize2,
73183
- resolve: resolve11,
73244
+ resolve: resolve12,
73184
73245
  resolveComponent,
73185
73246
  equal,
73186
73247
  serialize,
@@ -77795,14 +77856,14 @@ var ProbeWindow = class ProbeWindow2 {
77795
77856
  */
77796
77857
  async exchange(buildRequest, timeoutMs) {
77797
77858
  const id2 = `server-discover-probe-${++this._probeCounter}`;
77798
- return new Promise((resolve11) => {
77859
+ return new Promise((resolve12) => {
77799
77860
  let settled = false;
77800
77861
  const settle = (reply) => {
77801
77862
  if (settled) return;
77802
77863
  settled = true;
77803
77864
  clearTimeout(timer);
77804
77865
  if (this._pending?.id === id2) this._pending = void 0;
77805
- resolve11(reply);
77866
+ resolve12(reply);
77806
77867
  };
77807
77868
  const timer = setTimeout(() => settle({ kind: "timeout" }), timeoutMs);
77808
77869
  this._pending = {
@@ -79008,13 +79069,13 @@ var Client = class extends Protocol {
79008
79069
  let onCallerAbort;
79009
79070
  let resolveOpening;
79010
79071
  let rejectOpening;
79011
- const opening = new Promise((resolve11, reject) => {
79012
- resolveOpening = resolve11;
79072
+ const opening = new Promise((resolve12, reject) => {
79073
+ resolveOpening = resolve12;
79013
79074
  rejectOpening = reject;
79014
79075
  });
79015
79076
  let resolveClosed;
79016
- const closed = new Promise((resolve11) => {
79017
- resolveClosed = resolve11;
79077
+ const closed = new Promise((resolve12) => {
79078
+ resolveClosed = resolve12;
79018
79079
  });
79019
79080
  const settle = (outcome) => {
79020
79081
  if (state === "closed") return;
@@ -79786,8 +79847,8 @@ var StreamTransport = class {
79786
79847
  }
79787
79848
  send(message) {
79788
79849
  if (this.closed) return Promise.reject(new Error("app connection closed"));
79789
- return new Promise((resolve11, reject) => {
79790
- this.writable.write(serializeMessage(message), (err) => err ? reject(err) : resolve11());
79850
+ return new Promise((resolve12, reject) => {
79851
+ this.writable.write(serializeMessage(message), (err) => err ? reject(err) : resolve12());
79791
79852
  });
79792
79853
  }
79793
79854
  /** Closes only our own state. Closing the pipes is the shutdown rule's job (AppProcess.stop). */
@@ -79999,12 +80060,12 @@ ${tail}` : head;
79999
80060
  }
80000
80061
  waitExit(ms3) {
80001
80062
  if (!this.alive) return Promise.resolve(true);
80002
- return new Promise((resolve11) => {
80003
- const t = setTimeout(() => resolve11(false), ms3);
80063
+ return new Promise((resolve12) => {
80064
+ const t = setTimeout(() => resolve12(false), ms3);
80004
80065
  t.unref();
80005
80066
  this.exitWaiters.push(() => {
80006
80067
  clearTimeout(t);
80007
- resolve11(true);
80068
+ resolve12(true);
80008
80069
  });
80009
80070
  });
80010
80071
  }
@@ -83591,14 +83652,14 @@ function inputRequiredRoundsExceededMessage2(method, maxRounds) {
83591
83652
  return `Multi-round-trip request '${method}' still required input after ${maxRounds} rounds (inputRequired.maxRounds)`;
83592
83653
  }
83593
83654
  function sleep2(ms3, signal) {
83594
- return new Promise((resolve11, reject) => {
83655
+ return new Promise((resolve12, reject) => {
83595
83656
  if (signal?.aborted) {
83596
83657
  reject(signal.reason instanceof SdkError2 ? signal.reason : new SdkError2(SdkErrorCode2.RequestTimeout, String(signal.reason)));
83597
83658
  return;
83598
83659
  }
83599
83660
  const timer = setTimeout(() => {
83600
83661
  signal?.removeEventListener("abort", onAbort);
83601
- resolve11();
83662
+ resolve12();
83602
83663
  }, ms3);
83603
83664
  const onAbort = () => {
83604
83665
  clearTimeout(timer);
@@ -84392,7 +84453,7 @@ var Protocol2 = class {
84392
84453
  const flowStartedAt = Date.now();
84393
84454
  let onAbort;
84394
84455
  let cleanupMessageId;
84395
- return new Promise((resolve11, reject) => {
84456
+ return new Promise((resolve12, reject) => {
84396
84457
  const earlyReject = (error51) => {
84397
84458
  reject(error51);
84398
84459
  };
@@ -84461,7 +84522,7 @@ var Protocol2 = class {
84461
84522
  }
84462
84523
  if (decoded.kind === "invalid") return reject(decoded.error);
84463
84524
  if (decoded.kind === "input_required") {
84464
- if (options?.allowInputRequired === true) return resolve11(manualInputRequiredValue2(decoded));
84525
+ if (options?.allowInputRequired === true) return resolve12(manualInputRequiredValue2(decoded));
84465
84526
  const flow = {
84466
84527
  codec: codec2,
84467
84528
  request,
@@ -84473,11 +84534,11 @@ var Protocol2 = class {
84473
84534
  params
84474
84535
  }, resultSchema, legOptions)
84475
84536
  };
84476
- return resolve11(this._resolveNonCompleteResult(decoded, flow));
84537
+ return resolve12(this._resolveNonCompleteResult(decoded, flow));
84477
84538
  }
84478
84539
  const result = decoded.result;
84479
84540
  validateStandardSchema2(resultSchema, result).then((parseResult) => {
84480
- if (parseResult.success) resolve11(parseResult.data);
84541
+ if (parseResult.success) resolve12(parseResult.data);
84481
84542
  else reject(new SdkError2(SdkErrorCode2.InvalidResult, `Invalid result for ${request.method}: ${parseResult.error}`));
84482
84543
  }, reject);
84483
84544
  });
@@ -87389,7 +87450,7 @@ var require_compile2 = /* @__PURE__ */ __commonJSMin2(((exports) => {
87389
87450
  ref = (0, resolve_1.resolveUrl)(this.opts.uriResolver, baseId, ref);
87390
87451
  const schOrFunc = root3.refs[ref];
87391
87452
  if (schOrFunc) return schOrFunc;
87392
- let _sch = resolve11.call(this, root3, ref);
87453
+ let _sch = resolve12.call(this, root3, ref);
87393
87454
  if (_sch === void 0) {
87394
87455
  const schema = (_a4 = root3.localRefs) === null || _a4 === void 0 ? void 0 : _a4[ref];
87395
87456
  const { schemaId } = this.opts;
@@ -87415,7 +87476,7 @@ var require_compile2 = /* @__PURE__ */ __commonJSMin2(((exports) => {
87415
87476
  function sameSchemaEnv(s12, s22) {
87416
87477
  return s12.schema === s22.schema && s12.root === s22.root && s12.baseId === s22.baseId;
87417
87478
  }
87418
- function resolve11(root3, ref) {
87479
+ function resolve12(root3, ref) {
87419
87480
  let sch;
87420
87481
  while (typeof (sch = this.refs[ref]) == "string") ref = sch;
87421
87482
  return sch || this.schemas[ref] || resolveSchema.call(this, root3, ref);
@@ -87865,7 +87926,7 @@ var require_fast_uri2 = /* @__PURE__ */ __commonJSMin2(((exports, module) => {
87865
87926
  else if (typeof uri === "object") uri = parse7(serialize(uri, options), options);
87866
87927
  return uri;
87867
87928
  }
87868
- function resolve11(baseURI, relativeURI, options) {
87929
+ function resolve12(baseURI, relativeURI, options) {
87869
87930
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
87870
87931
  const resolved = resolveComponent(parse7(baseURI, schemelessOptions), parse7(relativeURI, schemelessOptions), schemelessOptions, true);
87871
87932
  schemelessOptions.skipEscape = true;
@@ -88039,7 +88100,7 @@ var require_fast_uri2 = /* @__PURE__ */ __commonJSMin2(((exports, module) => {
88039
88100
  const fastUri = {
88040
88101
  SCHEMES,
88041
88102
  normalize: normalize2,
88042
- resolve: resolve11,
88103
+ resolve: resolve12,
88043
88104
  resolveComponent,
88044
88105
  equal,
88045
88106
  serialize,
@@ -93472,7 +93533,7 @@ var StdioServerTransport = class {
93472
93533
  }
93473
93534
  send(message) {
93474
93535
  if (this._closed) return Promise.reject(/* @__PURE__ */ new Error("StdioServerTransport is closed"));
93475
- return new Promise((resolve11, reject) => {
93536
+ return new Promise((resolve12, reject) => {
93476
93537
  const json2 = serializeMessage2(message);
93477
93538
  let settled = false;
93478
93539
  const onError = (error51) => {
@@ -93487,14 +93548,14 @@ var StdioServerTransport = class {
93487
93548
  settled = true;
93488
93549
  this._stdout.off("error", onError);
93489
93550
  this._stdout.off("drain", onDrain2);
93490
- resolve11();
93551
+ resolve12();
93491
93552
  };
93492
93553
  this._stdout.once("error", onError);
93493
93554
  if (this._stdout.write(json2)) {
93494
93555
  if (settled) return;
93495
93556
  settled = true;
93496
93557
  this._stdout.off("error", onError);
93497
- resolve11();
93558
+ resolve12();
93498
93559
  } else if (!settled) this._stdout.once("drain", onDrain2);
93499
93560
  });
93500
93561
  }
@@ -93548,14 +93609,14 @@ var StdioConnectionChannel = class {
93548
93609
  */
93549
93610
  async whenRequestsAnswered(timeoutMs) {
93550
93611
  if (this._closed || this._pendingRequests.size === 0) return true;
93551
- return await new Promise((resolve11) => {
93612
+ return await new Promise((resolve12) => {
93552
93613
  const waiter = () => {
93553
93614
  clearTimeout(timer);
93554
- resolve11(true);
93615
+ resolve12(true);
93555
93616
  };
93556
93617
  const timer = setTimeout(() => {
93557
93618
  this._drainWaiters = this._drainWaiters.filter((pending) => pending !== waiter);
93558
- resolve11(false);
93619
+ resolve12(false);
93559
93620
  }, timeoutMs);
93560
93621
  this._drainWaiters.push(waiter);
93561
93622
  });
@@ -94468,7 +94529,7 @@ var BrokerDesk = class {
94468
94529
  try {
94469
94530
  answer = await Promise.race([
94470
94531
  asked.answer,
94471
- new Promise((resolve11) => call2.signal.aborted ? resolve11(null) : call2.signal.addEventListener("abort", () => resolve11(null), { once: true }))
94532
+ new Promise((resolve12) => call2.signal.aborted ? resolve12(null) : call2.signal.addEventListener("abort", () => resolve12(null), { once: true }))
94472
94533
  ]);
94473
94534
  } finally {
94474
94535
  call2.signal.removeEventListener("abort", leave);
@@ -96392,6 +96453,11 @@ var ExternalApps = class {
96392
96453
  }
96393
96454
  return out;
96394
96455
  }
96456
+ sessionBoundaryOpen = () => false;
96457
+ /** Host-owned policy; a request cannot claim an auto preset in its arguments. */
96458
+ setSessionBoundaryPolicy(policy) {
96459
+ this.sessionBoundaryOpen = policy;
96460
+ }
96395
96461
  /**
96396
96462
  * An app's tool list — if the app is down, **this is where it starts** (the first time it is
96397
96463
  * needed).
@@ -96400,9 +96466,9 @@ var ExternalApps = class {
96400
96466
  * read is the place it is enforced: this list becomes the source of truth both for what attaches to
96401
96467
  * a session (A-5) and for the tools a screen can call.
96402
96468
  */
96403
- async tools(ref, audience) {
96469
+ async tools(ref, audience, sessionId) {
96404
96470
  const e = this.require(ref);
96405
- const all = await this.use(e, async () => e.life.tools ?? []);
96471
+ const all = await this.use(e, async () => e.life.tools ?? [], !!sessionId && this.sessionBoundaryOpen(sessionId));
96406
96472
  return all.filter((t) => !audience || t.visibility.includes(audience)).map((t) => t.tool);
96407
96473
  }
96408
96474
  /**
@@ -96534,7 +96600,8 @@ var ExternalApps = class {
96534
96600
  return { runId, status, result, error: error51, durationMs };
96535
96601
  };
96536
96602
  if (!e.manifest) return done("rejected", null, `This app's manifest is invalid: ${e.error}`);
96537
- if (!e.scope.trusted) return done("rejected", null, "This app's project is not trusted, so Centralu does not call its apps");
96603
+ const open5 = caller.kind === "session" && this.sessionBoundaryOpen(caller.sessionId);
96604
+ if (!e.scope.trusted && !open5) return done("rejected", null, "This app's project is not trusted, so Centralu does not call its apps");
96538
96605
  const held3 = this.held(e);
96539
96606
  if (held3) return done("rejected", null, held3);
96540
96607
  if (e.life.gaveUp) return done("rejected", null, `This app stopped after failing ${this.timing.maxFailures} times in a row`);
@@ -96582,7 +96649,7 @@ var ExternalApps = class {
96582
96649
  abort.abort();
96583
96650
  for (const sig of upstream) sig.removeEventListener("abort", onUp);
96584
96651
  }
96585
- });
96652
+ }, open5);
96586
96653
  } catch (err) {
96587
96654
  return done("error", null, err.message);
96588
96655
  }
@@ -97186,11 +97253,11 @@ var ExternalApps = class {
97186
97253
  }
97187
97254
  // ── Lifecycle ──────────────────────────────────────────────────────────────────────
97188
97255
  /** An app is never idle while in use — the idle timer is set again once it is done */
97189
- async use(e, fn2) {
97256
+ async use(e, fn2, boundaryOpen = false) {
97190
97257
  e.life.inflight += 1;
97191
97258
  this.clearIdle(e);
97192
97259
  try {
97193
- return await fn2(await this.ensureRunning(e));
97260
+ return await fn2(await this.ensureRunning(e, boundaryOpen));
97194
97261
  } finally {
97195
97262
  e.life.inflight -= 1;
97196
97263
  if (e.life.inflight === 0) for (const w of e.life.idleWaiters.splice(0)) w();
@@ -97204,14 +97271,14 @@ var ExternalApps = class {
97204
97271
  */
97205
97272
  drain(e, ms3) {
97206
97273
  if (e.life.inflight === 0) return Promise.resolve(true);
97207
- return new Promise((resolve11) => {
97274
+ return new Promise((resolve12) => {
97208
97275
  const waiter = () => {
97209
97276
  clearTimeout(t);
97210
- resolve11(true);
97277
+ resolve12(true);
97211
97278
  };
97212
97279
  const t = setTimeout(() => {
97213
97280
  e.life.idleWaiters = e.life.idleWaiters.filter((w) => w !== waiter);
97214
- resolve11(false);
97281
+ resolve12(false);
97215
97282
  }, ms3);
97216
97283
  t.unref();
97217
97284
  e.life.idleWaiters.push(waiter);
@@ -97225,10 +97292,10 @@ var ExternalApps = class {
97225
97292
  * encountered restoring sessions (see the `resuming` comment in `manager.ts`). Everyone waits on the
97226
97293
  * same in-progress startup promise instead.
97227
97294
  */
97228
- ensureRunning(e) {
97295
+ ensureRunning(e, boundaryOpen = false) {
97229
97296
  const L2 = e.life;
97230
97297
  if (!e.manifest) throw new AppUnavailableError(`The app cannot start \u2014 its manifest is invalid: ${e.error}`);
97231
- if (!e.scope.trusted) {
97298
+ if (!e.scope.trusted && !boundaryOpen) {
97232
97299
  throw new AppUnavailableError("This app's project is not trusted, so the app does not start \u2014 trust the project and it will");
97233
97300
  }
97234
97301
  const held3 = this.held(e);
@@ -98030,12 +98097,12 @@ function readBook(raw) {
98030
98097
  return book;
98031
98098
  }
98032
98099
  function listenOn(port2, handler) {
98033
- return new Promise((resolve11, reject) => {
98100
+ return new Promise((resolve12, reject) => {
98034
98101
  const server2 = createServer2(handler);
98035
98102
  server2.once("error", reject);
98036
98103
  server2.listen(port2, "127.0.0.1", () => {
98037
98104
  server2.removeListener("error", reject);
98038
- resolve11(server2);
98105
+ resolve12(server2);
98039
98106
  });
98040
98107
  });
98041
98108
  }
@@ -98103,8 +98170,155 @@ var OriginPorts = class {
98103
98170
  }
98104
98171
  };
98105
98172
 
98173
+ // packages/agent-host/src/dev-services/worktree-recovery.ts
98174
+ import { randomUUID as randomUUID6 } from "node:crypto";
98175
+ import { existsSync as existsSync13 } from "node:fs";
98176
+ import { chmod, lstat as lstat3, mkdir as mkdir2, readFile, readdir as readdir2, realpath as realpath3, rm as rm2, stat as stat3, writeFile as writeFile2 } from "node:fs/promises";
98177
+ import { join as join16, resolve as resolve2 } from "node:path";
98178
+ var WORKTREE_RECOVERY_MAX_BYTES = 256 * 1024 * 1024;
98179
+ var RECOVERY_TIMEOUT = 12e4;
98180
+ function worktreeRecoveryDir(sessionId) {
98181
+ if (!isSessionId(sessionId)) throw new Error("Invalid session id for worktree recovery");
98182
+ return join16(dataRoot(), "worktree-recovery", sessionId);
98183
+ }
98184
+ async function clearWorktreeRecovery(sessionId) {
98185
+ await rm2(worktreeRecoveryDir(sessionId), { recursive: true, force: true });
98186
+ }
98187
+ async function saveWorktreeRecovery(sessionId, cwd, trunk, branch) {
98188
+ const plans = [];
98189
+ let bytes2 = 0;
98190
+ const account = (size) => {
98191
+ bytes2 += size;
98192
+ if (bytes2 > WORKTREE_RECOVERY_MAX_BYTES) {
98193
+ 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.`);
98194
+ }
98195
+ };
98196
+ const prepare = async (repo, path2) => {
98197
+ const tip = (await runGit(repo, ["rev-parse", "--verify", "HEAD"])).trim();
98198
+ const refs = ["HEAD"];
98199
+ if (!path2 && branch) {
98200
+ const ref = `refs/heads/${branch}`;
98201
+ if (await runGit(repo, ["rev-parse", "--verify", ref]).then(() => true).catch(() => false)) refs.push(ref);
98202
+ } else if (path2) {
98203
+ refs.push(...(await runGit(repo, ["for-each-ref", "--format=%(refname)", "refs/heads/"])).trim().split("\n").filter(Boolean));
98204
+ }
98205
+ const baselineOid = !path2 ? await runGit(repo, ["rev-parse", "--verify", `${trunk}^{commit}`]).then((s) => s.trim()).catch(() => null) : null;
98206
+ const exclude = path2 ? ["--not", "--remotes"] : baselineOid ? ["--not", baselineOid] : [];
98207
+ const commits = Number((await runGit(repo, ["rev-list", "--count", ...refs, ...exclude])).trim());
98208
+ plans.push({ repo, path: path2, tip, refs, commits });
98209
+ const stagedNames = new Set((await runGit(repo, ["diff", "--cached", "--name-only", "-z", "--no-renames", tip], { timeout: RECOVERY_TIMEOUT })).split("\0"));
98210
+ const modules = [];
98211
+ const stagedObjects = [];
98212
+ for (const row of (await runGit(repo, ["ls-files", "--stage", "-z"])).split("\0")) {
98213
+ if (!row) continue;
98214
+ const tab = row.indexOf(" ");
98215
+ const rel = row.slice(tab + 1);
98216
+ if (row.startsWith("160000 ")) modules.push(rel);
98217
+ else if (stagedNames.has(rel)) stagedObjects.push(row.split(" ")[1]);
98218
+ }
98219
+ if (stagedObjects.length) {
98220
+ const sizes = await runGit(repo, ["cat-file", "--batch-check=%(objectsize)"], { input: stagedObjects.join("\n") + "\n" });
98221
+ for (const size of sizes.trim().split("\n")) {
98222
+ if (!/^\d+$/.test(size)) throw new Error("Could not measure staged recovery data");
98223
+ account(Number(size));
98224
+ }
98225
+ }
98226
+ const files = new Set((await runGit(repo, ["ls-files", "--cached", "--others", "--exclude-standard", "-z"])).split("\0").filter(Boolean));
98227
+ for (const rel of files) {
98228
+ const file2 = await lstat3(join16(repo, rel)).catch((e) => {
98229
+ if (e.code !== "ENOENT") throw e;
98230
+ return null;
98231
+ });
98232
+ if (file2?.isFile() || file2?.isSymbolicLink()) account(file2.size);
98233
+ else if (file2?.isDirectory() && existsSync13(join16(repo, rel, ".git"))) modules.push(rel);
98234
+ }
98235
+ for (const rel of new Set(modules)) {
98236
+ await assertExistingPath(repo, rel).catch((e) => {
98237
+ if (existsSync13(join16(repo, rel))) throw e;
98238
+ });
98239
+ if (existsSync13(join16(repo, rel, ".git"))) await prepare(join16(repo, rel), path2 ? `${path2}/${rel}` : rel);
98240
+ }
98241
+ };
98242
+ await prepare(cwd, "");
98243
+ const snapshotId = randomUUID6();
98244
+ const dir = join16(worktreeRecoveryDir(sessionId), snapshotId);
98245
+ await mkdir2(dir, { recursive: true, mode: 448 });
98246
+ const snapshot2 = { snapshot: snapshotId, tip: plans[0].tip, changedFiles: 0, commits: 0, repositories: [] };
98247
+ const patchFile = async (repo, tip, file2, env) => {
98248
+ await runGit(repo, ["diff", "--cached", "--binary", "--full-index", "--no-renames", `--output=${file2}`, tip], { env, timeout: RECOVERY_TIMEOUT });
98249
+ await chmod(file2, 384);
98250
+ if ((await stat3(file2)).size > 0) return true;
98251
+ await rm2(file2);
98252
+ return false;
98253
+ };
98254
+ try {
98255
+ for (const [n, plan] of plans.entries()) {
98256
+ const { repo, path: path2, tip, refs, commits } = plan;
98257
+ const entry = { path: path2, tip, refs, patch: false, staged: false, bundle: commits > 0 };
98258
+ snapshot2.repositories.push(entry);
98259
+ snapshot2.commits += commits;
98260
+ if (entry.bundle) await runGit(repo, ["bundle", "create", join16(dir, `${n}.bundle`), ...refs], { timeout: RECOVERY_TIMEOUT });
98261
+ entry.staged = await patchFile(repo, tip, join16(dir, `${n}.staged.patch`));
98262
+ const stagedNames = await runGit(repo, ["diff", "--cached", "--name-only", "-z", "--no-renames", tip], { timeout: RECOVERY_TIMEOUT });
98263
+ const env = { GIT_INDEX_FILE: join16(dir, `${n}.index`) };
98264
+ try {
98265
+ await runGit(repo, ["read-tree", tip], { env });
98266
+ await runGit(repo, ["add", "-A", "--", "."], { env, timeout: RECOVERY_TIMEOUT });
98267
+ const names = await runGit(repo, ["diff", "--cached", "--name-only", "-z", "--no-renames", tip], { env, timeout: RECOVERY_TIMEOUT });
98268
+ snapshot2.changedFiles += new Set([...names.split("\0"), ...stagedNames.split("\0")].filter(Boolean)).size;
98269
+ entry.patch = await patchFile(repo, tip, join16(dir, `${n}.patch`), env);
98270
+ } finally {
98271
+ await rm2(env.GIT_INDEX_FILE, { force: true });
98272
+ }
98273
+ }
98274
+ await writeFile2(join16(dir, "snapshot.json"), JSON.stringify(snapshot2), { mode: 384 });
98275
+ return { snapshot: snapshotId, tip: snapshot2.tip, changedFiles: snapshot2.changedFiles, commits: snapshot2.commits };
98276
+ } catch (e) {
98277
+ await rm2(dir, { recursive: true, force: true });
98278
+ throw e;
98279
+ }
98280
+ }
98281
+ async function worktreeRecoveryBytes(sessionId) {
98282
+ const dir = worktreeRecoveryDir(sessionId);
98283
+ if (!existsSync13(dir)) return 0;
98284
+ const sizes = await Promise.all((await readdir2(dir, { recursive: true })).map(async (name) => {
98285
+ const s = await stat3(join16(dir, name));
98286
+ return s.isFile() ? s.size : 0;
98287
+ }));
98288
+ return sizes.reduce((a, b2) => a + b2, 0);
98289
+ }
98290
+ async function restoreWorktreeRecovery(sessionId, repo, path2, branch, recovery, trust) {
98291
+ if (!isSessionId(recovery.snapshot)) throw new Error("Invalid recovery snapshot");
98292
+ const dir = join16(worktreeRecoveryDir(sessionId), recovery.snapshot);
98293
+ const snapshot2 = JSON.parse(await readFile(join16(dir, "snapshot.json"), "utf8"));
98294
+ const root3 = snapshot2.repositories[0];
98295
+ if (!existsSync13(path2)) {
98296
+ if (await runGit(repo, ["show-ref", "--verify", `refs/heads/${branch}`]).then(() => true).catch(() => false)) {
98297
+ branch = `recovered/${sessionId}`;
98298
+ }
98299
+ if (root3.bundle) await runGit(repo, ["fetch", join16(dir, "0.bundle"), ...root3.refs ?? ["HEAD"]], { allowRepoPrograms: trust.trusted === true });
98300
+ await runGit(repo, ["worktree", "add", "-b", branch, path2, recovery.tip], { allowRepoPrograms: trust.trusted === true });
98301
+ if (root3.patch || root3.staged) {
98302
+ try {
98303
+ if (root3.staged) await runGit(path2, ["apply", "--cached", "--binary", join16(dir, "0.staged.patch")]);
98304
+ if (root3.patch) await runGit(path2, ["apply", "--binary", join16(dir, "0.patch")]);
98305
+ } catch {
98306
+ return { branch, notice: `Restored branch ${branch}. 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.` };
98307
+ }
98308
+ }
98309
+ } else {
98310
+ const common = async (cwd) => realpath3(resolve2(cwd, (await runGit(cwd, ["rev-parse", "--git-common-dir"])).trim()));
98311
+ if (await common(path2) !== await common(repo)) throw new Error(`The worktree path belongs to another repository. Recovery files remain at ${dir}`);
98312
+ branch = (await runGit(path2, ["symbolic-ref", "--quiet", "--short", "HEAD"])).trim();
98313
+ }
98314
+ return {
98315
+ branch,
98316
+ notice: `Restored branch ${branch}. 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.`
98317
+ };
98318
+ }
98319
+
98106
98320
  // packages/agent-host/src/sessions/manager.ts
98107
- import { randomUUID as randomUUID7 } from "node:crypto";
98321
+ import { randomUUID as randomUUID10 } from "node:crypto";
98108
98322
 
98109
98323
  // packages/agent-host/src/sessions/tool-cost.ts
98110
98324
  import { createHash as createHash8 } from "node:crypto";
@@ -98422,8 +98636,8 @@ var READER_TOOLS = [
98422
98636
  var DELEGATE_TOOLS = [
98423
98637
  {
98424
98638
  name: "ask_project",
98425
- 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.",
98426
- schema: external_exports.object({ project: external_exports.string(), task: external_exports.string().optional() })
98639
+ description: "Asks another project's existing session; returns its answer. For another project's apps, use find_apps.",
98640
+ schema: external_exports.object({ project: external_exports.string(), session: external_exports.string().optional(), task: external_exports.string().optional() })
98427
98641
  }
98428
98642
  ];
98429
98643
  var DELEGATE_BUDGET_CHARS = 400;
@@ -98547,7 +98761,7 @@ function askProjectText(r) {
98547
98761
  const where = `the session "${r.sessionName}" [${r.sessionId}] in ${r.project}`;
98548
98762
  if (r.state === "working") {
98549
98763
  return {
98550
- 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.`
98764
+ 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.`
98551
98765
  };
98552
98766
  }
98553
98767
  const lines = [`${r.project} answered (from ${where}), as JSON:`, trustedJsonText(r.answer || "(no final text)")];
@@ -98567,7 +98781,7 @@ function agentRunText(r) {
98567
98781
  return { text: `Stopped ${who}.` };
98568
98782
  case "deleted":
98569
98783
  return {
98570
- 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.`
98784
+ 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."
98571
98785
  };
98572
98786
  case "at_rest":
98573
98787
  return {
@@ -98603,7 +98817,8 @@ async function runOrchestratorTool(tools, name, args, caller = { sessionId: null
98603
98817
  const project2 = typeof args.project === "string" ? args.project.trim() : "";
98604
98818
  if (!project2) return { text: "Give a project \u2014 the name of another Centralu project.", isError: true };
98605
98819
  const task = typeof args.task === "string" && args.task.trim() ? args.task : void 0;
98606
- return askProjectText(await tools.askProject({ project: project2, ...task ? { task } : {} }, signal));
98820
+ const session2 = typeof args.session === "string" && args.session.trim() ? args.session.trim() : void 0;
98821
+ return askProjectText(await tools.askProject({ project: project2, ...session2 ? { session: session2 } : {}, ...task ? { task } : {} }, signal));
98607
98822
  }
98608
98823
  if (name === "start_agent" || name === "agent_session") {
98609
98824
  if (caller.profile !== "reader") return { text: `Not a tool of this session: ${name}`, isError: true };
@@ -99015,7 +99230,7 @@ function shortHash(value) {
99015
99230
 
99016
99231
  // packages/agent-host/src/sessions/orchestrator-home.ts
99017
99232
  import { mkdirSync as mkdirSync7 } from "node:fs";
99018
- import { join as join16 } from "node:path";
99233
+ import { join as join17 } from "node:path";
99019
99234
  var preamble = true;
99020
99235
  function setOrchestratorPreamble(on2) {
99021
99236
  preamble = on2;
@@ -99031,7 +99246,7 @@ function orchestratorRole() {
99031
99246
  ${ORCHESTRATOR_ROLE}` : ORCHESTRATOR_ROLE;
99032
99247
  }
99033
99248
  function orchestratorHome() {
99034
- const dir = join16(dataRoot(), "orchestrator");
99249
+ const dir = join17(dataRoot(), "orchestrator");
99035
99250
  mkdirSync7(dir, { recursive: true });
99036
99251
  return dir;
99037
99252
  }
@@ -99579,7 +99794,7 @@ var SessionAppsHub = class {
99579
99794
  * keep answering "yes" even after trust is revoked.
99580
99795
  */
99581
99796
  refsFor(session2) {
99582
- const usable = this.rt.list().filter((a) => !UNUSABLE.has(a.status));
99797
+ const usable = this.rt.list().filter((a) => !UNUSABLE.has(a.status) || a.status === "untrusted" && !!this.opts.boundaryOpen?.(session2.id));
99583
99798
  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));
99584
99799
  const taken = new Set(hits.map((h) => h.server));
99585
99800
  for (const x of this.opts.onDemand?.attached(session2.id) ?? []) {
@@ -99606,8 +99821,8 @@ var SessionAppsHub = class {
99606
99821
  const own = session2?.id ? this.opts.onDemand?.attached(session2.id).find((x) => sameRef(x.ref, ref)) : void 0;
99607
99822
  const onDemand = !!session2 && !!own && this.opts.onDemand.allowed(session2, app2);
99608
99823
  if (!session2 || !givesApp(session2, app2) && !onDemand) return { reachable: false, reason: "other-project" };
99609
- if (app2.status === "untrusted") return { reachable: false, reason: "untrusted" };
99610
- if (UNUSABLE.has(app2.status)) return { reachable: false, reason: "app-unusable", status: app2.status };
99824
+ if (app2.status === "untrusted" && !this.opts.boundaryOpen?.(session2.id)) return { reachable: false, reason: "untrusted" };
99825
+ if (UNUSABLE.has(app2.status) && !(app2.status === "untrusted" && this.opts.boundaryOpen?.(session2.id))) return { reachable: false, reason: "app-unusable", status: app2.status };
99611
99826
  const server2 = givesApp(session2, app2) ? appMcpServerName(app2.appId) : own.server;
99612
99827
  const attached = live?.appAttachment?.(server2) ?? "attached";
99613
99828
  if (attached === "restart") return { reachable: false, reason: "restart" };
@@ -99689,7 +99904,7 @@ var Attachment2 = class {
99689
99904
  let timer;
99690
99905
  try {
99691
99906
  const listed = await Promise.race([
99692
- this.hub.rt.tools(hit.ref, "model"),
99907
+ this.hub.rt.tools(hit.ref, "model", this.session.id),
99693
99908
  new Promise((_, reject) => {
99694
99909
  timer = setTimeout(() => reject(new Error(`did not list its tools within ${Math.round(waitMs / 1e3)}s`)), waitMs);
99695
99910
  })
@@ -99734,7 +99949,7 @@ var Attachment2 = class {
99734
99949
  let timer;
99735
99950
  const first = await Promise.race([
99736
99951
  pending.then((o) => ({ o })),
99737
- new Promise((resolve11) => timer = setTimeout(() => resolve11(null), opts.waitMs))
99952
+ new Promise((resolve12) => timer = setTimeout(() => resolve12(null), opts.waitMs))
99738
99953
  ]);
99739
99954
  clearTimeout(timer);
99740
99955
  if (first) return toResult(first.o);
@@ -99840,15 +100055,15 @@ The call has not stopped and carries on. Do not call it again: pass this id as r
99840
100055
  if (i !== -1) return Promise.resolve(this.noted.splice(i, 1)[0].callId);
99841
100056
  if (this.closed) return Promise.resolve(null);
99842
100057
  const waitMs = this.hub.opts.callJoinWaitMs ?? CALL_JOIN_WAIT_MS;
99843
- return new Promise((resolve11) => {
100058
+ return new Promise((resolve12) => {
99844
100059
  const waiter = {
99845
100060
  server: server2,
99846
100061
  tool,
99847
100062
  args: key,
99848
- resolve: resolve11,
100063
+ resolve: resolve12,
99849
100064
  timer: setTimeout(() => {
99850
100065
  this.waiters = this.waiters.filter((x) => x !== waiter);
99851
- resolve11(null);
100066
+ resolve12(null);
99852
100067
  }, waitMs)
99853
100068
  };
99854
100069
  waiter.timer.unref?.();
@@ -99959,12 +100174,12 @@ var AppAccess = class {
99959
100174
  }
99960
100175
  allowed(session2, app2) {
99961
100176
  const from = session2.projectId;
99962
- if (from === null || !this.projectTrusted(from)) return false;
100177
+ if (from === null || !this.projectTrusted(from) && !this.deps.boundaryOpen?.(session2.id)) return false;
99963
100178
  if (app2.projectId === null) return true;
99964
100179
  if (app2.projectId === from || app2.shared !== true) return false;
99965
100180
  const r = this.records(session2.id).find((x) => x.appId === app2.appId && x.projectId === app2.projectId);
99966
100181
  if (!r) return false;
99967
- return r.consent === "once" || !!this.deps.store.getProjectConsent(from, app2.projectId, "apps");
100182
+ return !!this.deps.boundaryOpen?.(session2.id) || r.consent === "once" || !!this.deps.store.getProjectConsent(from, app2.projectId, "apps");
99968
100183
  }
99969
100184
  // ── The three tools ──
99970
100185
  find(sessionId, query) {
@@ -99987,7 +100202,7 @@ var AppAccess = class {
99987
100202
  const app2 = named.app;
99988
100203
  const target = { projectId: app2.projectId, appId: app2.appId };
99989
100204
  const has = already.find((h) => h.ref.appId === target.appId && h.ref.projectId === target.projectId);
99990
- if (has) return { ok: true, server: has.server, tools: await this.toolNames(target), when: "now", already: true };
100205
+ if (has) return { ok: true, server: has.server, tools: await this.toolNames(target, sessionId), when: "now", already: true };
99991
100206
  if (!this.candidates(session2).some((a) => a.appId === app2.appId && a.projectId === app2.projectId)) {
99992
100207
  return { ok: false, error: this.whyNot(session2, app2) };
99993
100208
  }
@@ -100000,7 +100215,7 @@ var AppAccess = class {
100000
100215
  signal
100001
100216
  );
100002
100217
  if (!res.ok) return res;
100003
- consent = this.deps.store.getProjectConsent(session2.projectId, app2.projectId, "apps") ? "always" : "once";
100218
+ consent = this.deps.boundaryOpen?.(sessionId) ? "auto" : this.deps.store.getProjectConsent(session2.projectId, app2.projectId, "apps") ? "always" : "once";
100004
100219
  const again = this.gate(sessionId);
100005
100220
  if (!again.ok) return again;
100006
100221
  if (!this.candidates(again.session).some((a) => a.appId === app2.appId && a.projectId === app2.projectId)) {
@@ -100012,7 +100227,7 @@ var AppAccess = class {
100012
100227
  const server2 = this.freeServer(app2.appId, taken);
100013
100228
  this.save(sessionId, [...kept, { projectId: app2.projectId, appId: app2.appId, server: server2, consent, at: Date.now() }]);
100014
100229
  const when = this.deps.attachedChanged(sessionId);
100015
- return { ok: true, server: server2, tools: await this.toolNames(target), when };
100230
+ return { ok: true, server: server2, tools: await this.toolNames(target, sessionId), when };
100016
100231
  }
100017
100232
  detach(sessionId, ref) {
100018
100233
  const mine = this.records(sessionId);
@@ -100056,7 +100271,7 @@ var AppAccess = class {
100056
100271
  const session2 = this.deps.session(sessionId);
100057
100272
  if (!session2) return { ok: false, error: "This session cannot attach apps." };
100058
100273
  if (session2.projectId === null) return { ok: false, error: "Only a session in a project can attach apps from other projects." };
100059
- if (!this.projectTrusted(session2.projectId)) {
100274
+ if (!this.projectTrusted(session2.projectId) && !this.deps.boundaryOpen?.(sessionId, "attach from an untrusted project")) {
100060
100275
  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." };
100061
100276
  }
100062
100277
  return { ok: true, session: session2 };
@@ -100064,7 +100279,7 @@ var AppAccess = class {
100064
100279
  /** What this session could attach now: shared apps of other trusted projects, and user-folder apps, minus what it has */
100065
100280
  candidates(session2) {
100066
100281
  const have = this.deps.hub.refsFor(session2);
100067
- 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)));
100282
+ 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)));
100068
100283
  }
100069
100284
  whyNot(session2, app2) {
100070
100285
  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.`;
@@ -100120,13 +100335,13 @@ var AppAccess = class {
100120
100335
  for (let n = 2; ; n++) if (!taken.includes(`${base2}-${n}`)) return `${base2}-${n}`;
100121
100336
  }
100122
100337
  /** The app's agent tools for the tool result — started here if never listed, within the hub's cap; empty on failure */
100123
- async toolNames(ref) {
100338
+ async toolNames(ref, sessionId) {
100124
100339
  const known = this.deps.hub.rt.knownTools(ref, "model");
100125
100340
  if (known) return known.map((t) => t.name);
100126
100341
  let timer;
100127
100342
  try {
100128
100343
  const listed = await Promise.race([
100129
- this.deps.hub.rt.tools(ref, "model"),
100344
+ this.deps.hub.rt.tools(ref, "model", sessionId),
100130
100345
  new Promise((_, reject) => timer = setTimeout(() => reject(new Error("timeout")), this.deps.toolListWaitMs ?? TOOL_LIST_WAIT_MS2))
100131
100346
  ]);
100132
100347
  return listed.map((t) => t.name);
@@ -100160,7 +100375,7 @@ function parse3(raw) {
100160
100375
  const v = JSON.parse(raw);
100161
100376
  if (!Array.isArray(v)) return [];
100162
100377
  return v.filter(
100163
- (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")
100378
+ (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")
100164
100379
  );
100165
100380
  } catch {
100166
100381
  return [];
@@ -100183,17 +100398,19 @@ function finalAnswer(messages2) {
100183
100398
  }
100184
100399
  var AgentRunWait = class {
100185
100400
  constructor(notify, sessionName, onUsage = () => {
100186
- }) {
100401
+ }, readAnswer2) {
100187
100402
  this.notify = notify;
100188
100403
  this.sessionName = sessionName;
100189
100404
  this.onUsage = onUsage;
100190
- this.done = new Promise((resolve11, reject) => this.settle = { resolve: resolve11, reject });
100405
+ this.readAnswer = readAnswer2;
100406
+ this.done = new Promise((resolve12, reject) => this.settle = { resolve: resolve12, reject });
100191
100407
  this.done.catch(() => {
100192
100408
  });
100193
100409
  }
100194
100410
  notify;
100195
100411
  sessionName;
100196
100412
  onUsage;
100413
+ readAnswer;
100197
100414
  /** The person stopped that session's turn (`SessionManager.interrupt`) */
100198
100415
  stoppedByPerson = false;
100199
100416
  /** The person deleted that session — there is no session left to conclude */
@@ -100201,17 +100418,46 @@ var AgentRunWait = class {
100201
100418
  done;
100202
100419
  settle;
100203
100420
  settled = false;
100421
+ inputs = 0;
100422
+ trackedInputs = false;
100423
+ ending;
100424
+ get waitingForInputs() {
100425
+ return this.inputs > 0;
100426
+ }
100427
+ get finished() {
100428
+ return this.settled;
100429
+ }
100430
+ get tracksInputs() {
100431
+ return this.trackedInputs;
100432
+ }
100433
+ get completedAnswer() {
100434
+ return this.ending?.answer;
100435
+ }
100436
+ /** A parent's note may join this turn or run after it. Hold before send, since adapters can emit synchronously. */
100437
+ holdInput() {
100438
+ this.trackedInputs = true;
100439
+ this.inputs++;
100440
+ let released = false;
100441
+ return () => {
100442
+ if (released) return;
100443
+ released = true;
100444
+ this.inputs--;
100445
+ if (!this.inputs && this.ending) this.finish(this.ending);
100446
+ };
100447
+ }
100204
100448
  onEvent(e) {
100205
100449
  if (e.type === "usage_update") {
100206
100450
  const t = e.tokens;
100207
100451
  return this.onUsage({ input: t.inputTokens + t.cacheReadTokens + t.cacheCreationTokens, output: t.outputTokens });
100208
100452
  }
100209
100453
  if (this.settled) return;
100454
+ if (e.type === "message_delta" || e.type === "state_change" && e.state === "working") this.ending = void 0;
100210
100455
  switch (e.type) {
100211
100456
  case "turn_complete":
100212
100457
  if (this.stoppedByPerson) return this.fail(new Error("the person stopped the agent before it finished"));
100213
- this.settled = true;
100214
- return this.settle.resolve(e.output === void 0 ? {} : { output: e.output });
100458
+ this.ending = { ...e.output === void 0 ? {} : { output: e.output }, ...this.readAnswer ? { answer: this.readAnswer() } : {} };
100459
+ if (this.inputs) return;
100460
+ return this.finish(this.ending);
100215
100461
  case "error":
100216
100462
  return this.fail(new Error(this.stoppedByPerson ? "the person stopped the agent before it finished" : `the agent's turn failed: ${e.error.message}`));
100217
100463
  case "limit_reached":
@@ -100225,7 +100471,12 @@ var AgentRunWait = class {
100225
100471
  fail(err) {
100226
100472
  if (this.settled) return;
100227
100473
  this.settled = true;
100228
- this.settle.reject(err);
100474
+ this.settle.reject(this.stoppedByPerson ? new Error("the person stopped the agent before it finished") : err);
100475
+ }
100476
+ finish(ending) {
100477
+ if (this.settled) return;
100478
+ this.settled = true;
100479
+ this.settle.resolve(ending);
100229
100480
  }
100230
100481
  };
100231
100482
 
@@ -100367,7 +100618,7 @@ function windowOf(list, id2, label, since, writes, nameOf) {
100367
100618
 
100368
100619
  // packages/agent-host/src/sessions/ask-project.ts
100369
100620
  import { realpathSync as realpathSync3, statSync as statSync8 } from "node:fs";
100370
- import { isAbsolute as isAbsolute4, relative as relative4, resolve as resolve2, sep as sep4 } from "node:path";
100621
+ import { isAbsolute as isAbsolute4, relative as relative4, resolve as resolve3, sep as sep4 } from "node:path";
100371
100622
  var ASK_WAIT_MS = 24e4;
100372
100623
  var ANSWER_MAX_CHARS = 6e3;
100373
100624
  var ANSWER_HEAD_CHARS = 4500;
@@ -100436,7 +100687,7 @@ function readableGrants(paths, projectRoot) {
100436
100687
  return { granted, outside };
100437
100688
  }
100438
100689
  function underGrant(path2, grants, cwd) {
100439
- const abs = resolve2(cwd, path2);
100690
+ const abs = resolve3(cwd, path2);
100440
100691
  const r = real(abs) ?? abs;
100441
100692
  for (const g5 of grants) {
100442
100693
  if (r === g5 || r.startsWith(g5.endsWith(sep4) ? g5 : g5 + sep4)) return true;
@@ -100457,6 +100708,11 @@ function agentFrame(parentName, task) {
100457
100708
  task
100458
100709
  ].join("\n");
100459
100710
  }
100711
+ function agentNoteFrame(parentName, text2) {
100712
+ return `[A note from the parent session "${parentName}" through Centralu's agent_session. These are the parent's words about your ongoing work.]
100713
+
100714
+ ${text2}`;
100715
+ }
100460
100716
  function agentBranch(task, id2, max = 40) {
100461
100717
  const slug = task.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, max).replace(/-+$/, "");
100462
100718
  return `${APP_SLUG}/${slug || "agent"}-${id2.replace(/[^a-z0-9]/gi, "").slice(0, 4).toLowerCase()}`;
@@ -100732,13 +100988,79 @@ function orchestratorMemory(reader, sessionId) {
100732
100988
 
100733
100989
  // packages/agent-host/src/sessions/manager.ts
100734
100990
  import { homedir as homedir12 } from "node:os";
100735
- import { basename as basename5, isAbsolute as isAbsolute9, join as join28, relative as relative8, resolve as resolve10, sep as sep11 } from "node:path";
100736
- import { existsSync as existsSync18, rmSync as rmSync7, statSync as statSync12 } from "node:fs";
100991
+ import { basename as basename5, isAbsolute as isAbsolute9, join as join29, relative as relative8, resolve as resolve11, sep as sep11 } from "node:path";
100992
+ import { existsSync as existsSync19, rmSync as rmSync7, statSync as statSync12 } from "node:fs";
100737
100993
  import { exec as exec6 } from "node:child_process";
100738
100994
 
100739
100995
  // packages/agent-host/src/adapters/claude/index.ts
100740
100996
  init_sdk();
100741
100997
  import { homedir as homedir5 } from "node:os";
100998
+ import { randomUUID as randomUUID9 } from "node:crypto";
100999
+
101000
+ // packages/agent-host/src/adapters/input-receipts.ts
101001
+ import { randomUUID as randomUUID8 } from "node:crypto";
101002
+ var InputReceipts = class {
101003
+ pending = /* @__PURE__ */ new Map();
101004
+ ended = /* @__PURE__ */ new Set();
101005
+ create(observer) {
101006
+ const id2 = randomUUID8();
101007
+ let resolve12;
101008
+ let reject;
101009
+ const done = new Promise((yes, no2) => {
101010
+ resolve12 = yes;
101011
+ reject = no2;
101012
+ });
101013
+ done.catch(() => {
101014
+ });
101015
+ const receipt = { id: id2, done, cancelled: false, completed: false };
101016
+ this.pending.set(id2, { receipt, resolve: resolve12, reject, observer });
101017
+ return receipt;
101018
+ }
101019
+ get size() {
101020
+ return this.pending.size;
101021
+ }
101022
+ get ids() {
101023
+ return [...this.pending.keys()];
101024
+ }
101025
+ /** Codex's turn/start response can arrive after that turn's completion notification. */
101026
+ bind(receipt, turn) {
101027
+ const p = this.pending.get(receipt.id);
101028
+ if (!p) return;
101029
+ p.turn = turn;
101030
+ if (this.ended.has(turn)) this.finish(receipt.id);
101031
+ }
101032
+ endTurn(turn) {
101033
+ if (this.pending.size) this.ended.add(turn);
101034
+ for (const [id2, p] of this.pending) if (p.turn === turn) this.finish(id2);
101035
+ }
101036
+ /** Retries can reorder inputs; only UUIDs actually reported as consumed have completed. */
101037
+ complete(ids) {
101038
+ for (const id2 of ids) this.finish(id2);
101039
+ }
101040
+ failAll(error51) {
101041
+ const turns = [...new Set([...this.pending.values()].flatMap((p) => p.turn ? [p.turn] : []))];
101042
+ const pending = [...this.pending.values()];
101043
+ this.pending.clear();
101044
+ this.ended.clear();
101045
+ for (const p of pending) {
101046
+ p.receipt.cancelled = true;
101047
+ p.observer?.fail(error51);
101048
+ p.reject(error51);
101049
+ }
101050
+ return turns;
101051
+ }
101052
+ finish(id2) {
101053
+ const p = this.pending.get(id2);
101054
+ if (!p) return;
101055
+ this.pending.delete(id2);
101056
+ if (!this.pending.size) this.ended.clear();
101057
+ p.receipt.completed = true;
101058
+ p.observer?.complete();
101059
+ p.resolve();
101060
+ }
101061
+ };
101062
+
101063
+ // packages/agent-host/src/adapters/claude/index.ts
100742
101064
  import { execFile as execFile4 } from "node:child_process";
100743
101065
  import { promisify as promisify4 } from "node:util";
100744
101066
 
@@ -100802,7 +101124,7 @@ async function installedCliVersion(name, packageName, deps = realDeps2()) {
100802
101124
  // packages/agent-host/src/adapters/claude/exe-link.ts
100803
101125
  import { randomBytes as randomBytes2 } from "node:crypto";
100804
101126
  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";
100805
- import { basename as basename4, dirname as dirname8, join as join21 } from "node:path";
101127
+ import { basename as basename4, dirname as dirname8, join as join22 } from "node:path";
100806
101128
  var PLACEHOLDER_MAX_BYTES = 1e6;
100807
101129
  var ClaudePlaceholderError = class extends Error {
100808
101130
  name = "ClaudePlaceholderError";
@@ -100838,7 +101160,7 @@ function placeholderProblem(exe, size, fs2 = realExeFs) {
100838
101160
  }
100839
101161
  if (head.toString("latin1") === "MZ") return null;
100840
101162
  const pkg = dirname8(dirname8(exe));
100841
- const installer = basename4(dirname8(exe)).toLowerCase() === "bin" ? join21(pkg, "install.cjs") : null;
101163
+ const installer = basename4(dirname8(exe)).toLowerCase() === "bin" ? join22(pkg, "install.cjs") : null;
100842
101164
  let canRerun = false;
100843
101165
  if (installer) {
100844
101166
  try {
@@ -100853,14 +101175,14 @@ function placeholderProblem(exe, size, fs2 = realExeFs) {
100853
101175
  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`."
100854
101176
  ].join(" ");
100855
101177
  }
100856
- function claudeKey(exe, stat6, fs2 = realExeFs) {
101178
+ function claudeKey(exe, stat7, fs2 = realExeFs) {
100857
101179
  let version2 = null;
100858
101180
  try {
100859
- const pkg = JSON.parse(fs2.read(join21(dirname8(dirname8(exe)), "package.json")));
101181
+ const pkg = JSON.parse(fs2.read(join22(dirname8(dirname8(exe)), "package.json")));
100860
101182
  if (pkg.name === "@anthropic-ai/claude-code" && typeof pkg.version === "string" && pkg.version) version2 = pkg.version;
100861
101183
  } catch {
100862
101184
  }
100863
- const raw = version2 ? `${version2}-${stat6.size}` : `${stat6.size}-${Math.floor(stat6.mtimeMs)}`;
101185
+ const raw = version2 ? `${version2}-${stat7.size}` : `${stat7.size}-${Math.floor(stat7.mtimeMs)}`;
100864
101186
  return raw.replace(/[^A-Za-z0-9._-]/g, "_");
100865
101187
  }
100866
101188
  function windowsStart(platform = process.platform) {
@@ -100882,7 +101204,7 @@ var ClaudeLinks = class {
100882
101204
  return windowsStart(this.opts.platform);
100883
101205
  }
100884
101206
  base() {
100885
- return join21(this.opts.root(), "tools", "claude");
101207
+ return join22(this.opts.root(), "tools", "claude");
100886
101208
  }
100887
101209
  /**
100888
101210
  * What to start for the `claude.exe` found at `exe`. Off Windows, or for anything but an `.exe`
@@ -100893,26 +101215,26 @@ var ClaudeLinks = class {
100893
101215
  prepare(exe) {
100894
101216
  const direct = { path: exe, key: null };
100895
101217
  if (!this.enabled || !/\.exe$/i.test(exe)) return direct;
100896
- let stat6;
101218
+ let stat7;
100897
101219
  try {
100898
- stat6 = this.fs.stat(exe);
101220
+ stat7 = this.fs.stat(exe);
100899
101221
  } catch {
100900
101222
  return direct;
100901
101223
  }
100902
- const problem2 = placeholderProblem(exe, stat6.size, this.fs);
101224
+ const problem2 = placeholderProblem(exe, stat7.size, this.fs);
100903
101225
  if (problem2) throw new ClaudePlaceholderError(problem2);
100904
- const key = claudeKey(exe, stat6, this.fs);
100905
- const dir = join21(this.base(), key);
100906
- const target = join21(dir, "claude.exe");
101226
+ const key = claudeKey(exe, stat7, this.fs);
101227
+ const dir = join22(this.base(), key);
101228
+ const target = join22(dir, "claude.exe");
100907
101229
  const ready = () => {
100908
101230
  try {
100909
- return this.fs.stat(target).size === stat6.size;
101231
+ return this.fs.stat(target).size === stat7.size;
100910
101232
  } catch {
100911
101233
  return false;
100912
101234
  }
100913
101235
  };
100914
101236
  if (ready()) return this.use(key, target);
100915
- const tmp = join21(dir, `claude-${randomBytes2(4).toString("hex")}.tmp`);
101237
+ const tmp = join22(dir, `claude-${randomBytes2(4).toString("hex")}.tmp`);
100916
101238
  try {
100917
101239
  this.fs.mkdir(dir);
100918
101240
  try {
@@ -100972,7 +101294,7 @@ var ClaudeLinks = class {
100972
101294
  for (const name of names) {
100973
101295
  if (name === this.current || this.uses.has(name)) continue;
100974
101296
  try {
100975
- this.fs.removeDir(join21(this.base(), name));
101297
+ this.fs.removeDir(join22(this.base(), name));
100976
101298
  } catch {
100977
101299
  }
100978
101300
  }
@@ -102426,7 +102748,14 @@ function alwaysAllowKey(detail, opts = {}) {
102426
102748
  }
102427
102749
 
102428
102750
  // packages/agent-host/src/adapters/claude/cross-session.ts
102429
- function withoutCrossSessionMessaging(options) {
102751
+ function withoutCrossSessionMessaging(options, preset = "normal") {
102752
+ if (preset === "auto")
102753
+ return {
102754
+ ...options,
102755
+ settings: options.settings ?? {},
102756
+ disallowedTools: options.disallowedTools ?? [],
102757
+ env: { ...process.env, ...options.env }
102758
+ };
102430
102759
  const env = {
102431
102760
  ...process.env,
102432
102761
  ...options.env,
@@ -102526,7 +102855,7 @@ var ClaudeSession = class {
102526
102855
  this.opts = opts;
102527
102856
  this.emit = emit;
102528
102857
  this.launch = launch2;
102529
- this.ended = new Promise((resolve11) => this.markEnded = resolve11);
102858
+ this.ended = new Promise((resolve12) => this.markEnded = resolve12);
102530
102859
  this.stream = new ClaudeStreamNormalizer(sessionId, () => ({
102531
102860
  model: opts.model ?? null,
102532
102861
  effort: opts.effort ?? null,
@@ -102543,6 +102872,12 @@ var ClaudeSession = class {
102543
102872
  launch;
102544
102873
  externalId = null;
102545
102874
  queue = [];
102875
+ inputReceipts = new InputReceipts();
102876
+ /** Person messages already handed to the SDK, so an interrupt receipt can report cancellations. */
102877
+ personInputs = /* @__PURE__ */ new Set();
102878
+ trackedInputKnown = false;
102879
+ /** Installed only after the initial CLI metadata confirms the protocol we have verified. */
102880
+ sendTracked;
102546
102881
  /** The CLI version this process last reported (#297) */
102547
102882
  reportedVersion = null;
102548
102883
  notify = null;
@@ -102635,11 +102970,13 @@ var ClaudeSession = class {
102635
102970
  const next = self2.queue.shift();
102636
102971
  if (next !== void 0) {
102637
102972
  self2.inTurn.push(next);
102973
+ if (!next.parentNote) self2.personInputs.add(next.uuid);
102638
102974
  yield {
102639
102975
  type: "user",
102640
102976
  parent_tool_use_id: null,
102641
102977
  session_id: self2.externalId ?? "",
102642
- message: { role: "user", content: [{ type: "text", text: next }] }
102978
+ uuid: next.uuid,
102979
+ message: { role: "user", content: [{ type: "text", text: next.text }] }
102643
102980
  };
102644
102981
  continue;
102645
102982
  }
@@ -102769,8 +103106,8 @@ var ClaudeSession = class {
102769
103106
  if (questions.length === 0) return { behavior: "allow", updatedInput: toolInput };
102770
103107
  const requestId2 = `q-${++self2.reqCounter}`;
102771
103108
  self2.emit({ type: "question_request", sessionId: self2.sessionId, requestId: requestId2, questions });
102772
- return new Promise((resolve11) => {
102773
- self2.questions.set(requestId2, { resolve: resolve11, input: toolInput });
103109
+ return new Promise((resolve12) => {
103110
+ self2.questions.set(requestId2, { resolve: resolve12, input: toolInput });
102774
103111
  self2.releaseOnCancel(requestId2, options?.signal);
102775
103112
  });
102776
103113
  }
@@ -102779,17 +103116,17 @@ var ClaudeSession = class {
102779
103116
  if (key && self2.alwaysAllow.allows(key)) return { behavior: "allow", updatedInput: toolInput };
102780
103117
  const requestId = `req-${++self2.reqCounter}`;
102781
103118
  self2.emit({ type: "approval_request", sessionId: self2.sessionId, requestId, detail });
102782
- return new Promise((resolve11) => {
102783
- self2.pending.set(requestId, { resolve: resolve11, input: toolInput });
103119
+ return new Promise((resolve12) => {
103120
+ self2.pending.set(requestId, { resolve: resolve12, input: toolInput });
102784
103121
  self2.releaseOnCancel(requestId, options?.signal);
102785
103122
  });
102786
103123
  }
102787
- })
103124
+ }, preset)
102788
103125
  });
102789
103126
  ClaudeAdapter.liveQueries.add(q2);
102790
103127
  ClaudeAdapter.querySessions.set(q2, this.sessionId);
102791
103128
  this.releaseLostCalls(q2);
102792
- this.goalKnown = new Promise((resolve11) => this.settleGoalKnown = resolve11);
103129
+ this.goalKnown = new Promise((resolve12) => this.settleGoalKnown = resolve12);
102793
103130
  q2.supportedCommands().then((list) => {
102794
103131
  if (this.goalCommand === void 0 && Array.isArray(list)) this.goalCommand = list.some((c) => c?.name === "goal");
102795
103132
  }).catch(() => {
@@ -102800,7 +103137,23 @@ var ClaudeSession = class {
102800
103137
  for await (const msg of q2) {
102801
103138
  if (this.closed) break;
102802
103139
  const m = msg;
103140
+ if (!this.turnOpen && m.user_message_uuid && !m.parent_tool_use_id && (m.type === "assistant" || m.type === "stream_event")) {
103141
+ this.turnOpen = true;
103142
+ if (!this.pending.size && !this.questions.size) this.emit({ type: "state_change", sessionId: this.sessionId, state: "working" });
103143
+ }
102803
103144
  if (m.type === "system" && m.subtype === "init" && m.session_id) this.externalId = m.session_id;
103145
+ if (m.type === "system" && m.subtype === "init" && !this.trackedInputKnown) {
103146
+ this.trackedInputKnown = true;
103147
+ const version2 = typeof m.claude_code_version === "string" ? /^(\d+)\.(\d+)\.(\d+)$/.exec(m.claude_code_version) : null;
103148
+ const supported = version2 && (Number(version2[1]) > 2 || Number(version2[1]) === 2 && (Number(version2[2]) > 1 || Number(version2[2]) === 1 && Number(version2[3]) >= 289));
103149
+ if (supported && Array.isArray(m.capabilities) && m.capabilities.includes("interrupt_receipt_v1") && m.capabilities.includes("interrupt_cancel_queued_v1")) {
103150
+ this.sendTracked = (text2, observer) => {
103151
+ const receipt = this.inputReceipts.create(observer);
103152
+ this.sendInput(text2, receipt.id, receipt);
103153
+ return receipt.done;
103154
+ };
103155
+ }
103156
+ }
102804
103157
  if (m.type === "system" && m.subtype === "init" && typeof m.claude_code_version === "string" && m.claude_code_version !== this.reportedVersion) {
102805
103158
  this.reportedVersion = m.claude_code_version;
102806
103159
  this.emit({ type: "agent_version", sessionId: this.sessionId, version: m.claude_code_version });
@@ -102814,8 +103167,23 @@ var ClaudeSession = class {
102814
103167
  if (m.type === "result") this.turnOpen = false;
102815
103168
  let events2 = this.stream.push(msg);
102816
103169
  const retrying = m.type === "result" && this.retryAfterRefreshRace(msg);
103170
+ if (m.type === "result" && !retrying) {
103171
+ const result = msg;
103172
+ for (const id2 of result.user_message_uuids ?? (result.user_message_uuid ? [result.user_message_uuid] : [])) this.personInputs.delete(id2);
103173
+ }
103174
+ let completedInputs;
103175
+ if (m.type === "result" && !retrying && this.inputReceipts.size && events2.some((e) => e.type === "turn_complete" || e.type === "error")) {
103176
+ const result = msg;
103177
+ const ids = result.user_message_uuids ?? [];
103178
+ const failure3 = events2.find((e) => e.type === "error");
103179
+ if (ids.length && failure3?.type === "error") this.cancelParentNotes(new Error(failure3.error.message));
103180
+ else if (ids.length) completedInputs = ids;
103181
+ }
102817
103182
  if (retrying) events2 = events2.filter((e) => e.type !== "error");
102818
- for (const e of events2) this.emit(e);
103183
+ for (const e of events2) {
103184
+ this.emit(e);
103185
+ if (e.type === "turn_complete" && completedInputs) this.inputReceipts.complete(completedInputs);
103186
+ }
102819
103187
  this.armScheduleLook();
102820
103188
  if (m.type === "result" && !retrying) void this.reportContext(q2);
102821
103189
  }
@@ -102984,14 +103352,18 @@ var ClaudeSession = class {
102984
103352
  }
102985
103353
  }
102986
103354
  send(text2) {
103355
+ this.sendInput(text2);
103356
+ }
103357
+ sendInput(text2, uuid3 = randomUUID9(), note) {
102987
103358
  const goal = isGoalCommand(text2);
102988
103359
  if (!this.sendGate && !(goal && this.goalCommand === void 0)) {
102989
- this.deliver(text2);
103360
+ this.deliver(text2, uuid3, !!note);
102990
103361
  return;
102991
103362
  }
102992
103363
  const gate = this.sendGate = (this.sendGate ?? this.goalKnown).then(() => {
103364
+ if (note?.cancelled) return;
102993
103365
  if (!this.closed) {
102994
- this.deliver(text2);
103366
+ this.deliver(text2, uuid3, !!note);
102995
103367
  return;
102996
103368
  }
102997
103369
  this.emit({
@@ -103008,7 +103380,7 @@ var ClaudeSession = class {
103008
103380
  if (this.sendGate === gate) this.sendGate = null;
103009
103381
  });
103010
103382
  }
103011
- deliver(text2) {
103383
+ deliver(text2, uuid3, parentNote) {
103012
103384
  if (isGoalCommand(text2)) {
103013
103385
  if (this.goalCommand !== true) {
103014
103386
  this.emit({
@@ -103023,11 +103395,12 @@ var ClaudeSession = class {
103023
103395
  }
103024
103396
  this.stream.goal.commandSent();
103025
103397
  }
103026
- this.queue.push(text2);
103398
+ const starting = !this.turnOpen;
103399
+ this.queue.push({ text: text2, uuid: uuid3, parentNote });
103027
103400
  this.turnOpen = true;
103028
103401
  this.notify?.();
103029
103402
  this.notify = null;
103030
- this.emit({ type: "state_change", sessionId: this.sessionId, state: "working" });
103403
+ if (starting) this.emit({ type: "state_change", sessionId: this.sessionId, state: "working" });
103031
103404
  }
103032
103405
  respondApproval(requestId, decision, scope, matcher) {
103033
103406
  const p = this.pending.get(requestId);
@@ -103166,7 +103539,11 @@ var ClaudeSession = class {
103166
103539
  * 2) SDK interrupt: this actually cuts the turn. We can call this method because we pass the
103167
103540
  * prompt as an async generator, in streaming input mode.
103168
103541
  */
103169
- interrupt() {
103542
+ interrupt(cancelQueued = this.inputReceipts.size > 0, reason = new Error("The parent note was stopped before it finished")) {
103543
+ this.inputReceipts.failAll(reason);
103544
+ const held3 = (cancelQueued ? this.queue.filter((input) => !input.parentNote).length : 0) + (this.retry?.texts.filter((input) => !input.parentNote).length ?? 0);
103545
+ if (cancelQueued) this.queue.length = 0;
103546
+ this.reportUndelivered(held3);
103170
103547
  this.opts.apps?.cancelAll();
103171
103548
  this.cancelRetry();
103172
103549
  for (const [id2, p] of this.pending) {
@@ -103176,7 +103553,13 @@ var ClaudeSession = class {
103176
103553
  this.pending.clear();
103177
103554
  this.releaseQuestions("Stopped by user");
103178
103555
  if (this.turnOpen) this.stream.stopped();
103179
- void this.query?.interrupt().catch((err) => {
103556
+ void this.query?.interrupt(cancelQueued ? { cancelQueued: true } : void 0).then((response) => {
103557
+ const cancelled = response?.cancelled;
103558
+ if (!Array.isArray(cancelled)) return;
103559
+ let count = 0;
103560
+ for (const id2 of cancelled) if (typeof id2 === "string" && this.personInputs.delete(id2)) count++;
103561
+ this.reportUndelivered(count);
103562
+ }).catch((err) => {
103180
103563
  this.emit({
103181
103564
  type: "error",
103182
103565
  sessionId: this.sessionId,
@@ -103185,6 +103568,34 @@ var ClaudeSession = class {
103185
103568
  });
103186
103569
  this.emit({ type: "state_change", sessionId: this.sessionId, state: "waiting_input", reason: "interrupted" });
103187
103570
  }
103571
+ /** A failed turn is already over. Cancel its parent notes without interrupting the person's next turn. */
103572
+ cancelParentNotes(reason) {
103573
+ const ids = this.inputReceipts.ids;
103574
+ this.inputReceipts.failAll(reason);
103575
+ this.queue = this.queue.filter((input) => !input.parentNote);
103576
+ if (this.retry) {
103577
+ const texts = this.retry.texts;
103578
+ texts.splice(0, texts.length, ...texts.filter((input) => !input.parentNote));
103579
+ if (!texts.length) this.cancelRetry();
103580
+ }
103581
+ const query = this.query;
103582
+ if (!query?.cancelAsyncMessage || this.personInputs.size || this.queue.length || this.retry?.texts.length || this.sendGate) return;
103583
+ for (const id2 of ids) void query.cancelAsyncMessage(id2).catch((error51) => {
103584
+ this.emit({ type: "error", sessionId: this.sessionId, error: {
103585
+ code: "internal",
103586
+ message: `Could not cancel the parent note: ${error51.message}`,
103587
+ retryable: false
103588
+ } });
103589
+ });
103590
+ }
103591
+ reportUndelivered(count) {
103592
+ if (!count) return;
103593
+ this.emit({
103594
+ type: "error",
103595
+ sessionId: this.sessionId,
103596
+ error: { code: "internal", message: `${count} message(s) were still queued when the turn stopped and were not delivered \u2014 please resend`, retryable: false }
103597
+ });
103598
+ }
103188
103599
  /**
103189
103600
  * Stops one background task (#290). Measured on a backgrounded shell: `background_tasks_changed` without it,
103190
103601
  * `task_updated` killed and `task_notification` stopped follow at once, and the stream turns that into the
@@ -103203,6 +103614,7 @@ var ClaudeSession = class {
103203
103614
  * same way; only swapping the process was missing it.
103204
103615
  */
103205
103616
  async dispose() {
103617
+ this.inputReceipts.failAll(new Error("The session closed before the parent note finished"));
103206
103618
  this.closed = true;
103207
103619
  this.notify?.();
103208
103620
  this.settleGoalKnown();
@@ -103244,6 +103656,7 @@ var ClaudeSession = class {
103244
103656
  * the process (#98) — and ends every background task the session listed (#290), for the same reason.
103245
103657
  */
103246
103658
  releaseAgents(why) {
103659
+ this.inputReceipts.failAll(new Error(why));
103247
103660
  if (this.scheduleLook) clearTimeout(this.scheduleLook);
103248
103661
  this.scheduleLook = null;
103249
103662
  for (const e of this.stream.release(why)) this.emit(e);
@@ -103391,6 +103804,8 @@ var ClaudeAdapter = class _ClaudeAdapter {
103391
103804
  processModel: "per-session",
103392
103805
  builtInSubagents: true,
103393
103806
  peerMessaging: true,
103807
+ // The query's prompt iterator reads queued user messages while the current turn continues.
103808
+ midTurnInput: true,
103394
103809
  /*
103395
103810
  * An idle session may be put to sleep (#568): a resumed process read the prompt cache as the live one did (~100% at
103396
103811
  * 2, 6 and 10 min on the 1 h lifetime, CLI 2.1.29x with haiku, #561) for +0.7 s (p50) / +1.8 s (p95) before the first
@@ -103400,7 +103815,7 @@ var ClaudeAdapter = class _ClaudeAdapter {
103400
103815
  sleep: true
103401
103816
  };
103402
103817
  launchSettingsFingerprint(settings) {
103403
- return JSON.stringify({ v: 1, cwd: settings.cwd, permission: settings.permissionPreset, trusted: !!settings.projectTrusted, noSettings: !!settings.noSettingFiles });
103818
+ return JSON.stringify({ v: settings.permissionPreset === "auto" ? 2 : 1, cwd: settings.cwd, permission: settings.permissionPreset, trusted: !!settings.projectTrusted, noSettings: !!settings.noSettingFiles });
103404
103819
  }
103405
103820
  async detect() {
103406
103821
  const path2 = whichTool("claude");
@@ -103487,7 +103902,7 @@ var ClaudeAdapter = class _ClaudeAdapter {
103487
103902
  let timer;
103488
103903
  await Promise.race([
103489
103904
  Promise.allSettled([...this.launch.closing]),
103490
- new Promise((resolve11) => timer = setTimeout(resolve11, this.launch.exitGraceMs))
103905
+ new Promise((resolve12) => timer = setTimeout(resolve12, this.launch.exitGraceMs))
103491
103906
  ]);
103492
103907
  clearTimeout(timer);
103493
103908
  }
@@ -103497,7 +103912,7 @@ var ClaudeAdapter = class _ClaudeAdapter {
103497
103912
  import { execFile as execFile5 } from "node:child_process";
103498
103913
 
103499
103914
  // packages/agent-host/src/adapters/codex/bridge-path.ts
103500
- import { existsSync as existsSync14 } from "node:fs";
103915
+ import { existsSync as existsSync15 } from "node:fs";
103501
103916
  import { fileURLToPath as fileURLToPath3 } from "node:url";
103502
103917
  function bridgePath() {
103503
103918
  const candidates = [
@@ -103506,23 +103921,23 @@ function bridgePath() {
103506
103921
  new URL("./codex-orchestrator-bridge.mjs", import.meta.url)
103507
103922
  // the bundled build-output layout
103508
103923
  ].map((u) => fileURLToPath3(u));
103509
- const found = candidates.find((p) => existsSync14(p));
103924
+ const found = candidates.find((p) => existsSync15(p));
103510
103925
  if (!found) throw new Error(`orchestrator bridge not found: ${candidates.join(", ")}`);
103511
103926
  return found;
103512
103927
  }
103513
103928
 
103514
103929
  // packages/agent-host/src/adapters/codex/index.ts
103515
- import { existsSync as existsSync16, realpathSync as realpathSync7 } from "node:fs";
103930
+ import { existsSync as existsSync17, realpathSync as realpathSync7 } from "node:fs";
103516
103931
  import { homedir as homedir10 } from "node:os";
103517
- import { dirname as dirname9, join as join23, resolve as resolve8 } from "node:path";
103932
+ import { dirname as dirname9, join as join24, resolve as resolve9 } from "node:path";
103518
103933
  import { promisify as promisify5 } from "node:util";
103519
103934
 
103520
103935
  // packages/agent-host/src/adapters/codex/native.ts
103521
- import { existsSync as existsSync15, readFileSync as readFileSync11, realpathSync as realpathSync6 } from "node:fs";
103936
+ import { existsSync as existsSync16, readFileSync as readFileSync11, realpathSync as realpathSync6 } from "node:fs";
103522
103937
  var realDeps3 = () => ({
103523
103938
  platform: HOST_PLATFORM,
103524
103939
  arch: process.arch,
103525
- exists: existsSync15,
103940
+ exists: existsSync16,
103526
103941
  read: (p) => readFileSync11(p, "utf8"),
103527
103942
  realpath: (p) => realpathSync6(p),
103528
103943
  launch: (p) => launchFor(p),
@@ -103691,14 +104106,14 @@ var CodexClient = class {
103691
104106
  if (this.closed) return Promise.reject(new Error("codex app-server has already exited"));
103692
104107
  const id2 = `${this.idPrefix}${this.nextId++}`;
103693
104108
  this.write({ id: id2, method, params });
103694
- return new Promise((resolve11, reject) => {
104109
+ return new Promise((resolve12, reject) => {
103695
104110
  const timer = setTimeout(() => {
103696
104111
  if (this.pending.delete(id2)) reject(new Error(`No response for ${method}`));
103697
104112
  }, timeoutMs);
103698
104113
  this.pending.set(id2, {
103699
104114
  resolve: (v) => {
103700
104115
  clearTimeout(timer);
103701
- resolve11(v);
104116
+ resolve12(v);
103702
104117
  },
103703
104118
  reject: (e) => {
103704
104119
  clearTimeout(timer);
@@ -103722,12 +104137,12 @@ var CodexClient = class {
103722
104137
  if (this.closed) return;
103723
104138
  this.closed = true;
103724
104139
  this.proc.stdin.end();
103725
- const exited = await new Promise((resolve11) => {
103726
- if (this.proc.exitCode !== null || this.proc.signalCode != null || this.finished) return resolve11(true);
103727
- const t = setTimeout(() => resolve11(false), 2e3);
104140
+ const exited = await new Promise((resolve12) => {
104141
+ if (this.proc.exitCode !== null || this.proc.signalCode != null || this.finished) return resolve12(true);
104142
+ const t = setTimeout(() => resolve12(false), 2e3);
103728
104143
  this.proc.once("exit", () => {
103729
104144
  clearTimeout(t);
103730
- resolve11(true);
104145
+ resolve12(true);
103731
104146
  });
103732
104147
  });
103733
104148
  if (!exited) this.proc.kill("SIGKILL");
@@ -103749,18 +104164,18 @@ var CodexClient = class {
103749
104164
 
103750
104165
  // packages/agent-host/src/adapters/codex/rollout.ts
103751
104166
  import { createReadStream as createReadStream2 } from "node:fs";
103752
- import { readdir as readdir4 } from "node:fs/promises";
104167
+ import { readdir as readdir5 } from "node:fs/promises";
103753
104168
  import { createInterface as createInterface2 } from "node:readline";
103754
104169
  import { homedir as homedir6 } from "node:os";
103755
- import { join as join22 } from "node:path";
104170
+ import { join as join23 } from "node:path";
103756
104171
  var MIN_SUMMARY_CHARS = 200;
103757
- async function findRolloutPath(threadId, sessionsDir = join22(homedir6(), ".codex", "sessions")) {
104172
+ async function findRolloutPath(threadId, sessionsDir = join23(homedir6(), ".codex", "sessions")) {
103758
104173
  try {
103759
104174
  const suffix = `-${threadId}.jsonl`;
103760
- const names = await readdir4(sessionsDir, { recursive: true });
104175
+ const names = await readdir5(sessionsDir, { recursive: true });
103761
104176
  const hits = names.filter((n) => String(n).endsWith(suffix)).sort();
103762
104177
  const hit = hits[hits.length - 1];
103763
- return hit ? join22(sessionsDir, String(hit)) : null;
104178
+ return hit ? join23(sessionsDir, String(hit)) : null;
103764
104179
  } catch {
103765
104180
  return null;
103766
104181
  }
@@ -103961,7 +104376,7 @@ function userInputText(content) {
103961
104376
  }
103962
104377
 
103963
104378
  // packages/agent-host/src/adapters/codex/images.ts
103964
- import { readFile as readFile2, stat as stat4 } from "node:fs/promises";
104379
+ import { readFile as readFile3, stat as stat5 } from "node:fs/promises";
103965
104380
  var IMAGE_MIMES2 = {
103966
104381
  png: "image/png",
103967
104382
  jpg: "image/jpeg",
@@ -103977,9 +104392,9 @@ async function imageEventFromDisk(sessionId, path2, maxBytes = IMAGE_MAX_BYTES)
103977
104392
  const mime = IMAGE_MIMES2[ext2];
103978
104393
  if (!mime) return fail4(`Cannot display this format (.${ext2})`);
103979
104394
  try {
103980
- const s = await stat4(path2);
104395
+ const s = await stat5(path2);
103981
104396
  if (s.size > maxBytes) return fail4(`Image is too large (${Math.round(s.size / 1048576)}MB)`);
103982
- const buf = await readFile2(path2);
104397
+ const buf = await readFile3(path2);
103983
104398
  return { type: "message_image", sessionId, mime, data: buf.toString("base64"), path: path2 };
103984
104399
  } catch (err) {
103985
104400
  return fail4(`Failed to read image: ${err.message}`);
@@ -104963,16 +105378,16 @@ var CodexChildTracker = class {
104963
105378
  };
104964
105379
 
104965
105380
  // packages/agent-host/src/adapters/codex/sandbox.ts
104966
- import { realpath as realpath5 } from "node:fs/promises";
104967
- import { resolve as resolve7 } from "node:path";
105381
+ import { realpath as realpath6 } from "node:fs/promises";
105382
+ import { resolve as resolve8 } from "node:path";
104968
105383
  async function sandboxConfig(cwd, preset, readConfig) {
104969
105384
  const roots = [];
104970
105385
  try {
104971
105386
  const stdout = await runGit(cwd, ["rev-parse", "--path-format=absolute", "--git-dir", "--git-common-dir"], { timeout: 5e3 });
104972
105387
  const [gitDir, commonDir] = stdout.trim().split(/\r?\n/);
104973
105388
  if (gitDir && commonDir) {
104974
- const gitPath = await realpath5(resolve7(cwd, gitDir));
104975
- const commonPath = await realpath5(resolve7(cwd, commonDir));
105389
+ const gitPath = await realpath6(resolve8(cwd, gitDir));
105390
+ const commonPath = await realpath6(resolve8(cwd, commonDir));
104976
105391
  if (gitPath !== commonPath) roots.push(commonPath);
104977
105392
  }
104978
105393
  } catch {
@@ -105000,7 +105415,7 @@ function permissionOptionsFor2(preset) {
105000
105415
  function repoFilesConfig(opts) {
105001
105416
  if (opts.projectTrusted === true && !opts.noSettingFiles) return {};
105002
105417
  const keys = /* @__PURE__ */ new Set();
105003
- for (const start2 of [resolve8(opts.cwd), realPathOr(opts.cwd)]) {
105418
+ for (const start2 of [resolve9(opts.cwd), realPathOr(opts.cwd)]) {
105004
105419
  for (let dir = start2; ; dir = dirname9(dir)) {
105005
105420
  keys.add(dir);
105006
105421
  if (dirname9(dir) === dir) break;
@@ -105015,7 +105430,7 @@ function realPathOr(path2) {
105015
105430
  try {
105016
105431
  return realpathSync7.native(path2);
105017
105432
  } catch {
105018
- return resolve8(path2);
105433
+ return resolve9(path2);
105019
105434
  }
105020
105435
  }
105021
105436
  var APP_APPROVAL_MODE = {
@@ -105157,6 +105572,8 @@ var CodexSession = class {
105157
105572
  * **that we ourselves started**, and flush the queue as a single turn once that turn ends.
105158
105573
  */
105159
105574
  pendingInputs = [];
105575
+ inputReceipts = new InputReceipts();
105576
+ inputGroups = /* @__PURE__ */ new Map();
105160
105577
  /** An unsteerable turn (compact/review) is running — we know because we are the ones who started it */
105161
105578
  blockingTurn = false;
105162
105579
  /**
@@ -105524,6 +105941,16 @@ var CodexSession = class {
105524
105941
  this.activityBefore = null;
105525
105942
  }
105526
105943
  this.noteAppCall(n);
105944
+ if (n.method === "item/started" || n.method === "item/completed") {
105945
+ const p = n.params ?? {};
105946
+ if (p.item?.type === "userMessage" && p.item.clientId && p.turnId) {
105947
+ const group = this.inputGroups.get(p.item.clientId);
105948
+ if (group) {
105949
+ this.inputGroups.delete(p.item.clientId);
105950
+ this.takeInputTurn({ turn: { id: p.turnId } }, group);
105951
+ }
105952
+ }
105953
+ }
105527
105954
  if (n.method === "turn/completed" && this.blockingTurn) {
105528
105955
  this.blockingTurn = false;
105529
105956
  this.flushPending();
@@ -105541,6 +105968,7 @@ var CodexSession = class {
105541
105968
  }
105542
105969
  if (n.method === "mcpServer/startupStatus/updated" && !this.firstMcpFailure(n.params)) return;
105543
105970
  for (const e of normalizeNotification(this.sessionId, n)) {
105971
+ if (e.type === "error" && this.inputReceipts.size) this.interrupt(new Error(e.error.message));
105544
105972
  if (e.type === "message_image" && !e.data && e.path) {
105545
105973
  void imageEventFromDisk(this.sessionId, e.path).then((filled) => this.emit(filled));
105546
105974
  continue;
@@ -105559,6 +105987,10 @@ var CodexSession = class {
105559
105987
  else this.activityBefore = e.activity;
105560
105988
  }
105561
105989
  this.emit(e);
105990
+ if (e.type === "turn_complete") {
105991
+ const ended = turnIdOf(n.params);
105992
+ if (ended) this.inputReceipts.endTurn(ended);
105993
+ }
105562
105994
  }
105563
105995
  this.noteSpawn(n);
105564
105996
  if (n.method === "turn/completed" && this.unlinked.size > 0) {
@@ -105730,13 +106162,21 @@ var CodexSession = class {
105730
106162
  applyRules(matchers) {
105731
106163
  this.alwaysAllow.addAll(matchers);
105732
106164
  }
106165
+ sendTracked(text2, observer) {
106166
+ const receipt = this.inputReceipts.create(observer);
106167
+ this.sendInput(text2, receipt);
106168
+ return receipt.done;
106169
+ }
105733
106170
  send(text2) {
106171
+ this.sendInput(text2);
106172
+ }
106173
+ sendInput(text2, receipt) {
105734
106174
  const nth = ++this.sendsIssued;
105735
106175
  void this.ready.then(() => {
105736
- if (nth <= this.sendsStopped) return;
106176
+ if (nth <= this.sendsStopped || receipt?.cancelled) return;
105737
106177
  if (!this.threadId) throw new Error("Thread is not ready");
105738
106178
  if (this.blockingTurn) {
105739
- this.pendingInputs.push(text2);
106179
+ this.pendingInputs.push({ text: text2, receipt });
105740
106180
  return;
105741
106181
  }
105742
106182
  if (text2.trim() === "/compact") {
@@ -105789,8 +106229,10 @@ var CodexSession = class {
105789
106229
  say2(status === "active" ? `Goal set: ${arg}` : `Goal set (${status}): ${arg}`);
105790
106230
  });
105791
106231
  }
106232
+ if (receipt) this.inputGroups.set(receipt.id, [receipt]);
105792
106233
  return this.client.request("turn/start", {
105793
106234
  threadId: this.threadId,
106235
+ ...receipt ? { clientUserMessageId: receipt.id } : {},
105794
106236
  input: [{ type: "text", text: text2 }],
105795
106237
  /*
105796
106238
  * Reasoning effort is passed per turn — this is the slot codex documents as applying
@@ -105799,8 +106241,11 @@ var CodexSession = class {
105799
106241
  */
105800
106242
  ...this.opts.effort ? { effort: this.opts.effort } : {},
105801
106243
  ...this.outputSchemaParam()
105802
- }).then((res) => this.takeStartedTurn(turnIdOf(res)));
106244
+ }).then((res) => this.takeInputTurn(res, receipt ? [receipt] : []));
105803
106245
  }).catch((e) => {
106246
+ if (receipt) this.inputGroups.delete(receipt.id);
106247
+ if (receipt?.cancelled || receipt?.completed) return;
106248
+ if (receipt) this.interrupt(e);
105804
106249
  if (this.startFailed) return;
105805
106250
  this.emit({
105806
106251
  type: "error",
@@ -105812,14 +106257,22 @@ var CodexSession = class {
105812
106257
  /** Sends the messages that were queued out as one turn — each message as its own input item (does not blur the boundary) */
105813
106258
  flushPending() {
105814
106259
  if (this.pendingInputs.length === 0 || !this.threadId) return;
105815
- const input = this.pendingInputs.map((text2) => ({ type: "text", text: text2 }));
106260
+ const pending = this.pendingInputs;
106261
+ const receipts = pending.flatMap((p) => p.receipt ? [p.receipt] : []);
106262
+ const clientId = receipts[0]?.id;
106263
+ if (clientId) this.inputGroups.set(clientId, receipts);
106264
+ const input = pending.map(({ text: text2 }) => ({ type: "text", text: text2 }));
105816
106265
  this.pendingInputs = [];
105817
106266
  void this.client.request("turn/start", {
105818
106267
  threadId: this.threadId,
106268
+ ...clientId ? { clientUserMessageId: clientId } : {},
105819
106269
  input,
105820
106270
  ...this.opts.effort ? { effort: this.opts.effort } : {},
105821
106271
  ...this.outputSchemaParam()
105822
- }).then((res) => this.takeStartedTurn(turnIdOf(res))).catch((e) => {
106272
+ }).then((res) => this.takeInputTurn(res, receipts)).catch((e) => {
106273
+ if (clientId) this.inputGroups.delete(clientId);
106274
+ if (receipts.length && receipts.every((r) => r.cancelled || r.completed)) return;
106275
+ if (this.inputReceipts.size) this.interrupt(e);
105823
106276
  this.emit({
105824
106277
  type: "error",
105825
106278
  sessionId: this.sessionId,
@@ -105827,6 +106280,25 @@ var CodexSession = class {
105827
106280
  });
105828
106281
  });
105829
106282
  }
106283
+ takeInputTurn(res, receipts) {
106284
+ if (receipts.length && receipts.every((r) => r.completed)) return;
106285
+ const id2 = turnIdOf(res);
106286
+ if (!receipts.length) {
106287
+ this.takeStartedTurn(id2);
106288
+ return;
106289
+ }
106290
+ if (!id2) {
106291
+ this.interrupt(new Error("Codex did not identify the turn accepting the parent note"));
106292
+ return;
106293
+ }
106294
+ if (receipts.some((r) => r.cancelled) && !this.endedTurns.includes(id2)) {
106295
+ void this.client.request("turn/interrupt", { threadId: this.threadId, turnId: id2 }).catch(() => {
106296
+ });
106297
+ }
106298
+ if (receipts.some((r) => r.cancelled)) return;
106299
+ this.takeStartedTurn(id2);
106300
+ for (const receipt of receipts) this.inputReceipts.bind(receipt, id2);
106301
+ }
105830
106302
  /**
105831
106303
  * Takes a running turn's id from a response, unless the notifications already named one or already ended it. The
105832
106304
  * response and the notifications race: `turn/started` can come after the response (Stop pressed very quickly), and
@@ -105894,7 +106366,14 @@ var CodexSession = class {
105894
106366
  }
105895
106367
  return out;
105896
106368
  }
105897
- interrupt() {
106369
+ interrupt(reason = new Error("The parent note was stopped before it finished")) {
106370
+ const turns = this.inputReceipts.failAll(reason);
106371
+ this.pendingInputs = this.pendingInputs.filter((p) => !p.receipt);
106372
+ for (const turnId of turns) {
106373
+ if (!this.threadId || turnId === this.turnId || this.endedTurns.includes(turnId)) continue;
106374
+ void this.client.request("turn/interrupt", { threadId: this.threadId, turnId }).catch(() => {
106375
+ });
106376
+ }
105898
106377
  this.opts.apps?.cancelAll();
105899
106378
  if (!this.threadId && this.sendsIssued > this.sendsStopped) {
105900
106379
  this.sendsStopped = this.sendsIssued;
@@ -105927,6 +106406,8 @@ var CodexSession = class {
105927
106406
  }
105928
106407
  /** The app-server is gone, and the child threads with it — each still listed ends as stopped. */
105929
106408
  releaseChildren() {
106409
+ this.inputGroups.clear();
106410
+ this.inputReceipts.failAll(new Error("The session process ended before the parent note finished"));
105930
106411
  for (const e of this.children.release("Ended with the session process")) this.emit(e);
105931
106412
  }
105932
106413
  /**
@@ -106068,7 +106549,7 @@ function threadIdOf(res) {
106068
106549
  }
106069
106550
  function codexHome() {
106070
106551
  const custom2 = process.env.CODEX_HOME?.trim();
106071
- return custom2 ? custom2 : join23(homedir10(), ".codex");
106552
+ return custom2 ? custom2 : join24(homedir10(), ".codex");
106072
106553
  }
106073
106554
  var CODEX_VERBOSITIES = ["low", "medium", "high"];
106074
106555
  var CodexAdapter = class {
@@ -106102,6 +106583,8 @@ var CodexAdapter = class {
106102
106583
  processModel: "per-session",
106103
106584
  builtInSubagents: true,
106104
106585
  peerMessaging: false,
106586
+ // 0.160 TurnStartParams: turn/start steers an already-active regular turn; compact/review input is queued.
106587
+ midTurnInput: true,
106105
106588
  /*
106106
106589
  * Not put to sleep (#568). Waking a large thread measured 7-13 s (`thread/resume` scales with the rollout, ~13.5 s for
106107
106590
  * 550 MB), and its cache on resume is unmeasured. The memory is better won by one shared app-server per account
@@ -106122,7 +106605,7 @@ var CodexAdapter = class {
106122
106605
  const launch2 = launchFor(path2 ?? "codex");
106123
106606
  const { stdout } = await exec4(launch2.command, [...launch2.args, "--version"], { timeout: 5e3 });
106124
106607
  const version2 = `${stdout.trim()} \xB7 ${path2 ?? "PATH"}`;
106125
- const loggedIn = existsSync16(join23(codexHome(), "auth.json"));
106608
+ const loggedIn = existsSync17(join24(codexHome(), "auth.json"));
106126
106609
  return {
106127
106610
  tool: "codex",
106128
106611
  installed: true,
@@ -106256,7 +106739,7 @@ function registeredTool(adapters2, preferred) {
106256
106739
 
106257
106740
  // packages/agent-host/src/dev-services/store.ts
106258
106741
  import Database from "better-sqlite3";
106259
- import { existsSync as existsSync17, readFileSync as readFileSync12 } from "node:fs";
106742
+ import { existsSync as existsSync18, readFileSync as readFileSync12 } from "node:fs";
106260
106743
  import { fileURLToPath as fileURLToPath4 } from "node:url";
106261
106744
  function resolveSchemaPath() {
106262
106745
  const candidates = [
@@ -106265,7 +106748,7 @@ function resolveSchemaPath() {
106265
106748
  new URL("../../../protocol/src/schema/schema.sql", import.meta.url)
106266
106749
  // source tree
106267
106750
  ].map((u) => fileURLToPath4(u));
106268
- const found = candidates.find((p) => existsSync17(p));
106751
+ const found = candidates.find((p) => existsSync18(p));
106269
106752
  if (!found) throw new Error(`schema.sql not found: ${candidates.join(", ")}`);
106270
106753
  return found;
106271
106754
  }
@@ -106543,7 +107026,7 @@ var Store = class _Store {
106543
107026
  const probe = Object.create(_Store.prototype);
106544
107027
  const steps = probe.migrationSteps();
106545
107028
  const known = steps[steps.length - 1].to;
106546
- if (dbPath2 === ":memory:" || !existsSync17(dbPath2)) return empty(known);
107029
+ if (dbPath2 === ":memory:" || !existsSync18(dbPath2)) return empty(known);
106547
107030
  const db = new Database(dbPath2, { readonly: true, fileMustExist: true });
106548
107031
  try {
106549
107032
  probe.db = db;
@@ -108344,7 +108827,7 @@ var Store = class _Store {
108344
108827
  this.db.prepare(`DELETE FROM sessions WHERE id = ? AND deleted_at IS NOT NULL`).run(sessionId);
108345
108828
  return true;
108346
108829
  });
108347
- while (!step()) await new Promise((resolve11) => setImmediate(resolve11));
108830
+ while (!step()) await new Promise((resolve12) => setImmediate(resolve12));
108348
108831
  return true;
108349
108832
  }
108350
108833
  /** One session's messages in seq order, one transaction per chunk, letting the event loop go between chunks (#179) */
@@ -108359,7 +108842,7 @@ var Store = class _Store {
108359
108842
  if (rows.length > 0) after = rows[rows.length - 1].seq;
108360
108843
  return rows.length < chunk;
108361
108844
  });
108362
- while (!step()) await new Promise((resolve11) => setImmediate(resolve11));
108845
+ while (!step()) await new Promise((resolve12) => setImmediate(resolve12));
108363
108846
  }
108364
108847
  isTrashed(sessionId) {
108365
108848
  return !!this.db.prepare(`SELECT 1 FROM sessions WHERE id = ? AND deleted_at IS NOT NULL`).get(sessionId);
@@ -109026,7 +109509,8 @@ function parseTrashRecord(json2) {
109026
109509
  projectName: str8(r.projectName),
109027
109510
  projectPath: str8(r.projectPath),
109028
109511
  removeExternal: r.removeExternal === true,
109029
- removeWorktree: r.removeWorktree === true
109512
+ removeWorktree: r.removeWorktree === true,
109513
+ ...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 } : {}
109030
109514
  };
109031
109515
  }
109032
109516
  var OUT_OF_TRASH = `session_id NOT IN (SELECT id FROM sessions WHERE deleted_at IS NOT NULL)`;
@@ -109051,7 +109535,7 @@ function indexedText(kind, payload) {
109051
109535
 
109052
109536
  // packages/agent-host/src/dev-services/git.ts
109053
109537
  import { execFile as execFile6 } from "node:child_process";
109054
- import { isAbsolute as isAbsolute8, relative as relative7, resolve as resolve9, sep as sep9 } from "node:path";
109538
+ import { isAbsolute as isAbsolute8, relative as relative7, resolve as resolve10, sep as sep9 } from "node:path";
109055
109539
  import { promisify as promisify6 } from "node:util";
109056
109540
  var exec5 = promisify6(execFile6);
109057
109541
  var OK2 = { timeout: 1e4, maxBuffer: 32 * 1024 * 1024 };
@@ -109123,8 +109607,8 @@ function assertLexicalGitPath(cwd, path2) {
109123
109607
  if (!path2 || path2.includes("\0") || path2.startsWith(":")) {
109124
109608
  throw Object.assign(new Error("Invalid git path"), { code: "internal" });
109125
109609
  }
109126
- const root3 = resolve9(cwd);
109127
- const target = isAbsolute8(path2) ? resolve9(path2) : resolve9(root3, path2);
109610
+ const root3 = resolve10(cwd);
109611
+ const target = isAbsolute8(path2) ? resolve10(path2) : resolve10(root3, path2);
109128
109612
  const rel = relative7(root3, target);
109129
109613
  if (rel === "") throw Object.assign(new Error("Path is the project root, not a file in it"), { code: "internal" });
109130
109614
  if (rel !== ".." && !rel.startsWith(`..${sep9}`) && !isAbsolute8(rel)) return rel;
@@ -109243,14 +109727,14 @@ async function gitIgnoredEntries(cwd, limit = 50, trust = {}) {
109243
109727
  if (paths.length === 0) return [];
109244
109728
  const sizes = /* @__PURE__ */ new Map();
109245
109729
  try {
109246
- const out = await new Promise((resolve11, reject) => {
109730
+ const out = await new Promise((resolve12, reject) => {
109247
109731
  execFile6(
109248
109732
  "du",
109249
109733
  ["-sk", ...paths.slice(0, limit)],
109250
109734
  { cwd, timeout: 5e3, maxBuffer: 1 << 20 },
109251
109735
  (err, stdout) => (
109252
109736
  // du exits non-zero if even one entry cannot be read — whatever it did print is used anyway
109253
- stdout ? resolve11(stdout) : reject(err)
109737
+ stdout ? resolve12(stdout) : reject(err)
109254
109738
  )
109255
109739
  );
109256
109740
  });
@@ -109363,8 +109847,8 @@ async function gitValidBranchName(name) {
109363
109847
  return false;
109364
109848
  }
109365
109849
  }
109366
- async function gitWorktreeRemove(repoCwd, path2, force = false) {
109367
- await git(repoCwd, ["worktree", "remove", ...force ? ["--force"] : [], path2]);
109850
+ async function gitWorktreeRemove(repoCwd, path2, force = false, withSubmodules = false) {
109851
+ await git(repoCwd, ["worktree", "remove", ...force ? ["--force", ...withSubmodules ? ["--force"] : []] : [], path2]);
109368
109852
  }
109369
109853
  async function gitBranchDelete(repoCwd, branch, trust = {}) {
109370
109854
  await git(repoCwd, ["branch", "-D", branch], trust);
@@ -109380,13 +109864,13 @@ async function gitWorktreeDirty(path2, trust = {}) {
109380
109864
  }
109381
109865
 
109382
109866
  // packages/agent-host/src/dev-services/worktree-dependencies.ts
109383
- import { lstat as lstat4, readFile as readFile3, readdir as readdir5 } from "node:fs/promises";
109384
- import { join as join24 } from "node:path";
109867
+ import { lstat as lstat5, readFile as readFile4, readdir as readdir6 } from "node:fs/promises";
109868
+ import { join as join25 } from "node:path";
109385
109869
  var LOCKFILES = ["pnpm-lock.yaml", "package-lock.json", "npm-shrinkwrap.json", "yarn.lock", "bun.lock", "bun.lockb"];
109386
109870
  async function prepareWorktreeDependencies(projectCwd, worktreeCwd) {
109387
109871
  let locked = false;
109388
109872
  for (const name of LOCKFILES) {
109389
- const target = await lstat4(join24(worktreeCwd, name)).catch((err) => {
109873
+ const target = await lstat5(join25(worktreeCwd, name)).catch((err) => {
109390
109874
  if (err.code === "ENOENT") return null;
109391
109875
  throw err;
109392
109876
  });
@@ -109395,7 +109879,7 @@ async function prepareWorktreeDependencies(projectCwd, worktreeCwd) {
109395
109879
  resolveExisting(projectCwd, name),
109396
109880
  resolveExisting(worktreeCwd, name)
109397
109881
  ]);
109398
- const [before, after] = await Promise.all([readFile3(source), readFile3(destination)]);
109882
+ const [before, after] = await Promise.all([readFile4(source), readFile4(destination)]);
109399
109883
  if (!before.equals(after)) return;
109400
109884
  locked = true;
109401
109885
  }
@@ -109404,22 +109888,22 @@ async function prepareWorktreeDependencies(projectCwd, worktreeCwd) {
109404
109888
  const folders = [""];
109405
109889
  while (folders.length) {
109406
109890
  const folder = folders.pop();
109407
- const entries = await readdir5(join24(worktreeCwd, folder), { withFileTypes: true });
109891
+ const entries = await readdir6(join25(worktreeCwd, folder), { withFileTypes: true });
109408
109892
  for (const entry of entries) {
109409
109893
  if (entry.isDirectory() && entry.name !== ".git" && entry.name !== "node_modules") {
109410
- folders.push(join24(folder, entry.name));
109894
+ folders.push(join25(folder, entry.name));
109411
109895
  }
109412
109896
  }
109413
109897
  if (folder && !entries.some((e) => e.name === "package.json" && e.isFile())) continue;
109414
109898
  if (entries.some((e) => e.name === "node_modules")) continue;
109415
- const rel = join24(folder, "node_modules");
109899
+ const rel = join25(folder, "node_modules");
109416
109900
  try {
109417
109901
  const src = await resolveExisting(projectCwd, rel);
109418
- if (!(await lstat4(src)).isDirectory()) continue;
109419
- const manifest = join24(folder, "package.json");
109902
+ if (!(await lstat5(src)).isDirectory()) continue;
109903
+ const manifest = join25(folder, "package.json");
109420
109904
  const [source, target] = await Promise.all([
109421
- resolveExisting(projectCwd, manifest).then((p) => readFile3(p)),
109422
- resolveExisting(worktreeCwd, manifest).then((p) => readFile3(p))
109905
+ resolveExisting(projectCwd, manifest).then((p) => readFile4(p)),
109906
+ resolveExisting(worktreeCwd, manifest).then((p) => readFile4(p))
109423
109907
  ]);
109424
109908
  if (!source.equals(target)) continue;
109425
109909
  const dst = await prepareCopyTarget(worktreeCwd, rel);
@@ -109441,8 +109925,8 @@ async function prepareWorktreeDependencies(projectCwd, worktreeCwd) {
109441
109925
  }
109442
109926
 
109443
109927
  // packages/agent-host/src/dev-services/file-search.ts
109444
- import { readdir as readdir6 } from "node:fs/promises";
109445
- import { join as join25 } from "node:path";
109928
+ import { readdir as readdir7 } from "node:fs/promises";
109929
+ import { join as join26 } from "node:path";
109446
109930
  var MAX_FILES2 = 2e4;
109447
109931
  var TTL_MS2 = 15e3;
109448
109932
  var WALK_DEPTH = 6;
@@ -109470,7 +109954,7 @@ async function walk(root3) {
109470
109954
  const { dir, rel, depth } = queue.shift();
109471
109955
  let entries;
109472
109956
  try {
109473
- entries = await readdir6(dir, { withFileTypes: true });
109957
+ entries = await readdir7(dir, { withFileTypes: true });
109474
109958
  } catch {
109475
109959
  continue;
109476
109960
  }
@@ -109478,7 +109962,7 @@ async function walk(root3) {
109478
109962
  if (e.name.startsWith(".") && e.name !== ".env") continue;
109479
109963
  if (e.isDirectory()) {
109480
109964
  if (SKIP.has(e.name) || depth >= WALK_DEPTH) continue;
109481
- queue.push({ dir: join25(dir, e.name), rel: wireJoin(rel, e.name), depth: depth + 1 });
109965
+ queue.push({ dir: join26(dir, e.name), rel: wireJoin(rel, e.name), depth: depth + 1 });
109482
109966
  } else {
109483
109967
  out.push(wireJoin(rel, e.name).normalize("NFC"));
109484
109968
  if (out.length >= MAX_FILES2) break;
@@ -109543,14 +110027,14 @@ async function searchFiles(root3, query, limit = 20, trust = {}) {
109543
110027
  }
109544
110028
 
109545
110029
  // packages/agent-host/src/dev-services/attachments.ts
109546
- import { mkdir as mkdir4, writeFile as writeFile2, rm as rm4 } from "node:fs/promises";
109547
- import { extname as extname3, join as join26 } from "node:path";
109548
- var root = () => join26(dataRoot(), "attachments");
110030
+ import { mkdir as mkdir5, writeFile as writeFile3, rm as rm5 } from "node:fs/promises";
110031
+ import { extname as extname3, join as join27 } from "node:path";
110032
+ var root = () => join27(dataRoot(), "attachments");
109549
110033
  function sessionDir(sessionId) {
109550
110034
  if (!isSessionId(sessionId)) {
109551
110035
  throw Object.assign(new Error(`Not a session id: ${sessionId}`), { code: "internal" });
109552
110036
  }
109553
- return join26(root(), sessionId);
110037
+ return join27(root(), sessionId);
109554
110038
  }
109555
110039
  var EXT = {
109556
110040
  "image/png": ".png",
@@ -109560,43 +110044,43 @@ var EXT = {
109560
110044
  };
109561
110045
  async function saveAttachment(sessionId, name, mime, dataBase64) {
109562
110046
  const dir = sessionDir(sessionId);
109563
- await mkdir4(dir, { recursive: true });
110047
+ await mkdir5(dir, { recursive: true });
109564
110048
  const ext2 = extname3(name) || EXT[mime] || "";
109565
- const file2 = join26(dir, `${Date.now()}-${Math.random().toString(36).slice(2, 8)}${ext2}`);
110049
+ const file2 = join27(dir, `${Date.now()}-${Math.random().toString(36).slice(2, 8)}${ext2}`);
109566
110050
  const buf = Buffer.from(dataBase64, "base64");
109567
- await writeFile2(file2, buf);
110051
+ await writeFile3(file2, buf);
109568
110052
  return { kind: mime.startsWith("image/") ? "image" : "file", path: file2, name, mime, bytes: buf.length };
109569
110053
  }
109570
110054
  async function clearAttachments(sessionId) {
109571
- await rm4(sessionDir(sessionId), { recursive: true, force: true });
110055
+ await rm5(sessionDir(sessionId), { recursive: true, force: true });
109572
110056
  }
109573
110057
  async function attachmentBytes(sessionId) {
109574
- const { readdir: readdir9, lstat: lstat6 } = await import("node:fs/promises");
110058
+ const { readdir: readdir10, lstat: lstat7 } = await import("node:fs/promises");
109575
110059
  const dir = sessionDir(sessionId);
109576
- const names = await readdir9(dir).catch(() => []);
110060
+ const names = await readdir10(dir).catch(() => []);
109577
110061
  let total = 0;
109578
110062
  for (const name of names) {
109579
- const s = await lstat6(join26(dir, name)).catch(() => null);
110063
+ const s = await lstat7(join27(dir, name)).catch(() => null);
109580
110064
  if (s?.isFile()) total += s.size;
109581
110065
  }
109582
110066
  return total;
109583
110067
  }
109584
110068
  var ATTACHMENTS_MAX_BYTES = 500 * 1048576;
109585
110069
  async function sweepAttachments(maxBytes = ATTACHMENTS_MAX_BYTES) {
109586
- const { readdir: readdir9, stat: stat6, rm: rmFile } = await import("node:fs/promises");
110070
+ const { readdir: readdir10, stat: stat7, rm: rmFile } = await import("node:fs/promises");
109587
110071
  const files = [];
109588
110072
  let dirs;
109589
110073
  try {
109590
- dirs = await readdir9(root());
110074
+ dirs = await readdir10(root());
109591
110075
  } catch {
109592
110076
  return 0;
109593
110077
  }
109594
110078
  for (const d2 of dirs) {
109595
- const dir = join26(root(), d2);
109596
- const names = await readdir9(dir).catch(() => []);
110079
+ const dir = join27(root(), d2);
110080
+ const names = await readdir10(dir).catch(() => []);
109597
110081
  for (const name of names) {
109598
- const p = join26(dir, name);
109599
- const s = await stat6(p).catch(() => null);
110082
+ const p = join27(dir, name);
110083
+ const s = await stat7(p).catch(() => null);
109600
110084
  if (s?.isFile()) files.push({ path: p, size: s.size, mtime: s.mtimeMs });
109601
110085
  }
109602
110086
  }
@@ -109616,7 +110100,7 @@ async function sweepAttachments(maxBytes = ATTACHMENTS_MAX_BYTES) {
109616
110100
 
109617
110101
  // packages/agent-host/src/dev-services/message-image.ts
109618
110102
  import { constants as constants7 } from "node:fs";
109619
- import { open as open4, realpath as realpath6 } from "node:fs/promises";
110103
+ import { open as open4, realpath as realpath7 } from "node:fs/promises";
109620
110104
  import { homedir as homedir11 } from "node:os";
109621
110105
  import { posix as posix3, sep as sep10, win32 as win326 } from "node:path";
109622
110106
  import { fileURLToPath as fileURLToPath5 } from "node:url";
@@ -109673,7 +110157,7 @@ var mb2 = (n) => `${(n / 1e6).toFixed(1)} MB`;
109673
110157
  async function readMessageImage(abs, max = IMAGE_PREVIEW_MAX_BYTES) {
109674
110158
  let file2;
109675
110159
  try {
109676
- file2 = await realpath6(abs);
110160
+ file2 = await realpath7(abs);
109677
110161
  } catch (e) {
109678
110162
  const code2 = e.code;
109679
110163
  if (code2 === "EACCES" || code2 === "EPERM") return refuse5("unreadable", `Centralu may not read ${abs}`);
@@ -109712,36 +110196,36 @@ async function readMessageImage(abs, max = IMAGE_PREVIEW_MAX_BYTES) {
109712
110196
  }
109713
110197
 
109714
110198
  // packages/agent-host/src/dev-services/handoff-notes.ts
109715
- import { lstat as lstat5, mkdir as mkdir5, readdir as readdir7, rm as rm5, writeFile as writeFile3 } from "node:fs/promises";
109716
- import { join as join27 } from "node:path";
109717
- var root2 = () => join27(dataRoot(), "handoff");
110199
+ import { lstat as lstat6, mkdir as mkdir6, readdir as readdir8, rm as rm6, writeFile as writeFile4 } from "node:fs/promises";
110200
+ import { join as join28 } from "node:path";
110201
+ var root2 = () => join28(dataRoot(), "handoff");
109718
110202
  function handoffNoteDir(projectId) {
109719
110203
  if (!isProjectId(projectId)) {
109720
110204
  throw Object.assign(new Error(`Not a project id: ${projectId}`), { code: "internal" });
109721
110205
  }
109722
- return join27(root2(), projectId);
110206
+ return join28(root2(), projectId);
109723
110207
  }
109724
110208
  function handoffNotePath(projectId, sessionId) {
109725
110209
  if (!isSessionId(sessionId)) {
109726
110210
  throw Object.assign(new Error(`Not a session id: ${sessionId}`), { code: "internal" });
109727
110211
  }
109728
- return join27(handoffNoteDir(projectId), `${sessionId}.md`);
110212
+ return join28(handoffNoteDir(projectId), `${sessionId}.md`);
109729
110213
  }
109730
110214
  async function handoffNoteBytes(projectId, sessionId) {
109731
- const s = await lstat5(handoffNotePath(projectId, sessionId)).catch(() => null);
110215
+ const s = await lstat6(handoffNotePath(projectId, sessionId)).catch(() => null);
109732
110216
  return s?.isFile() ? s.size : 0;
109733
110217
  }
109734
110218
  async function writeHandoffNote(projectId, sessionId, text2) {
109735
110219
  const path2 = handoffNotePath(projectId, sessionId);
109736
- await mkdir5(handoffNoteDir(projectId), { recursive: true });
109737
- await rm5(path2, { force: true });
109738
- await writeFile3(path2, text2, "utf8");
110220
+ await mkdir6(handoffNoteDir(projectId), { recursive: true });
110221
+ await rm6(path2, { force: true });
110222
+ await writeFile4(path2, text2, "utf8");
109739
110223
  return path2;
109740
110224
  }
109741
110225
  async function sweepHandoffNotes(owned, projectId) {
109742
110226
  let dirs;
109743
110227
  try {
109744
- dirs = projectId ? [projectId] : (await readdir7(root2(), { withFileTypes: true })).filter((d2) => d2.isDirectory()).map((d2) => d2.name);
110228
+ dirs = projectId ? [projectId] : (await readdir8(root2(), { withFileTypes: true })).filter((d2) => d2.isDirectory()).map((d2) => d2.name);
109745
110229
  } catch {
109746
110230
  return;
109747
110231
  }
@@ -109752,12 +110236,12 @@ async function sweepHandoffNotes(owned, projectId) {
109752
110236
  } catch {
109753
110237
  continue;
109754
110238
  }
109755
- const entries = await readdir7(dir, { withFileTypes: true }).catch(() => []);
110239
+ const entries = await readdir8(dir, { withFileTypes: true }).catch(() => []);
109756
110240
  for (const e of entries) {
109757
110241
  if (!e.isFile() || !e.name.endsWith(".md")) continue;
109758
110242
  const owner = e.name.slice(0, -".md".length);
109759
110243
  if (owned(owner)) continue;
109760
- await rm5(join27(dir, e.name), { force: true }).catch(() => {
110244
+ await rm6(join28(dir, e.name), { force: true }).catch(() => {
109761
110245
  });
109762
110246
  }
109763
110247
  }
@@ -109831,7 +110315,7 @@ var HUB_SENDER = "_hub-orchestrator";
109831
110315
  var builderKey = (ref) => `${ref.projectId ?? "_user"}/${ref.appId}`;
109832
110316
  var APP_BUILDERS_KEY = "apps.builders";
109833
110317
  var SessionManager = class {
109834
- constructor(store2, adapters2, emit, endpoint, worktreeRoot = join28(homedir12(), DATA_DIR, "worktrees"), keeperOpts = {}) {
110318
+ constructor(store2, adapters2, emit, endpoint, worktreeRoot = join29(homedir12(), DATA_DIR, "worktrees"), keeperOpts = {}) {
109835
110319
  this.store = store2;
109836
110320
  this.adapters = adapters2;
109837
110321
  this.emit = emit;
@@ -109844,10 +110328,17 @@ var SessionManager = class {
109844
110328
  rawEmit(withoutToolRecord(full));
109845
110329
  };
109846
110330
  const LIVE_ONLY = ["working", "waiting_approval"];
109847
- for (const s of store2.listSessions()) {
110331
+ const storedSessions = store2.listSessions();
110332
+ const storedProjects = new Map(storedSessions.map((s) => [s.id, s.projectId]));
110333
+ for (const s of storedSessions) {
109848
110334
  const stale = LIVE_ONLY.includes(s.state) && !keeperOpts.keptSessions?.has(s.id);
109849
110335
  const fixed = stale ? { ...s, state: "idle", waitingSince: null } : s;
110336
+ const ownerProject = fixed.askedBy ? storedProjects.get(fixed.askedBy) ?? store2.trashedSession(fixed.askedBy)?.record.projectId : void 0;
110337
+ const first = fixed.askedBy && ownerProject === void 0 ? store2.loadMessagesFrom(fixed.id, 0, 1)[0]?.payload : void 0;
110338
+ const oldAsk = fixed.askedBy && (ownerProject !== void 0 ? ownerProject !== fixed.projectId : first?.from?.sessionId === fixed.askedBy && first.text?.includes("through Centralu's ask_project"));
110339
+ if (oldAsk) delete fixed.askedBy;
109850
110340
  this.meta.set(s.id, fixed);
110341
+ if (oldAsk) store2.upsertSession(fixed);
109851
110342
  if (stale) {
109852
110343
  console.error(`[agent-host] stale state reset: ${s.id.slice(0, 8)} ${s.state} -> idle`);
109853
110344
  store2.upsertSession(fixed);
@@ -109950,6 +110441,8 @@ var SessionManager = class {
109950
110441
  * app-server), even though the screen said "starting next turn".
109951
110442
  */
109952
110443
  restartAfterTurn = /* @__PURE__ */ new Set();
110444
+ /** Input arriving after a note completes waits for its deferred process replacement, including disposal. */
110445
+ deferredRestarts = /* @__PURE__ */ new Map();
109953
110446
  /** A new host served a different Centralu tool prefix; replace the old agent at its safe boundary. */
109954
110447
  definitionsRestartAfterTurn = /* @__PURE__ */ new Set();
109955
110448
  /** A kept process was started with adapter settings from an older host build (#589). */
@@ -109986,6 +110479,8 @@ var SessionManager = class {
109986
110479
  * comment on `runAppAgent`).
109987
110480
  */
109988
110481
  agentRuns = /* @__PURE__ */ new Map();
110482
+ /** Temporary readers of a person's turn; they never own work or pin the calling session awake. */
110483
+ turnObservers = /* @__PURE__ */ new Map();
109989
110484
  /**
109990
110485
  * A card the host raises in a session's approval slot — requestId -> question: an app's capability question (M4
109991
110486
  * D-4), or one project reaching another (#371). It uses the same slot as the adapter's card (the session's
@@ -109994,8 +110489,8 @@ var SessionManager = class {
109994
110489
  */
109995
110490
  hostAsks = /* @__PURE__ */ new Map();
109996
110491
  /**
109997
- * ask_project's delegations still owed to their caller (#371 part B) — `<caller session>><target project>` -> the
109998
- * delegation. One per pair at a time: a second task while the first runs is refused, and a call with no task waits
110492
+ * ask_project's delegations still owed to their caller (#371 part B) — `<caller session>><target project>><target session>` -> the
110493
+ * delegation. One per chosen session at a time: a second task while the first runs is refused, and a call with no task waits
109999
110494
  * on this one. Removed once the caller has read the outcome; an outcome nobody comes back for goes with the caller.
110000
110495
  */
110001
110496
  delegations = /* @__PURE__ */ new Map();
@@ -110057,11 +110552,11 @@ var SessionManager = class {
110057
110552
  );
110058
110553
  }
110059
110554
  async addProject(path2) {
110060
- if (!existsSync18(path2) || !statSync12(path2).isDirectory()) {
110555
+ if (!existsSync19(path2) || !statSync12(path2).isDirectory()) {
110061
110556
  throw Object.assign(new Error(`Directory not found: ${path2}`), { code: "internal" });
110062
110557
  }
110063
110558
  const existing = this.store.findProjectByPath(path2);
110064
- const id2 = existing?.id ?? randomUUID7();
110559
+ const id2 = existing?.id ?? randomUUID10();
110065
110560
  this.store.addProject({ id: id2, path: path2, name: basename5(path2) });
110066
110561
  return this.projectInfo(id2, path2);
110067
110562
  }
@@ -110073,8 +110568,8 @@ var SessionManager = class {
110073
110568
  projectsHome = () => {
110074
110569
  throw Object.assign(new Error("This host has no default location for a new project \u2014 give a parent folder"), { code: "internal" });
110075
110570
  };
110076
- useProjectsHome(resolve11) {
110077
- this.projectsHome = resolve11;
110571
+ useProjectsHome(resolve12) {
110572
+ this.projectsHome = resolve12;
110078
110573
  }
110079
110574
  /**
110080
110575
  * The folder a new project would get, and whether a clone source is one this host clones (#515) — checked, nothing
@@ -110575,6 +111070,9 @@ var SessionManager = class {
110575
111070
  * null — the only place that can create a session with no project is `orchestrator()` below.
110576
111071
  */
110577
111072
  async createSession(params) {
111073
+ if (params.askedBy && this.meta.get(params.askedBy)?.projectId !== params.projectId) {
111074
+ throw new Error("A session cannot create a session in another project");
111075
+ }
110578
111076
  if (!params.tool) {
110579
111077
  throw Object.assign(new Error("No agent adapter is available. Install and configure an agent CLI before creating a session."), { code: "tool_not_installed" });
110580
111078
  }
@@ -110589,7 +111087,7 @@ var SessionManager = class {
110589
111087
  );
110590
111088
  }
110591
111089
  }
110592
- const id2 = randomUUID7();
111090
+ const id2 = randomUUID10();
110593
111091
  let worktree = null;
110594
111092
  if (params.worktree && params.projectId) {
110595
111093
  const summary = await gitSummary(params.cwd, this.gitTrust(params.projectId));
@@ -110602,7 +111100,7 @@ var SessionManager = class {
110602
111100
  );
110603
111101
  }
110604
111102
  const path2 = this.worktreePathFor(params.projectId, id2);
110605
- if (!this.projectTrusted(params.projectId)) {
111103
+ if (!this.projectTrusted(params.projectId) && !(params.askedBy && this.autoBoundaryOpen(params.askedBy, "worktree in an untrusted project"))) {
110606
111104
  throw Object.assign(
110607
111105
  new Error("Worktree sessions need a trusted project: checking out runs the repository's hooks and filters. Trust the project first."),
110608
111106
  { code: "internal" }
@@ -110993,6 +111491,7 @@ var SessionManager = class {
110993
111491
  );
110994
111492
  }
110995
111493
  async doResumeSession(sessionId, adopt) {
111494
+ if (this.worktreeCleanup.has(sessionId)) throw Object.assign(new Error("The session worktree is being removed"), { code: "internal" });
110996
111495
  const m = this.meta.get(sessionId);
110997
111496
  if (!m) throw Object.assign(new Error(`Session not found: ${sessionId}`), { code: "session_not_found" });
110998
111497
  if (this.handles.has(sessionId)) return { session: m, resumed: true };
@@ -111181,6 +111680,7 @@ var SessionManager = class {
111181
111680
  * exactly as it was, and only what happens from here on piles up in the copy.
111182
111681
  */
111183
111682
  async forkConversation(sessionId) {
111683
+ if (this.worktreeCleanup.has(sessionId)) throw Object.assign(new Error("The session worktree is being removed"), { code: "internal" });
111184
111684
  const m = this.meta.get(sessionId);
111185
111685
  if (!m) throw Object.assign(new Error(`Session not found: ${sessionId}`), { code: "session_not_found" });
111186
111686
  const source = m.externalId ?? m.importedFrom;
@@ -111231,7 +111731,8 @@ var SessionManager = class {
111231
111731
  * delete knows that layout (the Claude SDK's `deleteSession`). Left in place it is also a second way back — the
111232
111732
  * tool, and **+ → Past conversations**, still have it.
111233
111733
  */
111234
- async trashSession(sessionId, removeWorktree = false, removeExternal = false) {
111734
+ async trashSession(sessionId, removeWorktree = false, removeExternal = false, recovery) {
111735
+ if (this.worktreeCleanup.has(sessionId) && !recovery) throw Object.assign(new Error("The session worktree is being removed"), { code: "internal" });
111235
111736
  const m = this.meta.get(sessionId);
111236
111737
  if (!m) {
111237
111738
  this.emit({ type: "session_deleted", sessionId });
@@ -111247,6 +111748,7 @@ var SessionManager = class {
111247
111748
  agentRun.deleted = true;
111248
111749
  agentRun.fail(new Error("the person deleted the agent session before it answered"));
111249
111750
  }
111751
+ for (const observer of this.turnObservers.get(sessionId) ?? []) observer.fail(new Error("the person deleted the session before it answered"));
111250
111752
  for (const [key, d2] of this.delegations) {
111251
111753
  if (d2.callerId !== sessionId) continue;
111252
111754
  this.delegations.delete(key);
@@ -111280,7 +111782,8 @@ var SessionManager = class {
111280
111782
  projectName: project2?.name ?? null,
111281
111783
  projectPath: project2?.path ?? null,
111282
111784
  removeExternal: removeExternal && !!externalId,
111283
- removeWorktree: removeWorktree && !!m.worktree
111785
+ removeWorktree: removeWorktree && !!m.worktree,
111786
+ ...recovery ? { recovery } : {}
111284
111787
  });
111285
111788
  this.appsHub?.sessionGone(sessionId);
111286
111789
  this.emit({ type: "session_deleted", sessionId });
@@ -111288,7 +111791,7 @@ var SessionManager = class {
111288
111791
  /** Only one trash operation per session at a time — a purge racing a restore would restore half of a deleted session */
111289
111792
  trashBusy = /* @__PURE__ */ new Set();
111290
111793
  async trashOp(sessionId, run) {
111291
- if (this.trashBusy.has(sessionId)) {
111794
+ if (this.trashBusy.has(sessionId) || this.worktreeCleanup.has(sessionId)) {
111292
111795
  throw Object.assign(new Error("That session is already being restored or deleted"), { code: "internal" });
111293
111796
  }
111294
111797
  this.trashBusy.add(sessionId);
@@ -111305,7 +111808,7 @@ var SessionManager = class {
111305
111808
  this.store.listTrash().map(async (r) => {
111306
111809
  const { record: record2 } = r;
111307
111810
  const live = record2.projectId ? projects.get(record2.projectId) : void 0;
111308
- const files = await attachmentBytes(r.id).catch(() => 0) + (record2.projectId ? await handoffNoteBytes(record2.projectId, r.id).catch(() => 0) : 0);
111811
+ 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);
111309
111812
  return {
111310
111813
  id: r.id,
111311
111814
  name: r.name,
@@ -111321,7 +111824,7 @@ var SessionManager = class {
111321
111824
  bytes: r.bytes + files,
111322
111825
  conversationFile: !r.hasConversationFile ? "none" : record2.removeExternal ? "remove" : "keep",
111323
111826
  // Only a worktree still on disk is listed — the merged-worktree cleanup removes the folder before the trash is emptied
111324
- worktree: r.worktree && existsSync18(r.worktree.path) ? { ...r.worktree, remove: record2.removeWorktree } : null
111827
+ worktree: r.worktree && existsSync19(r.worktree.path) ? { ...r.worktree, remove: record2.removeWorktree } : null
111325
111828
  };
111326
111829
  })
111327
111830
  );
@@ -111359,7 +111862,7 @@ var SessionManager = class {
111359
111862
  const home = projects.find((p) => p.id === record2.projectId) ?? (record2.projectPath ? projects.find((p) => p.path === record2.projectPath) : void 0);
111360
111863
  if (home) {
111361
111864
  projectId = home.id;
111362
- } else if (record2.projectPath && existsSync18(record2.projectPath) && statSync12(record2.projectPath).isDirectory()) {
111865
+ } else if (record2.projectPath && existsSync19(record2.projectPath) && statSync12(record2.projectPath).isDirectory()) {
111363
111866
  this.store.addProject({
111364
111867
  id: record2.projectId,
111365
111868
  path: record2.projectPath,
@@ -111378,14 +111881,21 @@ var SessionManager = class {
111378
111881
  } else if (t.session.kind === "orchestrator" && this.store.orchestratorId()) {
111379
111882
  throw Object.assign(new Error("Another orchestrator is in place \u2014 the app runs one at a time"), { code: "internal" });
111380
111883
  }
111884
+ let recovered;
111885
+ if (record2.recovery && t.session.worktree && projectId) {
111886
+ const repo = this.store.listProjects().find((p) => p.id === projectId).path;
111887
+ recovered = await restoreWorktreeRecovery(sessionId, repo, t.session.worktree.path, t.session.worktree.branch, record2.recovery, this.gitTrust(projectId));
111888
+ }
111381
111889
  const back = await this.store.restoreSession(sessionId, projectId);
111382
111890
  if (!back) throw Object.assign(new Error(`Not in the trash: ${sessionId}`), { code: "session_not_found" });
111383
111891
  const LIVE_ONLY = ["working", "waiting_approval"];
111384
111892
  const fixed = LIVE_ONLY.includes(back.state) ? { ...back, state: "idle", waitingSince: null } : back;
111385
- if (fixed !== back) this.store.upsertSession(fixed);
111893
+ if (recovered && fixed.worktree) fixed.worktree = { ...fixed.worktree, branch: recovered.branch };
111894
+ if (fixed !== back || recovered) this.store.upsertSession(fixed);
111386
111895
  this.meta.set(sessionId, fixed);
111387
111896
  const session2 = { ...this.meta.get(sessionId), live: false };
111388
111897
  this.emit({ type: "session_created", sessionId, session: session2 });
111898
+ if (recovered) this.onEvent({ type: "notice", sessionId, level: "info", from: "Centralu", label: "worktree recovery", text: recovered.notice });
111389
111899
  return { session: session2, project: registered };
111390
111900
  });
111391
111901
  }
@@ -111424,6 +111934,7 @@ var SessionManager = class {
111424
111934
  await gitWorktreeRemove(record2.projectPath, s.worktree.path, true).catch(() => {
111425
111935
  });
111426
111936
  }
111937
+ await clearWorktreeRecovery(sessionId);
111427
111938
  await this.store.purgeSession(sessionId);
111428
111939
  await clearAttachments(sessionId).catch(() => {
111429
111940
  });
@@ -111496,6 +112007,7 @@ var SessionManager = class {
111496
112007
  return live && m && live.model !== m.model ? "model_change" : void 0;
111497
112008
  }
111498
112009
  async updateSettings(sessionId, s) {
112010
+ if (this.worktreeCleanup.has(sessionId)) throw Object.assign(new Error("The session worktree is being removed"), { code: "internal" });
111499
112011
  const m = this.meta.get(sessionId);
111500
112012
  if (!m) throw Object.assign(new Error(`Session not found: ${sessionId}`), { code: "session_not_found" });
111501
112013
  if (s.model !== void 0 && s.model !== m.model) this.cacheLedger.note(sessionId, "model_change");
@@ -111610,20 +112122,31 @@ var SessionManager = class {
111610
112122
  }
111611
112123
  }
111612
112124
  }
112125
+ const run = e.sessionId ? this.agentRuns.get(e.sessionId) : void 0;
112126
+ run?.onEvent(e);
112127
+ if (e.sessionId) for (const observer of this.turnObservers.get(e.sessionId) ?? []) observer.onEvent(e);
111613
112128
  this.emit(seq != null ? { ...e, seq } : e);
111614
- if (endedTurn && e.sessionId) void this.reportBackIfAwaited(e.sessionId, e);
111615
- if (e.sessionId) this.agentRuns.get(e.sessionId)?.onEvent(e);
112129
+ if (endedTurn && e.sessionId && !run?.tracksInputs) void this.reportBackIfAwaited(e.sessionId, e, run?.completedAnswer);
111616
112130
  if (e.type === "approval_resolved" && e.sessionId) this.raiseHostAsks(e.sessionId);
111617
112131
  if (endedTurn && e.sessionId) this.forgetCommits(e.sessionId);
111618
- if (endedTurn && e.sessionId && this.restartAfterTurn.delete(e.sessionId)) {
111619
- const apps = this.appsRestartAfterTurn.delete(e.sessionId);
111620
- const definitions = this.definitionsRestartAfterTurn.delete(e.sessionId);
111621
- const launchSettings = this.launchSettingsRestartAfterTurn.delete(e.sessionId);
111622
- if ((apps || definitions || launchSettings || this.settingsDrifted(e.sessionId)) && this.handles.has(e.sessionId)) {
111623
- void this.restartSession(e.sessionId, launchSettings ? "launch_settings" : apps ? "thread_restart" : definitions ? "instructions" : this.restartCause(e.sessionId)).catch(() => {
111624
- });
111625
- }
111626
- }
112132
+ if (endedTurn && e.sessionId) this.applyDeferredRestart(e.sessionId);
112133
+ }
112134
+ /** A note receipt follows turn_complete: re-check the same deferred change when the last hold is released. */
112135
+ applyDeferredRestart(sessionId) {
112136
+ const session2 = this.meta.get(sessionId);
112137
+ if (!session2 || inTurn(session2.state) || this.deferredRestarts.has(sessionId) || this.agentRuns.get(sessionId)?.waitingForInputs || !this.restartAfterTurn.delete(sessionId)) return;
112138
+ const apps = this.appsRestartAfterTurn.delete(sessionId);
112139
+ const definitions = this.definitionsRestartAfterTurn.delete(sessionId);
112140
+ const launchSettings = this.launchSettingsRestartAfterTurn.delete(sessionId);
112141
+ if (!(apps || definitions || launchSettings || this.settingsDrifted(sessionId)) || !this.handles.has(sessionId)) return;
112142
+ const cause2 = launchSettings ? "launch_settings" : apps ? "thread_restart" : definitions ? "instructions" : this.restartCause(sessionId);
112143
+ const restarting = Promise.resolve().then(() => this.restartSession(sessionId, cause2)).then(() => {
112144
+ }).finally(() => {
112145
+ if (this.deferredRestarts.get(sessionId) === restarting) this.deferredRestarts.delete(sessionId);
112146
+ });
112147
+ this.deferredRestarts.set(sessionId, restarting);
112148
+ void restarting.catch(() => {
112149
+ });
111627
112150
  }
111628
112151
  /**
111629
112152
  * Notifies the orchestrator when the work it assigned finishes — **the other half of "one window".**
@@ -111637,14 +112160,14 @@ var SessionManager = class {
111637
112160
  * - Once notified, the marker is cleared. Otherwise that session would wake the orchestrator on
111638
112161
  * every turn it runs on its own afterward — which would become a loop of waking each other
111639
112162
  */
111640
- async reportBackIfAwaited(sessionId, ending) {
112163
+ async reportBackIfAwaited(sessionId, ending, answer) {
111641
112164
  const orchestratorId = this.awaitingReport.get(sessionId);
111642
112165
  if (!orchestratorId) return;
111643
112166
  this.awaitingReport.delete(sessionId);
111644
112167
  const target = this.meta.get(sessionId);
111645
112168
  if (!target || !this.meta.has(orchestratorId)) return;
111646
112169
  const project2 = target.projectId ? this.store.listProjects().find((p) => p.id === target.projectId)?.name ?? "(project no longer exists)" : "(none)";
111647
- await this.deliverReport(orchestratorId, sessionId, target.name, project2, previewOf(this.store, sessionId, 600), ending);
112170
+ await this.deliverReport(orchestratorId, sessionId, target.name, project2, answer?.slice(0, 600) ?? previewOf(this.store, sessionId, 600), ending);
111648
112171
  }
111649
112172
  /** Reports the end of a turn the orchestrator asked to hear about, as one message in its session (#166) */
111650
112173
  async deliverReport(orchestratorId, sessionId, name, project2, preview, ending) {
@@ -111980,7 +112503,10 @@ If you need more, you can read that session's recent conversation with read_sess
111980
112503
  * exposed on send(), which anything else can call — a boundary that can be flipped on by accident
111981
112504
  * is not a boundary.
111982
112505
  */
111983
- async deliver(sessionId, text2, attachments, from, relayed, fromApp, fromAppVia = "inline") {
112506
+ async deliver(sessionId, text2, attachments, from, relayed, fromApp, fromAppVia = "inline", sendInput) {
112507
+ const restarting = this.deferredRestarts.get(sessionId);
112508
+ if (restarting) await restarting;
112509
+ if (this.worktreeCleanup.has(sessionId)) throw Object.assign(new Error("The session worktree is being removed"), { code: "internal" });
111984
112510
  const m = this.meta.get(sessionId);
111985
112511
  if (!m) throw Object.assign(new Error(`Session not found: ${sessionId}`), { code: "session_not_found" });
111986
112512
  if (!this.handles.has(sessionId)) {
@@ -111991,6 +112517,7 @@ If you need more, you can read that session's recent conversation with read_sess
111991
112517
  });
111992
112518
  }
111993
112519
  }
112520
+ if (this.worktreeCleanup.has(sessionId)) throw Object.assign(new Error("The session worktree is being removed"), { code: "internal" });
111994
112521
  const h = this.requireHandle(sessionId);
111995
112522
  this.closeStream(sessionId);
111996
112523
  const seq = this.store.nextSeq(sessionId);
@@ -112027,7 +112554,8 @@ If you need more, you can read that session's recent conversation with read_sess
112027
112554
  const adapterText = fromApp ? appMessageFrame(fromApp, text2, fromAppVia) : from && (relayed || this.directs(sessionId) && !this.directs(from.sessionId) && from.sessionId !== HUB_SENDER) ? untrustedSourceSessionNotification(from.sessionId) : attachments?.length ? `${text2}
112028
112555
 
112029
112556
  ${attachments.map((a) => `@${a.path}`).join("\n")}` : text2;
112030
- h.send(adapterText);
112557
+ if (sendInput) sendInput(h, adapterText);
112558
+ else h.send(adapterText);
112031
112559
  }
112032
112560
  /**
112033
112561
  * Switches a session's agent (claude <-> codex).
@@ -112044,6 +112572,7 @@ ${attachments.map((a) => `@${a.path}`).join("\n")}` : text2;
112044
112572
  * untouched, and only the tool underneath it is swapped.
112045
112573
  */
112046
112574
  async switchTool(sessionId, tool) {
112575
+ if (this.worktreeCleanup.has(sessionId)) throw Object.assign(new Error("The session worktree is being removed"), { code: "internal" });
112047
112576
  const m = this.meta.get(sessionId);
112048
112577
  if (!m) throw Object.assign(new Error(`Session not found: ${sessionId}`), { code: "session_not_found" });
112049
112578
  if (m.tool === tool) return { ...m, live: this.handles.has(sessionId) };
@@ -112325,7 +112854,7 @@ ${attachments.map((a) => `@${a.path}`).join("\n")}` : text2;
112325
112854
  worktreePathFor(projectId, sessionId) {
112326
112855
  if (!isProjectId(projectId)) throw Object.assign(new Error(`Not a project id: ${projectId}`), { code: "internal" });
112327
112856
  if (!isSessionId(sessionId)) throw Object.assign(new Error(`Not a session id: ${sessionId}`), { code: "internal" });
112328
- return join28(this.worktreeRoot, projectId, sessionId);
112857
+ return join29(this.worktreeRoot, projectId, sessionId);
112329
112858
  }
112330
112859
  /**
112331
112860
  * Sets up a new worktree's workspace (#69): copy configured files, reuse JS dependencies, run setup.
@@ -112508,7 +113037,7 @@ ${String(stderr).slice(0, 2e3)}`
112508
113037
  const base2 = this.cwdFor(m);
112509
113038
  const paths = /* @__PURE__ */ new Set();
112510
113039
  for (const p of e.paths) {
112511
- const rel = relative8(root3, resolve10(base2, p));
113040
+ const rel = relative8(root3, resolve11(base2, p));
112512
113041
  if (!rel || rel === ".." || rel.startsWith(`..${sep11}`) || isAbsolute9(rel)) continue;
112513
113042
  paths.add(rel.replaceAll(sep11, "/"));
112514
113043
  }
@@ -112708,8 +113237,10 @@ ${String(stderr).slice(0, 2e3)}`
112708
113237
  * reports background work.
112709
113238
  */
112710
113239
  sessionIdle(sessionId) {
113240
+ if (this.worktreeCleanup.has(sessionId)) return { idle: false, reason: "turn" };
112711
113241
  const m = this.meta.get(sessionId);
112712
113242
  if (!m) throw Object.assign(new Error(`Session not found: ${sessionId}`), { code: "session_not_found" });
113243
+ if (this.agentRuns.get(sessionId)?.waitingForInputs) return { idle: false, reason: "turn" };
112713
113244
  return sessionIdle(m, {
112714
113245
  running: this.handles.has(sessionId),
112715
113246
  reportsBackground: !!this.adapters.get(m.tool)?.capabilities.backgroundTasks
@@ -112717,7 +113248,7 @@ ${String(stderr).slice(0, 2e3)}`
112717
113248
  }
112718
113249
  /**
112719
113250
  * The sessions whose running work this one waits on (#568, the sleep rule in sleep.ts): a child it started with
112720
- * `start_agent`, or a session ask_project opened for it, while the turn it gave them runs, and a session that owes
113251
+ * `start_agent`, or a session handling its ask_project task, while the turn it gave them runs, and a session that owes
112721
113252
  * it a report (`reportBack`, `awaitingReport`). Their answer is about to come back to it.
112722
113253
  */
112723
113254
  waitsOn(sessionId) {
@@ -112737,7 +113268,7 @@ ${String(stderr).slice(0, 2e3)}`
112737
113268
  */
112738
113269
  async sleepSession(sessionId) {
112739
113270
  const h = this.handles.get(sessionId);
112740
- if (!h || this.resuming.has(sessionId) || this.sleeping.has(sessionId) || this.leaving.has(sessionId)) return false;
113271
+ if (!h || this.resuming.has(sessionId) || this.deferredRestarts.has(sessionId) || this.sleeping.has(sessionId) || this.leaving.has(sessionId)) return false;
112741
113272
  if (!this.sessionIdle(sessionId).idle || this.waitsOn(sessionId).length > 0) return false;
112742
113273
  this.handles.delete(sessionId);
112743
113274
  this.running.delete(sessionId);
@@ -112757,12 +113288,14 @@ ${String(stderr).slice(0, 2e3)}`
112757
113288
  return true;
112758
113289
  }
112759
113290
  interrupt(sessionId) {
112760
- this.requireHandle(sessionId).interrupt();
112761
- for (const d2 of this.delegations.values()) {
112762
- if (d2.callerId === sessionId && (!d2.agent || (d2.waiters ?? 0) > 0)) d2.stop("the person stopped the session that asked");
112763
- }
113291
+ const handle = this.requireHandle(sessionId);
112764
113292
  const run = this.agentRuns.get(sessionId);
112765
113293
  if (run) run.stoppedByPerson = true;
113294
+ for (const observer of this.turnObservers.get(sessionId) ?? []) observer.stoppedByPerson = true;
113295
+ handle.interrupt();
113296
+ for (const d2 of this.delegations.values()) {
113297
+ if (d2.callerId === sessionId && (d2.waiters ?? 0) > 0) d2.stop("the person stopped the session that asked");
113298
+ }
112766
113299
  }
112767
113300
  /**
112768
113301
  * Restarts only the agent (an extension of FR-10).
@@ -112770,6 +113303,7 @@ ${String(stderr).slice(0, 2e3)}`
112770
113303
  * process.
112771
113304
  */
112772
113305
  async restartSession(sessionId, cause2) {
113306
+ if (this.worktreeCleanup.has(sessionId)) throw Object.assign(new Error("The session worktree is being removed"), { code: "internal" });
112773
113307
  if (cause2) {
112774
113308
  this.restartReasons.set(sessionId, cause2);
112775
113309
  if (cause2 === "launch_settings") console.error(`[sessions] restarting ${sessionId}: launch settings changed or previously unrecorded`);
@@ -113377,10 +113911,11 @@ ${String(stderr).slice(0, 2e3)}`
113377
113911
  kind: "project_access",
113378
113912
  access: access2,
113379
113913
  from: { id: from.id, name: from.name },
113380
- // Said on the card (#382): a session opened there follows its trust, so the person should know before allowing
113914
+ // Said on the card (#382): the selected session there follows its trust, so the person should know before allowing
113381
113915
  to: { id: to.id, name: to.name, trusted: to.trusted },
113382
113916
  text: what.text,
113383
- ...what.app ? { app: what.app } : {}
113917
+ ...what.app ? { app: what.app } : {},
113918
+ ...what.session ? { session: what.session } : {}
113384
113919
  },
113385
113920
  signal
113386
113921
  );
@@ -113395,20 +113930,26 @@ ${String(stderr).slice(0, 2e3)}`
113395
113930
  }
113396
113931
  return { ok: false, error: `The question to the person was withdrawn before they answered, so ${to.name} was not reached.` };
113397
113932
  }
113398
- /**
113399
- * Raises a card of the host's own in a session's approval slot and waits for the person's answer (#371, #515):
113400
- * null when `signal` fires first, the card then taken down so none is left unanswerable.
113401
- */
113933
+ /** The caller's own preset, read at the boundary, never its project's or parent's preset. */
113934
+ autoBoundaryOpen(sessionId, boundary) {
113935
+ if (this.meta.get(sessionId)?.permissionPreset !== "auto") return false;
113936
+ if (boundary) console.error(`[permissions] auto session ${sessionId}: ${boundary} allowed`);
113937
+ return true;
113938
+ }
113939
+ /** Auto allows and logs; other presets raise a host card, withdrawn if the signal fires. */
113402
113940
  askHost(sessionId, detail, signal) {
113403
- return new Promise((resolve11) => {
113404
- if (signal?.aborted) return resolve11(null);
113405
- const requestId = `${detail.kind === "project_access" ? "xp" : detail.kind === "agent_start" ? "ag" : detail.kind === "centralu_update" ? "up" : "mk"}-${randomUUID7()}`;
113406
- this.hostAsks.set(requestId, { requestId, sessionId, detail, shown: false, resolve: resolve11 });
113941
+ if (signal?.aborted) return Promise.resolve(null);
113942
+ const note = detail.kind === "project_access" && !detail.to.trusted ? " (target not trusted)" : "";
113943
+ if (this.autoBoundaryOpen(sessionId, `${detail.kind}${note}`)) return Promise.resolve("allow");
113944
+ return new Promise((resolve12) => {
113945
+ if (signal?.aborted) return resolve12(null);
113946
+ const requestId = `${detail.kind === "project_access" ? "xp" : detail.kind === "agent_start" ? "ag" : detail.kind === "centralu_update" ? "up" : "mk"}-${randomUUID10()}`;
113947
+ this.hostAsks.set(requestId, { requestId, sessionId, detail, shown: false, resolve: resolve12 });
113407
113948
  signal?.addEventListener("abort", () => {
113408
113949
  if (!this.hostAsks.delete(requestId)) return;
113409
113950
  const m = this.meta.get(sessionId);
113410
113951
  if (m?.pendingApproval?.requestId === requestId) this.onEvent({ type: "approval_resolved", sessionId, requestId, decision: "deny" });
113411
- resolve11(null);
113952
+ resolve12(null);
113412
113953
  }, { once: true });
113413
113954
  this.raiseHostAsks(sessionId);
113414
113955
  });
@@ -113440,25 +113981,24 @@ ${String(stderr).slice(0, 2e3)}`
113440
113981
  * and a session that was itself asked by another project cannot ask a third: depth one, so two projects
113441
113982
  * that consented to each other cannot bounce a task between them without a person.
113442
113983
  * 2. The person's consent for the pair (`ensureProjectAccess`, kind 'delegate').
113443
- * 3. A session in the target: the idle one this caller asked there before, else a new one with that project's
113444
- * folder, instructions and default tool, under the `normal` preset — what a session the person opens there
113445
- * gets, so its own approvals stand as cards in that session, where the person sees them. Marked `askedBy`.
113984
+ * 3. An existing ordinary session in the target, named by name or id; no creation or ownership change.
113985
+ * Without a name exactly one is selected, otherwise its sessions are listed for the caller to choose.
113446
113986
  * 4. The task goes in a frame (`askFrame`) marked as sent by the caller, and the call waits for the turn, up to
113447
113987
  * `ASK_WAIT_MS`. Past it the answer is "still working" and a call with no task waits again.
113448
113988
  * 5. The final answer comes back cut to size, and the paths it names inside the target project become readable
113449
113989
  * to the caller (`readableGrants`).
113450
113990
  *
113451
113991
  * Stop on the caller, the call's own signal, or the person deleting either session ends the wait; the first two
113452
- * stop the delegated turn as well.
113992
+ * stop an assigned idle target only while waiting; a task that joined the person's turn only ends the wait.
113453
113993
  */
113454
113994
  async askProject(callerId, opts, signal) {
113455
113995
  const caller = this.meta.get(callerId);
113456
113996
  if (!caller?.projectId) return { ok: false, error: "This session belongs to no project, so it cannot ask another one" };
113457
- if (caller.askedBy) {
113997
+ if ((caller.askedBy || [...this.delegations.values()].some((d8) => d8.sessionId === callerId && !d8.agent && !d8.settled)) && !this.autoBoundaryOpen(callerId, "delegation depth")) {
113458
113998
  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." };
113459
113999
  }
113460
114000
  const projects = this.store.listProjects();
113461
- if (projects.find((p) => p.id === caller.projectId)?.trusted !== true) {
114001
+ if (projects.find((p) => p.id === caller.projectId)?.trusted !== true && !this.autoBoundaryOpen(callerId, "delegate from an untrusted project")) {
113462
114002
  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." };
113463
114003
  }
113464
114004
  const wanted = opts.project.trim();
@@ -113471,112 +114011,203 @@ ${String(stderr).slice(0, 2e3)}`
113471
114011
  };
113472
114012
  }
113473
114013
  if (target.id === caller.projectId) return { ok: false, error: `${target.name} is this session's own project \u2014 do the work here.` };
113474
- const key = `${callerId}>${target.id}`;
114014
+ const candidates = [...this.meta.values()].filter(
114015
+ (s) => s.projectId === target.id && s.kind === "worker" && !s.appId && !this.builderRefOf(s)
114016
+ );
114017
+ const list = () => candidates.map((s) => JSON.stringify({
114018
+ id: s.id,
114019
+ name: s.name,
114020
+ state: s.state,
114021
+ lastActive: lastActiveOf(this.store, s.id) ?? null
114022
+ })).join("\n");
114023
+ const wantedSession = opts.session?.trim();
114024
+ const outstanding = [...this.delegations.entries()].filter(
114025
+ ([, d8]) => d8.callerId === callerId && d8.target.id === target.id && !d8.agent
114026
+ );
114027
+ if (!opts.task && !wantedSession && outstanding.length === 1) {
114028
+ const [key2, pending2] = outstanding[0];
114029
+ return this.awaitDelegation(key2, pending2, signal);
114030
+ }
114031
+ const named = wantedSession ? candidates.filter((s) => s.name.toLowerCase() === wantedSession.toLowerCase()) : candidates;
114032
+ const chosen = candidates.find((s) => s.id === wantedSession) ?? (named.length === 1 ? named[0] : void 0);
114033
+ if (!chosen) {
114034
+ 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):
114035
+ ${list()}` : `${target.name} has no ordinary session. The person has to open one there; this call cannot create one.` };
114036
+ }
114037
+ const key = `${callerId}>${target.id}>${chosen.id}`;
113475
114038
  const pending = this.delegations.get(key);
113476
114039
  if (pending && !opts.task) return this.awaitDelegation(key, pending, signal);
113477
114040
  if (pending && !pending.settled) {
113478
- return {
113479
- ok: false,
113480
- 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.`
113481
- };
113482
- }
113483
- if (pending) this.delegations.delete(key);
113484
- if (!opts.task) return { ok: false, error: `Give a task: what ${target.name} should do.` };
113485
- const consent = await this.ensureProjectAccess(callerId, target.id, "delegate", { text: taskLine(opts.task) }, signal);
114041
+ 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.` };
114042
+ }
114043
+ if (!opts.task) return { ok: false, error: `Give a task: what ${target.name} / ${chosen.name} should do.` };
114044
+ const available = () => {
114045
+ const current = this.meta.get(chosen.id);
114046
+ if (!current || current.projectId !== target.id || current.kind !== "worker" || current.appId || this.builderRefOf(current)) return "The chosen session is no longer available";
114047
+ 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.`;
114048
+ 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.`;
114049
+ };
114050
+ const busy = available();
114051
+ if (busy) return { ok: false, error: busy };
114052
+ const consent = await this.ensureProjectAccess(callerId, target.id, "delegate", {
114053
+ text: taskLine(opts.task),
114054
+ session: { id: chosen.id, name: chosen.name }
114055
+ }, signal);
113486
114056
  if (!consent.ok) return consent;
113487
- 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];
113488
- let delegatedId;
113489
- if (reuse) {
113490
- delegatedId = reuse.id;
113491
- } else {
113492
- const tool = this.firstTool(target.defaultTool);
113493
- 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.` };
113494
- const adapter = this.adapters.get(tool);
113495
- const found = await adapter.detect();
113496
- if (!found.installed || !found.loggedIn) {
113497
- return { ok: false, error: `${target.name}'s agent (${adapter.descriptor.label}) cannot take the task: ${found.detail}` };
113498
- }
113499
- try {
113500
- const remembered = this.store.projectToolDefaults(target.id)[tool];
113501
- const info = await this.createSession({
113502
- projectId: target.id,
113503
- cwd: target.path,
113504
- tool,
113505
- ...remembered?.model ? { model: remembered.model } : {},
113506
- ...remembered?.effort ? { effort: remembered.effort } : {},
113507
- permissionPreset: "normal",
113508
- askedBy: callerId
113509
- });
113510
- delegatedId = info.id;
113511
- } catch (e) {
113512
- return { ok: false, error: `Could not open a session in ${target.name}: ${e.message}` };
113513
- }
113514
- const callerProject2 = projects.find((p) => p.id === caller.projectId)?.name ?? "another project";
113515
- this.rename(delegatedId, `Asked by ${callerProject2} \xB7 ${(/* @__PURE__ */ new Date()).toTimeString().slice(0, 5)}`);
113516
- }
113517
- const d2 = this.startDelegation(key, callerId, delegatedId, target);
114057
+ const changed = available();
114058
+ if (changed) return { ok: false, error: changed };
114059
+ if (signal?.aborted) return { ok: false, error: "The call was cancelled before delivery" };
114060
+ if (pending) this.delegations.delete(key);
114061
+ const delegatedId = chosen.id;
114062
+ const midTurn = inTurn(this.meta.get(delegatedId).state);
114063
+ let releaseInput;
114064
+ const d2 = this.startDelegation(key, callerId, delegatedId, target, false, midTurn);
113518
114065
  const callerProject = projects.find((p) => p.id === caller.projectId)?.name ?? "another project";
113519
114066
  try {
113520
- await this.deliver(delegatedId, askFrame(callerProject, opts.task), void 0, { sessionId: callerId, name: labelOf(this.store, caller) }, false);
114067
+ const from = { sessionId: callerId, name: labelOf(this.store, caller) };
114068
+ if (midTurn) {
114069
+ const releaseHold = d2.wait.holdInput();
114070
+ const release = () => {
114071
+ releaseHold();
114072
+ this.applyDeferredRestart(delegatedId);
114073
+ };
114074
+ releaseInput = release;
114075
+ await this.deliver(delegatedId, askFrame(callerProject, opts.task), void 0, from, false, void 0, "inline", (h, input) => {
114076
+ const fail4 = (e) => {
114077
+ d2.wait.fail(e);
114078
+ release();
114079
+ };
114080
+ void h.sendTracked(input, { complete: release, fail: fail4 }).then(release, fail4);
114081
+ });
114082
+ } else {
114083
+ await this.deliver(delegatedId, askFrame(callerProject, opts.task), void 0, from, false);
114084
+ }
113521
114085
  } catch (e) {
113522
- this.delegations.delete(key);
114086
+ d2.wait.fail(e);
114087
+ releaseInput?.();
114088
+ if (this.delegations.get(key) === d2) this.delegations.delete(key);
113523
114089
  if (this.agentRuns.get(delegatedId) === d2.wait) this.agentRuns.delete(delegatedId);
113524
114090
  return { ok: false, error: `Could not hand the task to ${target.name}: ${e.message}` };
113525
114091
  }
113526
114092
  return this.awaitDelegation(key, d2, signal);
113527
114093
  }
113528
114094
  /** Registers the wait on the delegated session's turn (the same watcher an app's agent uses, `AgentRunWait`) */
113529
- startDelegation(key, callerId, sessionId, target, agent = false) {
114095
+ startDelegation(key, callerId, sessionId, target, agent = false, joinedTurn = false) {
113530
114096
  let notice;
113531
- const wait = new AgentRunWait((message) => notice = message, () => this.meta.get(sessionId)?.name ?? "the session");
114097
+ let answerAfter = this.meta.get(sessionId)?.lastSeq ?? 0;
114098
+ const wait = new AgentRunWait((message) => notice = message, () => this.meta.get(sessionId)?.name ?? "the session", void 0, () => {
114099
+ const answer = finalAnswer(this.store.loadMessages(sessionId, 50).filter((m) => m.seq > answerAfter));
114100
+ answerAfter = this.meta.get(sessionId)?.lastSeq ?? answerAfter;
114101
+ return answer;
114102
+ });
114103
+ let cancelWait;
114104
+ const waitCancelled = new Promise((resolve12) => {
114105
+ cancelWait = resolve12;
114106
+ });
113532
114107
  const d2 = {
114108
+ joinedTurn,
114109
+ waitCancelled,
113533
114110
  callerId,
113534
114111
  sessionId,
113535
114112
  target,
113536
114113
  wait,
113537
- settled: false,
114114
+ get settled() {
114115
+ return wait.finished;
114116
+ },
113538
114117
  notice: () => notice,
113539
114118
  outcome: wait.done.then(
113540
- () => ({ ok: true }),
114119
+ (r) => ({ ok: true, answer: r.answer }),
113541
114120
  (e) => ({ ok: false, error: e.message })
113542
114121
  ),
113543
- stop: (why) => {
114122
+ stop: (why, targetStop = false) => {
113544
114123
  if (d2.settled) return;
114124
+ if (joinedTurn && !targetStop) {
114125
+ cancelWait({ ok: false, error: `${why}; the caller stopped waiting and the target session was left running` });
114126
+ return;
114127
+ }
114128
+ wait.fail(new Error(`${why}, so the delegated turn was stopped`));
113545
114129
  try {
113546
114130
  this.handles.get(sessionId)?.interrupt();
113547
114131
  } catch {
113548
114132
  }
113549
- wait.fail(new Error(`${why}, so the delegated turn was stopped`));
113550
114133
  },
113551
114134
  ...agent ? { agent: true } : {}
113552
114135
  };
113553
- void d2.outcome.then(() => {
113554
- d2.settled = true;
114136
+ void d2.outcome.then((r) => {
113555
114137
  if (this.agentRuns.get(sessionId) === wait) this.agentRuns.delete(sessionId);
114138
+ 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);
113556
114139
  });
113557
114140
  this.agentRuns.set(sessionId, wait);
113558
114141
  this.delegations.set(key, d2);
113559
114142
  return d2;
113560
114143
  }
114144
+ /** Read an existing turn without claiming it in delegations, agentRuns or the caller's sleep graph. */
114145
+ observeTurn(child, subscribe) {
114146
+ const active = this.agentRuns.get(child.id);
114147
+ const shared = active && !active.finished ? active : void 0;
114148
+ const wait = shared ?? new AgentRunWait(
114149
+ () => {
114150
+ },
114151
+ () => child.name,
114152
+ void 0,
114153
+ () => finalAnswer(this.store.loadMessages(child.id, 50))
114154
+ );
114155
+ const project2 = this.store.listProjects().find((p) => p.id === child.projectId);
114156
+ const target = { id: child.projectId ?? "", name: project2?.name ?? "", path: this.store.sessionCwd(child.id) ?? project2?.path ?? "" };
114157
+ if (!shared && subscribe) {
114158
+ const readers = this.turnObservers.get(child.id) ?? /* @__PURE__ */ new Set();
114159
+ readers.add(wait);
114160
+ this.turnObservers.set(child.id, readers);
114161
+ }
114162
+ return {
114163
+ run: {
114164
+ // This is the target's own turn, never work owned by its observer.
114165
+ callerId: child.id,
114166
+ sessionId: child.id,
114167
+ target,
114168
+ wait,
114169
+ get settled() {
114170
+ return wait.finished;
114171
+ },
114172
+ notice: () => void 0,
114173
+ outcome: wait.done.then(
114174
+ (r) => ({ ok: true, answer: r.answer }),
114175
+ (e) => ({ ok: false, error: e.message })
114176
+ ),
114177
+ stop: (why) => {
114178
+ if (wait.finished) return;
114179
+ wait.fail(new Error(`${why}, so the turn was stopped`));
114180
+ this.handles.get(child.id)?.interrupt();
114181
+ }
114182
+ },
114183
+ release: () => {
114184
+ if (shared) return;
114185
+ const readers = this.turnObservers.get(child.id);
114186
+ readers?.delete(wait);
114187
+ if (readers?.size === 0) this.turnObservers.delete(child.id);
114188
+ }
114189
+ };
114190
+ }
113561
114191
  /** Waits for a delegation up to the bound, and turns its outcome into what the caller reads */
113562
114192
  async awaitDelegation(key, d2, signal) {
113563
114193
  const sessionName = () => this.meta.get(d2.sessionId)?.name ?? d2.sessionId;
113564
114194
  let timer;
113565
114195
  let onAbort;
113566
- const bound = new Promise((resolve11) => timer = setTimeout(() => resolve11("bound"), ASK_WAIT_MS));
113567
- const aborted2 = new Promise((resolve11) => {
114196
+ const bound = new Promise((resolve12) => timer = setTimeout(() => resolve12("bound"), ASK_WAIT_MS));
114197
+ const aborted2 = new Promise((resolve12) => {
113568
114198
  if (!signal) return;
113569
- onAbort = () => resolve11("aborted");
114199
+ onAbort = () => resolve12("aborted");
113570
114200
  if (signal.aborted) onAbort();
113571
114201
  else signal.addEventListener("abort", onAbort, { once: true });
113572
114202
  });
114203
+ d2.waiters = (d2.waiters ?? 0) + 1;
113573
114204
  try {
113574
- const r = await Promise.race([d2.outcome, bound, aborted2]);
114205
+ const r = await Promise.race([d2.outcome, ...d2.waitCancelled ? [d2.waitCancelled] : [], bound, aborted2]);
113575
114206
  if (r === "aborted") {
113576
114207
  d2.stop("the call that asked was cancelled");
113577
- await d2.outcome;
113578
- this.delegations.delete(key);
113579
- return { ok: false, error: `The call was cancelled, so ${d2.target.name}'s session "${sessionName()}" was stopped.` };
114208
+ if (!d2.joinedTurn) await d2.outcome;
114209
+ if (this.delegations.get(key) === d2) this.delegations.delete(key);
114210
+ 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.` };
113580
114211
  }
113581
114212
  if (r === "bound") {
113582
114213
  const notice = d2.notice();
@@ -113586,10 +114217,10 @@ ${String(stderr).slice(0, 2e3)}`
113586
114217
  if (!r.ok) {
113587
114218
  return {
113588
114219
  ok: false,
113589
- error: `${d2.target.name}'s session "${sessionName()}" [${d2.sessionId}] did not finish: ${r.error}. Tell the person, or ask again with a clearer task.`
114220
+ 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.`
113590
114221
  };
113591
114222
  }
113592
- const answer = finalAnswer(this.store.loadMessages(d2.sessionId, 50));
114223
+ const answer = r.answer ?? finalAnswer(this.store.loadMessages(d2.sessionId, 50));
113593
114224
  const { granted, outside } = readableGrants(pathsIn(answer), d2.target.path);
113594
114225
  if (granted.length) {
113595
114226
  const set3 = this.readGrants.get(d2.callerId) ?? /* @__PURE__ */ new Set();
@@ -113607,6 +114238,7 @@ ${String(stderr).slice(0, 2e3)}`
113607
114238
  outside
113608
114239
  };
113609
114240
  } finally {
114241
+ d2.waiters = (d2.waiters ?? 1) - 1;
113610
114242
  clearTimeout(timer);
113611
114243
  if (onAbort) signal?.removeEventListener("abort", onAbort);
113612
114244
  }
@@ -113628,19 +114260,19 @@ ${String(stderr).slice(0, 2e3)}`
113628
114260
  async startAgent(callerId, opts, signal) {
113629
114261
  const caller = this.meta.get(callerId);
113630
114262
  if (!caller?.projectId) return { ok: false, error: "This session belongs to no project, so it cannot start agents." };
113631
- if (caller.askedBy) {
114263
+ if ((caller.askedBy || [...this.delegations.values()].some((d8) => d8.sessionId === callerId && !d8.agent && !d8.settled)) && !this.autoBoundaryOpen(callerId, "delegation depth")) {
113632
114264
  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." };
113633
114265
  }
113634
114266
  const project2 = this.store.listProjects().find((p) => p.id === caller.projectId);
113635
114267
  if (!project2) return { ok: false, error: "This session's project is no longer registered." };
113636
- if (project2.trusted !== true) {
114268
+ if (project2.trusted !== true && !this.autoBoundaryOpen(callerId, "start in an untrusted project")) {
113637
114269
  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." };
113638
114270
  }
113639
114271
  const tool = opts.tool ?? this.firstTool(project2.defaultTool);
113640
114272
  if (!tool) return { ok: false, error: "No agent adapter is available. Install and configure an agent CLI before starting an agent." };
113641
114273
  const adapter = this.adapters.get(tool);
113642
114274
  if (!adapter) return { ok: false, error: `No agent named "${tool}" here. Installed tools: ${[...this.adapters.keys()].join(", ") || "none"}.` };
113643
- const full = this.agentCeilingError(project2.id);
114275
+ const full = this.agentCeilingError(callerId, project2.id);
113644
114276
  if (full) return { ok: false, error: full };
113645
114277
  const remembered = this.store.projectToolDefaults(project2.id)[tool];
113646
114278
  const model = opts.model ?? remembered?.model ?? void 0;
@@ -113665,7 +114297,7 @@ ${String(stderr).slice(0, 2e3)}`
113665
114297
  signal
113666
114298
  );
113667
114299
  if (!consent.ok) return consent;
113668
- const fullNow = this.agentCeilingError(project2.id);
114300
+ const fullNow = this.agentCeilingError(callerId, project2.id);
113669
114301
  if (fullNow) return { ok: false, error: fullNow };
113670
114302
  const cwd = worktree ? project2.path : this.store.sessionCwd(callerId) ?? caller.worktree?.path ?? project2.path;
113671
114303
  let child;
@@ -113703,73 +114335,115 @@ ${String(stderr).slice(0, 2e3)}`
113703
114335
  return result.ok && notice ? { ...result, notice: [result.notice, notice].filter(Boolean).join("; ") } : result;
113704
114336
  }
113705
114337
  /**
113706
- * agent_session (#561): the caller works with a session it started — and only those: one whose `askedBy` is the
113707
- * caller, in the caller's project.
114338
+ * agent_session (#561): safe/normal work only with their own children; auto opens ownership within the project.
113708
114339
  *
113709
114340
  * status where it stands, without waiting (the answer, once the turn the caller gave it has ended)
113710
114341
  * wait waits on that turn up to the bound, as start_agent does
113711
- * send gives it more: refused while a turn the caller gave it runs, while it works on the person's own
113712
- * message, or when the person has since raised its preset above the caller's
114342
+ * send gives it more, including during the caller's turn on adapters that accept mid-turn input; refused
114343
+ * while it works on the person's own message or has a higher preset than the caller
113713
114344
  * stop stops the turn the caller gave it; a turn the person started there is theirs
113714
- * delete cleans up a child's merged, clean worktree (`cleanUpWorktreeChild`, #76's hard gate, part 3)
114345
+ * delete sets aside local work in the trash, then removes an idle child's worktree and branch (#608)
113715
114346
  */
113716
114347
  async agentSession(callerId, opts, signal) {
114348
+ if (opts.sessionId === callerId && opts.action !== "status") {
114349
+ return { ok: false, error: "agent_session cannot wait on, stop, delete or send to its own calling session." };
114350
+ }
113717
114351
  const caller = this.meta.get(callerId);
113718
114352
  const child = this.meta.get(opts.sessionId);
113719
- if (!caller?.projectId || !child || child.askedBy !== callerId || child.projectId !== caller.projectId) {
114353
+ if (!caller?.projectId || !child || child.askedBy !== callerId && !this.autoBoundaryOpen(callerId, "session ownership") || child.projectId !== caller.projectId) {
113720
114354
  return { ok: false, error: `${opts.sessionId} is not a session this session started. agent_session works only on sessions start_agent started here.` };
113721
114355
  }
114356
+ if (opts.sessionId === callerId) return this.agentAtRest(child);
113722
114357
  const key = `${callerId}>${child.id}`;
113723
- const d2 = this.delegations.get(key);
113724
- switch (opts.action) {
113725
- case "status":
113726
- if (d2 && !d2.settled) return this.agentWorking(d2, false);
113727
- return d2 ? this.awaitAgent(key, d2, signal) : this.agentAtRest(child);
113728
- case "wait":
113729
- return d2 ? this.awaitAgent(key, d2, signal) : this.agentAtRest(child);
113730
- case "stop": {
113731
- if (!d2 || d2.settled) return { ok: false, error: `Nothing you gave "${child.name}" is running.` };
113732
- if (this.awaitingReport.get(child.id) === callerId) this.awaitingReport.delete(child.id);
113733
- d2.stop("the session that started it stopped it");
113734
- await d2.outcome;
113735
- this.delegations.delete(key);
113736
- return { ok: true, state: "stopped", sessionId: child.id, sessionName: child.name, tool: child.tool };
113737
- }
113738
- case "delete": {
113739
- if (d2 && !d2.settled) return { ok: false, error: `"${child.name}" is still working on what you gave it \u2014 stop it or wait first.` };
113740
- const r = await this.cleanUpWorktreeChild(child);
113741
- if (!r.ok) return r;
113742
- if (d2) this.delegations.delete(key);
113743
- return { ok: true, state: "deleted", sessionId: child.id, sessionName: child.name, tool: child.tool, branch: r.branch, proof: r.proof };
113744
- }
113745
- case "send": {
113746
- if (d2 && !d2.settled) {
113747
- return { ok: false, error: `"${child.name}" is still working on what you gave it. Wait for it (action "wait") or stop it first.` };
113748
- }
113749
- if (d2) this.delegations.delete(key);
113750
- if (child.state === "working" || child.state === "waiting_approval" || this.agentRuns.has(child.id)) {
113751
- return { ok: false, error: `"${child.name}" is busy with something else (the person may be using it). Try again later.` };
113752
- }
113753
- if (!presetWithin(child.permissionPreset, caller.permissionPreset)) {
113754
- return {
113755
- ok: false,
113756
- error: `"${child.name}" now has more permissions (${child.permissionPreset}) than this session (${caller.permissionPreset}), so this session cannot give it work. Tell the person.`
113757
- };
114358
+ let d2 = this.delegations.get(key);
114359
+ let releaseObservation;
114360
+ if (!d2 && this.autoBoundaryOpen(callerId)) {
114361
+ const existing = [...this.delegations.values()].find((run) => run.sessionId === child.id && !run.settled);
114362
+ if (existing) d2 = existing;
114363
+ else if (opts.action !== "delete" && (child.state === "working" || child.state === "waiting_approval")) {
114364
+ const observation = this.observeTurn(child, opts.action !== "status");
114365
+ d2 = observation.run;
114366
+ releaseObservation = observation.release;
114367
+ }
114368
+ }
114369
+ const observing = !!d2 && d2.callerId !== callerId;
114370
+ try {
114371
+ switch (opts.action) {
114372
+ case "status":
114373
+ if (d2 && !d2.settled) return this.agentWorking(d2, false, observing);
114374
+ return d2 ? await this.awaitAgent(key, d2, signal, observing) : this.agentAtRest(child);
114375
+ case "wait":
114376
+ return d2 ? await this.awaitAgent(key, d2, signal, observing) : this.agentAtRest(child);
114377
+ case "stop": {
114378
+ if (!d2 || d2.settled) return { ok: false, error: `Nothing you gave "${child.name}" is running.` };
114379
+ if (!observing && this.awaitingReport.get(child.id) === callerId) this.awaitingReport.delete(child.id);
114380
+ d2.stop("the session that started it stopped it", observing);
114381
+ await d2.outcome;
114382
+ if (!observing) this.delegations.delete(key);
114383
+ return { ok: true, state: "stopped", sessionId: child.id, sessionName: child.name, tool: child.tool };
114384
+ }
114385
+ case "delete": {
114386
+ if (d2 && !d2.settled) return { ok: false, error: `"${child.name}" is still working on what you gave it \u2014 stop it or wait first.` };
114387
+ const r = await this.cleanUpWorktreeChild(child);
114388
+ if (!r.ok) return r;
114389
+ if (d2) this.delegations.delete(key);
114390
+ 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 } : {} };
113758
114391
  }
113759
- const full = this.agentCeilingError(child.projectId ?? "");
113760
- if (full) return { ok: false, error: full };
113761
- const project2 = this.store.listProjects().find((p) => p.id === child.projectId);
113762
- const cwd = this.store.sessionCwd(child.id) ?? child.worktree?.path ?? project2?.path ?? "";
113763
- const next = this.startDelegation(key, callerId, child.id, { id: child.projectId ?? "", name: project2?.name ?? "", path: cwd }, true);
113764
- try {
113765
- await this.deliver(child.id, opts.text ?? "", void 0, { sessionId: callerId, name: labelOf(this.store, caller) }, false);
113766
- } catch (e) {
113767
- this.delegations.delete(key);
113768
- if (this.agentRuns.get(child.id) === next.wait) this.agentRuns.delete(child.id);
113769
- return { ok: false, error: `Could not send it: ${e.message}` };
114392
+ case "send": {
114393
+ if (!presetWithin(child.permissionPreset, caller.permissionPreset)) {
114394
+ return {
114395
+ ok: false,
114396
+ error: `"${child.name}" now has more permissions (${child.permissionPreset}) than this session (${caller.permissionPreset}), so this session cannot give it work. Tell the person.`
114397
+ };
114398
+ }
114399
+ const from = { sessionId: callerId, name: labelOf(this.store, caller) };
114400
+ if (d2 && !d2.settled) {
114401
+ const text2 = agentNoteFrame(from.name, opts.text ?? "");
114402
+ if (!this.adapters.get(child.tool)?.capabilities.midTurnInput || !this.handles.get(child.id)?.sendTracked) {
114403
+ return { ok: false, error: `"${child.name}" cannot take messages while working. Wait for it (action "wait") or stop it first.` };
114404
+ }
114405
+ const releaseHold = d2.wait.holdInput();
114406
+ const release = () => {
114407
+ releaseHold();
114408
+ this.applyDeferredRestart(child.id);
114409
+ };
114410
+ try {
114411
+ await this.deliver(child.id, text2, void 0, from, false, void 0, "inline", (h, input) => {
114412
+ const fail4 = (e) => {
114413
+ d2.wait.fail(e);
114414
+ release();
114415
+ };
114416
+ void h.sendTracked(input, { complete: release, fail: fail4 }).then(release, fail4);
114417
+ });
114418
+ } catch (e) {
114419
+ release();
114420
+ return { ok: false, error: `Could not send it: ${e.message}` };
114421
+ }
114422
+ return this.agentWorking(d2, false, observing);
114423
+ }
114424
+ if (child.state === "working" || child.state === "waiting_approval" || this.agentRuns.has(child.id) && !this.agentRuns.get(child.id).finished) {
114425
+ return { ok: false, error: `"${child.name}" is busy with something else (the person may be using it). Try again later.` };
114426
+ }
114427
+ if (d2) this.delegations.delete(key);
114428
+ const full = this.agentCeilingError(callerId, child.projectId ?? "");
114429
+ if (full) return { ok: false, error: full };
114430
+ const project2 = this.store.listProjects().find((p) => p.id === child.projectId);
114431
+ const cwd = this.store.sessionCwd(child.id) ?? child.worktree?.path ?? project2?.path ?? "";
114432
+ const next = this.startDelegation(key, callerId, child.id, { id: child.projectId ?? "", name: project2?.name ?? "", path: cwd }, true);
114433
+ try {
114434
+ await this.deliver(child.id, agentFrame(from.name, opts.text ?? ""), void 0, from, false);
114435
+ } catch (e) {
114436
+ this.delegations.delete(key);
114437
+ if (this.agentRuns.get(child.id) === next.wait) this.agentRuns.delete(child.id);
114438
+ return { ok: false, error: `Could not send it: ${e.message}` };
114439
+ }
114440
+ return this.awaitAgent(key, next, signal);
113770
114441
  }
113771
- return this.awaitAgent(key, next, signal);
114442
+ default:
114443
+ return { ok: false, error: "Unknown agent_session action." };
113772
114444
  }
114445
+ } finally {
114446
+ releaseObservation?.();
113773
114447
  }
113774
114448
  }
113775
114449
  /**
@@ -113782,65 +114456,67 @@ ${String(stderr).slice(0, 2e3)}`
113782
114456
  if (!summary.isRepo) return { error: "This project is not a git repository, so it has no worktrees. Start the agent without one." };
113783
114457
  const asked = opts.branch?.trim();
113784
114458
  if (asked) return await gitValidBranchName(asked) ? { branch: asked } : { error: `Not a valid branch name: ${asked}` };
113785
- return { branch: agentBranch(opts.task, randomUUID7()) };
114459
+ return { branch: agentBranch(opts.task, randomUUID10()) };
113786
114460
  }
113787
- /**
113788
- * Cleans up a child's merged worktree (#76's hard gate, moved from the retired worktree manager's tool to
113789
- * agent_session delete, #561 part 3). It runs **only when losslessness is proven at this moment**: no uncommitted
113790
- * changes, and the branch's current tip has landed on the trunk (by ancestry, or by a merged pull request whose head
113791
- * is that tip). Not the cached merged badge: a commit could have landed since (TOCTOU). Every deletion outside that
113792
- * proof stays the person's, in the delete dialog. The session goes to the trash like any other (an agent never
113793
- * deletes one for good), the worktree and branch are removed, the tool's own conversation is kept, and the tip is
113794
- * logged for reflog recovery.
113795
- */
114461
+ /** Prevent a send/resume from racing the snapshot and worktree removal. */
114462
+ worktreeCleanup = /* @__PURE__ */ new Set();
114463
+ /** #608: the parent decides; local work follows the session into the trash in every preset. */
113796
114464
  async cleanUpWorktreeChild(target) {
113797
114465
  if (!target.worktree?.base || !target.projectId) {
113798
114466
  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` };
113799
114467
  }
113800
- if (target.state === "working" || target.state === "waiting_approval" || this.agentRuns.has(target.id)) {
113801
- return { ok: false, error: `"${target.name}" is still working \u2014 clean it up once its turn ends` };
113802
- }
113803
- const cwd = this.cwdOf(target.projectId);
113804
- const { branch, path: path2, base: base2 } = target.worktree;
113805
- const wt2 = await gitWorktreeDirty(path2, this.gitTrust(target.projectId)).catch(() => ({ dirty: true, changedFiles: -1 }));
113806
- if (wt2.dirty) {
113807
- const n = wt2.changedFiles >= 0 ? `${wt2.changedFiles} ` : "";
113808
- return { ok: false, error: `There are ${n}uncommitted changes in "${target.name}" \u2014 have it commit (or discard) them, then delete again` };
114468
+ if (target.state === "working" || target.state === "waiting_approval" || this.agentRuns.has(target.id) || sessionBusy({ ...target, live: this.handles.has(target.id) })) {
114469
+ return { ok: false, error: `"${target.name}" is still working \u2014 stop it or wait first` };
113809
114470
  }
113810
- const trunk = this.trunkOf(target.projectId) ?? "HEAD";
113811
- let proof = null;
113812
- if (await gitBranchMerged(cwd, branch, base2, trunk).catch(() => false)) {
113813
- proof = "trunk ancestry";
113814
- } else if (this.ghAvailable) {
113815
- const pr = await this.prLookup(cwd, branch).catch(() => null);
113816
- if (pr === "unavailable") this.ghAvailable = false;
113817
- else if (pr && pr.state === "merged") {
113818
- const tip2 = await gitRevParse(cwd, `refs/heads/${branch}`);
113819
- if (pr.headOid && tip2 && tip2 === pr.headOid) proof = `PR #${pr.number}`;
113820
- else if (pr.headOid && tip2) {
113821
- 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` };
113822
- }
114471
+ 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` };
114472
+ if (this.worktreeCleanup.has(target.id)) return { ok: false, error: "This worktree is already being removed" };
114473
+ this.worktreeCleanup.add(target.id);
114474
+ const { branch, path: path2 } = target.worktree;
114475
+ try {
114476
+ const cwd = this.cwdOf(target.projectId);
114477
+ const handle = this.handles.get(target.id);
114478
+ if (handle) {
114479
+ await handle.dispose();
114480
+ this.handles.delete(target.id);
114481
+ this.running.delete(target.id);
114482
+ }
114483
+ const trunk = await gitRevParse(cwd, this.trunkOf(target.projectId) ?? "HEAD");
114484
+ const setAside = await saveWorktreeRecovery(target.id, path2, trunk ?? "missing-trunk", branch);
114485
+ if (setAside.changedFiles || setAside.commits) {
114486
+ this.onEvent({
114487
+ type: "notice",
114488
+ sessionId: target.id,
114489
+ level: "info",
114490
+ from: "Centralu",
114491
+ label: "worktree recovery",
114492
+ 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.`
114493
+ });
113823
114494
  }
114495
+ await this.trashSession(target.id, false, false, setAside);
114496
+ try {
114497
+ await gitWorktreeRemove(cwd, path2, true, true);
114498
+ } catch (e) {
114499
+ 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)}.` };
114500
+ }
114501
+ try {
114502
+ await gitBranchDelete(cwd, branch, this.gitTrust(target.projectId));
114503
+ } catch (e) {
114504
+ 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)}.` };
114505
+ }
114506
+ return { ok: true, branch, setAside };
114507
+ } catch (e) {
114508
+ return { ok: false, error: `Could not finish removing "${branch}": ${e.message}. Any saved recovery files are at ${worktreeRecoveryDir(target.id)}.` };
114509
+ } finally {
114510
+ this.worktreeCleanup.delete(target.id);
113824
114511
  }
113825
- if (!proof) {
113826
- return {
113827
- ok: false,
113828
- 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`
113829
- };
113830
- }
113831
- const tip = await gitRevParse(cwd, `refs/heads/${branch}`);
113832
- await this.trashSession(target.id);
113833
- await gitWorktreeRemove(cwd, path2, true).catch(() => {
113834
- });
113835
- await gitBranchDelete(cwd, branch, this.gitTrust(target.projectId)).catch(() => {
113836
- });
113837
- console.error(`[worktree] cleaned up ${branch} (tip ${tip?.slice(0, 8) ?? "?"}, proof: ${proof})`);
113838
- return { ok: true, branch, proof };
113839
114512
  }
113840
- /** Refuses a start when the project's children already running reach `AGENT_CEILING` (#561) */
113841
- agentCeilingError(projectId) {
114513
+ /** A saved ceiling overrides the caller's preset default: auto unlimited, otherwise AGENT_CEILING. */
114514
+ agentCeilingError(callerId, projectId) {
114515
+ const configured = this.uiPreferences().agentCeiling;
114516
+ const ceiling = configured ?? (this.autoBoundaryOpen(callerId) ? 0 : AGENT_CEILING);
114517
+ if (ceiling === 0) return null;
113842
114518
  const running = [...this.delegations.values()].filter((d2) => d2.agent && !d2.settled && this.meta.get(d2.sessionId)?.projectId === projectId).length;
113843
- if (running < AGENT_CEILING) return null;
114519
+ if (running < ceiling) return null;
113844
114520
  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.`;
113845
114521
  }
113846
114522
  /**
@@ -113900,8 +114576,8 @@ ${String(stderr).slice(0, 2e3)}`
113900
114576
  * "Working": the caller is told when the turn ends (#561), through the report the orchestrator's reportBack uses, unless
113901
114577
  * a call is waiting on it by then (`awaitAgent` takes the marker down while it waits)
113902
114578
  */
113903
- agentWorking(d2, arm = true) {
113904
- if (arm) this.awaitingReport.set(d2.sessionId, d2.callerId);
114579
+ agentWorking(d2, arm = true, observing = false) {
114580
+ if (arm && !observing) this.awaitingReport.set(d2.sessionId, d2.callerId);
113905
114581
  const notice = d2.notice();
113906
114582
  return {
113907
114583
  ok: true,
@@ -113909,7 +114585,7 @@ ${String(stderr).slice(0, 2e3)}`
113909
114585
  sessionId: d2.sessionId,
113910
114586
  sessionName: this.meta.get(d2.sessionId)?.name ?? d2.sessionId,
113911
114587
  tool: this.meta.get(d2.sessionId)?.tool ?? "",
113912
- reported: this.awaitingReport.get(d2.sessionId) === d2.callerId,
114588
+ reported: !observing && this.awaitingReport.get(d2.sessionId) === d2.callerId,
113913
114589
  ...notice ? { notice } : {}
113914
114590
  };
113915
114591
  }
@@ -113926,32 +114602,43 @@ ${String(stderr).slice(0, 2e3)}`
113926
114602
  };
113927
114603
  }
113928
114604
  /** Waits on a child's turn up to the bound (#561), like `awaitDelegation`, and reads its outcome for the caller */
113929
- async awaitAgent(key, d2, signal) {
113930
- if (this.awaitingReport.get(d2.sessionId) === d2.callerId) this.awaitingReport.delete(d2.sessionId);
114605
+ async awaitAgent(key, d2, signal, observing = false) {
114606
+ if (!observing && this.awaitingReport.get(d2.sessionId) === d2.callerId) this.awaitingReport.delete(d2.sessionId);
113931
114607
  const name = () => this.meta.get(d2.sessionId)?.name ?? d2.sessionId;
113932
114608
  let timer;
113933
114609
  let onAbort;
113934
- const bound = new Promise((resolve11) => timer = setTimeout(() => resolve11("bound"), ASK_WAIT_MS));
113935
- const aborted2 = new Promise((resolve11) => {
114610
+ const bound = new Promise((resolve12) => timer = setTimeout(() => resolve12("bound"), ASK_WAIT_MS));
114611
+ const aborted2 = new Promise((resolve12) => {
113936
114612
  if (!signal) return;
113937
- onAbort = () => resolve11("aborted");
114613
+ onAbort = () => resolve12("aborted");
113938
114614
  if (signal.aborted) onAbort();
113939
114615
  else signal.addEventListener("abort", onAbort, { once: true });
113940
114616
  });
113941
- d2.waiters = (d2.waiters ?? 0) + 1;
114617
+ if (!observing) d2.waiters = (d2.waiters ?? 0) + 1;
113942
114618
  try {
113943
114619
  const r = await Promise.race([d2.outcome, bound, aborted2]);
113944
- d2.waiters--;
114620
+ if (!observing) d2.waiters = (d2.waiters ?? 1) - 1;
113945
114621
  if (r === "aborted") {
114622
+ if (observing) return { ok: false, error: `The observation was cancelled; "${name()}" was left running.` };
113946
114623
  d2.stop("the call that started it was cancelled");
113947
114624
  await d2.outcome;
113948
114625
  this.delegations.delete(key);
113949
114626
  return { ok: false, error: `The call was cancelled, so "${name()}" was stopped.` };
113950
114627
  }
113951
- if (r === "bound") return this.agentWorking(d2);
113952
- if (this.delegations.get(key) === d2) this.delegations.delete(key);
114628
+ if (r === "bound") return this.agentWorking(d2, !observing, observing);
114629
+ if (!observing && this.delegations.get(key) === d2) this.delegations.delete(key);
113953
114630
  if (!r.ok) return { ok: false, error: `"${name()}" [${d2.sessionId}] did not finish: ${r.error}. Tell the person, or give it a clearer task.` };
113954
- const answer = finalAnswer(this.store.loadMessages(d2.sessionId, 50));
114631
+ const answer = r.answer ?? finalAnswer(this.store.loadMessages(d2.sessionId, 50));
114632
+ if (observing) return {
114633
+ ok: true,
114634
+ state: "done",
114635
+ sessionId: d2.sessionId,
114636
+ sessionName: name(),
114637
+ tool: this.meta.get(d2.sessionId)?.tool ?? "",
114638
+ answer: clipAnswer(answer, name()),
114639
+ readable: [],
114640
+ outside: []
114641
+ };
113955
114642
  const { granted, outside } = readableGrants(pathsIn(answer), d2.target.path);
113956
114643
  if (granted.length) {
113957
114644
  const set3 = this.readGrants.get(d2.callerId) ?? /* @__PURE__ */ new Set();
@@ -113974,39 +114661,39 @@ ${String(stderr).slice(0, 2e3)}`
113974
114661
  }
113975
114662
  }
113976
114663
  askCapability(q2, signal) {
113977
- return new Promise((resolve11) => {
113978
- if (signal.aborted) return resolve11(null);
114664
+ return new Promise((resolve12) => {
114665
+ if (signal.aborted) return resolve12(null);
113979
114666
  const app2 = { appId: q2.app.appId, projectId: q2.app.projectId, name: q2.appName };
113980
114667
  if (q2.origin.kind === "session" && this.meta.has(q2.origin.sessionId)) {
113981
- const requestId = `cap-${randomUUID7()}`;
114668
+ const requestId = `cap-${randomUUID10()}`;
113982
114669
  const ask = {
113983
114670
  requestId,
113984
114671
  sessionId: q2.origin.sessionId,
113985
114672
  detail: { kind: "capability", app: app2, capability: q2.capability, text: q2.text },
113986
114673
  shown: false,
113987
- resolve: (d2) => resolve11(d2 === "always" || d2 === "always_project" ? "allow" : d2)
114674
+ resolve: (d2) => resolve12(d2 === "always" || d2 === "always_project" ? "allow" : d2)
113988
114675
  };
113989
114676
  this.hostAsks.set(requestId, ask);
113990
114677
  signal.addEventListener("abort", () => {
113991
114678
  if (!this.hostAsks.delete(requestId)) return;
113992
114679
  const m = this.meta.get(ask.sessionId);
113993
114680
  if (m?.pendingApproval?.requestId === requestId) this.onEvent({ type: "approval_resolved", sessionId: ask.sessionId, requestId, decision: "deny" });
113994
- resolve11(null);
114681
+ resolve12(null);
113995
114682
  }, { once: true });
113996
114683
  this.raiseHostAsks(ask.sessionId);
113997
114684
  return;
113998
114685
  }
113999
114686
  const origin = q2.origin.kind === "view" ? q2.origin.app : q2.app;
114000
- const id2 = `q-${randomUUID7()}`;
114687
+ const id2 = `q-${randomUUID10()}`;
114001
114688
  this.appQuestions.set(id2, {
114002
114689
  question: { id: id2, app: app2, capability: q2.capability, text: q2.text, origin: { appId: origin.appId, projectId: origin.projectId }, askedAt: q2.askedAt, expiresAt: q2.expiresAt },
114003
- resolve: resolve11
114690
+ resolve: resolve12
114004
114691
  });
114005
114692
  this.emit({ type: "external_app_questions_changed" });
114006
114693
  signal.addEventListener("abort", () => {
114007
114694
  if (!this.appQuestions.delete(id2)) return;
114008
114695
  this.emit({ type: "external_app_questions_changed" });
114009
- resolve11(null);
114696
+ resolve12(null);
114010
114697
  }, { once: true });
114011
114698
  });
114012
114699
  }
@@ -114315,6 +115002,7 @@ ${String(stderr).slice(0, 2e3)}`
114315
115002
  this.appsHub?.dispose();
114316
115003
  this.appsHub = new SessionAppsHub(rt2, {
114317
115004
  ...opts,
115005
+ boundaryOpen: (id2) => this.autoBoundaryOpen(id2),
114318
115006
  onDemand: {
114319
115007
  attached: (id2) => this.appAccess?.attached(id2) ?? [],
114320
115008
  /*
@@ -114325,7 +115013,9 @@ ${String(stderr).slice(0, 2e3)}`
114325
115013
  allowed: (session2, app2) => this.toolProfileOf(session2.id) === "reader" && (this.appAccess?.allowed(session2, app2) ?? false)
114326
115014
  }
114327
115015
  });
115016
+ rt2.setSessionBoundaryPolicy((id2) => this.autoBoundaryOpen(id2));
114328
115017
  this.appAccess = new AppAccess({
115018
+ boundaryOpen: (id2, boundary) => this.autoBoundaryOpen(id2, boundary),
114329
115019
  hub: this.appsHub,
114330
115020
  store: this.store,
114331
115021
  session: (id2) => {
@@ -114679,14 +115369,14 @@ ${String(stderr).slice(0, 2e3)}`
114679
115369
  }
114680
115370
  async loadMessages(sessionId, limit, beforeSeq) {
114681
115371
  const rows = this.store.loadMessages(sessionId, limit, beforeSeq);
114682
- const { readFile: readFile5 } = await import("node:fs/promises");
115372
+ const { readFile: readFile6 } = await import("node:fs/promises");
114683
115373
  return Promise.all(
114684
115374
  rows.map(async (r) => {
114685
115375
  if (r.kind === "image") {
114686
115376
  const p = r.payload;
114687
115377
  if (!p.path || p.note) return r;
114688
115378
  try {
114689
- const data = (await readFile5(p.path)).toString("base64");
115379
+ const data = (await readFile6(p.path)).toString("base64");
114690
115380
  return { ...r, payload: { ...p, data } };
114691
115381
  } catch {
114692
115382
  return { ...r, payload: { ...p, note: "The image was cleared and no longer exists (total-size cap)" } };
@@ -114699,7 +115389,7 @@ ${String(stderr).slice(0, 2e3)}`
114699
115389
  p.attachments.map(async (a) => {
114700
115390
  if (a.kind !== "image") return a;
114701
115391
  try {
114702
- return { ...a, data: (await readFile5(a.path)).toString("base64") };
115392
+ return { ...a, data: (await readFile6(a.path)).toString("base64") };
114703
115393
  } catch {
114704
115394
  return a;
114705
115395
  }
@@ -114713,6 +115403,7 @@ ${String(stderr).slice(0, 2e3)}`
114713
115403
  }
114714
115404
  async disposeAll() {
114715
115405
  this.watchers.close();
115406
+ for (const readers of this.turnObservers.values()) for (const observer of readers) observer.fail(new Error("Centralu is stopping"));
114716
115407
  this.appsHub?.rt.attachBrokerHost(null);
114717
115408
  for (const run of [...this.agentRuns.values()]) run.fail(new Error("Centralu is shutting down"));
114718
115409
  for (const ask of [...this.hostAsks.values()]) ask.resolve(null);
@@ -114743,6 +115434,7 @@ ${String(stderr).slice(0, 2e3)}`
114743
115434
  this.watchers.close();
114744
115435
  this.appsHub?.rt.attachBrokerHost(null);
114745
115436
  for (const run of [...this.agentRuns.values()]) run.fail(new Error("Centralu is restarting"));
115437
+ for (const readers of this.turnObservers.values()) for (const observer of readers) observer.fail(new Error("Centralu is restarting"));
114746
115438
  for (const ask of [...this.hostAsks.values()]) ask.resolve(null);
114747
115439
  this.hostAsks.clear();
114748
115440
  for (const q2 of [...this.appQuestions.values()]) q2.resolve(null);
@@ -114774,8 +115466,8 @@ import { relative as relative9 } from "node:path";
114774
115466
  // packages/agent-host/src/themes.ts
114775
115467
  import { randomBytes as randomBytes4 } from "node:crypto";
114776
115468
  import { watch as watch2 } from "node:fs";
114777
- 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";
114778
- import { basename as basename6, join as join29 } from "node:path";
115469
+ 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";
115470
+ import { basename as basename6, join as join30 } from "node:path";
114779
115471
  var MAX_FILE_BYTES = 256 * 1024;
114780
115472
  var SETTLE_MS = 100;
114781
115473
  var ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,59}$/;
@@ -114790,7 +115482,7 @@ var ThemeFiles = class {
114790
115482
  timer = null;
114791
115483
  /** Makes the folder, writes the schema next to the themes, and starts watching */
114792
115484
  async start() {
114793
- await mkdir6(this.dir, { recursive: true });
115485
+ await mkdir7(this.dir, { recursive: true });
114794
115486
  await this.writeSchema();
114795
115487
  try {
114796
115488
  this.watcher = watch2(this.dir, (_event, name) => {
@@ -114820,15 +115512,15 @@ var ThemeFiles = class {
114820
115512
  * a token updates it, and an unchanged one does not touch the folder (or wake the watcher).
114821
115513
  */
114822
115514
  async writeSchema() {
114823
- const path2 = join29(this.dir, THEME_SCHEMA_FILE);
115515
+ const path2 = join30(this.dir, THEME_SCHEMA_FILE);
114824
115516
  const text2 = `${JSON.stringify(themeFileJsonSchema(), null, 2)}
114825
115517
  `;
114826
- const current = await readFile4(path2, "utf8").catch(() => null);
115518
+ const current = await readFile5(path2, "utf8").catch(() => null);
114827
115519
  if (current !== text2) await this.atomicWrite(path2, text2);
114828
115520
  }
114829
115521
  /** Every `<id>.json` in the folder, read and checked. A file that cannot be read is listed with the reason. */
114830
115522
  async list() {
114831
- const names = await readdir8(this.dir).catch(() => []);
115523
+ const names = await readdir9(this.dir).catch(() => []);
114832
115524
  const entries = [];
114833
115525
  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();
114834
115526
  for (const id2 of ids) entries.push(await this.read(id2));
@@ -114837,11 +115529,11 @@ var ThemeFiles = class {
114837
115529
  async read(id2) {
114838
115530
  const path2 = this.pathOf(id2);
114839
115531
  try {
114840
- const info = await stat5(path2);
115532
+ const info = await stat6(path2);
114841
115533
  if (info.size > MAX_FILE_BYTES) {
114842
115534
  return { id: id2, path: path2, name: id2, base: "dark", tokens: {}, problems: ["The file is too large to be a theme."], broken: true };
114843
115535
  }
114844
- return parseThemeFile(id2, path2, await readFile4(path2, "utf8"));
115536
+ return parseThemeFile(id2, path2, await readFile5(path2, "utf8"));
114845
115537
  } catch (e) {
114846
115538
  return { id: id2, path: path2, name: id2, base: "dark", tokens: {}, problems: [`Could not read the file: ${e.message}`], broken: true };
114847
115539
  }
@@ -114849,16 +115541,16 @@ var ThemeFiles = class {
114849
115541
  /** Writes a theme. `id` null makes a new file named after the theme (`My theme` → `my-theme.json`). */
114850
115542
  async save(id2, content) {
114851
115543
  const target = id2 ?? await this.freeId(content.name);
114852
- await mkdir6(this.dir, { recursive: true });
115544
+ await mkdir7(this.dir, { recursive: true });
114853
115545
  await this.atomicWrite(this.pathOf(target), formatThemeFile(content));
114854
115546
  return this.read(target);
114855
115547
  }
114856
115548
  /** Copies a file from elsewhere in, under a fresh id if its name is taken. Refuses what does not read as a theme. */
114857
115549
  async importFrom(source) {
114858
- const info = await stat5(source);
115550
+ const info = await stat6(source);
114859
115551
  if (!info.isFile()) throw Object.assign(new Error("That is not a file."), { code: "invalid_params" });
114860
115552
  if (info.size > MAX_FILE_BYTES) throw Object.assign(new Error("The file is too large to be a theme."), { code: "invalid_params" });
114861
- const text2 = await readFile4(source, "utf8");
115553
+ const text2 = await readFile5(source, "utf8");
114862
115554
  const parsed = parseThemeFile("import", source, text2);
114863
115555
  if (parsed.broken) throw Object.assign(new Error(parsed.problems[0] ?? "Not a theme file."), { code: "invalid_params" });
114864
115556
  const fromName = basename6(source).replace(/\.json$/i, "");
@@ -114869,13 +115561,13 @@ var ThemeFiles = class {
114869
115561
  /** The absolute path for an id, for the shell to trash or reveal. Refuses anything that is not an id. */
114870
115562
  pathOf(id2) {
114871
115563
  if (!ID_PATTERN.test(id2)) throw Object.assign(new Error(`Not a theme id: ${id2}`), { code: "invalid_params" });
114872
- return join29(this.dir, `${id2}.json`);
115564
+ return join30(this.dir, `${id2}.json`);
114873
115565
  }
114874
115566
  async freeId(name) {
114875
115567
  const base2 = name.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "theme";
114876
115568
  for (let n = 1; n < 1e3; n++) {
114877
115569
  const id2 = n === 1 ? base2 : `${base2}-${n}`;
114878
- const taken = await stat5(this.pathOf(id2)).then(
115570
+ const taken = await stat6(this.pathOf(id2)).then(
114879
115571
  () => true,
114880
115572
  () => false
114881
115573
  );
@@ -114887,7 +115579,7 @@ var ThemeFiles = class {
114887
115579
  async atomicWrite(path2, text2) {
114888
115580
  const tmp = `${path2}.${randomBytes4(6).toString("hex")}.tmp`;
114889
115581
  try {
114890
- await writeFile4(tmp, text2, "utf8");
115582
+ await writeFile5(tmp, text2, "utf8");
114891
115583
  await rename4(tmp, path2);
114892
115584
  } catch (e) {
114893
115585
  await unlink2(tmp).catch(() => {
@@ -115032,13 +115724,24 @@ async function stopStrays(pids, roots, selfPid = process.pid, held3 = []) {
115032
115724
  }
115033
115725
 
115034
115726
  // packages/agent-host/src/app-home-view.ts
115035
- async function openHomeView(apps, views2, ref, holder) {
115727
+ async function openHomeView(apps, views2, ref, holder, openKey) {
115728
+ const prior = openKey ? views2.homeOpen(ref, openKey, holder) : null;
115729
+ if (prior) return prior;
115036
115730
  const home = await apps.homeView(ref);
115731
+ const concurrent = openKey ? views2.homeOpen(ref, openKey, holder) : null;
115732
+ if (concurrent) return concurrent;
115037
115733
  const toolInput = {};
115038
- const out = await apps.call(ref, home.tool, toolInput, { kind: "view" });
115039
- if (!out.result) throw Object.assign(new Error(out.error ?? `The home tool "${home.tool}" did not answer`), { code: "internal" });
115040
- const { instanceId } = views2.open(ref, home.resourceUri, holder);
115041
- return { instanceId, tool: home.tool, resourceUri: home.resourceUri, toolInput, toolResult: out.result, runId: out.runId };
115734
+ const invoke = async () => {
115735
+ const out = await apps.call(ref, home.tool, toolInput, { kind: "view" });
115736
+ if (!out.result) throw Object.assign(new Error(out.error ?? `The home tool "${home.tool}" did not answer`), { code: "internal" });
115737
+ return { tool: home.tool, resourceUri: home.resourceUri, toolInput, toolResult: out.result, runId: out.runId };
115738
+ };
115739
+ if (openKey) {
115740
+ const { instanceId } = views2.open(ref, home.resourceUri, holder);
115741
+ return views2.rememberHome(instanceId, openKey, async () => ({ instanceId, ...await invoke() }));
115742
+ }
115743
+ const outcome = await invoke();
115744
+ return { ...views2.open(ref, home.resourceUri, holder), ...outcome };
115042
115745
  }
115043
115746
 
115044
115747
  // packages/agent-host/src/builder-requests.ts
@@ -115351,8 +116054,14 @@ function createRpcHandler(mgr2, adapters2, { terminals: terminals2, updates: upd
115351
116054
  return { appId: d2.app.appId, projectId: d2.app.projectId, uri: d2.uri, origin: d2.origin, resource: d2.resource };
115352
116055
  },
115353
116056
  "apps.openView": async (p, ctx) => {
115354
- const { appId, projectId, lease } = RpcMethods["apps.openView"].params.parse(p);
115355
- return openHomeView(requireExternalApps(), requireViews(), { appId, projectId }, lease && ctx ? ctx.client : void 0);
116057
+ const { appId, projectId, lease, openKey } = RpcMethods["apps.openView"].params.parse(p);
116058
+ return openHomeView(requireExternalApps(), requireViews(), { appId, projectId }, lease && ctx ? ctx.client : void 0, openKey);
116059
+ },
116060
+ "apps.recoverView": async (p, ctx) => {
116061
+ const { appId, projectId, openKey, lease } = RpcMethods["apps.recoverView"].params.parse(p);
116062
+ const prior = requireViews().homeOpen({ appId, projectId }, openKey, lease && ctx ? ctx.client : void 0);
116063
+ if (prior) return prior;
116064
+ throw new Error("The original app open could not be recovered \u2014 the home tool may or may not have run; check before opening again");
115356
116065
  },
115357
116066
  "apps.holdViews": async (p, ctx) => {
115358
116067
  const { instanceIds } = RpcMethods["apps.holdViews"].params.parse(p);
@@ -115891,7 +116600,7 @@ var sameCause = (a, b2) => a !== null && b2 !== null && JSON.stringify(a) === JS
115891
116600
 
115892
116601
  // packages/agent-host/src/dev-services/terminal.ts
115893
116602
  import { createRequire } from "node:module";
115894
- import { existsSync as existsSync19 } from "node:fs";
116603
+ import { existsSync as existsSync20 } from "node:fs";
115895
116604
  import { win32 as win327 } from "node:path";
115896
116605
  var require2 = createRequire(import.meta.url);
115897
116606
  var SCROLLBACK_BYTES = 256 * 1024;
@@ -116123,9 +116832,9 @@ function commandShell(command, platform = process.platform, env = process.env) {
116123
116832
  }
116124
116833
  function shellPath() {
116125
116834
  const fromEnv = process.env.SHELL;
116126
- if (fromEnv && existsSync19(fromEnv)) return fromEnv;
116835
+ if (fromEnv && existsSync20(fromEnv)) return fromEnv;
116127
116836
  for (const candidate of ["/bin/zsh", "/bin/bash", "/bin/sh"]) {
116128
- if (existsSync19(candidate)) return candidate;
116837
+ if (existsSync20(candidate)) return candidate;
116129
116838
  }
116130
116839
  return process.platform === "win32" ? "powershell.exe" : "/bin/sh";
116131
116840
  }
@@ -116312,7 +117021,7 @@ var CommandRunner = class {
116312
117021
 
116313
117022
  // packages/agent-host/src/updates.ts
116314
117023
  import { execFile as execFile8 } from "node:child_process";
116315
- import { existsSync as existsSync20 } from "node:fs";
117024
+ import { existsSync as existsSync21 } from "node:fs";
116316
117025
  var REGISTRY_URL = `https://registry.npmjs.org/${APP_SLUG}/latest`;
116317
117026
  var CHECK_INTERVAL_MS = 6 * 60 * 60 * 1e3;
116318
117027
  var SCHEDULED_TIMEOUT_MS = 3e3;
@@ -116322,7 +117031,7 @@ var NPM_RETRY_DELAY_MS = 3e4;
116322
117031
  var NPM_RETRY_WINDOW_MS = 10 * 60 * 1e3;
116323
117032
  function existingInstalledCopy() {
116324
117033
  const path2 = installedCopyPath();
116325
- return existsSync20(path2) ? path2 : null;
117034
+ return existsSync21(path2) ? path2 : null;
116326
117035
  }
116327
117036
  async function fetchLatestFromRegistry(timeoutMs) {
116328
117037
  try {
@@ -116336,16 +117045,16 @@ async function fetchLatestFromRegistry(timeoutMs) {
116336
117045
  return { ok: false, reason: "Could not reach the registry \u2014 check the network" };
116337
117046
  }
116338
117047
  }
116339
- function commandFor(file2, args, resolve11 = (f) => resolveCommand(f)) {
116340
- const launch2 = resolve11(file2);
117048
+ function commandFor(file2, args, resolve12 = (f) => resolveCommand(f)) {
117049
+ const launch2 = resolve12(file2);
116341
117050
  return { file: launch2.command, args: [...launch2.args, ...args] };
116342
117051
  }
116343
117052
  function failureDetail(file2, stderr) {
116344
117053
  const lines = stderr.split(/\r?\n/).map((l) => l.trim()).filter((l) => l && !/complete log of this run/i.test(l));
116345
117054
  return (file2 === "npm" ? lines.at(-1) : lines[0]) ?? null;
116346
117055
  }
116347
- function runCommand(file2, args, resolve11) {
116348
- const cmd = commandFor(file2, args, resolve11);
117056
+ function runCommand(file2, args, resolve12) {
117057
+ const cmd = commandFor(file2, args, resolve12);
116349
117058
  return new Promise((done, reject) => {
116350
117059
  execFile8(cmd.file, cmd.args, { timeout: INSTALL_TIMEOUT_MS, windowsHide: true }, (err, _stdout, stderr) => {
116351
117060
  if (!err) return done();
@@ -116373,7 +117082,7 @@ var UpdateService = class {
116373
117082
  readAutoApply: deps.readAutoApply ?? (() => false),
116374
117083
  writeAutoApply: deps.writeAutoApply ?? (() => {
116375
117084
  }),
116376
- wait: deps.wait ?? ((ms3) => new Promise((resolve11) => setTimeout(resolve11, ms3)))
117085
+ wait: deps.wait ?? ((ms3) => new Promise((resolve12) => setTimeout(resolve12, ms3)))
116377
117086
  };
116378
117087
  this.status = {
116379
117088
  /*
@@ -116919,7 +117628,7 @@ var SessionSleeper = class {
116919
117628
  // packages/agent-host/src/dev-services/instance-lock.ts
116920
117629
  import { execFileSync as execFileSync4 } from "node:child_process";
116921
117630
  import { readFileSync as readFileSync13, unlinkSync as unlinkSync3, writeFileSync as writeFileSync7 } from "node:fs";
116922
- import { dirname as dirname10, join as join30 } from "node:path";
117631
+ import { dirname as dirname10, join as join31 } from "node:path";
116923
117632
  import Database2 from "better-sqlite3";
116924
117633
  var OWNERSHIP_FILE = "host-ownership.sqlite";
116925
117634
  var held = /* @__PURE__ */ new Set();
@@ -116953,7 +117662,7 @@ function parseHolder(raw) {
116953
117662
  function acquireInstanceLock(dbPath2, startOf = processStartTime) {
116954
117663
  if (dbPath2 === ":memory:") return { ok: true, release: () => {
116955
117664
  } };
116956
- const lockPath = join30(dirname10(dbPath2), "host.lock");
117665
+ const lockPath = join31(dirname10(dbPath2), "host.lock");
116957
117666
  const readHolder = () => {
116958
117667
  try {
116959
117668
  return parseHolder(readFileSync13(lockPath, "utf8"));
@@ -116961,7 +117670,7 @@ function acquireInstanceLock(dbPath2, startOf = processStartTime) {
116961
117670
  return null;
116962
117671
  }
116963
117672
  };
116964
- const owner = takeOwnership(join30(dirname10(dbPath2), OWNERSHIP_FILE));
117673
+ const owner = takeOwnership(join31(dirname10(dbPath2), OWNERSHIP_FILE));
116965
117674
  if (!owner) {
116966
117675
  const holder = readHolder();
116967
117676
  return { ok: false, heldByPid: holder && Number.isInteger(holder.pid) && holder.pid > 0 ? holder.pid : null, lockPath };
@@ -117282,9 +117991,9 @@ var ControlChannel = class {
117282
117991
  }
117283
117992
  next(op) {
117284
117993
  if (this.ended) return Promise.reject(new Error("the keeper closed the control channel"));
117285
- return new Promise((resolve11, reject) => {
117994
+ return new Promise((resolve12, reject) => {
117286
117995
  const list = this.waiters.get(op) ?? [];
117287
- list.push({ resolve: resolve11, reject });
117996
+ list.push({ resolve: resolve12, reject });
117288
117997
  this.waiters.set(op, list);
117289
117998
  });
117290
117999
  }
@@ -117389,7 +118098,7 @@ async function runDrain(bound, deps) {
117389
118098
  }
117390
118099
 
117391
118100
  // packages/agent-host/src/links/install.ts
117392
- import { existsSync as existsSync21, readFileSync as readFileSync14 } from "node:fs";
118101
+ import { existsSync as existsSync22, readFileSync as readFileSync14 } from "node:fs";
117393
118102
  import { hostname as hostname3 } from "node:os";
117394
118103
  import { fileURLToPath as fileURLToPath6 } from "node:url";
117395
118104
  import { gzipSync } from "node:zlib";
@@ -117713,7 +118422,7 @@ var SshTunnel = class {
117713
118422
  if (this.closing) throw new Error(`The link to ${this.opts.target} was closed`);
117714
118423
  }
117715
118424
  run(args, stdin) {
117716
- return new Promise((resolve11, reject) => {
118425
+ return new Promise((resolve12, reject) => {
117717
118426
  const child = spawn5(this.opts.ssh ?? "ssh", args, { env: this.opts.env ?? process.env, stdio: [stdin === void 0 ? "ignore" : "pipe", "pipe", "pipe"] });
117718
118427
  if (stdin !== void 0) {
117719
118428
  child.stdin.on("error", () => {
@@ -117730,7 +118439,7 @@ var SshTunnel = class {
117730
118439
  this.asking.delete(child);
117731
118440
  reject(e);
117732
118441
  });
117733
- child.once("close", (code2) => resolve11({ code: code2, stdout, stderr }));
118442
+ child.once("close", (code2) => resolve12({ code: code2, stdout, stderr }));
117734
118443
  });
117735
118444
  }
117736
118445
  config() {
@@ -117777,7 +118486,7 @@ var SshTunnel = class {
117777
118486
  this.child = child;
117778
118487
  let stderr = "";
117779
118488
  child.stderr.on("data", (d2) => stderr += String(d2));
117780
- return new Promise((resolve11, reject) => {
118489
+ return new Promise((resolve12, reject) => {
117781
118490
  let settled = false;
117782
118491
  const deadline = Date.now() + (this.opts.forwardTimeoutMs ?? 2e4);
117783
118492
  const fail4 = (message) => {
@@ -117796,7 +118505,7 @@ var SshTunnel = class {
117796
118505
  if (await accepts(localPort)) {
117797
118506
  settled = true;
117798
118507
  this.opts.log?.(`[links] forward up: 127.0.0.1:${localPort} -> ${this.opts.target}:127.0.0.1:${remotePort}`);
117799
- return resolve11();
118508
+ return resolve12();
117800
118509
  }
117801
118510
  if (Date.now() > deadline) {
117802
118511
  this.ended.add(child);
@@ -117814,12 +118523,12 @@ var SshTunnel = class {
117814
118523
  this.child = null;
117815
118524
  if (!child || child.exitCode !== null || child.signalCode !== null) return;
117816
118525
  this.ended.add(child);
117817
- await new Promise((resolve11) => {
117818
- child.once("exit", () => resolve11());
118526
+ await new Promise((resolve12) => {
118527
+ child.once("exit", () => resolve12());
117819
118528
  child.kill("SIGTERM");
117820
118529
  setTimeout(() => {
117821
118530
  child.kill("SIGKILL");
117822
- resolve11();
118531
+ resolve12();
117823
118532
  }, 2e3).unref();
117824
118533
  });
117825
118534
  }
@@ -117829,29 +118538,29 @@ function lastLine(s) {
117829
118538
  return [...lines].reverse().find((l) => /error|denied|refused|timed out|not found|cannot|could not/i.test(l)) ?? lines.at(-1) ?? "";
117830
118539
  }
117831
118540
  function accepts(port2) {
117832
- return new Promise((resolve11) => {
118541
+ return new Promise((resolve12) => {
117833
118542
  const s = createConnection2({ host: "127.0.0.1", port: port2 });
117834
118543
  s.once("connect", () => {
117835
118544
  s.destroy();
117836
- resolve11(true);
118545
+ resolve12(true);
117837
118546
  });
117838
- s.once("error", () => resolve11(false));
118547
+ s.once("error", () => resolve12(false));
117839
118548
  });
117840
118549
  }
117841
118550
  function portFree(port2) {
117842
- return new Promise((resolve11) => {
118551
+ return new Promise((resolve12) => {
117843
118552
  const probe = createServer3();
117844
- probe.once("error", () => resolve11(false));
117845
- probe.listen(port2, "127.0.0.1", () => probe.close(() => resolve11(true)));
118553
+ probe.once("error", () => resolve12(false));
118554
+ probe.listen(port2, "127.0.0.1", () => probe.close(() => resolve12(true)));
117846
118555
  });
117847
118556
  }
117848
118557
  function freePort() {
117849
- return new Promise((resolve11, reject) => {
118558
+ return new Promise((resolve12, reject) => {
117850
118559
  const probe = createServer3();
117851
118560
  probe.once("error", reject);
117852
118561
  probe.listen(0, "127.0.0.1", () => {
117853
118562
  const { port: port2 } = probe.address();
117854
- probe.close(() => resolve11(port2));
118563
+ probe.close(() => resolve12(port2));
117855
118564
  });
117856
118565
  });
117857
118566
  }
@@ -118067,7 +118776,7 @@ async function installRemote(o) {
118067
118776
  function bundled(name, source) {
118068
118777
  for (const url2 of [new URL(`./${name}`, import.meta.url), new URL(source, import.meta.url)]) {
118069
118778
  const file2 = fileURLToPath6(url2);
118070
- if (existsSync21(file2)) return readFileSync14(file2, "utf8");
118779
+ if (existsSync22(file2)) return readFileSync14(file2, "utf8");
118071
118780
  }
118072
118781
  throw new Error(`${name} is missing from this host`);
118073
118782
  }
@@ -118170,7 +118879,7 @@ function probeTarget(target, opts = {}) {
118170
118879
  return Promise.reject(Object.assign(new Error("Not an ssh target: use a host alias from ~/.ssh/config, or user@host"), { code: "internal" }));
118171
118880
  }
118172
118881
  const args = [...opts.configFile ? ["-F", opts.configFile] : [], "-T", "-o", "BatchMode=yes", "-o", "ConnectTimeout=10", "--", t, probeCommand()];
118173
- return new Promise((resolve11) => {
118882
+ return new Promise((resolve12) => {
118174
118883
  let stdout = "";
118175
118884
  let stderr = "";
118176
118885
  let done = false;
@@ -118178,7 +118887,7 @@ function probeTarget(target, opts = {}) {
118178
118887
  if (done) return;
118179
118888
  done = true;
118180
118889
  clearTimeout(timer);
118181
- resolve11(parseProbe(t, run));
118890
+ resolve12(parseProbe(t, run));
118182
118891
  };
118183
118892
  const child = spawn6(opts.ssh ?? "ssh", args, { env: opts.env ?? process.env, stdio: ["ignore", "pipe", "pipe"] });
118184
118893
  const timer = setTimeout(() => {
@@ -118196,7 +118905,7 @@ function probeTarget(target, opts = {}) {
118196
118905
  import { spawn as spawn7 } from "node:child_process";
118197
118906
  import { readdirSync as readdirSync8, readFileSync as readFileSync15, statSync as statSync13 } from "node:fs";
118198
118907
  import { homedir as homedir13 } from "node:os";
118199
- import { isAbsolute as isAbsolute10, join as join31, normalize, parse as parse6, sep as sep12 } from "node:path";
118908
+ import { isAbsolute as isAbsolute10, join as join32, normalize, parse as parse6, sep as sep12 } from "node:path";
118200
118909
  var MAX_DEPTH2 = 16;
118201
118910
  function splitConfigLine(raw) {
118202
118911
  const m = /^\s*([^\s=#]+)\s*(?:=\s*)?([\s\S]*)$/.exec(raw);
@@ -118262,7 +118971,7 @@ function expandGlob(path2) {
118262
118971
  const next = [];
118263
118972
  for (const base2 of found) {
118264
118973
  if (!MAGIC.test(seg)) {
118265
- next.push(join31(base2, seg));
118974
+ next.push(join32(base2, seg));
118266
118975
  continue;
118267
118976
  }
118268
118977
  let names;
@@ -118274,7 +118983,7 @@ function expandGlob(path2) {
118274
118983
  const re2 = segmentRe(seg);
118275
118984
  for (const name of names.sort()) {
118276
118985
  if (name.startsWith(".") && !seg.startsWith(".")) continue;
118277
- if (re2.test(name)) next.push(join31(base2, name));
118986
+ if (re2.test(name)) next.push(join32(base2, name));
118278
118987
  }
118279
118988
  }
118280
118989
  found = next;
@@ -118329,11 +119038,11 @@ function readSshConfig(opts) {
118329
119038
  }
118330
119039
  for (const arg of split2.args) {
118331
119040
  let path2 = arg;
118332
- if (path2.startsWith("~/")) path2 = join31(opts.home, path2.slice(2));
119041
+ if (path2.startsWith("~/")) path2 = join32(opts.home, path2.slice(2));
118333
119042
  else if (path2.startsWith("~")) {
118334
119043
  notes.push(`An Include of another user's folder (${where(file2, n + 1)}) was not read`);
118335
119044
  continue;
118336
- } else if (!isAbsolute10(path2)) path2 = join31(opts.home, ".ssh", path2);
119045
+ } else if (!isAbsolute10(path2)) path2 = join32(opts.home, ".ssh", path2);
118337
119046
  for (const f of expandGlob(path2)) read(f, depth + 1);
118338
119047
  }
118339
119048
  }
@@ -118356,7 +119065,7 @@ function parseSshG(stdout) {
118356
119065
  return { hostName: first.get("hostname") || null, user: first.get("user") || null, port: Number.isInteger(port2) && port2 > 0 ? port2 : null };
118357
119066
  }
118358
119067
  function resolveAlias(alias, opts = {}) {
118359
- return new Promise((resolve11) => {
119068
+ return new Promise((resolve12) => {
118360
119069
  const args = [...opts.configFile ? ["-F", opts.configFile] : [], "-G", "--", alias];
118361
119070
  let stdout = "";
118362
119071
  let stderr = "";
@@ -118365,7 +119074,7 @@ function resolveAlias(alias, opts = {}) {
118365
119074
  if (done) return;
118366
119075
  done = true;
118367
119076
  clearTimeout(timer);
118368
- resolve11(r);
119077
+ resolve12(r);
118369
119078
  };
118370
119079
  const none = (error51) => ({ hostName: null, user: null, port: null, error: error51 });
118371
119080
  const child = spawn7(opts.ssh ?? "ssh", args, { env: opts.env ?? process.env, stdio: ["ignore", "pipe", "pipe"] });
@@ -118383,18 +119092,18 @@ var MAX_HOSTS = 100;
118383
119092
  var RESOLVE_AT_ONCE = 8;
118384
119093
  async function listSshHosts(opts = {}) {
118385
119094
  const home = opts.home ?? homedir13();
118386
- const file2 = opts.configFile ?? join31(home, ".ssh", "config");
119095
+ const file2 = opts.configFile ?? join32(home, ".ssh", "config");
118387
119096
  const read = readSshConfig({ file: file2, home });
118388
119097
  const notes = [...read.notes];
118389
119098
  const listed = read.hosts.slice(0, MAX_HOSTS);
118390
119099
  if (read.hosts.length > MAX_HOSTS) notes.push(`Only the first ${MAX_HOSTS} of ${read.hosts.length} hosts are listed; type any other below`);
118391
- const resolve11 = opts.resolve ?? ((alias) => resolveAlias(alias, opts));
119100
+ const resolve12 = opts.resolve ?? ((alias) => resolveAlias(alias, opts));
118392
119101
  const resolved = new Array(listed.length);
118393
119102
  let next = 0;
118394
119103
  const worker = async () => {
118395
119104
  while (next < listed.length) {
118396
119105
  const i = next++;
118397
- resolved[i] = await resolve11(listed[i].alias);
119106
+ resolved[i] = await resolve12(listed[i].alias);
118398
119107
  }
118399
119108
  };
118400
119109
  await Promise.all(Array.from({ length: Math.min(RESOLVE_AT_ONCE, listed.length) }, worker));
@@ -118883,11 +119592,11 @@ var RemoteClient = class {
118883
119592
  if (!this.ready || !this.ws) return Promise.reject(linkError("The machine is not reachable right now", true));
118884
119593
  const ws2 = this.ws;
118885
119594
  const id2 = String(this.nextId++);
118886
- return new Promise((resolve11, reject) => {
119595
+ return new Promise((resolve12, reject) => {
118887
119596
  const timer = setTimeout(() => {
118888
119597
  if (this.pending.delete(id2)) reject(Object.assign(new Error(`The machine did not answer ${method} in time`), { code: "internal", retryable: true }));
118889
119598
  }, this.opts.callTimeoutMs ?? DEFAULT_CALL_TIMEOUT_MS);
118890
- this.pending.set(id2, { resolve: resolve11, reject, timer });
119599
+ this.pending.set(id2, { resolve: resolve12, reject, timer });
118891
119600
  ws2.send(JSON.stringify({ kind: "rpc", id: id2, method, params }));
118892
119601
  });
118893
119602
  }
@@ -119250,14 +119959,14 @@ var LinkedMachine = class {
119250
119959
  if (this.client?.connected) return this.client.call("host.activity", {}).then(read, () => null);
119251
119960
  const e = this.endpoint;
119252
119961
  if (!e?.url || this.versions?.compatible === false || this.held) return Promise.resolve(null);
119253
- return new Promise((resolve11) => {
119962
+ return new Promise((resolve12) => {
119254
119963
  let settled = false;
119255
119964
  const done = (v) => {
119256
119965
  if (settled) return;
119257
119966
  settled = true;
119258
119967
  clearTimeout(timer);
119259
119968
  client.close();
119260
- resolve11(v);
119969
+ resolve12(v);
119261
119970
  };
119262
119971
  const client = new RemoteClient(
119263
119972
  { ...this.deps.client, url: e.url, token: e.token, handshakeTimeoutMs: 5e3, callTimeoutMs: 5e3 },
@@ -119331,16 +120040,16 @@ var LinkedMachine = class {
119331
120040
  const started = await this.tunnel.startHost();
119332
120041
  this.hostStarted = { ...started, at: Date.now() };
119333
120042
  },
119334
- answers: (version2, ms3) => new Promise((resolve11) => {
120043
+ answers: (version2, ms3) => new Promise((resolve12) => {
119335
120044
  const waiter = (v) => {
119336
120045
  if (v !== version2) return;
119337
120046
  clearTimeout(timer);
119338
120047
  this.helloWaiters.delete(waiter);
119339
- resolve11(true);
120048
+ resolve12(true);
119340
120049
  };
119341
120050
  const timer = setTimeout(() => {
119342
120051
  this.helloWaiters.delete(waiter);
119343
- void hold().then(() => resolve11(false));
120052
+ void hold().then(() => resolve12(false));
119344
120053
  }, this.deps.checkMs ?? ms3);
119345
120054
  this.helloWaiters.add(waiter);
119346
120055
  this.held = false;
@@ -120129,6 +120838,7 @@ var ROUTES = {
120129
120838
  * link, not the window, so its views are opened without one and live until closed, as before
120130
120839
  */
120131
120840
  "apps.openView": { kind: "project", orMachine: true, result: (r, q2) => q2.openedView(r), params: ({ lease: _lease, ...p }) => p },
120841
+ "apps.recoverView": { kind: "project", orMachine: true, result: (r, q2) => q2.openedView(r), params: ({ lease: _lease, ...p }) => p },
120132
120842
  "apps.readResource": view(["projectId", "instanceId"]),
120133
120843
  "apps.invoke": view(["projectId", "instanceId"]),
120134
120844
  "apps.inlineReopen": session((r, q2) => q2.inlineView(r)),
@@ -120488,7 +121198,7 @@ function storeMirror(store2) {
120488
121198
  var start = new StartSequence();
120489
121199
  start.at("launch variables");
120490
121200
  var launch = takeLaunchEnv();
120491
- var BUILD = true ? "60665cc" : "dev";
121201
+ var BUILD = true ? "5dca7b8" : "dev";
120492
121202
  var { values } = parseArgs({
120493
121203
  options: {
120494
121204
  port: { type: "string", default: "5175" },
@@ -120509,12 +121219,12 @@ start.at("data folder");
120509
121219
  var movedNote = null;
120510
121220
  function defaultDbPath() {
120511
121221
  const isDev = process.env.CC_DEV === "1";
120512
- const dir = join32(homedir14(), isDev ? DATA_DIR_DEV : DATA_DIR);
120513
- const legacy = join32(homedir14(), isDev ? DATA_DIR_LEGACY.dev : DATA_DIR_LEGACY.prod);
121222
+ const dir = join33(homedir14(), isDev ? DATA_DIR_DEV : DATA_DIR);
121223
+ const legacy = join33(homedir14(), isDev ? DATA_DIR_LEGACY.dev : DATA_DIR_LEGACY.prod);
120514
121224
  movedNote = migrateLegacyDataDir(legacy, dir) ? `[agent-host] data folder moved: ${legacy} -> ${dir}` : null;
120515
121225
  mkdirSync11(dir, { recursive: true });
120516
121226
  process.env.CC_DATA_DIR = dir;
120517
- return join32(dir, "store.db");
121227
+ return join33(dir, "store.db");
120518
121228
  }
120519
121229
  var dbPath = values.memory ? ":memory:" : values.db ?? defaultDbPath();
120520
121230
  start.at("log");
@@ -120592,7 +121302,7 @@ var mgr = new SessionManager(
120592
121302
  },
120593
121303
  (sessionId) => bridgeAddress(launch.frontDoor, port, token, sessionId),
120594
121304
  // Worktrees are created next to the data folder — dev and the packaged app never touch each other's worktrees
120595
- join32(dirname11(dbPath), "worktrees"),
121305
+ join33(dirname11(dbPath), "worktrees"),
120596
121306
  held2 ? { processes: held2.processes, keptSessions: held2.kept.sessionIds } : {}
120597
121307
  );
120598
121308
  mgr.useProjectsHome(() => projectsHome());
@@ -120626,7 +121336,7 @@ if (held2) {
120626
121336
  }
120627
121337
  var AUTO_UPDATE_CHECK_KEY = "updates.auto";
120628
121338
  var AUTO_APPLY_UPDATES_KEY = "updates.autoApply";
120629
- var themes = new ThemeFiles(join32(dataRoot(), "themes"), () => events.push({ type: "themes_changed" }));
121339
+ var themes = new ThemeFiles(join33(dataRoot(), "themes"), () => events.push({ type: "themes_changed" }));
120630
121340
  await themes.start().catch((e) => console.error(`[themes] the themes folder is unavailable: ${e.message}`));
120631
121341
  var updates = new UpdateService((status) => events.push({ type: "update_status", status }), {
120632
121342
  // On by default when nothing has been saved yet. Since the check is read-only and every failure
@@ -120845,7 +121555,7 @@ var endings = wireEndings({
120845
121555
  heldChildren: held2 !== null,
120846
121556
  leave: (mode, handOver) => void shutdown.leave(mode, handOver),
120847
121557
  force: (why) => shutdown.force(why),
120848
- record: crashRecorder(join32(dirname11(dbPath), "host-errors.log"), (line) => console.error(line)),
121558
+ record: crashRecorder(join33(dirname11(dbPath), "host-errors.log"), (line) => console.error(line)),
120849
121559
  stdin: process.stdin,
120850
121560
  watchParent: values["watch-parent"] === true,
120851
121561
  log: (line) => console.error(line)