@centralu/host-linux-arm64 0.1.0-beta.43 → 0.1.0-beta.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/host/main.mjs CHANGED
@@ -43054,7 +43054,7 @@ import { dirname as dirname26, join as join58 } from "node:path";
43054
43054
  import { mkdirSync as mkdirSync20, writeSync as writeSync3 } from "node:fs";
43055
43055
 
43056
43056
  // packages/protocol/src/version.generated.ts
43057
- var APP_VERSION = "0.1.0-beta.43";
43057
+ var APP_VERSION = "0.1.0-beta.44";
43058
43058
 
43059
43059
  // packages/protocol/src/brand.ts
43060
43060
  var APP_NAME = "Centralu";
@@ -59081,7 +59081,31 @@ var UsageSnapshot = external_exports.object({
59081
59081
  plan: external_exports.string().nullable().default(null),
59082
59082
  windows: external_exports.array(UsageWindow).default([]),
59083
59083
  /** Daily tokens. A tool that cannot report this leaves it empty — the UI collapses that row on its own */
59084
- daily: external_exports.array(external_exports.object({ date: external_exports.string(), tokens: external_exports.number() })).default([])
59084
+ daily: external_exports.array(external_exports.object({ date: external_exports.string(), tokens: external_exports.number() })).default([]),
59085
+ /**
59086
+ * What the tool counted locally, as a total and not a limit (#1623): a tool with no limits to read (`agents.usage`
59087
+ * answers it `notReported`) may still say how much was used on this computer. Absent when it says nothing, and from a
59088
+ * host that predates it.
59089
+ */
59090
+ totals: external_exports.object({
59091
+ /** What the totals cover, in the words the person reads under their title ("all time") */
59092
+ scope: external_exports.string(),
59093
+ /**
59094
+ * When the host read them, in ms since the epoch. A large database is read slowly, so the host keeps the last totals and
59095
+ * the screen says how old they are. Absent from a host that predates it
59096
+ */
59097
+ asOf: external_exports.number().optional(),
59098
+ rows: external_exports.array(
59099
+ external_exports.object({
59100
+ id: external_exports.string(),
59101
+ label: external_exports.string(),
59102
+ /** Null: the tool printed it in a form the host could not read. The screen shows it as unread, never as a number */
59103
+ value: external_exports.number().nullable(),
59104
+ /** How the screen writes the value: a count, a number of tokens, or dollars */
59105
+ unit: external_exports.enum(["count", "tokens", "usd"])
59106
+ })
59107
+ )
59108
+ }).optional()
59085
59109
  });
59086
59110
 
59087
59111
  // packages/protocol/src/entities/errors.ts
@@ -59865,6 +59889,8 @@ var NormalizedEvent = external_exports.discriminatedUnion("type", [
59865
59889
  data: external_exports.string(),
59866
59890
  /** If the image exists on disk, its source path (Codex imageView) */
59867
59891
  path: external_exports.string().optional(),
59892
+ /** The file the agent viewed, as its path: `path` of a stored row is the host's own copy of it (#1464) */
59893
+ name: external_exports.string().optional(),
59868
59894
  note: external_exports.string().optional()
59869
59895
  }),
59870
59896
  /**
@@ -61685,8 +61711,14 @@ var hostParams = {
61685
61711
  * command can start), so a turn cannot slip in between the check and the stop. Otherwise `{ ok: false, busy, calls }`
61686
61712
  * and nothing changes. A host older than `ifIdle` ignores it and stops; the hand-over sends it only to a host that says
61687
61713
  * its logon, which came with it
61714
+ *
61715
+ * `forUpdate` (#1618, additive): a linked hub stops this host to update it, roll it back or put a version back, and starts
61716
+ * one again in this folder. As it lets its sessions go, this host writes those in a turn to a list of its own in the
61717
+ * folder, each with the last row of its conversation (docs/agent-host.md §4.8), so the host that starts next continues
61718
+ * each of those turns once, while its conversation still ends there. A host
61719
+ * older than it ignores it and stops, and its turns end as a stop's always did
61688
61720
  */
61689
- "host.stop": external_exports.object({ ifIdle: external_exports.boolean().optional() }),
61721
+ "host.stop": external_exports.object({ ifIdle: external_exports.boolean().optional(), forUpdate: external_exports.boolean().optional() }),
61690
61722
  /**
61691
61723
  * What a `serve` in the person's Windows sign-in says about taking over from this host (#1224): this host keeps it,
61692
61724
  * says it in its hellos and sends `host_handover`, so a hub's row can say it. `handover: null` clears it. Additive
@@ -62771,6 +62803,12 @@ var sessionsParams = {
62771
62803
  * reading of limits nobody will look at. Absent: read them.
62772
62804
  */
62773
62805
  limits: external_exports.boolean().optional(),
62806
+ /**
62807
+ * `true`: when a read of the tool's own command is in flight (an answer marked `provisional`), wait for it instead of
62808
+ * answering at once (#1623). The screen asks once more with this when the first answer was provisional, so it
62809
+ * receives the result when the command finishes and does not poll. Absent: answer at once.
62810
+ */
62811
+ settle: external_exports.boolean().optional(),
62774
62812
  ...machineParam
62775
62813
  }),
62776
62814
  /**
@@ -62980,6 +63018,20 @@ var sessionsResults = {
62980
63018
  supported: external_exports.boolean(),
62981
63019
  reason: external_exports.string().optional(),
62982
63020
  usage: UsageSnapshot.nullable(),
63021
+ /**
63022
+ * With `supported: false`: the tool has no way to report limits at all (its adapter has none), as against a read
63023
+ * that failed, which also answers `supported: false` (not logged in, a timeout). The screen draws only this one as
63024
+ * a plain dimmed ring (#1616); a host that predates it sends neither, and its failed and never answers look alike,
63025
+ * so both stay the unknown they were. Absent otherwise.
63026
+ */
63027
+ notReported: external_exports.boolean().optional(),
63028
+ /**
63029
+ * The answer stands in for one the host is still reading (#1623): a tool with no limits whose own command was
63030
+ * slower than the host waits (`usage` holds nothing yet), or whose last answer is old and is being read again (`usage`
63031
+ * is that answer, with its `totals.asOf`). Asking again with `settle` returns what the command read. Absent
63032
+ * otherwise, and from a host that predates it.
63033
+ */
63034
+ provisional: external_exports.boolean().optional(),
62983
63035
  /**
62984
63036
  * Set on the hub's answer for a linked machine when `usage` is this computer's own because both are logged in to
62985
63037
  * the same account (#726); the meter says so. Absent otherwise, and from a host that predates it.
@@ -63508,7 +63560,15 @@ var HelloServer = external_exports.object({
63508
63560
  */
63509
63561
  logon: HostLogon.optional().catch(void 0),
63510
63562
  /** A sign-in's `serve` waiting to take over from this host, or one that gave up (#1224, `HostHandover`). Additive */
63511
- handover: HostHandover.optional().catch(void 0)
63563
+ handover: HostHandover.optional().catch(void 0),
63564
+ /**
63565
+ * The turns this host continues because an update of this machine cut them (#1618, `host.stop { forUpdate }`), with when
63566
+ * its start took each up, on this host's clock: what tells a linked hub's caller of such a session that its turn goes on
63567
+ * rather than that the link dropped. Named from the start on, before any hello is answered, whether or not the Continue
63568
+ * is in yet; one that turns out lost is no longer named. An empty list says the host continues the turns an update cuts;
63569
+ * absent, it predates that (or the field could not be read). Additive
63570
+ */
63571
+ updateContinued: external_exports.array(external_exports.object({ sessionId: external_exports.string(), at: external_exports.number() })).optional().catch(void 0)
63512
63572
  });
63513
63573
  var RpcRequest = external_exports.object({
63514
63574
  kind: external_exports.literal("rpc"),
@@ -63968,6 +64028,12 @@ function activityCounts(s) {
63968
64028
  }
63969
64029
  return c;
63970
64030
  }
64031
+ function turnCut(s) {
64032
+ return inTurn(s.state);
64033
+ }
64034
+ function cutTurns(s) {
64035
+ return s.sessions.flatMap((x) => x.id !== void 0 && turnCut(x) ? [x.id] : []);
64036
+ }
63971
64037
  function sessionIdle(s, process4) {
63972
64038
  if (s.pendingApproval) return { idle: false, reason: "approval" };
63973
64039
  if (s.pendingQuestions.length > 0) return { idle: false, reason: "question" };
@@ -66441,7 +66507,8 @@ var HostServer = class _HostServer {
66441
66507
  ...this.opts.build ? { build: this.opts.build } : {},
66442
66508
  ...elevatedField(this.opts.elevated?.()),
66443
66509
  ...this.opts.logon?.() ? { logon: this.opts.logon() } : {},
66444
- ...this.opts.handover?.() ? { handover: this.opts.handover() } : {}
66510
+ ...this.opts.handover?.() ? { handover: this.opts.handover() } : {},
66511
+ ...this.opts.updateContinued ? { updateContinued: this.opts.updateContinued() } : {}
66445
66512
  });
66446
66513
  }
66447
66514
  /**
@@ -105573,6 +105640,12 @@ var RestartPolicy = class {
105573
105640
  * app-server), even though the screen said "starting next turn".
105574
105641
  */
105575
105642
  restartAfterTurn = /* @__PURE__ */ new Set();
105643
+ /**
105644
+ * Sessions whose turn a linked hub's update cut, being woken to continue it (#1618, `continueCutTurns`): their new process
105645
+ * keeps the turn's state as it comes up, as a re-attached one does, rather than settling idle, so a hub reading the machine
105646
+ * between the wake and the note's turn does not read the cut turn as ended
105647
+ */
105648
+ continuingTurns = /* @__PURE__ */ new Set();
105576
105649
  /** Input arriving after a note completes waits for its deferred process replacement, including disposal. */
105577
105650
  deferredRestarts = /* @__PURE__ */ new Map();
105578
105651
  /** A new host served a different Centralu tool prefix; replace the old agent at its safe boundary. */
@@ -107404,6 +107477,7 @@ function interruptionWords(i) {
107404
107477
  return `reconnecting to its API for ${Math.max(1, Math.round((Date.now() - i.at) / 6e4))} min`;
107405
107478
  }
107406
107479
  }
107480
+ var UPDATE_CONTINUE_LINE = "Centralu was updated during this turn, which stopped it; Centralu continued it on the new version.";
107407
107481
 
107408
107482
  // packages/agent-host/src/sessions/error-kind.ts
107409
107483
  var USAGE_LIMIT = /usage limit|rate.?limit|\b429\b|quota|limit reached|hit your limit/i;
@@ -110484,6 +110558,9 @@ Now:
110484
110558
  ${machineLine(machine)}` };
110485
110559
  }
110486
110560
 
110561
+ // packages/agent-host/src/usage-clock.ts
110562
+ var isFromTheFuture = (at2, now) => at2 > now;
110563
+
110487
110564
  // packages/agent-host/src/dev-services/store/usage-history.ts
110488
110565
  var HOUR_MS = 60 * 6e4;
110489
110566
  var BUCKET_COLUMNS = `hour, session_id AS sessionId, project_id AS projectId, tool, model, machine, requests, input,
@@ -110518,6 +110595,27 @@ var UsageRecords = class {
110518
110595
  })();
110519
110596
  return written;
110520
110597
  }
110598
+ /**
110599
+ * Keeps the totals a tool that reports no limits last read (`usage_local_totals`, v64), replacing the ones before: a restart
110600
+ * then shows them at once with `totals.asOf`, while a fresh read goes on. An older read than the one kept is not stored.
110601
+ */
110602
+ saveLocalTotals(tool, totals, now = Date.now()) {
110603
+ if (totals.asOf === void 0) return;
110604
+ const kept = this.db.prepare(`SELECT at FROM usage_local_totals WHERE tool = ?`).get(tool);
110605
+ if (kept && kept.at > totals.asOf && !isFromTheFuture(kept.at, now)) return;
110606
+ this.db.prepare(`INSERT INTO usage_local_totals (tool, at, totals) VALUES (?, ?, ?) ON CONFLICT(tool) DO UPDATE SET at = excluded.at, totals = excluded.totals`).run(tool, totals.asOf, JSON.stringify(totals));
110607
+ }
110608
+ /** The totals `saveLocalTotals` kept for a tool, or null: none, or not in the form this build reads (never a guess) */
110609
+ localTotals(tool) {
110610
+ const row = this.db.prepare(`SELECT at, totals FROM usage_local_totals WHERE tool = ?`).get(tool);
110611
+ if (!row) return null;
110612
+ try {
110613
+ const parsed = UsageSnapshot.shape.totals.safeParse(JSON.parse(row.totals));
110614
+ return parsed.success && parsed.data && parsed.data.asOf === row.at ? parsed.data : null;
110615
+ } catch {
110616
+ return null;
110617
+ }
110618
+ }
110521
110619
  /**
110522
110620
  * The readings with `from <= at < to`, oldest first, and for each series the last one before `from`: a reading is
110523
110621
  * stored only when it changes, so the value in force when the range began is that older row. One tool's when `tool` is given.
@@ -113082,6 +113180,10 @@ var RemoteAgents = class {
113082
113180
  * turned Auto reconnect off, or disconnected it, while it was down
113083
113181
  */
113084
113182
  toldDown = /* @__PURE__ */ new Map();
113183
+ /** The machines whose link this hub's own update or rollback holds, until it is back (#1618, `linkChanged`) */
113184
+ updateHolds = /* @__PURE__ */ new Set();
113185
+ /** The turns an update of their machine cut whose callers were told it goes on (#1618), by session and when it was taken up: once each */
113186
+ toldCut = /* @__PURE__ */ new Set();
113085
113187
  /** The cards of a caller's turn its caller was told of, once each (`<session>\0<turn>\0<request>`, #676) */
113086
113188
  toldCards = /* @__PURE__ */ new Set();
113087
113189
  /** Whose each child's turn is, as settled in this host (`Whose`); one not here is read from its record (`whoseOf`) */
