@centralu/darwin-arm64 0.1.0-beta.14 → 0.1.0-beta.15

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.
@@ -42886,7 +42886,7 @@ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync10, writeSync
42886
42886
 
42887
42887
  // packages/protocol/src/brand.ts
42888
42888
  var APP_NAME = "Centralu";
42889
- var APP_VERSION = "0.1.0-beta.14";
42889
+ var APP_VERSION = "0.1.0-beta.15";
42890
42890
  var APP_SLUG = "centralu";
42891
42891
  var CLIENT_INFO = { name: APP_SLUG, title: APP_NAME, version: APP_VERSION };
42892
42892
  var DATA_DIR = ".centralu";
@@ -42926,7 +42926,10 @@ function isNewerVersion(a, b2) {
42926
42926
  }
42927
42927
  return false;
42928
42928
  }
42929
- function split(v) {
42929
+ function split(raw) {
42930
+ let v = raw.trim().replace(/^v/i, "");
42931
+ const plus = v.indexOf("+");
42932
+ if (plus !== -1) v = v.slice(0, plus);
42930
42933
  const dash = v.indexOf("-");
42931
42934
  const core = (dash === -1 ? v : v.slice(0, dash)).split(".").map(Number);
42932
42935
  const pre2 = dash === -1 ? null : v.slice(dash + 1);
@@ -58153,6 +58156,11 @@ var MachineOperation = external_exports.object({
58153
58156
  target: external_exports.string().nullable().default(null),
58154
58157
  at: external_exports.number()
58155
58158
  });
58159
+ var MachineAutostart = external_exports.object({
58160
+ on: external_exports.boolean().catch(false),
58161
+ how: external_exports.enum(["systemd", "task"]).nullable().catch(null),
58162
+ linger: external_exports.boolean().nullable().catch(null).default(null)
58163
+ });
58156
58164
  var MachineInfo = external_exports.object({
58157
58165
  id: MachineId,
58158
58166
  /** What the person calls it; shown as the group header in the sidebar */
@@ -58197,7 +58205,13 @@ var MachineInfo = external_exports.object({
58197
58205
  * An install, update, rollback or uninstall the hub is running there, and its step, while it runs
58198
58206
  * (`machines.install` / `update` / `rollback` / `uninstall`). Null otherwise. Additive
58199
58207
  */
58200
- operation: MachineOperation.nullable().catch(null).default(null)
58208
+ operation: MachineOperation.nullable().catch(null).default(null),
58209
+ /**
58210
+ * Whether the machine starts its host at boot, from its connection line and the last
58211
+ * `machines.autostart` (`MachineAutostart`). Null when the machine has not answered, or predates it.
58212
+ * Additive
58213
+ */
58214
+ autostart: MachineAutostart.nullable().catch(null).default(null)
58201
58215
  });
58202
58216
  var MachineInstallResult = external_exports.object({
58203
58217
  machine: MachineInfo,
@@ -58207,6 +58221,7 @@ var MachineInstallResult = external_exports.object({
58207
58221
  left: external_exports.array(external_exports.string()).default([])
58208
58222
  });
58209
58223
  var MachineUninstallResult = external_exports.object({ machine: MachineInfo, stopped: external_exports.boolean() });
58224
+ var MachineAutostartResult = external_exports.object({ machine: MachineInfo, autostart: MachineAutostart });
58210
58225
  var HostActivity = external_exports.object({
58211
58226
  working: external_exports.number().int().default(0),
58212
58227
  approvals: external_exports.number().int().default(0),
@@ -60820,6 +60835,14 @@ var RpcMethods = {
60820
60835
  * data and an npm install of its own. The machine stays linked
60821
60836
  */
60822
60837
  "machines.uninstall": { params: external_exports.object({ machineId: MachineId }), result: MachineUninstallResult },
60838
+ /**
60839
+ * Starts a linked machine's host at boot, or stops doing that, or (without `on`) reads whether it
60840
+ * does (docs/plans/remote-hub.md §10.4, owner decision 4: per machine, off by default). The
60841
+ * Centralu there writes its own entry (`centralu serve --autostart`): a systemd user unit with
60842
+ * lingering on Linux, a scheduled task at sign-in on Windows and for WSL. A host running there is
60843
+ * neither started nor stopped. Refused by a machine that predates it or offers none (macOS)
60844
+ */
60845
+ "machines.autostart": { params: external_exports.object({ machineId: MachineId, on: external_exports.boolean().optional() }), result: MachineAutostartResult },
60823
60846
  /**
60824
60847
  * What would stop on a linked machine if its host stopped now (`host.activity`, asked of it), for the
60825
60848
  * "Update <machine>" prompt. Null when the machine cannot say (unreachable, other protocol, older)
@@ -62090,12 +62113,7 @@ var HostServer = class _HostServer {
62090
62113
  this.listenError = null;
62091
62114
  if (err.code === "EADDRINUSE") {
62092
62115
  reject(
62093
- new Error(
62094
- `Port ${this.opts.port} is already in use.
62095
- \xB7 If a host is already running, just use it (start only the UI)
62096
- \xB7 To clean up a leftover process: lsof -ti:${this.opts.port} | xargs kill
62097
- \xB7 To use another port: pnpm host --port ${this.opts.port + 1}`
62098
- )
62116
+ new Error(portInUseMessage(this.opts.port, this.opts.servedBy))
62099
62117
  );
62100
62118
  return;
62101
62119
  }
@@ -62330,6 +62348,17 @@ function errorCode(raw) {
62330
62348
  const known = ProtocolErrorCode.safeParse(raw);
62331
62349
  return known.success ? known.data : "internal";
62332
62350
  }
62351
+ function portInUseMessage(port2, servedBy) {
62352
+ if (servedBy === "serve") {
62353
+ return `Port ${port2} is already in use.
62354
+ \xB7 Another program listens on 127.0.0.1:${port2}; on Windows this is often WSL (wslrelay.exe) relaying a Centralu inside a distro
62355
+ \xB7 To use another port: centralu serve --port <n> (it is kept for the next start)`;
62356
+ }
62357
+ return `Port ${port2} is already in use.
62358
+ \xB7 If a host is already running, just use it (start only the UI)
62359
+ \xB7 To clean up a leftover process: lsof -ti:${port2} | xargs kill
62360
+ \xB7 To use another port: pnpm host --port ${port2 + 1}`;
62361
+ }
62333
62362
 
62334
62363
  // packages/agent-host/src/view-handover.ts
62335
62364
  var VIEW_HANDOVER_KEY = "views.handover";
@@ -98168,19 +98197,21 @@ ${summary}
98168
98197
  ` : "";
98169
98198
  const verbatimMark = "\u2500\u2500 verbatim from here \u2500\u2500\n\n";
98170
98199
  const fixed = bytes(instructions + touchedLine + summarySection + verbatimMark) + 200;
98171
- const size = () => rendered.reduce((n, e) => n + bytes(e.text) + 2, fixed);
98200
+ const sizes = rendered.map((e) => bytes(e.text) + 2);
98201
+ let size = sizes.reduce((n, s) => n + s, fixed);
98172
98202
  let dropped = 0;
98173
- while (size() > cap && rendered.length > 1) {
98174
- rendered.shift();
98203
+ while (size > cap && rendered.length - dropped > 1) {
98204
+ size -= sizes[dropped];
98175
98205
  dropped++;
98176
98206
  }
98177
- const from = rendered.length ? rendered[0].seq : lastSeq;
98207
+ const kept = rendered.slice(dropped);
98208
+ const from = kept.length ? kept[0].seq : lastSeq;
98178
98209
  const covers = `covers seq ${num(from)}\u2013${num(lastSeq)} of ${num(lastSeq)} \xB7 ${(cap / 1e6).toFixed(1)} MB cap` + (dropped > 0 || folded ? " \xB7 earlier material stays in the app's records" : "");
98179
98210
  const header = `# Handoff \xB7 ${opts.name} \xB7 ${opts.tool}${opts.toTool ? ` \u2192 ${opts.toTool}` : ""}
98180
98211
  ${covers}
98181
98212
  ${touchedLine}
98182
98213
  `;
98183
- return header + instructions + summarySection + verbatimMark + rendered.map((e) => e.text).join("\n\n") + "\n";
98214
+ return header + instructions + summarySection + verbatimMark + kept.map((e) => e.text).join("\n\n") + "\n";
98184
98215
  }
98185
98216
 
98186
98217
  // packages/agent-host/src/sessions/orchestrator-proposals.ts
@@ -104218,6 +104249,7 @@ var SessionManager = class _SessionManager {
104218
104249
  if (m.projectId) project2 = this.store.listProjects().find((p) => p.id === m.projectId);
104219
104250
  this.running.delete(sessionId);
104220
104251
  this.restartAfterTurn.delete(sessionId);
104252
+ this.prCheckedAt.delete(sessionId);
104221
104253
  this.meta.delete(sessionId);
104222
104254
  await this.store.trashSession(sessionId, {
104223
104255
  projectId: m.projectId,
@@ -104424,9 +104456,11 @@ var SessionManager = class _SessionManager {
104424
104456
  * location is no longer ours — it is never looked at.
104425
104457
  */
104426
104458
  async sweepOrphanHandoffNotes(projectId) {
104427
- const claimed = this.store.handoffPredecessors();
104428
- const trashed = this.store.trashedIds();
104429
- await sweepHandoffNotes((owner) => this.meta.has(owner) || claimed.has(owner) || trashed.has(owner), projectId);
104459
+ await sweepHandoffNotes(
104460
+ (owner) => this.meta.has(owner) || // A session in the trash still owns its note (#204) — restoring it must find it; only purging lets it go
104461
+ this.store.trashedIds().has(owner) || this.store.handoffPredecessors().has(owner),
104462
+ projectId
104463
+ );
104430
104464
  }
104431
104465
  /**
104432
104466
  * Changes model or permissions (FR-7). If the adapter supports it, it applies starting next turn;
@@ -106916,7 +106950,7 @@ ${String(stderr).slice(0, 2e3)}`
106916
106950
  let merged = await gitBranchMerged(cwd, m.worktree.branch, m.worktree.base, trunk).catch(() => false);
106917
106951
  if (!merged && this.ghAvailable) {
106918
106952
  const now = Date.now();
106919
- if (now - (this.prCheckedAt.get(m.id) ?? 0) >= this.prPollMs) {
106953
+ if (this.meta.get(m.id) === m && now - (this.prCheckedAt.get(m.id) ?? 0) >= this.prPollMs) {
106920
106954
  this.prCheckedAt.set(m.id, now);
106921
106955
  const pr = await this.prLookup(cwd, m.worktree.branch).catch(() => null);
106922
106956
  if (pr === "unavailable") {
@@ -106933,6 +106967,7 @@ ${String(stderr).slice(0, 2e3)}`
106933
106967
  }
106934
106968
  }
106935
106969
  if (!merged || this.meta.get(m.id) !== m) continue;
106970
+ this.prCheckedAt.delete(m.id);
106936
106971
  m.worktreeMerged = true;
106937
106972
  this.emit({ type: "worktree_merged", sessionId: m.id });
106938
106973
  console.error(`[worktree] branch merged into trunk: ${m.worktree.branch} (${m.id.slice(0, 8)})`);
@@ -108880,7 +108915,7 @@ var ClaudeSession = class {
108880
108915
  * from #92, which exists so files from the repo cannot decide approvals. (The SDK leaves a
108881
108916
  * `CLAUDE_SDK_CAN_USE_TOOL_SHADOWED` warning line for this combination.)
108882
108917
  */
108883
- canUseTool: async (toolName, toolInput) => {
108918
+ canUseTool: async (toolName, toolInput, options) => {
108884
108919
  if (isOrchestratorTool(toolName) && self2.orchestratorServer) {
108885
108920
  return { behavior: "allow", updatedInput: toolInput };
108886
108921
  }
@@ -108905,6 +108940,7 @@ var ClaudeSession = class {
108905
108940
  self2.emit({ type: "question_request", sessionId: self2.sessionId, requestId: requestId2, questions });
108906
108941
  return new Promise((resolve10) => {
108907
108942
  self2.questions.set(requestId2, { resolve: resolve10, input: toolInput });
108943
+ self2.releaseOnCancel(requestId2, options?.signal);
108908
108944
  });
108909
108945
  }
108910
108946
  const detail = approvalDetail(toolName, toolInput, self2.opts.cwd);
@@ -108914,6 +108950,7 @@ var ClaudeSession = class {
108914
108950
  self2.emit({ type: "approval_request", sessionId: self2.sessionId, requestId, detail });
108915
108951
  return new Promise((resolve10) => {
108916
108952
  self2.pending.set(requestId, { resolve: resolve10, input: toolInput });
108953
+ self2.releaseOnCancel(requestId, options?.signal);
108917
108954
  });
108918
108955
  }
108919
108956
  }
@@ -109377,6 +109414,32 @@ var ClaudeSession = class {
109377
109414
  releaseAgents(why) {
109378
109415
  for (const e of this.stream.release(why)) this.emit(e);
109379
109416
  }
109417
+ /**
109418
+ * Closes a card when the CLI withdraws its question. The SDK aborts the callback's `signal` when the CLI sends
109419
+ * `control_cancel_request` for that permission request (the tool was aborted, or the turn ended without our Stop)
109420
+ * and when the query closes. Nothing else tells us: without this the card stayed open forever, and answering it
109421
+ * resolved a promise nobody was waiting on. A card already answered or released is no longer in its map, so this
109422
+ * closes nothing twice.
109423
+ */
109424
+ releaseOnCancel(requestId, signal) {
109425
+ if (!signal) return;
109426
+ const release = () => {
109427
+ const approval = this.pending.get(requestId);
109428
+ if (approval) {
109429
+ this.pending.delete(requestId);
109430
+ approval.resolve({ behavior: "deny", message: "Cancelled by Claude Code" });
109431
+ this.emit({ type: "approval_resolved", sessionId: this.sessionId, requestId, decision: "deny" });
109432
+ }
109433
+ const question = this.questions.get(requestId);
109434
+ if (question) {
109435
+ this.questions.delete(requestId);
109436
+ question.resolve({ behavior: "deny", message: "Cancelled by Claude Code" });
109437
+ this.emit({ type: "question_resolved", sessionId: this.sessionId, requestId });
109438
+ }
109439
+ };
109440
+ if (signal.aborted) release();
109441
+ else signal.addEventListener("abort", release, { once: true });
109442
+ }
109380
109443
  /** Releases a choice that was still waiting on an answer — **never released silently**, for the same reason as approval. */
109381
109444
  releaseQuestions(why) {
109382
109445
  for (const [id2, open5] of this.questions) {
@@ -109684,10 +109747,19 @@ var CodexClient = class {
109684
109747
  const id2 = `${this.idPrefix}${this.nextId++}`;
109685
109748
  this.write({ id: id2, method, params });
109686
109749
  return new Promise((resolve10, reject) => {
109687
- this.pending.set(id2, { resolve: resolve10, reject });
109688
- setTimeout(() => {
109750
+ const timer = setTimeout(() => {
109689
109751
  if (this.pending.delete(id2)) reject(new Error(`No response for ${method}`));
109690
109752
  }, timeoutMs);
109753
+ this.pending.set(id2, {
109754
+ resolve: (v) => {
109755
+ clearTimeout(timer);
109756
+ resolve10(v);
109757
+ },
109758
+ reject: (e) => {
109759
+ clearTimeout(timer);
109760
+ reject(e);
109761
+ }
109762
+ });
109691
109763
  });
109692
109764
  }
109693
109765
  notify(method, params) {
@@ -109706,7 +109778,7 @@ var CodexClient = class {
109706
109778
  this.closed = true;
109707
109779
  this.proc.stdin.end();
109708
109780
  const exited = await new Promise((resolve10) => {
109709
- if (this.proc.exitCode !== null || this.finished) return resolve10(true);
109781
+ if (this.proc.exitCode !== null || this.proc.signalCode != null || this.finished) return resolve10(true);
109710
109782
  const t = setTimeout(() => resolve10(false), 2e3);
109711
109783
  this.proc.once("exit", () => {
109712
109784
  clearTimeout(t);
@@ -110366,6 +110438,8 @@ var CODEX_KNOWN_NOTIFICATIONS = /* @__PURE__ */ new Set([
110366
110438
  "item/reasoning/summaryPartAdded",
110367
110439
  "thread/compacted",
110368
110440
  "account/rateLimits/updated",
110441
+ // read in index.ts: closes the approval card of a request Codex ended without our answer
110442
+ "serverRequest/resolved",
110369
110443
  // mapped by #290 for a child thread (CodexChildTracker); the parent's own status is still not read
110370
110444
  "thread/status/changed",
110371
110445
  // mapped by #304: notices, model switches and MCP servers that failed to start (thread/settings/updated in index.ts)
@@ -110383,7 +110457,6 @@ var CODEX_KNOWN_NOTIFICATIONS = /* @__PURE__ */ new Set([
110383
110457
  "rawResponseItem/completed",
110384
110458
  "rawResponse/completed",
110385
110459
  // correctly ignored
110386
- "serverRequest/resolved",
110387
110460
  "thread/started",
110388
110461
  "thread/archived",
110389
110462
  "thread/unarchived",
@@ -111027,7 +111100,18 @@ var CodexSession = class {
111027
111100
  * if stop is pressed very quickly, the response can arrive before the notification.
111028
111101
  */
111029
111102
  turnId = null;
111030
- /** Our requestId to the Codex server's request id */
111103
+ /**
111104
+ * Turns already reported ended (`turn/completed`/`turn/failed`), the latest few. The `turn/start` response can
111105
+ * arrive after its own turn's `turn/completed`; taking the id from that late response would bring a finished
111106
+ * turn back as the target of Stop, and the next Stop would send `turn/interrupt` for a dead turn and say
111107
+ * "Could not stop". Bounded: only a response still in flight can name one of these.
111108
+ */
111109
+ endedTurns = [];
111110
+ /**
111111
+ * Our requestId to the Codex server's request, with the thread and turn it was asked in. Codex ends a request
111112
+ * itself when its turn ends or is interrupted (and says so with `serverRequest/resolved`); the card has to close
111113
+ * with it, or an answer goes to a request id nobody is waiting on.
111114
+ */
111031
111115
  approvals = /* @__PURE__ */ new Map();
111032
111116
  /**
111033
111117
  * Cards raised for app tool approval (M4 A-5) — the answer has a different shape (elicitation's
@@ -111188,7 +111272,7 @@ var CodexSession = class {
111188
111272
  }
111189
111273
  this.threadId = threadIdOf(res) ?? this.opts.resumeExternalId;
111190
111274
  this.threadSettings = threadSettingsOf(res);
111191
- if (adopted) this.turnId ??= runningTurnOf(res) ?? await this.latestRunningTurn(res);
111275
+ if (adopted) this.takeStartedTurn(runningTurnOf(res) ?? await this.latestRunningTurn(res));
111192
111276
  void this.client.request("thread/goal/get", { threadId: this.threadId }).then((r) => {
111193
111277
  const g5 = r.goal ? goalFromCodex(r.goal) : null;
111194
111278
  if (g5) this.emit({ type: "goal", sessionId: this.sessionId, goal: g5 });
@@ -111310,6 +111394,7 @@ var CodexSession = class {
111310
111394
  }
111311
111395
  onNotification(n) {
111312
111396
  this.noteFileChange(n);
111397
+ this.noteRequestEnded(n);
111313
111398
  this.unmapped.note(n.method);
111314
111399
  const from = n.params?.threadId;
111315
111400
  if (typeof from === "string" && this.threadId !== null && from !== this.threadId) {
@@ -111319,7 +111404,11 @@ var CodexSession = class {
111319
111404
  return;
111320
111405
  }
111321
111406
  if (n.method === "turn/started") this.turnId = turnIdOf(n.params);
111322
- if (n.method === "turn/completed" || n.method === "turn/failed") this.turnId = null;
111407
+ if (n.method === "turn/completed" || n.method === "turn/failed") {
111408
+ this.turnId = null;
111409
+ const ended = turnIdOf(n.params);
111410
+ if (ended) this.endedTurns = [...this.endedTurns.slice(-(ENDED_TURNS_KEPT - 1)), ended];
111411
+ }
111323
111412
  if (this.retrying && n.method.startsWith("item/")) {
111324
111413
  this.retrying = false;
111325
111414
  this.emit({ type: "activity", sessionId: this.sessionId, activity: this.activityBefore });
@@ -111421,6 +111510,34 @@ var CodexSession = class {
111421
111510
  for (const early of held3) this.onChildNotification(thread, early);
111422
111511
  }
111423
111512
  }
111513
+ /**
111514
+ * Closes the cards of server requests Codex ended without our answer. `serverRequest/resolved` names one request
111515
+ * (generated `ServerRequestResolvedNotification`, codex-cli 0.160.0: `{ threadId, requestId }`); a turn that ends
111516
+ * takes every request asked in it, whether or not that notification comes. A request we answered is already out of
111517
+ * `approvals`, so its own `serverRequest/resolved` finds nothing and closes nothing twice.
111518
+ */
111519
+ noteRequestEnded(n) {
111520
+ const p = n.params ?? {};
111521
+ if (n.method === "serverRequest/resolved") {
111522
+ if (typeof p.requestId !== "string" && typeof p.requestId !== "number") return;
111523
+ const serverId = String(p.requestId);
111524
+ this.closeApprovals((a) => String(a.serverId) === serverId);
111525
+ return;
111526
+ }
111527
+ if (n.method !== "turn/completed" && n.method !== "turn/failed") return;
111528
+ const turn = turnIdOf(n.params);
111529
+ if (typeof p.threadId !== "string" || !turn) return;
111530
+ this.closeApprovals((a) => a.threadId === p.threadId && a.turnId === turn);
111531
+ }
111532
+ /** Closes the matching cards as denied, without answering Codex: the request is already gone on its side */
111533
+ closeApprovals(match) {
111534
+ for (const [requestId, a] of this.approvals) {
111535
+ if (!match(a)) continue;
111536
+ this.approvals.delete(requestId);
111537
+ this.elicitations.delete(requestId);
111538
+ this.emit({ type: "approval_resolved", sessionId: this.sessionId, requestId, decision: "deny" });
111539
+ }
111540
+ }
111424
111541
  /** Keeps a file-change item's changes until it completes — see `fileChanges` */
111425
111542
  noteFileChange(n) {
111426
111543
  const p = n.params ?? {};
@@ -111460,7 +111577,7 @@ var CodexSession = class {
111460
111577
  const p = typeof r.params === "object" && r.params !== null ? r.params : {};
111461
111578
  if (typeof p.serverName === "string" && this.appServers.has(p.serverName) && approvalKindOf(p._meta) === "mcp_tool_call") {
111462
111579
  const requestId2 = `codex-req-${++this.reqCounter}`;
111463
- this.approvals.set(requestId2, r.id);
111580
+ this.approvals.set(requestId2, requestOrigin(r));
111464
111581
  this.elicitations.add(requestId2);
111465
111582
  this.emit({ type: "approval_request", sessionId: this.sessionId, requestId: requestId2, detail: appApprovalDetail(p.serverName, p.message, p._meta) });
111466
111583
  return;
@@ -111488,7 +111605,7 @@ var CodexSession = class {
111488
111605
  return;
111489
111606
  }
111490
111607
  const requestId = `codex-req-${++this.reqCounter}`;
111491
- this.approvals.set(requestId, r.id);
111608
+ this.approvals.set(requestId, requestOrigin(r));
111492
111609
  this.emit({ type: "approval_request", sessionId: this.sessionId, requestId, detail });
111493
111610
  }
111494
111611
  applyRules(matchers) {
@@ -111563,9 +111680,7 @@ var CodexSession = class {
111563
111680
  */
111564
111681
  ...this.opts.effort ? { effort: this.opts.effort } : {},
111565
111682
  ...this.outputSchemaParam()
111566
- }).then((res) => {
111567
- this.turnId ??= turnIdOf(res);
111568
- });
111683
+ }).then((res) => this.takeStartedTurn(turnIdOf(res)));
111569
111684
  }).catch((e) => {
111570
111685
  if (this.startFailed) return;
111571
111686
  this.emit({
@@ -111585,9 +111700,7 @@ var CodexSession = class {
111585
111700
  input,
111586
111701
  ...this.opts.effort ? { effort: this.opts.effort } : {},
111587
111702
  ...this.outputSchemaParam()
111588
- }).then((res) => {
111589
- this.turnId ??= turnIdOf(res);
111590
- }).catch((e) => {
111703
+ }).then((res) => this.takeStartedTurn(turnIdOf(res))).catch((e) => {
111591
111704
  this.emit({
111592
111705
  type: "error",
111593
111706
  sessionId: this.sessionId,
@@ -111595,6 +111708,14 @@ var CodexSession = class {
111595
111708
  });
111596
111709
  });
111597
111710
  }
111711
+ /**
111712
+ * Takes a running turn's id from a response, unless the notifications already named one or already ended it. The
111713
+ * response and the notifications race: `turn/started` can come after the response (Stop pressed very quickly), and
111714
+ * `turn/completed` can come before it (a short turn), so a late response must not bring an ended turn back.
111715
+ */
111716
+ takeStartedTurn(id2) {
111717
+ if (id2 && !this.endedTurns.includes(id2)) this.turnId ??= id2;
111718
+ }
111598
111719
  /**
111599
111720
  * A turn belonging to an agent that an app gave a schema and asked for (M4 D-1). Codex receives
111600
111721
  * the schema **per turn** — the installed 0.153.4's generated type is
@@ -111608,9 +111729,10 @@ var CodexSession = class {
111608
111729
  return this.opts.outputSchema ? { outputSchema: this.opts.outputSchema } : {};
111609
111730
  }
111610
111731
  respondApproval(requestId, decision, _scope, matcher) {
111611
- const serverId = this.approvals.get(requestId);
111612
- if (serverId === void 0) return false;
111732
+ const open5 = this.approvals.get(requestId);
111733
+ if (open5 === void 0) return false;
111613
111734
  this.approvals.delete(requestId);
111735
+ const serverId = open5.serverId;
111614
111736
  if (this.elicitations.delete(requestId)) {
111615
111737
  this.client.respond(serverId, {
111616
111738
  action: decision === "deny" ? "decline" : "accept",
@@ -111659,6 +111781,10 @@ var CodexSession = class {
111659
111781
  this.emit({ type: "state_change", sessionId: this.sessionId, state: "waiting_input", reason: "interrupted" });
111660
111782
  return;
111661
111783
  }
111784
+ for (const [requestId, a] of this.approvals) {
111785
+ if (a.threadId !== null && a.threadId !== this.threadId) continue;
111786
+ this.respondApproval(requestId, "deny");
111787
+ }
111662
111788
  if (!this.threadId || !this.turnId) return;
111663
111789
  void this.client.request("turn/interrupt", { threadId: this.threadId, turnId: this.turnId }).catch((err) => {
111664
111790
  this.emit({
@@ -111801,6 +111927,15 @@ function runningTurnOf(res) {
111801
111927
  }
111802
111928
  return null;
111803
111929
  }
111930
+ var ENDED_TURNS_KEPT = 8;
111931
+ function requestOrigin(r) {
111932
+ const p = typeof r.params === "object" && r.params !== null ? r.params : {};
111933
+ return {
111934
+ serverId: r.id,
111935
+ threadId: typeof p.threadId === "string" ? p.threadId : null,
111936
+ turnId: typeof p.turnId === "string" ? p.turnId : null
111937
+ };
111938
+ }
111804
111939
  function turnIdOf(payload) {
111805
111940
  const turn = payload?.turn;
111806
111941
  return typeof turn?.id === "string" ? turn.id : null;
@@ -112916,6 +113051,10 @@ function createRpcHandler(mgr2, adapters2, { terminals: terminals2, updates: upd
112916
113051
  "machines.update": async (p) => requireMachines().update(RpcMethods["machines.update"].params.parse(p).machineId),
112917
113052
  "machines.rollback": async (p) => requireMachines().rollback(RpcMethods["machines.rollback"].params.parse(p).machineId),
112918
113053
  "machines.uninstall": async (p) => requireMachines().uninstall(RpcMethods["machines.uninstall"].params.parse(p).machineId),
113054
+ "machines.autostart": async (p) => {
113055
+ const { machineId, on: on2 } = RpcMethods["machines.autostart"].params.parse(p);
113056
+ return requireMachines().autostart(machineId, on2);
113057
+ },
112919
113058
  "machines.activity": async (p) => ({ activity: await requireMachines().activity(RpcMethods["machines.activity"].params.parse(p).machineId) }),
112920
113059
  "host.activity": async () => {
112921
113060
  if (!activity2) throw Object.assign(new Error("This host does not report its activity"), { code: "internal" });
@@ -114210,8 +114349,16 @@ import { createPublicKey, verify } from "node:crypto";
114210
114349
  var NPM_REGISTRY = "https://registry.npmjs.org";
114211
114350
  var PACKAGE_NAME = /^(@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
114212
114351
  var VERSION = /^[0-9A-Za-z][0-9A-Za-z.+-]{0,63}$/;
114213
- async function getJson(url2, f) {
114352
+ var NotPublished = class extends Error {
114353
+ constructor(id2) {
114354
+ super(`${id2} is not on the npm registry`);
114355
+ this.id = id2;
114356
+ }
114357
+ id;
114358
+ };
114359
+ async function getJson(url2, f, id2) {
114214
114360
  const res = await f(url2, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(3e4) });
114361
+ if (res.status === 404 && id2) throw new NotPublished(id2);
114215
114362
  if (!res.ok) throw new Error(`The npm registry answered ${res.status} for ${url2}`);
114216
114363
  return res.json();
114217
114364
  }
@@ -114220,7 +114367,7 @@ async function verifiedPackage(name, version2, opts = {}) {
114220
114367
  const registry2 = (opts.registry ?? NPM_REGISTRY).replace(/\/+$/, "");
114221
114368
  const f = opts.fetch ?? globalThis.fetch;
114222
114369
  const now = opts.now?.() ?? Date.now();
114223
- const [meta3, keys] = await Promise.all([getJson(`${registry2}/${name}/${version2}`, f), getJson(`${registry2}/-/npm/v1/keys`, f)]);
114370
+ const [meta3, keys] = await Promise.all([getJson(`${registry2}/${name}/${version2}`, f, `${name}@${version2}`), getJson(`${registry2}/-/npm/v1/keys`, f)]);
114224
114371
  const m = meta3;
114225
114372
  if (m?.name !== name || m.version !== version2) throw new Error(`The npm registry answered for another package than ${name}@${version2}`);
114226
114373
  const tarball = m.dist?.tarball;
@@ -114271,6 +114418,7 @@ function parseConnectionLine(stdout) {
114271
114418
  throw new Error("The remote answered with an incomplete connection line");
114272
114419
  }
114273
114420
  const install = parseInstall(o.install);
114421
+ const autostart = parseAutostart(o.autostart);
114274
114422
  return {
114275
114423
  v: 1,
114276
114424
  port: o.port,
@@ -114279,7 +114427,8 @@ function parseConnectionLine(stdout) {
114279
114427
  protocolVersion: o.protocolVersion,
114280
114428
  dataDir: typeof o.dataDir === "string" ? o.dataDir : "",
114281
114429
  hostRunning: o.hostRunning === true,
114282
- ...install ? { install } : {}
114430
+ ...install ? { install } : {},
114431
+ ...autostart ? { autostart } : {}
114283
114432
  };
114284
114433
  }
114285
114434
  }
@@ -114294,6 +114443,13 @@ function parseInstall(raw) {
114294
114443
  };
114295
114444
  return { managed: o.managed === true, current: pointer(o.current), previous: pointer(o.previous), node: typeof o.node === "string" ? o.node : null };
114296
114445
  }
114446
+ function parseAutostart(raw) {
114447
+ if (!raw || typeof raw !== "object") return null;
114448
+ const o = raw;
114449
+ if (typeof o.on !== "boolean") return null;
114450
+ const how = o.how === "systemd" || o.how === "task" ? o.how : null;
114451
+ return { on: o.on && how !== null, how, linger: typeof o.linger === "boolean" ? o.linger : null };
114452
+ }
114297
114453
  function parseDetachLine(stdout) {
114298
114454
  const lines = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
114299
114455
  for (let i = lines.length - 1; i >= 0; i--) {
@@ -114316,6 +114472,15 @@ function powershell(script) {
114316
114472
  ${script}`;
114317
114473
  return `powershell -NoProfile -NonInteractive -EncodedCommand ${Buffer.from(full, "utf16le").toString("base64")}`;
114318
114474
  }
114475
+ var WINDOWS_INLINE_MAX = 7900;
114476
+ var PS_FROM_STDIN = "$s=[Console]::In.ReadToEnd(); $c=[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String(($s -replace '[^A-Za-z0-9+/=]',''))); & ([ScriptBlock]::Create($c))";
114477
+ function powershellScript(script) {
114478
+ const inline = powershell(script);
114479
+ if (inline.length <= WINDOWS_INLINE_MAX) return inline;
114480
+ const full = `[Console]::OutputEncoding=[Text.Encoding]::UTF8
114481
+ ${script}`;
114482
+ return { command: powershell(PS_FROM_STDIN), stdin: Buffer.from(full, "utf8").toString("base64") };
114483
+ }
114319
114484
  function wslKeepAlive(spec) {
114320
114485
  if (spec?.shell !== "wsl") return null;
114321
114486
  const distro = spec.wslDistro ?? "";
@@ -114337,6 +114502,9 @@ function stopCommand(spec) {
114337
114502
  function serveCommand(spec) {
114338
114503
  return remoteCommand(spec, "serve");
114339
114504
  }
114505
+ function autostartCommand(spec, action) {
114506
+ return remoteCommand(spec, `serve --autostart ${action}`);
114507
+ }
114340
114508
  function remoteCommand(spec, args) {
114341
114509
  const custom2 = spec.command?.trim() || null;
114342
114510
  if (spec.shell === "powershell") {
@@ -114354,12 +114522,14 @@ function remoteCommand(spec, args) {
114354
114522
  return powershell(`wsl.exe -d '${distro}' -- bash -lc 'echo ${b64} | base64 -d | bash -l'`);
114355
114523
  }
114356
114524
  function remoteScript(spec, script) {
114357
- if (spec.shell === "powershell") return powershell(script.ps);
114525
+ if (spec.shell === "powershell") return powershellScript(script.ps);
114358
114526
  const b64 = Buffer.from(script.sh, "utf8").toString("base64");
114359
114527
  if (spec.shell === "posix") return `printf %s ${b64} | base64 -d | sh`;
114360
114528
  const distro = spec.wslDistro ?? "";
114361
114529
  if (!WSL_DISTRO_RE.test(distro)) throw new Error(`Not a WSL distro name: ${distro || "(none)"}`);
114362
- return powershell(`wsl.exe -d '${distro}' -- bash -lc 'echo ${b64} | base64 -d | bash -l'`);
114530
+ const inline = powershell(`wsl.exe -d '${distro}' -- bash -lc 'echo ${b64} | base64 -d | bash -l'`);
114531
+ if (inline.length <= WINDOWS_INLINE_MAX) return inline;
114532
+ return { command: powershell(`$s=[Console]::In.ReadToEnd(); $s | wsl.exe -d '${distro}' -- bash -lc 'base64 -di | bash -l'`), stdin: b64 };
114363
114533
  }
114364
114534
  var SshTunnel = class {
114365
114535
  constructor(opts) {
@@ -114372,11 +114542,18 @@ var SshTunnel = class {
114372
114542
  asking = /* @__PURE__ */ new Set();
114373
114543
  downListeners = [];
114374
114544
  closing = false;
114545
+ /**
114546
+ * Forwards this tunnel ended on purpose (a reopen, a forward that did not come up in time): their
114547
+ * exit is not the link going down. Reported as down, a reopen of a live link made the link retry,
114548
+ * and that retry's reopen ended the new forward again, every few seconds without end
114549
+ */
114550
+ ended = /* @__PURE__ */ new WeakSet();
114375
114551
  /** The remote cannot start a host outside a session (WMI blocked): the forward runs it (`startHost`) */
114376
114552
  linkBound = false;
114377
114553
  async open() {
114378
- await this.stopChild();
114379
114554
  this.closing = false;
114555
+ await this.stopChild();
114556
+ this.ifClosed();
114380
114557
  const line = await this.connectionLine().catch((err) => {
114381
114558
  this.ifClosed();
114382
114559
  throw err;
@@ -114402,6 +114579,16 @@ var SshTunnel = class {
114402
114579
  const served = hosting ? await this.untilServing() : line;
114403
114580
  return { url: `ws://127.0.0.1:${localPort}`, token: served.token, line: served, localPort };
114404
114581
  }
114582
+ /**
114583
+ * Where Centralu runs, for a sentence: "on <target>", or "in WSL (<distro>) on <target>" (#482).
114584
+ * A Windows laptop can be linked twice, as Windows and as a distro, and both rows used to say
114585
+ * "on <target>" alike.
114586
+ */
114587
+ get where() {
114588
+ const remote = this.opts.remote;
114589
+ if (remote?.shell !== "wsl") return `on ${this.opts.target}`;
114590
+ return `in WSL (${remote.wslDistro || "its default distro"}) on ${this.opts.target}`;
114591
+ }
114405
114592
  /**
114406
114593
  * Starts the remote's host with `centralu serve --detach` (plan §10.4, owner decision 7): it then
114407
114594
  * runs outside any ssh session and outlives this link. When the remote says WMI process creation
@@ -114415,7 +114602,7 @@ var SshTunnel = class {
114415
114602
  this.ifClosed();
114416
114603
  if (r.code === 255) throw new Error(`ssh could not reach ${this.opts.target}: ${lastLine(r.stderr) || "no answer"}`);
114417
114604
  if (r.stdout.includes(NOT_FOUND2) || r.code === 127 && !remote.command) {
114418
- throw new Error(`Centralu is not installed on ${this.opts.target} (run \`npm i -g centralu\` there, then \`centralu serve\` once)`);
114605
+ throw new Error(`Centralu is not installed ${this.where} (run \`npm i -g centralu\` there, then \`centralu serve\` once)`);
114419
114606
  }
114420
114607
  let answer;
114421
114608
  try {
@@ -114444,7 +114631,8 @@ var SshTunnel = class {
114444
114631
  */
114445
114632
  async exec(command) {
114446
114633
  this.ifClosed();
114447
- const r = await this.run([...this.config(), "-T", "-o", "BatchMode=yes", "-o", "ConnectTimeout=15", "--", this.opts.target, command]);
114634
+ const [line, stdin] = typeof command === "string" ? [command, void 0] : [command.command, command.stdin];
114635
+ const r = await this.run([...this.config(), "-T", "-o", "BatchMode=yes", "-o", "ConnectTimeout=15", "--", this.opts.target, line], stdin);
114448
114636
  if (r.code === 255) throw new Error(`ssh could not reach ${this.opts.target}: ${lastLine(r.stderr) || "no answer"}`);
114449
114637
  return r;
114450
114638
  }
@@ -114474,16 +114662,24 @@ var SshTunnel = class {
114474
114662
  ifClosed() {
114475
114663
  if (this.closing) throw new Error(`The link to ${this.opts.target} was closed`);
114476
114664
  }
114477
- run(args) {
114665
+ run(args, stdin) {
114478
114666
  return new Promise((resolve10, reject) => {
114479
- const child = spawn5(this.opts.ssh ?? "ssh", args, { env: this.opts.env ?? process.env, stdio: ["ignore", "pipe", "pipe"] });
114667
+ const child = spawn5(this.opts.ssh ?? "ssh", args, { env: this.opts.env ?? process.env, stdio: [stdin === void 0 ? "ignore" : "pipe", "pipe", "pipe"] });
114668
+ if (stdin !== void 0) {
114669
+ child.stdin.on("error", () => {
114670
+ });
114671
+ child.stdin.end(stdin);
114672
+ }
114480
114673
  this.asking.add(child);
114481
114674
  child.once("exit", () => this.asking.delete(child));
114482
114675
  let stdout = "";
114483
114676
  let stderr = "";
114484
114677
  child.stdout.on("data", (d2) => stdout += String(d2));
114485
114678
  child.stderr.on("data", (d2) => stderr += String(d2));
114486
- child.once("error", reject);
114679
+ child.once("error", (e) => {
114680
+ this.asking.delete(child);
114681
+ reject(e);
114682
+ });
114487
114683
  child.once("close", (code2) => resolve10({ code: code2, stdout, stderr }));
114488
114684
  });
114489
114685
  }
@@ -114496,7 +114692,7 @@ var SshTunnel = class {
114496
114692
  const r = await this.run([...base2, connectionCommand(remote)]);
114497
114693
  if (r.code === 255) throw new Error(`ssh could not reach ${this.opts.target}: ${lastLine(r.stderr) || "no answer"}`);
114498
114694
  if (r.stdout.includes(NOT_FOUND2) || r.code === 127 && !remote.command) {
114499
- throw new Error(`Centralu is not installed on ${this.opts.target} (run \`npm i -g centralu\` there, then \`centralu serve\` once)`);
114695
+ throw new Error(`Centralu is not installed ${this.where} (run \`npm i -g centralu\` there, then \`centralu serve\` once)`);
114500
114696
  }
114501
114697
  try {
114502
114698
  return parseConnectionLine(r.stdout);
@@ -114543,7 +114739,7 @@ var SshTunnel = class {
114543
114739
  child.once("exit", (code2, signal) => {
114544
114740
  if (this.child === child) this.child = null;
114545
114741
  if (!settled) return fail4(`The ssh forward to ${this.opts.target} ended (${signal ?? code2}): ${lastLine(stderr) || "no reason given"}`);
114546
- if (!this.closing) for (const l of this.downListeners) l(`ssh exited (${signal ?? code2})${stderr ? `: ${lastLine(stderr)}` : ""}`);
114742
+ if (!this.closing && !this.ended.has(child)) for (const l of this.downListeners) l(`ssh exited (${signal ?? code2})${stderr ? `: ${lastLine(stderr)}` : ""}`);
114547
114743
  });
114548
114744
  const poll = async () => {
114549
114745
  while (!settled) {
@@ -114553,6 +114749,7 @@ var SshTunnel = class {
114553
114749
  return resolve10();
114554
114750
  }
114555
114751
  if (Date.now() > deadline) {
114752
+ this.ended.add(child);
114556
114753
  child.kill("SIGTERM");
114557
114754
  return fail4(`The ssh forward to ${this.opts.target} did not come up in time`);
114558
114755
  }
@@ -114566,6 +114763,7 @@ var SshTunnel = class {
114566
114763
  const child = this.child;
114567
114764
  this.child = null;
114568
114765
  if (!child || child.exitCode !== null || child.signalCode !== null) return;
114766
+ this.ended.add(child);
114569
114767
  await new Promise((resolve10) => {
114570
114768
  child.once("exit", () => resolve10());
114571
114769
  child.kill("SIGTERM");
@@ -114790,12 +114988,21 @@ function readStep(run, ok, target) {
114790
114988
  if (!rest.startsWith(ok)) throw new Error(`Installing on ${target} answered something unexpected: ${rest.slice(0, 120)}`);
114791
114989
  return rest.slice(ok.length).trim();
114792
114990
  }
114991
+ var hostOnlyPackage = (platform) => `@centralu/host-${platform}`;
114992
+ async function hostPackage(platform, version2, registry2) {
114993
+ try {
114994
+ return await verifiedPackage(hostOnlyPackage(platform), version2, registry2);
114995
+ } catch (err) {
114996
+ if (err instanceof NotPublished) return verifiedPackage(`@centralu/${platform}`, version2, registry2);
114997
+ throw err;
114998
+ }
114999
+ }
114793
115000
  async function installRemote(o) {
114794
115001
  o.onStep?.("preflight");
114795
115002
  const pf2 = preflight(parsePreflight((await o.exec(preflightCommand(o.spec))).stdout), o.runtime);
114796
115003
  if (!pf2.ok) throw new Error(`Cannot install on ${o.target}: ${pf2.reason}`);
114797
115004
  o.onStep?.("registry");
114798
- const packages = await Promise.all([verifiedPackage("centralu", o.version, o.registry), verifiedPackage(`@centralu/${pf2.platform}`, o.version, o.registry)]);
115005
+ const packages = await Promise.all([verifiedPackage("centralu", o.version, o.registry), hostPackage(pf2.platform, o.version, o.registry)]);
114799
115006
  const archive = o.runtime.node.archives[pf2.platform];
114800
115007
  const nodeVersion = o.runtime.node.version;
114801
115008
  o.onStep?.("node");
@@ -114821,6 +115028,52 @@ function remoteRuntime() {
114821
115028
  return JSON.parse(bundled("remote-runtime.json", "../../../../packaging/remote-runtime.json"));
114822
115029
  }
114823
115030
 
115031
+ // packages/agent-host/src/links/autostart.ts
115032
+ function parseAutostartLine(stdout) {
115033
+ for (const line of stdout.split(/\r?\n/).reverse()) {
115034
+ const t = line.trim();
115035
+ if (!t.startsWith("{")) continue;
115036
+ try {
115037
+ const o = JSON.parse(t);
115038
+ const state = o.v === 1 ? parseAutostart(o.autostart) : null;
115039
+ if (state && typeof o.autostart?.ok === "boolean") {
115040
+ return { ok: o.autostart.ok, state, message: typeof o.autostart.message === "string" ? o.autostart.message : null };
115041
+ }
115042
+ } catch {
115043
+ }
115044
+ }
115045
+ return null;
115046
+ }
115047
+ var FAILED = {
115048
+ on: "could not be set to start at boot",
115049
+ off: "could not stop starting at boot",
115050
+ status: "could not say whether it starts at boot"
115051
+ };
115052
+ async function autostartRemote(c, action) {
115053
+ const r = await c.exec(autostartCommand(c.spec, action));
115054
+ const unasked = (reason, message) => Object.assign(new Error(message), { reason });
115055
+ if (r.stdout.includes(NOT_FOUND2)) throw unasked("not_found", `Centralu is not installed on ${c.target}`);
115056
+ const answer = parseAutostartLine(r.stdout);
115057
+ if (!answer) {
115058
+ if (/unknown option for serve: --autostart/.test(r.stderr)) {
115059
+ throw unasked("too_old", `The Centralu on ${c.target} is too old to start at boot; update it first`);
115060
+ }
115061
+ const why = lastLine(r.stderr);
115062
+ throw new Error(`Centralu on ${c.target} gave no answer about starting at boot${why ? `: ${why}` : r.code ? ` (exit ${r.code})` : ""}`);
115063
+ }
115064
+ if (!answer.ok) throw new Error(`Centralu on ${c.target} ${FAILED[action]}: ${answer.message ?? "no reason given"}`);
115065
+ return answer.state;
115066
+ }
115067
+ async function removeAutostart(c) {
115068
+ try {
115069
+ await autostartRemote(c, "off");
115070
+ return true;
115071
+ } catch (err) {
115072
+ if (err.reason) return false;
115073
+ throw err;
115074
+ }
115075
+ }
115076
+
114824
115077
  // packages/agent-host/src/links/update.ts
114825
115078
  var CHECK_MS = 3e4;
114826
115079
  function parseStopLine(stdout) {
@@ -114940,6 +115193,8 @@ async function uninstallRemote(c) {
114940
115193
  await c.hold();
114941
115194
  c.step("stop");
114942
115195
  const { wasRunning } = await stopRemoteHost(c);
115196
+ c.step("autostart");
115197
+ await removeAutostart(c);
114943
115198
  c.step("remove");
114944
115199
  readStep(await c.exec(uninstallCommand(c.spec)), "uninstalled", c.target);
114945
115200
  return { stopped: wasRunning };
@@ -115417,6 +115672,9 @@ var linkError2 = (message) => Object.assign(new Error(message), { code: "interna
115417
115672
  function installOf(i) {
115418
115673
  return i ? { managed: i.managed, current: i.current, previous: i.previous } : null;
115419
115674
  }
115675
+ function autostartOf(a) {
115676
+ return a ? { on: a.on, how: a.how, linger: a.linger } : null;
115677
+ }
115420
115678
  function hiddenKind(s) {
115421
115679
  return isObj2(s) && (s.kind === "orchestrator" || s.kind === "coordinator");
115422
115680
  }
@@ -115463,6 +115721,8 @@ var LinkedMachine = class {
115463
115721
  helloWaiters = /* @__PURE__ */ new Set();
115464
115722
  /** What the hub installed there, from the last connection line (plan §10.5, S12) */
115465
115723
  installed = null;
115724
+ /** Whether it starts its host at boot, from the last connection line or `machines.autostart` (plan §10.4) */
115725
+ bootStart = null;
115466
115726
  get id() {
115467
115727
  return this.record.id;
115468
115728
  }
@@ -115488,7 +115748,8 @@ var LinkedMachine = class {
115488
115748
  sameLocalPort: this.endpoint ? this.endpoint.localPort === this.endpoint.line.port : false,
115489
115749
  hostStarted: this.hostStarted,
115490
115750
  install: this.installed,
115491
- operation: this.operation
115751
+ operation: this.operation,
115752
+ autostart: this.bootStart
115492
115753
  };
115493
115754
  }
115494
115755
  // ── RoutedMachine ───────────────────────────────────────────────────────────────────────
@@ -115584,10 +115845,38 @@ var LinkedMachine = class {
115584
115845
  return this.operate("uninstall", null, async (c) => {
115585
115846
  const r = await uninstallRemote(c);
115586
115847
  this.installed = { managed: false, current: null, previous: null };
115848
+ if (this.bootStart) this.bootStart = { ...this.bootStart, on: false, linger: null };
115587
115849
  this.deps.log?.(`[links] ${this.name}: removed the Centralu this computer installed; its data stays`);
115588
115850
  return { machine: this.info(), stopped: r.stopped };
115589
115851
  });
115590
115852
  }
115853
+ /**
115854
+ * `machines.autostart` (plan §10.4, owner decision 4): the machine's host started at boot, or not, or
115855
+ * (without `on`) whether it is. The Centralu there writes its own entry; nothing starts or stops now.
115856
+ * Not while an install, update, rollback or uninstall runs there: the entry names a launcher that one
115857
+ * may be removing
115858
+ */
115859
+ async autostart(on2) {
115860
+ const exec8 = this.tunnel.exec?.bind(this.tunnel);
115861
+ if (!exec8) throw linkError2(`This computer has no shell on ${this.name} to change how it starts`);
115862
+ if (this.busy) throw linkError2(`${this.name} is busy with ${this.operation?.kind ?? "another change"}; wait until it is done`);
115863
+ this.busy = true;
115864
+ try {
115865
+ const a = await autostartRemote({ exec: exec8, spec: this.record.remote, target: this.record.sshTarget }, on2 === void 0 ? "status" : on2 ? "on" : "off");
115866
+ const state = { on: a.on, how: a.how, linger: a.linger };
115867
+ this.bootStart = state;
115868
+ if (on2 !== void 0) {
115869
+ this.deps.log?.(`[links] ${this.name}: ${a.on ? `starts Centralu at boot (${a.how}${a.how === "systemd" && a.linger === false ? ", at login until lingering is enabled" : ""})` : "no longer starts Centralu at boot"}`);
115870
+ }
115871
+ this.deps.broadcast({ type: "machine_status", machine: this.info() });
115872
+ return { machine: this.info(), autostart: state };
115873
+ } catch (err) {
115874
+ this.deps.log?.(`[links] ${this.name}: autostart ${on2 === void 0 ? "status" : on2 ? "on" : "off"} failed: ${err.message}`);
115875
+ throw linkError2(err.message);
115876
+ } finally {
115877
+ this.busy = false;
115878
+ }
115879
+ }
115591
115880
  /**
115592
115881
  * What would stop there if its host stopped now (`host.activity` on the remote), for the "Update"
115593
115882
  * prompt. Through the link when connected; while the link is held at `versions_differ` on one
@@ -115643,8 +115932,9 @@ var LinkedMachine = class {
115643
115932
  this.operation = { kind, step: s, target, at: at2 };
115644
115933
  this.deps.broadcast({ type: "machine_status", machine: this.info() });
115645
115934
  };
115935
+ let result;
115646
115936
  try {
115647
- return await fn2(this.control(tunnelExec, installer.script(), step), installer);
115937
+ result = await fn2(this.control(tunnelExec, installer.script(), step), installer);
115648
115938
  } catch (err) {
115649
115939
  this.deps.log?.(`[links] ${this.name}: ${kind} failed: ${err.message}`);
115650
115940
  throw linkError2(err.message);
@@ -115656,6 +115946,7 @@ var LinkedMachine = class {
115656
115946
  if (!this.stopped && (wasHeld || this.status !== "connected")) this.reconnect();
115657
115947
  this.deps.broadcast({ type: "machine_status", machine: this.info() });
115658
115948
  }
115949
+ return result && typeof result === "object" && "machine" in result ? { ...result, machine: this.info() } : result;
115659
115950
  }
115660
115951
  /** What update.ts runs things there with */
115661
115952
  control(exec8, script, step) {
@@ -115719,6 +116010,7 @@ var LinkedMachine = class {
115719
116010
  let endpoint = await this.openTunnel();
115720
116011
  if (!endpoint) return;
115721
116012
  this.installed = installOf(endpoint.line.install);
116013
+ this.bootStart = autostartOf(endpoint.line.autostart);
115722
116014
  if (!endpoint.line.hostRunning && this.tunnel.startHost) {
115723
116015
  if (this.held) return;
115724
116016
  this.setStatus("starting", null);
@@ -115740,6 +116032,7 @@ var LinkedMachine = class {
115740
116032
  endpoint = await this.openTunnel();
115741
116033
  if (!endpoint) return;
115742
116034
  this.installed = installOf(endpoint.line.install);
116035
+ this.bootStart = autostartOf(endpoint.line.autostart);
115743
116036
  }
115744
116037
  if (this.held) return;
115745
116038
  this.endpoint = endpoint;
@@ -115747,6 +116040,7 @@ var LinkedMachine = class {
115747
116040
  this.setStatus("not_running", `Centralu is installed on ${this.name}, but no \`centralu serve\` is running there`);
115748
116041
  return this.retryLater();
115749
116042
  }
116043
+ for (const w of [...this.helloWaiters]) w(endpoint.line.version);
115750
116044
  const versions = this.check({ version: endpoint.line.version, protocolVersion: endpoint.line.protocolVersion, dev: false });
115751
116045
  if (!mayConnect(versions)) return;
115752
116046
  this.connectClient(endpoint);
@@ -115948,6 +116242,9 @@ var Links = class {
115948
116242
  uninstall(id2) {
115949
116243
  return this.need(id2).uninstall();
115950
116244
  }
116245
+ autostart(id2, on2) {
116246
+ return this.need(id2).autostart(on2);
116247
+ }
115951
116248
  activity(id2) {
115952
116249
  return this.need(id2).activity();
115953
116250
  }
@@ -116176,6 +116473,7 @@ var ROUTES = {
116176
116473
  "machines.update": HUB,
116177
116474
  "machines.rollback": HUB,
116178
116475
  "machines.uninstall": HUB,
116476
+ "machines.autostart": HUB,
116179
116477
  "machines.activity": HUB,
116180
116478
  // The hub's own answer; a hub asks a remote's through `machines.activity`
116181
116479
  "host.activity": HUB,
@@ -116313,7 +116611,11 @@ var Router = class {
116313
116611
  if (kind === "projects") return list.map((p) => mark(m.q.project(p)));
116314
116612
  return list.filter((s) => !m.hides(s)).map((s) => mark(m.q.session(s)));
116315
116613
  }
116316
- /** The person's project order, split per machine: each machine keeps the order of its own */
116614
+ /**
116615
+ * The person's project order, split per machine: each machine keeps the order of its own. A
116616
+ * machine that cannot take it does not fail the rest, which has already been saved: its projects
116617
+ * come back from the mirror as they were, marked `unreachable`, the way `projects.list` gives them
116618
+ */
116317
116619
  async reorderProjects(machines, params) {
116318
116620
  const ids = isObj3(params) && Array.isArray(params.orderedIds) ? params.orderedIds : [];
116319
116621
  const perMachine = /* @__PURE__ */ new Map();
@@ -116325,7 +116627,13 @@ var Router = class {
116325
116627
  }
116326
116628
  const [mine, ...theirs] = await Promise.all([
116327
116629
  this.opts.local("projects.reorder", { ...isObj3(params) ? params : {}, orderedIds: local }),
116328
- ...[...perMachine].map(async ([m, orderedIds]) => m.q.projects(await this.forward(m, "projects.reorder", { orderedIds })))
116630
+ ...[...perMachine].map(async ([m, orderedIds]) => {
116631
+ try {
116632
+ return m.q.projects(await this.forward(m, "projects.reorder", { orderedIds }));
116633
+ } catch {
116634
+ return this.present(m, "projects", m.lastKnown("projects") ?? [], true);
116635
+ }
116636
+ })
116329
116637
  ]);
116330
116638
  return [...mine, ...theirs.flat()];
116331
116639
  }
@@ -116419,7 +116727,7 @@ function storeMirror(store2) {
116419
116727
  }
116420
116728
 
116421
116729
  // packages/agent-host/src/main.ts
116422
- var BUILD = true ? "1a563a8" : "dev";
116730
+ var BUILD = true ? "34635b7" : "dev";
116423
116731
  var underKeeper = process.env.CC_KEEPER === "1";
116424
116732
  var keeperSource = process.env.CC_HOST_SOURCE;
116425
116733
  var frontDoor = process.env.CC_FRONT_DOOR;
@@ -116682,6 +116990,7 @@ var router = new Router({
116682
116990
  });
116683
116991
  var server = new HostServer({
116684
116992
  port: Number(values.port),
116993
+ ...startedByServe ? { servedBy: "serve" } : {},
116685
116994
  token,
116686
116995
  allowedOrigins,
116687
116996
  onRpc: router.handle,