@centralu/host-linux-arm64 0.1.0-beta.21 → 0.1.0-beta.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
- "commit": "5dca7b8",
2
+ "commit": "8dd78b6",
3
3
  "protocolVersion": 1,
4
- "builtAt": "2026-10-13T12:07:09.843Z",
4
+ "builtAt": "2026-10-13T19:11:12.573Z",
5
5
  "runtime": "system-node",
6
6
  "platform": "linux",
7
7
  "arch": "arm64"
package/host/main.mjs CHANGED
@@ -3235,7 +3235,7 @@ var require_stream = __commonJS({
3235
3235
  };
3236
3236
  duplex._final = function(callback) {
3237
3237
  if (ws2.readyState === ws2.CONNECTING) {
3238
- ws2.once("open", function open5() {
3238
+ ws2.once("open", function open6() {
3239
3239
  duplex._final(callback);
3240
3240
  });
3241
3241
  return;
@@ -3256,7 +3256,7 @@ var require_stream = __commonJS({
3256
3256
  };
3257
3257
  duplex._write = function(chunk, encoding, callback) {
3258
3258
  if (ws2.readyState === ws2.CONNECTING) {
3259
- ws2.once("open", function open5() {
3259
+ ws2.once("open", function open6() {
3260
3260
  duplex._write(chunk, encoding, callback);
3261
3261
  });
3262
3262
  return;
@@ -42965,7 +42965,7 @@ 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.21";
42968
+ var APP_VERSION = "0.1.0-beta.22";
42969
42969
  var APP_SLUG = "centralu";
42970
42970
  var CLIENT_INFO = { name: APP_SLUG, title: APP_NAME, version: APP_VERSION };
42971
42971
  var DATA_DIR = ".centralu";
@@ -62908,17 +62908,17 @@ function portInUseMessage(port2, servedBy) {
62908
62908
  var VIEW_HANDOVER_KEY = "views.handover";
62909
62909
  var VIEW_HANDOVER_MAX_AGE_MS = 10 * 6e4;
62910
62910
  function recordViewHandover(settings, views2, inline, now = Date.now()) {
62911
- const open5 = views2.list().map((v) => {
62911
+ const open6 = views2.list().map((v) => {
62912
62912
  const owner = inline?.owner(v.id);
62913
62913
  return owner ? { ...v, inline: { sessionId: owner.sessionId, callId: owner.callId, tool: owner.tool } } : v;
62914
62914
  });
62915
- if (open5.length === 0) {
62915
+ if (open6.length === 0) {
62916
62916
  settings.deleteAppSetting(VIEW_HANDOVER_KEY);
62917
62917
  return 0;
62918
62918
  }
62919
- const record2 = { at: now, views: open5 };
62919
+ const record2 = { at: now, views: open6 };
62920
62920
  settings.setAppSetting(VIEW_HANDOVER_KEY, JSON.stringify(record2));
62921
- return open5.length;
62921
+ return open6.length;
62922
62922
  }
62923
62923
  function restoreViewHandover(settings, views2, inline, now = Date.now()) {
62924
62924
  const raw = settings.appSetting(VIEW_HANDOVER_KEY);
@@ -63639,9 +63639,9 @@ import { join as join15 } from "node:path";
63639
63639
  import { lstatSync as lstatSync2, watch } from "node:fs";
63640
63640
 
63641
63641
  // packages/agent-host/src/dev-services/fs.ts
63642
- import { spawn } from "node:child_process";
63642
+ import { spawn as spawn2 } from "node:child_process";
63643
63643
  import { constants as constants3 } from "node:fs";
63644
- import { lstat as lstat2, mkdir, open, readdir, readlink, realpath as realpath2, rename, rm, stat as stat2, writeFile } from "node:fs/promises";
63644
+ import { lstat as lstat2, mkdir, open as open2, readdir, readlink, realpath as realpath2, rename, rm, stat as stat2, writeFile } from "node:fs/promises";
63645
63645
  import { basename, dirname as dirname2, extname, isAbsolute as isAbsolute2, join as join5, relative as relative2, resolve, sep as sep2 } from "node:path";
63646
63646
 
63647
63647
  // packages/agent-host/src/dev-services/path-guard.ts
@@ -63776,7 +63776,8 @@ function isMissingFsPath(error51) {
63776
63776
  }
63777
63777
 
63778
63778
  // packages/agent-host/src/dev-services/git-exec.ts
63779
- import { execFile } from "node:child_process";
63779
+ import { execFile, spawn } from "node:child_process";
63780
+ import { open } from "node:fs/promises";
63780
63781
  import { devNull } from "node:os";
63781
63782
  import { promisify } from "node:util";
63782
63783
 
@@ -64002,6 +64003,40 @@ async function gitArgv(cwd, args, allowRepoPrograms = false) {
64002
64003
  }
64003
64004
  async function runGit(cwd, args, opts = {}) {
64004
64005
  const argv = await gitArgv(cwd, args, opts.allowRepoPrograms);
64006
+ if (opts.outputFile) {
64007
+ const output = await open(opts.outputFile, "w", 384);
64008
+ try {
64009
+ await new Promise((resolve12, reject) => {
64010
+ const child = spawn(programPath("git"), argv, {
64011
+ cwd,
64012
+ stdio: ["pipe", output.fd, "pipe"],
64013
+ timeout: opts.timeout ?? DEFAULTS.timeout,
64014
+ ...opts.env ? { env: { ...process.env, ...opts.env } } : {}
64015
+ });
64016
+ let stderr = "";
64017
+ let overflow = false;
64018
+ child.stderr.on("data", (chunk) => {
64019
+ if (overflow) return;
64020
+ stderr += chunk.toString();
64021
+ if (Buffer.byteLength(stderr) > (opts.maxBuffer ?? DEFAULTS.maxBuffer)) {
64022
+ overflow = true;
64023
+ child.kill();
64024
+ }
64025
+ });
64026
+ child.on("error", reject);
64027
+ child.on("close", (code2, signal) => {
64028
+ if (code2 === 0 && !overflow) resolve12();
64029
+ else reject(Object.assign(new Error(overflow ? "Git stderr exceeded maxBuffer" : stderr || `Git exited with ${code2 ?? signal}`), { code: code2, stderr }));
64030
+ });
64031
+ child.stdin.on("error", () => {
64032
+ });
64033
+ child.stdin.end(opts.input);
64034
+ });
64035
+ return "";
64036
+ } finally {
64037
+ await output.close();
64038
+ }
64039
+ }
64005
64040
  const run = exec(programPath("git"), argv, {
64006
64041
  cwd,
64007
64042
  timeout: opts.timeout ?? DEFAULTS.timeout,
@@ -64162,7 +64197,7 @@ async function readTextFile(root3, rel) {
64162
64197
  const file2 = await resolveExisting(root3, rel);
64163
64198
  const pathInfo = await stat2(file2, { bigint: true });
64164
64199
  if (!pathInfo.isFile()) fail2("Path is not a regular file");
64165
- const handle = await open(file2, READ_TEXT_FLAGS);
64200
+ const handle = await open2(file2, READ_TEXT_FLAGS);
64166
64201
  try {
64167
64202
  const opened = await handle.stat({ bigint: true });
64168
64203
  if (!opened.isFile()) fail2("Path is not a regular file");
@@ -64228,7 +64263,7 @@ async function copyTree(src, dst, clone2 = cloneTree) {
64228
64263
  async function cloneTree(src, dst) {
64229
64264
  if (!clonesFiles()) return false;
64230
64265
  return new Promise((done) => {
64231
- const p = spawn("/bin/cp", ["-Rc", src, dst], { stdio: "ignore" });
64266
+ const p = spawn2("/bin/cp", ["-Rc", src, dst], { stdio: "ignore" });
64232
64267
  p.on("error", () => done(false));
64233
64268
  p.on("close", (code2) => done(code2 === 0));
64234
64269
  });
@@ -64385,7 +64420,7 @@ function isSymlink(path2) {
64385
64420
  }
64386
64421
 
64387
64422
  // packages/agent-host/src/apps/external/app-process.ts
64388
- import { spawn as spawn2 } from "node:child_process";
64423
+ import { spawn as spawn3 } from "node:child_process";
64389
64424
  import { closeSync as closeSync2, existsSync as existsSync5, mkdirSync, openSync as openSync2, renameSync as renameSync3, statSync as statSync3, writeSync as writeSync3 } from "node:fs";
64390
64425
  import { dirname as dirname3 } from "node:path";
64391
64426
 
@@ -79941,7 +79976,7 @@ var AppProcess = class _AppProcess {
79941
79976
  log.note(`starting: ${spec.command} ${spec.args.join(" ")}`);
79942
79977
  const launch2 = resolveCommand(spec.command, spec.env);
79943
79978
  const spawnedAt = Date.now();
79944
- const child = spawn2(launch2.command, [...launch2.args, ...spec.args], {
79979
+ const child = spawn3(launch2.command, [...launch2.args, ...spec.args], {
79945
79980
  cwd: spec.cwd,
79946
79981
  env: spec.env,
79947
79982
  /*
@@ -94045,7 +94080,7 @@ var RUN_RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
94045
94080
  var FAILURES_KEPT = 20;
94046
94081
  var SUMMARY_CHARS = 200;
94047
94082
  function announcingLedger(inner, announce) {
94048
- const open5 = /* @__PURE__ */ new Map();
94083
+ const open6 = /* @__PURE__ */ new Map();
94049
94084
  const tell = (apps) => {
94050
94085
  for (const a of apps ?? []) announce(a);
94051
94086
  };
@@ -94054,20 +94089,20 @@ function announcingLedger(inner, announce) {
94054
94089
  begin(row) {
94055
94090
  inner.begin(row);
94056
94091
  const apps = [{ projectId: row.projectId, appId: row.appId }];
94057
- for (const up2 of row.parentRunId ? open5.get(row.parentRunId) ?? [] : []) {
94092
+ for (const up2 of row.parentRunId ? open6.get(row.parentRunId) ?? [] : []) {
94058
94093
  if (!apps.some((a) => a.projectId === up2.projectId && a.appId === up2.appId)) apps.push(up2);
94059
94094
  }
94060
- open5.set(row.id, apps);
94095
+ open6.set(row.id, apps);
94061
94096
  tell(apps);
94062
94097
  },
94063
94098
  link(id2, sessionId) {
94064
94099
  inner.link(id2, sessionId);
94065
- tell(open5.get(id2));
94100
+ tell(open6.get(id2));
94066
94101
  },
94067
94102
  end(id2, e) {
94068
94103
  inner.end(id2, e);
94069
- tell(open5.get(id2));
94070
- open5.delete(id2);
94104
+ tell(open6.get(id2));
94105
+ open6.delete(id2);
94071
94106
  }
94072
94107
  };
94073
94108
  }
@@ -96600,8 +96635,8 @@ var ExternalApps = class {
96600
96635
  return { runId, status, result, error: error51, durationMs };
96601
96636
  };
96602
96637
  if (!e.manifest) return done("rejected", null, `This app's manifest is invalid: ${e.error}`);
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");
96638
+ const open6 = caller.kind === "session" && this.sessionBoundaryOpen(caller.sessionId);
96639
+ if (!e.scope.trusted && !open6) return done("rejected", null, "This app's project is not trusted, so Centralu does not call its apps");
96605
96640
  const held3 = this.held(e);
96606
96641
  if (held3) return done("rejected", null, held3);
96607
96642
  if (e.life.gaveUp) return done("rejected", null, `This app stopped after failing ${this.timing.maxFailures} times in a row`);
@@ -96649,7 +96684,7 @@ var ExternalApps = class {
96649
96684
  abort.abort();
96650
96685
  for (const sig of upstream) sig.removeEventListener("abort", onUp);
96651
96686
  }
96652
- }, open5);
96687
+ }, open6);
96653
96688
  } catch (err) {
96654
96689
  return done("error", null, err.message);
96655
96690
  }
@@ -97960,9 +97995,9 @@ var InlineViews = class {
97960
97995
  * just opened is never the one closed.
97961
97996
  */
97962
97997
  capLive(sessionId, keep) {
97963
- const open5 = [...this.bySession.get(sessionId)?.values() ?? []].filter((x) => x.instanceId).sort((a, b2) => a.openedAt - b2.openedAt);
97998
+ const open6 = [...this.bySession.get(sessionId)?.values() ?? []].filter((x) => x.instanceId).sort((a, b2) => a.openedAt - b2.openedAt);
97964
97999
  const n = this.limits.livePerSession;
97965
- for (const old of open5.slice(0, Math.max(0, open5.length - n))) {
98000
+ for (const old of open6.slice(0, Math.max(0, open6.length - n))) {
97966
98001
  if (old === keep) continue;
97967
98002
  this.shut(old);
97968
98003
  this.deps.emit({
@@ -98172,8 +98207,9 @@ var OriginPorts = class {
98172
98207
 
98173
98208
  // packages/agent-host/src/dev-services/worktree-recovery.ts
98174
98209
  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";
98210
+ import { createReadStream, existsSync as existsSync13 } from "node:fs";
98211
+ import { chmod, lstat as lstat3, mkdir as mkdir2, mkdtemp, readFile, readdir as readdir2, realpath as realpath3, rm as rm2, stat as stat3, writeFile as writeFile2 } from "node:fs/promises";
98212
+ import { tmpdir as tmpdir2 } from "node:os";
98177
98213
  import { join as join16, resolve as resolve2 } from "node:path";
98178
98214
  var WORKTREE_RECOVERY_MAX_BYTES = 256 * 1024 * 1024;
98179
98215
  var RECOVERY_TIMEOUT = 12e4;
@@ -98184,6 +98220,25 @@ function worktreeRecoveryDir(sessionId) {
98184
98220
  async function clearWorktreeRecovery(sessionId) {
98185
98221
  await rm2(worktreeRecoveryDir(sessionId), { recursive: true, force: true });
98186
98222
  }
98223
+ async function* gitRecords(repo, args, input) {
98224
+ const dir = await mkdtemp(join16(tmpdir2(), "cc-recovery-git-"));
98225
+ const file2 = join16(dir, "records");
98226
+ try {
98227
+ await runGit(repo, args, { outputFile: file2, timeout: RECOVERY_TIMEOUT, input });
98228
+ let pending = "";
98229
+ for await (const chunk of createReadStream(file2, { encoding: "utf8" })) {
98230
+ pending += chunk;
98231
+ let end;
98232
+ while ((end = pending.indexOf("\0")) !== -1) {
98233
+ yield pending.slice(0, end);
98234
+ pending = pending.slice(end + 1);
98235
+ }
98236
+ }
98237
+ if (pending) yield pending;
98238
+ } finally {
98239
+ await rm2(dir, { recursive: true, force: true });
98240
+ }
98241
+ }
98187
98242
  async function saveWorktreeRecovery(sessionId, cwd, trunk, branch) {
98188
98243
  const plans = [];
98189
98244
  let bytes2 = 0;
@@ -98193,7 +98248,7 @@ async function saveWorktreeRecovery(sessionId, cwd, trunk, branch) {
98193
98248
  throw new Error(`Worktree recovery exceeds the 256 MiB input limit (tracked and non-ignored untracked files plus staged changes, including submodules). No files were staged or removed; save the work elsewhere before deleting.`);
98194
98249
  }
98195
98250
  };
98196
- const prepare = async (repo, path2) => {
98251
+ const prepare = async (repo, path2, gitlinks = []) => {
98197
98252
  const tip = (await runGit(repo, ["rev-parse", "--verify", "HEAD"])).trim();
98198
98253
  const refs = ["HEAD"];
98199
98254
  if (!path2 && branch) {
@@ -98201,20 +98256,85 @@ async function saveWorktreeRecovery(sessionId, cwd, trunk, branch) {
98201
98256
  if (await runGit(repo, ["rev-parse", "--verify", ref]).then(() => true).catch(() => false)) refs.push(ref);
98202
98257
  } else if (path2) {
98203
98258
  refs.push(...(await runGit(repo, ["for-each-ref", "--format=%(refname)", "refs/heads/"])).trim().split("\n").filter(Boolean));
98259
+ if (gitlinks.length) {
98260
+ const available = await runGit(repo, ["cat-file", "--batch-check=%(objectname) %(objecttype)"], {
98261
+ input: gitlinks.map((oid) => `${oid}^{commit}`).join("\n") + "\n",
98262
+ timeout: RECOVERY_TIMEOUT,
98263
+ // Older Git ignores the lazy-fetch switch; disallow transports there too.
98264
+ env: { GIT_NO_LAZY_FETCH: "1", GIT_ALLOW_PROTOCOL: "" }
98265
+ });
98266
+ for (const row of available.trim().split("\n")) {
98267
+ const [oid, type] = row.split(" ");
98268
+ if (type === "commit") refs.push(oid);
98269
+ }
98270
+ }
98204
98271
  }
98205
98272
  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());
98273
+ const exclude = path2 ? (await runGit(repo, ["for-each-ref", "--format=^%(objectname)", "refs/remotes/"])).trim().split("\n").filter(Boolean) : baselineOid ? [`^${baselineOid}`] : [];
98274
+ const revisions = [...refs, ...exclude].join("\n") + "\n";
98275
+ const commits = Number((await runGit(repo, ["rev-list", "--count", "--stdin"], { input: revisions, timeout: RECOVERY_TIMEOUT })).trim());
98208
98276
  plans.push({ repo, path: path2, tip, refs, commits });
98209
98277
  const stagedNames = new Set((await runGit(repo, ["diff", "--cached", "--name-only", "-z", "--no-renames", tip], { timeout: RECOVERY_TIMEOUT })).split("\0"));
98210
98278
  const modules = [];
98279
+ const moduleCommits = /* @__PURE__ */ new Map();
98280
+ const keepGitlink = (rel, oid) => {
98281
+ modules.push(rel);
98282
+ const tips = moduleCommits.get(rel) ?? /* @__PURE__ */ new Set();
98283
+ tips.add(oid);
98284
+ moduleCommits.set(rel, tips);
98285
+ };
98286
+ const manifests = /* @__PURE__ */ new Set();
98287
+ const keepManifest = (rel, oid) => {
98288
+ if (rel === ".gitmodules" && !/^0+$/.test(oid)) manifests.add(oid);
98289
+ };
98290
+ let change = null;
98291
+ for await (const row of gitRecords(repo, ["log", "--raw", "-z", "--no-renames", "--ignore-submodules=none", "--no-abbrev", "--format=", "--root", "--diff-merges=separate", "--stdin"], revisions)) {
98292
+ if (change) {
98293
+ const [, oldMode, newMode, oldOid, newOid] = change;
98294
+ if (oldMode === "160000") keepGitlink(row, oldOid);
98295
+ if (newMode === "160000") keepGitlink(row, newOid);
98296
+ keepManifest(row, oldOid);
98297
+ keepManifest(row, newOid);
98298
+ change = null;
98299
+ } else change = row.match(/^\n*:(\d{6}) (\d{6}) ([a-f0-9]+) ([a-f0-9]+) [A-Z]$/);
98300
+ }
98301
+ for await (const row of gitRecords(repo, ["ls-tree", "-r", "-z", "HEAD"])) {
98302
+ const tab = row.indexOf(" ");
98303
+ const [mode, , oid] = row.slice(0, tab).split(" ");
98304
+ const rel = row.slice(tab + 1);
98305
+ if (mode === "160000") keepGitlink(rel, oid);
98306
+ keepManifest(rel, oid);
98307
+ }
98211
98308
  const stagedObjects = [];
98212
- for (const row of (await runGit(repo, ["ls-files", "--stage", "-z"])).split("\0")) {
98309
+ for await (const row of gitRecords(repo, ["ls-files", "--stage", "-z"])) {
98213
98310
  if (!row) continue;
98214
98311
  const tab = row.indexOf(" ");
98215
98312
  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]);
98313
+ const oid = row.split(" ")[1];
98314
+ if (row.startsWith("160000 ")) keepGitlink(rel, oid);
98315
+ else if (stagedNames.has(rel)) stagedObjects.push(oid);
98316
+ keepManifest(rel, oid);
98317
+ }
98318
+ const modulePaths = /* @__PURE__ */ new Map();
98319
+ for (const oid of manifests) {
98320
+ const config2 = await runGit(repo, ["config", "--blob", oid, "-z", "--get-regexp", "^submodule\\..*\\.path$"]).catch((e) => {
98321
+ if (e.code === 1) return "";
98322
+ throw e;
98323
+ });
98324
+ for (const row of config2.split("\0").filter(Boolean)) {
98325
+ const newline = row.indexOf("\n");
98326
+ const name = row.slice("submodule.".length, newline - ".path".length);
98327
+ const paths = modulePaths.get(name) ?? /* @__PURE__ */ new Set();
98328
+ paths.add(row.slice(newline + 1));
98329
+ modulePaths.set(name, paths);
98330
+ }
98331
+ }
98332
+ for (const paths of modulePaths.values()) {
98333
+ const tips = new Set([...paths].flatMap((rel) => [...moduleCommits.get(rel) ?? []]));
98334
+ for (const rel of paths) {
98335
+ modules.push(rel);
98336
+ moduleCommits.set(rel, /* @__PURE__ */ new Set([...moduleCommits.get(rel) ?? [], ...tips]));
98337
+ }
98218
98338
  }
98219
98339
  if (stagedObjects.length) {
98220
98340
  const sizes = await runGit(repo, ["cat-file", "--batch-check=%(objectsize)"], { input: stagedObjects.join("\n") + "\n" });
@@ -98236,7 +98356,7 @@ async function saveWorktreeRecovery(sessionId, cwd, trunk, branch) {
98236
98356
  await assertExistingPath(repo, rel).catch((e) => {
98237
98357
  if (existsSync13(join16(repo, rel))) throw e;
98238
98358
  });
98239
- if (existsSync13(join16(repo, rel, ".git"))) await prepare(join16(repo, rel), path2 ? `${path2}/${rel}` : rel);
98359
+ if (existsSync13(join16(repo, rel, ".git"))) await prepare(join16(repo, rel), path2 ? `${path2}/${rel}` : rel, [...moduleCommits.get(rel) ?? []]);
98240
98360
  }
98241
98361
  };
98242
98362
  await prepare(cwd, "");
@@ -98257,7 +98377,26 @@ async function saveWorktreeRecovery(sessionId, cwd, trunk, branch) {
98257
98377
  const entry = { path: path2, tip, refs, patch: false, staged: false, bundle: commits > 0 };
98258
98378
  snapshot2.repositories.push(entry);
98259
98379
  snapshot2.commits += commits;
98260
- if (entry.bundle) await runGit(repo, ["bundle", "create", join16(dir, `${n}.bundle`), ...refs], { timeout: RECOVERY_TIMEOUT });
98380
+ if (entry.bundle) {
98381
+ const temporaryRefs = [];
98382
+ const updates2 = [];
98383
+ try {
98384
+ entry.refs = [];
98385
+ for (const ref of refs) {
98386
+ if (ref === "HEAD" || ref.startsWith("refs/")) entry.refs.push(ref);
98387
+ else {
98388
+ const name = `refs/centralu-recovery/${snapshotId}/gitlink-${temporaryRefs.length}`;
98389
+ temporaryRefs.push(name);
98390
+ updates2.push(`create ${name} ${ref}`);
98391
+ entry.refs.push(name);
98392
+ }
98393
+ }
98394
+ if (updates2.length) await runGit(repo, ["update-ref", "--stdin"], { input: updates2.join("\n") + "\n", timeout: RECOVERY_TIMEOUT });
98395
+ await runGit(repo, ["bundle", "create", join16(dir, `${n}.bundle`), "--stdin"], { input: entry.refs.join("\n") + "\n", timeout: RECOVERY_TIMEOUT });
98396
+ } finally {
98397
+ if (temporaryRefs.length) await runGit(repo, ["update-ref", "--stdin"], { input: temporaryRefs.map((ref) => `delete ${ref}`).join("\n") + "\n", timeout: RECOVERY_TIMEOUT });
98398
+ }
98399
+ }
98261
98400
  entry.staged = await patchFile(repo, tip, join16(dir, `${n}.staged.patch`));
98262
98401
  const stagedNames = await runGit(repo, ["diff", "--cached", "--name-only", "-z", "--no-renames", tip], { timeout: RECOVERY_TIMEOUT });
98263
98402
  const env = { GIT_INDEX_FILE: join16(dir, `${n}.index`) };
@@ -98292,7 +98431,28 @@ async function restoreWorktreeRecovery(sessionId, repo, path2, branch, recovery,
98292
98431
  const dir = join16(worktreeRecoveryDir(sessionId), recovery.snapshot);
98293
98432
  const snapshot2 = JSON.parse(await readFile(join16(dir, "snapshot.json"), "utf8"));
98294
98433
  const root3 = snapshot2.repositories[0];
98434
+ let extraNotice = "";
98435
+ if (root3.bundle) {
98436
+ const heads = (await runGit(repo, ["bundle", "list-heads", join16(dir, "0.bundle")])).trim().split("\n");
98437
+ for (const row of heads) {
98438
+ const [tip, ref] = row.split(" ");
98439
+ if (!tip || !ref?.startsWith("refs/heads/") || tip === recovery.tip) continue;
98440
+ const base2 = `recovered/${sessionId}-stored-branch`;
98441
+ let recovered = base2;
98442
+ for (let suffix = 1; ; suffix++) {
98443
+ const existing = await runGit(repo, ["rev-parse", "--verify", `refs/heads/${recovered}`]).then((s) => s.trim()).catch(() => null);
98444
+ if (existing === tip) break;
98445
+ if (!existing) {
98446
+ await runGit(repo, ["fetch", join16(dir, "0.bundle"), `${ref}:refs/heads/${recovered}`], { allowRepoPrograms: trust.trusted === true });
98447
+ break;
98448
+ }
98449
+ recovered = `${base2}-${suffix}`;
98450
+ }
98451
+ extraNotice += `Stored branch ${ref.slice("refs/heads/".length)} recovered as ${recovered}. `;
98452
+ }
98453
+ }
98295
98454
  if (!existsSync13(path2)) {
98455
+ await rm2(join16(dir, "patches-applied"), { force: true });
98296
98456
  if (await runGit(repo, ["show-ref", "--verify", `refs/heads/${branch}`]).then(() => true).catch(() => false)) {
98297
98457
  branch = `recovered/${sessionId}`;
98298
98458
  }
@@ -98302,18 +98462,26 @@ async function restoreWorktreeRecovery(sessionId, repo, path2, branch, recovery,
98302
98462
  try {
98303
98463
  if (root3.staged) await runGit(path2, ["apply", "--cached", "--binary", join16(dir, "0.staged.patch")]);
98304
98464
  if (root3.patch) await runGit(path2, ["apply", "--binary", join16(dir, "0.patch")]);
98465
+ await writeFile2(join16(dir, "patches-applied"), await realpath3(path2), { mode: 384 });
98305
98466
  } 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.` };
98467
+ return { branch, notice: `Restored branch ${branch}. ${extraNotice}Patches could not all be applied. Recovery files remain at ${dir}; apply numbered .patch files manually with git apply and .staged.patch files with git apply --cached.` };
98307
98468
  }
98308
98469
  }
98309
98470
  } else {
98310
98471
  const common = async (cwd) => realpath3(resolve2(cwd, (await runGit(cwd, ["rev-parse", "--git-common-dir"])).trim()));
98311
98472
  if (await common(path2) !== await common(repo)) throw new Error(`The worktree path belongs to another repository. Recovery files remain at ${dir}`);
98312
98473
  branch = (await runGit(path2, ["symbolic-ref", "--quiet", "--short", "HEAD"])).trim();
98474
+ if (root3.patch || root3.staged) {
98475
+ const appliedTo = await readFile(join16(dir, "patches-applied"), "utf8").catch((e) => {
98476
+ if (e.code === "ENOENT") return null;
98477
+ throw e;
98478
+ });
98479
+ extraNotice += appliedTo === await realpath3(path2) ? "Saved patches were applied during the previous restore attempt; they were not reapplied on this retry. " : "The worktree already existed; saved patches were not applied on this retry and may still need manual recovery. ";
98480
+ }
98313
98481
  }
98314
98482
  return {
98315
98483
  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.`
98484
+ notice: `Restored branch ${branch}. ${extraNotice}Recovery files remain at ${dir}: ${recovery.changedFiles} uncommitted files, ${recovery.commits} commits absent from the trunk or submodule remotes. Numbered .patch files can be applied with git apply; .bundle files list their saved refs with git bundle list-heads and can be fetched with git fetch <bundle> <ref>:<branch>. snapshot.json names each repository and its refs. Submodule recovery is manual. These files are removed when this session is deleted for good.`
98317
98485
  };
98318
98486
  }
98319
98487
 
@@ -98406,8 +98574,12 @@ and approved skills, and whether every session can look at its own project), App
98406
98574
  Shortcuts (the shortcut list), and Updates.`,
98407
98575
  updates: `# Updates
98408
98576
  Check from Settings \u2192 Updates. It reports a new version based on the npm registry, and the person
98409
- clicks to install it. The check for a new version runs on startup and every six hours, and it can
98410
- be turned off.
98577
+ clicks to install it. With automatic checking on, the check runs on startup, every six hours, when
98578
+ Updates opens, and on focus when the last check is over an hour old. With it off, use Check now.
98579
+ Ordinary checks follow npm, including dist-tag rollbacks. Installing reuses a successful check
98580
+ from the last minute or checks again. Only while installing can a higher stored version seen less
98581
+ than an hour ago win over the fresh tag. Older observations yield to npm. If the check fails, it
98582
+ uses the stored version and says so. Checks preserve install errors until the next install attempt.
98411
98583
  Once installed, the desktop app offers "Apply now": it relaunches into the new version, and running
98412
98584
  agents, terminals and commands keep going. "Apply updates automatically when idle" (off by default)
98413
98585
  installs a new version without a click and applies it once no session is working or waiting, no
@@ -98780,6 +98952,7 @@ function agentRunText(r) {
98780
98952
  case "stopped":
98781
98953
  return { text: `Stopped ${who}.` };
98782
98954
  case "deleted":
98955
+ if (r.branch === void 0) return { text: `Moved ${who} to the trash. Tell the person in one line.` };
98783
98956
  return {
98784
98957
  text: (r.warning ? `Moved ${who} to the trash. Warning: ${r.warning} ` : `Cleaned up ${who}: its worktree and branch "${r.branch}" were removed and the session went to the trash. `) + (r.setAside.changedFiles || r.setAside.commits ? `${r.setAside.changedFiles} uncommitted files, ${r.setAside.commits} commits not on the trunk, kept in the trash. ` : "Nothing needed setting aside. ") + "Tell the person in one line."
98785
98958
  };
@@ -101604,7 +101777,7 @@ async function readClaudeModels(q2) {
101604
101777
  }
101605
101778
 
101606
101779
  // packages/agent-host/src/adapters/local-process.ts
101607
- import { spawn as spawn4 } from "node:child_process";
101780
+ import { spawn as spawn5 } from "node:child_process";
101608
101781
  import { EventEmitter as EventEmitter3 } from "node:events";
101609
101782
  var EXIT_AFTER_OUTPUT_MS3 = 1e3;
101610
101783
  var STDERR_TAIL = 8 * 1024;
@@ -101634,7 +101807,7 @@ var LocalAgentProcess = class _LocalAgentProcess extends EventEmitter3 {
101634
101807
  exitReported = false;
101635
101808
  spawnedAt = Date.now();
101636
101809
  static spawn(spec, os2) {
101637
- const child = spawn4(spec.command, spec.args, {
101810
+ const child = spawn5(spec.command, spec.args, {
101638
101811
  cwd: spec.cwd,
101639
101812
  env: spec.env,
101640
101813
  stdio: ["pipe", "pipe", "pipe"],
@@ -102818,10 +102991,19 @@ function readTarget(toolName, input) {
102818
102991
  const p = toolName === "Read" ? input.file_path : input.path;
102819
102992
  return typeof p === "string" && p ? p : null;
102820
102993
  }
102821
- function permissionOptionsFor(preset) {
102822
- if (preset === "auto") return { permissionMode: "bypassPermissions" };
102823
- if (preset === "safe") return { permissionMode: "default" };
102824
- return { resolvePermissionModeInCli: true };
102994
+ async function permissionOptionsFor(opts) {
102995
+ if (opts.permissionPreset === "auto") return { permissionMode: "bypassPermissions" };
102996
+ if (opts.permissionPreset === "safe") return { permissionMode: "default" };
102997
+ if (opts.noSettingFiles) return { permissionMode: await personsDefaultMode() ?? "default" };
102998
+ try {
102999
+ const resolved = await D9t({ cwd: opts.cwd, ...settingSourcesFor(opts) });
103000
+ const mode = qtt(resolved).permissions?.defaultMode;
103001
+ if (typeof mode === "string" && PERMISSION_MODES.includes(mode)) {
103002
+ return { permissionMode: mode };
103003
+ }
103004
+ } catch {
103005
+ }
103006
+ return { permissionMode: "default" };
102825
103007
  }
102826
103008
  var PERMISSION_MODES = ["default", "acceptEdits", "bypassPermissions", "plan", "dontAsk", "auto"];
102827
103009
  async function personsDefaultMode() {
@@ -102961,7 +103143,7 @@ var ClaudeSession = class {
102961
103143
  this.orchestratorServer = orchestratorMcp(this.opts.orchestratorTools, this.opts.toolProfile, this.opts.sessionId);
102962
103144
  }
102963
103145
  const servers = this.mcpServers();
102964
- const personsMode = preset === "normal" && this.opts.noSettingFiles ? await personsDefaultMode() : null;
103146
+ const permissionOptions = await permissionOptionsFor(this.opts);
102965
103147
  if (!this.opts.processSource?.adopt) await this.launch.gate.turn();
102966
103148
  this.runKey = run?.key ?? null;
102967
103149
  this.launch.links.acquire(this.runKey);
@@ -103054,7 +103236,7 @@ var ClaudeSession = class {
103054
103236
  */
103055
103237
  ...this.opts.systemPromptAppend ? { systemPrompt: { type: "preset", preset: "claude_code", append: this.opts.systemPromptAppend } } : {}
103056
103238
  } : {},
103057
- ...personsMode ? { permissionMode: personsMode } : permissionOptionsFor(preset),
103239
+ ...permissionOptions,
103058
103240
  /*
103059
103241
  * The agent an app hands a schema and asks for output (M4 D-1). It is a per-query option,
103060
103242
  * so it can only be given when the session starts. Measured (SDK 0.3.263, CLI 2.1.282,
@@ -103296,8 +103478,8 @@ var ClaudeSession = class {
103296
103478
  * turn released, rather than left hanging; the person can send the message again.
103297
103479
  */
103298
103480
  releaseLostCalls(q2) {
103299
- const open5 = this.opts.processSource?.adopt?.openCalls ?? [];
103300
- const lost = open5.filter((c) => isOrchestratorTool(c.tool) || appToolOf(c.tool) !== null);
103481
+ const open6 = this.opts.processSource?.adopt?.openCalls ?? [];
103482
+ const lost = open6.filter((c) => isOrchestratorTool(c.tool) || appToolOf(c.tool) !== null);
103301
103483
  if (lost.length === 0) return;
103302
103484
  this.turnOpen = true;
103303
103485
  this.stream.stopped();
@@ -103428,11 +103610,11 @@ var ClaudeSession = class {
103428
103610
  * the model receives the answers in the CLI's own words (see the AskUserQuestion branch of `canUseTool`).
103429
103611
  */
103430
103612
  answerQuestion(requestId, answers) {
103431
- const open5 = this.questions.get(requestId);
103432
- if (!open5) return false;
103613
+ const open6 = this.questions.get(requestId);
103614
+ if (!open6) return false;
103433
103615
  this.questions.delete(requestId);
103434
103616
  const byQuestion = Object.fromEntries(answers.map((a) => [a.question, a.answers.join(", ")]));
103435
- open5.resolve({ behavior: "allow", updatedInput: { ...open5.input, answers: byQuestion } });
103617
+ open6.resolve({ behavior: "allow", updatedInput: { ...open6.input, answers: byQuestion } });
103436
103618
  this.emit({ type: "question_resolved", sessionId: this.sessionId, requestId });
103437
103619
  return true;
103438
103620
  }
@@ -103708,8 +103890,8 @@ var ClaudeSession = class {
103708
103890
  }
103709
103891
  /** Releases a choice that was still waiting on an answer — **never released silently**, for the same reason as approval. */
103710
103892
  releaseQuestions(why) {
103711
- for (const [id2, open5] of this.questions) {
103712
- open5.resolve({ behavior: "deny", message: why });
103893
+ for (const [id2, open6] of this.questions) {
103894
+ open6.resolve({ behavior: "deny", message: why });
103713
103895
  this.emit({ type: "question_resolved", sessionId: this.sessionId, requestId: id2 });
103714
103896
  }
103715
103897
  this.questions.clear();
@@ -103815,7 +103997,7 @@ var ClaudeAdapter = class _ClaudeAdapter {
103815
103997
  sleep: true
103816
103998
  };
103817
103999
  launchSettingsFingerprint(settings) {
103818
- return JSON.stringify({ v: settings.permissionPreset === "auto" ? 2 : 1, cwd: settings.cwd, permission: settings.permissionPreset, trusted: !!settings.projectTrusted, noSettings: !!settings.noSettingFiles });
104000
+ return JSON.stringify({ v: settings.permissionPreset === "safe" ? 1 : 2, cwd: settings.cwd, permission: settings.permissionPreset, trusted: !!settings.projectTrusted, noSettings: !!settings.noSettingFiles });
103819
104001
  }
103820
104002
  async detect() {
103821
104003
  const path2 = whichTool("claude");
@@ -104163,7 +104345,7 @@ var CodexClient = class {
104163
104345
  };
104164
104346
 
104165
104347
  // packages/agent-host/src/adapters/codex/rollout.ts
104166
- import { createReadStream as createReadStream2 } from "node:fs";
104348
+ import { createReadStream as createReadStream3 } from "node:fs";
104167
104349
  import { readdir as readdir5 } from "node:fs/promises";
104168
104350
  import { createInterface as createInterface2 } from "node:readline";
104169
104351
  import { homedir as homedir6 } from "node:os";
@@ -104199,7 +104381,7 @@ async function lastCompactSummary(threadId, sessionsDir) {
104199
104381
  if (!path2) return null;
104200
104382
  try {
104201
104383
  let last = null;
104202
- const rl2 = createInterface2({ input: createReadStream2(path2, "utf8"), crlfDelay: Infinity });
104384
+ const rl2 = createInterface2({ input: createReadStream3(path2, "utf8"), crlfDelay: Infinity });
104203
104385
  for await (const line of rl2) {
104204
104386
  if (!line.includes('"compacted"')) continue;
104205
104387
  try {
@@ -106321,10 +106503,10 @@ var CodexSession = class {
106321
106503
  return this.opts.outputSchema ? { outputSchema: this.opts.outputSchema } : {};
106322
106504
  }
106323
106505
  respondApproval(requestId, decision, _scope, matcher) {
106324
- const open5 = this.approvals.get(requestId);
106325
- if (open5 === void 0) return false;
106506
+ const open6 = this.approvals.get(requestId);
106507
+ if (open6 === void 0) return false;
106326
106508
  this.approvals.delete(requestId);
106327
- const serverId = open5.serverId;
106509
+ const serverId = open6.serverId;
106328
106510
  if (this.elicitations.delete(requestId)) {
106329
106511
  this.client.respond(serverId, {
106330
106512
  action: decision === "deny" ? "decline" : "accept",
@@ -110100,7 +110282,7 @@ async function sweepAttachments(maxBytes = ATTACHMENTS_MAX_BYTES) {
110100
110282
 
110101
110283
  // packages/agent-host/src/dev-services/message-image.ts
110102
110284
  import { constants as constants7 } from "node:fs";
110103
- import { open as open4, realpath as realpath7 } from "node:fs/promises";
110285
+ import { open as open5, realpath as realpath7 } from "node:fs/promises";
110104
110286
  import { homedir as homedir11 } from "node:os";
110105
110287
  import { posix as posix3, sep as sep10, win32 as win326 } from "node:path";
110106
110288
  import { fileURLToPath as fileURLToPath5 } from "node:url";
@@ -110165,7 +110347,7 @@ async function readMessageImage(abs, max = IMAGE_PREVIEW_MAX_BYTES) {
110165
110347
  }
110166
110348
  let handle;
110167
110349
  try {
110168
- handle = await open4(file2, OPEN_FLAGS);
110350
+ handle = await open5(file2, OPEN_FLAGS);
110169
110351
  } catch (e) {
110170
110352
  const code2 = e.code;
110171
110353
  if (code2 === "EACCES" || code2 === "EPERM") return refuse5("unreadable", `Centralu may not read ${file2}`, file2);
@@ -111451,14 +111633,14 @@ var SessionManager = class {
111451
111633
  }
111452
111634
  /** Tool calls the store holds for this session with no result after them (the newest 200 rows) */
111453
111635
  openToolCalls(sessionId) {
111454
- const open5 = /* @__PURE__ */ new Map();
111636
+ const open6 = /* @__PURE__ */ new Map();
111455
111637
  for (const r of this.store.loadMessages(sessionId, 200)) {
111456
111638
  const p = r.payload;
111457
111639
  if (typeof p?.callId !== "string") continue;
111458
- if (r.kind === "tool_call" && typeof p.summary?.tool === "string") open5.set(p.callId, p.summary.tool);
111459
- else if (r.kind === "tool_result") open5.delete(p.callId);
111640
+ if (r.kind === "tool_call" && typeof p.summary?.tool === "string") open6.set(p.callId, p.summary.tool);
111641
+ else if (r.kind === "tool_result") open6.delete(p.callId);
111460
111642
  }
111461
- return [...open5].map(([callId, tool]) => ({ callId, tool }));
111643
+ return [...open6].map(([callId, tool]) => ({ callId, tool }));
111462
111644
  }
111463
111645
  /**
111464
111646
  * Re-attaches the tool processes a previous host left running in the keeper (#280 step 2), so
@@ -111713,6 +111895,7 @@ var SessionManager = class {
111713
111895
  this.leaving.delete(sessionId);
111714
111896
  }
111715
111897
  }
111898
+ trashing = /* @__PURE__ */ new Map();
111716
111899
  /**
111717
111900
  * Moves a session to the trash (#204) — what deleting a session means now. Nothing is destroyed: the process
111718
111901
  * stops, and the session leaves every list (sidebar, inbox, palette, grid, search, the agents' session tools and
@@ -111732,6 +111915,17 @@ var SessionManager = class {
111732
111915
  * tool, and **+ → Past conversations**, still have it.
111733
111916
  */
111734
111917
  async trashSession(sessionId, removeWorktree = false, removeExternal = false, recovery) {
111918
+ const pending = this.trashing.get(sessionId);
111919
+ if (pending) return pending;
111920
+ const deleting = this.trashSessionOnce(sessionId, removeWorktree, removeExternal, recovery);
111921
+ this.trashing.set(sessionId, deleting);
111922
+ try {
111923
+ await deleting;
111924
+ } finally {
111925
+ this.trashing.delete(sessionId);
111926
+ }
111927
+ }
111928
+ async trashSessionOnce(sessionId, removeWorktree, removeExternal, recovery) {
111735
111929
  if (this.worktreeCleanup.has(sessionId) && !recovery) throw Object.assign(new Error("The session worktree is being removed"), { code: "internal" });
111736
111930
  const m = this.meta.get(sessionId);
111737
111931
  if (!m) {
@@ -114342,7 +114536,7 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
114342
114536
  * send gives it more, including during the caller's turn on adapters that accept mid-turn input; refused
114343
114537
  * while it works on the person's own message or has a higher preset than the caller
114344
114538
  * stop stops the turn the caller gave it; a turn the person started there is theirs
114345
- * delete sets aside local work in the trash, then removes an idle child's worktree and branch (#608)
114539
+ * delete trashes a child without a worktree, stopping it first; worktree cleanup requires an idle child (#608)
114346
114540
  */
114347
114541
  async agentSession(callerId, opts, signal) {
114348
114542
  if (opts.sessionId === callerId && opts.action !== "status") {
@@ -114350,7 +114544,9 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
114350
114544
  }
114351
114545
  const caller = this.meta.get(callerId);
114352
114546
  const child = this.meta.get(opts.sessionId);
114353
- if (!caller?.projectId || !child || child.askedBy !== callerId && !this.autoBoundaryOpen(callerId, "session ownership") || child.projectId !== caller.projectId) {
114547
+ const deletingWithoutWorktree = opts.action === "delete" && !child?.worktree;
114548
+ const startedHere = child?.askedBy === callerId || deletingWithoutWorktree && child?.parentSessionId === callerId;
114549
+ if (!caller?.projectId || !child || !startedHere && (deletingWithoutWorktree || !this.autoBoundaryOpen(callerId, "session ownership")) || child.projectId !== caller.projectId) {
114354
114550
  return { ok: false, error: `${opts.sessionId} is not a session this session started. agent_session works only on sessions start_agent started here.` };
114355
114551
  }
114356
114552
  if (opts.sessionId === callerId) return this.agentAtRest(child);
@@ -114383,6 +114579,15 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
114383
114579
  return { ok: true, state: "stopped", sessionId: child.id, sessionName: child.name, tool: child.tool };
114384
114580
  }
114385
114581
  case "delete": {
114582
+ if (!child.worktree) {
114583
+ const busy = child.state === "working" || child.state === "waiting_approval" || !!child.pendingApproval || child.pendingQuestions.length > 0 || this.agentRuns.has(child.id) && !this.agentRuns.get(child.id).finished;
114584
+ if (!this.trashing.has(child.id) && busy && (!d2 || d2.settled || d2.callerId !== callerId)) {
114585
+ return { ok: false, error: `"${child.name}" is busy with something else; the person may be using it. Wait for that turn and its pending approval or question to finish before deleting it.` };
114586
+ }
114587
+ await this.trashSession(child.id);
114588
+ if (d2) this.delegations.delete(key);
114589
+ return { ok: true, state: "deleted", sessionId: child.id, sessionName: child.name, tool: child.tool };
114590
+ }
114386
114591
  if (d2 && !d2.settled) return { ok: false, error: `"${child.name}" is still working on what you gave it \u2014 stop it or wait first.` };
114387
114592
  const r = await this.cleanUpWorktreeChild(child);
114388
114593
  if (!r.ok) return r;
@@ -114468,6 +114673,9 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
114468
114673
  if (target.state === "working" || target.state === "waiting_approval" || this.agentRuns.has(target.id) || sessionBusy({ ...target, live: this.handles.has(target.id) })) {
114469
114674
  return { ok: false, error: `"${target.name}" is still working \u2014 stop it or wait first` };
114470
114675
  }
114676
+ if (!existsSync19(target.worktree.path)) {
114677
+ return { ok: false, error: `The worktree folder for "${target.name}" is missing: ${target.worktree.path}. Restore the folder before cleanup; its branch has been left in place.` };
114678
+ }
114471
114679
  if (this.resuming.has(target.id) || this.deferredRestarts.has(target.id)) return { ok: false, error: `"${target.name}" is waking or restarting \u2014 wait for it first` };
114472
114680
  if (this.worktreeCleanup.has(target.id)) return { ok: false, error: "This worktree is already being removed" };
114473
114681
  this.worktreeCleanup.add(target.id);
@@ -115355,8 +115563,8 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
115355
115563
  return readMessageImage(abs);
115356
115564
  }
115357
115565
  repliesName(sessionId, written) {
115358
- const open5 = this.streams.get(sessionId);
115359
- if (open5?.kind === "text" && mentions(open5.text, written)) return true;
115566
+ const open6 = this.streams.get(sessionId);
115567
+ if (open6?.kind === "text" && mentions(open6.text, written)) return true;
115360
115568
  return writtenForms(written).some((form) => this.store.replyTextsContaining(sessionId, form).some((text2) => mentions(text2, written)));
115361
115569
  }
115362
115570
  /**
@@ -116558,9 +116766,9 @@ function broadcastAppChanges(send, windowMs = APP_CHANGE_WINDOW_MS) {
116558
116766
  return {
116559
116767
  emit(ref, cause2 = null) {
116560
116768
  const key = JSON.stringify([ref.projectId, ref.appId]);
116561
- const open5 = pending.get(key);
116562
- if (open5) {
116563
- if (!sameCause(open5.cause, cause2)) open5.cause = null;
116769
+ const open6 = pending.get(key);
116770
+ if (open6) {
116771
+ if (!sameCause(open6.cause, cause2)) open6.cause = null;
116564
116772
  return;
116565
116773
  }
116566
116774
  const timer = setTimeout(() => {
@@ -117026,6 +117234,8 @@ var REGISTRY_URL = `https://registry.npmjs.org/${APP_SLUG}/latest`;
117026
117234
  var CHECK_INTERVAL_MS = 6 * 60 * 60 * 1e3;
117027
117235
  var SCHEDULED_TIMEOUT_MS = 3e3;
117028
117236
  var FORCED_TIMEOUT_MS = 8e3;
117237
+ var INSTALL_CHECK_MAX_AGE_MS = 6e4;
117238
+ var STORED_RELEASE_MAX_AGE_MS = 60 * 60 * 1e3;
117029
117239
  var INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
117030
117240
  var NPM_RETRY_DELAY_MS = 3e4;
117031
117241
  var NPM_RETRY_WINDOW_MS = 10 * 60 * 1e3;
@@ -117106,6 +117316,8 @@ var UpdateService = class {
117106
117316
  }
117107
117317
  publish;
117108
117318
  status;
117319
+ stopped = false;
117320
+ installing = false;
117109
117321
  timer = null;
117110
117322
  deps;
117111
117323
  /** One check at a time — the timer and the button can land together */
@@ -117121,16 +117333,22 @@ var UpdateService = class {
117121
117333
  * closed while releases happened.
117122
117334
  */
117123
117335
  start() {
117336
+ this.stopped = false;
117124
117337
  if (!this.status.auto) return;
117125
117338
  void this.check(false);
117126
117339
  this.startTimer();
117127
117340
  }
117341
+ /** Shutdown (including a host swap): no asynchronous continuation may start more work. */
117128
117342
  stop() {
117343
+ this.stopped = true;
117344
+ this.stopTimer();
117345
+ }
117346
+ stopTimer() {
117129
117347
  if (this.timer) clearInterval(this.timer);
117130
117348
  this.timer = null;
117131
117349
  }
117132
117350
  startTimer() {
117133
- this.stop();
117351
+ this.stopTimer();
117134
117352
  this.timer = setInterval(() => void this.check(false), CHECK_INTERVAL_MS);
117135
117353
  this.timer.unref?.();
117136
117354
  }
@@ -117146,7 +117364,7 @@ var UpdateService = class {
117146
117364
  this.status = { ...this.status, auto: enabled };
117147
117365
  this.deps.writeAuto(enabled);
117148
117366
  if (!enabled) {
117149
- this.stop();
117367
+ this.stopTimer();
117150
117368
  this.emit();
117151
117369
  return this.current();
117152
117370
  }
@@ -117183,32 +117401,36 @@ var UpdateService = class {
117183
117401
  * failed, nothing about a background failure interrupts anyone.
117184
117402
  */
117185
117403
  async check(force) {
117404
+ if (this.stopped || this.installing) return this.current();
117186
117405
  if (this.inFlight) return this.inFlight;
117187
117406
  if (!force && !this.status.auto) return this.current();
117188
117407
  if (!force && this.status.checkedAt !== null && !this.isDue()) return this.current();
117189
117408
  if (this.status.phase === "updating" || this.status.phase === "waiting_for_npm" || this.status.phase === "restart_required") return this.current();
117190
- this.status = { ...this.status, phase: "checking" };
117409
+ const failure3 = this.status.phase === "failed" ? { error: this.status.error } : null;
117410
+ if (!failure3) this.status = { ...this.status, phase: "checking" };
117191
117411
  this.emit();
117192
- this.inFlight = this.runCheck(force);
117412
+ this.inFlight = this.runCheck(force, failure3);
117193
117413
  try {
117194
117414
  return await this.inFlight;
117195
117415
  } finally {
117196
117416
  this.inFlight = null;
117197
117417
  }
117198
117418
  }
117199
- async runCheck(force) {
117419
+ async runCheck(force, failure3) {
117200
117420
  const res = await this.deps.fetchLatest(force ? FORCED_TIMEOUT_MS : SCHEDULED_TIMEOUT_MS);
117421
+ if (this.stopped || this.installing) return this.current();
117201
117422
  if (!res.ok) {
117202
- this.status = { ...this.status, phase: "idle", error: res.reason };
117423
+ this.status = { ...this.status, phase: failure3 ? "failed" : "idle", error: failure3 ? failure3.error : res.reason };
117203
117424
  this.emit();
117204
117425
  return this.current();
117205
117426
  }
117427
+ const latest = res.version;
117206
117428
  this.status = {
117207
117429
  ...this.status,
117208
- latest: res.version,
117209
- newer: isNewerVersion(res.version, this.status.current),
117210
- phase: "idle",
117211
- error: null,
117430
+ latest,
117431
+ newer: isNewerVersion(latest, this.status.current),
117432
+ phase: failure3 ? "failed" : "idle",
117433
+ error: failure3?.error ?? null,
117212
117434
  checkedAt: this.deps.now()
117213
117435
  };
117214
117436
  this.emit();
@@ -117224,13 +117446,14 @@ var UpdateService = class {
117224
117446
  * rest of the story arrives as `update_status` events.
117225
117447
  */
117226
117448
  apply() {
117227
- if (this.status.phase === "updating" || this.status.phase === "waiting_for_npm") return this.current();
117449
+ if (this.stopped || this.installing || this.status.phase === "restart_required") return this.current();
117228
117450
  const latest = this.status.latest;
117229
117451
  if (!this.status.newer || !latest) {
117230
117452
  this.status = { ...this.status, phase: "failed", error: "There is no newer version to install" };
117231
117453
  this.emit();
117232
117454
  return this.current();
117233
117455
  }
117456
+ this.installing = true;
117234
117457
  this.status = { ...this.status, phase: "updating", error: null };
117235
117458
  this.emit();
117236
117459
  void this.runApply(latest);
@@ -117240,16 +117463,42 @@ var UpdateService = class {
117240
117463
  let packageInstalled = false;
117241
117464
  const copy = this.deps.installedCopy();
117242
117465
  try {
117466
+ await this.inFlight;
117467
+ if (this.stopped) return;
117468
+ const age2 = this.status.checkedAt === null ? Infinity : this.deps.now() - this.status.checkedAt;
117469
+ if (age2 < 0 || age2 >= INSTALL_CHECK_MAX_AGE_MS) {
117470
+ const fresh = await this.deps.fetchLatest(FORCED_TIMEOUT_MS);
117471
+ if (this.stopped) return;
117472
+ if (fresh.ok) {
117473
+ const now = this.deps.now();
117474
+ const storedAge = this.status.checkedAt === null ? Infinity : now - this.status.checkedAt;
117475
+ const keepStored = storedAge >= 0 && storedAge < STORED_RELEASE_MAX_AGE_MS && isNewerVersion(version2, fresh.version);
117476
+ if (!keepStored) version2 = fresh.version;
117477
+ this.status = { ...this.status, latest: version2, newer: isNewerVersion(version2, this.status.current), checkedAt: keepStored ? this.status.checkedAt : now };
117478
+ if (!this.status.newer) {
117479
+ this.status = { ...this.status, phase: "idle", error: null };
117480
+ return;
117481
+ }
117482
+ } else {
117483
+ this.status = { ...this.status, error: `Could not refresh latest (${fresh.reason}); using previously checked ${version2}.` };
117484
+ }
117485
+ }
117486
+ this.emit();
117243
117487
  await this.installPackage(version2);
117488
+ if (this.stopped) return;
117244
117489
  packageInstalled = true;
117245
117490
  if (copy) await this.deps.run(APP_SLUG, ["install"]);
117246
- this.status = { ...this.status, phase: "restart_required", error: null };
117491
+ if (this.stopped) return;
117492
+ this.status = { ...this.status, phase: "restart_required" };
117247
117493
  } catch (e) {
117494
+ if (this.stopped) return;
117248
117495
  const reason = e.message;
117249
117496
  const error51 = packageInstalled ? `npm installed ${version2}, but the copy at ${copy} was not replaced (${reason}). Quit ${APP_NAME}, then run "${APP_SLUG} install" in a terminal.` : reason;
117250
117497
  this.status = { ...this.status, phase: "failed", error: error51 };
117498
+ } finally {
117499
+ this.installing = false;
117500
+ if (!this.stopped) this.emit();
117251
117501
  }
117252
- this.emit();
117253
117502
  }
117254
117503
  /**
117255
117504
  * A just-published release can be visible from `latest` before its package is on the npm edge
@@ -117259,16 +117508,20 @@ var UpdateService = class {
117259
117508
  */
117260
117509
  async installPackage(version2) {
117261
117510
  const started = this.deps.now();
117511
+ const notice = this.status.error;
117262
117512
  for (; ; ) {
117513
+ if (this.stopped) return;
117263
117514
  try {
117264
117515
  await this.deps.run("npm", ["i", "-g", "--prefer-online", `${APP_SLUG}@${version2}`]);
117265
117516
  return;
117266
117517
  } catch (err) {
117518
+ if (this.stopped) return;
117267
117519
  if (!npmHasNotServedVersion(err) || this.deps.now() - started >= NPM_RETRY_WINDOW_MS) throw err;
117268
- this.status = { ...this.status, phase: "waiting_for_npm", error: `Waiting for npm to serve ${version2}` };
117520
+ this.status = { ...this.status, phase: "waiting_for_npm", error: `Waiting for npm to serve ${version2}${notice ? `. ${notice}` : ""}` };
117269
117521
  this.emit();
117270
117522
  await this.deps.wait(NPM_RETRY_DELAY_MS);
117271
- this.status = { ...this.status, phase: "updating", error: null };
117523
+ if (this.stopped) return;
117524
+ this.status = { ...this.status, phase: "updating", error: notice };
117272
117525
  this.emit();
117273
117526
  }
117274
117527
  }
@@ -118159,7 +118412,7 @@ async function verifiedPackage(name, version2, opts = {}) {
118159
118412
  }
118160
118413
 
118161
118414
  // packages/agent-host/src/links/tunnel.ts
118162
- import { spawn as spawn5 } from "node:child_process";
118415
+ import { spawn as spawn6 } from "node:child_process";
118163
118416
  import { createConnection as createConnection2, createServer as createServer3 } from "node:net";
118164
118417
  function parseConnectionLine(stdout) {
118165
118418
  const lines = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
@@ -118423,7 +118676,7 @@ var SshTunnel = class {
118423
118676
  }
118424
118677
  run(args, stdin) {
118425
118678
  return new Promise((resolve12, reject) => {
118426
- const child = spawn5(this.opts.ssh ?? "ssh", args, { env: this.opts.env ?? process.env, stdio: [stdin === void 0 ? "ignore" : "pipe", "pipe", "pipe"] });
118679
+ const child = spawn6(this.opts.ssh ?? "ssh", args, { env: this.opts.env ?? process.env, stdio: [stdin === void 0 ? "ignore" : "pipe", "pipe", "pipe"] });
118427
118680
  if (stdin !== void 0) {
118428
118681
  child.stdin.on("error", () => {
118429
118682
  });
@@ -118482,7 +118735,7 @@ var SshTunnel = class {
118482
118735
  this.opts.target,
118483
118736
  ...keepAlive ? [keepAlive] : []
118484
118737
  ];
118485
- const child = spawn5(this.opts.ssh ?? "ssh", args, { env: this.opts.env ?? process.env, stdio: ["ignore", "ignore", "pipe"] });
118738
+ const child = spawn6(this.opts.ssh ?? "ssh", args, { env: this.opts.env ?? process.env, stdio: ["ignore", "ignore", "pipe"] });
118486
118739
  this.child = child;
118487
118740
  let stderr = "";
118488
118741
  child.stderr.on("data", (d2) => stderr += String(d2));
@@ -118834,7 +119087,7 @@ async function removeAutostart(c) {
118834
119087
  }
118835
119088
 
118836
119089
  // packages/agent-host/src/links/probe.ts
118837
- import { spawn as spawn6 } from "node:child_process";
119090
+ import { spawn as spawn7 } from "node:child_process";
118838
119091
  var PROBE_MARK = "CENTRALU-PROBE";
118839
119092
  var PROBE_PS = [
118840
119093
  "$ErrorActionPreference = 'SilentlyContinue'",
@@ -118889,7 +119142,7 @@ function probeTarget(target, opts = {}) {
118889
119142
  clearTimeout(timer);
118890
119143
  resolve12(parseProbe(t, run));
118891
119144
  };
118892
- const child = spawn6(opts.ssh ?? "ssh", args, { env: opts.env ?? process.env, stdio: ["ignore", "pipe", "pipe"] });
119145
+ const child = spawn7(opts.ssh ?? "ssh", args, { env: opts.env ?? process.env, stdio: ["ignore", "pipe", "pipe"] });
118893
119146
  const timer = setTimeout(() => {
118894
119147
  child.kill("SIGTERM");
118895
119148
  finish2({ code: 255, stdout: "", stderr: `ssh to ${t} did not answer in time` });
@@ -118902,7 +119155,7 @@ function probeTarget(target, opts = {}) {
118902
119155
  }
118903
119156
 
118904
119157
  // packages/agent-host/src/links/ssh-config.ts
118905
- import { spawn as spawn7 } from "node:child_process";
119158
+ import { spawn as spawn8 } from "node:child_process";
118906
119159
  import { readdirSync as readdirSync8, readFileSync as readFileSync15, statSync as statSync13 } from "node:fs";
118907
119160
  import { homedir as homedir13 } from "node:os";
118908
119161
  import { isAbsolute as isAbsolute10, join as join32, normalize, parse as parse6, sep as sep12 } from "node:path";
@@ -119077,7 +119330,7 @@ function resolveAlias(alias, opts = {}) {
119077
119330
  resolve12(r);
119078
119331
  };
119079
119332
  const none = (error51) => ({ hostName: null, user: null, port: null, error: error51 });
119080
- const child = spawn7(opts.ssh ?? "ssh", args, { env: opts.env ?? process.env, stdio: ["ignore", "pipe", "pipe"] });
119333
+ const child = spawn8(opts.ssh ?? "ssh", args, { env: opts.env ?? process.env, stdio: ["ignore", "pipe", "pipe"] });
119081
119334
  const timer = setTimeout(() => {
119082
119335
  child.kill("SIGKILL");
119083
119336
  finish2(none("ssh -G did not answer"));
@@ -121198,7 +121451,7 @@ function storeMirror(store2) {
121198
121451
  var start = new StartSequence();
121199
121452
  start.at("launch variables");
121200
121453
  var launch = takeLaunchEnv();
121201
- var BUILD = true ? "5dca7b8" : "dev";
121454
+ var BUILD = true ? "8dd78b6" : "dev";
121202
121455
  var { values } = parseArgs({
121203
121456
  options: {
121204
121457
  port: { type: "string", default: "5175" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@centralu/host-linux-arm64",
3
- "version": "0.1.0-beta.21",
3
+ "version": "0.1.0-beta.22",
4
4
  "description": "Centralu's host without the window, for Linux on arm64: what a hub installs on a linked machine",
5
5
  "license": "MIT",
6
6
  "author": "ijun17",