@@ -113913,7 +114015,10 @@ var RemoteAgents = class {
113913
114015
  */
113914
114016
  linkChanged(machine) {
113915
114017
  const m = machine;
113916
- if (!m || typeof m.id !== "string" || m.status === "connected") return;
114018
+ if (!m || typeof m.id !== "string") return;
114019
+ if (updating(m.operation)) this.updateHolds.add(m.id);
114020
+ else if (m.status !== "connecting" && m.status !== "starting") this.updateHolds.delete(m.id);
114021
+ if (m.status === "connected") return;
113917
114022
  const alone = this.host.linkedReader?.machineWaitsForPerson(m.id) === true;
113918
114023
  const mayRecover = m.autoReconnect === false && !alone && m.status !== "versions_differ" && m.status !== "updating";
113919
114024
  const name = typeof m.name === "string" && m.name ? m.name : m.id;
@@ -113932,6 +114037,7 @@ var RemoteAgents = class {
113932
114037
  const told = this.toldDown.get(rec.sessionId);
113933
114038
  if (!report || report.orchestratorId !== rec.callerId || report.paused || told !== void 0 && (told || !alone)) continue;
113934
114039
  if (mayRecover) continue;
114040
+ if (this.updateHolds.has(m.id) && this.host.linkedReader?.continuesCutTurns?.(m.id) === true) continue;
113935
114041
  this.toldDown.set(rec.sessionId, alone);
113936
114042
  const why = !alone ? "It cannot be followed until the link is back: " : m.status === "disconnected" ? "The person disconnected the machine, so nothing here reconnects it: ask the person to connect it. It cannot be followed until then: " : "The machine's Auto reconnect is off, so nothing here reconnects it: ask the person to connect it. It cannot be followed until then: ";
113937
114043
  const rest = why + "its report comes then, once its turn has ended, if that turn is still yours. agent_session status reads its state once the machine can be reached.";
@@ -113970,10 +114076,35 @@ var RemoteAgents = class {
113970
114076
  if (!report.busy && !missed && !held3) continue;
113971
114077
  const now = await reader.read(rec.sessionId, 1);
113972
114078
  if (!now.ok || this.host.children.linkedReports.get(rec.sessionId) !== report || !this.gateNow(rec).owned) continue;
113973
- if (inTurn(now.state ?? "idle")) report.busy = true;
113974
- else this.host.settleLinkedTurn(rec.sessionId);
114079
+ if (inTurn(now.state ?? "idle")) {
114080
+ report.busy = true;
114081
+ this.tellContinued(rec);
114082
+ } else this.host.settleLinkedTurn(rec.sessionId);
113975
114083
  }
113976
114084
  }
114085
+ /**
114086
+ * A child whose turn an update of its machine cut, found in a turn again once the link is back: the host that update
114087
+ * started continued it (#1618). Its caller is told so once, and that the report it is owed is that turn's end, so it
114088
+ * neither waits on a link that came back nor continues the child by hand a second time.
114089
+ *
114090
+ * Whatever the order of the Continue and the reconnect (#1626 review): the machine's host names each turn it continues
114091
+ * from its start on, before it answers any hello, not once the Continue is in (`SessionManager.updateContinued`), so a
114092
+ * link back while the Continue is still on its way is told it too
114093
+ */
114094
+ tellContinued(rec) {
114095
+ const at2 = this.host.linkedReader?.continuedAfterUpdate?.(rec.sessionId);
114096
+ if (at2 == null || this.toldCut.has(`${rec.sessionId}@${at2}`)) return;
114097
+ this.toldCut.add(`${rec.sessionId}@${at2}`);
114098
+ const rest = "and Centralu continued it there on the new version. Its report comes when that turn ends; nothing is needed from you now.";
114099
+ void this.host.tellSession(
114100
+ rec.callerId,
114101
+ `[Centralu] An update of Centralu on its machine cut the turn "${rec.name}" [${rec.sessionId}] was running there, ${rest}`,
114102
+ { sessionId: rec.sessionId, name: rec.name },
114103
+ true,
114104
+ // What the model reads (#1186): all of it but the name, which is not Centralu's words (#120)
114105
+ `[Centralu] An update of Centralu on its machine cut the turn the session [${rec.sessionId}] was running there, ${rest}`
114106
+ );
114107
+ }
113977
114108
  /**
113978
114109
  * Whether a message the child took was sent by this hub over this link (#1038 review): the machine records a hub's
113979
114110
  * instruction from `HUB_SENDER`, which reaches the hub qualified with that machine's id. Another machine's prefix is no
@@ -114080,6 +114211,9 @@ var RemoteAgents = class {
114080
114211
  return { error: `${machine.name} has no checkout of ${key}, so nothing was started there.${listed2 ? ` Its projects: ${listed2}.` : " It has no projects."} The person can clone it there first.` };
114081
114212
  }
114082
114213
  };
114214
+ function updating(op) {
114215
+ return op?.kind === "update" || op?.kind === "rollback";
114216
+ }
114083
114217
 
114084
114218
  // packages/agent-host/src/sessions/delegation-flow.ts
114085
114219
  var DelegationFlow = class {
@@ -114162,8 +114296,9 @@ var DelegationFlow = class {
114162
114296
  if (!joinedTurn) this.host.children.ownTurn(sessionId, callerId, false);
114163
114297
  void d2.outcome.then((r) => {
114164
114298
  if (this.host.agentRuns.get(sessionId) === wait) this.host.agentRuns.delete(sessionId);
114165
- if (!joinedTurn && !this.host.handingOver) this.host.children.releaseTurn(sessionId, d2.callerId);
114166
- if (!agent && r.ok && !joinedTurn && !this.host.handingOver && this.host.meta.has(d2.callerId) && !this.host.children.pausedReports.has(sessionId)) {
114299
+ if (this.host.closing) return;
114300
+ if (!joinedTurn) this.host.children.releaseTurn(sessionId, d2.callerId);
114301
+ if (!agent && r.ok && !joinedTurn && this.host.meta.has(d2.callerId) && !this.host.children.pausedReports.has(sessionId)) {
114167
114302
  const left = this.host.ownTasksRunning(sessionId);
114168
114303
  if (left.length) {
114169
114304
  this.host.children.markReport(sessionId, d2.callerId);
@@ -114269,6 +114404,7 @@ var NOT_READ = "(not read: the session's last turn was the person's; read_sessio
114269
114404
  function outcomeWord(work, stop, ending) {
114270
114405
  if (work && "tookOver" in work) return "took_over";
114271
114406
  if (work && "swapped" in work) return "swapped";
114407
+ if (work && "resumed" in work) return "resumed";
114272
114408
  if (work && ("machineRemoved" in work || "linkLost" in work)) return "stopped";
114273
114409
  if (work && ("paused" in work || "waitingOn" in work)) return "paused";
114274
114410
  if (work && "settled" in work) return "settled";
@@ -114319,6 +114455,9 @@ function machineRemovedReportLine(machine, paused, name = STORED) {
114319
114455
  function swappedReportLine() {
114320
114456
  return "[Centralu] The assigned work's session ended a turn while Centralu was restarting, so that end was not followed. It is most likely the end of the work you gave it; check its last response before relying on it.\n";
114321
114457
  }
114458
+ function resumedReportLine(owed) {
114459
+ return "[Centralu] The assigned work's turn was cut short by an update of Centralu on its machine, and Centralu continued it there on the new version. " + (owed ? "Its report follows when that turn ends; this is not it, and nothing is needed from you now.\n" : "Its end is not reported to you; read the session to follow it.\n");
114460
+ }
114322
114461
  function tookOverReportLine(paused, name = STORED) {
114323
114462
  return "[Centralu] The person took this session over; its later turns are theirs, and none of them will be reported to you. " + (paused.length ? `The work you assigned had paused on its own background work (${name.tasks(paused)}), and no final report follows.
114324
114463
  ` : "The work you assigned was still running, and no report of it follows.\n");
@@ -114515,6 +114654,19 @@ var ChildReports = class {
114515
114654
  const { name, project: project2 } = this.reportedAs(m);
114516
114655
  await this.deliverReport(asker, sessionId, name, project2, previewOf(this.host.store.messages, sessionId, 600), { type: stop.reason === "network" ? "error" : "state_change" }, void 0, this.answerSeq(sessionId));
114517
114656
  }
114657
+ /**
114658
+ * A turn of this session that a linked hub's update cut, and this host continued (#1618): its requester hears it once,
114659
+ * so it does not read the session's stop as the end of the work, nor continue it a second time by hand. The report it is
114660
+ * owed, if one is, stays owed: the continued turn's end is it, so it is neither lost nor sent twice
114661
+ */
114662
+ async tellCutByUpdate(sessionId, requester) {
114663
+ const m = this.host.meta.get(sessionId);
114664
+ if (!m || !this.host.meta.has(requester) || this.host.handingOver) return;
114665
+ const { name, project: project2 } = this.reportedAs(m);
114666
+ const owed = this.host.children.awaitingReport.get(sessionId) === requester;
114667
+ console.error(`[sessions] ${sessionId.slice(0, 8)}: an update cut its turn and it was continued; telling ${requester.slice(0, 8)}`);
114668
+ await this.deliverReport(requester, sessionId, name, project2, previewOf(this.host.store.messages, sessionId, 600), { type: "state_change" }, { resumed: true, owed }, this.answerSeq(sessionId));
114669
+ }
114518
114670
  /**
114519
114671
  * A report a host swap held back (#646, #1186): its session ended a turn while the old host let go, and the mark the
114520
114672
  * store kept is dropped at start (`restoreReports`), so a later turn is never reported as the work. The caller is
@@ -114567,7 +114719,7 @@ ${read} shows what the person and the session did since.`;
114567
114719
  async deliverReport(orchestratorId, sessionId, name, project2, preview, ending, work, answerSeq, part) {
114568
114720
  const stop = this.host.meta.get(sessionId)?.interruption;
114569
114721
  const cutShort = stop?.reason === "network" || stop?.reason === "interrupted" || stop?.reason === "quit";
114570
- const outcomeOf2 = (name2) => work && "tookOver" in work ? tookOverReportLine(work.paused, name2) : work && "swapped" in work ? swappedReportLine() : work && "machineRemoved" in work ? machineRemovedReportLine(work.machineRemoved, work.paused, name2) : work && "linkLost" in work ? linkLostReportLine(work.linkLost, work.paused, name2) : work && "paused" in work ? pausedReportLine(work.paused, work.stoppable ? this.agentCall(orchestratorId, { action: "stop" }) : void 0, name2) : work && "settled" in work ? settledReportLine(work.settled, name2) : work && "waitingOn" in work ? waitingReportLine(work.waitingOn) : stop?.reason === "network" ? `[Centralu] The assigned work stopped before finishing: a network error ended its turn.
114722
+ const outcomeOf2 = (name2) => work && "tookOver" in work ? tookOverReportLine(work.paused, name2) : work && "swapped" in work ? swappedReportLine() : work && "resumed" in work ? resumedReportLine(work.owed) : work && "machineRemoved" in work ? machineRemovedReportLine(work.machineRemoved, work.paused, name2) : work && "linkLost" in work ? linkLostReportLine(work.linkLost, work.paused, name2) : work && "paused" in work ? pausedReportLine(work.paused, work.stoppable ? this.agentCall(orchestratorId, { action: "stop" }) : void 0, name2) : work && "settled" in work ? settledReportLine(work.settled, name2) : work && "waitingOn" in work ? waitingReportLine(work.waitingOn) : stop?.reason === "network" ? `[Centralu] The assigned work stopped before finishing: a network error ended its turn.
114571
114723
  ` : stop?.reason === "interrupted" ? `[Centralu] The assigned work stopped before finishing: its tool interrupted the turn.
114572
114724
  ` : stop?.reason === "quit" ? `[Centralu] The assigned work stopped before finishing: Centralu quit or restarted during its turn.
114573
114725
  ` : ending.type === "error" ? `[Centralu] The assigned work failed.
@@ -114601,6 +114753,7 @@ ${preview || "(no content)"}
114601
114753
  If you need more, you can read that session's recent conversation with read_session.` + cleanUp;
114602
114754
  const holding = !!work && "paused" in work && !("tookOver" in work) && !("machineRemoved" in work) && !("linkLost" in work);
114603
114755
  const handedOn = !!work && "waitingOn" in work;
114756
+ const goesOn = !!work && "resumed" in work;
114604
114757
  const reported = this.toMailbox(orchestratorId, sessionId, {
114605
114758
  outcome: outcomeWord(work, stop, ending),
114606
114759
  body: forModel(true),
@@ -114608,7 +114761,7 @@ If you need more, you can read that session's recent conversation with read_sess
114608
114761
  ...kind ? { error: kindWord(kind) } : {},
114609
114762
  ...holding ? { paused: work.paused.length } : {},
114610
114763
  ...part?.wrote || part?.added ? { personNote: true } : {},
114611
- then: holding || handedOn ? "running" : "review"
114764
+ then: holding || handedOn || goesOn ? "running" : "review"
114612
114765
  });
114613
114766
  if (reported === "mailed" || reported === "closed") return;
114614
114767
  await this.tellSession(
@@ -119744,7 +119897,7 @@ function unfitError(reason) {
119744
119897
  }
119745
119898
 
119746
119899
  // packages/agent-host/src/sessions/session-start.ts
119747
- import { randomUUID as randomUUID19 } from "node:crypto";
119900
+ import { randomUUID as randomUUID20 } from "node:crypto";
119748
119901
 
119749
119902
  // packages/agent-host/src/sessions/opened-settings.ts
119750
119903
  function adoptOpenedSettings(handle, record2, launched, imported) {
@@ -119774,10 +119927,112 @@ function remember(map2, key, value, cap = FOLDER_CACHE_KEYS) {
119774
119927
  }
119775
119928
  }
119776
119929
 
119930
+ // packages/agent-host/src/adapters/input-receipts.ts
119931
+ import { randomUUID as randomUUID19 } from "node:crypto";
119932
+ var UUID_FORMAT = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
119933
+ function isUuid(s) {
119934
+ return UUID_FORMAT.test(s);
119935
+ }
119936
+ var CENTRALU_INPUT_MARK = "ce47a1ce47a1";
119937
+ function centraluInputId() {
119938
+ const id3 = randomUUID19();
119939
+ return `${id3.slice(0, 8)}-${id3.slice(9, 13)}-${id3.slice(14, 18)}-${id3.slice(19, 23)}-${CENTRALU_INPUT_MARK}`;
119940
+ }
119941
+ function isCentraluInputId(id3) {
119942
+ return typeof id3 === "string" && id3.length === 36 && id3.toLowerCase().endsWith(`-${CENTRALU_INPUT_MARK}`);
119943
+ }
119944
+ var UPDATE_CONTINUE_TEXT = "[Centralu] Centralu on this machine was updated while your turn was running, which stopped the turn before it finished. Continue where you stopped. Check what already happened before repeating a step.";
119945
+ function isUpdateContinueNote(text2) {
119946
+ return text2.trim() === UPDATE_CONTINUE_TEXT;
119947
+ }
119948
+ var InputReceipts = class {
119949
+ pending = /* @__PURE__ */ new Map();
119950
+ ended = /* @__PURE__ */ new Set();
119951
+ /** `named`: the id the caller gave the input, which the tool then names back; none, one is made (with Centralu's mark) */
119952
+ create(observer, named) {
119953
+ const id3 = named ?? centraluInputId();
119954
+ let resolve21;
119955
+ let reject;
119956
+ const done2 = new Promise((yes, no2) => {
119957
+ resolve21 = yes;
119958
+ reject = no2;
119959
+ });
119960
+ done2.catch(() => {
119961
+ });
119962
+ const receipt = { id: id3, done: done2, cancelled: false, completed: false };
119963
+ this.pending.set(id3, { receipt, resolve: resolve21, reject, observer });
119964
+ return receipt;
119965
+ }
119966
+ get size() {
119967
+ return this.pending.size;
119968
+ }
119969
+ get ids() {
119970
+ return [...this.pending.keys()];
119971
+ }
119972
+ /** The tool took the input (once): the observer is told before the turn is recorded */
119973
+ accept(id3) {
119974
+ const p = this.pending.get(id3);
119975
+ if (!p || p.accepted) return;
119976
+ p.accepted = true;
119977
+ p.observer?.accepted?.();
119978
+ }
119979
+ /** Codex's turn/start response can arrive after that turn's completion notification. */
119980
+ bind(receipt, turn) {
119981
+ const p = this.pending.get(receipt.id);
119982
+ if (!p) return;
119983
+ this.accept(receipt.id);
119984
+ p.turn = turn;
119985
+ if (this.ended.has(turn)) this.finish(receipt.id);
119986
+ }
119987
+ endTurn(turn) {
119988
+ if (this.pending.size) this.ended.add(turn);
119989
+ for (const [id3, p] of this.pending) if (p.turn === turn) this.finish(id3);
119990
+ }
119991
+ /** Retries can reorder inputs; only UUIDs actually reported as consumed have completed. */
119992
+ complete(ids) {
119993
+ for (const id3 of ids) this.finish(id3);
119994
+ }
119995
+ /** These inputs did not finish (a stopped turn, a process gone): each one's observer is told, the others go on waiting */
119996
+ fail(ids, error51) {
119997
+ for (const id3 of ids) {
119998
+ const p = this.pending.get(id3);
119999
+ if (!p) continue;
120000
+ this.pending.delete(id3);
120001
+ if (!this.pending.size) this.ended.clear();
120002
+ p.receipt.cancelled = true;
120003
+ p.observer?.fail(error51);
120004
+ p.reject(error51);
120005
+ }
120006
+ }
120007
+ failAll(error51) {
120008
+ const turns = [...new Set([...this.pending.values()].flatMap((p) => p.turn ? [p.turn] : []))];
120009
+ const pending = [...this.pending.values()];
120010
+ this.pending.clear();
120011
+ this.ended.clear();
120012
+ for (const p of pending) {
120013
+ p.receipt.cancelled = true;
120014
+ p.observer?.fail(error51);
120015
+ p.reject(error51);
120016
+ }
120017
+ return turns;
120018
+ }
120019
+ finish(id3) {
120020
+ const p = this.pending.get(id3);
120021
+ if (!p) return;
120022
+ this.pending.delete(id3);
120023
+ if (!this.pending.size) this.ended.clear();
120024
+ if (!p.accepted) p.observer?.accepted?.();
120025
+ p.receipt.completed = true;
120026
+ p.observer?.complete();
120027
+ p.resolve();
120028
+ }
120029
+ };
120030
+
119777
120031
  // packages/agent-host/src/sessions/external-conversations.ts
119778
120032
  var HISTORY_LIMIT = 200;
119779
120033
  var SYNC_LIMIT = 600;
119780
120034
  var externalSyncedKey = (sessionId) => `external_synced:${sessionId}`;
120035
+ var ownNote = (h) => h.role === "user" && isUpdateContinueNote(h.text);
119781
120036
  function historyRow(sessionId, seq, h, fallbackTs) {
119782
120037
  const ts = h.ts ?? fallbackTs;
119783
120038
  if (h.role === "system") {
@@ -119914,39 +120169,32 @@ var ExternalConversations = class {
119914
120169
  return parts.length > 0 ? parts.join("") : void 0;
119915
120170
  };
119916
120171
  const lastText = lastMessageText()?.trim();
120172
+ const said = lastText ? history.findLastIndex((h) => h.role !== "system" && h.text.trim() === lastText) : -1;
120173
+ const wrote = history.findLastIndex((h) => h.role === "user" && h.sent === true);
119917
120174
  let start2 = -1;
119918
- if (lastText) {
119919
- for (let i = history.length - 1; i >= 0; i--) {
119920
- const h = history[i];
119921
- if (h.role !== "system" && h.text.trim() === lastText) {
119922
- start2 = i + 1;
119923
- break;
119924
- }
119925
- }
119926
- } else if (ours.length === 0) {
119927
- start2 = 0;
119928
- }
120175
+ if (ours.length === 0) start2 = 0;
120176
+ else if (said >= 0 || !lastText && wrote >= 0) start2 = Math.max(said, wrote) + 1;
119929
120177
  if (start2 < 0 || start2 >= history.length) {
119930
120178
  mark();
119931
120179
  return 0;
119932
120180
  }
119933
120181
  const known = /* @__PURE__ */ new Map();
119934
- for (let i = ours.length - 1; i >= 0 && ours[i].kind !== "text"; i--) {
120182
+ let trailing = true;
120183
+ for (let i = ours.length - 1; i >= 0; i--) {
120184
+ if (ours[i].kind === "text") trailing = false;
119935
120185
  const key2 = eventKey(ours[i]);
119936
- if (key2) known.set(key2, (known.get(key2) ?? 0) + 1);
120186
+ if (key2 && (key2 !== "compaction" || trailing)) known.set(key2, (known.get(key2) ?? 0) + 1);
119937
120187
  }
119938
120188
  let end = start2;
119939
120189
  while (history[end]?.role === "system") end++;
119940
- const fresh = [
119941
- ...history.slice(start2, end).filter((h) => {
119942
- const key2 = eventKey(historyRow(info.id, 0, h, 0));
119943
- const left = key2 ? known.get(key2) ?? 0 : 0;
119944
- if (!key2 || left === 0) return true;
119945
- known.set(key2, left - 1);
119946
- return false;
119947
- }),
119948
- ...history.slice(end)
119949
- ];
120190
+ const fresh = history.slice(start2).filter((h, i) => {
120191
+ if (ownNote(h)) return false;
120192
+ const key2 = eventKey(historyRow(info.id, 0, h, 0));
120193
+ const left = key2 && (key2 !== "compaction" || start2 + i < end) ? known.get(key2) ?? 0 : 0;
120194
+ if (!key2 || left === 0) return true;
120195
+ known.set(key2, left - 1);
120196
+ return false;
120197
+ });
119950
120198
  if (fresh.length === 0) {
119951
120199
  mark();
119952
120200
  return 0;
@@ -119983,6 +120231,7 @@ var ExternalConversations = class {
119983
120231
  });
119984
120232
  return;
119985
120233
  }
120234
+ history = history.filter((h) => !ownNote(h));
119986
120235
  if (history.length === 0) return;
119987
120236
  const base2 = this.host.store.messages.nextSeq(info.id);
119988
120237
  const rows3 = history.map((h, i) => historyRow(info.id, base2 + i, h, info.createdAt));
@@ -120099,7 +120348,7 @@ var SessionStarts = class {
120099
120348
  throw rpcError(`This conversation is already open in the "${holder.name}" session \u2014 continue there`);
120100
120349
  }
120101
120350
  }
120102
- const id3 = randomUUID19();
120351
+ const id3 = randomUUID20();
120103
120352
  const permissionPreset = presetFromHub(params, this.host.uiPreferences());
120104
120353
  let worktree = null;
120105
120354
  let setupProblems = [];
@@ -120541,6 +120790,9 @@ var SessionStarts = class {
120541
120790
  this.host.emit({ type: "state_change", sessionId, state: m.state, reason: "reattached" });
120542
120791
  if (kept === null) this.host.restartPolicy.backgroundUnknown.set(sessionId, Date.now());
120543
120792
  else if (kept.length > 0) this.host.onEvent({ type: "background_tasks", sessionId, live: kept });
120793
+ } else if (this.host.restartPolicy.continuingTurns.has(sessionId)) {
120794
+ this.host.store.sessions.upsertSession(m);
120795
+ this.host.emit({ type: "state_change", sessionId, state: m.state, reason: "continued" });
120544
120796
  } else {
120545
120797
  this.host.settleState(m, { type: "state_change", sessionId, state: "idle", reason: "resumed" });
120546
120798
  }
@@ -120591,7 +120843,6 @@ function startFit(adapter, opts, sink) {
120591
120843
  }
120592
120844
 
120593
120845
  // packages/agent-host/src/sessions/session-events.ts
120594
- import { randomUUID as randomUUID20 } from "node:crypto";
120595
120846
  import { relative as relative5, resolve as resolve13, sep as sep5 } from "node:path";
120596
120847
 
120597
120848
  // packages/agent-host/src/dev-services/git-attrib.ts
@@ -121603,7 +121854,7 @@ ${attachments.map((a) => `@${a.path}`).join("\n")}` : text2;
121603
121854
  ${adapterText}` : adapterText;
121604
121855
  const joins = inTurn(m.state) && this.host.joinsRunningTurn(sessionId);
121605
121856
  const hubInput = from?.sessionId === HUB_SENDER;
121606
- const inputId = joins || hubInput ? randomUUID20() : void 0;
121857
+ const inputId = joins || hubInput ? centraluInputId() : void 0;
121607
121858
  this.host.children.noteMessage(sessionId, !from && !fromApp2 && !fromScript ? "person" : "work", inTurn(m.state), this.host.joinsRunningTurn(sessionId), intent === "task", inputId, relayed ? "report" : fromApp2 ? "app" : "caller");
121608
121859
  if (inputId && from && hubInput) this.host.children.noteInputOrigin(sessionId, inputId, { seq, from });
121609
121860
  this.host.cardRests.turnAsked(sessionId);
@@ -121644,7 +121895,8 @@ ${adapterText}` : adapterText;
121644
121895
  this.host.cardRests.turnAsked(sessionId);
121645
121896
  await takeInput(handle, input.text, late);
121646
121897
  }
121647
- this.onEvent({ type: "notice", sessionId, level: "info", from: "Centralu", label: "mail", text: input.line });
121898
+ if (input.skipLine?.()) return;
121899
+ this.onEvent({ type: "notice", sessionId, level: "info", from: "Centralu", label: input.label ?? "mail", text: input.line });
121648
121900
  }
121649
121901
  /**
121650
121902
  * Centralu's own compact command (#1451): the tool's `/compact` as a turn of its own, in no one's name and with no user row,
@@ -122272,97 +122524,8 @@ function spawnLocalAgent(spec, os2) {
122272
122524
  return LocalAgentProcess.spawn(spec, os2);
122273
122525
  }
122274
122526
 
122275
- // packages/agent-host/src/adapters/input-receipts.ts
122276
- import { randomUUID as randomUUID21 } from "node:crypto";
122277
- var UUID_FORMAT = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
122278
- function isUuid(s) {
122279
- return UUID_FORMAT.test(s);
122280
- }
122281
- var InputReceipts = class {
122282
- pending = /* @__PURE__ */ new Map();
122283
- ended = /* @__PURE__ */ new Set();
122284
- /** `named`: the id the caller gave the input, which the tool then names back; none, one is made */
122285
- create(observer, named) {
122286
- const id3 = named ?? randomUUID21();
122287
- let resolve21;
122288
- let reject;
122289
- const done2 = new Promise((yes, no2) => {
122290
- resolve21 = yes;
122291
- reject = no2;
122292
- });
122293
- done2.catch(() => {
122294
- });
122295
- const receipt = { id: id3, done: done2, cancelled: false, completed: false };
122296
- this.pending.set(id3, { receipt, resolve: resolve21, reject, observer });
122297
- return receipt;
122298
- }
122299
- get size() {
122300
- return this.pending.size;
122301
- }
122302
- get ids() {
122303
- return [...this.pending.keys()];
122304
- }
122305
- /** The tool took the input (once): the observer is told before the turn is recorded */
122306
- accept(id3) {
122307
- const p = this.pending.get(id3);
122308
- if (!p || p.accepted) return;
122309
- p.accepted = true;
122310
- p.observer?.accepted?.();
122311
- }
122312
- /** Codex's turn/start response can arrive after that turn's completion notification. */
122313
- bind(receipt, turn) {
122314
- const p = this.pending.get(receipt.id);
122315
- if (!p) return;
122316
- this.accept(receipt.id);
122317
- p.turn = turn;
122318
- if (this.ended.has(turn)) this.finish(receipt.id);
122319
- }
122320
- endTurn(turn) {
122321
- if (this.pending.size) this.ended.add(turn);
122322
- for (const [id3, p] of this.pending) if (p.turn === turn) this.finish(id3);
122323
- }
122324
- /** Retries can reorder inputs; only UUIDs actually reported as consumed have completed. */
122325
- complete(ids) {
122326
- for (const id3 of ids) this.finish(id3);
122327
- }
122328
- /** These inputs did not finish (a stopped turn, a process gone): each one's observer is told, the others go on waiting */
122329
- fail(ids, error51) {
122330
- for (const id3 of ids) {
122331
- const p = this.pending.get(id3);
122332
- if (!p) continue;
122333
- this.pending.delete(id3);
122334
- if (!this.pending.size) this.ended.clear();
122335
- p.receipt.cancelled = true;
122336
- p.observer?.fail(error51);
122337
- p.reject(error51);
122338
- }
122339
- }
122340
- failAll(error51) {
122341
- const turns = [...new Set([...this.pending.values()].flatMap((p) => p.turn ? [p.turn] : []))];
122342
- const pending = [...this.pending.values()];
122343
- this.pending.clear();
122344
- this.ended.clear();
122345
- for (const p of pending) {
122346
- p.receipt.cancelled = true;
122347
- p.observer?.fail(error51);
122348
- p.reject(error51);
122349
- }
122350
- return turns;
122351
- }
122352
- finish(id3) {
122353
- const p = this.pending.get(id3);
122354
- if (!p) return;
122355
- this.pending.delete(id3);
122356
- if (!this.pending.size) this.ended.clear();
122357
- if (!p.accepted) p.observer?.accepted?.();
122358
- p.receipt.completed = true;
122359
- p.observer?.complete();
122360
- p.resolve();
122361
- }
122362
- };
122363
-
122364
122527
  // packages/agent-host/src/adapters/session-kit.ts
122365
- import { randomUUID as randomUUID22 } from "node:crypto";
122528
+ import { randomUUID as randomUUID21 } from "node:crypto";
122366
122529
  var notices = 0;
122367
122530
  function adapterNotice(sessionId, text2) {
122368
122531
  return { type: "message_delta", sessionId, role: "assistant", messageId: `centralu-notice-${++notices}`, text: text2 };
@@ -122410,7 +122573,7 @@ function stopNotConfirmed(sessionId, text2) {
122410
122573
  }
122411
122574
  var CardIds = class {
122412
122575
  counter = 0;
122413
- tag = randomUUID22().slice(0, 8);
122576
+ tag = randomUUID21().slice(0, 8);
122414
122577
  next(prefix) {
122415
122578
  return `${prefix}-${this.tag}-${++this.counter}`;
122416
122579
  }
@@ -124305,10 +124468,14 @@ var AcpAdapter = class {
124305
124468
  this.profile = profile;
124306
124469
  this.spawn = spawn6;
124307
124470
  this.stopConfirmMs = stopConfirmMs;
124471
+ const localUsage = profile.localUsage?.bind(profile);
124472
+ if (localUsage) this.localUsage = localUsage;
124308
124473
  }
124309
124474
  profile;
124310
124475
  spawn;
124311
124476
  stopConfirmMs;
124477
+ /** What the tool's own command says about its usage (the profile's `localUsage`), when it has one */
124478
+ localUsage;
124312
124479
  get descriptor() {
124313
124480
  return this.profile.descriptor;
124314
124481
  }
@@ -125395,7 +125562,7 @@ var AntigravityAdapter = class extends AcpAdapter {
125395
125562
  import { tmpdir as tmpdir3 } from "node:os";
125396
125563
  import { mkdtemp as mkdtemp3, readdir as readdir10, rm as rm8, stat as stat8 } from "node:fs/promises";
125397
125564
  import { join as join43 } from "node:path";
125398
- import { randomUUID as randomUUID25 } from "node:crypto";
125565
+ import { randomUUID as randomUUID23 } from "node:crypto";
125399
125566
  import { execFile as execFile6 } from "node:child_process";
125400
125567
  import { promisify as promisify5 } from "node:util";
125401
125568
 
@@ -126058,20 +126225,58 @@ function contentBlocks(m) {
126058
126225
 
126059
126226
  // packages/agent-host/src/adapters/images.ts
126060
126227
  import { readFile as readFile6, stat as stat6 } from "node:fs/promises";
126061
- var IMAGE_MIMES2 = {
126062
- png: "image/png",
126063
- jpg: "image/jpeg",
126064
- jpeg: "image/jpeg",
126065
- gif: "image/gif",
126066
- webp: "image/webp",
126067
- bmp: "image/bmp"
126068
- };
126228
+ var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set(["png", "jpg", "jpeg", "gif", "webp", "bmp"]);
126229
+ var startsWith = (buf, bytes2, at2 = 0) => bytes2.every((b2, i) => buf[at2 + i] === b2);
126230
+ function gifReachesTrailer(buf) {
126231
+ const n = buf.length;
126232
+ const at2 = (i8) => buf[i8] ?? 0;
126233
+ const table = (flags) => flags & 128 ? 3 * 2 ** ((flags & 7) + 1) : 0;
126234
+ const skipSubBlocks = (from) => {
126235
+ let i8 = from;
126236
+ while (i8 < n && at2(i8) !== 0) i8 += at2(i8) + 1;
126237
+ return i8 + 1;
126238
+ };
126239
+ let i = 13 + table(at2(10));
126240
+ while (i < n) {
126241
+ const b2 = at2(i);
126242
+ if (b2 === 59) return true;
126243
+ if (b2 === 33) i = skipSubBlocks(i + 2);
126244
+ else if (b2 === 44) i = skipSubBlocks(i + 11 + table(at2(i + 9)));
126245
+ else return true;
126246
+ }
126247
+ return false;
126248
+ }
126249
+ function sniffImage(buf) {
126250
+ const n = buf.length;
126251
+ if (startsWith(buf, [137, 80, 78, 71, 13, 10, 26, 10])) {
126252
+ return { mime: "image/png", complete: buf.lastIndexOf("IEND", n, "latin1") >= 8 };
126253
+ }
126254
+ if (startsWith(buf, [255, 216, 255])) return { mime: "image/jpeg", complete: buf.lastIndexOf(Buffer.from([255, 217])) > 2 };
126255
+ const head = buf.subarray(0, 6).toString("latin1");
126256
+ if (head === "GIF87a" || head === "GIF89a") return { mime: "image/gif", complete: gifReachesTrailer(buf) };
126257
+ if (head.startsWith("RIFF") && buf.subarray(8, 12).toString("latin1") === "WEBP") {
126258
+ return { mime: "image/webp", complete: buf.readUInt32LE(4) + 8 <= n };
126259
+ }
126260
+ if (startsWith(buf, [66, 77]) && n >= 26 && buf.readUInt32LE(6) === 0 && [12, 40, 52, 56, 64, 108, 124].includes(buf.readUInt32LE(14))) return { mime: "image/bmp", complete: buf.readUInt32LE(2) <= n };
126261
+ return null;
126262
+ }
126263
+ var SIGNATURES = [[137, 80, 78, 71, 13, 10, 26, 10], [255, 216, 255], [...Buffer.from("GIF89a")], [...Buffer.from("RIFF")], [66, 77]];
126264
+ function judgeImage(buf) {
126265
+ const seen = sniffImage(buf);
126266
+ if (seen?.complete) return { mime: seen.mime };
126267
+ if (seen || buf.length < 8 && SIGNATURES.some((sig) => buf.every((b2, i) => i >= sig.length || b2 === sig[i]))) {
126268
+ return { note: `the file is cut short (${buf.length} bytes): it may still be being written` };
126269
+ }
126270
+ return null;
126271
+ }
126069
126272
  var IMAGE_MAX_BYTES = 8 * 1048576;
126070
- var tooLarge = (bytes2) => `Image is too large (${Math.round(bytes2 / 1048576)}MB)`;
126273
+ var tooLarge = (bytes2) => `too large (${Math.round(bytes2 / 1048576)}MB)`;
126071
126274
  function inlineImageEvent(sessionId, mime, data) {
126072
126275
  const bytes2 = Math.floor(data.length * 3 / 4);
126073
126276
  if (bytes2 > IMAGE_MAX_BYTES) return { type: "message_image", sessionId, mime, data: "", note: tooLarge(bytes2) };
126074
- return { type: "message_image", sessionId, mime: mime || "image/png", data };
126277
+ const judged = judgeImage(Buffer.from(data, "base64"));
126278
+ if (judged && "note" in judged) return undrawnImageEvent(sessionId, judged.note);
126279
+ return { type: "message_image", sessionId, mime: judged?.mime ?? (mime || "image/png"), data };
126075
126280
  }
126076
126281
  function undrawnImageEvent(sessionId, note) {
126077
126282
  return { type: "message_image", sessionId, mime: "", data: "", note };
@@ -126084,22 +126289,31 @@ function mcpImageEvents(sessionId, content) {
126084
126289
  if (part.type !== "image") continue;
126085
126290
  const data = typeof part.data === "string" ? part.data : "";
126086
126291
  const mime = typeof part.mimeType === "string" ? part.mimeType : "";
126087
- out.push(data ? inlineImageEvent(sessionId, mime, data) : undrawnImageEvent(sessionId, "The tool sent an image with no data"));
126292
+ out.push(data ? inlineImageEvent(sessionId, mime, data) : undrawnImageEvent(sessionId, "the tool sent it with no data"));
126088
126293
  }
126089
126294
  return out;
126090
126295
  }
126296
+ function readFailure2(err) {
126297
+ const code2 = asObj(err).code;
126298
+ if (code2 === "ENOENT") return "the file is gone";
126299
+ if (code2 === "EACCES" || code2 === "EPERM") return "no permission to read it";
126300
+ if (code2 === "EISDIR") return "it is a folder";
126301
+ return typeof code2 === "string" ? `the system refused the read (${code2})` : "the read failed";
126302
+ }
126091
126303
  async function imageEventFromDisk(sessionId, path2, maxBytes = IMAGE_MAX_BYTES) {
126092
126304
  const fail4 = (note) => ({ ...undrawnImageEvent(sessionId, note), path: path2 });
126093
- const ext2 = path2.slice(path2.lastIndexOf(".") + 1).toLowerCase();
126094
- const mime = IMAGE_MIMES2[ext2];
126095
- if (!mime) return fail4(`Cannot display this format (.${ext2})`);
126305
+ const file2 = path2.split(/[\\/]/).pop() ?? path2;
126306
+ const ext2 = file2.includes(".") ? file2.slice(file2.lastIndexOf(".") + 1).toLowerCase() : "";
126096
126307
  try {
126097
126308
  const s = await stat6(path2);
126098
126309
  if (s.size > maxBytes) return fail4(tooLarge(s.size));
126099
126310
  const buf = await readFile6(path2);
126100
- return { type: "message_image", sessionId, mime, data: buf.toString("base64"), path: path2 };
126311
+ const seen = judgeImage(buf);
126312
+ if (!seen) return fail4(!ext2 ? "the file is not a picture" : IMAGE_EXTENSIONS.has(ext2) ? `the file is not a valid .${ext2} image` : `the .${ext2} format cannot be shown`);
126313
+ if ("note" in seen) return fail4(seen.note);
126314
+ return { type: "message_image", sessionId, mime: seen.mime, data: buf.toString("base64"), path: path2 };
126101
126315
  } catch (err) {
126102
- return fail4(`Failed to read image: ${err.message}`);
126316
+ return fail4(readFailure2(err));
126103
126317
  }
126104
126318
  }
126105
126319
 
@@ -126327,7 +126541,7 @@ async function readClaudeHistory(externalId, cwd, limit) {
126327
126541
  try {
126328
126542
  const message = readableMessage(r?.message);
126329
126543
  if (r?.type === "assistant") out.push(...assistantLines(message, agents));
126330
- else if (r?.type === "user") out.push(...userLines(message, agents));
126544
+ else if (r?.type === "user") out.push(...userLines(message, agents, isCentraluInputId(r.uuid)));
126331
126545
  } catch (err) {
126332
126546
  console.error(`[claude] a transcript row was skipped on read-back: ${err.message}`);
126333
126547
  }
@@ -126355,11 +126569,14 @@ function agentResult(e, agents) {
126355
126569
  if (!agents.has(e.callId) && !FOR_THE_MODEL.test(e.output ?? e.summary)) return e;
126356
126570
  return toolResultEvent(HISTORY_SESSION, e.callId, e.ok, "", AGENT_REPORT_NOT_RECORDED);
126357
126571
  }
126358
- function userLines(message, agents) {
126572
+ var INTERRUPT_LINE = /^\[Request interrupted by user( for tool use)?\]$/;
126573
+ function userLines(message, agents, sent) {
126359
126574
  const raw = userText(message);
126360
126575
  if (COMPACT_SUMMARY.test(raw)) return [{ role: "system", event: { type: "compaction", sessionId: HISTORY_SESSION, failed: false } }];
126361
- const text2 = stripInjectedBlocks(raw);
126362
- const out = text2 ? [{ role: "user", text: text2 }] : [];
126576
+ const text2 = INTERRUPT_LINE.test(raw) ? "" : stripInjectedBlocks(raw);
126577
+ const note = isUpdateContinueNote(text2);
126578
+ if (note && !sent) console.error("[claude] Centralu's Continue note is in the transcript without the input id Centralu gave it: this CLI does not keep it. The catch-up recognises the note by its text");
126579
+ const out = text2 ? [{ role: "user", text: text2, ...sent || note ? { sent: true } : {} }] : [];
126363
126580
  const events2 = userEvents({ message }, HISTORY_SESSION);
126364
126581
  out.push(...historyEvents(events2.map((e) => e.type === "tool_result" ? agentResult(e, agents) : e)));
126365
126582
  return out;
@@ -126467,7 +126684,6 @@ async function readClaudeModels(q2) {
126467
126684
 
126468
126685
  // packages/agent-host/src/adapters/claude/session.ts
126469
126686
  init_sdk();
126470
- import { randomUUID as randomUUID24 } from "node:crypto";
126471
126687
 
126472
126688
  // packages/agent-host/src/adapters/subagent-steps.ts
126473
126689
  function subagentSteps(sessionId, parentCallId, events2) {
@@ -128901,8 +129117,9 @@ var ClaudeSession = class {
128901
129117
  this.closed = true;
128902
129118
  this.notify?.();
128903
129119
  }
129120
+ /** Every input goes out under an id with Centralu's mark, so its line in the transcript reads back as Centralu's (`centraluInputId`) */
128904
129121
  send(text2, opts) {
128905
- this.sendInput(text2, opts?.id !== void 0 && isUuid(opts.id) ? opts.id : randomUUID24(), void 0, opts?.origin, opts?.quiet);
129122
+ this.sendInput(text2, opts?.id !== void 0 && isUuid(opts.id) ? opts.id : centraluInputId(), void 0, opts?.origin, opts?.quiet);
128906
129123
  }
128907
129124
  /** A note read with the next message or at the running turn's next tool round, starting nothing (#1492): quiet input, stamped as Centralu's */
128908
129125
  nudge(text2) {
@@ -128920,7 +129137,7 @@ var ClaudeSession = class {
128920
129137
  turnInputs() {
128921
129138
  return this.resultInputs;
128922
129139
  }
128923
- sendInput(text2, uuid3 = randomUUID24(), note, origin, quiet) {
129140
+ sendInput(text2, uuid3 = centraluInputId(), note, origin, quiet) {
128924
129141
  const goal = isGoalCommand(text2);
128925
129142
  if (!this.sendGate && !(goal && this.goalCommand === void 0)) {
128926
129143
  this.deliver(text2, uuid3, !!note, origin, quiet);
@@ -129397,7 +129614,7 @@ var ClaudeAdapter = class _ClaudeAdapter {
129397
129614
  await sweepProbeDirs();
129398
129615
  }
129399
129616
  const dir = await mkdtemp3(join43(tmpdir3(), PROBE_DIR_PREFIX));
129400
- const sessionId = `usage-probe-${randomUUID25()}`;
129617
+ const sessionId = `usage-probe-${randomUUID23()}`;
129401
129618
  _ClaudeAdapter.probeIds.add(sessionId);
129402
129619
  let probe = null;
129403
129620
  try {
@@ -130479,7 +130696,7 @@ function itemToMessages(item, ts) {
130479
130696
  }
130480
130697
  if (it2.type === "userMessage") {
130481
130698
  const text2 = stripInjectedBlocks(userInputText(it2.content));
130482
- return text2 ? [{ role: "user", text: text2, ts }] : [];
130699
+ return text2 ? [{ role: "user", text: text2, ts, ...isUpdateContinueNote(text2) ? { sent: true } : {} }] : [];
130483
130700
  }
130484
130701
  const live2 = (method) => normalizeNotification(HISTORY_SESSION, { method, params: { item: it2 } });
130485
130702
  return historyEvents([...live2("item/started"), ...live2("item/completed")], ts);
@@ -130870,6 +131087,29 @@ var CODEX_KNOWN_NOTIFICATIONS = /* @__PURE__ */ new Set([
130870
131087
  "thread/realtime/closed"
130871
131088
  ]);
130872
131089
 
131090
+ // packages/agent-host/src/adapters/codex/reasoning-parts.ts
131091
+ var ReasoningParts = class {
131092
+ item = null;
131093
+ /** Something ended the open row (a tool call, the end of the turn, an image): the next chunk starts a row, not a part */
131094
+ reset() {
131095
+ this.item = null;
131096
+ }
131097
+ /** The events one notification maps to, with the blank line in front of a new item's first chunk */
131098
+ join(n, events2) {
131099
+ if (n.method !== "item/reasoning/summaryTextDelta") return events2;
131100
+ const p = asObj(n.params);
131101
+ const item = str(p.itemId);
131102
+ if (!item) return events2;
131103
+ if (!events2.some((e) => e.type === "reasoning_delta" && e.text)) return events2;
131104
+ const apart = this.item !== null && this.item !== item;
131105
+ this.item = item;
131106
+ if (!apart || typeof p.summaryIndex === "number" && p.summaryIndex > 0) return events2;
131107
+ return events2.map((e) => e.type === "reasoning_delta" && e.text ? { ...e, text: `
131108
+
131109
+ ${e.text}` } : e);
131110
+ }
131111
+ };
131112
+
130873
131113
  // packages/agent-host/src/adapters/codex/thread-settings.ts
130874
131114
  function threadSettingsOf(res) {
130875
131115
  if (!res) return null;
@@ -131819,7 +132059,10 @@ var CodexSession = class {
131819
132059
  this.sessionId = opts.sessionId;
131820
132060
  this.livePreset = opts.permissionPreset;
131821
132061
  const sink = emit;
131822
- this.emit = (e) => sink(keepAttachedAppMarks(e, () => (opts.apps?.current() ?? []).map((a) => a.server)));
132062
+ this.emit = (e) => {
132063
+ if (endsStreamRow(e) || streamRowKind(e) === "text") this.reasoningParts.reset();
132064
+ sink(keepAttachedAppMarks(e, () => (opts.apps?.current() ?? []).map((a) => a.server)));
132065
+ };
131823
132066
  this.unmapped = new UnmappedTypes("codex", opts.sessionId, CODEX_KNOWN_NOTIFICATIONS);
131824
132067
  this.retry = new RetryActivity(opts.sessionId);
131825
132068
  this.slashCommands = new CodexSlashCommands({
@@ -131978,6 +132221,8 @@ var CodexSession = class {
131978
132221
  ready;
131979
132222
  /** The preset the session is set to now (#1525), which can differ from the one it launched under only between `normal` and `edits` */
131980
132223
  livePreset;
132224
+ /** Which reasoning item the open reasoning row last grew from, so a second item's summary does not run into the first's (#1464) */
132225
+ reasoningParts = new ReasoningParts();
131981
132226
  setPermissionPreset(preset) {
131982
132227
  this.livePreset = preset;
131983
132228
  }
@@ -132123,7 +132368,7 @@ var CodexSession = class {
132123
132368
  }
132124
132369
  /** A notification of this thread's own, as the events it maps to (`normalizeNotification`), each the way it goes out */
132125
132370
  emitNotification(n) {
132126
- for (const said of normalizeNotification(this.sessionId, n)) {
132371
+ for (const said of this.reasoningParts.join(n, normalizeNotification(this.sessionId, n))) {
132127
132372
  const sort = said.type === "compaction" ? { ...said, trigger: this.compactTurn ? "manual" : "auto" } : said;
132128
132373
  const compactAt = codexCompactAt(this.opts);
132129
132374
  const e = sort.type === "context_update" && compactAt !== void 0 ? { ...sort, compactAt } : sort;
@@ -133147,6 +133392,111 @@ var CURSOR_SURFACE = {
133147
133392
  baseline: SURFACE_BASELINE3
133148
133393
  };
133149
133394
 
133395
+ // packages/agent-host/src/adapters/usage-read.ts
133396
+ function softRead(o) {
133397
+ const now = o.now ?? Date.now;
133398
+ const freshFor = (e) => typeof o.freshMs === "function" ? o.freshMs(e.v, e.tookMs) : o.freshMs;
133399
+ let value = null;
133400
+ let failedAt = null;
133401
+ let inflight = null;
133402
+ const standIn = () => value && (o.staleOk?.(value.v) ?? true) ? value.v : o.fallback();
133403
+ const start2 = () => {
133404
+ const began = now();
133405
+ return inflight ??= o.read().then(
133406
+ (v) => {
133407
+ value = { at: now(), v, tookMs: now() - began };
133408
+ failedAt = null;
133409
+ return v;
133410
+ },
133411
+ (e) => {
133412
+ failedAt = now();
133413
+ o.onFail?.(e instanceof Error ? e : new Error(String(e)));
133414
+ return standIn();
133415
+ }
133416
+ ).finally(() => {
133417
+ inflight = null;
133418
+ });
133419
+ };
133420
+ return async (ask = {}) => {
133421
+ const t = now();
133422
+ if (!value && ask.seed) value = { at: ask.seed.at, v: ask.seed.value, tookMs: 0 };
133423
+ if (value && !isFromTheFuture(value.at, t) && t - value.at < freshFor(value)) return { value: value.v, provisional: false };
133424
+ if (failedAt !== null && t - failedAt < o.failMs) return { value: standIn(), provisional: false };
133425
+ const run4 = start2();
133426
+ const held3 = value && (o.staleOk?.(value.v) ?? true) ? { v: value.v } : null;
133427
+ if (held3 && !ask.settle) return { value: held3.v, provisional: true };
133428
+ let timer;
133429
+ const soft = new Promise((resolve21) => {
133430
+ timer = setTimeout(() => resolve21({ value: held3 ? held3.v : o.fallback(), provisional: true }), ask.settle ? o.settleMs ?? 2e4 : o.softMs);
133431
+ timer.unref?.();
133432
+ });
133433
+ try {
133434
+ return await Promise.race([run4.then((v) => ({ value: v, provisional: false })), soft]);
133435
+ } finally {
133436
+ clearTimeout(timer);
133437
+ }
133438
+ };
133439
+ }
133440
+ function localUsageReader(o) {
133441
+ const read = softRead(o);
133442
+ return async (ask) => {
133443
+ const at2 = ask?.seed?.totals?.asOf;
133444
+ const { value, provisional } = await read({ settle: ask?.settle, ...ask?.seed && at2 !== void 0 ? { seed: { value: ask.seed, at: at2 } } : {} });
133445
+ return provisional ? { ...value, provisional: true } : value;
133446
+ };
133447
+ }
133448
+
133449
+ // packages/agent-host/src/adapters/cursor-agent/usage.ts
133450
+ var ABOUT = external_exports.object({ subscriptionTier: external_exports.unknown().optional() });
133451
+ function parseAbout(stdout) {
133452
+ const about = ABOUT.parse(jsonObjectOf(stdout));
133453
+ if (about.subscriptionTier === null) return { plan: null, loggedOut: true };
133454
+ const tier = typeof about.subscriptionTier === "string" ? about.subscriptionTier.trim() : "";
133455
+ return { plan: tier || null };
133456
+ }
133457
+ function jsonObjectOf(stdout) {
133458
+ for (let from = stdout.indexOf("{"); from >= 0; from = stdout.indexOf("{", from + 1)) {
133459
+ let depth = 0;
133460
+ let inString = false;
133461
+ for (let i = from; i < stdout.length; i++) {
133462
+ const c = stdout[i];
133463
+ if (inString) {
133464
+ if (c === "\\") i++;
133465
+ else if (c === '"') inString = false;
133466
+ } else if (c === '"') inString = true;
133467
+ else if (c === "{") depth++;
133468
+ else if (c === "}" && --depth === 0) {
133469
+ try {
133470
+ const value = JSON.parse(stdout.slice(from, i + 1));
133471
+ if (typeof value === "object" && value !== null) return value;
133472
+ } catch {
133473
+ }
133474
+ break;
133475
+ }
133476
+ }
133477
+ }
133478
+ throw new Error("`agent about` printed no JSON object");
133479
+ }
133480
+ var NOTHING = { plan: null };
133481
+ var FRESH_MS = 5 * 6e4;
133482
+ var LOGGED_OUT_FRESH_MS = 1e4;
133483
+ var FAIL_MS = 6e4;
133484
+ var SOFT_MS = 3e3;
133485
+ var ABOUT_RUN_MS = 15e3;
133486
+ function cursorUsageReader(run4, now = Date.now) {
133487
+ return localUsageReader({
133488
+ now,
133489
+ freshMs: (v) => v.loggedOut ? LOGGED_OUT_FRESH_MS : FRESH_MS,
133490
+ // A "not logged in" that is ten seconds old is read again before it is told: a login a moment ago is seen at once
133491
+ staleOk: (v) => !v.loggedOut,
133492
+ failMs: FAIL_MS,
133493
+ softMs: SOFT_MS,
133494
+ fallback: () => NOTHING,
133495
+ onFail: (e) => console.error(`[usage] cursor: could not read the plan: ${e.message}`),
133496
+ read: async () => parseAbout(await run4(ABOUT_RUN_MS))
133497
+ });
133498
+ }
133499
+
133150
133500
  // packages/agent-host/src/adapters/cursor-agent/index.ts
133151
133501
  var exec7 = promisify8(execFile9);
133152
133502
  var CAPABILITIES2 = {
@@ -133185,6 +133535,12 @@ function centraluToolOfCall(call2) {
133185
133535
  var STATUS_FOR_MS = 6e4;
133186
133536
  function cursorProfile() {
133187
133537
  let status = null;
133538
+ const localUsage = cursorUsageReader(async (timeout) => {
133539
+ const install = findCursor();
133540
+ if (!install) throw new Error("Cursor CLI not found");
133541
+ const r = await exec7(install.command, [...install.args, "about", "--format", "json"], { timeout, windowsHide: true, env: { ...process.env, ...install.env } });
133542
+ return r.stdout;
133543
+ });
133188
133544
  return {
133189
133545
  descriptor: DESCRIPTOR2,
133190
133546
  capabilities: CAPABILITIES2,
@@ -133197,6 +133553,7 @@ function cursorProfile() {
133197
133553
  return findCursor()?.version ?? null;
133198
133554
  },
133199
133555
  centraluToolOf: centraluToolOfCall,
133556
+ localUsage,
133200
133557
  async detect() {
133201
133558
  if (status && Date.now() - status.at < STATUS_FOR_MS) return status.result;
133202
133559
  const install = findCursor();
@@ -133560,6 +133917,75 @@ var OPENCODE_SURFACE = {
133560
133917
  baseline: SURFACE_BASELINE4
133561
133918
  };
133562
133919
 
133920
+ // packages/agent-host/src/adapters/opencode/usage.ts
133921
+ var ROWS4 = [
133922
+ { match: "Sessions", id: "sessions", label: "Sessions", unit: "count" },
133923
+ { match: "Messages", id: "messages", label: "Messages", unit: "count" },
133924
+ { match: "Input", id: "input", label: "Input", unit: "tokens" },
133925
+ { match: "Output", id: "output", label: "Output", unit: "tokens" },
133926
+ { match: "Cache Read", id: "cache-read", label: "Cache read", unit: "tokens" },
133927
+ { match: "Cache Write", id: "cache-write", label: "Cache write", unit: "tokens" },
133928
+ { match: "Total Cost", id: "cost", label: "Cost", unit: "usd" }
133929
+ ];
133930
+ function parseAmount(text2) {
133931
+ const m = /^\$?\s*([\d,]*\.?\d+)\s*([KMB])?$/i.exec(text2.trim());
133932
+ const digits = m?.[1];
133933
+ if (!digits) return null;
133934
+ const n = Number(digits.replace(/,/g, ""));
133935
+ if (!Number.isFinite(n)) return null;
133936
+ const k2 = SUFFIX.get((m[2] ?? "").toUpperCase()) ?? 1;
133937
+ return Math.round(n * k2 * 100) / 100;
133938
+ }
133939
+ var GROUPED = /^\d{1,3}([.,\u00A0\u202F '’])\d{3}(?:\1\d{3})*$/;
133940
+ var LAKH = /^\d{1,2}(?:,\d{2})+,\d{3}$/;
133941
+ function parseCount(text2) {
133942
+ const t = text2.trim();
133943
+ if (!(/^\d+$/.test(t) || GROUPED.test(t) || LAKH.test(t))) return null;
133944
+ const n = Number(t.replace(/\D/g, ""));
133945
+ return Number.isSafeInteger(n) ? n : null;
133946
+ }
133947
+ var SUFFIX = /* @__PURE__ */ new Map([
133948
+ ["K", 1e3],
133949
+ ["M", 1e6],
133950
+ ["B", 1e9]
133951
+ ]);
133952
+ var ANSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
133953
+ function parseStats(stdout) {
133954
+ const found = /* @__PURE__ */ new Map();
133955
+ for (const raw of stdout.replace(ANSI, "").split(/\r?\n/)) {
133956
+ const line = raw.replace(/^[│|]\s*/, "").replace(/\s*[│|]$/, "").trim();
133957
+ const cells = /^(\S.*?)\s{2,}(\S.*)$/.exec(line);
133958
+ const row = ROWS4.find((r) => r.match === cells?.[1]);
133959
+ if (!row || !cells?.[2] || found.has(row.id)) continue;
133960
+ found.set(row.id, row.unit === "count" ? parseCount(cells[2]) : parseAmount(cells[2]));
133961
+ }
133962
+ const rows3 = ROWS4.flatMap((r) => {
133963
+ const value = found.get(r.id);
133964
+ return value === void 0 ? [] : [{ id: r.id, label: r.label, value, unit: r.unit }];
133965
+ });
133966
+ return { plan: null, ...rows3.length > 0 ? { totals: { scope: "all time", rows: rows3 } } : {} };
133967
+ }
133968
+ var NOTHING2 = { plan: null };
133969
+ var MIN_FRESH_MS = 6e4;
133970
+ var FRESH_PER_RUN = 10;
133971
+ var FAIL_MS2 = 6e4;
133972
+ var SOFT_MS2 = 3e3;
133973
+ var STATS_RUN_MS = 5 * 6e4;
133974
+ function openCodeUsageReader(run4, now = Date.now) {
133975
+ return localUsageReader({
133976
+ freshMs: (_v2, tookMs) => Math.max(MIN_FRESH_MS, tookMs * FRESH_PER_RUN),
133977
+ failMs: FAIL_MS2,
133978
+ softMs: SOFT_MS2,
133979
+ now,
133980
+ fallback: () => NOTHING2,
133981
+ onFail: (e) => console.error(`[usage] opencode: could not read the totals: ${e.message}`),
133982
+ read: async () => {
133983
+ const read = parseStats(await run4(STATS_RUN_MS));
133984
+ return read.totals ? { ...read, totals: { ...read.totals, asOf: now() } } : read;
133985
+ }
133986
+ });
133987
+ }
133988
+
133563
133989
  // packages/agent-host/src/adapters/opencode/index.ts
133564
133990
  var exec9 = promisify10(execFile11);
133565
133991
  var CAPABILITIES3 = {
@@ -133592,6 +134018,12 @@ var DESCRIPTOR3 = {
133592
134018
  var VERSION_FOR_MS = 6e4;
133593
134019
  function openCodeProfile() {
133594
134020
  let known = null;
134021
+ const localUsage = openCodeUsageReader(async (timeout) => {
134022
+ const l = findOpenCode();
134023
+ if (!l) throw new Error("OpenCode not found");
134024
+ const r = await exec9(l.command, [...l.args, "stats"], { timeout, windowsHide: true, maxBuffer: 1024 * 1024 });
134025
+ return r.stdout;
134026
+ });
133595
134027
  const version2 = async () => {
133596
134028
  if (known && Date.now() - known.at < VERSION_FOR_MS) return known.version;
133597
134029
  const l = findOpenCode();
@@ -133604,6 +134036,7 @@ function openCodeProfile() {
133604
134036
  capabilities: CAPABILITIES3,
133605
134037
  surface: OPENCODE_SURFACE,
133606
134038
  infersCompaction: true,
134039
+ localUsage,
133607
134040
  async launch(opts) {
133608
134041
  const l = findOpenCode();
133609
134042
  return l ? { command: l.command, args: [...l.args, "acp"], env: presetEnv(opts.permissionPreset, process.env, readOpenCodeConfigs(opts.cwd, process.env)) } : null;
@@ -135577,8 +136010,8 @@ var AppRunRecords = class {
135577
136010
  /**
135578
136011
  * Closes out runs that never saw an end (once, at startup). When the host dies, the app
135579
136012
  * process's input also closes and it ends — a row left at `running` would look like it is
135580
- * running forever. The same reasoning as fixing session state at startup (`manager.ts`'s
135581
- * LIVE_ONLY): a "live" state is only true while a process backs it.
136013
+ * running forever. The same reasoning as fixing session state at startup (`manager.ts`, by
136014
+ * `turnCut`): a "live" state is only true while a process backs it.
135582
136015
  */
135583
136016
  settleUnfinishedAppRuns(error51) {
135584
136017
  return this.db.prepare(`UPDATE app_runs SET status = 'error', error = ? WHERE status = 'running'`).run(error51).changes;
@@ -138710,6 +139143,28 @@ var StepsV62 = class extends MigrationRange {
138710
139143
  this.db.exec(`ALTER TABLE usage_buckets ADD COLUMN rebuild_tokens INTEGER NOT NULL DEFAULT 0`);
138711
139144
  }
138712
139145
  }
139146
+ },
139147
+ {
139148
+ to: 64,
139149
+ breaksOlderReaders: false,
139150
+ /**
139151
+ * The last good totals of a tool that reports no limits (#1623), `usage_local_totals`: one row per tool with the JSON of
139152
+ * its `UsageSnapshot.totals` and `at`, when they were read (ms). A tool whose command reads a large database takes a
139153
+ * minute to count, so after a restart the usage panel shows these at once, with the time they were read, while a fresh
139154
+ * read goes on. A row is replaced by each newer read. Counts only, no content; local to this computer.
139155
+ *
139156
+ * **Expand only (#292's rule).** A new table that an older host neither reads nor writes. It goes away when the totals
139157
+ * are not needed any more, in a contract step one release after the code stops using it.
139158
+ */
139159
+ run: () => {
139160
+ this.db.exec(`
139161
+ CREATE TABLE IF NOT EXISTS usage_local_totals (
139162
+ tool TEXT PRIMARY KEY,
139163
+ at INTEGER NOT NULL,
139164
+ totals TEXT NOT NULL
139165
+ );
139166
+ `);
139167
+ }
138713
139168
  }
138714
139169
  ];
138715
139170
  }
@@ -139744,6 +140199,116 @@ var Store = class extends MigrationRunner {
139744
140199
  };
139745
140200
  var WAL_SIZE_LIMIT = 64 * 1024 * 1024;
139746
140201
 
140202
+ // packages/agent-host/src/lifecycle/update-resume.ts
140203
+ var UPDATE_RESUME_PREFIX = "update-resume.";
140204
+ var MAX_TURNS = 1e3;
140205
+ var ID3 = /^[0-9A-Za-z_.:-]{1,200}$/;
140206
+ var READ_TRIES = 5;
140207
+ var READ_RETRY_MS = 200;
140208
+ function sleepSync(ms3) {
140209
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms3);
140210
+ }
140211
+ function readHeld(folder, name, wait) {
140212
+ for (let attempt = 1; ; attempt++) {
140213
+ try {
140214
+ return folder.read(name);
140215
+ } catch (err) {
140216
+ const busy = err instanceof Error && "code" in err && err.code === "EBUSY";
140217
+ if (!busy || attempt >= READ_TRIES) throw err;
140218
+ console.error(`[agent-host] ${name} is held by another process (${errorMessage(err)}); reading it again (${attempt}/${READ_TRIES})`);
140219
+ wait(READ_RETRY_MS);
140220
+ }
140221
+ }
140222
+ }
140223
+ function parseUpdateResume(text2) {
140224
+ let o;
140225
+ try {
140226
+ o = JSON.parse(text2);
140227
+ } catch {
140228
+ return "torn";
140229
+ }
140230
+ if (typeof o !== "object" || o === null || !("v" in o) || o.v !== 1 || !("turns" in o) || !Array.isArray(o.turns)) return "other";
140231
+ const turns = [];
140232
+ const listed2 = o.turns;
140233
+ for (const t of listed2) {
140234
+ if (typeof t !== "object" || t === null || !("id" in t) || !("seq" in t)) continue;
140235
+ if (typeof t.id === "string" && ID3.test(t.id) && typeof t.seq === "number" && Number.isSafeInteger(t.seq) && t.seq >= 0) turns.push({ id: t.id, seq: t.seq });
140236
+ }
140237
+ return turns.slice(0, MAX_TURNS);
140238
+ }
140239
+ function recordUpdateCut(folder, cut2) {
140240
+ const turns = cut2.filter((t) => ID3.test(t.id)).slice(0, MAX_TURNS);
140241
+ if (!turns.length) return;
140242
+ try {
140243
+ folder.add(`${JSON.stringify({ v: 1, turns })}
140244
+ `);
140245
+ } catch (err) {
140246
+ console.error(`[agent-host] the turns this update ends could not be listed for the next start (${errorMessage(err)}); they end as a stop's do`);
140247
+ }
140248
+ }
140249
+ var UpdateResumeLists = class {
140250
+ /**
140251
+ * `wait` sleeps between the tries at a list a scanner holds (`READ_TRIES`); the host's own start is one thread with nothing
140252
+ * yet to serve, so it blocks. Tests pass their own
140253
+ */
140254
+ constructor(folder, wait = sleepSync) {
140255
+ this.folder = folder;
140256
+ let names;
140257
+ try {
140258
+ names = folder.names();
140259
+ } catch (err) {
140260
+ console.error(`[agent-host] the turns an update ended could not be read (${errorMessage(err)}); none is continued`);
140261
+ return;
140262
+ }
140263
+ for (const name of names) {
140264
+ let text2;
140265
+ try {
140266
+ text2 = readHeld(folder, name, wait);
140267
+ } catch (err) {
140268
+ console.error(`[agent-host] ${name} could not be read (${errorMessage(err)}); the list is kept for a later start, and the turns it names end as a quit's do`);
140269
+ continue;
140270
+ }
140271
+ if (text2 === null) continue;
140272
+ const turns = parseUpdateResume(text2);
140273
+ if (turns === "other") continue;
140274
+ if (turns === "torn") this.removeList(name);
140275
+ else this.lists.set(name, turns);
140276
+ }
140277
+ }
140278
+ folder;
140279
+ /** Each list's name and the turns it names, as read at the start */
140280
+ lists = /* @__PURE__ */ new Map();
140281
+ dealt = /* @__PURE__ */ new Set();
140282
+ /** The sessions the lists name */
140283
+ ids() {
140284
+ return new Set([...this.lists.values()].flat().map((t) => t.id));
140285
+ }
140286
+ /** Whether a stop listed this session's turn as the one it cut: its conversation still ends where it ended then */
140287
+ cut(id3, lastSeq) {
140288
+ return [...this.lists.values()].some((turns) => turns.some((t) => t.id === id3 && t.seq === lastSeq));
140289
+ }
140290
+ /**
140291
+ * One listed session dealt with: continued, reported as lost, or not found cut. Each list all of whose sessions are
140292
+ * dealt with goes. One that cannot be removed is said in the log and stays harmless: the turn it names is over, so
140293
+ * its mark no longer matches, and a later start removes it
140294
+ */
140295
+ done(id3) {
140296
+ this.dealt.add(id3);
140297
+ for (const [name, turns] of this.lists) {
140298
+ if (!turns.some((t) => t.id === id3) || !turns.every((t) => this.dealt.has(t.id))) continue;
140299
+ this.lists.delete(name);
140300
+ this.removeList(name);
140301
+ }
140302
+ }
140303
+ removeList(name) {
140304
+ try {
140305
+ this.folder.remove(name);
140306
+ } catch (err) {
140307
+ console.error(`[agent-host] ${name} could not be removed (${errorMessage(err)}); the turns it names are over, and a later start removes it`);
140308
+ }
140309
+ }
140310
+ };
140311
+
139747
140312
  // packages/agent-host/src/sessions/card-rest.ts
139748
140313
  var CardRests = class {
139749
140314
  constructor(record2 = null, now = () => Date.now()) {
@@ -140614,7 +141179,7 @@ function pausedWords(tasks, sessionId, there = "") {
140614
141179
  }
140615
141180
 
140616
141181
  // packages/agent-host/src/sessions/handoff-flow.ts
140617
- import { randomUUID as randomUUID26 } from "node:crypto";
141182
+ import { randomUUID as randomUUID24 } from "node:crypto";
140618
141183
  var TIMINGS = { callMs: 24e4, noteMs: 10 * 6e4 };
140619
141184
  var HandoffFlow = class {
140620
141185
  constructor(host, timings = TIMINGS) {
@@ -140693,7 +141258,7 @@ var HandoffFlow = class {
140693
141258
  const again = this.refusal(callerId, now, mode);
140694
141259
  if (again) return this.refuse(callerId, now.id, again);
140695
141260
  if (always) this.always.add(callerId);
140696
- const run4 = { id: `ho-${randomUUID26().slice(0, 8)}`, callerId, targetId: now.id, done: Promise.resolve({ ok: false, error: "" }) };
141261
+ const run4 = { id: `ho-${randomUUID24().slice(0, 8)}`, callerId, targetId: now.id, done: Promise.resolve({ ok: false, error: "" }) };
140697
141262
  this.running.set(now.id, run4);
140698
141263
  this.runs.set(run4.id, run4);
140699
141264
  if (viaAuto && !this.host.autoBoundaryOpen(callerId, `${SESSION_HANDOFF} of ${now.id} (${mode}, to ${tool})`)) {
@@ -140829,7 +141394,7 @@ var HandoffFlow = class {
140829
141394
 
140830
141395
  // packages/agent-host/src/sessions/host-cards.ts
140831
141396
  import { AsyncLocalStorage } from "node:async_hooks";
140832
- import { randomUUID as randomUUID27 } from "node:crypto";
141397
+ import { randomUUID as randomUUID25 } from "node:crypto";
140833
141398
  var HostCards = class {
140834
141399
  constructor(host) {
140835
141400
  this.host = host;
@@ -140893,7 +141458,7 @@ var HostCards = class {
140893
141458
  if (!card.alwaysAsk && this.host.autoBoundaryOpen(sessionId, card.log ?? `${detail.kind}${note}`)) return Promise.resolve("allow");
140894
141459
  return new Promise((resolve21) => {
140895
141460
  if (signal?.aborted) return resolve21(null);
140896
- const requestId = card.requestId ?? `${detail.kind === "project_access" ? "xp" : detail.kind === "agent_start" ? "ag" : detail.kind === "centralu_update" || isCentraluApply(detail) ? "up" : "mk"}-${randomUUID27()}`;
141461
+ const requestId = card.requestId ?? `${detail.kind === "project_access" ? "xp" : detail.kind === "agent_start" ? "ag" : detail.kind === "centralu_update" || isCentraluApply(detail) ? "up" : "mk"}-${randomUUID25()}`;
140897
141462
  const ask = { requestId, sessionId, detail, shown: card.shown ?? false, resolve: resolve21 };
140898
141463
  this.hostAsks.set(requestId, ask);
140899
141464
  signal?.addEventListener("abort", () => this.withdrawHostAsk(requestId), { once: true });
@@ -140954,7 +141519,7 @@ var HostCards = class {
140954
141519
  if (signal.aborted) return resolve21(null);
140955
141520
  const app2 = { appId: q2.app.appId, projectId: q2.app.projectId, name: q2.appName };
140956
141521
  if (q2.origin.kind === "session" && this.host.meta.has(q2.origin.sessionId)) {
140957
- const requestId = `cap-${randomUUID27()}`;
141522
+ const requestId = `cap-${randomUUID25()}`;
140958
141523
  const ask = {
140959
141524
  requestId,
140960
141525
  sessionId: q2.origin.sessionId,
@@ -140968,7 +141533,7 @@ var HostCards = class {
140968
141533
  return;
140969
141534
  }
140970
141535
  const origin = q2.origin.kind === "view" ? q2.origin.app : q2.app;
140971
- const id3 = `q-${randomUUID27()}`;
141536
+ const id3 = `q-${randomUUID25()}`;
140972
141537
  this.appQuestions.set(id3, {
140973
141538
  question: { id: id3, app: app2, capability: q2.capability, text: q2.text, origin: { appId: origin.appId, projectId: origin.projectId }, askedAt: q2.askedAt, expiresAt: q2.expiresAt },
140974
141539
  resolve: resolve21
@@ -141038,7 +141603,7 @@ import { posix as posix4, sep as sep10, win32 as win328 } from "node:path";
141038
141603
  import { fileURLToPath as fileURLToPath5 } from "node:url";
141039
141604
  var OPEN_FLAGS = constants8.O_RDONLY | constants8.O_NONBLOCK | (constants8.O_NOFOLLOW ?? 0);
141040
141605
  var MESSAGE_IMAGE_KINDS = "PNG, JPEG, GIF or WebP";
141041
- function sniffImage(b2) {
141606
+ function sniffImage2(b2) {
141042
141607
  const at2 = (i, ...bytes2) => bytes2.every((x, k2) => b2[i + k2] === x);
141043
141608
  if (b2.length >= 8 && at2(0, 137, 80, 78, 71, 13, 10, 26, 10)) return "image/png";
141044
141609
  if (b2.length >= 3 && at2(0, 255, 216, 255)) return "image/jpeg";
@@ -141126,7 +141691,7 @@ async function readMessageImage(abs, max = IMAGE_PREVIEW_MAX_BYTES) {
141126
141691
  }
141127
141692
  if (total > max) return refuse6("too_large", `The image is over ${mb3(max)}; a reply shows images up to ${mb3(max)}`, file2);
141128
141693
  const bytes2 = buf.subarray(0, total);
141129
- const mime = sniffImage(bytes2);
141694
+ const mime = sniffImage2(bytes2);
141130
141695
  if (!mime) return refuse6("not_an_image", `${file2} is not a ${MESSAGE_IMAGE_KINDS} image`, file2);
141131
141696
  return { ok: true, mime, data: bytes2.toString("base64"), file: file2 };
141132
141697
  } finally {
@@ -141249,9 +141814,10 @@ var SessionRecord = class {
141249
141814
  return run4.seq;
141250
141815
  }
141251
141816
  if (run4) this.closeStream(m.id);
141252
- if (!text2) return null;
141817
+ const first = streamKind === "reasoning" ? text2.replace(/^\s+/, "") : text2;
141818
+ if (!first) return null;
141253
141819
  const seq2 = this.host.store.messages.nextSeq(m.id);
141254
- const fresh = { seq: seq2, kind: streamKind, payload: { ...e }, text: text2, written: 0, lastWrite: 0 };
141820
+ const fresh = { seq: seq2, kind: streamKind, payload: { ...e }, text: first, written: 0, lastWrite: 0 };
141255
141821
  this.streams.set(m.id, fresh);
141256
141822
  this.flushStream(m.id, fresh);
141257
141823
  m.lastSeq = seq2;
@@ -141372,13 +141938,14 @@ var SessionRecord = class {
141372
141938
  persistImage(e, m) {
141373
141939
  const ts = Date.now();
141374
141940
  const seq = this.host.store.messages.nextSeq(m.id);
141941
+ const name = e.path;
141375
141942
  const row = (path2, note) => ({
141376
141943
  sessionId: m.id,
141377
141944
  seq,
141378
141945
  role: "system",
141379
141946
  kind: "image",
141380
141947
  ts,
141381
- payload: { type: "message_image", sessionId: m.id, mime: e.mime, data: "", path: path2, note }
141948
+ payload: { type: "message_image", sessionId: m.id, mime: e.mime, data: "", path: path2, note, ...name ? { name } : {} }
141382
141949
  });
141383
141950
  this.host.store.messages.appendMessages([row(e.path, e.note)]);
141384
141951
  m.lastSeq = seq;
@@ -141393,8 +141960,8 @@ var SessionRecord = class {
141393
141960
  stored = (await saveAttachment(m.id, "agent-image", e.mime, e.data)).path;
141394
141961
  void sweepAttachments().catch(() => {
141395
141962
  });
141396
- } catch (err) {
141397
- note = `Could not save the image: ${err.message}`;
141963
+ } catch {
141964
+ note = "a copy could not be kept";
141398
141965
  }
141399
141966
  try {
141400
141967
  this.host.store.messages.upsertMessageNoIndex(row(stored ?? e.path, note));
@@ -141443,7 +142010,7 @@ var SessionRecord = class {
141443
142010
  const data = (await readFile9(p.path)).toString("base64");
141444
142011
  return { ...r, payload: { ...p, data } };
141445
142012
  } catch {
141446
- return { ...r, payload: { ...p, note: "The image was cleared and no longer exists (total-size cap)" } };
142013
+ return { ...r, payload: { ...p, note: "the saved copy was cleared (total-size cap)" } };
141447
142014
  }
141448
142015
  }
141449
142016
  if (r.kind === "text" && r.role === "user") {
@@ -141470,6 +142037,9 @@ var SessionRecord = class {
141470
142037
  // packages/agent-host/src/sessions/tool-queries.ts
141471
142038
  var MODEL_LIST_READ_MS = 6e4;
141472
142039
  var MODEL_LIST_RETRY_MS = 6e4;
142040
+ function noLimitsReason(label) {
142041
+ return `${label} does not report usage limits`;
142042
+ }
141473
142043
  var ToolQueries = class {
141474
142044
  constructor(host) {
141475
142045
  this.host = host;
@@ -141524,7 +142094,22 @@ var ToolQueries = class {
141524
142094
  async usageFor(tool, opts = {}) {
141525
142095
  const cache2 = this.host.cacheLedger.stats(tool, (id3) => this.host.meta.get(id3)?.name);
141526
142096
  const adapter = this.host.adapters.get(tool);
141527
- if (!adapter?.listUsage) return { supported: false, reason: `${tool} does not support usage queries`, usage: null, cache: cache2 };
142097
+ const label = adapter?.descriptor.label ?? tool;
142098
+ if (!adapter) return { supported: false, reason: `${tool} does not support usage queries`, usage: null, cache: cache2 };
142099
+ if (!adapter.listUsage) {
142100
+ const none = { supported: false, reason: noLimitsReason(label), notReported: true, usage: null, cache: cache2 };
142101
+ if (!adapter.localUsage || opts.limits === false) return none;
142102
+ try {
142103
+ const local = await adapter.localUsage({ settle: opts.settle, seed: this.localTotalsSeed(tool) });
142104
+ this.keepLocalTotals(tool, local);
142105
+ if (local.loggedOut) return { supported: false, reason: `${label} is not logged in on this computer`, usage: null, cache: cache2 };
142106
+ const usage = { plan: local.plan, windows: [], daily: [], ...local.totals ? { totals: local.totals } : {} };
142107
+ return { ...none, usage, ...local.provisional ? { provisional: true } : {} };
142108
+ } catch (err) {
142109
+ console.error(`[usage] ${tool}: the tool's own usage could not be read: ${errorMessage(err)}`);
142110
+ return none;
142111
+ }
142112
+ }
141528
142113
  if (opts.limits === false) return { supported: true, usage: null, cache: cache2 };
141529
142114
  const hit = this.usageCache.get(tool);
141530
142115
  if (hit && Date.now() - hit.at < 6e4) return { supported: true, usage: hit.snapshot, cache: cache2 };
@@ -141537,6 +142122,38 @@ var ToolQueries = class {
141537
142122
  return { supported: false, reason: err.message, usage: hit?.snapshot ?? null, cache: cache2 };
141538
142123
  }
141539
142124
  }
142125
+ /** Per tool: the read time of the totals last kept, so a stale answer returned again is not written again */
142126
+ keptTotalsAt = /* @__PURE__ */ new Map();
142127
+ /** Tools whose kept totals were already offered to their reader: it keeps its own answer after that */
142128
+ seededTotals = /* @__PURE__ */ new Set();
142129
+ /**
142130
+ * The totals the last process kept for a tool (v64), offered once to its reader, which stands on them as on a stale answer
142131
+ * until its own read ends. After a restart a panel then shows the totals at once, with the time they were read.
142132
+ */
142133
+ localTotalsSeed(tool) {
142134
+ if (this.seededTotals.has(tool)) return void 0;
142135
+ this.seededTotals.add(tool);
142136
+ try {
142137
+ const totals = this.host.store.usage.localTotals(tool);
142138
+ if (!totals) return void 0;
142139
+ if (totals.asOf !== void 0) this.keptTotalsAt.set(tool, totals.asOf);
142140
+ return { plan: null, totals };
142141
+ } catch (err) {
142142
+ console.error(`[usage] ${tool}: the kept totals could not be read: ${errorMessage(err)}`);
142143
+ return void 0;
142144
+ }
142145
+ }
142146
+ /** Keeps totals a read produced (not a stale answer returned again, nor a provisional one), for the next process */
142147
+ keepLocalTotals(tool, local) {
142148
+ const totals = local.totals;
142149
+ if (!totals || totals.asOf === void 0 || local.provisional || this.keptTotalsAt.get(tool) === totals.asOf) return;
142150
+ this.keptTotalsAt.set(tool, totals.asOf);
142151
+ try {
142152
+ this.host.store.usage.saveLocalTotals(tool, totals);
142153
+ } catch (err) {
142154
+ console.error(`[usage] ${tool}: the totals could not be kept: ${errorMessage(err)}`);
142155
+ }
142156
+ }
141540
142157
  /**
141541
142158
  * The models that can be picked, and each model's reasoning effort levels.
141542
142159
  *
@@ -142954,11 +143571,15 @@ var SessionManager = class {
142954
143571
  this.emit = (full) => {
142955
143572
  rawEmit(withoutToolRecord(full));
142956
143573
  };
142957
- const LIVE_ONLY = ["working", "waiting_approval"];
142958
143574
  const stoppedByQuit = [];
143575
+ const cutByUpdate = [];
143576
+ const lists = keeperOpts.updateResume ? new UpdateResumeLists(keeperOpts.updateResume) : null;
143577
+ this.updateLists = lists;
142959
143578
  for (const s of store2.sessions.listSessions()) {
142960
- const stale = LIVE_ONLY.includes(s.state) && !keeperOpts.keptSessions?.has(s.id);
142961
- const fixed = stale ? { ...s, state: "idle", waitingSince: null } : s;
143579
+ const cut2 = turnCut(s) && !keeperOpts.keptSessions?.has(s.id);
143580
+ const goesOn = cut2 && !!lists?.cut(s.id, s.lastSeq);
143581
+ const stale = cut2 && !goesOn;
143582
+ const fixed = stale ? { ...s, state: "idle", waitingSince: null } : goesOn ? { ...s, state: "working", waitingSince: null } : s;
142962
143583
  this.meta.set(s.id, withHubMark(store2.settings, fixed));
142963
143584
  if (stale) {
142964
143585
  console.error(`[agent-host] stale state reset: ${s.id.slice(0, 8)} ${s.state} -> idle`);
@@ -142967,27 +143588,39 @@ var SessionManager = class {
142967
143588
  this.children.processGone(s.id);
142968
143589
  stoppedByQuit.push(fixed);
142969
143590
  }
143591
+ if (goesOn) {
143592
+ console.error(`[agent-host] ${s.id.slice(0, 8)}: an update of this machine ended its turn; continuing it`);
143593
+ store2.sessions.upsertSession(fixed);
143594
+ cutByUpdate.push(fixed);
143595
+ this.restartPolicy.continuingTurns.add(s.id);
143596
+ this.continuedAfterUpdate.push({ sessionId: s.id, at: Date.now() });
143597
+ }
142970
143598
  }
143599
+ for (const id3 of lists?.ids() ?? []) if (!cutByUpdate.some((s) => s.id === id3)) this.updateResumeDone(id3);
143600
+ const goingOn = /* @__PURE__ */ new Set([...keeperOpts.keptSessions ?? [], ...cutByUpdate.map((s) => s.id)]);
142971
143601
  this.quitPersonWork = stoppedByQuit.map((s) => s.id);
142972
143602
  const marks = new Map(store2.children.listReportMarks().map((r) => [r.sessionId, r.callerSessionId]));
142973
143603
  const turnGivers = new Map(store2.children.listDelegatedTurns().map((r) => [r.sessionId, r.callerSessionId]));
143604
+ const requesterOf = (s) => [marks.get(s.id), turnGivers.get(s.id), s.askedBy].find((id3) => !!id3 && id3 !== s.id && this.meta.has(id3));
142974
143605
  for (const s of stoppedByQuit) {
142975
- const requester = [marks.get(s.id), turnGivers.get(s.id), s.askedBy].find((id3) => !!id3 && id3 !== s.id && this.meta.has(id3));
143606
+ const requester = requesterOf(s);
142976
143607
  if (requester) this.quitNotices.push({ sessionId: s.id, requester });
142977
143608
  }
143609
+ this.updateCuts = cutByUpdate.map((s) => ({ sessionId: s.id, requester: requesterOf(s) ?? null }));
142978
143610
  this.cardRests.restore((id3) => !!keeperOpts.keptSessions?.has(id3) && this.meta.get(id3)?.state === "waiting_approval");
142979
- for (const m of this.children.restoreReports(this.meta, keeperOpts.keptSessions)) this.quitNotices.push({ sessionId: m.sessionId, requester: m.callerSessionId, swapped: true });
143611
+ for (const m of this.children.restoreReports(this.meta, goingOn)) this.quitNotices.push({ sessionId: m.sessionId, requester: m.callerSessionId, swapped: true });
142980
143612
  this.delegationFlow.starts.restoreAgentStarts();
142981
143613
  this.delegationFlow.remote.restore();
142982
- this.children.restoreTurns(this.meta, keeperOpts.keptSessions);
142983
- this.keptAtStart = new Set(keeperOpts.keptSessions ?? []);
143614
+ this.children.restoreTurns(this.meta, goingOn);
143615
+ this.keptAtStart = goingOn;
142984
143616
  this.children.restoreQueued(this.meta, (r) => r.via === "ask_project" && !this.queue.pairConsented(r.callerSessionId, this.meta.get(r.sessionId)));
142985
143617
  if (this.children.queued.size) {
142986
143618
  setTimeout(() => {
142987
- for (const id3 of this.children.queued.keys()) if (!keeperOpts.keptSessions?.has(id3)) this.queue.scheduleDrain(id3);
143619
+ for (const id3 of this.children.queued.keys()) if (!goingOn.has(id3)) this.queue.scheduleDrain(id3);
142988
143620
  }, 0);
142989
143621
  }
142990
- if (this.quitNotices.length || this.quitPersonWork.length) setTimeout(() => this.tellQuitNotices((id3) => !keeperOpts.keptSessions?.has(id3)), 0);
143622
+ if (this.quitNotices.length || this.quitPersonWork.length) setTimeout(() => this.tellQuitNotices((id3) => !goingOn.has(id3)), 0);
143623
+ if (this.updateCuts.length) setTimeout(() => void this.continueCutTurns(), 0);
142991
143624
  void this.handoffNotes.sweepOrphanHandoffNotes().catch(() => {
142992
143625
  });
142993
143626
  this.recoveryRepair = repairWorktreeRecoveryRemovals((id3) => store2.trash.isTrashed(id3)).catch((e) => console.error(`[archive] recovery folders left by an interrupted purge were not repaired: ${e.message}`));
@@ -143163,6 +143796,18 @@ var SessionManager = class {
143163
143796
  quitNotices = [];
143164
143797
  /** Sessions this start found mid-turn with no process: the person is told of the ones whose work is theirs (§8.1), once the mailbox is wired */
143165
143798
  quitPersonWork = [];
143799
+ /** Turns a hub's update cut, to continue once the start is done, and who to tell (#1618, `continueCutTurns`) */
143800
+ updateCuts = [];
143801
+ /**
143802
+ * The turns this host continues because an update cut them, and when its start took them up (#1618): said in every hello,
143803
+ * for a hub's callers. Taken at the start, before this host answers any hello, so a hub that connects while a Continue is
143804
+ * still on its way is told of it too (#1626 review); one that turns out lost leaves it (`turnLostInSwap`)
143805
+ */
143806
+ continuedAfterUpdate = [];
143807
+ /** The lists of the turns an update cut that the stops before this start left (#1618), each removed once dealt with */
143808
+ updateLists = null;
143809
+ /** A linked hub stops this host for an update (`host.stop { forUpdate }`): the turns its stop ends are listed (#1618) */
143810
+ stoppingForUpdate = false;
143166
143811
  /** `disposeAll` has begun (#673): a card it withdraws is not the person's answer, and its request stays stored */
143167
143812
  disposing = false;
143168
143813
  /**
@@ -143684,6 +144329,73 @@ var SessionManager = class {
143684
144329
  );
143685
144330
  this.tellQuitNotices(() => true);
143686
144331
  }
144332
+ /**
144333
+ * Continues each turn a linked hub's update cut (#1618, owner 2026-10-26): the update no longer waits for the turns on a
144334
+ * machine, so the host it starts takes them up again, once. The session was left `working` by the start; it is woken
144335
+ * and given Centralu's note through Centralu's own channel (`deliverMail`: no message of the person's, so the turn stays
144336
+ * whose it was and its report, if one is owed, is still that turn's end). Its requester here is told that the turn was
144337
+ * cut and goes on, unless it is one of the turns continued, whose own note covers its children, as a quit's rule has it
144338
+ * (`tellAskerOfInterruption`). A session that cannot be continued (its tool cannot resume it, it is gone there) ends
144339
+ * as a kept process that could not be re-attached does (`turnLostInSwap`): idle, marked stopped, its report or its
144340
+ * requester told. Each is taken once: the list is emptied first, and its id leaves the file once its Continue was sent
144341
+ * or its loss reported (`updateResumeDone`), not before, so a host stopped in between leaves it for the next.
144342
+ */
144343
+ async continueCutTurns() {
144344
+ const cuts = this.updateCuts;
144345
+ this.updateCuts = [];
144346
+ const continued = new Set(cuts.map((c) => c.sessionId));
144347
+ await Promise.all(
144348
+ cuts.map(async ({ sessionId, requester }) => {
144349
+ const m = this.meta.get(sessionId);
144350
+ if (this.disposing || this.handingOver) return;
144351
+ if (!m || !inTurn(m.state)) {
144352
+ this.restartPolicy.continuingTurns.delete(sessionId);
144353
+ this.notContinued(sessionId);
144354
+ this.updateResumeDone(sessionId);
144355
+ return;
144356
+ }
144357
+ try {
144358
+ await this.events.deliverMail(
144359
+ sessionId,
144360
+ { text: UPDATE_CONTINUE_TEXT, quiet: false, line: UPDATE_CONTINUE_LINE, label: "update", skipLine: () => this.disposing && this.stoppingForUpdate },
144361
+ { leaving: () => this.handingOver || this.disposing, atRest: () => true }
144362
+ );
144363
+ } catch (e) {
144364
+ if (this.disposing || this.handingOver || this.meta.get(sessionId) !== m) return;
144365
+ this.restartPolicy.continuingTurns.delete(sessionId);
144366
+ console.error(`[agent-host] ${sessionId.slice(0, 8)}: the turn an update ended could not be continued (${errorMessage(e)})`);
144367
+ this.turnLostInSwap(m);
144368
+ this.notContinued(sessionId);
144369
+ this.updateResumeDone(sessionId);
144370
+ return;
144371
+ }
144372
+ if (this.disposing || this.handingOver) return;
144373
+ this.restartPolicy.continuingTurns.delete(sessionId);
144374
+ this.updateResumeDone(sessionId);
144375
+ if (requester && !continued.has(requester)) void this.reports.tellCutByUpdate(sessionId, requester);
144376
+ })
144377
+ );
144378
+ this.tellQuitNotices(() => true);
144379
+ }
144380
+ /** One turn the lists named is dealt with: a list goes once all its turns are (#1618, `UpdateResumeLists.done`) */
144381
+ updateResumeDone(sessionId) {
144382
+ this.updateLists?.done(sessionId);
144383
+ }
144384
+ /** A turn the start took up that is not continued after all: the hello stops naming it (#1618) */
144385
+ notContinued(sessionId) {
144386
+ this.continuedAfterUpdate = this.continuedAfterUpdate.filter((c) => c.sessionId !== sessionId);
144387
+ }
144388
+ /**
144389
+ * A linked hub stops this host to update it, roll it back or put a version back (`host.stop { forUpdate }`, #1618): as the
144390
+ * sessions are let go of (`disposeAll`), the ones in a turn are listed for the host that starts next, which continues them
144391
+ */
144392
+ stopForUpdate() {
144393
+ this.stoppingForUpdate = true;
144394
+ }
144395
+ /** The turns this host continues after an update cut them (#1618), for the hello a linked hub reads */
144396
+ updateContinued() {
144397
+ return [...this.continuedAfterUpdate];
144398
+ }
143687
144399
  /** A turn a quit cut in a session whose work the person owns: reported to them. */
143688
144400
  tellPersonOfInterruption(sessionId) {
143689
144401
  if (!this.meta.has(sessionId)) return;
@@ -145100,10 +145812,14 @@ var SessionManager = class {
145100
145812
  async disposeEverything() {
145101
145813
  this.disposing = true;
145102
145814
  this.leaveHost("Centralu is shutting down");
145815
+ const cut2 = this.stoppingForUpdate && this.keeperOpts.updateResume ? cutTurns({ sessions: this.listSessions(), terminals: 0, commandRuns: 0 }) : [];
145103
145816
  for (const [id3, run4] of this.record.streams) this.record.flushStream(id3, run4);
145104
145817
  await Promise.allSettled([...[...this.handles.values()].map((h) => h.dispose()), ...this.merges.mergeChecks]);
145105
145818
  await Promise.allSettled([...this.adapters.values()].map((a) => a.settle?.()));
145106
145819
  for (const id3 of [...this.record.streams.keys()]) this.record.closeStream(id3);
145820
+ if (cut2.length && this.keeperOpts.updateResume) {
145821
+ recordUpdateCut(this.keeperOpts.updateResume, cut2.map((id3) => ({ id: id3, seq: this.store.messages.nextSeq(id3) - 1 })));
145822
+ }
145107
145823
  for (const id3 of this.handles.keys()) this.externals.stampExternalSynced(id3);
145108
145824
  this.handles.clear();
145109
145825
  }
@@ -148499,8 +149215,8 @@ function createRpcHandler(mgr2, adapters2, { settleDeletedProjects, deleteOutloo
148499
149215
  },
148500
149216
  "agents.commands": async (p) => mgr2.listCommands(RpcMethods["agents.commands"].params.parse(p).sessionId),
148501
149217
  "agents.usage": async (p) => {
148502
- const { tool, limits } = RpcMethods["agents.usage"].params.parse(p);
148503
- return mgr2.usageFor(tool, { limits });
149218
+ const { tool, limits, settle } = RpcMethods["agents.usage"].params.parse(p);
149219
+ return mgr2.usageFor(tool, { limits, settle });
148504
149220
  },
148505
149221
  "usage.history": async (p) => mgr2.usageHistory(RpcMethods["usage.history"].params.parse(p)),
148506
149222
  "agents.account": async (p) => mgr2.accountFor(RpcMethods["agents.account"].params.parse(p).tool),
@@ -148752,14 +149468,14 @@ function createRpcHandler(mgr2, adapters2, { settleDeletedProjects, deleteOutloo
148752
149468
  if (!stopHost) {
148753
149469
  throw rpcError("This host runs under the Centralu app, which stops it: quit the app instead");
148754
149470
  }
148755
- const { ifIdle } = RpcMethods["host.stop"].params.parse(p ?? {});
149471
+ const { ifIdle, forUpdate } = RpcMethods["host.stop"].params.parse(p ?? {});
148756
149472
  if (ifIdle) {
148757
149473
  if (!stopGate) throw rpcError("This host cannot tell whether it is idle");
148758
149474
  const running2 = stopGate.busy();
148759
149475
  if (running2) return { ok: false, ...running2 };
148760
149476
  stopGate.refuse();
148761
149477
  }
148762
- setTimeout(stopHost, 50);
149478
+ setTimeout(() => stopHost({ forUpdate: forUpdate === true }), 50);
148763
149479
  return { ok: true };
148764
149480
  },
148765
149481
  "host.handover": async (p) => {
@@ -148832,7 +149548,7 @@ function onExternalAppListChanged(apps, emit) {
148832
149548
  }
148833
149549
 
148834
149550
  // packages/agent-host/src/update-apply.ts
148835
- import { randomUUID as randomUUID28 } from "node:crypto";
149551
+ import { randomUUID as randomUUID26 } from "node:crypto";
148836
149552
  var ANSWER_MS = 5e3;
148837
149553
  var OUTCOME_MS = 2e4;
148838
149554
  var GONE_GRACE_MS = 5e3;
@@ -148866,7 +149582,7 @@ var UpdateApplyBroker = class {
148866
149582
  /** Asks every window whether its "Apply now" could relaunch now; resolves with the first ready one */
148867
149583
  probe() {
148868
149584
  if (this.opts.windows() === 0) return Promise.resolve({ ready: false, error: NO_WINDOW });
148869
- const requestId = randomUUID28();
149585
+ const requestId = randomUUID26();
148870
149586
  return new Promise((resolve21) => {
148871
149587
  const p = {
148872
149588
  requestId,
@@ -148889,7 +149605,7 @@ var UpdateApplyBroker = class {
148889
149605
  if (!p || p.answered.has(client)) return { ticket: null };
148890
149606
  p.answered.add(client);
148891
149607
  if (a.ready) {
148892
- const ticket = randomUUID28();
149608
+ const ticket = randomUUID26();
148893
149609
  p.finish({ ready: true, requestId: a.requestId, ticket, client, ...a.version ? { version: a.version } : {} });
148894
149610
  return { ticket };
148895
149611
  }
@@ -150289,7 +151005,7 @@ var SessionSleeper = class {
150289
151005
  };
150290
151006
 
150291
151007
  // packages/agent-host/src/host-requests.ts
150292
- import { randomUUID as randomUUID29 } from "node:crypto";
151008
+ import { randomUUID as randomUUID27 } from "node:crypto";
150293
151009
 
150294
151010
  // packages/agent-host/src/host-request-targets.ts
150295
151011
  function asItRuns(method, params) {
@@ -150524,7 +151240,7 @@ var HostRequests = class {
150524
151240
  if (cannot) return Promise.resolve({ decision: null, outcome: cannot });
150525
151241
  const createdAt = this.now();
150526
151242
  const request = {
150527
- requestId: randomUUID29(),
151243
+ requestId: randomUUID27(),
150528
151244
  method,
150529
151245
  question,
150530
151246
  client: from.client === null ? null : quotedName2(from.client),
@@ -150844,7 +151560,8 @@ function wireEndings(o) {
150844
151560
  }
150845
151561
 
150846
151562
  // packages/agent-host/src/lifecycle/stopped-mark.ts
150847
- import { rmSync as rmSync7, writeFileSync as writeFileSync11 } from "node:fs";
151563
+ import { randomUUID as randomUUID28 } from "node:crypto";
151564
+ import { readdirSync as readdirSync15, readFileSync as readFileSync23, rmSync as rmSync7, writeFileSync as writeFileSync11 } from "node:fs";
150848
151565
  import { join as join56 } from "node:path";
150849
151566
  var STOPPED_MARK_FILE = "stopped.json";
150850
151567
  function clearStoppedMark(dataDir) {
@@ -150860,6 +151577,22 @@ function markQuit(dataDir, now = Date.now()) {
150860
151577
  } catch {
150861
151578
  }
150862
151579
  }
151580
+ function updateResumeFolder(dataDir) {
151581
+ const NAME3 = /^update-resume\.[0-9a-f-]{36}\.json$/;
151582
+ return {
151583
+ names: () => readdirSync15(dataDir).filter((n) => NAME3.test(n)),
151584
+ read: (name) => {
151585
+ try {
151586
+ return readFileSync23(join56(dataDir, name), "utf8");
151587
+ } catch (err) {
151588
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") return null;
151589
+ throw err;
151590
+ }
151591
+ },
151592
+ add: (text2) => writeFileSync11(join56(dataDir, `${UPDATE_RESUME_PREFIX}${randomUUID28()}.json`), text2, { flag: "wx" }),
151593
+ remove: (name) => rmSync7(join56(dataDir, name), { force: true })
151594
+ };
151595
+ }
150863
151596
 
150864
151597
  // packages/agent-host/src/swap-control.ts
150865
151598
  var KEEPS_AGENTS_ACROSS_SWAP = true;
@@ -150996,7 +151729,7 @@ async function runDrain(bound, deps) {
150996
151729
  }
150997
151730
 
150998
151731
  // packages/agent-host/src/links/install.ts
150999
- import { existsSync as existsSync27, readFileSync as readFileSync23 } from "node:fs";
151732
+ import { existsSync as existsSync27, readFileSync as readFileSync24 } from "node:fs";
151000
151733
  import { hostname as hostname4 } from "node:os";
151001
151734
  import { fileURLToPath as fileURLToPath6 } from "node:url";
151002
151735
  import { gzipSync } from "node:zlib";
@@ -151376,8 +152109,8 @@ function detachCommand(spec, from = null) {
151376
152109
  const name = from === null ? null : hubNameForRemote(from);
151377
152110
  return remoteCommand(spec, name ? `serve --detach --from ${name}` : "serve --detach");
151378
152111
  }
151379
- function stopCommand(spec) {
151380
- return remoteCommand(spec, "serve --stop", true);
152112
+ function stopCommand(spec, o) {
152113
+ return remoteCommand(spec, "serve --stop", o.forUpdate ? "update" : "stop");
151381
152114
  }
151382
152115
  function serveCommand(spec) {
151383
152116
  return remoteCommand(spec, "serve");
@@ -151385,15 +152118,15 @@ function serveCommand(spec) {
151385
152118
  function autostartCommand(spec, action) {
151386
152119
  return remoteCommand(spec, `serve --autostart ${action}`);
151387
152120
  }
151388
- function remoteCommand(spec, args, hubStop = false) {
152121
+ function remoteCommand(spec, args, hubStop = null) {
151389
152122
  const custom2 = spec.command?.trim() || null;
151390
152123
  if (spec.shell === "powershell") {
151391
152124
  if (custom2) return powershell(`${custom2} ${args}`);
151392
152125
  return powershell(
151393
- `${hubStop ? "$env:CENTRALU_STOP_BY = 'hub'; " : ""}${MANAGED_PS}; if (Test-Path $m) { & $m ${args} } elseif (Get-Command centralu.cmd -ErrorAction SilentlyContinue) { & centralu.cmd ${args} } else { $l = Join-Path $d 'bin\\centralu.cmd'; if (Test-Path $l) { & $l ${args} } else { '${NOT_FOUND2}' } }`
152126
+ `${hubStop ? "$env:CENTRALU_STOP_BY = 'hub'; " : ""}${hubStop === "update" ? "$env:CENTRALU_STOP_FOR = 'update'; " : ""}${MANAGED_PS}; if (Test-Path $m) { & $m ${args} } elseif (Get-Command centralu.cmd -ErrorAction SilentlyContinue) { & centralu.cmd ${args} } else { $l = Join-Path $d 'bin\\centralu.cmd'; if (Test-Path $l) { & $l ${args} } else { '${NOT_FOUND2}' } }`
151394
152127
  );
151395
152128
  }
151396
- const lookup = `${hubStop ? "CENTRALU_STOP_BY=hub; export CENTRALU_STOP_BY; " : ""}${MANAGED_SH}; if [ -x "$m" ]; then exec "$m" ${args}; elif command -v centralu >/dev/null 2>&1; then exec centralu ${args}; elif [ -x "$d/bin/centralu" ]; then exec "$d/bin/centralu" ${args}; else echo ${NOT_FOUND2}; exit 127; fi`;
152129
+ const lookup = `${hubStop ? "CENTRALU_STOP_BY=hub; export CENTRALU_STOP_BY; " : ""}${hubStop === "update" ? "CENTRALU_STOP_FOR=update; export CENTRALU_STOP_FOR; " : ""}${MANAGED_SH}; if [ -x "$m" ]; then exec "$m" ${args}; elif command -v centralu >/dev/null 2>&1; then exec centralu ${args}; elif [ -x "$d/bin/centralu" ]; then exec "$d/bin/centralu" ${args}; else echo ${NOT_FOUND2}; exit 127; fi`;
151397
152130
  if (spec.shell === "posix") return custom2 ? `${custom2} ${args}` : `sh -c '${lookup}'`;
151398
152131
  const distro = distroOf(spec);
151399
152132
  const script = custom2 ? `${custom2} ${args}` : `PATH=$(printf %s "$PATH" | tr : '\\n' | grep -v '^/mnt/' | paste -sd: -); ${lookup}`;
@@ -151681,7 +152414,7 @@ var SSH_STDIN_READY_MS = SSH_CONNECT_ALLOWANCE_MS + SSH_POWERSHELL_START_MS;
151681
152414
  var SSH_STDIN_ACK_MS = 3e4;
151682
152415
  var FORWARD_STDERR_KEEP = 16 * 1024;
151683
152416
  var CLIXML_MARK = /<\/?(?:Objs|Obj|S|MS|PR|TN|TNRef|Props|I32|I64|B|AV)[\s>/]|_x00[0-9A-F]{2}_/;
151684
- var ANSI = /\x1b\[[0-?]*[ -/]*[@-~]|\x1b[@-_]?/g;
152417
+ var ANSI2 = /\x1b\[[0-?]*[ -/]*[@-~]|\x1b[@-_]?/g;
151685
152418
  function powershellErrorLine(line) {
151686
152419
  const l = line.trim();
151687
152420
  if (l === "+" || l.startsWith("+ ")) return null;
@@ -151693,7 +152426,7 @@ function powershellErrorLine(line) {
151693
152426
  function clixmlErrorText(xml) {
151694
152427
  return [...xml.matchAll(/<S S="Error">([\s\S]*?)<\/S>/g)].map(
151695
152428
  (m) => m[1].replace(/_x([0-9A-Fa-f]{4})_/g, (_, hex3) => String.fromCharCode(parseInt(hex3, 16))).replace(/&(lt|gt|quot|apos|amp);/g, (_, e) => ({ lt: "<", gt: ">", quot: '"', apos: "'", amp: "&" })[e])
151696
- ).join("").replace(ANSI, "").split(/\r?\n/).flatMap((l) => {
152429
+ ).join("").replace(ANSI2, "").split(/\r?\n/).flatMap((l) => {
151697
152430
  const kept = powershellErrorLine(l);
151698
152431
  return kept === null ? [] : [kept];
151699
152432
  }).join("\n");
@@ -151721,7 +152454,7 @@ function withoutClixml(s) {
151721
152454
  text2 = end < 0 ? "" : text2.slice(end + "</Objs>".length);
151722
152455
  }
151723
152456
  parts.push(plainPart(text2));
151724
- return parts.join("").replace(ANSI, "").split(/\r?\n/);
152457
+ return parts.join("").replace(ANSI2, "").split(/\r?\n/);
151725
152458
  }
151726
152459
  function lastLine(s, ansi = ansiText) {
151727
152460
  const lines = withoutClixml(s).map((l) => l.trim()).filter((l) => l && !l.startsWith("**") && !/^Node\.js v\d/.test(l) && !/^at /.test(l));
@@ -152036,7 +152769,7 @@ async function installRemote(o) {
152036
152769
  function bundled(name, source) {
152037
152770
  for (const url2 of [new URL(`./${name}`, import.meta.url), new URL(source, import.meta.url)]) {
152038
152771
  const file2 = fileURLToPath6(url2);
152039
- if (existsSync27(file2)) return readFileSync23(file2, "utf8");
152772
+ if (existsSync27(file2)) return readFileSync24(file2, "utf8");
152040
152773
  }
152041
152774
  throw new Error(`${name} is missing from this host`);
152042
152775
  }
@@ -152620,7 +153353,7 @@ var RemoteClient = class {
152620
153353
  this.cursorValid = true;
152621
153354
  this.ready = true;
152622
153355
  this.attempt = 0;
152623
- this.on.hello?.({ resync, newLifetime: !sameLifetime, protocolVersion: frame.protocolVersion, ...frame.build ? { build: frame.build } : {}, ...typeof frame.elevated === "boolean" ? { elevated: frame.elevated } : {}, ...frame.logon ? { logon: frame.logon } : {}, ...frame.handover ? { handover: frame.handover } : {} });
153356
+ this.on.hello?.({ resync, newLifetime: !sameLifetime, protocolVersion: frame.protocolVersion, ...frame.build ? { build: frame.build } : {}, ...typeof frame.elevated === "boolean" ? { elevated: frame.elevated } : {}, ...frame.logon ? { logon: frame.logon } : {}, ...frame.handover ? { handover: frame.handover } : {}, ...frame.updateContinued ? { updateContinued: frame.updateContinued } : {} });
152624
153357
  return;
152625
153358
  }
152626
153359
  if (frame.kind === "res") {
@@ -152916,6 +153649,11 @@ var LinkedMachineConnection = class {
152916
153649
  logon = null;
152917
153650
  /** A sign-in's serve waiting to take over from the remote host, as its hello or `host_handover` last said (#1224) */
152918
153651
  handover = null;
153652
+ /**
153653
+ * The turns the remote host said in its last hello it continues after an update cut them, with when its start took each
153654
+ * up, on its clock (#1618); null: a host from before that, which continues none
153655
+ */
153656
+ updateContinued = null;
152919
153657
  /** The floor could not be read from this host once already; said once in the log */
152920
153658
  floorUnreadable = false;
152921
153659
  /**
@@ -153060,6 +153798,14 @@ var LinkedMachineConnection = class {
153060
153798
  /** The link has just connected, its hello and lists read; `LinkedMachine` looks at the versions then (#1508) */
153061
153799
  connectedNow() {
153062
153800
  }
153801
+ /** Whether the remote host continues the turns an update's stop cuts (#1618): its last hello said which it continues */
153802
+ continuesCutTurns() {
153803
+ return this.updateContinued !== null;
153804
+ }
153805
+ /** When the remote host took up this session's turn (its own id) after an update cut it (#1618); null when it did not */
153806
+ continuedAfterUpdate(sessionId) {
153807
+ return this.updateContinued?.get(sessionId) ?? null;
153808
+ }
153063
153809
  info() {
153064
153810
  return {
153065
153811
  id: this.record.id,
@@ -153565,6 +154311,7 @@ var LinkedMachineConnection = class {
153565
154311
  }
153566
154312
  this.logon = logon2;
153567
154313
  this.handover = h.handover ?? null;
154314
+ this.updateContinued = h.updateContinued ? new Map(h.updateContinued.map((c) => [c.sessionId, c.at])) : null;
153568
154315
  this.switched = null;
153569
154316
  this.switchTried = false;
153570
154317
  this.switchFailure = null;
@@ -153686,8 +154433,8 @@ function parseStopLine(stdout) {
153686
154433
  }
153687
154434
  return null;
153688
154435
  }
153689
- async function stopRemoteHost(c) {
153690
- const r = await c.exec(stopCommand(c.spec));
154436
+ async function stopRemoteHost(c, o) {
154437
+ const r = await c.exec(stopCommand(c.spec, o));
153691
154438
  if (r.stdout.includes(NOT_FOUND2)) return { wasRunning: false };
153692
154439
  const answer = parseStopLine(r.stdout);
153693
154440
  if (!answer) {
@@ -153739,7 +154486,7 @@ var restoredNote = (version2, target) => `The store on ${target} was put back as
153739
154486
  async function startOrRestore(c, to, from) {
153740
154487
  const why = await startAndCheck(c, to.version);
153741
154488
  if (!why || !isLocalBuildVersion(from?.version) || isLocalBuildVersion(to.version)) return { why };
153742
- await stopRemoteHost(c).catch(() => void 0);
154489
+ await stopRemoteHost(c, { forUpdate: true }).catch(() => void 0);
153743
154490
  let restored = false;
153744
154491
  try {
153745
154492
  restored = await restoreStore(c, to);
@@ -153757,7 +154504,7 @@ async function putBack(c, to, why, was, restore = false) {
153757
154504
  const log = tail ? ` The end of its host.log: ${tail}` : "";
153758
154505
  const back = was.current ? `Centralu ${was.current.version}` : "the Centralu that was there";
153759
154506
  try {
153760
- await stopRemoteHost(c).catch(() => void 0);
154507
+ await stopRemoteHost(c, { forUpdate: true }).catch(() => void 0);
153761
154508
  await setPointers(c, to, was.current, was.previous);
153762
154509
  if (restore && was.current) await restoreStore(c, was.current);
153763
154510
  if (was.running) await c.start();
@@ -153778,7 +154525,7 @@ async function switchRemote(c, next, was, keep, o = {}) {
153778
154525
  const stoppedTurns = await c.beforeStop?.() ?? null;
153779
154526
  await c.hold();
153780
154527
  c.step("stop");
153781
- const { wasRunning } = await stopRemoteHost(c);
154528
+ const { wasRunning } = await stopRemoteHost(c, { forUpdate: true });
153782
154529
  const saveStore = o.saveStore === true && was.current !== null;
153783
154530
  if (saveStore && was.current) {
153784
154531
  c.step("backup");
@@ -153801,7 +154548,7 @@ async function switchRemote(c, next, was, keep, o = {}) {
153801
154548
  async function rollbackRemote(c, there) {
153802
154549
  await c.hold();
153803
154550
  c.step("stop");
153804
- const { wasRunning } = await stopRemoteHost(c);
154551
+ const { wasRunning } = await stopRemoteHost(c, { forUpdate: true });
153805
154552
  c.step("switch");
153806
154553
  await setPointers(c, there.previous, there.previous, null);
153807
154554
  const { why, note } = await startOrRestore(c, there.previous, there.current);
@@ -153827,7 +154574,7 @@ function uninstallCommand(spec) {
153827
154574
  async function uninstallRemote(c) {
153828
154575
  await c.hold();
153829
154576
  c.step("stop");
153830
- const { wasRunning } = await stopRemoteHost(c);
154577
+ const { wasRunning } = await stopRemoteHost(c, { forUpdate: false });
153831
154578
  c.step("autostart");
153832
154579
  await removeAutostart(c);
153833
154580
  c.step("remove");
@@ -153840,7 +154587,6 @@ var linkError2 = (message) => rpcError(message);
153840
154587
  var ACTIVITY_POLL_MS2 = 15e3;
153841
154588
  var ACTIVITY_SETTLE_MS = 3e3;
153842
154589
  var ACTIVITY_UNREAD_RETRIES = 2;
153843
- var OPEN_WORK_WAIT_MS = 30 * 6e4;
153844
154590
  var FOLLOW_RETRY_MS = 10 * 6e4;
153845
154591
  var FOLLOW_TRIES = 2;
153846
154592
  var reasonOf = (signal) => signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason));
@@ -153895,9 +154641,10 @@ var LinkedMachine = class extends LinkedMachineConnection {
153895
154641
  }
153896
154642
  /**
153897
154643
  * `machines.update` (update.ts, plan §10.5): install beside, wait until no turn runs there (`when: 'idle'`, the default, #1399),
153898
- * stop, switch, start, check, or put back. `when: 'now'`: no wait, the turns running are stopped and the answer says so.
153899
- * `unasked`: an update nobody approved, which automatic updates cover (#1508): the hub's own catch-up (`followHub`), or an agent's
153900
- * call that went without a card (`followsHub`). It stops nothing it cannot see and gives up when automatic updates go off
154644
+ * stop, switch, start, check, or put back. `when: 'now'`: no wait, the turns running are stopped, the answer says so, and the
154645
+ * next host continues each of them once (#1618, `stopAtOnce`). `unasked`: an update nobody approved, which automatic updates
154646
+ * cover (#1508): the hub's own catch-up (`followHub`), or an agent's call that went without a card (`followsHub`). It goes at
154647
+ * once too (owner, 2026-10-26, #1618: a machine always busy never updated), and stops no host it cannot ask what runs
153901
154648
  */
153902
154649
  update(opts = {}) {
153903
154650
  return opts.unasked === true ? this.unaskedRun(() => this.updateNow(opts)) : this.updateNow(opts);
@@ -153929,7 +154676,6 @@ var LinkedMachine = class extends LinkedMachineConnection {
153929
154676
  cancelUpdate() {
153930
154677
  const wait = this.idleWait;
153931
154678
  if (!wait) return false;
153932
- if (this.followRun && this.follow === this.followRun) Object.assign(this.follow, { tries: FOLLOW_TRIES, cancelled: true });
153933
154679
  wait.abort(new Error(`The update of ${this.name} was cancelled while it waited for the sessions there to finish; nothing was switched`));
153934
154680
  return true;
153935
154681
  }
@@ -153947,35 +154693,18 @@ var LinkedMachine = class extends LinkedMachineConnection {
153947
154693
  * every answer failed is read again twice, then not waited for either: waiting on what was never seen would never end. Once an
153948
154694
  * answer has come, though, the wait goes on through failed readings (the row says `wait_unread`) and the host is stopped only on
153949
154695
  * a successful reading of none in a turn, since a busy or dropped remote reads as failed too; Cancel or Update now are the ways
153950
- * out. `now`: stops at once, and the answer says how many turns that ended.
153951
- *
153952
- * `unasked` (#1508): an update nobody approved stops nothing it cannot see, so a machine that cannot be told what runs there is
153953
- * not updated by it (the person's Update still can), and it gives up once automatic updates go off or the hub hands over
153954
- * to another build (`LinkDeps.autoUpdate`). Either way it ends before the host is held, so nothing was switched
154696
+ * out. `now`, and every update nobody approved (`unasked`, #1508; at once since #1618): `stopAtOnce`.
153955
154697
  */
153956
154698
  async beforeStop(c, when, unasked = false) {
153957
- if (when === "now") {
153958
- const running2 = turnsRunning(await this.activity());
153959
- if (running2 === 0) return null;
153960
- const note = `Updating stopped Centralu on ${this.name} with ${running2} ${running2 === 1 ? "session" : "sessions"} in a turn; they resume on the new version, and a turn in progress was lost`;
153961
- this.deps.log?.(`[links] ${this.name}: ${note}`);
153962
- return note;
153963
- }
154699
+ if (when === "now" || unasked) return this.stopAtOnce(unasked);
153964
154700
  const wait = new AbortController();
153965
154701
  this.idleWait = wait;
153966
154702
  const poll = this.deps.activityPollMs ?? ACTIVITY_POLL_MS2;
153967
154703
  const settle = Math.min(poll, ACTIVITY_SETTLE_MS);
153968
- const stillOn = () => {
153969
- if (unasked && this.deps.autoUpdate?.() !== true) {
153970
- throw new Error(`Automatic updates went off, or this computer began switching to another build, while the update of ${this.name} waited; nothing was switched`);
153971
- }
153972
- };
153973
154704
  const read = async () => {
153974
154705
  wait.signal.throwIfAborted();
153975
- stillOn();
153976
154706
  const r = await this.readActivity();
153977
154707
  wait.signal.throwIfAborted();
153978
- stillOn();
153979
154708
  return r;
153980
154709
  };
153981
154710
  try {
@@ -153992,32 +154721,17 @@ var LinkedMachine = class extends LinkedMachineConnection {
153992
154721
  const unread = () => show("wait_unread", "what runs there cannot be read now; the update keeps waiting, since the sessions there may be in a turn (Cancel ends it)");
153993
154722
  const busy = (a) => {
153994
154723
  const turns = turnsRunning(a);
153995
- const n = open6(a);
153996
- if (turns > 0) show("wait_idle", `the update waits for ${turns} ${turns === 1 ? "session" : "sessions"} in a turn`, turns);
153997
- else show("wait_open", `the update waits for ${n} open ${n === 1 ? "terminal or project command" : "terminals and project commands"}, which an update this computer starts by itself closes none of`, n);
154724
+ show("wait_idle", `the update waits for ${turns} ${turns === 1 ? "session" : "sessions"} in a turn`, turns);
153998
154725
  };
153999
- const open6 = (a) => unasked ? a.terminals + a.commandRuns : 0;
154000
- const holding = (a) => turnsRunning(a) + open6(a);
154001
- let openSince = null;
154002
- const now = this.deps.now ?? Date.now;
154003
- const openLimit = this.deps.openWorkWaitMs ?? OPEN_WORK_WAIT_MS;
154004
154726
  for (; ; ) {
154005
154727
  const r = await read();
154006
154728
  if (r.activity) {
154007
154729
  seen = true;
154008
154730
  blind = 0;
154009
- if (open6(r.activity) > 0) {
154010
- openSince ??= now();
154011
- if (now() - openSince >= openLimit) {
154012
- throw new Error(
154013
- `Terminals or project commands stayed open on ${this.name}, and an update this computer starts by itself closes none of them; nothing was switched`
154014
- );
154015
- }
154016
- } else openSince = null;
154017
- if (holding(r.activity) === 0) {
154731
+ if (turnsRunning(r.activity) === 0) {
154018
154732
  await pause(settle, wait.signal);
154019
154733
  const again = await read();
154020
- if (again.activity && holding(again.activity) === 0) {
154734
+ if (again.activity && turnsRunning(again.activity) === 0) {
154021
154735
  if (shown3) c.step("stop");
154022
154736
  return null;
154023
154737
  }
@@ -154027,7 +154741,6 @@ var LinkedMachine = class extends LinkedMachineConnection {
154027
154741
  } else if (seen) {
154028
154742
  unread();
154029
154743
  } else if (r.cannot || ++blind > ACTIVITY_UNREAD_RETRIES) {
154030
- if (unasked) throw new Error(this.blindSentence());
154031
154744
  this.deps.log?.(`[links] ${this.name}: it cannot be told what runs there, so the update does not wait for it`);
154032
154745
  return null;
154033
154746
  }
@@ -154038,14 +154751,38 @@ var LinkedMachine = class extends LinkedMachineConnection {
154038
154751
  }
154039
154752
  }
154040
154753
  /**
154041
- * Before an update nobody approved installs anything (#1508 review): what runs there must be readable. A failed reading is
154042
- * tried again twice, a poll apart, as the wait does (`ACTIVITY_UNREAD_RETRIES`); a host that cannot be asked at all is not
154754
+ * An update that does not wait (#1618): the person's `when: 'now'`, and every update nobody approved (owner, 2026-10-26: the
154755
+ * hub's own catch-up used to wait for the turns there, and for its terminals and project commands, and a machine always busy
154756
+ * never updated). It reads what runs there once, for the answer's sentence, and the host is stopped: the turns running end,
154757
+ * and terminals and project commands close. Which turns that stop ends the stopping host lists itself, as it stops
154758
+ * (`host.stop { forUpdate }`, update.ts `stopRemoteHost`), and the host the update starts continues each of them once.
154759
+ *
154760
+ * An update nobody approved stops no host it cannot ask what runs (#1508): a reading that fails is tried again as
154761
+ * `probeActivity` does, then the update fails before the hold, nothing switched. The person's `now` goes on unread
154762
+ */
154763
+ async stopAtOnce(unasked) {
154764
+ const a = unasked ? await this.probeActivity() : await this.activity();
154765
+ const running2 = turnsRunning(a);
154766
+ const open6 = a ? a.terminals + a.commandRuns : 0;
154767
+ const goesOn = this.continuesCutTurns();
154768
+ const after = goesOn ? "Centralu continues each of those turns on the new version" : "they resume on the new version, and a turn in progress was lost";
154769
+ if (unasked) this.deps.log?.(`[links] ${this.name}: updating it now, since automatic updates are on${running2 ? `; ${running2} ${running2 === 1 ? "session" : "sessions"} in a turn there: ${after}` : ""}`);
154770
+ if (running2 === 0) return null;
154771
+ const closed = open6 ? `, and ${open6} open ${open6 === 1 ? "terminal or project command" : "terminals and project commands"} there closed` : "";
154772
+ const note = `Updating stopped Centralu on ${this.name} with ${running2} ${running2 === 1 ? "session" : "sessions"} in a turn; ${after}${closed}`;
154773
+ this.deps.log?.(`[links] ${this.name}: ${note}`);
154774
+ return note;
154775
+ }
154776
+ /**
154777
+ * Before an update nobody approved installs anything (#1508 review), and again right before it stops the host (#1618): what
154778
+ * runs there must be readable. A failed reading is tried again twice, a poll apart, as the wait does
154779
+ * (`ACTIVITY_UNREAD_RETRIES`); a host that cannot be asked at all is not
154043
154780
  */
154044
154781
  async probeActivity() {
154045
154782
  const poll = this.deps.activityPollMs ?? ACTIVITY_POLL_MS2;
154046
154783
  for (let i = 0; ; i++) {
154047
154784
  const r = await this.readActivity();
154048
- if (r.activity) return;
154785
+ if (r.activity) return r.activity;
154049
154786
  if (r.cannot || i >= ACTIVITY_UNREAD_RETRIES) throw linkError2(this.blindSentence());
154050
154787
  await pause(poll, new AbortController().signal);
154051
154788
  }
@@ -154060,8 +154797,6 @@ var LinkedMachine = class extends LinkedMachineConnection {
154060
154797
  * failed, and the retry's timer. `due`: the retry's time has come, and it runs at the next chance (connected, not busy)
154061
154798
  */
154062
154799
  follow = null;
154063
- /** The hub's own update that runs now, if one does */
154064
- followRun = null;
154065
154800
  /**
154066
154801
  * Whether an idle update of this machine is the hub's catch-up that needs no one's approval (#1508): the hub's automatic
154067
154802
  * updates are on (`LinkDeps.autoUpdate`), this hub can update it (`operationRefusal`), the person did not remove what the
@@ -154076,7 +154811,7 @@ var LinkedMachine = class extends LinkedMachineConnection {
154076
154811
  const hub = this.deps.hub.version;
154077
154812
  if (!v || v.hub.version !== hub || isLocalBuildVersion(hub) || isLocalBuildVersion(v.remote.version)) return false;
154078
154813
  const f = this.follow;
154079
- if (f?.version === hub && (f.cancelled === true || f.tries >= FOLLOW_TRIES)) return false;
154814
+ if (f?.version === hub && f.tries >= FOLLOW_TRIES) return false;
154080
154815
  return v.older === "remote";
154081
154816
  }
154082
154817
  /** At every connect: a machine found on an older release follows the hub (#1508) */
@@ -154084,9 +154819,10 @@ var LinkedMachine = class extends LinkedMachineConnection {
154084
154819
  this.followHub();
154085
154820
  }
154086
154821
  /**
154087
- * Starts the hub's own idle update of this machine (#1508) when it follows the hub (`followsHub`), the link is up and
154088
- * nothing else runs there: once per version of the hub's, and once more `FOLLOW_RETRY_MS` after a failure. Sessions in a
154089
- * turn there are waited for (`machines.update { when: 'idle' }`, #1399); the steps show on the row as the person's do
154822
+ * Starts the hub's own update of this machine (#1508) when it follows the hub (`followsHub`), the link is up and nothing
154823
+ * else runs there: once per version of the hub's, and once more `FOLLOW_RETRY_MS` after a failure. It does not wait for
154824
+ * what runs there (#1618, `stopAtOnce`): the turns it ends continue on the new version, and terminals and project
154825
+ * commands close. The steps show on the row as the person's do
154090
154826
  */
154091
154827
  followHub() {
154092
154828
  if (this.stopped || this.busy || this.status !== "connected" || !this.followsHub()) return;
@@ -154097,13 +154833,13 @@ var LinkedMachine = class extends LinkedMachineConnection {
154097
154833
  this.follow = { version: version2, tries: 0, failed: null, retryAt: null, due: false };
154098
154834
  }
154099
154835
  this.deps.log?.(
154100
- `[links] ${this.name}: runs Centralu ${this.versions?.remote.version ?? "an older version"}, older than this computer's ${version2}; updating it once no session there is in a turn, since automatic updates are on`
154836
+ `[links] ${this.name}: runs Centralu ${this.versions?.remote.version ?? "an older version"}, older than this computer's ${version2}; updating it, since automatic updates are on`
154101
154837
  );
154102
- void this.update({ when: "idle", unasked: true }).catch(() => void 0);
154838
+ void this.update({ when: "now", unasked: true }).catch(() => void 0);
154103
154839
  }
154104
154840
  /**
154105
154841
  * Runs an update nobody approved as one try for this hub's version (#1508 review): the hub's own catch-up and an agent's
154106
- * card-less `machines.update` or `install_machine` alike, so a cancel or two failures spend the version for both
154842
+ * card-less `machines.update` or `install_machine` alike, so two failures spend the version for both
154107
154843
  * (`followsHub`). A failure shows on the row and, with a try left, is tried once more `FOLLOW_RETRY_MS` later. A call refused
154108
154844
  * because something else runs there spends nothing
154109
154845
  */
@@ -154120,7 +154856,6 @@ var LinkedMachine = class extends LinkedMachineConnection {
154120
154856
  f.tries += 1;
154121
154857
  f.due = false;
154122
154858
  f.retryAt = null;
154123
- this.followRun = f;
154124
154859
  try {
154125
154860
  const r = await run4();
154126
154861
  if (this.follow === f) {
@@ -154130,8 +154865,8 @@ var LinkedMachine = class extends LinkedMachineConnection {
154130
154865
  return r;
154131
154866
  } catch (err) {
154132
154867
  if (this.follow === f && !this.stopped) {
154133
- f.failed = f.cancelled ? null : errorMessage(err);
154134
- if (!f.cancelled && f.tries < FOLLOW_TRIES) {
154868
+ f.failed = errorMessage(err);
154869
+ if (f.tries < FOLLOW_TRIES) {
154135
154870
  const ms3 = this.deps.followRetryMs ?? FOLLOW_RETRY_MS;
154136
154871
  f.retryAt = Date.now() + ms3;
154137
154872
  f.timer = setTimeout(() => {
@@ -154144,8 +154879,6 @@ var LinkedMachine = class extends LinkedMachineConnection {
154144
154879
  this.deps.broadcast({ type: "machine_status", machine: this.info() });
154145
154880
  }
154146
154881
  throw err;
154147
- } finally {
154148
- if (this.followRun === f) this.followRun = null;
154149
154882
  }
154150
154883
  }
154151
154884
  info() {
@@ -154479,7 +155212,7 @@ function probeTarget(target, opts = {}) {
154479
155212
  }
154480
155213
 
154481
155214
  // packages/agent-host/src/links/ssh-config.ts
154482
- import { readdirSync as readdirSync15, readFileSync as readFileSync24, statSync as statSync15 } from "node:fs";
155215
+ import { readdirSync as readdirSync16, readFileSync as readFileSync25, statSync as statSync15 } from "node:fs";
154483
155216
  import { homedir as homedir16 } from "node:os";
154484
155217
  import { isAbsolute as isAbsolute11, join as join57, normalize, parse as parse5, sep as sep11 } from "node:path";
154485
155218
  var MAX_DEPTH = 16;
@@ -154552,7 +155285,7 @@ function expandGlob(path2) {
154552
155285
  }
154553
155286
  let names;
154554
155287
  try {
154555
- names = readdirSync15(base2);
155288
+ names = readdirSync16(base2);
154556
155289
  } catch {
154557
155290
  continue;
154558
155291
  }
@@ -154589,7 +155322,7 @@ function readSshConfig(opts) {
154589
155322
  }
154590
155323
  let text2;
154591
155324
  try {
154592
- text2 = readFileSync24(file2, "utf8");
155325
+ text2 = readFileSync25(file2, "utf8");
154593
155326
  } catch (err) {
154594
155327
  if (depth > 0 && err.code !== "ENOENT") notes.push(`${tildePath(file2, opts.home)} could not be read`);
154595
155328
  return;
@@ -155294,6 +156027,13 @@ function linkedSessions(machines, route = notRouted, hubName2, options = {}) {
155294
156027
  machineWaitsForPerson(machineId) {
155295
156028
  return machineById(machineId)?.waitsForPerson === true;
155296
156029
  },
156030
+ continuesCutTurns(machineId) {
156031
+ return machineById(machineId)?.continuesCutTurns?.() === true;
156032
+ },
156033
+ continuedAfterUpdate(sessionId) {
156034
+ const at2 = splitQualified(sessionId);
156035
+ return (at2 && machineById(at2.machine)?.continuedAfterUpdate?.(at2.id)) ?? null;
156036
+ },
155297
156037
  async backgroundTasks(machineId) {
155298
156038
  const m = machineById(machineId);
155299
156039
  if (!m || !m.reachable) return null;
@@ -156665,7 +157405,7 @@ var handedToken = launch.tokenOnStdin ? await readHandedToken(process.stdin).cat
156665
157405
  console.error(`[agent-host] the launcher did not hand over the token: ${e.message}`);
156666
157406
  process.exit(1);
156667
157407
  }) : launch.token;
156668
- var BUILD = true ? "137a245" : "dev";
157408
+ var BUILD = true ? "6e6bc62" : "dev";
156669
157409
  var { values } = parseArgs({
156670
157410
  options: {
156671
157411
  port: { type: "string", default: "5175" },
@@ -156744,6 +157484,7 @@ if (!lock.ok) {
156744
157484
  var ownership = lock;
156745
157485
  process.on("exit", ownership.release);
156746
157486
  if (dbPath !== ":memory:") clearStoppedMark(dirname26(dbPath));
157487
+ var updateResume = dbPath === ":memory:" ? void 0 : updateResumeFolder(dirname26(dbPath));
156747
157488
  start.at("hold signals");
156748
157489
  var signals = holdSignals(process, (line) => console.error(line));
156749
157490
  start.at("store");
@@ -156778,7 +157519,7 @@ var mgr = new SessionManager(
156778
157519
  (sessionId) => bridgeAddress(launch.frontDoor, port, token, sessionId),
156779
157520
  // Worktrees are created next to the data folder — dev and the packaged app never touch each other's worktrees
156780
157521
  join58(dirname26(dbPath), "worktrees"),
156781
- held2 ? { processes: held2.processes, keptSessions: held2.kept.sessionIds } : {}
157522
+ { ...held2 ? { processes: held2.processes, keptSessions: held2.kept.sessionIds } : {}, ...updateResume ? { updateResume } : {} }
156782
157523
  );
156783
157524
  mgr.useProjectsHome(() => projectsHome());
156784
157525
  mgr.useMemoryGate(new MemoryGate());
@@ -157005,8 +157746,9 @@ if (launch.underKeeper) {
157005
157746
  }
157006
157747
  var stopNow = null;
157007
157748
  var stopAsked = false;
157008
- var stopFromRequest = () => {
157009
- console.error("[agent-host] asked to stop (centralu serve --stop)");
157749
+ var stopFromRequest = (o) => {
157750
+ console.error(`[agent-host] asked to stop (centralu serve --stop${o.forUpdate ? ", for an update" : ""})`);
157751
+ if (o.forUpdate) mgr.stopForUpdate();
157010
157752
  if (stopNow) stopNow();
157011
157753
  else stopAsked = true;
157012
157754
  };
@@ -157081,7 +157823,7 @@ var router = new Router({
157081
157823
  inlineViews,
157082
157824
  heldPids: held2?.heldPids,
157083
157825
  machines: links,
157084
- stopHost: launch.startedByServe && !launch.underKeeper ? () => stopFromRequest() : void 0,
157826
+ stopHost: launch.startedByServe && !launch.underKeeper ? (o) => stopFromRequest(o) : void 0,
157085
157827
  // `host.stop {ifIdle}`: the check and the refusal of new calls in one step (#1224, lifecycle/stop-gate.ts)
157086
157828
  stopGate: launch.startedByServe && !launch.underKeeper ? createStopGate({ drain: hostDrain, activity: () => activityCounts(activity()) }) : void 0,
157087
157829
  // A sign-in's serve says it waits to take over, or gave up (#1224): kept for every hello, and told to who is connected
@@ -157139,6 +157881,8 @@ var server = new HostServer({
157139
157881
  // Which Windows logon it runs in, for a hub linked to it (#1224); asked with the elevation
157140
157882
  logon: () => logon,
157141
157883
  handover: () => handover,
157884
+ // The turns this host continued after an update cut them, for a linked hub's callers (#1618)
157885
+ updateContinued: () => mgr.updateContinued(),
157142
157886
  // A planned swap waits for running RPCs, within a bound (#280 step 3, drain.ts)
157143
157887
  drain: hostDrain
157144
157888
  });